docs/brainstorms/2026-05-30-voice-pack-requirements.md
两件事一起做,号池负载均衡优先:
app_id 只有 10 并发,买并发贵,所以一个账号开 10 个 app(10 个 app_id)= 100 并发。需要一个容量感知的号池:实时追踪每个 app_id 的在途请求数,把流量路由到还有并发余量的号,并对池子整体水位做监控。voice_packs 表,把 provider + model + voice + 参数覆盖 收敛成用户只选一个「声线」。绑定到角色卡时把解析后的值快照冻结进卡,之后改表永不影响已绑定的卡。一个 Voice Pack pin 一个 tts model id,该 model 的 upstreams/keys 就是上面那个号池。号池并发约束。 上游 TTS 服务按 app_id 限制并发(典型 10),扩并发额度很贵。绕开的办法是同一账号注册多个 app 拿到多个 app_id 凑并发。但当前服务端 routeTts 的 createKeyRotator(apps/server/src/services/.../router.ts:429)是盲轮转:不追踪每个号的在途请求数,会把某个号打爆到并发上限、别的号还闲着;跨 upstream 更是固定顺序、不分摊。结果是 100 并发的理论容量用不满,还会因为单号超限触发 429。
音色被动漂移。 当前音色是全局 UI 状态:active-provider + active-model + voice 三个独立 localStorage key(packages/stage-ui/src/stores/modules/speech.ts:32-35),不绑定角色卡、不是快照。voice catalog 是 per-model 的,上游 model 下线、默认音色被改、目录调整时,用户选好的音色会悄悄变成另一个甚至失效。DEFAULT_TTS_VOICES(commit 95915923e)已把 per-model 默认音色配置化、并要求 caller 必须显式传 voice,但「绑定后永不变」这层语义还不存在。
用户被迫理解 provider 拓扑。 选音色要先懂 Microsoft / 阿里云 等各自的 model 和 voice id 格式,对用户是无关负担。
voice_packs 表(复数表名),管理员策展。 不是前端 localStorage。本轮只装「云提供商音色」= provider + model + voice + 参数覆盖,同时覆盖标准 voice 与阿里云克隆 model id 两类(结构相同)。软禁用用 enabled 列,不删行。provider+model+voice 配不同 pitch / 响度 = 不同 Voice Pack,用户分别可选(Neuro-sama 那个 Pitch +20%、响度 +5% 的例子)。provider/model/voice/params/tier + pin 的 tts model id 进 extensions.airi.modules.speech.voicePack(扩 airi-card.ts:173-176 现有 speech 快照点)。存外键会导致改表连带改卡,回到漂移。resolveAiriExtension(airi-card.ts:161)处理字段缺失,不加 backward-compat guard。routeTts 现有跨 upstream/key 重试。R7「等价判定」(同音色、可复现参数)是服务端不校验的新语义,只能在 voice_packs 定义层把关。tts-billing-tiers.md 的 lite/standard/pro/premium 命名。 voice_packs 一列,冻进快照。本轮只有一个 meter(FLUX_PER_1K_CHARS_TTS 单值),四档 meter 拆分属 billing 独立线,所以本轮 tier 是展示 + 数据,「按最高档取价」暂无真实差价效果。voice_packs 读 enabled 的 pack → 把解析后的值快照冻结写入角色卡 extensions → 角色卡此后只读自己的冻结快照。TTS 号池负载均衡(最高优先)
app_id(key)的实时在途请求数,路由时挑还有并发余量的号,不用盲轮转。triedKeys/triedUpstreams 类上下文,复用现有 mapUpstreamError 模式。Voice Pack 表与管理
voice_packs 表存「云提供商音色」:provider + model + voice_id + 参数覆盖(pitch/rate/volume 等)+ tier + enabled。同时覆盖标准 voice 与云端克隆 model id 两类。provider+model+voice 不同参数 = 不同 pack。enabled 的 pack。角色卡绑定与合成
extensions.airi.modules.speech.voicePack;改表不影响已绑定卡。voice_packs 一列,复用 lite/standard/pro/premium,冻进快照(本轮展示 + 数据,不碰实际扣费)。voice_packs 编辑 A(换 voice、改参数)或禁用 A → 角色卡音色不变,仍用绑定时快照。Deferred for later(第二轮或独立线)
voice_pack_reference 子表(FK → voice_packs,一个 pack 多条参考音频);voice_packs 永远是唯一身份/市场/计费实体,市场/绑定/合成只读它、不做多态双表读。本轮只把这个形状记进文档,不建表。upload → 调云端 clone API → 轮询 model id 流程;本轮只消费已克隆好的 model id。ttsMeter),属 tts-billing-tiers.md 线。routeTts 跨 upstream/key 重试、mapUpstreamError、fallbackHttpCodes(含 429)是号池 failover 的复用基础。app_id / access token 在 ttsUpstreamSchema(config-kv.ts:57-61)的落位(keys[] 还是 adapterParams)需按 Volcengine adapter 实测确认,决定「一个号」对应 schema 哪个粒度。packages/ccc 的 Extensions 开放可扩展(extensions.ts:1),冻结快照扩 extensions.airi.modules.speech。角色卡正在上整卡 LWW 云同步(docs/ai/context/plans/2026-05-09-character-cards-cloud-sync-design.md),快照 schema 改动会被同步带走,需对齐。tts-billing-tiers.md 的四档命名是 tier 取值来源;该文档当前在 main worktree 未提交,本分支引用时注意同步。apps/server/src/services/.../router.ts:413-617 — routeTts 主循环、dispatchOneTtsUpstream、createKeyRotator(盲轮转,号池 LB 的改造点)。apps/server/src/app.ts:616-632 — ttsMeter = createFluxMeter(Redis 用法,号池并发计数可复用的 Redis pattern)。apps/server/src/services/adapters/config-kv.ts:57-61, 83-87 — ttsUpstreamSchema / ttsModelSchema(多 upstreams/keys 结构,号池建模点)。apps/server/src/routes/openai/v1/index.ts:489-642, 738 — handleTTS、/audio/voices catalog、ttsGuard。packages/stage-ui/src/stores/modules/airi-card.ts:161-215 — 角色卡 speech 快照写入/读取(冻结快照落点)。packages/stage-ui/src/stores/modules/speech.ts:32-35, 298-338 — 当前全局 voice 状态、generateSSML(pitch/rate/volume)。packages/ccc/src/export/types/extensions.ts:1 — 开放 extensions。docs/ai/context/tts-billing-tiers.md — 四档 tier 命名来源。docs/ai/context/plans/2026-05-09-character-cards-cloud-sync-design.md — 整卡 LWW 云同步,快照 schema 需对齐。