edit | blame | history | raw

Round 1 PRD:模型 Profile 基础设施

状态:v0.2 评审修订稿
所属子项目:02-P-NBL
Round:Round 1
日期:2026-06-02
关联文档:

0. 本版修订结论

本版根据 2 位架构师、2 位程序员、2 位 QA 对 v0.1 PRD 的评审意见修订。

Round 1 的核心定位保持不变:

不改变用户体验,把现有 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.onnxmodel.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 完成后,系统内部应从:

写死 SenseVoice 路径和 SenseVoice config

演进为:

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/sensevoicemodels/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 采用以下职责划分:

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 表示一个可安装、可加载、可校验的识别模型。

建议结构:

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 defaultadvanced
SupportedLanguageIDs 模型理论支持的语言
RecommendedFor 本产品推荐该模型服务的语言
InstallDirName 新安装目录名
RequiredFiles 安装完整性规则
ProviderOrder 例如 macOS 可为 coreml,cpu

注意:

  • 必须区分 SupportedLanguageIDsRecommendedFor
  • SenseVoice 支持多语言,但 Round 1 产品推荐仍是中文。
  • 后续 Parakeet 支持欧洲语言,不能因为 English 高级定位而丢失多语言扩展能力。

6.2 RequiredFileRule

RequiredFiles []string 不够表达 SenseVoice 的二选一模型文件,因此 Round 1 必须支持结构化规则。

建议结构:

type RequiredFileRule struct {
	Role     string
	AllOf    []string
	AnyOf    []string
	Required bool
}

规则说明:

  • AllOf 中的路径必须全部存在。
  • AnyOf 中的路径至少存在一个。
  • 文件存在但为 0 字节,视为无效。
  • 目录类 required item 后续可扩展;Round 1 只需要文件。

SenseVoice profile:

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.onnxmodel.onnx 时,优先使用 model.int8.onnx
  2. 只有 model.onnx 时,允许使用 model.onnx
  3. 两者都不存在或文件为 0 字节,模型不完整。

6.3 LanguageProfile

Round 1 只做最小可编译语言 profile,不做运行时语言切换。

建议结构:

type LanguageProfile struct {
	ID              string
	DisplayName     string
	UILocale        string
	SystemMatchers  []string
	DefaultModelID  string
	UpgradeModelIDs []string
}

Round 1 至少注册:

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

文件位置:

models/state.json

路径字段必须存相对 models 根目录的相对路径,避免 App Support 目录变化后绝对路径失效。

建议结构:

{
  "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 newlegacy

状态文件原则:

  • state.json 是索引,不是真相。
  • required files 校验结果优先于 state.json
  • state.json 不存在、为空、非法 JSON、model id 未知、路径不存在时,不得导致旧模型不可用。
  • 发现 state stale 但 fallback 找到完整模型时,可以 best-effort 修复 state;修复失败不阻塞模型加载。

6.5 ResolvedModel

EngineFactory 不直接接收 ModelProfile,也不自己查路径。它只接收 ModelResolver 产出的 ResolvedModel

建议结构:

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 示例:

{
  "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 初始化的前置条件:

Status == installed_valid
或
Status == state_invalid_fallback_found

其他状态不得进入 engine 初始化。

7. 路径解析规则

Round 1 默认当前模型为:

sensevoice-zh

路径候选顺序:

  1. models/state.jsonsensevoice-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 公开调用兼容

前端现有调用必须继续可用:

DownloadModel(primaryURL string, fallbackURL string) error

Round 1 行为:

  • DownloadModel(primaryURL, fallbackURL) 内部映射到默认模型 sensevoice-zh
  • 传入的 primaryURL / fallbackURL 继续作为下载 URL 权威来源。
  • ModelProfile.DownloadURLs 可以作为内部默认值或后续扩展,但 Round 1 不要求前端改为传 modelID

8.2 安全安装流程

下载必须使用 staging 流程:

models/.downloads/sensevoice-zh/<run-id>/archive
models/.downloads/sensevoice-zh/<run-id>/extract
models/.staging/sensevoice-zh-<run-id>
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 对外兼容

现有调用入口必须保留:

func New() (Engine, error)

Round 1 中:

  • engine.New() 内部使用当前 selected model。
  • 如果 SelectedModelID 为空,默认 sensevoice-zh
  • engine.New() 调用 Resolver 获取 ResolvedModel
  • Resolver 返回可用模型后,交给 EngineFactory 初始化。

建议新增内部入口:

func NewWithModelID(modelID string) (Engine, error)
func NewWithResolvedModel(resolved model.ResolvedModel) (Engine, error)

平台实现应从无参:

newPlatformEngine()

改为接收已解析路径:

newPlatformEngine(resolved model.ResolvedModel)

9.2 SenseVoice config builder

Round 1 只需要支持:

backendKind = sensevoice

生成现有 sherpa-onnx config:

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 顺序:

coreml -> cpu

用户可见硬件信息保持现状风格:

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 中新增隐藏字段:

type Config struct {
	// existing fields...
	SelectedModelID string `json:"SelectedModelID,omitempty"`
}

兼容规则:

  • 老配置没有 SelectedModelID 时,加载后默认 sensevoice-zh
  • SelectedModelID 为空时,默认 sensevoice-zh
  • SelectedModelID 是未知 model id 时,回退 sensevoice-zh,记录日志。
  • 保存配置时不得丢失现有字段。
  • ModelDownloadUrlFallbackModelDownloadUrl 本轮继续保留。
  • Round 1 不新增 LanguageMode / LanguageID 的运行时逻辑;这些留到 Round 2。

11. Wails 与前端兼容契约

Round 1 不要求前端改 UI,也不要求前端切换到新模型 API。

必须保持以下 Wails public API:

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() 继续返回 ModelDownloadUrlFallbackModelDownloadUrl
  • 可以新增内部方法或新增 public 方法,但不能破坏旧方法。

必须保持以下事件契约:

model:download-progress

payload:

{
  "percent": 42.5,
  "downloaded": 123456,
  "total": 789012
}
engine:status

状态语义继续支持:

loading
ready
need_model
error

ready 时继续带:

{
  "hardwareInfo": "SenseVoice · CoreML (Apple Neural Engine)"
}

前端现有链路必须保持:

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.onnxmodel.onnx 都不存在时,模型不可用。
  • 任何 required file 为 0 字节时,模型不可用。
  • 同时存在 model.int8.onnxmodel.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.onnxmodel.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-zhstate.json
  • 下载失败不破坏已有旧目录。
  • engine 初始化失败时不破坏 selected model。

15.3 测试 fixture

建议补充固定测试数据:

privatevoice.src/testdata/asr/zh_short.wav

建议音频内容:

请帮我测试 SenseVoice 离线输入。

自动化识别验收不强制标点完全一致,至少要求包含关键词:

测试
离线
输入

用户词库 fixture:

{
  "replacements": [
    {
      "from": "sins voice",
      "to": "SenseVoice"
    }
  ]
}

历史 fixture:

{
  "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.onnxmodel.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。