fix: move transcript timeline below video
This commit is contained in:
2
RULES.md
2
RULES.md
@@ -15,7 +15,7 @@
|
||||
|
||||
## 部署事实
|
||||
- 平台:VPS `76.13.31.179`(Ubuntu 24.04 / Docker Compose / Coolify Traefik)
|
||||
- 发布状态:已部署并验证(2026-05-19,逐句时间轴窄版面板 + 波形同框时间对齐画面胶片 + 胶片密度按钮上移波形顶部 + 去分隔线 + 胶片上下错落 + body 顶层原位大放大 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401,认证后首页 200;容器内 `/health` 返回 `ok:true`
|
||||
- 发布状态:已部署并验证(2026-05-19,逐句时间轴移到原版视频下方并支持双行文案 + 波形同框时间对齐画面胶片 + 胶片密度按钮上移波形顶部 + 去分隔线 + 胶片上下错落 + body 顶层原位大放大 + 隐藏源视频工作区音频解析摘要卡 + 隐藏工作区顶部状态提示条 + 三字段候选生成工作流 + 折叠紧凑候选区);`https://marketing.skg.com` 已启用应用内登录页,未登录 API 返回 401,认证后首页 200;容器内 `/health` 返回 `ok:true`
|
||||
- 主站 / 前端:`https://marketing.skg.com`
|
||||
- API / 后端:`https://marketing.skg.com/api`
|
||||
- 代码仓库 / Gitea:`https://git.kang-kang.com/kangwan/20260512-skg-tk`
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2576,8 +2576,8 @@ function AudioIntakePanel({
|
||||
|
||||
<div className="grid gap-2 border-t border-white/8 pt-2">
|
||||
<div className="grid gap-2">
|
||||
<div className="grid gap-3 xl:grid-cols-[360px_minmax(0,1fr)] 2xl:grid-cols-[390px_minmax(0,1fr)]">
|
||||
<div className="min-w-0">
|
||||
<div className="grid gap-3 xl:grid-cols-[430px_minmax(0,1fr)] 2xl:grid-cols-[460px_minmax(0,1fr)]">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<SectionTitle icon={<Play className="h-4 w-4" />} title="原版视频" />
|
||||
<span className="font-mono text-[11px] text-white/38">{currentTime.toFixed(1)}s</span>
|
||||
@@ -2615,6 +2615,13 @@ function AudioIntakePanel({
|
||||
当前点抽帧
|
||||
</button>
|
||||
</div>
|
||||
<TranscriptTimelinePanel
|
||||
job={job}
|
||||
processing={processing}
|
||||
activeSegmentIndex={activeSegment?.index ?? null}
|
||||
rowRefs={rowRefs}
|
||||
onSeek={seekTo}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-2">
|
||||
@@ -2655,7 +2662,7 @@ function AudioIntakePanel({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 xl:grid-cols-[360px_minmax(0,1fr)] 2xl:grid-cols-[400px_minmax(0,1fr)]">
|
||||
<div className="min-w-0">
|
||||
<SourceKeyframePicker
|
||||
job={job}
|
||||
frames={frames}
|
||||
@@ -2669,46 +2676,6 @@ function AudioIntakePanel({
|
||||
filmstripDragging={filmstripDragTime !== null}
|
||||
onDropFilmstripFrame={(time) => void addFilmstripFrame(time)}
|
||||
/>
|
||||
|
||||
<div className="min-w-0 xl:max-w-[620px] 2xl:max-w-[680px]">
|
||||
<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>
|
||||
</div>
|
||||
@@ -2725,6 +2692,62 @@ function AudioIntakePanel({
|
||||
)
|
||||
}
|
||||
|
||||
function TranscriptTimelinePanel({
|
||||
job,
|
||||
processing,
|
||||
activeSegmentIndex,
|
||||
rowRefs,
|
||||
onSeek,
|
||||
}: {
|
||||
job: Job
|
||||
processing: boolean
|
||||
activeSegmentIndex: number | null
|
||||
rowRefs: { current: Record<number, HTMLDivElement | null> }
|
||||
onSeek: (time: number) => void
|
||||
}) {
|
||||
return (
|
||||
<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-[68px_minmax(0,1fr)] border-b border-white/10 bg-white/[0.04] px-2.5 py-1.5 text-[11px] font-semibold text-white/50">
|
||||
<div>时间</div>
|
||||
<div>原文 / 中文</div>
|
||||
</div>
|
||||
<div className="max-h-[252px] overflow-y-auto 2xl:max-h-[306px]">
|
||||
{job.transcript.map((segment) => {
|
||||
const active = activeSegmentIndex === segment.index
|
||||
return (
|
||||
<div
|
||||
key={segment.index}
|
||||
ref={(node) => { rowRefs.current[segment.index] = node }}
|
||||
onClick={() => onSeek(segment.start)}
|
||||
className={`grid cursor-pointer grid-cols-[68px_minmax(0,1fr)] items-start gap-2 border-b px-2.5 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={`pt-0.5 font-mono text-[10px] leading-tight ${active ? "text-emerald-100" : "text-white/38"}`}>{segment.start.toFixed(1)}-{segment.end.toFixed(1)}s</div>
|
||||
<div className="min-w-0">
|
||||
<div className="line-clamp-2 break-words" title={segment.en}>{segment.en || <span className="text-white/30">-</span>}</div>
|
||||
<div className={`mt-0.5 line-clamp-2 break-words 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>
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineFilmstrip({
|
||||
frames,
|
||||
status,
|
||||
|
||||
Reference in New Issue
Block a user