1119 lines
54 KiB
HTML
1119 lines
54 KiB
HTML
<!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/<jobId>/state.json</code></td><td>运行时状态文件,不在源码列表里,但刷新恢复依赖它。</td></tr>
|
||
<tr><td><code>jobs/<jobId>/frames</code></td><td>关键帧 jpg。注意 frame.index 是稳定 ID,不等于数组下标。</td></tr>
|
||
<tr><td><code>jobs/<jobId>/cleaned</code></td><td>清洗后待应用图片。</td></tr>
|
||
<tr><td><code>jobs/<jobId>/elements</code></td><td>元素提取图,多版本命名:<code>idx_elementId_cutoutId.jpg</code>。</td></tr>
|
||
<tr><td><code>jobs/<jobId>/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/<jobId>/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>ASR:SKG 网关 audio endpoint 404 或渠道不可用。</li>
|
||
<li>Translate:本身 text 通,但产品流里依赖 ASR 段落。</li>
|
||
<li>Rewrite:需要 SKG 产品信息模板和目标脚本结构。</li>
|
||
<li>Video Gen:模型层按业务保留 Seedance / Kling / Veo/Voe 选择;网关调用层通过 <code>VIDEO_CREATE_PATHS</code> 多入口尝试,当前常见入口实测返回 404/unsupported,若平台后台有其它入口要直接配置到该变量。</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 · 生视频提交不再被前端锁死</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>
|