Workspace 导航、连接与共享 — 规范¶
目标: 让应用感觉像一个互联的工作室,而不是八个并列的标签页。三个动作:(1) 一个更简单的导航信息架构(IA),带有一致的工作区外壳;(2) 一种通用交接(handoff)机制,让任何资产(voice、audio、transcript、project、video)都能通过单一的 “Use in ▸” 操作入口在工作区之间移动——取代当前约 9 个临时桥接;(3) 一个统一的资产与本地共享模型,让 OmniDrive 成为统一枢纽,且导出/显示/复制路径在任何地方都同为一个操作。
硬性约束 — 本地优先(来自 CLAUDE.md): 这里的“共享”指 设备上的交接 + 文件导出,绝不涉及云链接、账户或遥测。不引入任何网络出站流量。每个工作区在离线状态下都保持完全可用。
本文是 docs/specs/voice-studio-unification.md 的配套文档——该规范将 Clone+Design 合并为一个 Voice 工作区,并泛化了右侧历史面板(P1,已实现)。本规范假定这一方向,并把右侧历史模式扩展到每个工作区(“等等”)。
当前现状(基线):NavRail.jsx 是一个扁平的 8 项侧栏(launchpad, clone, design, dub, stories, gallery, transcriptions, projects);“OmniDrive” 是 pages/Projects.jsx 的标签(聚合了 voice_profiles + studio_projects + generation_history + dub_history + export_history + transcriptions);Transcripts 是仅使用 localStorage 的 STT 日志(pages/Transcriptions.jsx)。交接目前是一次性的:pendingProfileId、setVdStates+setMode、restoreHistory、loadProject、restoreDubHistory、handleSaveHistoryAsProfile 等。
Part A — 导航信息架构(更简化)¶
A.1 分组侧栏,更少的一级入口¶
收拢为分组侧栏(用视觉分隔线,而不是新增点击层级)。根据统一规范,Clone+Design → Voice,一级入口数量从 8 降到 6:
HOME
◉ Launchpad
─────────────
CREATE
◉ Voice (clone + design merged)
◉ Stories
◉ Dub
─────────────
LIBRARY
◉ Gallery (voices to bring in)
◉ OmniDrive (everything you've made)
─────────────
CAPTURE
◉ Transcribe
─────────────
◉ Settings (footer)
NavRail.jsx:渲染分组标签和分隔线;沿用同一套setMode机制。tools/queue/donate/enterprise仍不在侧栏中(通过上下文进入),voice仍作为模态的 profile 浮层。AppMode(uiSlice.ts):clone|design → studio(统一规范 §2)。其他 id 保持不变。
A.2 一致的工作区外壳¶
每个 CREATE 工作区都使用同一个三区域外壳(由统一规范中的 Voice 布局泛化而来):
[ LEFT rail ] [ contextual library ] [ work column ] [ right: history/output ]
(collapsible) (prompt/canvas/timeline) (this workspace's runs)
- 右侧历史面板 = 泛化后的 P1
<WorkspaceHistory>:Voice 显示 clone/design 的生成结果;Dub 显示配音任务;Stories 显示渲染完成的有声书。每个面板都限定在各自工作区内,每一行复用共享的<WaveformPlayer>和同一个 “Use in ▸” 菜单(Part B)。这正对应了“每个工作区右侧有各自历史记录,等等”的诉求。 - 左侧上下文库:Projects(Dub/Stories)或 Saved profiles(Voice)——侧栏中已部分具备;现在统一标准化。
Part A 不涉及后端。
Part B — 通用交接(“Use in ▸”)¶
B.1 一个桥接取代九个¶
把 pendingProfileId 模式(uiSlice.ts + App.jsx 中的消费方 effect)泛化为 store 中一个带类型的交接机制:
// store/handoffSlice.ts
type AssetKind = 'voice' | 'design' | 'audio' | 'transcript' | 'project' | 'video';
interface Handoff {
target: AppMode; // 'studio' | 'dub' | 'stories' | 'transcribe' | ...
kind: AssetKind;
payload: Record<string, unknown>; // e.g. {profileId} | {vdStates} | {audioPath} | {text,lang} | {projectId}
ts: number;
}
interface HandoffSlice {
handoff: Handoff | null;
sendTo(target: AppMode, kind: AssetKind, payload): void; // sets handoff + setMode(target)
consumeHandoff(): Handoff | null; // returns & clears (one-shot)
}
sendTo设置handoff并执行导航(setMode(target))。- 每个工作区在变为活动状态时调用
consumeHandoff();如果handoff.target匹配且kind是该工作区接受的类型,就应用 payload(并像 P1 的pendingProfileIdeffect 对新创建的 profile 所做的那样,等待异步加载完成)。一次性;读取后即清除。 - 把现有的一次性逻辑迁移到该机制上:
pendingProfileId(voice→studio)、gallery 的attrs→design、restoreHistory、loadProject、restoreDubHistory、handleSaveHistoryAsProfile都变成sendTo(...)+ 每个工作区各自的 consumer。最终:一个机制,可独立测试,取代散落的setMode+setter 配对。
B.2 操作入口¶
一个统一的 <UseInMenu asset={…} /> 下拉菜单,渲染在每张资产卡片上(历史行、profile 卡片、gallery 卡片、transcript 行、OmniDrive 卡片、dub 片段的配音选择器)。它只列出该资产类型对应的有效目标:
| 资产类型 | 来源示例 | “Use in ▸” 目标 |
|---|---|---|
voice (profile) |
Gallery、OmniDrive、Voice 的 profile 条 | Voice(加载)、Dub(设置片段/默认 voice)、Stories(分配 cast) |
design (vd_states) |
Gallery 原型、design 生成结果 | Voice → By design(预填滑块) |
audio(一条生成结果) |
Voice history、OmniDrive | 保存为 profile、用于 Stories(作为一句台词的录音)、导出 |
transcript(text+lang) |
Transcribe 页面、Dub 自动转写 | 新建 Dub(初始化片段)、新建 Story(初始化台词)、复制文本 |
project |
OmniDrive、Launchpad 最近项 | 在 Dub / Stories 中打开 |
video |
Dub 源 | 在 Dub 中重新打开 |
这张表 就是 连接性审计中各项缺口的闭合方案(design→dub、clone-gen→stories、dub-transcript→transcribe、stories-cast→profile、transcript→dub/stories)——每一个缺口,不过是某一行中的目标变得可用而已。
Part C — 统一资产底层与本地共享¶
C.1 资产是一等公民¶
OmniDrive 已经聚合了五张表 + localStorage 转录记录。请形式化一个读取模型,使每个界面(OmniDrive、每工作区右侧面板、<UseInMenu>)都采用同一种结构:
interface Asset {
id: string; kind: AssetKind;
title: string; subtitle?: string;
created_at: number;
preview?: { audio_path?: string; thumb?: string };
source: AppMode; // where it was made
targets: AppMode[]; // valid "Use in ▸" destinations (from the B.2 table)
}
- v1 阶段仅基于现有表进行纯派生 —— 对 profiles/gens/dubs/exports 不新增存储。后续可(可选)通过
GET /assets?kind=&source=&limit=端点集中处理;v1 在useAppData中于客户端派生。
C.2 将转录记录提升到后端¶
转录记录是唯一的孤儿数据(localStorage omni_transcriptions,没有后端,也没有交接机制)。为使它们成为一流资产并可可靠交接:
- 通过 alembic
0004新建表transcriptions(id, text, language, duration_s, segments_json, source, created_at)(幂等,模式与0002/0003相同)。 CaptureWidget和 Dub 自动转录 都写入此表(填补“dub transcript → Transcribe”的缺口)。向后兼容:首次加载时一次性导入现有的omni_transcriptionslocalStorage 条目,此后 localStorage 仅作为缓存。- WS
transcriptions事件(镜像现有的generation_history/profiles事件)用于实时更新。
C.3 共享 = 本地、统一¶
每个音频/视频资产生成一条 Export 路径,通过 <UseInMenu> → Export 提供:
- 单一对话框:格式(音频用 WAV/MP3;dub/stories 用 MP4/WAV/stems/SRT)、目标位置、“导出后显示”。复用现有的
handleNativeExport/triggerDownload/exportRecord—— 只是统一放到一个组件后面,并记录到export_history(→ OmniDrive Downloads)。 - “共享”功能全部在设备本地:导出文件、在文件夹中显示(
exportReveal)、复制文件路径。明确不提供云上传/共享链接(本地优先)。浏览器/Docker 构建继续像今天一样回退到 blob 下载。
分阶段实施(持续合并到 main;每个阶段可单独发布)¶
- C0 —
<UseInMenu>+handoffSlice包装现有交接(voice→studio、gallery→design、restore/loadProject)。纯重构为单一机制;暂无新目的地。为后续所有工作降低风险。 - A1 — 分组 NavRail + 将
<WorkspaceHistory>扩展到 Dub & Stories(右侧面板出现在所有位置)。仅前端。 - B 类缺口 — 新目标 一次一行:voice→Dub segment、design→Voice、audio→Stories、transcript→Dub/Stories。每一项都是一个消费者 + 一个菜单项。
- C2 — 将转录记录移至后端(alembic
0004、dub-transcript 直写、localStorage 导入)。后端 + 前端。 - C3 — 统一导出对话框,整合三条导出路径;OmniDrive 成为基于
Asset读取模型的统一资产浏览器。
向后兼容、风险、测试¶
- 数据: 仅做加法(alembic
0004转录记录;现有表不动)。localStorage 转录记录只导入一次,绝不丢弃。现有的 project/profile/history 流程继续工作,因为 C0 是包装它们而不是替换语义。 - 跨平台: macOS/Win/Linux 上所有默认行为一致;原生保存/显示已按 OS 抽象;浏览器构建保留 blob-download 回退。不引入任何仅限某平台的默认行为(本地优先 + 一致性规则)。
- Mode-id 变更: 仅
clone|design→studio(与统一规范共享);一个 restore 兼容层负责映射旧 ids/payloads。 - 文档同步: NavRail/IA 变更会涉及任何描述这些标签页的
docs/**/README → 在同一 PR 中更新。 - 测试:
handoffSlice的 consume-once 语义(单元测试);每个<UseInMenu>仅列出每种 kind 的有效目标;异步资产加载后应用的交接(P1 wait-for-profile 场景)会落到正确的资产上;alembic0004升级/降级 + localStorage 导入;导出对话框记录到export_history并在每个 OS 路径上显示。
待定决策(易更改)¶
- OmniDrive 作为主页? 本规范仍然以 Launchpad 为主页,OmniDrive 作为资产库。如果你更希望 OmniDrive 就是登录枢纽(一切都从“你的内容”开始),那就是 Launchpad/OmniDrive 合并 —— 请说明,并把它并入 A1。
- 拖放 vs 菜单。 v1 提供
<UseInMenu>下拉菜单(可发现、支持键盘)。将资产卡片拖到导航项上是后续在同一个sendTo之上的附加层。 GET /assets端点。 v1 在客户端派生资产;只有在 OmniDrive 分页需要时,才提升为后端读取模型。
工作区控制行为¶
转换过程会保持其方法标签页处于激活状态,直到请求结束(包括失败时)。引擎快速切换触发器和展开的选择使用相同的格式化模型名称。配音适配徽章、说明、更短改写操作、播放控制以及引擎驻留标签在所有支持的 UI 语言中均已本地化。
紧凑的引擎家族徽章保留标准的 TTS、ASR 和 LLM 缩写。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 om/VoiceStudio