测试视频复刻
This commit is contained in:
@@ -34,26 +34,6 @@ const KIND_LABEL: Record<string, string> = { product: "商品", person: "角色"
|
||||
// 脚本来源 → 「来源」brief pill 文案
|
||||
// 入口收敛为「脚本辅助生成 / 上传脚本」两种。theme 键保留:历史稿的 source 仍可能是旧的「一句话主题」。
|
||||
const SOURCE_LABEL: Record<string, string> = { ai: "脚本辅助生成", theme: "脚本辅助生成", manual: "上传脚本", video: "上传视频提炼" };
|
||||
type DirectorBeat = { time: string; direction: string; narration: string };
|
||||
const DIRECTOR_BEAT_RE = /(?:^|\n)\s*(\d{1,2})\s*[-–—~到至]\s*(\d{1,2})\s*(?:s|秒)?\s*[::]*\s*([^\n]+)/g;
|
||||
function buildDirectorBeats(visual: string, narration: string, duration: number): DirectorBeat[] {
|
||||
const matches = [...(visual || "").matchAll(DIRECTOR_BEAT_RE)];
|
||||
const narrationLines = (narration || "").split(/(?<=[。!?!?])/).map((line) => line.trim()).filter(Boolean);
|
||||
if (matches.length) {
|
||||
return matches.slice(0, 5).map((match, index) => ({
|
||||
time: `${match[1]}–${match[2]}s`,
|
||||
direction: match[3].trim(),
|
||||
narration: narrationLines[index] || (index === 0 ? narration.trim() : ""),
|
||||
}));
|
||||
}
|
||||
const count = Math.min(3, Math.max(1, narrationLines.length || 1));
|
||||
const step = duration / count;
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
time: `${Math.round(index * step)}–${Math.round((index + 1) * step)}s`,
|
||||
direction: visual.trim() || "按本镜脚本完成关键动作与商品露出",
|
||||
narration: narrationLines[index] || (index === 0 ? narration.trim() : ""),
|
||||
}));
|
||||
}
|
||||
// 旁白配音音色预设(语音合成经典版试用包实测可用,与后端 VOICEOVER_VOICES 对齐)
|
||||
const VO_VOICES = [
|
||||
{ key: "BV700_streaming", label: "灿灿 · 活力女声" },
|
||||
@@ -2070,7 +2050,7 @@ export function PipelinePage(props: {
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
}, [gutterDragging]);
|
||||
const SB_PROMPT_DEFAULT = "统一商品、人物、场景风格,生成干净、可直接指导视频的关键帧;导演分镜说明按脚本自动生成";
|
||||
const SB_PROMPT_DEFAULT = "统一商品、人物、场景风格,生成完整、可直接指导视频的导演故事板";
|
||||
// 整张风格提示词:项目级(原 StoryboardVersion.prompt 的去处),重跑时生效
|
||||
const sbSavedPrompt = (project.metadata as Record<string, unknown> | undefined)?.storyboard_prompt as string | undefined;
|
||||
const [storyboardPrompt, setStoryboardPrompt] = useState(sbSavedPrompt || SB_PROMPT_DEFAULT);
|
||||
@@ -3871,13 +3851,6 @@ export function PipelinePage(props: {
|
||||
const sbViewedIsAdopted = !sbViewedVer || sbViewedVer.is_adopted || sbViewedVer.id === sbActiveShot?.adopted_version;
|
||||
const mainUrl = sbViewedVer ? (sbViewedVer.asset_url || assetUrl(sbViewedVer.asset)) : shotImg(sbActiveShot);
|
||||
const mainAid = sbViewedVer?.asset || sbActiveShot?.adopted_asset || "";
|
||||
const activeScriptShot = sbActiveShot ? shots[sbActiveShot.sort_order] : null;
|
||||
const directorBeats = buildDirectorBeats(
|
||||
activeScriptShot?.visual_prompt || "",
|
||||
activeScriptShot?.narration || "",
|
||||
activeScriptShot?.duration_seconds || SEGMENT_DURATION_MAX,
|
||||
);
|
||||
const directorRange = sceneTimes[sbSelected] || "—";
|
||||
return (
|
||||
<section className="stage active" data-stage-pane="3">
|
||||
<div className="stage-storyboard">
|
||||
@@ -3949,22 +3922,7 @@ export function PipelinePage(props: {
|
||||
<span>{sbActiveShot.error_message}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="muted-2" style={{ fontSize: "12px", lineHeight: 1.55, marginBottom: "10px" }}>每个场生成一张干净视频关键帧;下方导演稿按本镜真实脚本展开,视频只使用关键帧,不会带入文字或边框。</div>
|
||||
<div className="sb-director-board" aria-label="本场导演故事板">
|
||||
<div className="sb-director-head">
|
||||
<div><span className="mono">DIRECTOR BOARD</span><strong>场 {sbSelected + 1} · {directorRange}</strong></div>
|
||||
<span className="pill pill-l3 neutral"><span className="dot" />9:16 · 关键帧</span>
|
||||
</div>
|
||||
{directorBeats.length ? directorBeats.map((beat, index) => (
|
||||
<div className="sb-director-beat" key={`${beat.time}-${index}`}>
|
||||
<div className="sb-director-time">{beat.time}</div>
|
||||
<div className="sb-director-copy">
|
||||
<div className="sb-director-direction">{beat.direction}</div>
|
||||
{beat.narration && <div className="sb-director-narration">口播/旁白:{beat.narration}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)) : <div className="sb-director-empty">脚本生成后,这里会自动展示每段时间、机位、动作和旁白。</div>}
|
||||
</div>
|
||||
<div className="muted-2" style={{ fontSize: "12px", lineHeight: 1.55, marginBottom: "10px" }}>每个场由 image-2 直接生成一张完整导演故事板图片;可单独「重跑本场」只重出这一张,每场各自留历史版本,互不影响。</div>
|
||||
<div className="muted mono" style={{ fontSize: "12px", fontWeight: 500, marginBottom: "6px", letterSpacing: ".04em" }}>整张风格提示词(重跑时生效,可编辑)</div>
|
||||
<PromptBox
|
||||
className="prompt-edit"
|
||||
|
||||
@@ -334,7 +334,10 @@ export function VideoReplacePage({
|
||||
resolution: "720p",
|
||||
duration: outputDuration,
|
||||
refs: [
|
||||
...(videoRef ? [{ type: "video", duration: videoRef.duration || videoMeta.duration }] : []),
|
||||
// 商品复刻的参考视频只用来提炼分镜稿,不会发给火山,算进估价会让显示积分高于实际扣费。
|
||||
...(videoRef && replaceMode === "character"
|
||||
? [{ type: "video", duration: videoRef.duration || videoMeta.duration }]
|
||||
: []),
|
||||
...((source === "library"
|
||||
? Array.from({ length: Math.max(1, libraryImageCount) }, () => ({ type: "image" as const }))
|
||||
: (tempFiles.length ? tempFiles : tempAssetRefs)
|
||||
|
||||
Reference in New Issue
Block a user