# Round 1 PRD:模型 Profile 基础设施 状态:v0.2 评审修订稿 所属子项目:`02-P-NBL` Round:Round 1 日期:2026-06-02 关联文档: - [`../product-plan.md`](../product-plan.md) - [`../engineering-plan.md`](../engineering-plan.md) - [`../plan.md`](../plan.md) ## 0. 本版修订结论 本版根据 2 位架构师、2 位程序员、2 位 QA 对 v0.1 PRD 的评审意见修订。 Round 1 的核心定位保持不变: ```text 不改变用户体验,把现有 SenseVoice 纳入可扩展的 ModelProfile 架构。 ``` 本版把以下事项从“建议/待确认”改为确定规则: | 事项 | Round 1 决策 | |---|---| | `models/state.json` | 本轮必须落地最小版读写;它只是索引,不能绕过真实文件校验 | | 新下载目录 | 新下载的 SenseVoice 统一安装到 `models/sensevoice-zh` | | 旧目录 | `models/sensevoice` 只读兼容,不自动迁移、不删除 | | `SelectedModelID` | 本轮加入隐藏配置,默认 `sensevoice-zh`,不暴露 UI | | Wails 公开 API | 保持旧签名兼容,不要求前端改调用 | | `RequiredFiles` | 需要支持 `allOf` / `anyOf`,不能只用 `[]string` | | EngineFactory | 只接收已解析、已校验的 `ResolvedModel` | | 下载器 | 必须 staging + 校验 + 安全落盘,失败不得破坏已有可用模型 | ## 1. 背景 当前 VoiceSnap 的离线识别链路基本围绕 SenseVoice 写死: - 模型目录固定为 `models/sensevoice`。 - 模型文件固定查找 `model.int8.onnx` 或 `model.onnx`。 - `tokens.txt` 固定在 SenseVoice 目录下。 - engine 初始化固定使用 SenseVoice config。 - 下载器只识别 SenseVoice 压缩包和 SenseVoice 目录结构。 - 前端通过现有 Wails 方法判断模型是否存在,并触发下载。 但后续产品规划已经确定要支持: | 语言版本 | 默认轻量模型 | 高级模型 | |---|---|---| | 中文 | SenseVoice | Qwen3-ASR | | English | Moonshine English | Parakeet | 因此 Round 1 不是接入新模型,而是先把当前单模型架构改成可扩展的模型 Profile 架构,并确保现有中文 SenseVoice 用户体验不退化。 ## 2. Round 1 目标 Round 1 完成后,系统内部应从: ```text 写死 SenseVoice 路径和 SenseVoice config ``` 演进为: ```text ModelRegistry -> ModelResolver -> ResolvedModel -> EngineFactory -> SenseVoice config ``` 对用户来说,Round 1 交付后产品应基本无感: - 不出现 Moonshine。 - 不出现 Parakeet。 - 不出现 Qwen3-ASR。 - 不出现语言版本设置。 - 不出现模型选择页。 - 当前 SenseVoice 下载、加载、识别、上屏、历史、词库能力不退化。 ## 3. 本轮非目标 明确不做: - 不接入 Moonshine。 - 不接入 Parakeet。 - 不接入 Qwen3-ASR。 - 不做语言 UI。 - 不做模型选择 UI。 - 不做模型删除 UI。 - 不做模型市场。 - 不改变首次启动语言判断逻辑。 - 不改用户词库格式。 - 不改历史记录格式。 - 不做实时 streaming partial transcript。 - 不做 `models/sensevoice` 到 `models/sensevoice-zh` 的自动大文件迁移。 - 不做每个模型目录下的 `manifest.json`。 - 不做 checksum、签名校验、远程模型 catalog。 ## 4. 用户故事 ### 4.1 已安装 SenseVoice 的老用户 作为一个已经安装过 SenseVoice 的用户,我升级到 Round 1 版本后,应用应该继续识别到我的本地模型,并能正常离线识别。 验收点: - 旧模型目录 `models/sensevoice` 可以被识别。 - 不要求用户重新下载模型。 - 不移动、不删除旧模型目录。 - 不破坏用户词库、历史、设置。 ### 4.2 新安装用户 作为一个新用户,我第一次启动应用时,仍然按当前流程准备 SenseVoice 模型,不需要理解模型 Profile 或语言版本。 验收点: - 仍然能通过现有 Onboarding 流程下载 SenseVoice。 - 新下载模型最终安装到 `models/sensevoice-zh`。 - 下载完成后 required files 校验通过。 - UI 流程不增加额外模型选择步骤。 ### 4.3 后续研发 作为后续 Round 2/3/4/5 的研发人员,我可以在不重写主流程的情况下,新增 Moonshine、Parakeet、Qwen3-ASR profile,并通过 EngineFactory 生成不同 backend 的 sherpa-onnx config。 验收点: - 新模型接入点清晰。 - 业务主流程不继续复制 SenseVoice 硬编码逻辑。 - required files、安装目录、backend kind、provider order 都能通过 profile 描述。 ## 5. 架构边界 Round 1 采用以下职责划分: ```text ModelRegistry -> ModelResolver -> ModelInstaller -> EngineFactory -> EngineService ``` | 模块 | 建议包/文件 | 职责 | |---|---|---| | `ModelRegistry` | `internal/model/registry.go` | 注册内置模型 profile,按 model id 查询 profile | | `LanguageRegistry` | `internal/model/registry.go` 或独立文件 | 注册最小语言 profile,为 Round 2 做准备,不参与 UI | | `ModelResolver` | `internal/model/resolver.go` | 根据 profile、state、新旧目录解析出可用模型路径 | | `ModelValidator` | `internal/model/validator.go` | 校验 required files,支持 `allOf` / `anyOf` | | `ModelInstallState` | `internal/model/install_state.go` | 读写 `models/state.json` | | `ModelInstaller` | `internal/model/downloader.go` 或新文件 | 下载、解压、staging、校验、安全落盘 | | `EngineFactory` | `internal/engine/factory.go` | 根据 `ResolvedModel` 生成 sherpa config 并初始化 engine | | `EngineService` | `services/engine_service.go` | 保持 Wails public API 兼容,内部映射到默认模型 | | `paths` | `internal/paths/paths.go` | 只提供基础路径函数,不承载模型业务判断 | 关键原则: - Registry 只描述模型,不判断本地是否安装。 - Resolver 负责路径查找和状态判断。 - Validator 负责文件完整性。 - EngineFactory 只接收已经 resolved 的模型,不再猜路径。 - EngineService 对前端保持旧接口。 ## 6. 数据结构 ### 6.1 ModelProfile `ModelProfile` 表示一个可安装、可加载、可校验的识别模型。 建议结构: ```go type ModelProfile struct { ID string DisplayName string BackendKind string Tier string SupportedLanguageIDs []string RecommendedFor []string Description string ApproxSize string DownloadURLs []string InstallDirName string RequiredFiles []RequiredFileRule ProviderOrder []string NumThreads int } ``` 字段说明: | 字段 | 说明 | |---|---| | `ID` | 稳定技术 ID,不随显示名变化 | | `DisplayName` | 用户可见名称,例如 `SenseVoice` | | `BackendKind` | 决定使用哪类 sherpa config builder | | `Tier` | `default` 或 `advanced` | | `SupportedLanguageIDs` | 模型理论支持的语言 | | `RecommendedFor` | 本产品推荐该模型服务的语言 | | `InstallDirName` | 新安装目录名 | | `RequiredFiles` | 安装完整性规则 | | `ProviderOrder` | 例如 macOS 可为 `coreml,cpu` | 注意: - 必须区分 `SupportedLanguageIDs` 和 `RecommendedFor`。 - SenseVoice 支持多语言,但 Round 1 产品推荐仍是中文。 - 后续 Parakeet 支持欧洲语言,不能因为 English 高级定位而丢失多语言扩展能力。 ### 6.2 RequiredFileRule `RequiredFiles []string` 不够表达 SenseVoice 的二选一模型文件,因此 Round 1 必须支持结构化规则。 建议结构: ```go type RequiredFileRule struct { Role string AllOf []string AnyOf []string Required bool } ``` 规则说明: - `AllOf` 中的路径必须全部存在。 - `AnyOf` 中的路径至少存在一个。 - 文件存在但为 0 字节,视为无效。 - 目录类 required item 后续可扩展;Round 1 只需要文件。 SenseVoice profile: ```text id: sensevoice-zh displayName: SenseVoice backendKind: sensevoice tier: default supportedLanguageIds: zh-CN, zh-Hans, zh-Hant, yue, en, ja, ko recommendedFor: zh-CN installDirName: sensevoice-zh requiredFiles: - role: model anyOf: - model.int8.onnx - model.onnx - role: tokens allOf: - tokens.txt providerOrder: - coreml - cpu numThreads: 4 ``` 模型文件优先级: 1. 同时存在 `model.int8.onnx` 和 `model.onnx` 时,优先使用 `model.int8.onnx`。 2. 只有 `model.onnx` 时,允许使用 `model.onnx`。 3. 两者都不存在或文件为 0 字节,模型不完整。 ### 6.3 LanguageProfile Round 1 只做最小可编译语言 profile,不做运行时语言切换。 建议结构: ```go type LanguageProfile struct { ID string DisplayName string UILocale string SystemMatchers []string DefaultModelID string UpgradeModelIDs []string } ``` Round 1 至少注册: ```text id: zh-CN displayName: 中文 uiLocale: zh-CN systemMatchers: zh, zh-CN, zh-Hans, zh-Hant defaultModelId: sensevoice-zh upgradeModelIds: [] ``` Round 1 不使用它做 UI,也不做系统语言自动切换。 ### 6.4 ModelInstallState Round 1 必须落地最小版 `models/state.json`。 文件位置: ```text models/state.json ``` 路径字段必须存相对 `models` 根目录的相对路径,避免 App Support 目录变化后绝对路径失效。 建议结构: ```json { "schemaVersion": 1, "selectedModelId": "sensevoice-zh", "installedModels": { "sensevoice-zh": { "installedAt": 1780120000000, "version": "unknown", "path": "sensevoice-zh", "sourceDirKind": "new" } } } ``` 字段说明: | 字段 | 说明 | |---|---| | `schemaVersion` | 状态文件 schema 版本,本轮为 `1` | | `selectedModelId` | 当前选择模型,本轮默认 `sensevoice-zh` | | `installedModels[modelId].path` | 相对 `models` 根目录的路径 | | `installedModels[modelId].sourceDirKind` | `new` 或 `legacy` | 状态文件原则: - `state.json` 是索引,不是真相。 - required files 校验结果优先于 `state.json`。 - `state.json` 不存在、为空、非法 JSON、model id 未知、路径不存在时,不得导致旧模型不可用。 - 发现 state stale 但 fallback 找到完整模型时,可以 best-effort 修复 state;修复失败不阻塞模型加载。 ### 6.5 ResolvedModel `EngineFactory` 不直接接收 `ModelProfile`,也不自己查路径。它只接收 `ModelResolver` 产出的 `ResolvedModel`。 建议结构: ```go type ModelInstallStatus string const ( ModelNotInstalled ModelInstallStatus = "not_installed" ModelInstalledValid ModelInstallStatus = "installed_valid" ModelInstalledIncomplete ModelInstallStatus = "installed_incomplete" ModelStateInvalidFallbackFound ModelInstallStatus = "state_invalid_fallback_found" ModelStateInvalidNoFallback ModelInstallStatus = "state_invalid_no_fallback" ) type ResolvedModel struct { ModelID string Profile ModelProfile RootDir string SourceDirKind string Files map[string]string BackendKind string ProviderOrder []string Status ModelInstallStatus Missing []string Problems []string } ``` `Files` 示例: ```json { "model": "/Users/ar/Library/Application Support/PrivateVoice Input/models/sensevoice-zh/model.int8.onnx", "tokens": "/Users/ar/Library/Application Support/PrivateVoice Input/models/sensevoice-zh/tokens.txt" } ``` 进入 engine 初始化的前置条件: ```text Status == installed_valid 或 Status == state_invalid_fallback_found ``` 其他状态不得进入 engine 初始化。 ## 7. 路径解析规则 Round 1 默认当前模型为: ```text sensevoice-zh ``` 路径候选顺序: 1. `models/state.json` 中 `sensevoice-zh` 记录的路径。 2. 新目录 `models/sensevoice-zh`。 3. 旧目录 `models/sensevoice`。 每个候选目录都必须经过 required files 校验。 解析算法: 1. 读取 `models/state.json`。 2. 如果 state 可读且包含 `sensevoice-zh`,把其中路径作为第一候选。 3. 追加新目录 `models/sensevoice-zh`。 4. 追加旧目录 `models/sensevoice`。 5. 去重后按顺序校验。 6. 第一个完整候选即为 resolved model。 7. 如果 state 路径无效但后续目录找到完整模型,返回 `state_invalid_fallback_found`。 8. 如果所有候选都不存在,返回 `not_installed`。 9. 如果存在候选目录但文件缺失或 0 字节,返回 `installed_incomplete`,并列出缺失/异常项。 10. 如果 state 无效且没有 fallback,返回 `state_invalid_no_fallback`。 优先级规则: - 没有 state 时,新目录优先于旧目录。 - state 指向旧目录且旧目录完整时,允许继续使用旧目录。 - state 指向损坏目录时,不得直接失败,必须继续尝试新目录和旧目录。 - 新旧目录都完整且没有 state 时,使用新目录。 - 不自动迁移旧目录。 - 不自动删除任何完整模型目录。 ## 8. 下载与安装规则 Round 1 不接入新模型,但下载器必须向通用 `DownloadModel(profile)` 边界靠拢,内部只实现 SenseVoice。 ### 8.1 公开调用兼容 前端现有调用必须继续可用: ```go DownloadModel(primaryURL string, fallbackURL string) error ``` Round 1 行为: - `DownloadModel(primaryURL, fallbackURL)` 内部映射到默认模型 `sensevoice-zh`。 - 传入的 `primaryURL` / `fallbackURL` 继续作为下载 URL 权威来源。 - `ModelProfile.DownloadURLs` 可以作为内部默认值或后续扩展,但 Round 1 不要求前端改为传 `modelID`。 ### 8.2 安全安装流程 下载必须使用 staging 流程: ```text models/.downloads/sensevoice-zh//archive models/.downloads/sensevoice-zh//extract models/.staging/sensevoice-zh- models/sensevoice-zh ``` 流程: 1. 创建本次下载临时目录。 2. 下载到临时 archive 文件。 3. 解压到临时 extract 目录。 4. 在解压结果中定位 SenseVoice 模型目录。 5. 复制或移动到 staging 目录。 6. 对 staging 目录执行 required files 校验。 7. 校验通过后再落到最终目录 `models/sensevoice-zh`。 8. 写入或更新 `models/state.json`。 9. 清理本次临时目录。 失败处理: - 下载失败不得修改 `models/sensevoice`。 - 下载失败不得修改已有完整的 `models/sensevoice-zh`。 - 解压失败不得写入 state 为已安装。 - 校验失败不得写入 state 为已安装。 - 失败后再次下载不要求用户手动清理。 - 临时目录残留允许存在,但不得被 Resolver 误判为已安装模型。 目标目录处理: - 如果 `models/sensevoice-zh` 不存在,校验通过后移动 staging 到该目录。 - 用户主动触发下载时,可以用 staging 校验通过的新模型替换 `models/sensevoice-zh`。 - 替换 `models/sensevoice-zh` 前应先 rename 到 backup 临时名;替换失败时必须恢复 backup。 - 如果 primary URL 下载成功但解压或校验失败,必须继续尝试 fallback URL。 - 任何情况下不得删除完整的旧目录 `models/sensevoice`。 ## 9. EngineFactory 规则 ### 9.1 对外兼容 现有调用入口必须保留: ```go func New() (Engine, error) ``` Round 1 中: - `engine.New()` 内部使用当前 selected model。 - 如果 `SelectedModelID` 为空,默认 `sensevoice-zh`。 - `engine.New()` 调用 Resolver 获取 `ResolvedModel`。 - Resolver 返回可用模型后,交给 EngineFactory 初始化。 建议新增内部入口: ```go func NewWithModelID(modelID string) (Engine, error) func NewWithResolvedModel(resolved model.ResolvedModel) (Engine, error) ``` 平台实现应从无参: ```go newPlatformEngine() ``` 改为接收已解析路径: ```go newPlatformEngine(resolved model.ResolvedModel) ``` ### 9.2 SenseVoice config builder Round 1 只需要支持: ```text backendKind = sensevoice ``` 生成现有 sherpa-onnx config: ```go config.ModelConfig.SenseVoice.Model = resolved.Files["model"] config.ModelConfig.SenseVoice.UseInverseTextNormalization = 1 config.ModelConfig.Tokens = resolved.Files["tokens"] config.ModelConfig.NumThreads = resolved.Profile.NumThreads config.ModelConfig.Provider = provider ``` provider 顺序: ```text coreml -> cpu ``` 用户可见硬件信息保持现状风格: ```text SenseVoice · CoreML (Apple Neural Engine) SenseVoice · CPU ``` 不要向用户展示 `sensevoice-zh` 这种内部 profile id。 ### 9.3 初始化失败 如果 sherpa-onnx 初始化失败: - 不得导致 Wails 后端崩溃。 - 不得导致主窗口卡死。 - 不得占用麦克风资源。 - 不得改变 selected model。 - 不得把不完整模型写成可用状态。 - 应通过现有 `engine:status` 事件返回错误状态。 ## 10. 配置兼容 Round 1 在 `config.json` 中新增隐藏字段: ```go type Config struct { // existing fields... SelectedModelID string `json:"SelectedModelID,omitempty"` } ``` 兼容规则: - 老配置没有 `SelectedModelID` 时,加载后默认 `sensevoice-zh`。 - `SelectedModelID` 为空时,默认 `sensevoice-zh`。 - `SelectedModelID` 是未知 model id 时,回退 `sensevoice-zh`,记录日志。 - 保存配置时不得丢失现有字段。 - `ModelDownloadUrl` 和 `FallbackModelDownloadUrl` 本轮继续保留。 - Round 1 不新增 `LanguageMode` / `LanguageID` 的运行时逻辑;这些留到 Round 2。 ## 11. Wails 与前端兼容契约 Round 1 不要求前端改 UI,也不要求前端切换到新模型 API。 必须保持以下 Wails public API: ```go EngineService.ModelExists() bool EngineService.DownloadModel(primaryURL string, fallbackURL string) error ConfigService.GetConfig() ``` 语义要求: - `ModelExists()` 返回“当前 selected/default model 是否完整可用”。 - 目录存在但 required files 缺失时,`ModelExists()` 必须返回 `false`。 - `DownloadModel(primaryURL, fallbackURL)` 继续触发现有下载进度事件。 - `ConfigService.GetConfig()` 继续返回 `ModelDownloadUrl` 和 `FallbackModelDownloadUrl`。 - 可以新增内部方法或新增 public 方法,但不能破坏旧方法。 必须保持以下事件契约: ```text model:download-progress ``` payload: ```json { "percent": 42.5, "downloaded": 123456, "total": 789012 } ``` ```text engine:status ``` 状态语义继续支持: ```text loading ready need_model error ``` `ready` 时继续带: ```json { "hardwareInfo": "SenseVoice · CoreML (Apple Neural Engine)" } ``` 前端现有链路必须保持: ```text App 启动 -> EngineService.ModelExists() -> need_model -> OnboardingView -> EngineService.DownloadModel(primaryURL, fallbackURL) -> model:download-progress -> engine:status ready ``` ## 12. UI 要求 Round 1 原则上不新增 UI。 允许的 UI 变化: - 错误文案更清晰。 - 模型缺失提示仍然指向下载 SenseVoice。 - 下载进度行为保持现状。 不允许的 UI 变化: - 不展示模型列表。 - 不展示语言版本列表。 - 不展示 Moonshine。 - 不展示 Parakeet。 - 不展示 Qwen3-ASR。 - 不引导用户选择模型。 - 不展示内部 profile id。 ## 13. 离线与隐私要求 Round 1 不改变隐私承诺: - 识别仍然本地离线执行。 - 用户音频不上传。 - 用户历史不上传。 - 用户词库不上传。 - replacement 规则不上传。 允许联网的场景: - 用户无模型且触发模型下载。 不允许联网的场景: - 已有完整模型时冷启动识别。 - 录音识别过程中。 - 用户词库处理过程中。 - 历史记录写入过程中。 - 为了判断模型是否存在而请求远程服务。 ## 14. 验收标准 ### 14.1 老用户兼容 - 已存在 `models/sensevoice` 且文件完整的用户,升级后不需要重新下载模型。 - 应用能正常初始化 SenseVoice。 - 不自动移动旧目录。 - 不自动删除旧目录。 - 用户词库、历史、设置文件升级前后存在且字段不丢。 ### 14.2 新用户流程 - 新用户无 `models` 目录时,现有 Onboarding 流程仍然可用。 - 下载完成后模型最终位于 `models/sensevoice-zh`。 - `models/state.json` 写入 `selectedModelId = sensevoice-zh`。 - required files 校验通过后 engine 初始化成功。 ### 14.3 状态文件兼容 - `state.json` 不存在时,可以通过目录探测找到模型。 - `state.json` 非法 JSON 时,不影响完整模型加载。 - `state.json` 指向不存在目录时,应 fallback 到新目录和旧目录。 - `state.json` 指向不完整目录时,应 fallback 到其他完整目录。 - fallback 成功时,可 best-effort 修复 state;修复失败不阻塞加载。 ### 14.4 模型完整性 - 只有 `tokens.txt` 时,模型不可用。 - 只有模型文件没有 `tokens.txt` 时,模型不可用。 - `model.int8.onnx` 和 `model.onnx` 都不存在时,模型不可用。 - 任何 required file 为 0 字节时,模型不可用。 - 同时存在 `model.int8.onnx` 和 `model.onnx` 时,优先使用 `model.int8.onnx`。 - 不完整模型不得进入 engine 初始化。 ### 14.5 下载失败保护 - 下载 URL 失败时,已有完整模型仍可继续使用。 - 压缩包损坏时,已有完整模型仍可继续使用。 - 解压失败时,不写入已安装 state。 - 校验失败时,不写入已安装 state。 - 下载失败后再次下载可以重试。 - 失败残留临时目录不得被误判为已安装模型。 ### 14.6 Wails 与 UI 回归 - `EngineService.ModelExists()` 签名不变。 - `EngineService.DownloadModel(primaryURL, fallbackURL)` 签名不变。 - `ConfigService.GetConfig()` 继续返回旧下载 URL 字段。 - `model:download-progress` payload 不变。 - `engine:status` 事件语义不变。 - 设置页、首页、下载页不出现 Moonshine、Parakeet、Qwen3-ASR、语言选择、模型选择入口。 ### 14.7 主流程回归 至少验证: - 应用冷启动。 - 模型不存在时进入下载流程。 - 模型已存在时不进入下载流程。 - 一次完整语音识别。 - 用户词库 replacement 仍然生效。 - 历史记录仍然写入。 - 权限页、首页、词库页、历史页不受影响。 - 完全断网且已有模型时,启动、录音、识别、历史写入都可用。 ## 15. 测试建议 ### 15.1 单元测试 覆盖: - `ModelRegistry` 根据 ID 获取 profile。 - 未知 model id 返回错误。 - `LanguageRegistry` 至少包含 `zh-CN`。 - required files 的 `allOf` 规则。 - required files 的 `anyOf` 规则。 - 同时存在 `model.int8.onnx` 和 `model.onnx` 时选择 int8。 - 只有 `model.onnx` 时通过。 - 缺少 `tokens.txt` 时失败。 - required file 为 0 字节时失败。 - 目录不存在时返回 `not_installed`。 - state 缺失时 fallback 到目录探测。 - state 非法 JSON 时 fallback 到目录探测。 - state 指向坏目录但旧目录完整时返回可用。 - 新旧目录都完整且无 state 时使用新目录。 - `SelectedModelID` 缺失时默认 `sensevoice-zh`。 - 无效 `SelectedModelID` 回退 `sensevoice-zh`。 ### 15.2 集成测试 覆盖: - 使用旧 `models/sensevoice` 目录初始化。 - 使用新 `models/sensevoice-zh` 目录初始化。 - `state.json` 指向旧目录时初始化。 - `state.json` 损坏但新目录完整时初始化。 - 下载 SenseVoice 后写入 `models/sensevoice-zh` 和 `state.json`。 - 下载失败不破坏已有旧目录。 - engine 初始化失败时不破坏 selected model。 ### 15.3 测试 fixture 建议补充固定测试数据: ```text privatevoice.src/testdata/asr/zh_short.wav ``` 建议音频内容: ```text 请帮我测试 SenseVoice 离线输入。 ``` 自动化识别验收不强制标点完全一致,至少要求包含关键词: ```text 测试 离线 输入 ``` 用户词库 fixture: ```json { "replacements": [ { "from": "sins voice", "to": "SenseVoice" } ] } ``` 历史 fixture: ```json { "retentionDays": 0, "entries": [] } ``` ### 15.4 QA 测试矩阵 | 场景 | 前置数据 | 网络 | 重点验收 | |---|---|---|---| | 老用户旧目录 | 只有 `models/sensevoice`,文件完整 | 断网 | 不重新下载,能初始化和识别 | | 新用户无模型 | 无 `models` 目录 | 联网 | 下载到 `models/sensevoice-zh`,校验通过 | | 新目录优先 | 新旧目录都完整,无 state | 断网 | 使用 `models/sensevoice-zh` | | state 指旧目录 | state path=`sensevoice`,旧目录完整 | 断网 | 使用旧目录且不迁移 | | state 损坏 | `state.json` 非法 JSON,新/旧目录完整 | 断网 | fallback 到目录探测 | | state 指坏目录 | state 指向不完整目录,旧目录完整 | 断网 | fallback 到旧目录 | | 目录不完整 | 只有 `tokens.txt` 或只有模型文件 | 断网 | 显示模型不可用,不初始化 engine | | 两种模型文件 | 同时有 `model.int8.onnx` 和 `model.onnx` | 断网 | 使用 int8 | | 下载失败 | 已有旧模型,下载 URL 失败 | 联网/断网 | 旧模型仍可用,旧目录不被破坏 | | 解压失败 | 已有旧模型,压缩包损坏 | 联网 | 旧模型仍可用,不写入完整 state | | 回归主流程 | 模型完整、词库和历史已有数据 | 断网 | 录音、上屏、replacement、历史写入正常 | | UI 回归 | 任意模型状态 | 任意 | 不出现新模型/语言选择入口 | | 权限回归 | 麦克风权限未授权/已授权 | 任意 | 权限页、首页、词库页、历史页可用 | ## 16. 发布前检查 Round 1 发布测试包前,必须完成: - 构建升级测试样本目录:旧目录完整、旧目录损坏、新目录完整、新旧同时存在、非法 state。 - 跑 Go 单元测试。 - 跑前端构建或现有可用的前端检查。 - 真机验证冷启动。 - 真机验证首次无模型下载。 - 真机验证已有模型识别。 - 真机验证麦克风权限未授权/已授权。 - 真机验证上屏文本。 - 真机验证 replacement。 - 真机验证历史写入。 - 断网验证已有模型识别全程可用。 - 断网验证无模型时不会出现异常联网重试风暴。 - 识别期间观察网络请求,确认没有上传音频、文本、历史、词库或自动请求模型元数据。 发布记录至少保留: - App 版本和 build 编号。 - macOS 版本。 - Apple Silicon / Intel 覆盖情况。 - 模型目录组合。 - 关键失败场景结果。 - 是否断网通过。 ## 17. 风险与对策 | 风险 | 影响 | 对策 | |---|---|---| | 抽象过度导致 Round 1 变大 | 延误后续 Round | 只实现 `sensevoice` backend,接口预留但不接新模型 | | 旧目录兼容遗漏 | 老用户升级后模型失效 | `models/sensevoice` 是必须兼容路径,纳入自动化和手工测试 | | state 和真实文件不一致 | 误判模型可用性 | required files 校验优先于 state | | 下载器覆盖旧模型 | 老用户可用模型被破坏 | staging + 校验 + 安全落盘,禁止删除完整旧目录 | | Wails API 改签名 | 前端运行时断裂 | Round 1 保持旧 public API | | EngineFactory 抽象不彻底 | 后续接 Moonshine/Parakeet 返工 | EngineFactory 只接收 `ResolvedModel` | | 不完整模型进入初始化 | 崩溃或卡死 | Resolver/Validator 阻止进入 engine | | 识别时触发联网 | 破坏离线承诺 | 已有模型时禁止冷启动/识别阶段请求远程 | ## 18. 交付物 Round 1 结束时应交付: - 可运行应用。 - `ModelProfile` / `ModelRegistry`。 - `LanguageProfile` / `LanguageRegistry` 最小实现。 - `RequiredFileRule`,支持 `allOf` / `anyOf`。 - `ModelInstallState`,读写 `models/state.json`。 - `ModelResolver`,支持 state、新目录、旧目录 fallback。 - `ResolvedModel`。 - `sensevoice-zh` profile。 - `EngineFactory` 初步实现。 - SenseVoice 新旧路径兼容。 - 下载器 staging + 安全落盘。 - `SelectedModelID` 隐藏配置,默认 `sensevoice-zh`。 - Wails public API 兼容。 - 对应单元测试和必要集成测试。 - 简短研发说明,说明后续新增 Moonshine profile 的接入点。 ## 19. 明确暂缓项 以下事项不进入 Round 1: - Moonshine 真实模型接入。 - Parakeet 真实模型接入。 - Qwen3-ASR 真实模型接入。 - 语言设置 UI。 - 模型选择 UI。 - 模型删除 UI。 - 自动迁移旧模型目录。 - 每个模型目录下的 `manifest.json`。 - checksum / 签名校验。 - 远程模型 catalog。 - 模型 marketplace。