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 / 文档上传同源: // 同一棵 `/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 }) }