|
|
|
@@ -43,6 +43,8 @@ const PRODUCT_SOURCE_PURPOSE = "video_replace_product";
|
|
|
|
|
const REF_SECONDS_MAX = { product: 30, character: 30 } as const;
|
|
|
|
|
const REF_BYTES_MAX = { product: 200 * 1024 * 1024, character: 200 * 1024 * 1024 } as const;
|
|
|
|
|
const SEEDANCE_MAX_OUTPUT_SECONDS = 30;
|
|
|
|
|
// 第 3 步补充信息上限,与后端 MAX_SUBJECT_BRIEF 保持一致
|
|
|
|
|
const SUBJECT_BRIEF_MAX = 500;
|
|
|
|
|
|
|
|
|
|
type ProductSource = "library" | "temporary" | "";
|
|
|
|
|
type ReplaceMode = "product" | "character";
|
|
|
|
@@ -74,6 +76,10 @@ const REPLACE_MODE_COPY = {
|
|
|
|
|
drawerEmpty: "还没有商品,先去商品库创建一个",
|
|
|
|
|
historyKind: "商品",
|
|
|
|
|
pickToast: "已选择商品",
|
|
|
|
|
briefStep: "3. 商品信息",
|
|
|
|
|
briefOptional: "选填 · 填了更准",
|
|
|
|
|
briefPlaceholder: "例:素颜霜,膏体按压泵,主打提亮和保湿,适合日常通勤",
|
|
|
|
|
briefHint: "填写商品名称、品类和卖点后,口播和使用动作会按真实用途来写;不填则只按参考图的外观拍。",
|
|
|
|
|
},
|
|
|
|
|
character: {
|
|
|
|
|
modeLabel: "角色复刻",
|
|
|
|
@@ -101,6 +107,10 @@ const REPLACE_MODE_COPY = {
|
|
|
|
|
drawerEmpty: "还没有人物,先去模特库添加",
|
|
|
|
|
historyKind: "角色",
|
|
|
|
|
pickToast: "已选择角色",
|
|
|
|
|
briefStep: "3. 角色信息",
|
|
|
|
|
briefOptional: "选填 · 建议写明性别",
|
|
|
|
|
briefPlaceholder: "例:男性,30 岁左右,中低音域,语速平稳,声音干净有亲和力",
|
|
|
|
|
briefHint: "配音会按这里的设定重新生成声线,建议写明男 / 女,避免沿用原视频人物的音色。",
|
|
|
|
|
},
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
@@ -284,6 +294,7 @@ interface ModeFormState {
|
|
|
|
|
tempFiles: File[];
|
|
|
|
|
tempAssetRefs: FreeVideoRef[];
|
|
|
|
|
filledSubjectName: string;
|
|
|
|
|
subjectBrief: string;
|
|
|
|
|
job: FreeVideoTask | null;
|
|
|
|
|
jobId: string;
|
|
|
|
|
}
|
|
|
|
@@ -300,6 +311,7 @@ const createInitialModeState = (): ModeFormState => ({
|
|
|
|
|
tempFiles: [],
|
|
|
|
|
tempAssetRefs: [],
|
|
|
|
|
filledSubjectName: "",
|
|
|
|
|
subjectBrief: "",
|
|
|
|
|
job: null,
|
|
|
|
|
jobId: "",
|
|
|
|
|
});
|
|
|
|
@@ -357,6 +369,7 @@ export function VideoReplacePage({
|
|
|
|
|
tempFiles,
|
|
|
|
|
tempAssetRefs,
|
|
|
|
|
filledSubjectName,
|
|
|
|
|
subjectBrief,
|
|
|
|
|
job,
|
|
|
|
|
jobId,
|
|
|
|
|
} = currentForm;
|
|
|
|
@@ -824,6 +837,7 @@ export function VideoReplacePage({
|
|
|
|
|
product_id: current.source === "library" && replaceMode === "product" ? current.selectedProduct?.id : undefined,
|
|
|
|
|
model_id: current.source === "library" && replaceMode === "character" ? current.selectedModel?.id : undefined,
|
|
|
|
|
image_asset_ids: current.source === "temporary" ? imageAssetIds : undefined,
|
|
|
|
|
subject_brief: current.subjectBrief.trim() || undefined,
|
|
|
|
|
model: preferredModel.name,
|
|
|
|
|
aspect_ratio: aspectRatio,
|
|
|
|
|
resolution: "720p",
|
|
|
|
@@ -902,6 +916,7 @@ export function VideoReplacePage({
|
|
|
|
|
...sizeFromRatio(task.aspect_ratio || "9:16"),
|
|
|
|
|
},
|
|
|
|
|
filledSubjectName: subject,
|
|
|
|
|
subjectBrief: task.subject_brief || "",
|
|
|
|
|
selectedModel: model || null,
|
|
|
|
|
selectedProduct: null,
|
|
|
|
|
source: model ? "library" : (images.length ? "temporary" : ""),
|
|
|
|
@@ -926,6 +941,7 @@ export function VideoReplacePage({
|
|
|
|
|
...sizeFromRatio(task.aspect_ratio || "9:16"),
|
|
|
|
|
},
|
|
|
|
|
filledSubjectName: subject,
|
|
|
|
|
subjectBrief: task.subject_brief || "",
|
|
|
|
|
selectedProduct: product || null,
|
|
|
|
|
selectedModel: null,
|
|
|
|
|
source: product ? "library" : (images.length ? "temporary" : ""),
|
|
|
|
@@ -1224,6 +1240,23 @@ export function VideoReplacePage({
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="video-flow-step">
|
|
|
|
|
<div className="video-flow-step-head">
|
|
|
|
|
<strong>{copy.briefStep}</strong>
|
|
|
|
|
<span>{copy.briefOptional}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<textarea
|
|
|
|
|
className="textarea replace-brief-input"
|
|
|
|
|
value={subjectBrief}
|
|
|
|
|
maxLength={SUBJECT_BRIEF_MAX}
|
|
|
|
|
placeholder={copy.briefPlaceholder}
|
|
|
|
|
disabled={generating}
|
|
|
|
|
aria-label={copy.briefStep}
|
|
|
|
|
onChange={(event) => updateCurrentForm({ subjectBrief: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
<p className="field-hint replace-brief-hint">{copy.briefHint}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="video-flow-actions replace-generate-action">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|