auto-save 2026-05-17 22:57 (~2)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user