- 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>
195 lines
6.1 KiB
C++
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;
|
|
}
|
|
|
|
}
|