fix(wire): inject speak VoiceSynth/VoiceStore into loop options
CI / Backend (build / vet / test / cross-compile) (push) Canceled after 0s
CI / Frontend (tsc + vite build) (push) Canceled after 0s

M57/M58 三片 scope 均不含 cmd/cyrene——组合根接线漏了,
voiceEnabled() 恒 false 导致 speak 从不被广告(她说不能发语音)。
synth 与网关同配置独立装配(emotionTTS 未启用时 nil=不广告);
store 与 channelbridge 同目录同表(引用计数单一事实来源)。
This commit is contained in:
AskaEth committed 2026-10-28 17:38:06 +08:00
1 parent 7c4424f857
commit 9d28477e3a
2 files changed
+62

No files matched your search

+4
View File
@@ -329,6 +329,10 @@ func (a *app) wireRuntime() error {
Tracer: tracer,
Flow: flowPub,
Guards: guards,
// speak 的两个依赖(wire_speak.go):emotionTTS 未启用时 synth 为 nil,
// speak 不广告(voiceEnabled() 要求两者齐备)。
VoiceSynth: a.speakSynthForLoop(),
VoiceStore: a.speakStoreForLoop(docDataDir),
PersonaID: personaID,
PersonaName: a.personaName,
PersonaKnobs: a.personaKnobs,
+58
View File
@@ -0,0 +1,58 @@
package main
// speak 的组合根接线(M57/M58 三片合并后的集成缝隙修复,2026-10-27):
// `loop.VoiceSynth` / `VoiceStore` 两个依赖此前没有任何 wire 层注入 ⇒
// `voiceEnabled()` 恒 false ⇒ speak 从不被广告(症状:她说"不能发语音"、
// 查工具清单也看不到 speak)。本文件补上这一跳。
//
// 语义:speak 依赖 emotionTTS(发音人按人格解析在那条链上)——面板
// voice-tts.json 未启用时注入 nil,speak 不广告(比"广告了但永远
// not_configured 降级"诚实)。副本落盘与 channelbridge / 文档上传同源:
// 同一棵 `<DataDir>/uploads/documents` 目录、同一张 documents 表
// (引用计数与 GC 的单一事实来源不被旁路)。
import (
"context"
"path/filepath"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
voicetts "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/voice/tts"
)
const speakWireSource = "cmd/cyrene/speak"
// speakSynthForLoop 返回注入 loop 的合成能力:emotionTTS 按面板启用时是
// 一个独立装配的 voicetts.Service(与网关的实例同配置不同对象——装配期
// 一次性构造,无共享状态竞争);未启用返回 nil(speak 不广告)。
func (a *app) speakSynthForLoop() loop.VoiceSynth {
cfg, enabled := a.voiceTTSConfig()
if !enabled {
logger.WithSource(a.log, speakWireSource).Debug("speak stays unadvertised: emotionTTS not enabled",
"config", "voice-tts.json", "hint", "set {\"enabled\": true}")
return nil
}
client := voicetts.New(cfg, voicetts.WithLogger(logger.WithSource(a.log, speakWireSource)))
return voicetts.NewService(client, a.voice, voicetts.WithServiceLogger(logger.WithSource(a.log, speakWireSource)))
}
// speakStoreForLoop 返回注入 loop 的音频副本落盘能力:与 channelbridge 的
// DocumentStore 同目录同表(wire_channels.go 的口径),经 VoiceStoreFunc
// 做 documents.Ref → loop.VoiceRef 的窄转换(loop 不 import documents,
// 测试期 import cycle 的既定裁决)。
func (a *app) speakStoreForLoop(docDataDir string) loop.VoiceStore {
store := documents.NewStore(documents.StoreConfig{
DataDir: docDataDir,
UploadsDir: filepath.Join(docDataDir, "uploads", "documents"),
Meta: documents.MetadataWriterFor(a.dbPath, a.log),
Log: a.log,
})
return loop.VoiceStoreFunc(func(ctx context.Context, origName string, data []byte) (loop.VoiceRef, error) {
ref, err := store.SaveAudio(ctx, origName, data)
if err != nil {
return loop.VoiceRef{}, err
}
return loop.VoiceRef{Name: ref.Name, MIME: ref.MIME, Size: ref.Size}, nil
})
}