已取代(2026-06-25) 由 00-roadmap-elevenlabs-parity.md 及规范 01–03 取代。保留作历史参考。
Studio / Projects — v1 规范¶
目标: 实现 ElevenLabs-Studio 对长篇叙述的同等能力。用户粘贴(或拖入)一份 10 页的脚本,应用将其拆分为多个块(block),用户为每个块分配音色,内联预览,然后点击“生成”得到一条拼接好的 WAV。
v1 不包含:视频同步、多轨混音、音乐衬底、音效(SFX)、未拼接音频的实时播放。这些将在后续版本加入。
1 — 数据模型¶
复用 studio_projects。为 state_json 增加一个严格结构:
interface ProjectState {
kind: 'studio'; // discriminates from dub projects (same table)
blocks: Block[];
default_voice_id: string; // profile_id used for blocks that don't pin one
default_lang?: string;
created_by_version: string;
}
interface Block {
id: string; // uuid, stable across edits
text: string;
voice_id?: string; // overrides project default; null → inherit
pause_before_ms?: number; // inserted silence (0–5000)
pause_after_ms?: number;
// Generation state — server-owned, not user-edited
gen?: {
audio_path: string; // absolute path to per-block WAV in scratch
duration_ms: number;
hash: string; // SHA-256 of (text, voice_id, gen knobs) — cache key
generated_at: number;
};
}
需要两个迁移:
- ALTER TABLE studio_projects ADD COLUMN kind TEXT DEFAULT 'dub' 这样我们就可以在列表视图中区分 studio 与 dub 项目。
- 一个以 hash 为键的 project_block_cache 表,这样重新打开项目时可无需重新生成而直接回放已有音频。(v1 可跳过,只需将路径存储在 state_json.blocks[*].gen 内 —— 单写入者,无并发编辑。)
2 — 后端端点¶
只有两个新路由;其余全部复用现有生成逻辑:
POST /studio/projects/{id}/blocks/{block_id}/generate
body: { text, voice_id, knobs? } (knobs = same FormData shape as /generate)
response: { audio_path, duration_ms, hash }
Internally calls the same TTS pipeline /generate does, just writes to
scratch under projects/{id}/ instead of the global history dir.
POST /studio/projects/{id}/stitch
body: { include_block_ids?: string[] } // defaults to all
response: { audio_path, duration_ms }
Reads blocks in order, inserts silence for pause_before/after_ms,
concatenates via ffmpeg, returns final WAV.
另外,扩展现有 PUT /projects/{id} 以接受新的 state_json 结构 —— 零代码改动,因为它已经是 JSON blob 透传。
其余一切(列表、创建、删除、用于音色选择器的 profiles 端点)均已构建完成。
3 — UI 形态¶
一个新路由:/studio/:projectId。像处理 CloneDesignTab 那样进行懒加载。
┌─────────────────────────────────────────────────────────────────┐
│ ← Projects [project title, inline-edit] [Export WAV] │
├────────────────┬────────────────────────────────────────────────┤
│ │ │
│ BLOCK LIST │ BLOCK EDITOR (selected block) │
│ (left rail) │ │
│ │ ┌─────────────────────────────────────────┐ │
│ ┌───────────┐ │ │ [textarea — the text for this block] │ │
│ │ Block 1 ▸ │ │ │ │ │
│ │ "In a…" │ │ └─────────────────────────────────────────┘ │
│ │ [voice:A] │ │ │
│ │ ▶ 0:12 │ │ Voice: [SearchableSelect — profiles ▾] │
│ └───────────┘ │ Pause before: [___] ms │
│ ┌───────────┐ │ Pause after: [___] ms │
│ │ Block 2 ▸ │ │ │
│ │ "The…" │ │ [Generate block] [Preview] [⚙ advanced ▾] │
│ │ [voice:B] │ │ │
│ │ ▶ 0:08 │ │ ── Generated audio ────────────────────── │
│ └───────────┘ │ [waveform] [▶ play] [hash: 3f2a…] │
│ ┌───────────┐ │ │
│ │ + add │ │ │
│ └───────────┘ │ │
│ │ │
├────────────────┴────────────────────────────────────────────────┤
│ TRANSPORT │
│ [▶ play all] [Regenerate stale (3)] [Stitch & export WAV] │
│ ▓▓▓▓▓▓▓▓▓░░░░░░░░ block 3 of 12 · 1:42 / 6:10 │
└─────────────────────────────────────────────────────────────────┘
关键 UX 设计要点:
- 粘贴即拆分:用户粘贴长脚本时,应自动按段落分界将其拆分为多个块(复用
backend/services/subtitle_segmenter.py—— 它已能处理句子边界)。v1 不提供手动创建块。 - 过期指示器:如果
block.gen.hash≠hash(current text+voice+knobs),显示 🔄 徽章。传输栏中的“重新生成过期块”按钮会并行处理所有过期块(后端已通过loop.create_task实现并行安全)。 - 音色继承:块中音色为空 = 使用项目默认值。下拉框显示“↳ 默认(Voice A)”以便一目了然。
- v1 中没有时间线标尺。 块采用纵向列表而非横向时间线。带有每块时长可视化的横向时间线是 v2 的内容——这涉及大量 UI 工作,而且直到用户需要配合音乐衬底进行编排之前,都不需要它。
4 — 可复用原语(已构建)¶
| 组件 | 位置 | 复用方式 |
|---|---|---|
SearchableSelect |
frontend/src/components/ |
音色选择器 |
WaveformTimeline |
frontend/src/components/ |
单块播放 |
Profiles API (listProfiles) |
frontend/src/api/profiles.ts |
音色下拉数据源 |
| TTS generation | backend/api/routers/generation.py (/generate) |
逐块生成 |
| Subtitle segmenter | backend/services/subtitle_segmenter.py |
粘贴即拆分 |
| SSE 进度 | backend/utils/hf_progress.py |
流式返回生成状态 |
| ffmpeg concat | backend/services/ffmpeg_utils.py |
拼接 |
5 — 黄金路径(用户操作流程)¶
- 首页 → 项目 → 新建 Studio 项目 → 自动创建 id,进入
/studio/:id - 粘贴 5 段文本 → 自动拆分为 5 个块,全部指定为项目默认语音
- 点击块 3 → 将语音切换为第二个音色档案(叙述者 → 角色)
- 点击重新生成过期块 → 后端并行发起 5 个
/studio/.../generate调用,进度流式返回 - 点击▶ 全部播放 → 客户端按顺序播放每个块的音频,并带静音间隔(预览无需拼接)
- 拼接并导出 WAV → 后端执行拼接,返回文件路径,Tauri 在 Finder 中显示
6 — 按周工作划分¶
| 天 | 工作内容 |
|---|---|
| 第 1 天 | 后端:schema 迁移、/studio/projects/{id}/blocks/{block_id}/generate 路由、/studio/projects/{id}/stitch 路由。复用 subtitle_segmenter 实现粘贴拆分。 |
| 第 2 天 | 前端:StudioPage.jsx 骨架 + 路由 + 新建项目功能。左侧栏块列表。右侧面板块编辑器(文本 + 语音 + 停顿)。 |
| 第 3 天 | 逐块生成逻辑接入 + 过期哈希检测 + 播放控制条 + 并行重新生成。复用 WaveformTimeline 进行预览。 |
| 第 4 天 | 全部播放序列处理(客户端 — Web Audio 队列)、拼接与导出。Toast 提示 + Finder 显示。 |
| 第 5 天 | 粘贴拆分、键盘快捷键(⌘↩ 重新生成,⌘S 保存,⌘E 导出)、空状态、细节打磨。 |
| 第 6 天 | 发布测试:在全新安装的 DMG 上,创建一个包含 10 个块的项目,切换语音,导出。修复所有出现的问题。 |
| 第 7 天 | 预留缓冲时间 / 文档 / 发布版本。 |
7 — 明确不在 v1 范围内¶
- 水平时间标尺 / 波形拖拽时间线
- 块间过渡(交叉淡化、闪避)
- 音乐 / 音效垫层
- 多轨(并行语音层)
- 从
.docx/.fdx导入(编剧格式) - 可共享的项目链接(本地优先 — 跳过)
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 om/VoiceStudio