Skip to content

审查:wolf 语言域两份草稿 vs 主线现状 #6

Description

@SineStriker

审查:wolf 语言域两份草稿 vs 主线现状,2026-08-25 20:28

审查对象:

  • D:\GitHub\synthrt-refactor\docs\language-level-1-draft.md(契约草案,311 行)
  • D:\GitHub\synthrt-refactor\docs\language-g2p-variants-wolf-draft.md(wolf 变体参考,273 行)

对照基准:主线 D:\GitHub\synthrt\docs\ds-spec-2.4.md(871 行)与 docs/DevHelp.md(786 行)。

总评:契约设计本身站得住——三段式 G2P/S2P/Onset 的切分、变体粒度即分派粒度、暴露面不泄露内部子资源标识,这几条都对,而且与 spec 2.4 的思路一致。问题几乎全部集中在与主线运行时脱节:草稿写于 spec 2.4 的一个较早版本(refactor 仓库那份 832 行的副本),而主线这段时间长出了 roleruntimeLevel、Load 事务、provider discovery 全序等一批机制。

其中有一处两份草稿互相矛盾,一处违反规范对 interface 契约的硬性要求


一、阻断级:不改就是错的

B1. 所有 imports 示例都缺 role,按现行规范是无效声明

role 出现次数:两份草稿各 0 次,主线 spec 2.4 的 Singer 示例 3 次

主线《imports》一节把 role 定为必选字段

  • 必选字段
    • role:该 import 在当前模块内唯一的用途名称,使用 contrib-id 的单段语法
    • ref:被引用模块的 ModuleReference

受影响的具体位置:

  • 契约草案 §5 的歌手 imports 示例(三条全缺)
  • 变体草案 §3 的 pipe-chain 声明示例(一条缺)

主线现在长这样:

"imports": [
    { "role": "acoustic", "ref": ":inference/acoustic" },
    { "role": "pitch", "ref": "bar:inference/pitch", "options": { "roles": ["pitch"] } }
]

改动量很小,但示例是被人照抄的,留着就会批量产出加载失败的包。

B2. pipe-chain 的序位绑定与运行时冲突,而且两份草稿自相矛盾

变体草案 §3 写:

imports 中第 i 个 G2P 条目注入第 i 个启用的 model 步(声明顺序对应)

以及 step 表注 2:

步内不写任何模块引用,后端取本模块 imports 中同序位的条目

而 DevHelp §2.3 明确:

代码应使用 ContribSpec::findImport(role) 定位 import,不依赖数组位置

更麻烦的是,契约草案 §1.5 自己说的是相反的话

语言 imports 按目标契约角色匹配,序位不承载语义

两份草稿对同一个机制给出了相反的规定。

需要说明的是,序位绑定并非不可实现——主线规范确实承诺「加载器必须保持声明顺序,不得排序或重排」。但它逆着 role 机制走,而 role 正是为这件事设计的。

建议model step 的 params 增加一个 role 字段,直接点名它消费哪个 import。这样 step 与 import 的对应关系写在配置里、可读可校验,也不再依赖「启用的 model 步」这个需要先解析整条链才能算出来的隐式序号。

B3. 语言 imports 的「按目标契约匹配」不是主线机制

契约草案 §1.5 的基数表按目标契约给出(G2P 恰好 1、S2P 恰好 1、Onset 0..1)。主线把这件事交给导入方的 variant:

role是导入方为该条目指定的本地 slot。导入模块通过role区分各项用途,并由自己的variant规定哪些 role 必须存在以及每个 role 接受哪一种目标契约

所以那张表应当重写成 role 表

role 目标契约 必需性
g2p org.openvpi.lang.G2pInference 必需
s2p org.openvpi.lang.S2pInference 必需
onset org.openvpi.lang.OnsetInference 可选

功能上两种写法在「恰好 1」的情形下等价,但主线的写法是唯一有效的表述方式,而且它天然支持将来一个 role 出现多个实例。

同时缺了一条:主线规定

Singer 等导入方不得仅因遇到自身不认识的 role 而拒绝整个模块;导入方仍可严格要求自己契约规定的 role 存在且只指向规定的目标契约。

契约草案 §1.5 说「Level 1 内集合恒定」,容易被读成「出现表外 role 即失败」。必须补一句:表列 role 必须存在且契约正确,表外 role 一律放行。这条正是为了让第三方类别以后能往语言里挂新 role 而不破坏兼容。


二、设计级:与规范的硬性要求冲突

D1. error 的值域下放给变体,违反《interface 契约规范》

契约草案 §2.4 的 error 变量写着「失败类型(值域枚举见各变体文档)」,而变体草案里散落着具体值:PhonemeGenerationFailed(§3 注 4)、ModelInferenceFailedDriverUnavailable(§3 model 步行为)。

主线《interface 契约规范》要求契约规范至少必须包含

  • 运行时输入与输出的数据结构、数据类型、形状和单位
  • 必选能力、可选能力及其语义
  • 错误条件及其可观察语义

error 是契约面的输出变量,导入方要根据它决定兜底。值域由变体定义意味着换一个变体,导入方的错误处理就失效——这恰好是 variant 轴存在的前提(同契约各变体对导入方可互换)被打破。

建议:把 error 的枚举提升为契约词,Level 1 至少固定几个通用值(转换失败 / 后端不可用 / 语言不支持之类),变体只能在契约允许的范围内细化,不得自定义新值。变体草案里现有那三个值正好可以作为契约值的候选。

顺带一提,同一节的 mode 枚举(convert/copy/skip)是写死在契约里的,处理方式是对的——error 应当照它办。

D2. 类别注册名用裸词 language 存疑

契约草案 §1.1 写「本类别注册名为 language」。而主线里凡是举语言类别的例子,用的都是反向域名:

  • spec 2.4《imports》::com.vendor.language/cmn 引用「一个第三方语言模块」
  • DevHelp §3.1:ContribCategoryRegistry::Add<MyCategory>("com.example.language", "")

按 spec 2.4 对 variant 的规定,裸词是规范自身保留、第三方用反向域名。类别名很可能适用同一条规矩,而 wolf 相对 synthrt 是第三方。

这条我没有找到规范里的明文规定(只有两处示例的一致倾向),所以列为待确认而非错误。但它影响所有语言包的 desc.json 和所有 ModuleReference 的写法,值得在动手前问清楚——改起来是全量的


三、同步级:陈旧引用与遗漏

S1. 行号引用已经失效

变体草案里有 spec 2.4(:561):565/:571/:589:603/:619 一类引用。这些指向的是 refactor 仓库那份 832 行的副本,主线已经是 871 行且章节重排过,行号对不上了。

契约草案用的是章节名引用(《模块 provider 发现》《何时递增 Level》),这些我逐条查过,在主线里全部存在。建议变体草案统一改成章节名——行号引用进活文档就是维护陷阱。

S2. 未提及 runtimeLevel

runtimeLevel 在草案中出现 0 次,主线 spec 中 11 次。DevHelp §2.4:

desc.json 必须显式写 runtimeLevel: 1

契约草案 §6 在规定公共语言包该长什么样,却没提这个必选字段。至少要在 §6 提一句。

S3. 未说明变量展开由谁执行

两份草稿都正确写了「使用前已完成 ${vars} 展开」,但没说是谁展开的。DevHelp §2.4:

Loader 会在 Category 和解释器看到 JSON 前统一完成展开,扩展代码不得再次展开字符串

对 wolf 的解释器实现者是要紧的一句——重复展开会把用户数据里的 ${...} 二次解释。建议在两份草稿的「路径解析」约定里各补一句。


四、已经对齐的部分(不必再动)

为免下一轮重复劳动,把核查过、确认与主线一致的列出来:

  • exports 这个词——主线已从 schema 改名 exports,两份草稿用的都是新名
  • $version 只写在 desc.json,模块声明不携带 $versionid——与主线一致
  • 模块三元组 (interface, level, variant) 全部必填、精确匹配——一致
  • configurationvariant 全权规定、不参与 provider 选择——与主线《模块 provider 发现》末段一致
  • provider 首匹配规则——契约草案 §2.5 的「按搜索全序取首个」与主线一致;变体草案 §1.1 的「同一变体名只许一个 provider 承载」被正确地表述为 wolf 的自律而非规范规则
  • formatVersionlevel 分层——变体草案 §1.2 的两层划分,与主线《何时递增 Level》的判据同构
  • 多语言 name——主线已改用 BCP 47,草稿未涉及具体语言码,无冲突
  • 章节名引用——契约草案引用的《贡献类别是开放的》《推理解释器》《何时递增 Level》《模块 provider 发现》《interface 契约规范》《加载事务》《字符串变量》在主线中逐条存在

五、命名空间的一个观察(非缺陷)

契约草案用 org.openvpi.lang.*LanguageG2pInferenceS2pInferenceOnsetInference),主线 Singer 示例用的是 org.openvpi.svs.singer.DiffSinger,Inference 用 org.openvpi.svs.*

即 SVS 域的层次是 org.openvpi.svs.<类别>.<契约>,而语言域用的是 org.openvpi.lang.<契约>,少一层。两种都合法,但如果 svs.singer. 这个多一层的形态是有意为之(为了同类别下多契约留位置),语言域大概也该跟上,写成 org.openvpi.lang.g2p.* 之类。

这纯属一致性问题,不影响功能,但契约名一旦发布就不能改,值得现在定。


六、建议的处理顺序

  1. B1(补 role)——机械改动,先做,防止示例被照抄
  2. D2(类别名)与第五节(契约命名空间)——两个「一旦发布不可改」的决定,要在写代码前定
  3. B3(role 表)与 B2(pipe-chain 绑定)——同一个机制的两面,一起改
  4. D1error 值域上提)——需要设计,不是改写
  5. S1/S2/S3——收尾

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions