| 1418 | } |
| 1419 | |
| 1420 | int rcli_speak(RCLIHandle handle, const char* text) { |
| 1421 | if (!handle || !text) return -1; |
| 1422 | auto* engine = static_cast<RCLIEngine*>(handle); |
| 1423 | if (!engine->initialized) return -1; |
| 1424 | |
| 1425 | LOG_TRACE("RCLI", "[speak] entry, text='%.40s'", text); |
| 1426 | |
| 1427 | // Kill any currently playing TTS first |
| 1428 | rcli_stop_speaking(handle); |
| 1429 | |
| 1430 | // Ensure CoreAudio playback is running (stop_capture_and_transcribe stops it) |
| 1431 | if (!engine->pipeline.audio().is_running()) { |
| 1432 | engine->pipeline.audio().start(); |
| 1433 | } |
| 1434 | |
| 1435 | std::string clean_text = rastack::sanitize_for_tts(std::string(text)); |
| 1436 | if (clean_text.empty()) { LOG_TRACE("RCLI", "[speak] clean_text empty, returning 0"); return 0; } |
| 1437 | |
| 1438 | LOG_TRACE("RCLI", "[speak] clean_text='%.40s' (%zu chars)", clean_text.c_str(), clean_text.size()); |
| 1439 | |
| 1440 | std::vector<float> audio; |
| 1441 | int sample_rate; |
| 1442 | if (engine->pipeline.using_metalrt_tts()) { |
| 1443 | LOG_TRACE("RCLI", "[speak] calling metalrt_tts().synthesize() ..."); |
| 1444 | audio = engine->pipeline.metalrt_tts().synthesize(clean_text); |
| 1445 | LOG_TRACE("RCLI", "[speak] synthesize returned %zu samples", audio.size()); |
| 1446 | sample_rate = engine->pipeline.metalrt_tts().sample_rate(); |
| 1447 | } else if (engine->pipeline.using_metalrt()) { |
| 1448 | LOG_ERROR("RCLI", "MetalRT is active but TTS is not on GPU — aborting TTS. " |
| 1449 | "Reinstall MetalRT components with: rcli setup --metalrt"); |
| 1450 | return -1; |
| 1451 | } else { |
| 1452 | audio = engine->pipeline.tts().synthesize(clean_text); |
| 1453 | sample_rate = engine->pipeline.tts().sample_rate(); |
| 1454 | } |
| 1455 | if (audio.empty()) { LOG_TRACE("RCLI", "[speak] audio empty, returning -1"); return -1; } |
| 1456 | |
| 1457 | LOG_TRACE("RCLI", "[speak] saving WAV (%zu samples, sr=%d) ...", audio.size(), sample_rate); |
| 1458 | std::string tmp_path = "/tmp/rcli_speak_" + std::to_string(getpid()) + ".wav"; |
| 1459 | if (!AudioIO::save_wav(tmp_path, audio.data(), static_cast<int>(audio.size()), sample_rate)) { |
| 1460 | LOG_ERROR("RCLI", "Failed to write TTS audio to %s", tmp_path.c_str()); |
| 1461 | return -1; |
| 1462 | } |
| 1463 | |
| 1464 | LOG_TRACE("RCLI", "[speak] forking afplay for %s ...", tmp_path.c_str()); |
| 1465 | pid_t pid = fork(); |
| 1466 | if (pid == 0) { |
| 1467 | execl("/usr/bin/afplay", "afplay", tmp_path.c_str(), nullptr); |
| 1468 | _exit(127); |
| 1469 | } else if (pid > 0) { |
| 1470 | engine->tts_pid.store(pid, std::memory_order_release); |
| 1471 | LOG_TRACE("RCLI", "[speak] afplay pid=%d, returning 0", pid); |
| 1472 | std::thread([engine, pid, tmp_path]() { |
| 1473 | int status = 0; |
| 1474 | waitpid(pid, &status, 0); |
| 1475 | pid_t expected = pid; |
| 1476 | engine->tts_pid.compare_exchange_strong(expected, 0, std::memory_order_release); |
| 1477 | unlink(tmp_path.c_str()); |