diff --git a/apps/web/src/pages/ai-avatar/components/ModalBRollEditor.tsx b/apps/web/src/pages/ai-avatar/components/ModalBRollEditor.tsx index fbda50381..2b1aef327 100644 --- a/apps/web/src/pages/ai-avatar/components/ModalBRollEditor.tsx +++ b/apps/web/src/pages/ai-avatar/components/ModalBRollEditor.tsx @@ -65,10 +65,10 @@ const ModalBRollEditor: React.FC = ({ const [pipPosition, setPipPosition] = useState("top-right") const [pipScale, setPipScale] = useState(0.3) - /** 文案分句(使用后端精确时间戳) */ + /** 文案分句(优先使用后端精确时间戳,降级为字数比例估算) */ const sentences = useMemo( - () => splitScriptIntoSentences(scriptText, sentenceTimings), - [scriptText, sentenceTimings], + () => splitScriptIntoSentences(scriptText, sentenceTimings, _outputDuration), + [scriptText, sentenceTimings, _outputDuration], ) /** 已被现有 segments 占用的素材 id 集合(标灰、禁止重复选择) */ @@ -145,7 +145,7 @@ const ModalBRollEditor: React.FC = ({ setSelectedAsset(asset) } - /** 确认添加一段 B-roll(⑥ 时间取所选句子的估算起止) */ + /** 确认添加一段 B-roll(⑥ 时间取所选句子的精确起止,后端静音检测 / 前端字数比例降级) */ const handleConfirm = () => { if (!selectedAsset || !selectedSentence) return const startTime = selectedSentence.startTime @@ -267,11 +267,9 @@ const ModalBRollEditor: React.FC = ({ > {sent.index + 1} {sent.text} - {sent.endTime > 0 && ( - - {sent.startTime.toFixed(1)}-{sent.endTime.toFixed(1)}s - - )} + + {sent.startTime.toFixed(1)}-{sent.endTime.toFixed(1)}s + ) })} diff --git a/apps/web/src/pages/ai-avatar/components/PanelCoverAndGenerate.tsx b/apps/web/src/pages/ai-avatar/components/PanelCoverAndGenerate.tsx index 2c7d48893..ee5720f00 100644 --- a/apps/web/src/pages/ai-avatar/components/PanelCoverAndGenerate.tsx +++ b/apps/web/src/pages/ai-avatar/components/PanelCoverAndGenerate.tsx @@ -120,7 +120,7 @@ const PanelCoverAndGenerate: React.FC = ({ wordBreak: "break-word", whiteSpace: "pre-wrap", color: titleConfig.color || "#ffffff", - fontSize: `${(titleConfig.size || 28) * 0.55}px`, // 预览容器缩放,与 PanelLipsyncPreview 对齐 + fontSize: `${(titleConfig.size || 48) * 0.35}px`, // 预览容器缩放,与 PanelLipsyncPreview 对齐 fontFamily: getFontFamily(titleConfig.font), fontWeight: titleConfig.bold ? "bold" : "normal", fontStyle: titleConfig.italic ? "italic" : "normal", diff --git a/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx b/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx index f78c951aa..a222370eb 100755 --- a/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx +++ b/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx @@ -58,7 +58,7 @@ export function PanelLipsyncPreview({ position: "absolute", color: titleConfig.color || "#ffffff", fontFamily: titleConfig.font || "思源黑体", - fontSize: `${(titleConfig.size || 28) * 0.55}px`, + fontSize: `${(titleConfig.size || 48) * 0.35}px`, fontWeight: titleConfig.bold ? 700 : 400, fontStyle: titleConfig.italic ? "italic" : "normal", textAlign: "center", diff --git a/apps/web/src/pages/ai-avatar/types.ts b/apps/web/src/pages/ai-avatar/types.ts index d149a3d3c..f12f1aa50 100644 --- a/apps/web/src/pages/ai-avatar/types.ts +++ b/apps/web/src/pages/ai-avatar/types.ts @@ -122,7 +122,7 @@ export const DEFAULT_TITLE_CONFIG: AiAvatarTitleConfig = { title: "", position: "bottom", font: "思源黑体", - size: 28, + size: 48, bold: true, italic: false, stroke: false, diff --git a/apps/web/src/pages/ai-avatar/utils/contract.ts b/apps/web/src/pages/ai-avatar/utils/contract.ts index a8ce5b78d..f617262b9 100644 --- a/apps/web/src/pages/ai-avatar/utils/contract.ts +++ b/apps/web/src/pages/ai-avatar/utils/contract.ts @@ -39,7 +39,7 @@ export function buildTitleConfigPayload(cfg: AiAvatarTitleConfig): Record sum + p.replace(/\s/g, "").length, 0) + // 如果后端返回了完整的时间戳(至少有一个有效结束时间),使用精确时间 + const hasBackendTimings = + sentenceTimings && + sentenceTimings.length > 0 && + sentenceTimings.some((t) => (t.end_time ?? 0) > 0) + + // 若有后端时间戳,取音频总时长;否则用外部传入的 outputDuration 做字数比例降级 + const audioDuration = hasBackendTimings + ? (sentenceTimings![sentenceTimings!.length - 1]?.end_time ?? 0) + : outputDuration + rawParts.forEach((part, i) => { const charCount = part.replace(/\s/g, "").length - // 从后端精确时间戳获取;无数据时返回 0 - const timing = sentenceTimings?.[i] - const startTime = timing?.start_time ?? 0 - const endTime = timing?.end_time ?? 0 - - sentences.push({ - index: i, - text: part, - charCount, - startChar: accChar, - startTime, - endTime, - }) + if (hasBackendTimings) { + // 使用后端精确时间戳 + const timing = sentenceTimings![i] + sentences.push({ + index: i, + text: part, + charCount, + startChar: accChar, + startTime: timing?.start_time ?? 0, + endTime: timing?.end_time ?? 0, + }) + } else { + // 降级方案:按字数比例估算(必须保证有时间,否则B-roll无法定位) + const startTime = totalChars > 0 ? (accChar / totalChars) * (audioDuration || 0) : 0 + const endTime = + totalChars > 0 ? ((accChar + charCount) / totalChars) * (audioDuration || 0) : 0 + sentences.push({ + index: i, + text: part, + charCount, + startChar: accChar, + startTime: Math.round(startTime * 100) / 100, + endTime: Math.round(endTime * 100) / 100, + }) + } accChar += charCount }) diff --git a/packages/domain/video_filter_builder.py b/packages/domain/video_filter_builder.py index 6b3062cd5..5a0ddd49f 100755 --- a/packages/domain/video_filter_builder.py +++ b/packages/domain/video_filter_builder.py @@ -481,7 +481,7 @@ def build_title_drawtext_filter( # ── 样式参数 ── font_name = title_config.get("font") or title_config.get("font_preset") or "思源黑体" - font_size = int(title_config.get("font_size") or title_config.get("size") or 28) + font_size = int(title_config.get("font_size") or title_config.get("size") or 48) font_color = title_config.get("font_color") or title_config.get("color") or "#ffffff" # 去掉 # 前缀(drawtext 用纯 hex 或颜色名) if font_color.startswith("#"):