M57/M58 三片 scope 均不含 cmd/cyrene——组合根接线漏了, voiceEnabled() 恒 false 导致 speak 从不被广告(她说不能发语音)。 synth 与网关同配置独立装配(emotionTTS 未启用时 nil=不广告); store 与 channelbridge 同目录同表(引用计数单一事实来源)。
59 lines
2.7 KiB
Go
59 lines
2.7 KiB
Go
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
|
||
})
|
||
}
|