视频复刻添加描述框
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user