aissia/src/modules/VoiceModule.cpp
StillHammer 26a5d3438b refactor: Services architecture for GroveEngine compliance
- Create 4 infrastructure services (LLM, Storage, Platform, Voice)
- Refactor all modules to pure business logic (no HTTP/SQLite/Win32)
- Add bundled SQLite amalgamation for MinGW compatibility
- Make OpenSSL optional in CMake configuration
- Fix topic naming convention (colon format)
- Add succession documentation

Build status: CMake config needs SQLite C language fix (documented)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 11:57:53 +08:00

195 lines
6.1 KiB
C++

#include "VoiceModule.h"
#include <grove/JsonDataNode.h>
namespace aissia {
VoiceModule::VoiceModule() {
m_logger = spdlog::get("VoiceModule");
if (!m_logger) {
m_logger = spdlog::stdout_color_mt("VoiceModule");
}
m_config = std::make_unique<grove::JsonDataNode>("config");
}
void VoiceModule::setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) {
m_io = io;
m_config = std::make_unique<grove::JsonDataNode>("config");
// TTS config
auto* ttsNode = configNode.getChildReadOnly("tts");
if (ttsNode) {
m_ttsEnabled = ttsNode->getBool("enabled", true);
}
// STT config
auto* sttNode = configNode.getChildReadOnly("stt");
if (sttNode) {
m_sttEnabled = sttNode->getBool("enabled", true);
m_language = sttNode->getString("language", "fr");
}
// Subscribe to topics
if (m_io) {
grove::SubscriptionConfig subConfig;
m_io->subscribe("ai:response", subConfig);
m_io->subscribe("ai:suggestion", subConfig);
m_io->subscribe("notification:speak", subConfig);
m_io->subscribe("voice:speaking_started", subConfig);
m_io->subscribe("voice:speaking_ended", subConfig);
m_io->subscribe("voice:transcription", subConfig);
}
m_logger->info("VoiceModule configure (v2 - sans infrastructure): TTS={}, STT={}",
m_ttsEnabled, m_sttEnabled);
}
const grove::IDataNode& VoiceModule::getConfiguration() {
return *m_config;
}
void VoiceModule::process(const grove::IDataNode& input) {
processMessages();
}
void VoiceModule::processMessages() {
if (!m_io) return;
while (m_io->hasMessages() > 0) {
auto msg = m_io->pullMessage();
if (msg.topic == "ai:response" && msg.data) {
handleAIResponse(*msg.data);
}
else if (msg.topic == "ai:suggestion" && msg.data) {
handleSuggestion(*msg.data);
}
else if (msg.topic == "notification:speak" && msg.data) {
handleNotificationSpeak(*msg.data);
}
else if (msg.topic == "voice:speaking_started" && msg.data) {
handleSpeakingStarted(*msg.data);
}
else if (msg.topic == "voice:speaking_ended" && msg.data) {
handleSpeakingEnded(*msg.data);
}
else if (msg.topic == "voice:transcription" && msg.data) {
handleTranscription(*msg.data);
}
}
}
void VoiceModule::requestSpeak(const std::string& text, bool priority) {
if (!m_io || !m_ttsEnabled || text.empty()) return;
auto request = std::make_unique<grove::JsonDataNode>("speak");
request->setString("text", text);
request->setBool("priority", priority);
m_io->publish("voice:speak", std::move(request));
m_logger->debug("Speak request: {} (priority={})",
text.size() > 50 ? text.substr(0, 50) + "..." : text, priority);
}
void VoiceModule::handleAIResponse(const grove::IDataNode& data) {
if (!m_ttsEnabled) return;
std::string text = data.getString("text", "");
if (!text.empty()) {
requestSpeak(text, false);
}
}
void VoiceModule::handleSuggestion(const grove::IDataNode& data) {
if (!m_ttsEnabled) return;
std::string message = data.getString("message", "");
if (!message.empty()) {
// Suggestions are priority messages
requestSpeak(message, true);
}
}
void VoiceModule::handleNotificationSpeak(const grove::IDataNode& data) {
if (!m_ttsEnabled) return;
std::string message = data.getString("message", "");
if (!message.empty()) {
requestSpeak(message, false);
}
}
void VoiceModule::handleSpeakingStarted(const grove::IDataNode& data) {
m_isSpeaking = true;
m_totalSpoken++;
std::string text = data.getString("text", "");
m_logger->debug("Speaking started: {}", text.size() > 30 ? text.substr(0, 30) + "..." : text);
}
void VoiceModule::handleSpeakingEnded(const grove::IDataNode& data) {
m_isSpeaking = false;
m_logger->debug("Speaking ended");
}
void VoiceModule::handleTranscription(const grove::IDataNode& data) {
std::string text = data.getString("text", "");
float confidence = data.getDouble("confidence", 0.0);
if (!text.empty()) {
m_totalTranscribed++;
m_logger->info("Transcription: {} (conf={:.2f})", text, confidence);
// Forward to AI module
if (m_io) {
auto event = std::make_unique<grove::JsonDataNode>("query");
event->setString("query", text);
event->setString("source", "voice");
m_io->publish("ai:query", std::move(event));
}
}
}
std::unique_ptr<grove::IDataNode> VoiceModule::getHealthStatus() {
auto status = std::make_unique<grove::JsonDataNode>("status");
status->setString("status", "running");
status->setBool("ttsEnabled", m_ttsEnabled);
status->setBool("sttEnabled", m_sttEnabled);
status->setBool("isSpeaking", m_isSpeaking);
status->setInt("totalSpoken", m_totalSpoken);
status->setInt("totalTranscribed", m_totalTranscribed);
return status;
}
void VoiceModule::shutdown() {
m_logger->info("VoiceModule arrete. Spoken: {}, Transcribed: {}",
m_totalSpoken, m_totalTranscribed);
}
std::unique_ptr<grove::IDataNode> VoiceModule::getState() {
auto state = std::make_unique<grove::JsonDataNode>("state");
state->setInt("totalSpoken", m_totalSpoken);
state->setInt("totalTranscribed", m_totalTranscribed);
return state;
}
void VoiceModule::setState(const grove::IDataNode& state) {
m_totalSpoken = state.getInt("totalSpoken", 0);
m_totalTranscribed = state.getInt("totalTranscribed", 0);
m_logger->info("Etat restore: spoken={}, transcribed={}", m_totalSpoken, m_totalTranscribed);
}
} // namespace aissia
extern "C" {
grove::IModule* createModule() {
return new aissia::VoiceModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
}