auto-save 2026-05-17 22:57 (~2)

This commit is contained in:
2026-05-17 22:57:27 +08:00
parent 71c9a45a1f
commit b4b2259440
2 changed files with 140 additions and 134 deletions

View File

@@ -808,40 +808,44 @@ export function AdRecreationBoard({
/>
<section className="flex min-h-0 flex-col rounded-lg border border-white/10 bg-white/[0.035] shadow-2xl">
<header className="shrink-0 border-b border-white/10 p-4">
<div className="flex items-start justify-between gap-4">
<div>
<header className="shrink-0 border-b border-white/10 p-3">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-rose-500/12 text-rose-100"><Mic className="h-4 w-4" /></span>
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-rose-500/12 text-rose-100"><Mic className="h-3.5 w-3.5" /></span>
<span className="font-mono text-[12px] text-white/36">02</span>
<h2 className="text-[15px] font-semibold leading-tight text-white"></h2>
</div>
<div className="mt-1 truncate text-[11px] text-white/38" title={job?.message}>
{job?.message || "下载源视频后解析音频,再抽参考帧并选择相似主角。"}
</div>
<h2 className="mt-2 text-[17px] font-semibold leading-tight text-white"></h2>
<p className="mt-1 text-[12px] text-white/42"></p>
</div>
<div className="flex shrink-0 flex-wrap justify-end gap-2">
<ActionButton disabled={!job?.video_url || job.status === "transcribing"} onClick={() => data.onTranscribeAudio?.(job?.id)}>
<Mic className="h-3.5 w-3.5" />
</ActionButton>
</div>
<ActionButton disabled={!job?.video_url || job.status === "transcribing"} onClick={() => data.onTranscribeAudio?.(job?.id)}>
<Mic className="h-3.5 w-3.5" />
</ActionButton>
</div>
<div className="mt-3 grid grid-cols-1 items-start gap-3 xl:grid-cols-[minmax(0,1fr)_470px]">
<details className="group rounded-lg border border-white/10 bg-black/32 p-3">
<summary className="flex cursor-pointer list-none items-center justify-between gap-3">
<SectionTitle icon={<FileText className="h-4 w-4" />} title="音频文案依据" />
<div className="mt-2 grid grid-cols-1 gap-2 xl:grid-cols-[minmax(0,1fr)_260px]">
<div className="grid grid-cols-4 gap-2 rounded-md border border-white/10 bg-black/28 p-2 text-[11px] text-white/52">
<Requirement label="素材" ready={!!job} detail={job ? shortId(job.id) : "待输入"} />
<Requirement label="视频" ready={!!job?.video_url} detail={job?.status === "downloading" ? "下载中" : job?.video_url ? "已就绪" : "待下载"} />
<Requirement label="音频" ready={!!job?.source_audio_url} detail={job?.status === "transcribing" ? "解析中" : job?.source_audio_url ? "已提取" : "待提取"} />
<Requirement label="文案" ready={audioReady} detail={audioReady ? `${transcriptCount}` : "待解析"} />
</div>
<details className="group rounded-md border border-white/10 bg-black/28 p-2">
<summary className="flex cursor-pointer list-none items-center justify-between gap-2">
<SectionTitle icon={<FileText className="h-4 w-4" />} title="文案依据" />
<div className="flex items-center gap-2">
<span className="font-mono text-[11px] text-white/38">{transcriptCount ? `${transcriptCount}` : "待解析"}</span>
<StatusPill ready={audioReady} running={job?.status === "transcribing" || job?.audio_script?.status === "rewriting"} />
<span className="font-mono text-[10.5px] text-white/38">{transcriptCount ? `${transcriptCount}` : "待解析"}</span>
<ChevronDown className="h-4 w-4 text-white/38 transition group-open:rotate-180" />
</div>
</summary>
<div className="mt-3 max-h-24 overflow-y-auto rounded-md border border-white/10 bg-black/35 p-2 text-[12px] leading-relaxed text-white/62">
<div className="mt-2 max-h-20 overflow-y-auto rounded border border-white/10 bg-black/35 p-2 text-[11px] leading-relaxed text-white/58">
{audioPreview(job)}
</div>
</details>
<AudioIntakeStatus job={job} audioReady={audioReady} />
</div>
</header>
@@ -1121,7 +1125,7 @@ function AudioIntakePanel({
return (
<section className="rounded-lg border border-white/10 bg-black/28 p-2.5">
<div className="mb-2 flex items-center justify-between gap-3">
<SectionTitle icon={<Film className="h-4 w-4" />} title="音频解析结果" />
<SectionTitle icon={<Film className="h-4 w-4" />} title="源视频工作区" />
<div className="flex items-center gap-2 font-mono text-[11px] text-white/38">
<span>{job.transcript.length} </span>
<span>{formatSeconds(job.duration)}</span>
@@ -1135,53 +1139,20 @@ function AudioIntakePanel({
</div>
<div className="grid gap-2 border-t border-white/8 pt-2">
<div className="rounded-md border border-white/10 bg-black/32 p-2">
<div className="mb-1 flex items-center justify-between gap-3 text-[10px] text-white/40">
<span> / </span>
<div className="flex items-center gap-2 font-mono">
<span> {currentTime.toFixed(1)}s</span>
<span> {formatSeconds(timelineDuration)}</span>
<span>{waveTimeHint}</span>
</div>
</div>
<AudioWaveform
features={audioFeatures}
status={audioFeatureStatus}
currentTime={currentTime}
hoverTime={waveHoverTime}
duration={timelineDuration}
segments={job.transcript}
onSeek={seekTo}
onHoverTimeChange={setWaveHoverTime}
/>
</div>
<div className="grid gap-2">
<div className="grid gap-2 xl:grid-cols-[minmax(520px,1fr)_360px] 2xl:grid-cols-[minmax(640px,1fr)_420px]">
<div className="grid gap-3 xl:grid-cols-[280px_minmax(0,1fr)] 2xl:grid-cols-[300px_minmax(0,1fr)]">
<div className="min-w-0">
<div className="mb-2 flex items-center justify-between gap-3">
<SectionTitle icon={<Play className="h-4 w-4" />} title="原版视频" />
<div className="flex items-center gap-2">
<span className="font-mono text-[11px] text-white/38">{currentTime.toFixed(1)}s / {formatSeconds(timelineDuration)}</span>
<button
type="button"
onClick={() => void addFrameAtCurrentTime()}
disabled={!job.video_url || !onAddFrame || manualBusy || job.status === "splitting"}
title={`按当前播放位置手动抽帧:${currentTime.toFixed(1)}s`}
className="inline-flex h-7 items-center justify-center gap-1 rounded-md border border-emerald-300/20 bg-emerald-300/[0.08] px-2 text-[10.5px] font-semibold text-emerald-100 transition hover:border-emerald-200/45 hover:bg-emerald-300/[0.14] disabled:cursor-not-allowed disabled:opacity-35"
>
{manualBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
</button>
</div>
<span className="font-mono text-[11px] text-white/38">{currentTime.toFixed(1)}s</span>
</div>
<div className="overflow-hidden rounded-md border border-white/10 bg-black/45">
<div className="mx-auto aspect-[9/16] h-[420px] overflow-hidden rounded-md border border-white/10 bg-black 2xl:h-[480px]">
{job.video_url ? (
<video
ref={videoRef}
controls
playsInline
className="h-[320px] w-full bg-black object-contain 2xl:h-[380px]"
className="h-full w-full bg-black object-contain"
src={videoSrcUrl}
onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime)}
onSeeked={(event) => setCurrentTime(event.currentTarget.currentTime)}
@@ -1195,49 +1166,82 @@ function AudioIntakePanel({
}}
/>
) : (
<div className="flex h-[320px] items-center justify-center text-[12px] text-white/38 2xl:h-[380px]"></div>
<div className="flex h-full items-center justify-center text-[12px] text-white/38"></div>
)}
</div>
<button
type="button"
onClick={() => void addFrameAtCurrentTime()}
disabled={!job.video_url || !onAddFrame || manualBusy || job.status === "splitting"}
title={`按当前播放位置手动抽帧:${currentTime.toFixed(1)}s`}
className="mt-2 inline-flex h-8 w-full items-center justify-center gap-1 rounded-md border border-emerald-300/20 bg-emerald-300/[0.08] px-2 text-[11px] font-semibold text-emerald-100 transition hover:border-emerald-200/45 hover:bg-emerald-300/[0.14] disabled:cursor-not-allowed disabled:opacity-35"
>
{manualBusy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Plus className="h-3.5 w-3.5" />}
· {currentTime.toFixed(1)}s
</button>
</div>
<div className="min-w-0">
<div className="mb-2 flex items-center justify-between gap-3">
<SectionTitle icon={<FileText className="h-4 w-4" />} title="逐句时间轴" />
<span className="rounded-md border border-white/10 bg-black/35 px-2 py-1 text-[11px] text-white/45">{job.transcript.length} </span>
</div>
{job.transcript.length ? (
<div className="overflow-hidden rounded-md border border-white/10">
<div className="grid grid-cols-[76px_minmax(0,1fr)] border-b border-white/10 bg-white/[0.04] px-3 py-1.5 text-[11px] font-semibold text-white/50">
<div></div>
<div> / </div>
</div>
<div className="max-h-[320px] overflow-y-auto 2xl:max-h-[380px]">
{job.transcript.map((segment) => {
const active = activeSegment?.index === segment.index
return (
<div
key={segment.index}
ref={(node) => { rowRefs.current[segment.index] = node }}
onClick={() => seekTo(segment.start)}
className={`grid cursor-pointer grid-cols-[76px_minmax(0,1fr)] gap-2 border-b px-3 py-1.5 text-[11.5px] leading-snug transition last:border-b-0 ${
active
? "border-emerald-300/18 bg-emerald-300/[0.12] text-white"
: "border-white/8 text-white/64 hover:bg-white/[0.045]"
}`}
>
<div className={`font-mono text-[10.5px] ${active ? "text-emerald-100" : "text-white/38"}`}>{segment.start.toFixed(1)}-{segment.end.toFixed(1)}s</div>
<div className="min-w-0">
<div className="truncate" title={segment.en}>{segment.en || <span className="text-white/30">-</span>}</div>
<div className={`mt-0.5 truncate text-[11px] ${active ? "text-emerald-50/80" : "text-white/42"}`} title={segment.zh}>{segment.zh || <span className="text-white/30"></span>}</div>
</div>
</div>
)
})}
<div className="min-w-0 space-y-2">
<div className="rounded-md border border-white/10 bg-black/32 p-2">
<div className="mb-1 flex items-center justify-between gap-3 text-[10px] text-white/40">
<span> / </span>
<div className="flex items-center gap-2 font-mono">
<span> {currentTime.toFixed(1)}s</span>
<span> {formatSeconds(timelineDuration)}</span>
<span>{waveTimeHint}</span>
</div>
</div>
) : (
<EmptyState text={processing ? "音频解析中,完成后这里会按时间列出原文案和中文翻译。" : "下载完成后会自动解析音频;也可以点击右上角“解析音频”手动重试。"} />
)}
<AudioWaveform
features={audioFeatures}
status={audioFeatureStatus}
currentTime={currentTime}
hoverTime={waveHoverTime}
duration={timelineDuration}
segments={job.transcript}
onSeek={seekTo}
onHoverTimeChange={setWaveHoverTime}
/>
</div>
<div className="min-w-0">
<div className="mb-2 flex items-center justify-between gap-3">
<SectionTitle icon={<FileText className="h-4 w-4" />} title="逐句时间轴" />
<span className="rounded-md border border-white/10 bg-black/35 px-2 py-1 text-[11px] text-white/45">{job.transcript.length} </span>
</div>
{job.transcript.length ? (
<div className="overflow-hidden rounded-md border border-white/10">
<div className="grid grid-cols-[76px_minmax(0,1fr)] border-b border-white/10 bg-white/[0.04] px-3 py-1.5 text-[11px] font-semibold text-white/50">
<div></div>
<div> / </div>
</div>
<div className="max-h-[288px] overflow-y-auto 2xl:max-h-[346px]">
{job.transcript.map((segment) => {
const active = activeSegment?.index === segment.index
return (
<div
key={segment.index}
ref={(node) => { rowRefs.current[segment.index] = node }}
onClick={() => seekTo(segment.start)}
className={`grid cursor-pointer grid-cols-[76px_minmax(0,1fr)] gap-2 border-b px-3 py-1.5 text-[11.5px] leading-snug transition last:border-b-0 ${
active
? "border-emerald-300/18 bg-emerald-300/[0.12] text-white"
: "border-white/8 text-white/64 hover:bg-white/[0.045]"
}`}
>
<div className={`font-mono text-[10.5px] ${active ? "text-emerald-100" : "text-white/38"}`}>{segment.start.toFixed(1)}-{segment.end.toFixed(1)}s</div>
<div className="min-w-0">
<div className="truncate" title={segment.en}>{segment.en || <span className="text-white/30">-</span>}</div>
<div className={`mt-0.5 truncate text-[11px] ${active ? "text-emerald-50/80" : "text-white/42"}`} title={segment.zh}>{segment.zh || <span className="text-white/30"></span>}</div>
</div>
</div>
)
})}
</div>
</div>
) : (
<EmptyState text={processing ? "音频解析中,完成后这里会按时间列出原文案和中文翻译。" : "下载完成后会自动解析音频;也可以点击右上角“解析音频”手动重试。"} />
)}
</div>
</div>
</div>
@@ -1402,23 +1406,26 @@ function SourceReferenceBuildPanel({
{framePreviewPortal}
<div className="mb-2 flex items-center justify-between gap-3">
<SectionTitle icon={<ImageIcon className="h-4 w-4" />} title="关键帧 / 相似主角" />
<span className="rounded-md border border-white/10 bg-black/35 px-2 py-1 text-[11px] text-white/45">
{frames.length ? `${frames.length}` : "待抽帧"} · {selectedReferenceFrames.length}
</span>
</div>
<div className="h-[300px] overflow-y-auto rounded-md border border-white/10 bg-black/32 p-2 2xl:h-[340px]">
<div className="flex items-center gap-2">
<span className="rounded-md border border-white/10 bg-black/35 px-2 py-1 text-[11px] text-white/45">
{frames.length ? `${frames.length}` : "待抽帧"} · {selectedReferenceFrames.length}
</span>
<button
type="button"
onClick={() => void extractKeyframes()}
disabled={!job.video_url || extracting || job.status === "splitting"}
title="按之前的动作峰值逻辑抽帧,更偏向手势、表情变化、节奏点和镜头变化"
className="inline-flex h-7 items-center justify-center gap-1 rounded-md border border-white/10 bg-white/[0.055] px-2 text-[10.5px] font-semibold text-white/66 transition hover:border-cyan-300/35 hover:text-cyan-100 disabled:cursor-not-allowed disabled:opacity-35"
title="自动按动作峰值抽 12 张参考帧,更偏向手势、表情变化、节奏点和镜头变化"
className="inline-flex h-7 items-center justify-center gap-1 rounded-md bg-white px-2.5 text-[10.5px] font-semibold text-black transition hover:bg-white/90 disabled:cursor-not-allowed disabled:opacity-40"
>
{extracting || job.status === "splitting" ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Scissors className="h-3.5 w-3.5" />}
12
12
</button>
</div>
</div>
<div className="h-[300px] overflow-y-auto rounded-md border border-white/10 bg-black/32 p-2 2xl:h-[340px]">
<div className="flex items-center justify-between gap-2">
<span className="text-[10.5px] text-white/34"></span>
<span className="text-[10.5px] text-white/30"></span>
</div>
<div className="mt-2 grid grid-cols-6 gap-1.5 md:grid-cols-8 xl:grid-cols-12">
@@ -2306,12 +2313,12 @@ function AudioWaveform({
function ProfileTile({ label, value, running }: { label: string; value?: string; running?: boolean }) {
return (
<div className="min-h-[74px] rounded-md border border-white/10 bg-black/35 p-2.5">
<div className="mb-1.5 flex items-center justify-between gap-2">
<div className="min-h-[58px] rounded-md border border-white/10 bg-black/35 p-2">
<div className="mb-1 flex items-center justify-between gap-2">
<span className="text-[11px] font-semibold text-white/48">{label}</span>
{running ? <Loader2 className="h-3.5 w-3.5 animate-spin text-cyan-200" /> : value ? <Check className="h-3.5 w-3.5 text-emerald-200" /> : <Circle className="h-3.5 w-3.5 text-white/32" />}
</div>
<p className="max-h-[34px] overflow-hidden text-[11.5px] leading-snug text-white/62" title={value}>
<p className="line-clamp-2 text-[11px] leading-snug text-white/58" title={value}>
{value || (running ? "模型分析中..." : "等待音频分析结果。")}
</p>
</div>