跳转至

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 的 pendingProfileId effect 对新创建的 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_transcriptions localStorage 条目,此后 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;每个阶段可单独发布)

  1. C0 — <UseInMenu> + handoffSlice 包装现有交接(voice→studio、gallery→design、restore/loadProject)。纯重构为单一机制;暂无新目的地。为后续所有工作降低风险。
  2. A1 — 分组 NavRail + 将 <WorkspaceHistory> 扩展到 Dub & Stories(右侧面板出现在所有位置)。仅前端。
  3. B 类缺口 — 新目标 一次一行:voice→Dub segment、design→Voice、audio→Stories、transcript→Dub/Stories。每一项都是一个消费者 + 一个菜单项。
  4. C2 — 将转录记录移至后端(alembic 0004、dub-transcript 直写、localStorage 导入)。后端 + 前端。
  5. 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 场景)会落到正确的资产上;alembic 0004 升级/降级 + 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