fix(ai-avatar): B-roll文案显示时长+标题字号调大 #1870
@@ -65,10 +65,10 @@ const ModalBRollEditor: React.FC<ModalBRollEditorProps> = ({
|
||||
const [pipPosition, setPipPosition] = useState<PipPosition>("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<ModalBRollEditorProps> = ({
|
||||
setSelectedAsset(asset)
|
||||
}
|
||||
|
||||
/** 确认添加一段 B-roll(⑥ 时间取所选句子的估算起止) */
|
||||
/** 确认添加一段 B-roll(⑥ 时间取所选句子的精确起止,后端静音检测 / 前端字数比例降级) */
|
||||
const handleConfirm = () => {
|
||||
if (!selectedAsset || !selectedSentence) return
|
||||
const startTime = selectedSentence.startTime
|
||||
@@ -267,11 +267,9 @@ const ModalBRollEditor: React.FC<ModalBRollEditorProps> = ({
|
||||
>
|
||||
<span className="aa-sentence-item__idx">{sent.index + 1}</span>
|
||||
<span className="aa-sentence-item__text">{sent.text}</span>
|
||||
{sent.endTime > 0 && (
|
||||
<span className="aa-sentence-item__time">
|
||||
{sent.startTime.toFixed(1)}-{sent.endTime.toFixed(1)}s
|
||||
</span>
|
||||
)}
|
||||
<span className="aa-sentence-item__time">
|
||||
{sent.startTime.toFixed(1)}-{sent.endTime.toFixed(1)}s
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -120,7 +120,7 @@ const PanelCoverAndGenerate: React.FC<PanelCoverAndGenerateProps> = ({
|
||||
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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -122,7 +122,7 @@ export const DEFAULT_TITLE_CONFIG: AiAvatarTitleConfig = {
|
||||
title: "",
|
||||
position: "bottom",
|
||||
font: "思源黑体",
|
||||
size: 28,
|
||||
size: 48,
|
||||
bold: true,
|
||||
italic: false,
|
||||
stroke: false,
|
||||
|
||||
@@ -39,7 +39,7 @@ export function buildTitleConfigPayload(cfg: AiAvatarTitleConfig): Record<string
|
||||
text,
|
||||
enabled: true,
|
||||
font: cfg.font || "思源黑体",
|
||||
font_size: Math.round(cfg.size) || 28,
|
||||
font_size: Math.round(cfg.size) || 48,
|
||||
font_color: cfg.color || "#ffffff",
|
||||
position,
|
||||
bold: !!cfg.bold,
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ScriptSentence {
|
||||
export function splitScriptIntoSentences(
|
||||
scriptText: string,
|
||||
sentenceTimings?: SentenceTiming[] | null,
|
||||
outputDuration: number = 0,
|
||||
): ScriptSentence[] {
|
||||
const text = (scriptText || "").trim()
|
||||
if (!text) return []
|
||||
@@ -39,21 +40,45 @@ export function splitScriptIntoSentences(
|
||||
|
||||
const sentences: ScriptSentence[] = []
|
||||
let accChar = 0
|
||||
const totalChars = rawParts.reduce((sum, p) => 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
|
||||
})
|
||||
|
||||
|
||||
@@ -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("#"):
|
||||
|
||||
Reference in New Issue
Block a user