最佳实践
为一个新的歌声合成工程格式编写插件时,按目标格式的类型选择解析与生成技术。下表是本仓库的选型速查,每一行都给出可直接参考的现有插件:
| 格式类型 | 解析 / 验证 | 生成 | 参考插件 |
|---|---|---|---|
| JSON / CBOR / YAML 等标准序列化 | 序列化库转 dict → pydantic 验证 | pydantic → 序列化库 | ace、ds、vpr、tlpx、acep、ustx |
| XML | xsdata(xsdata-pydantic) |
xsdata XmlSerializer | vsqx、ccs、musicxml、vspx、xvsq |
| 自定义文本格式 | tatsu PEG 文法 | jinja2 模板 | ust、nn |
| Protobuf | protobuf-py 运行时 |
同左 | svip3 |
| 自定义二进制 | construct | construct | dv、mid、mtp、ppsf、svip、vshp、vsq、vxf |
常见序列化格式:pydantic 验证
这类格式的分层很清晰:序列化库(json / cbor2 / yamlutils.load_yaml_1_2 等)只负责字节 ↔ dict,结构验证全部交给 pydantic——model.py 是一棵 pydantic 模型树,parser 里 FormatModel.model_validate(已反序列化的 dict) 一步完成(YAML 格式参考 ustx,CBOR 参考 tlpx)。
初版模型不必手写:用 datamodel-code-generator(在 code_gen 依赖组,命令名叫 datamodel-codegen)从一份真实样例文件生成:
bash
uv run --group code_gen datamodel-codegen \
--input 样例文件.json \
--output libresvip/plugins/<format>/model_gen.py \
--output-model-type pydantic_v2.BaseModel \
--base-class libresvip.model.base.BaseModel \
--snake-case-field --disable-timestamp
--snake-case-field 会把 camelCase 键转成 snake_case 字段并自动保留原始键名作为别名(生成 full_name: str = Field(..., alias='fullName')),正好对齐本仓库“snake_case 字段 + camelCase 别名 + by_alias=True 序列化”的约定,省掉最机械的手写工作;--base-class 让模型直接继承仓库的 BaseModel;--disable-timestamp 保证重复生成不产生无谓 diff。生成结果仍是初版——之后照代码规范清理:把必填改成可选并给默认值、删掉转换用不到的冗余字段。
XML:xsdata
XML 格式统一用 xsdata 处理,仓库用的是它的 pydantic 集成 xsdata-pydantic(XmlParser / XmlSerializer)。生成的模型放进插件目录的 models/ 子包(参考 vsqx:models/vsqx3.py、models/vsqx4.py):
bash
uv run --group code_gen xsdata generate --output xsdata_pydantic 样例文件或xsd -o libresvip/plugins/<format>/models/
scripts/ts_model.py 是同一个套路应用在 Qt Linguist .ts 文件上的例子——为"格式本身没有官方 schema"的情况提供了参照:拿一份样例文件生成模型,再手工修整。
自定义文本格式:tatsu 文法 + jinja2 模板
解析侧用 tatsu(仓库锁定 tatsu-lts),PEG 文法直接以字符串内嵌在 model.py 顶部,tatsu.compile(...) 编译后用 NodeWalker 遍历语法树(完整范例见 nn 与 ust 插件的 model.py)。不要用正则拼自定义文本解析器——文法写一次,后续遇到边角格式(引号、转义、可选段)时改一行文法即可。
生成侧不要手写字符串拼接。仓库封装了统一的 jinja2 环境 libresvip.core.compat.jinja_env:在插件的 template.py 里用 add_template("格式名", "模板字符串") 注册,再 render_template("格式名", **上下文) 渲染(参考 ust、lrc、nn 的 template.py)。
Protobuf:buf 生成 + protobuf-py 运行时
.proto 文件统一放在 libresvip/res/protos/(现有 libresvip.proto、svip3.proto)。生成流程:
bash
./scripts/gen_proto_files.sh # 内部执行 cd libresvip/res/protos && uv run -- buf generate
buf.gen.yaml 使用本仓库的 protoc-gen-py 插件,生成的 *_pb.py(内嵌序列化的 file descriptor)直接提交进仓库,运行时用 protobuf-py 而非 google 官方 protobuf 包(API 为 from protobuf import Message / protobuf.wkt)。
从闭源软件提取 proto 定义:pbtk
目标软件没有公开 schema 时(如 X Studio 的 svip3),用 pbtk(在 code_gen 依赖组)从其二进制可执行文件中提取内嵌的 protobuf descriptor,导出成 .proto 后放进 libresvip/res/protos/ 再走上面的生成流程。svip3.proto(package xstudio.proto)就是这么来的。
自定义二进制格式:construct
自定义二进制结构用 construct(仓库用 construct-typing 提供类型标注)声明式描述,解析与生成共用同一个结构定义。参考 dv/model.py、mid/midi_parser.py、ppsf/legacy_model.py。
经验法则:
- 结构体写成模块级常量(
Struct(...)/Padded(...)/Array(...)嵌套),不要在函数里现拼; - 整个文件的容错(加密区段、版本分支、头部魔数)放在 construct 结构之外,用普通 Python 处理(参考
acep的加密区段处理); - 大端/小端、对齐与字符串编码在结构定义里显式声明。
逆向与辅助工具速查
| 工具 | 用途 | 所在依赖组 |
|---|---|---|
| datamodel-code-generator | 从样例 JSON/YAML 生成 pydantic 模型初版 | code_gen |
| xsdata CLI | 从 XSD / 样例 XML 生成模型 | code_gen |
| pbtk | 从二进制可执行文件提取 protobuf 定义 | code_gen |
| buf + protoc-gen-py | 从 .proto 生成 Python 绑定 |
code_gen(脚本 scripts/gen_proto_files.sh) |
babel(scripts/extract_messages.py、batch_compile_catalog.py) |
抽取与编译翻译目录 | i18n / CI |
scripts/ 下其余脚本与格式插件无关:gen_snake_case_pyi.sh 生成 PySide6 的 snake_case stub(GUI 开发用),ts_model.py 是 Qt .ts 翻译文件的 xsdata 模型(配合 extract_messages.py 把 Qt 翻译同步进 gettext 流程)。