开发约定
为 Mizuoto 写代码要遵守的约定:代码风格、分层规则、原生代码的规则、结果一致性规则,新增命令、路由、设置和引擎选项的步骤,测试要求,双语文档和提交信息的格式。
本页列出改动 Mizuoto 时必须遵守的约定。大多数约定都有测试或检查守着:python scripts/dev.py check 通过,是一个改动可以合并的最低要求。每条约定背后的设计见整体架构和设计参考与决策。
代码风格
- Python ≥ 3.10,支持 3.10–3.13。
ty以 3.10 为目标检查类型,所以不要用 3.11 之后才有的语法和标准库(tomllib要有tomli回退)。 - ruff 与 ty 不锁版本,
pyproject.toml把lint.select逐条写出(E4、E7、E9、F、I、UP、B、SIM、RUF100、ANN),所以升级 ruff 不会悄悄改变规则集。行宽 180,缩进 4。ty 以error-on-warning运行。python scripts/dev.py format格式化并做安全的自动修复,lint和typecheck-py只检查。 - 每个函数都有类型标注(
mizuoto/和packages/native/下;测试和脚本除外)。Any只用在值确实可以是任何东西的地方(JSON)。 - 跨越 API 边界的模型都继承
Record(mizuoto/core/record.py,pydantic v2,json_schema_serialization_defaults_required):设置、请求、响应、事件、--json的输出都是。有默认值的字段在输出模式里是必填的,生成的 TypeScript 类型因此与服务器实际发送的一致。 - 重依赖在函数内导入:核心里的 av、numpy 热路径,命令行里的 fastapi、uvicorn,以及
core/inference里的所有模型代码。mizuoto version和mizuoto --help因此保持快速,测试会检查这一点。 - 有类注解里用到
list[...]的类不定义名为list的方法(ty 会把这个名字解析成方法)。写成list_models、list_voices、list_presets。 - 标识符和时间用
mizuoto/core/clock.py:new_id(prefix)(毫秒时间戳的十六进制加 8 个随机十六进制字符,大致按创建时间排序)和now_iso()(UTC,毫秒,以Z结尾,可按文本排序)。 - 错误只用
mizuoto/core/errors.py的类,错误码只在mizuoto/core/codes.py。给用户的修复命令放在detail["hint"],命令行会把它作为第二行打出,界面显示在错误下方。不要在路由或命令里自己拼 HTTP 状态码或退出码。 - 日志用
mizuoto日志记录器(logging.getLogger(__name__)位于mizuoto.*之下);只有入口(mizuoto/logger.py的setup_logging)配置处理器。 - 与上游库逐行对应、需要保持可比对的代码(
mizuoto/nn/dsp/kaldi.py,torchaudio 的 fbank)按原样保留,在pyproject.toml里单独豁免 lint 规则,而不是改写。
分层规则
tests/test_architecture.py 用 ast 检查每个模块的导入,规则表见整体架构。要点:
core、api、cli永远不导入 torch、torchaudio、transformers、onnxruntime 或需要接受许可的引擎包,函数内也不行。core只在core/inference的函数内部触及runtime、nn、engines和training。- 路由和命令很薄:解析输入、调用一个核心方法、整理输出。每个操作只有一个核心方法,所以 API 的 JSON 与命令行的
--json形状相同。业务逻辑不进api/和cli/。 - 引擎只导入
runtime、nn、text、training、core.codes、core.inference.tasks和自己的包,从不导入另一个引擎。两个引擎共用的代码放进nn或runtime。 core/codes.py、core/jobs/events.py、core/jobs/jsonl.py、core/remote/frames.py和core/inference/tasks.py只用标准库:训练阶段、远程帧和引擎都要用它们。- 新加一个第三方依赖,要同时把它加到
pyproject.toml和该层在tests/test_architecture.py里允许的列表中,并说明它为什么必要(见发布)。
原生代码的规则
引擎、训练器、运行时和服务器共用一个进程,所以 runtime、nn、text、engines 和 packages/native 下的代码要守这些规则(细节见运行时):
- 不改动进程级状态。 不给全局随机数生成器播种,不改
os.environ、工作目录或sys.path,不调用logging.basicConfig、torch.set_default_dtype、torch.use_deterministic_algorithms、torch.set_float32_matmul_precision,不给torch.backends.*或torch._inductor.config赋值,不用sdpa_kernel(它对所有线程切换 SDPA 的内核)。compute.fast的那几个开关只在mizuoto/runtime/accel/features.py里设置,由主机和训练阶段调用。 - 随机数来自请求。 用
ctx.generator_for(device)和ctx.rng,每个随机操作都显式传入生成器(torch.randn(..., generator=g)),并按上游的顺序、形状和设备抽取。randn_like等不接受生成器的调用改写成等价的显式形式。上游在构建模型时从全局生成器抽过随机数的,用私有生成器重放。 - 加载不抽随机数。 模块用
runtime.ops的层构建(ops.default跳过随机初始化),检查点严格加载(缺少或多出的键都是错误,除非引擎明确列为可忽略)。 - 加载在推理模式之外进行,参数保持
nn.Module创建时的样子(不调用requires_grad_(False))。CPU 上有两个会改变结果的内核陷阱:对非连续的三维输入,F.linear在权重requires_grad或权重是推理张量(在torch.inference_mode()里创建)时会选用另一个内核,即使处在no_grad或推理模式下。上游的权重是需要梯度的普通参数,所以原生引擎也这样保留;请求仍在推理模式里运行。 - 注意力都经过
accel.attention(q, k, v, site=…),不直接调用 SDPA 或它的 aten 内核,这样compute.attention才能切换后端(见注意力加速)。 - 不下载任何东西。 引擎用的文件都是模型文件或共享资源,由
ModelService下载并通过LoadSpec交给引擎。 - 模块级缓存要以完整输入为键(包括每个参数、dtype 和设备),或者干脆每次调用时构建。只按部分参数作键的全局缓存,会让同一进程里的第二种配置用上第一种的结果。
- 按阶段放置组件,按请求估算显存。 引擎用
ctx.use/ctx.run把每个阶段需要的组件放上设备,在模块forward之外读取权重的模块要标成fixed;需要 float64 的模块经DeviceService.float64_device放置。见显存管理。 - 可取消。 每个循环(自回归 token、ODE 步、分段、束搜索步)里调用
ctx.check_cancel()。Cancelled继承自BaseException,不要用宽泛的except BaseException吞掉它。
结果一致性
Mizuoto 的每个引擎都是对上游项目的移植,必须给出与上游相同的结果:在 CPU 上以 fp32、相同种子运行时,音频逐采样相同(训练器则是损失、学习率和保存的权重逐字节相同)。这条规则决定了很多写法:
- 先复现,再修正。 上游的怪癖(采样顺序、填充方式、对齐的边界情况)先原样复现,用对照工具证实一致;确有必要修正时,作为单独的改动,并记录偏差及原因。
- 偏差要记录。 每个与上游不同的地方(为了跨平台、为了不改全局状态、因为上游行为依赖线程时序等)写进该引擎的说明和设计记录
AGENTS.md,附上对照数字。 - 提速不改变结果。 性能工作(CUDA 图、同步消除、编译、内存流式加载)必须得到相同的 token、相同的音频;每一轮都用
scripts/parity/perf.py和train_perf.py与上一轮对照(见性能工作)。确实会改变比特的优化(如 Qwen3-TTS 的fused_projections、compute.kv_cache = static)只能是需要用户主动开启的选项,并在输出的参数里注明。 - GPU 上降精度运行时不要求逐比特相同,以模块级误差和统计指标判断。各层级的容差见测试。
新增东西的步骤
| 要新增 | 步骤 |
|---|---|
| 一条命令 | 在 mizuoto/cli/commands/ 写普通函数并提供 --json,在 cli/app.py 的 get_app() 中注册(名字和帮助写在注册处),加进 tests/cli/test_cli.py 的 EXPECTED_TREE。见新增一条命令 |
| 一个路由 | 核心方法(Record 进出)→ api/routers/ 里带 operation_id 的路由 → python scripts/dev.py typegen 并提交 schema.d.ts → tests/api/test_api.py。见新增一个路由 |
| 一个事件 | 在 core/events/models.py 加 EventBase 子类(__event_name__),它自动进入 OpenAPI 模式;重新生成类型,并在界面的 api/socket.ts 里处理它 |
| 一个设置项 | 在 core/settings/models.py 对应的组里加字段,带默认值和说明文档字符串;需要界面时在 Settings 界面加控件、两种语言的文字和 typegen;在配置页面的两个语言版本里写明。engines.<id>.* 的改动会让该引擎的模型卸载,下次按新设置加载 |
| 一个引擎选项 | 在清单里加 [options.<名字>](或加载选项 [load_options.<名字>]),并在引擎的 OPTION_SCHEMA 里加同样的项,测试要求两者完全一致。见接入引擎 |
| 一种任务 | 在服务的构造函数里 jobs.register(kind, factory),工厂返回带资源(engine:<引擎>:<设备>、gpu:<设备>、cpu、io…)的 JobSpec;结束时会影响哪些列表,就在界面 socket.ts 的 AFFECTS 里登记 |
| 一个引擎或训练器 | 见接入引擎和训练器 |
| 一个界面 | 见 Web UI:只从 @/ui 取组件、只用令牌写样式、两种语言的文字同时加 |
测试要求
- 每个改动都带测试,并让
python scripts/dev.py check(lint、typecheck-py、test-py、test-web、typegen-check)通过。CI 在 Linux、Windows、macOS 和 Python 3.10–3.13 上运行同样的测试,所以注意路径、编码和换行在 Windows 上的行为。 - 核心的行为用 dummy 引擎测(不需要 torch 和模型);外部网络用
httpx.MockTransport替代;测试用make_services得到隔离的数据目录和空环境变量。 - 改动引擎或训练器:更新
tests/engines_native/的小模型测试(包括模拟设备上的流式加载检查),用真实模型跑契约套件,并用对照工具与上游比较。 - API 改了就重新生成类型并提交,否则
typegen-check失败。界面改动要通过规则测试(导入、令牌、布局、浮层、i18n 键一致)。
文档
- 两种语言。 文档站的每个页面都有
<slug>.mdx(简体中文,默认语言)和<slug>.en.mdx(英文),结构逐节对应;侧边栏是meta.json和meta.en.json,顺序相同。测试检查每个中文页面都有英文版。README 也有两份(README.md、README.zh-CN.md),界面文字在en.ts和zh-CN.ts里,键必须一致。 - 改行为就改文档:用户可见的变化写进使用教程、命令行或进阶页面,开发者需要知道的写进开发文档。
- 设计记录。 仓库根目录的
AGENTS.md记录代码实际做了什么、守着哪些规则、为什么,以及长期有效的发现(例如为得到与上游相同的输出复现了哪些行为)。改了设计或得出了新发现,就同时更新它。 - 中文页面沿用已有页面的术语(音色、合成、克隆、模型、引擎、共享资源、许可、任务……)。写清楚是什么、为什么,不写开发过程。
提交信息
提交信息采用 类型(范围): 摘要 的格式,摘要用英文小写开头的祈使句或名词短语,说明改动做了什么:
feat(voxcpm): VoxCPM as a built-in engine, with full and LoRA fine-tuning
fix(runtime): size working memory by the request, so models load whole when they fit
perf(cosyvoice): CosyVoice 2/3 sample on the device and decode as CUDA graphs
docs(site): the mirror_fetch and mirror_readme scripts- 类型:
feat(新功能)、fix(修复)、perf(不改变结果的提速)、refactor(不改变行为的重构)、test、docs、build(构建、CI、Docker)、chore(杂项)。不兼容的改动在类型后加!,如refactor!:。 - 范围是受影响的部分:
runtime、webui、training、models、scripts、site,或引擎 id(qwen3tts、gptsovits等);跨越多处时可以写多个(runtime,training)或省略。 - 正文说明为什么这样做、对照或测量的结果,以及有意保留的行为。