refactor: compact audio intake panel

This commit is contained in:
2026-05-17 14:12:15 +08:00
parent 660348f39d
commit 3030f8938d
3 changed files with 68 additions and 83 deletions

View File

@@ -1,31 +1,5 @@
{ {
"entries": [ "entries": [
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-14 18:21 (~1)",
"ts": "2026-05-14T10:26:15Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"hash": "995dd88",
"message": "auto-save 2026-05-14 18:27 (~1)",
"ts": "2026-05-14T18:27:37+08:00",
"type": "commit"
},
{
"files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-14 18:27 (~1)",
"ts": "2026-05-14T10:28:43Z",
"type": "session-heartbeat"
},
{
"files_changed": 1,
"hash": "97c039b",
"message": "auto-save 2026-05-14 22:00 (~1)",
"ts": "2026-05-14T22:33:31+08:00",
"type": "commit"
},
{ {
"files_changed": 1, "files_changed": 1,
"message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-14 22:00 (~1)", "message": "Codex 会话活跃 · 最近命令codex · 1 项未提交变更 · 最近提交auto-save 2026-05-14 22:00 (~1)",
@@ -3270,6 +3244,32 @@
"message": "auto-save 2026-05-17 13:56 (~4)", "message": "auto-save 2026-05-17 13:56 (~4)",
"hash": "c4b6980", "hash": "c4b6980",
"files_changed": 4 "files_changed": 4
},
{
"ts": "2026-05-17T13:58:05+08:00",
"type": "commit",
"message": "fix: use local asr for transcript timeline",
"hash": "660348f",
"files_changed": 2
},
{
"ts": "2026-05-17T05:58:25Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交fix: use local asr for transcript timeline",
"files_changed": 1
},
{
"ts": "2026-05-17T06:08:25Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 main · 1 项未提交变更 · 最近提交fix: use local asr for transcript timeline",
"files_changed": 1
},
{
"ts": "2026-05-17T14:12:15+08:00",
"type": "commit",
"message": "auto-save 2026-05-17 14:12 (~3)",
"hash": "c17fd19",
"files_changed": 3
} }
] ]
} }

View File

@@ -588,7 +588,7 @@
<tr><td><code>web/next.config.mjs</code></td><td>Next.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator并移除 Next 16 已不支持的 <code>eslint</code> 顶层配置,避免本地 dev 出现配置 Issue 提示。</td></tr> <tr><td><code>web/next.config.mjs</code></td><td>Next.js 构建配置:静态导出、图片不走优化、禁用开发环境左下角 Next Dev Indicator并移除 Next 16 已不支持的 <code>eslint</code> 顶层配置,避免本地 dev 出现配置 Issue 提示。</td></tr>
<tr><td><code>web/app/globals.css</code></td><td>全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 <code>nextjs-portal</code> 遮挡隐藏规则。</td></tr> <tr><td><code>web/app/globals.css</code></td><td>全局主题变量、登录页视觉样式、ReactFlow 样式引用,以及本地开发态 <code>nextjs-portal</code> 遮挡隐藏规则。</td></tr>
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态jobs、activeJobId、生成任务状态主渲染为全屏素材输入列 + 音频解析工作表;“开始”编排状态只负责在下载完成后自动触发 <code>triggerTranscribe</code>不再默认触发抽帧、Vision 扫描或分镜初稿保存;底部吸附音频条不再从主界面渲染。</td></tr> <tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态jobs、activeJobId、生成任务状态主渲染为全屏素材输入列 + 音频解析工作表;“开始”编排状态只负责在下载完成后自动触发 <code>triggerTranscribe</code>不再默认触发抽帧、Vision 扫描或分镜初稿保存;底部吸附音频条不再从主界面渲染。</td></tr>
<tr><td><code>web/components/ad-recreation-board.tsx</code></td><td>信息流广告音频解析工作表:左侧素材输入;右侧展示视频下载状态、原文案/中文翻译、讲话人/节奏/背景音分析和逐句时间轴。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。</td></tr> <tr><td><code>web/components/ad-recreation-board.tsx</code></td><td>信息流广告音频解析工作表:左侧素材输入;右侧展示视频下载状态、默认折叠的音频文案依据、逐句时间轴和讲话人/节奏/背景音分析。旧分镜卡、抽帧控制和视频生成组件仍保留在文件里,但当前主路径不渲染。</td></tr>
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。</td></tr> <tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:访问账号/访问密钥表单、保持登录、错误/成功状态;当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录表单;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。</td></tr>
<tr><td><code>web/app/login/layout.tsx</code></td><td>登录路由专属 layout覆盖全站默认网页标题和描述为空避免 <code>/login</code> 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。</td></tr> <tr><td><code>web/app/login/layout.tsx</code></td><td>登录路由专属 layout覆盖全站默认网页标题和描述为空避免 <code>/login</code> 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。</td></tr>
<tr><td><code>web/components/login/oasis-canvas.tsx</code></td><td>登录页全屏动态视觉层:用 iframe 直接承载下载包 <code>web/public/oasis-source/index.html</code> 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 <code>postMessage</code> 转发给 iframe避免登录面板或输入框遮挡时草地失去鼠标响应。</td></tr> <tr><td><code>web/components/login/oasis-canvas.tsx</code></td><td>登录页全屏动态视觉层:用 iframe 直接承载下载包 <code>web/public/oasis-source/index.html</code> 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 <code>postMessage</code> 转发给 iframe避免登录面板或输入框遮挡时草地失去鼠标响应。</td></tr>
@@ -612,7 +612,7 @@
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回。</td></tr> <tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端登录会话、状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、原音频转写/翻译、声音与背景音分析、后续口播改写/TTS、文件返回。</td></tr>
<tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr> <tr><td><code>api/product_library/skg-products</code></td><td>内置 SKG 白底产品图库:<code>manifest.json</code> 记录从桌面产品图筛出的 gallery 白底图和桌面 4 张产品角度图,<code>images/</code> 存 45 张参考图。</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/state.json</code></td><td>运行时状态文件,不在源码列表里,但刷新恢复依赖它。</td></tr> <tr><td><code>jobs/&lt;jobId&gt;/state.json</code></td><td>运行时状态文件,不在源码列表里,但刷新恢复依赖它。</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/audio.wav</code></td><td>拆轨得到的原始音频,底部 Audio Strip 会通过只读接口拉取并在浏览器里解码成波形峰值</td></tr> <tr><td><code>jobs/&lt;jobId&gt;/audio.wav</code></td><td>拆轨得到的原始音频,当前只作为后端分析和后续必要预览的只读文件来源;主界面不再默认渲染底部音频条</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/frames</code></td><td>关键帧 jpg。注意 frame.index 是稳定 ID不等于数组下标。</td></tr> <tr><td><code>jobs/&lt;jobId&gt;/frames</code></td><td>关键帧 jpg。注意 frame.index 是稳定 ID不等于数组下标。</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/cleaned</code></td><td>清洗后待应用图片。</td></tr> <tr><td><code>jobs/&lt;jobId&gt;/cleaned</code></td><td>清洗后待应用图片。</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/elements</code></td><td>元素提取图,多版本命名:<code>idx_elementId_cutoutId.jpg</code></td></tr> <tr><td><code>jobs/&lt;jobId&gt;/elements</code></td><td>元素提取图,多版本命名:<code>idx_elementId_cutoutId.jpg</code></td></tr>
@@ -625,7 +625,7 @@
web/app/page.tsx web/app/page.tsx
-> 音频解析工作表web/components/ad-recreation-board.tsx -> 音频解析工作表web/components/ad-recreation-board.tsx
-> 开始:创建/激活 job → 下载完成后自动触发音频处理 -> 开始:创建/激活 job → 下载完成后自动触发音频处理
-> 左侧素材输入列 + 右侧原文案/中文翻译/声音背景音分析/逐句时间轴 -> 左侧素材输入列 + 右侧默认折叠的音频文案依据 + 逐句时间轴 + 声音背景音分析
-> 底部音频条:不再渲染,音频结果集中到右侧工作表 -> 底部音频条:不再渲染,音频结果集中到右侧工作表
-> 旧节点/深度素材面板web/components/nodes/index.tsx、web/components/lightbox.tsx、web/components/storyboard-workbench.tsx底层保留当前不作为主入口 -> 旧节点/深度素材面板web/components/nodes/index.tsx、web/components/lightbox.tsx、web/components/storyboard-workbench.tsx底层保留当前不作为主入口
-> API 契约web/lib/api.ts -> API 契约web/lib/api.ts
@@ -643,12 +643,12 @@ api/main.py
<div class="flow-row"> <div class="flow-row">
<div><strong>你看到的区域</strong><span>信息流广告音频解析工作表</span></div> <div><strong>你看到的区域</strong><span>信息流广告音频解析工作表</span></div>
<div><strong>主要源码</strong><span><code>AdRecreationBoard</code> in <code>web/components/ad-recreation-board.tsx</code>;状态、轮询和接口回写仍在 <code>web/app/page.tsx</code></span></div> <div><strong>主要源码</strong><span><code>AdRecreationBoard</code> in <code>web/components/ad-recreation-board.tsx</code>;状态、轮询和接口回写仍在 <code>web/app/page.tsx</code></span></div>
<div><strong>适合怎么描述</strong><span>“素材输入列、开始后的自动下载/音频解析、原文案/翻译/声音背景音结果怎么展示”。</span></div> <div><strong>适合怎么描述</strong><span>“素材输入列、开始后的自动下载/音频解析、逐句时间轴和声音背景音结果怎么展示”。</span></div>
</div> </div>
<div class="flow-row"> <div class="flow-row">
<div><strong>你看到的区域</strong><span>音频解析结果表</span></div> <div><strong>你看到的区域</strong><span>音频解析结果表</span></div>
<div><strong>主要源码</strong><span><code>AudioIntakePanel</code> / <code>AudioIntakeStatus</code> in <code>web/components/ad-recreation-board.tsx</code>;复用 <code>triggerTranscribe</code><code>AudioScript</code></span></div> <div><strong>主要源码</strong><span><code>AudioIntakePanel</code> / <code>AudioIntakeStatus</code> in <code>web/components/ad-recreation-board.tsx</code>;复用 <code>triggerTranscribe</code><code>AudioScript</code></span></div>
<div><strong>适合怎么描述</strong><span>原始文案、中文翻译、讲话人、节奏、背景音、逐句时间轴还需要哪些字段”。</span></div> <div><strong>适合怎么描述</strong><span>逐句时间轴、讲话人、节奏、背景音还需要哪些字段;全文文案依据是否需要展开查看”。</span></div>
</div> </div>
<div class="flow-row"> <div class="flow-row">
<div><strong>你看到的区域</strong><span>旧深度素材面板(当前不作为主路径)</span></div> <div><strong>你看到的区域</strong><span>旧深度素材面板(当前不作为主路径)</span></div>
@@ -863,8 +863,8 @@ SubjectAsset {
</tr> </tr>
<tr> <tr>
<td><span class="tag gray">音频条</span></td> <td><span class="tag gray">音频条</span></td>
<td>音频解析工作表顶部触发音频解析,结果在右侧原文案、中文翻译、逐句时间轴和声音/背景音分析区展示;底部 <code>AudioStrip</code> 当前不渲染。</td> <td>音频解析工作表顶部触发音频解析;全文音频文案依据默认折叠,主展示以逐句时间轴和声音/背景音分析区为准;底部 <code>AudioStrip</code> 当前不渲染。</td>
<td>当前第一步不要默认展示底部音频条、新配音播放器,或把 MiniMax 配音当作已完成结果。</td> <td>当前第一步不要默认展示底部音频条、新配音播放器、独立原文案提取大卡片,或把 MiniMax 配音当作已完成结果。</td>
<td><code>web/components/audio-strip.tsx</code><code>pipeline_transcribe</code><code>AudioScript</code></td> <td><code>web/components/audio-strip.tsx</code><code>pipeline_transcribe</code><code>AudioScript</code></td>
</tr> </tr>
<tr> <tr>
@@ -941,6 +941,18 @@ SubjectAsset {
<h2>变更记录</h2> <h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p> <p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog"> <div class="changelog">
<article class="change">
<header>
<h3>2026-05-17 · 收紧音频解析第一步版面</h3>
<span class="tag rose">UI</span>
<span class="tag cyan">Workflow</span>
</header>
<div class="body">
<p><strong>问题:</strong>音频解析第一步顶部“音频文案依据”和主区域“原文案提取”同时展示全文,和逐句时间轴重复,占用太多工作台版面。</p>
<p><strong>改动:</strong><code>web/components/ad-recreation-board.tsx</code> 将顶部“音频文案依据”改为默认折叠的 <code>details</code>;移除主面板里的“原文案提取”大卡片,让“逐句时间轴”成为音频解析后的主展示,声音/背景音分析降到其后。</p>
<p><strong>影响:</strong><code>web/components/ad-recreation-board.tsx</code><code>docs/source-analysis.html</code>。后续如果需要全文原文案,只展开“音频文案依据”,不要再恢复独立原文案大卡片。</p>
</div>
</article>
<article class="change"> <article class="change">
<header> <header>
<h3>2026-05-17 · 修复逐句时间轴假字幕</h3> <h3>2026-05-17 · 修复逐句时间轴假字幕</h3>

View File

@@ -2,7 +2,7 @@
import { type ReactNode, type RefObject, useEffect, useRef, useState } from "react" import { type ReactNode, type RefObject, useEffect, useRef, useState } from "react"
import { import {
AlertTriangle, Check, Circle, Film, FileText, Image as ImageIcon, Link2, Loader2, AlertTriangle, Check, ChevronDown, Circle, Film, FileText, Image as ImageIcon, Link2, Loader2,
Mic, Package, PanelRight, Play, Plus, Scissors, Sparkles, Trash2, Upload, Wand2, Mic, Package, PanelRight, Play, Plus, Scissors, Sparkles, Trash2, Upload, Wand2,
} from "lucide-react" } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
@@ -387,23 +387,20 @@ export function AdRecreationBoard({
</div> </div>
</div> </div>
<div className="mt-3 grid grid-cols-[minmax(0,1fr)_470px] gap-3"> <div className="mt-3 grid grid-cols-1 items-start gap-3 xl:grid-cols-[minmax(0,1fr)_470px]">
<div className="rounded-lg border border-white/10 bg-black/32 p-3"> <details className="group rounded-lg border border-white/10 bg-black/32 p-3">
<div className="mb-2 flex items-center justify-between"> <summary className="flex cursor-pointer list-none items-center justify-between gap-3">
<SectionTitle icon={<FileText className="h-4 w-4" />} title="音频文案依据" /> <SectionTitle icon={<FileText className="h-4 w-4" />} title="音频文案依据" />
<StatusPill ready={audioReady} running={job?.status === "transcribing" || job?.audio_script?.status === "rewriting"} /> <div className="flex items-center gap-2">
</div> <span className="font-mono text-[11px] text-white/38">{transcriptCount ? `${transcriptCount}` : "待解析"}</span>
<div className="max-h-20 overflow-y-auto rounded-md border border-white/10 bg-black/35 p-2 text-[12px] leading-relaxed text-white/62"> <StatusPill ready={audioReady} running={job?.status === "transcribing" || job?.audio_script?.status === "rewriting"} />
<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">
{audioPreview(job)} {audioPreview(job)}
</div> </div>
{(job?.audio_script?.speaker_profile || job?.audio_script?.rhythm_profile) && ( </details>
<div className="mt-2 grid gap-1 text-[11px] leading-relaxed text-white/42">
{job.audio_script.speaker_profile && <div>{job.audio_script.speaker_profile}</div>}
{job.audio_script.rhythm_profile && <div>{job.audio_script.rhythm_profile}</div>}
{job.audio_script.background_audio_profile && <div>{job.audio_script.background_audio_profile}</div>}
</div>
)}
</div>
<AudioIntakeStatus job={job} audioReady={audioReady} /> <AudioIntakeStatus job={job} audioReady={audioReady} />
</div> </div>
@@ -548,8 +545,6 @@ function AudioIntakePanel({ job }: { job: Job | null }) {
} }
const script = job.audio_script const script = job.audio_script
const original = script?.source_text?.trim() || job.transcript.map((item) => item.en).filter(Boolean).join(" ")
const translated = script?.source_zh?.trim() || job.transcript.map((item) => item.zh).filter(Boolean).join(" ")
const profiles = [ const profiles = [
{ label: "讲话人", value: script?.speaker_profile }, { label: "讲话人", value: script?.speaker_profile },
{ label: "节奏", value: script?.rhythm_profile }, { label: "节奏", value: script?.rhythm_profile },
@@ -559,29 +554,6 @@ function AudioIntakePanel({ job }: { job: Job | null }) {
return ( return (
<div className="grid gap-3"> <div className="grid gap-3">
<section className="rounded-lg border border-white/10 bg-black/28 p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<SectionTitle icon={<FileText className="h-4 w-4" />} title="原文案提取" />
<StatusPill ready={!!original || job.transcript.length > 0} running={processing} />
</div>
<div className="grid gap-3 xl:grid-cols-2">
<TextBlock title="原始文案" value={original} empty={processing ? "正在提取原音频文案..." : "还没有提取到原文案。"} />
<TextBlock title="中文翻译" value={translated} empty={processing ? "正在翻译..." : "还没有中文翻译。"} />
</div>
</section>
<section className="rounded-lg border border-white/10 bg-black/28 p-3">
<div className="mb-3 flex items-center justify-between gap-3">
<SectionTitle icon={<Mic className="h-4 w-4" />} title="声音与背景音分析" />
<span className="font-mono text-[11px] text-white/38">{formatSeconds(job.duration)}</span>
</div>
<div className="grid gap-2 lg:grid-cols-3">
{profiles.map((item) => (
<ProfileTile key={item.label} label={item.label} value={item.value} running={processing} />
))}
</div>
</section>
<section className="rounded-lg border border-white/10 bg-black/28 p-3"> <section className="rounded-lg border border-white/10 bg-black/28 p-3">
<div className="mb-3 flex items-center justify-between gap-3"> <div className="mb-3 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="逐句时间轴" />
@@ -608,17 +580,18 @@ function AudioIntakePanel({ job }: { job: Job | null }) {
<EmptyState text={processing ? "音频解析中,完成后这里会按时间列出原文案和中文翻译。" : "下载完成后会自动解析音频;也可以点击右上角“解析音频”手动重试。"} /> <EmptyState text={processing ? "音频解析中,完成后这里会按时间列出原文案和中文翻译。" : "下载完成后会自动解析音频;也可以点击右上角“解析音频”手动重试。"} />
)} )}
</section> </section>
</div>
)
}
function TextBlock({ title, value, empty }: { title: string; value?: string; empty: string }) { <section className="rounded-lg border border-white/10 bg-black/28 p-3">
return ( <div className="mb-3 flex items-center justify-between gap-3">
<div className="min-h-[156px] rounded-lg border border-white/10 bg-black/35 p-3"> <SectionTitle icon={<Mic className="h-4 w-4" />} title="声音与背景音分析" />
<div className="mb-2 text-[11px] font-semibold text-white/48">{title}</div> <span className="font-mono text-[11px] text-white/38">{formatSeconds(job.duration)}</span>
<div className="max-h-[220px] overflow-y-auto whitespace-pre-wrap text-[12.5px] leading-relaxed text-white/72"> </div>
{value || <span className="text-white/32">{empty}</span>} <div className="grid gap-2 lg:grid-cols-3">
</div> {profiles.map((item) => (
<ProfileTile key={item.label} label={item.label} value={item.value} running={processing} />
))}
</div>
</section>
</div> </div>
) )
} }