Files
20260512-skg-tk/docs/source-analysis.html
2026-05-13 21:35:36 +08:00

1191 lines
61 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>SKG TK 二创验证 · 源码解析与协作地图</title>
<style>
:root {
--bg: #f8fafc;
--panel: #ffffff;
--panel-soft: #f1f5f9;
--ink: #0f172a;
--muted: #64748b;
--line: #dbe3ed;
--blue: #2563eb;
--violet: #7c3aed;
--orange: #ea580c;
--green: #059669;
--rose: #e11d48;
--code: #111827;
--shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--ink);
background:
linear-gradient(180deg, rgba(37, 99, 235, 0.06), transparent 340px),
var(--bg);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
}
a { color: inherit; }
code, pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
}
.shell {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
min-height: 100vh;
}
aside {
position: sticky;
top: 0;
height: 100vh;
overflow: auto;
padding: 24px 18px;
border-right: 1px solid var(--line);
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(16px);
}
main {
width: min(1180px, calc(100vw - 320px));
padding: 34px 40px 72px;
}
.brand {
margin-bottom: 18px;
padding-bottom: 18px;
border-bottom: 1px solid var(--line);
}
.brand h1 {
margin: 0 0 8px;
font-size: 18px;
line-height: 1.25;
letter-spacing: 0;
}
.brand p {
margin: 0;
color: var(--muted);
font-size: 13px;
}
nav a {
display: block;
padding: 8px 10px;
margin: 2px 0;
border-radius: 8px;
color: #334155;
text-decoration: none;
font-size: 13px;
}
nav a:hover,
nav a:focus {
color: var(--blue);
background: #eff6ff;
outline: none;
}
.toolbar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
margin-top: 18px;
}
.search {
width: 100%;
min-height: 42px;
padding: 0 14px;
border: 1px solid var(--line);
border-radius: 10px;
background: #fff;
color: var(--ink);
font-size: 14px;
outline: none;
}
.search:focus {
border-color: rgba(37, 99, 235, 0.55);
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 0 14px;
border: 1px solid var(--line);
border-radius: 10px;
background: #fff;
color: #334155;
font-size: 13px;
cursor: pointer;
}
.button:hover { border-color: #94a3b8; color: var(--ink); }
.hero {
padding: 28px;
border: 1px solid var(--line);
border-radius: 18px;
background: var(--panel);
box-shadow: var(--shadow);
}
.hero .eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
color: var(--blue);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero h2 {
margin: 0 0 12px;
max-width: 980px;
font-size: clamp(30px, 5vw, 54px);
line-height: 1.04;
letter-spacing: 0;
}
.hero p {
max-width: 880px;
margin: 0;
color: #475569;
font-size: 16px;
}
.meta-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.meta {
padding: 13px;
border: 1px solid var(--line);
border-radius: 12px;
background: #f8fafc;
}
.meta b {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #334155;
}
.meta span {
color: var(--muted);
font-size: 12px;
word-break: break-word;
}
section {
margin-top: 28px;
padding: 24px;
border: 1px solid var(--line);
border-radius: 18px;
background: rgba(255, 255, 255, 0.86);
box-shadow: 0 10px 35px rgba(15, 23, 42, 0.045);
}
section h2 {
margin: 0 0 12px;
font-size: 24px;
letter-spacing: 0;
}
section > p {
margin-top: 0;
color: #475569;
}
.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.card {
padding: 16px;
border: 1px solid var(--line);
border-radius: 14px;
background: #fff;
}
.card h3 {
margin: 0 0 8px;
font-size: 16px;
}
.card p,
.card li {
color: #475569;
font-size: 14px;
}
.card p:last-child { margin-bottom: 0; }
.tag {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0 8px;
border-radius: 999px;
background: #e2e8f0;
color: #334155;
font-size: 12px;
font-weight: 650;
}
.tag.blue { background: #dbeafe; color: #1d4ed8; }
.tag.violet { background: #ede9fe; color: #6d28d9; }
.tag.orange { background: #ffedd5; color: #c2410c; }
.tag.green { background: #d1fae5; color: #047857; }
.tag.rose { background: #ffe4e6; color: #be123c; }
.tag.gray { background: #e2e8f0; color: #475569; }
.pipeline {
display: grid;
grid-template-columns: repeat(8, minmax(120px, 1fr));
gap: 10px;
overflow-x: auto;
padding-bottom: 4px;
}
.step {
min-width: 138px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 14px;
background: #fff;
}
.step .num {
width: 26px;
height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
border-radius: 8px;
background: #eff6ff;
color: var(--blue);
font-weight: 800;
font-size: 12px;
}
.step h3 {
margin: 0 0 6px;
font-size: 14px;
}
.step p {
margin: 0;
color: #64748b;
font-size: 12px;
line-height: 1.45;
}
table {
width: 100%;
border-collapse: collapse;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 12px;
background: #fff;
}
th,
td {
padding: 11px 12px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
font-size: 13px;
}
th {
background: #f8fafc;
color: #334155;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
tr:last-child td { border-bottom: 0; }
td code {
color: #0f172a;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 2px 5px;
font-size: 12px;
}
pre {
margin: 12px 0 0;
padding: 14px;
overflow: auto;
border-radius: 12px;
background: var(--code);
color: #e5e7eb;
font-size: 12px;
line-height: 1.55;
}
.callout {
padding: 14px 16px;
border-radius: 14px;
border: 1px solid #bfdbfe;
background: #eff6ff;
color: #1e3a8a;
}
.callout.warn {
border-color: #fed7aa;
background: #fff7ed;
color: #9a3412;
}
.callout.good {
border-color: #bbf7d0;
background: #f0fdf4;
color: #166534;
}
.callout p { margin: 0; }
.flow {
display: grid;
gap: 10px;
}
.flow-row {
display: grid;
grid-template-columns: 170px minmax(0, 1fr) minmax(0, 1fr);
gap: 10px;
align-items: stretch;
}
.flow-row > div {
padding: 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: #fff;
font-size: 13px;
}
.flow-row strong { display: block; margin-bottom: 4px; }
.flow-row span { color: var(--muted); }
.todo {
display: grid;
gap: 10px;
counter-reset: todo;
}
.todo-item {
position: relative;
padding: 14px 14px 14px 46px;
border: 1px solid var(--line);
border-radius: 14px;
background: #fff;
}
.todo-item::before {
counter-increment: todo;
content: counter(todo);
position: absolute;
left: 14px;
top: 14px;
width: 22px;
height: 22px;
display: grid;
place-items: center;
border-radius: 7px;
background: #f1f5f9;
color: #475569;
font-size: 12px;
font-weight: 800;
}
.todo-item h3 { margin: 0 0 4px; font-size: 14px; }
.todo-item p { margin: 0; color: var(--muted); font-size: 13px; }
.changelog {
display: grid;
gap: 12px;
}
.change {
border: 1px solid var(--line);
border-radius: 14px;
background: #fff;
overflow: hidden;
}
.change header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
background: #f8fafc;
}
.change h3 { margin: 0; font-size: 14px; }
.change .body { padding: 14px; }
.change .body p { margin: 0 0 8px; color: #475569; font-size: 13px; }
.change .body p:last-child { margin-bottom: 0; }
.hidden-by-search { display: none !important; }
@media (max-width: 980px) {
.shell { display: block; }
aside {
position: static;
height: auto;
border-right: 0;
border-bottom: 1px solid var(--line);
}
main {
width: 100%;
padding: 24px 18px 56px;
}
.meta-grid,
.grid-2,
.grid-3,
.grid-4,
.flow-row {
grid-template-columns: 1fr;
}
}
@media print {
aside, .toolbar { display: none; }
.shell { display: block; }
main { width: 100%; padding: 0; }
section, .hero { box-shadow: none; break-inside: avoid; }
body { background: #fff; }
}
</style>
</head>
<body>
<div class="shell">
<aside>
<div class="brand">
<h1>SKG TK 二创验证</h1>
<p>源码解析与协作地图。用于把产品需求准确映射到代码位置。</p>
</div>
<nav aria-label="页面目录">
<a href="#purpose">为什么有这页</a>
<a href="#how-to-use">怎么用它描述需求</a>
<a href="#runtime">运行与入口</a>
<a href="#pipeline">业务管线</a>
<a href="#source-map">源码结构地图</a>
<a href="#ui-map">界面区域到源码</a>
<a href="#data-model">数据模型</a>
<a href="#api-map">接口地图</a>
<a href="#node-contract">节点职责边界</a>
<a href="#current-state">当前已通与阻塞</a>
<a href="#request-language">需求描述模板</a>
<a href="#change-log">变更记录</a>
<a href="#update-rule">更新规则</a>
</nav>
<div class="toolbar">
<input id="search" class="search" type="search" placeholder="搜索:节点 / 文件 / 接口 / 功能" />
<button class="button" type="button" onclick="window.print()">打印</button>
</div>
</aside>
<main>
<div class="hero" id="purpose" data-search>
<div class="eyebrow">Source Analysis · 2026-05-13</div>
<h2>这个页面是产品协作地图,不是应用功能页。</h2>
<p>
它把“你看到的界面、你想改的功能、实际要动的源码、可能影响的数据和接口”放在同一个地方。
后续描述需求时,可以直接说“改源码地图里的某个区域 / 某个节点职责 / 某个接口行为”,这样改动范围会更准,也更容易追踪每次变更带来的影响。
</p>
<div class="meta-grid">
<div class="meta"><b>项目路径</b><span>/Users/kangwan/Projects/business/20260512-20260512-skg-tk-二创验证</span></div>
<div class="meta"><b>前端</b><span>Next.js 16 · 端口 4290 · web/app/page.tsx</span></div>
<div class="meta"><b>后端</b><span>FastAPI · 端口 4291 · api/main.py</span></div>
<div class="meta"><b>本文档位置</b><span>docs/source-analysis.html · 独立文件,不接入应用</span></div>
</div>
</div>
<section id="how-to-use" data-search>
<h2>怎么用它描述需求</h2>
<div class="grid-3">
<div class="card">
<h3>1. 先说你在改哪个产品区</h3>
<p>例如“镜头拆解 / 元素提取面板”、“元素改造 Storyboard 节点”、“分镜头编排下拉区 4 图槽”。不要只说“这里乱”,要指向页面里的功能区。</p>
</div>
<div class="card">
<h3>2. 再说这个区应该承担什么职责</h3>
<p>例如“Vision 只给候选元素,用户必须能编辑、删除、重新提取”,这会直接落到 <code>FrameLightbox</code> 和元素接口。</p>
</div>
<div class="card">
<h3>3. 最后说不希望发生什么</h3>
<p>例如“点击元素不要跳页面”、“不要直接进入编排打断思路”、“不要把参考视频复刻成一样的东西”。这能约束交互和文案。</p>
</div>
</div>
<div class="callout good" style="margin-top:14px">
<p>建议表达格式:我要改「功能区」;当前问题是「行为」;正确职责是「业务目的」;不要影响「已有流程」。</p>
</div>
</section>
<section id="runtime" data-search>
<h2>运行与入口</h2>
<table>
<thead>
<tr><th>项目</th><th>命令 / 入口</th><th>说明</th></tr>
</thead>
<tbody>
<tr>
<td>前端开发服务</td>
<td><code>cd web && pnpm dev</code></td>
<td>Next.js App Router主页面是 <code>web/app/page.tsx</code>,默认端口 4290。</td>
</tr>
<tr>
<td>后端开发服务</td>
<td><code>cd api && source .venv/bin/activate && uvicorn main:app --port 4291 --reload</code></td>
<td>FastAPI所有任务状态、视频、关键帧、清洗、元素、分镜保存都在 <code>api/main.py</code></td>
</tr>
<tr>
<td>测试页面</td>
<td><code>http://localhost:4290/?job=c6767f3a166b</code></td>
<td>URL 里可放多个 job id<code>?job=id1,id2,id3</code>,前端会恢复多个任务并激活最后一个。</td>
</tr>
<tr>
<td>源码解析页</td>
<td><code>open docs/source-analysis.html</code></td>
<td>独立静态 HTML不被 Next 构建、不影响产品工作台。</td>
</tr>
</tbody>
</table>
</section>
<section id="pipeline" data-search>
<h2>业务管线</h2>
<p>当前产品不是“复制别人的视频”,而是拆解参考视频,提取可借鉴的镜头元素,再改造成 SKG 产品语境的视频素材。</p>
<div class="pipeline">
<div class="step"><div class="num">1</div><h3>输入</h3><p>TK 链接或本地上传,后端下载/保存源视频。</p></div>
<div class="step"><div class="num">2</div><h3>镜头拆解</h3><p>拆轨、抽关键帧、手动加帧,形成参考分镜池。</p></div>
<div class="step"><div class="num">3</div><h3>清洗水印</h3><p>对关键帧做全图或区域清洗,必要时应用为当前参考图。</p></div>
<div class="step"><div class="num">4</div><h3>Vision 识别</h3><p>识别场景和候选元素,只是候选,不应锁死。</p></div>
<div class="step"><div class="num">5</div><h3>元素提取</h3><p>编辑/新增/删除元素,对元素反复生成提取图。</p></div>
<div class="step"><div class="num">6</div><h3>元素改造</h3><p>把参考主体、场景、动作和 SKG 产品放入分镜结构。</p></div>
<div class="step"><div class="num">7</div><h3>生成视频</h3><p>用分镜 4 图槽、改造目标和时长调用 Seedance / Kling / Veo 3 生视频 API结果回写到 Video Gen 节点。</p></div>
<div class="step"><div class="num">8</div><h3>合成成品</h3><p>片段、字幕、配音、转场合成最终 mp4。当前未实现。</p></div>
</div>
</section>
<section id="source-map" data-search>
<h2>源码结构地图</h2>
<div class="grid-2">
<div class="card">
<h3>前端核心</h3>
<table>
<tbody>
<tr><td><code>web/app/page.tsx</code></td><td>产品工作台主状态jobs、activeJobId、selectedFrames、clipboard、ReactFlow 节点和边。</td></tr>
<tr><td><code>web/components/nodes/index.tsx</code></td><td>DAG 节点定义Input、Keyframe、ASR、Translate、Rewrite、Storyboard、VideoGen、Compose。</td></tr>
<tr><td><code>web/components/lightbox.tsx</code></td><td>镜头拆解和元素提取的主工作面板:清洗、识别、元素编辑、区域提取、抠图。</td></tr>
<tr><td><code>web/components/storyboard-bar.tsx</code></td><td>顶部分镜编排条:展示选入编排的关键帧,并作为唯一分镜导航。</td></tr>
<tr><td><code>web/components/storyboard-workbench.tsx</code></td><td>顶部分镜编排条下方的明细区4 图槽、改造目标、时长、自动保存。</td></tr>
<tr><td><code>web/lib/api.ts</code></td><td>前端类型和 API client是前后端数据契约镜像。</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h3>后端核心</h3>
<table>
<tbody>
<tr><td><code>api/main.py</code></td><td>FastAPI 单文件后端状态模型、任务恢复、下载、抽帧、Vision、清洗、元素、分镜、文件返回。</td></tr>
<tr><td><code>jobs/&lt;jobId&gt;/state.json</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;/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;/gen</code></td><td>关键帧生图结果。</td></tr>
</tbody>
</table>
</div>
</div>
<pre>前端主链路:
web/app/page.tsx
-> ReactFlow 节点web/components/nodes/index.tsx
-> 画布内镜头拆解面板KeyframeNode 内嵌 web/components/lightbox.tsx
-> 顶部分镜条web/components/storyboard-bar.tsx
-> 分镜工作台web/components/storyboard-workbench.tsx
-> API 契约web/lib/api.ts
后端主链路:
api/main.py
-> Job / KeyFrame / KeyElement / StoryboardScene
-> 下载 / 上传 / 抽帧 / Vision / 清洗 / 元素提取 / 分镜保存
-> jobs/&lt;jobId&gt;/state.json + 图片文件落盘</pre>
</section>
<section id="ui-map" data-search>
<h2>界面区域到源码</h2>
<div class="flow">
<div class="flow-row">
<div><strong>你看到的区域</strong><span>Input 节点和视频缩略图</span></div>
<div><strong>主要源码</strong><span><code>InputNode</code> in <code>web/components/nodes/index.tsx</code>;状态处理在 <code>page.tsx</code></span></div>
<div><strong>适合怎么描述</strong><span>“输入/下载/视频就绪这个节点应该如何提示用户下一步”。</span></div>
</div>
<div class="flow-row">
<div><strong>你看到的区域</strong><span>镜头拆解节点上方关键帧</span></div>
<div><strong>主要源码</strong><span><code>KeyframeNode</code> 内嵌 <code>FrameLightbox</code>;后端 <code>/frames</code><code>/describe</code><code>/cleanup</code></span></div>
<div><strong>适合怎么描述</strong><span>“关键帧详情面板在无限画布上怎么展示、缩放、跟随,以及清洗/识别/元素提取的操作顺序”。</span></div>
</div>
<div class="flow-row">
<div><strong>你看到的区域</strong><span>元素列表和提取图</span></div>
<div><strong>主要源码</strong><span><code>FrameLightbox</code>;类型 <code>KeyElement</code>;接口 <code>addElement/updateElement/deleteElement/cutoutElement/deleteCutout</code></span></div>
<div><strong>适合怎么描述</strong><span>“Vision 识别出来的是候选,用户要能修正、重复提取、删除错误元素”。</span></div>
</div>
<div class="flow-row">
<div><strong>你看到的区域</strong><span>元素改造 · Storyboard 节点</span></div>
<div><strong>主要源码</strong><span><code>StoryboardNode</code>;上方元素缩略图来自所有已提取 cutouts。</span></div>
<div><strong>适合怎么描述</strong><span>“这里是素材入口,不是最终视频编辑器;点击是否进入工作台要不要打断当前任务”。</span></div>
</div>
<div class="flow-row">
<div><strong>你看到的区域</strong><span>顶部分镜条</span></div>
<div><strong>主要源码</strong><span><code>StoryboardBar</code>;只展示 selectedFrames不负责提取元素。</span></div>
<div><strong>适合怎么描述</strong><span>“选好的分镜如何按时间序组织,以及如何进入具体分镜编排”。</span></div>
</div>
<div class="flow-row">
<div><strong>你看到的区域</strong><span>顶部分镜头编排下拉面板</span></div>
<div><strong>主要源码</strong><span><code>StoryboardWorkbench</code>;保存到 <code>frame.storyboard</code>;接口 <code>PUT /storyboard</code></span></div>
<div><strong>适合怎么描述</strong><span>“每个分镜需要哪些图片槽、哪些改造说明,如何为视频生成做准备”。</span></div>
</div>
</div>
</section>
<section id="data-model" data-search>
<h2>数据模型</h2>
<div class="grid-2">
<div class="card">
<h3>Job</h3>
<p>一个视频任务。前端维护多个 <code>jobs[]</code>,当前激活的是 <code>activeJobId</code>。URL 查询参数会持久化多个 job。</p>
<pre>Job {
id, url, status, progress, message,
video_url, duration, width, height,
frames: KeyFrame[],
transcript: TranscriptSegment[],
storyboard_images?: StoryboardImage[]
}</pre>
</div>
<div class="card">
<h3>KeyFrame</h3>
<p>关键帧是整个产品的核心单位。<code>index</code> 是稳定 ID手动加帧后不连续不能用数组下标代替。</p>
<pre>KeyFrame {
index, timestamp, url,
description,
cleaned_url, cleaned_applied,
elements: KeyElement[],
storyboard: StoryboardScene,
generated_images: GeneratedImage[]
}</pre>
</div>
<div class="card">
<h3>KeyElement</h3>
<p>从关键帧里识别或手动添加的可借鉴元素。Vision 给的是候选,用户可编辑,并可多次生成提取图。</p>
<pre>KeyElement {
id,
name_zh, name_en, position,
source: auto | manual | region,
region,
cutouts: string[],
cutout_id
}</pre>
</div>
<div class="card">
<h3>StoryboardScene</h3>
<p>分镜编排结果,不是复刻说明。它把参考图和 SKG 改造方向绑定到一个分镜上。</p>
<pre>StoryboardScene {
duration,
subject_image,
scene_image,
product_image,
action_image,
subject,
product,
scene,
action
}</pre>
</div>
</div>
</section>
<section id="api-map" data-search>
<h2>接口地图</h2>
<table>
<thead>
<tr><th>功能</th><th>接口</th><th>前端调用</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载,停在 downloaded 等用户点解析。</td></tr>
<tr><td>上传视频</td><td><code>POST /jobs/upload</code></td><td><code>uploadJob</code></td><td>保存 source.mp4然后同样进入下载完成状态。</td></tr>
<tr><td>解析视频</td><td><code>POST /jobs/{id}/analyze</code></td><td><code>analyzeJob</code></td><td>拆轨 + 抽关键帧。当前不自动跑 ASR避免 audio 阻塞视觉管线。</td></tr>
<tr><td>手动加帧</td><td><code>POST /jobs/{id}/frames?t=</code></td><td><code>addManualFrame</code></td><td>按视频时间戳抽一帧index 递增但 frames 按 timestamp 排序。</td></tr>
<tr><td>Vision 识别</td><td><code>POST /frames/{idx}/describe</code></td><td><code>describeFrame</code></td><td>写入 frame.description后续可从 objects 加候选元素。</td></tr>
<tr><td>清洗水印</td><td><code>POST /frames/{idx}/cleanup</code></td><td><code>cleanupFrame</code></td><td>支持全图和区域清洗,生成 cleaned 待应用版本。</td></tr>
<tr><td>应用清洗</td><td><code>POST /cleanup/apply</code></td><td><code>applyCleanedFrame</code></td><td>物理覆盖 frames/{idx}.jpg并备份原图。</td></tr>
<tr><td>元素增改删</td><td><code>POST/PATCH/DELETE /elements</code></td><td><code>addElement/updateElement/deleteElement</code></td><td>让用户修正 Vision 错误,避免候选结果锁死。</td></tr>
<tr><td>元素提取</td><td><code>POST /elements/{element_id}/cutout</code></td><td><code>cutoutElement</code></td><td>调用图像模型生成独立白底素材图,每次累积一张 cutout。</td></tr>
<tr><td>分镜保存</td><td><code>PUT /frames/{idx}/storyboard</code></td><td><code>updateStoryboard</code></td><td>保存 4 图槽、时长和改造说明。</td></tr>
<tr><td>生图</td><td><code>POST /frames/{idx}/generate</code></td><td><code>generateImage</code></td><td>基于关键帧或已选生成图做 image-to-image目前可用。</td></tr>
</tbody>
</table>
</section>
<section id="node-contract" data-search>
<h2>节点职责边界</h2>
<table>
<thead>
<tr><th>节点</th><th>当前职责</th><th>不该承担</th><th>改动主要文件</th></tr>
</thead>
<tbody>
<tr>
<td><span class="tag blue">输入 Input</span></td>
<td>创建/上传任务,显示视频就绪,触发解析。</td>
<td>不要自动一路跑到 ASR 或生图;用户需要控制解析节奏。</td>
<td><code>page.tsx</code><code>InputNode</code><code>api/main.py</code></td>
</tr>
<tr>
<td><span class="tag orange">镜头拆解 / 元素提取</span></td>
<td>关键帧选择、清洗、Vision 候选、元素编辑、区域提取、元素 cutout。</td>
<td>不要把 Vision 结果当最终结论;不要点击元素就跳走。</td>
<td><code>KeyframeNode</code><code>FrameLightbox</code>、元素接口</td>
</tr>
<tr>
<td><span class="tag violet">元素改造 Storyboard</span></td>
<td>展示可用元素素材,承接“参考元素 → SKG 画面”的入口。</td>
<td>不要等同于最终视频生成;不要暗示复刻原视频。</td>
<td><code>StoryboardNode</code><code>StoryboardBar</code></td>
</tr>
<tr>
<td><span class="tag violet">分镜工作台</span></td>
<td>每个分镜填 4 图槽和改造 brief为后续生成首帧/视频片段做准备。</td>
<td>当前不负责真实调用视频模型。</td>
<td><code>StoryboardWorkbench</code><code>updateStoryboard</code></td>
</tr>
<tr>
<td><span class="tag gray">ASR / Translate / Rewrite</span></td>
<td>未来的文案轨,目前部分占位或受 audio 阻塞。</td>
<td>不要阻断视觉素材管线。</td>
<td><code>ASRNode</code><code>TranslateNode</code><code>RewriteNode</code>、ASR 接口</td>
</tr>
<tr>
<td><span class="tag green">Video Gen / Compose</span></td>
<td>承载生视频任务状态和完成后的 MP4。</td>
<td>分镜工作台提交任务Video Gen 节点只展示任务和结果。</td>
<td><code>VideoGenNode</code><code>/storyboard/video</code><code>generated_videos</code></td>
</tr>
</tbody>
</table>
</section>
<section id="current-state" data-search>
<h2>当前已通与阻塞</h2>
<div class="grid-2">
<div class="card">
<h3>已通</h3>
<ul>
<li>TK 链接 / 上传创建 job。</li>
<li>视频下载或本地保存ffmpeg 抽关键帧。</li>
<li>手动按时间戳加关键帧。</li>
<li>关键帧清洗水印,全图或区域清洗。</li>
<li>Vision 识别关键帧,输出 scene、objects、style、suggested_prompt。</li>
<li>元素增改删、区域元素添加、元素多次提取图。</li>
<li>分镜工作台 4 图槽和改造说明自动保存。</li>
<li>nano-banana-pro image-to-image 生图。</li>
</ul>
</div>
<div class="card">
<h3>阻塞 / 占位</h3>
<ul>
<li>ASRSKG 网关 audio endpoint 404 或渠道不可用。</li>
<li>Translate本身 text 通,但产品流里依赖 ASR 段落。</li>
<li>Rewrite需要 SKG 产品信息模板和目标脚本结构。</li>
<li>Video Gen模型层按业务保留 Seedance / Kling / Veo/Voe 选择;后端已支持 Poe 视频通道,别名默认映射到 <code>seedance-2-fast</code><code>kling-omni</code><code>veo-3.1-fast</code>,提交后写入 Video Gen 节点。</li>
<li>Compose还没做本地 ffmpeg 字幕/TTS 合成。</li>
</ul>
</div>
</div>
<div class="callout warn" style="margin-top:14px">
<p>最重要的产品判断:当前视觉素材管线已经能继续推进,文案/音频/视频生成不要再反过来卡住镜头拆解和元素改造。</p>
</div>
</section>
<section id="request-language" data-search>
<h2>需求描述模板</h2>
<div class="todo">
<div class="todo-item">
<h3>改镜头拆解 / 元素提取</h3>
<p>“我在关键帧 lightbox 里Vision 识别后的元素列表应该怎么编辑/重提取/删除;点击元素不要跳转;提取图怎么预览和复制。”</p>
</div>
<div class="todo-item">
<h3>改 Storyboard 节点</h3>
<p>“我在 DAG 的元素改造节点它上方缩略图展示什么、hover 预览什么、点击后是否进入工作台、是否自动选中对应分镜。”</p>
</div>
<div class="todo-item">
<h3>改分镜工作台</h3>
<p>“我在顶部分镜头编排下拉面板,每个分镜需要哪些槽位、字段如何命名、保存后如何传给后续生成视频。”</p>
</div>
<div class="todo-item">
<h3>改数据/接口</h3>
<p>“这个动作需要持久化到 state.json字段加在 Job/KeyFrame/KeyElement/StoryboardScene 哪一层,刷新后要恢复。”</p>
</div>
<div class="todo-item">
<h3>改节点语义</h3>
<p>“这个节点的业务职责要改,不只是 UI 文案请同步更新节点标题、subtitle、说明、可点击行为、状态推导和本源码解析页。”</p>
</div>
</div>
</section>
<section id="change-log" data-search>
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-13 · 首尾帧编排增加 SKG 产品槽</h3>
<span class="tag violet">StoryboardWorkbench</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>首尾帧可以控制视频起止,但还需要单独指定 SKG 产品图,避免模型只模仿原视频动作而没有稳定产品外观。</p>
<p><strong>改动:</strong>分镜编排区新增 <code>SKG 产品</code> 槽,和首帧、尾帧并列;生成视频时把该槽作为 <code>product_image</code> 提交。Ark 请求会附加一张 <code>reference_image</code> 产品参考图;如果接口不接受额外参考图,后端自动回退到首尾帧生成。</p>
<p><strong>影响:</strong><code>web/components/storyboard-workbench.tsx</code><code>web/app/page.tsx</code><code>api/main.py</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 分镜编排改为首尾帧生成</h3>
<span class="tag violet">StoryboardWorkbench</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>赶交付时顶部横向分镜缩略条占空间4 图槽也不如“首帧到尾帧”直接;用户希望直接做首尾帧视频生成。</p>
<p><strong>改动:</strong>移除 <code>StoryboardBar</code> 的横向分镜缩略图区域,只保留标题栏和展开按钮;<code>StoryboardWorkbench</code> 改成首帧 / 尾帧两个槽,首帧默认当前分镜,尾帧默认下一张已选分镜,也可从剪贴板粘贴指定结束画面。后端 <code>/storyboard/video</code> 支持 <code>first_image</code>/<code>last_image</code>Ark 请求同时传 <code>first_frame</code>/<code>last_frame</code>,如果接口不接受尾帧字段则自动回退到单首帧。</p>
<p><strong>影响:</strong><code>web/components/storyboard-bar.tsx</code><code>web/components/storyboard-workbench.tsx</code><code>web/app/page.tsx</code><code>web/lib/api.ts</code><code>api/main.py</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 生视频携带原视频链接做节奏参考</h3>
<span class="tag violet">StoryboardWorkbench</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>用户赶交付,希望直接把上传的原视频链接给视频模型参考,而不是只靠单张关键帧。</p>
<p><strong>改动:</strong>前端提交生视频时增加 <code>source_ref: { kind: "source_video", url: job.url }</code>Ark 请求体在文本 prompt 和首帧之外追加 <code>video_url</code> 参考视频,用于模仿节奏、镜头运动和动作顺序。如果 Ark 返回 400/422 不接受参考视频字段,后端自动回退到“当前关键帧首帧生成”,保证这次不会直接阻断出片。</p>
<p><strong>影响:</strong><code>web/app/page.tsx</code><code>web/lib/api.ts</code><code>api/main.py</code><code>docs/source-analysis.html</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 快速出片改为关键帧直生视频</h3>
<span class="tag violet">StoryboardWorkbench</span>
<span class="tag blue">Prompt</span>
</header>
<div class="body">
<p><strong>问题:</strong>赶交付时不适合再让 4 图槽决定首帧;如果某个槽里是抠图元素,模型会拿碎元素当第一帧,视频容易不连贯。</p>
<p><strong>改动:</strong>“生成视频”按钮改成直接用当前分镜关键帧作为首帧提交4 图槽和改造目标只作为提示词参考;提示词强调一镜到底、首帧稳定、时间线连续、禁止跳切/换场景/主体变形。后端取关键帧时优先使用未应用的清洗版,否则使用当前 frame 文件。</p>
<p><strong>影响:</strong><code>web/app/page.tsx</code><code>web/components/storyboard-workbench.tsx</code><code>api/main.py</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 生视频支持火山方舟 Ark 异步任务</h3>
<span class="tag rose">VideoGenNode</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>用户提供火山方舟 <code>https://ark.cn-beijing.volces.com/api/v3</code> 作为生视频通道;这个通道不是 Poe 的 <code>/videos</code> 形态,而是内容生成异步任务。</p>
<p><strong>改动:</strong>后端识别 Ark base 后,提交改为 <code>POST /contents/generations/tasks</code>,请求体使用 <code>content</code> 数组:文本 prompt + 首帧 <code>image_url</code> data URL轮询改为 <code>GET /contents/generations/tasks/{id}</code>,成功后读取 <code>content.video_url</code> 下载 MP4。本机默认 Seedance 模型改为 Ark 可见的 <code>doubao-seedance-2-0-fast-260128</code></p>
<p><strong>影响:</strong><code>api/main.py</code><code>api/.env.example</code><code>docs/source-analysis.html</code>。本机 <code>api/.env</code> 需要把 <code>VIDEO_API_BASE_URL</code>/<code>VIDEO_API_KEY</code>/<code>VIDEO_CREATE_PATHS</code>/<code>VIDEO_STATUS_PATH</code> 指向 Ark。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 生视频改接 Poe 视频模型</h3>
<span class="tag rose">VideoGenNode</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>SKG ezlink 的 OpenAI 兼容 base 可列出部分模型,但常规 <code>/videos</code> 入口返回 404/unsupported用户确认可用的视频模型在 Poe 通道里。</p>
<p><strong>改动:</strong>后端新增 <code>POE_API_BASE_URL</code>/<code>POE_API_KEY</code> 配置,未显式配置 <code>VIDEO_API_BASE_URL</code> 时优先走 PoeSeedance / Kling / Veo/Voe 业务别名默认映射到 Poe 真实模型 <code>seedance-2-fast</code><code>kling-omni</code><code>veo-3.1-fast</code>。Poe 提交使用 <code>input_image</code> base64继续轮询 <code>/videos/{id}</code> 并下载 <code>/videos/{id}/content</code></p>
<p><strong>影响:</strong><code>api/main.py</code><code>api/.env.example</code><code>docs/source-analysis.html</code>。密钥只放本地 <code>api/.env</code>,不进入源码解析页。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 生视频提交不再被前端锁死</h3>
<span class="tag violet">StoryboardWorkbench</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>虽然当前探测到常见视频入口返回 404/unsupported但模型层确实有视频模型不能在前端简单判定“未开通”并禁用。</p>
<p><strong>改动:</strong>撤掉分镜编排里的前置禁用;后端允许提交 seedance / kling / veo / voe并支持通过 <code>VIDEO_CREATE_PATHS</code> 逗号分隔配置多个候选生成入口,逐个尝试。</p>
<p><strong>影响:</strong><code>api/main.py</code><code>api/.env.example</code><code>web/app/page.tsx</code><code>web/components/storyboard-workbench.tsx</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 生视频错误提示改为可读原因</h3>
<span class="tag rose">VideoGenNode</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>提交生视频失败时,前端把 <code>generateStoryboardVideo 503 {"detail": ...}</code> 原样展示,用户无法快速判断是配置、端点还是 UI 问题。</p>
<p><strong>改动:</strong><code>generateStoryboardVideo</code> 解析后端 JSON 的 <code>detail</code> 后再抛错后端错误文案区分“模型存在”和“入口不可用”Video Gen 失败卡把 <code>/videos 404</code> 长错误压缩成一句可读原因。</p>
<p><strong>影响:</strong><code>web/lib/api.ts</code><code>web/components/nodes/index.tsx</code><code>api/main.py</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · Video Gen 卡片增加复制和删除</h3>
<span class="tag rose">VideoGenNode</span>
<span class="tag blue">API</span>
</header>
<div class="body">
<p><strong>问题:</strong>Video Gen 节点上方失败/完成任务卡只有整卡点击复制,不够明确;失败任务也无法从界面清掉。</p>
<p><strong>改动:</strong>每张视频任务卡左上角增加复制 prompt 按钮,右上角增加删除任务按钮;后端新增 <code>DELETE /jobs/{job_id}/storyboard-videos/{video_id}</code>,删除 <code>generated_videos</code> 记录并清理本地任务目录。</p>
<p><strong>影响:</strong><code>web/components/nodes/index.tsx</code><code>web/app/page.tsx</code><code>web/lib/api.ts</code><code>api/main.py</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 分镜编排接入真实生视频任务</h3>
<span class="tag violet">StoryboardWorkbench</span>
<span class="tag rose">VideoGenNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>4 图槽已经粘贴参考图后,用户要直接调用生视频 API而不是只生成 prompt 或图片任务。</p>
<p><strong>改动:</strong>分镜编排明细区增加 Seedance / Kling / Veo 3 模型选择和“调用模型生成视频”按钮;后端新增 <code>/jobs/{job_id}/frames/{idx}/storyboard/video</code>。提交后按 <code>VIDEO_CREATE_PATHS</code> 逐个尝试生成入口,成功后轮询并保存 MP4失败时保留任务卡和具体入口错误方便继续排查网关实际路径。<code>VideoGenNode</code> 读取 <code>job.generated_videos</code> 展示排队、生成中、失败和完成视频。</p>
<p><strong>影响:</strong><code>api/main.py</code><code>api/.env.example</code><code>web/components/storyboard-workbench.tsx</code><code>web/components/nodes/index.tsx</code><code>web/app/page.tsx</code><code>web/lib/api.ts</code>。Sora 不再作为默认模型;真实模型 ID 通过 <code>VIDEO_MODEL_SEEDANCE</code><code>VIDEO_MODEL_KLING</code><code>VIDEO_MODEL_VEO3</code> 配置,真实视频 API 地址通过 <code>VIDEO_API_BASE_URL</code>/<code>VIDEO_API_KEY</code> 配置。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 分镜编排下拉区支持上推缩小</h3>
<span class="tag violet">StoryboardWorkbench</span>
</header>
<div class="body">
<p><strong>问题:</strong>分镜编排明细区默认占用太多顶部面积,展开后下方画布空间不足。</p>
<p><strong>改动:</strong>明细区默认高度降为 320px并增加底部拖拽手柄可上推缩到 180px也可下拉放大查看完整内容。</p>
<p><strong>影响:</strong><code>web/components/storyboard-workbench.tsx</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 分镜缩略图条与编排明细合并为一个下拉区</h3>
<span class="tag violet">StoryboardBar</span>
<span class="tag violet">StoryboardWorkbench</span>
</header>
<div class="body">
<p><strong>问题:</strong>顶部分镜缩略图条和下方内嵌工作台都带分镜导航,看起来像两个不同板块。</p>
<p><strong>改动:</strong><code>StoryboardBar</code> 成为唯一分镜导航;<code>StoryboardWorkbench</code> 移除自己的标题栏、左侧分镜列表和底部快捷栏,只保留当前分镜的 4 图槽与改造目标明细。</p>
<p><strong>影响:</strong><code>web/components/storyboard-bar.tsx</code><code>web/components/storyboard-workbench.tsx</code><code>web/app/page.tsx</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 分镜头编排工作台改为内嵌下拉</h3>
<span class="tag violet">StoryboardWorkbench</span>
<span class="tag violet">StoryboardBar</span>
</header>
<div class="body">
<p><strong>问题:</strong>元素改造节点等入口仍会打开 <code>fixed inset-0</code> 的全屏 <code>StoryboardWorkbench</code>,用户感觉像跳转页面。</p>
<p><strong>改动:</strong>移除 <code>StoryboardWorkbench</code> 的 portal 全屏承载方式,改为渲染在顶部分镜栏下方;所有“打开编排”入口只展开这个内嵌区域。</p>
<p><strong>影响:</strong><code>web/components/storyboard-workbench.tsx</code><code>web/components/storyboard-bar.tsx</code><code>web/app/page.tsx</code><code>web/components/nodes/index.tsx</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 钉住面板停靠到分镜头编排边缘</h3>
<span class="tag orange">KeyframePanelNode</span>
<span class="tag violet">StoryboardBar</span>
</header>
<div class="body">
<p><strong>问题:</strong>关键帧详情钉住在浏览器左侧固定位置时,会遮挡顶部分镜头编排栏展开后的缩略图区域。</p>
<p><strong>改动:</strong><code>StoryboardBar</code> 增加稳定 DOM 标记;钉住面板实时读取该区域下边缘,并吸附到其下方。展开 / 折叠分镜头编排时,钉住面板自动让位。</p>
<p><strong>影响:</strong><code>web/components/storyboard-bar.tsx</code><code>web/components/nodes/index.tsx</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 顶部分镜头编排不再跳转全屏工作台</h3>
<span class="tag violet">StoryboardBar</span>
</header>
<div class="body">
<p><strong>问题:</strong>顶部 <code>StoryboardBar</code> 的“进入编排”和分镜缩略图点击会打开全屏 <code>StoryboardWorkbench</code>,打断当前画布流程。</p>
<p><strong>改动:</strong>顶部按钮改为“展开编排”,只下拉展示当前分镜列表;缩略图点击只聚焦该分镜,不再触发全屏跳转。后续已把工作台整体改成内嵌下拉,见上方最新记录。</p>
<p><strong>影响:</strong><code>web/components/storyboard-bar.tsx</code><code>web/app/page.tsx</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 钉住关键帧详情改为左侧停靠</h3>
<span class="tag orange">KeyframePanelNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>钉住后仍像自由浮层一样停在画布附近,用户继续缩放画布或调整面板尺寸时容易把它和画布节点混在一起。</p>
<p><strong>改动:</strong>钉住后统一吸附到浏览器左侧边缘,脱离 ReactFlow 画布缩放;钉住瞬间把当前可见大小转换成面板真实尺寸,之后只由右下角拖拽或标题栏按钮调整。</p>
<p><strong>影响:</strong><code>web/components/nodes/index.tsx</code>;钉住语义从“原地浮在上层”改为“左侧停靠工作面板”。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 关键帧详情支持右下角拖拽缩放和上层钉住</h3>
<span class="tag orange">KeyframePanelNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>只有按钮缩放不够直观;钉住后仍作为画布节点,会继续随 ReactFlow 画布缩放。</p>
<p><strong>改动:</strong>增加右下角拖拽缩放手柄;钉住时通过 portal 固定到浏览器上层,脱离 ReactFlow 画布缩放和平移。</p>
<p><strong>影响:</strong><code>web/components/nodes/index.tsx</code><code>web/app/page.tsx</code>;未钉住时仍是画布节点,钉住后保持屏幕固定位置。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 关键帧详情面板增加钉住按钮</h3>
<span class="tag orange">KeyframePanelNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>面板可以拖动后,用户仍可能误拖;切换图片时希望保持固定工作位置。</p>
<p><strong>改动:</strong>在标题栏增加钉子按钮。钉住后面板节点禁止拖动,切换关键帧只切换内容不移动位置;取消钉住后可继续拖动。</p>
<p><strong>影响:</strong><code>web/app/page.tsx</code><code>web/components/nodes/index.tsx</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 切换关键帧不再重置详情面板位置</h3>
<span class="tag orange">KeyframePanelNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>用户把关键帧详情面板拖到合适位置后,再点击下一张关键帧会把面板拉回默认位置,造成视觉疲劳。</p>
<p><strong>改动:</strong>已打开的面板只切换内容,不移动位置;只有面板不存在、首次打开时才放到默认位置并自动聚焦。</p>
<p><strong>影响:</strong><code>web/app/page.tsx</code>;关闭后重新打开仍会出现在默认位置。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 关键帧详情面板增加缩放控制</h3>
<span class="tag orange">KeyframePanelNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>关键帧详情面板作为画布节点后可以随画布缩放,但面板自身没有尺寸控制,用户无法单独放大或缩小它。</p>
<p><strong>改动:</strong>在面板标题栏增加 <code>-</code>、百分比重置、<code>+</code> 控制,支持 75% 到 135% 的面板级缩放。</p>
<p><strong>影响:</strong><code>web/app/page.tsx</code><code>web/components/nodes/index.tsx</code>;点击新关键帧仍会找回到默认位置,缩放比例保留。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 关键帧详情从固定左侧抽屉迁到无限画布</h3>
<span class="tag orange">KeyframeNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>关键帧详情 / 元素提取面板固定在左侧 drawer和 ReactFlow 无限画布割裂,也不会跟随画布缩放。</p>
<p><strong>改动:</strong>移除主页面隐藏渲染的 <code>Dashboard</code> drawer 承载方式,新增独立 <code>keyframePanel</code> ReactFlow 节点来挂载 <code>FrameLightbox</code></p>
<p><strong>影响:</strong><code>web/app/page.tsx</code><code>web/components/nodes/index.tsx</code>;点关键帧后面板默认出现在流程左侧空白画布里,不遮挡 Input / Keyframe 主节点;标题栏可拖动,跟随 ReactFlow 平移和缩放。再次点击关键帧缩略图会把面板找回到默认位置,并自动把视野拉到“关键帧 + 面板”。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 元素改造 hover 预览简化为原帧预览</h3>
<span class="tag violet">StoryboardNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>元素改造节点的 hover 预览虽然已改为节点内显示,但仍比关键帧节点复杂,多了“来源原帧 / 提取元素”两栏和元素名称,信息过载。</p>
<p><strong>改动:</strong>改成和镜头拆解关键帧一致的简单预览:只显示来源原帧,底部显示分镜编号和时间。</p>
<p><strong>影响:</strong><code>web/components/nodes/index.tsx</code>;元素改造板块 hover 现在更轻,不干扰当前判断。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 元素改造 hover 预览改为节点内效果</h3>
<span class="tag violet">StoryboardNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>上一版把元素预览用 <code>createPortal</code> 挂到 <code>body</code>DevTools 里会出现额外 fixed 层,交互形态和关键帧节点不一致。</p>
<p><strong>改动:</strong>改成节点内 <code>group-hover</code> 预览,不再向 <code>body</code> 插入预览层。后续又简化为只展示来源原帧,见上方最新记录。</p>
<p><strong>影响:</strong><code>web/components/nodes/index.tsx</code>;元素改造板块的 DOM 和交互效果更接近关键帧缩略图。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 新增独立源码解析与协作地图</h3>
<span class="tag blue">docs</span>
</header>
<div class="body">
<p><strong>目的:</strong>把产品功能区、源码位置、接口、数据模型、需求描述方式固定下来,减少“描述不准导致改偏”。</p>
<p><strong>影响:</strong>新增 <code>docs/source-analysis.html</code>,不接入 Next 应用,不影响工作台运行。</p>
<p><strong>以后描述:</strong>可以直接引用本页的功能区名称、节点职责和源码文件名。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · Storyboard 元素缩略图 hover 预览修复</h3>
<span class="tag violet">StoryboardNode</span>
</header>
<div class="body">
<p><strong>问题:</strong>元素改造节点上方小图 hover 没有像镜头拆解节点一样显示原图预览,并且首次修复时出现运行时错误。</p>
<p><strong>原因:</strong>节点内部 overflow 裁剪了预览;随后 portal 预览里把变量写成了不存在的 <code>aspectRatio</code></p>
<p><strong>影响:</strong><code>web/components/nodes/index.tsx</code>。该记录之后又改为节点内预览,见上方最新记录。</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 元素识别结果不再锁死</h3>
<span class="tag orange">元素提取</span>
</header>
<div class="body">
<p><strong>问题:</strong>Vision 识别可能错,但元素列表像最终结果;点击提取图会跳页面,打断用户思路。</p>
<p><strong>改动:</strong>支持元素改名、改英文提示、改位置、删除元素、重复提取、删除单张提取图;提取图不再用链接跳新页。</p>
<p><strong>影响:</strong><code>FrameLightbox</code><code>web/lib/api.ts</code><code>PATCH /jobs/{job_id}/frames/{idx}/elements/{element_id}</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 分镜编排入口聚焦到具体分镜</h3>
<span class="tag violet">StoryboardWorkbench</span>
</header>
<div class="body">
<p><strong>问题:</strong>从 Storyboard 或顶部分镜条进入编排时,没有明确定位到用户正在看的那一帧。</p>
<p><strong>改动:</strong>工作台接受 <code>focusedFrame</code>,点击缩略图会打开工作台并聚焦对应分镜。</p>
<p><strong>影响:</strong><code>page.tsx</code><code>StoryboardBar</code><code>StoryboardWorkbench</code><code>StoryboardNode</code></p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 视觉管线不再被 ASR 阻断</h3>
<span class="tag green">Pipeline</span>
</header>
<div class="body">
<p><strong>问题:</strong>SKG 网关 audio 不通时,视觉解析也容易被标记失败。</p>
<p><strong>改动:</strong><code>analyze</code> 主流程强调拆轨和关键帧,声音文案轨独立处理。</p>
<p><strong>影响:</strong><code>api/main.py</code><code>page.tsx</code>、节点语义说明。</p>
</div>
</article>
</div>
</section>
<section id="update-rule" data-search>
<h2>更新规则</h2>
<div class="callout">
<p>以后任何改动只要影响产品理解、节点职责、界面行为、数据模型、API、运行方式或用户操作路径都要同步更新本页的对应章节和“变更记录”。</p>
</div>
<table style="margin-top:14px">
<thead>
<tr><th>改动类型</th><th>必须更新本页哪里</th><th>原因</th></tr>
</thead>
<tbody>
<tr><td>节点文案 / 节点功能</td><td>业务管线、节点职责边界、界面区域到源码、变更记录</td><td>避免用户按旧节点理解描述需求。</td></tr>
<tr><td>新增 / 修改接口</td><td>接口地图、数据模型、变更记录</td><td>避免前后端契约不清。</td></tr>
<tr><td>新增数据字段</td><td>数据模型、源码结构地图、变更记录</td><td>刷新恢复和 state.json 依赖字段一致。</td></tr>
<tr><td>改交互路径</td><td>界面区域到源码、需求描述模板、变更记录</td><td>用户描述“点击哪里”时必须和真实路径一致。</td></tr>
<tr><td>修 bug</td><td>变更记录;如果暴露新坑,也更新当前已通与阻塞</td><td>让后续同类问题能快速定位。</td></tr>
</tbody>
</table>
</section>
</main>
</div>
<script>
const input = document.getElementById("search");
const searchable = Array.from(document.querySelectorAll("[data-search]"));
input.addEventListener("input", () => {
const q = input.value.trim().toLowerCase();
searchable.forEach((el) => {
const text = el.innerText.toLowerCase();
el.classList.toggle("hidden-by-search", q && !text.includes(q));
});
});
</script>
</body>
</html>