From f23acc8c1ce02d84b4984453782bc9b7b7ac730f Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Sat, 21 Feb 2026 20:48:10 +0100 Subject: [PATCH] Ajout sample CPP --- CPP/elevenlabs-convai-cpp-main/.gitignore | 152 ++++++++++++ CPP/elevenlabs-convai-cpp-main/CMakeLists.txt | 42 ++++ CPP/elevenlabs-convai-cpp-main/LICENSE | 21 ++ CPP/elevenlabs-convai-cpp-main/README.md | 197 +++++++++++++++ .../include/AudioInterface.hpp | 23 ++ .../include/Conversation.hpp | 72 ++++++ .../include/DefaultAudioInterface.hpp | 45 ++++ .../src/Conversation.cpp | 230 ++++++++++++++++++ .../src/DefaultAudioInterface.cpp | 131 ++++++++++ CPP/elevenlabs-convai-cpp-main/src/main.cpp | 31 +++ .../PS_AI_Agent/Content/test_AI_Actor.uasset | Bin 158165 -> 157437 bytes ...ElevenLabsConversationalAgentComponent.cpp | 10 +- .../Private/ElevenLabsWebSocketProxy.cpp | 26 +- .../ElevenLabsConversationalAgentComponent.h | 2 +- .../Public/ElevenLabsWebSocketProxy.h | 7 + 15 files changed, 985 insertions(+), 4 deletions(-) create mode 100644 CPP/elevenlabs-convai-cpp-main/.gitignore create mode 100644 CPP/elevenlabs-convai-cpp-main/CMakeLists.txt create mode 100644 CPP/elevenlabs-convai-cpp-main/LICENSE create mode 100644 CPP/elevenlabs-convai-cpp-main/README.md create mode 100644 CPP/elevenlabs-convai-cpp-main/include/AudioInterface.hpp create mode 100644 CPP/elevenlabs-convai-cpp-main/include/Conversation.hpp create mode 100644 CPP/elevenlabs-convai-cpp-main/include/DefaultAudioInterface.hpp create mode 100644 CPP/elevenlabs-convai-cpp-main/src/Conversation.cpp create mode 100644 CPP/elevenlabs-convai-cpp-main/src/DefaultAudioInterface.cpp create mode 100644 CPP/elevenlabs-convai-cpp-main/src/main.cpp diff --git a/CPP/elevenlabs-convai-cpp-main/.gitignore b/CPP/elevenlabs-convai-cpp-main/.gitignore new file mode 100644 index 0000000..80545b2 --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/.gitignore @@ -0,0 +1,152 @@ +# Build directories +build/ +cmake-build-*/ +out/ + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app +convai_cpp + +# CMake +CMakeCache.txt +CMakeFiles/ +CMakeScripts/ +Testing/ +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps/ + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msm +*.msp +*.lnk + +# Linux +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# Logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ \ No newline at end of file diff --git a/CPP/elevenlabs-convai-cpp-main/CMakeLists.txt b/CPP/elevenlabs-convai-cpp-main/CMakeLists.txt new file mode 100644 index 0000000..d206779 --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.14) + +project(elevenlabs_convai_cpp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find dependencies +find_package(Boost REQUIRED COMPONENTS system thread) +find_package(OpenSSL REQUIRED) +# PortAudio via vcpkg CMake config +find_package(portaudio CONFIG REQUIRED) + +# Find nlohmann_json +find_package(nlohmann_json 3.11 QUIET) + +if(NOT nlohmann_json_FOUND) + include(FetchContent) + # Fallback: header-only fetch to avoid old CMake policies in upstream CMakeLists + FetchContent_Declare( + nlohmann_json_src + URL https://raw.githubusercontent.com/nlohmann/json/v3.11.2/single_include/nlohmann/json.hpp + ) + FetchContent_MakeAvailable(nlohmann_json_src) + add_library(nlohmann_json::nlohmann_json INTERFACE IMPORTED) + target_include_directories(nlohmann_json::nlohmann_json INTERFACE ${nlohmann_json_src_SOURCE_DIR}/single_include) +endif() + +add_executable(convai_cpp + src/main.cpp + src/Conversation.cpp + src/DefaultAudioInterface.cpp +) + +target_include_directories(convai_cpp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + +# MSVC: set Windows target version and suppress getenv deprecation warning +if(MSVC) + target_compile_definitions(convai_cpp PRIVATE _WIN32_WINNT=0x0A00 _CRT_SECURE_NO_WARNINGS) +endif() + +target_link_libraries(convai_cpp PRIVATE Boost::system Boost::thread OpenSSL::SSL OpenSSL::Crypto portaudio nlohmann_json::nlohmann_json) \ No newline at end of file diff --git a/CPP/elevenlabs-convai-cpp-main/LICENSE b/CPP/elevenlabs-convai-cpp-main/LICENSE new file mode 100644 index 0000000..3165092 --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Jitendra + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/CPP/elevenlabs-convai-cpp-main/README.md b/CPP/elevenlabs-convai-cpp-main/README.md new file mode 100644 index 0000000..f829e18 --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/README.md @@ -0,0 +1,197 @@ +# ElevenLabs Conversational AI - C++ Implementation + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![C++17](https://img.shields.io/badge/C%2B%2B-17-blue.svg)](https://en.wikipedia.org/wiki/C%2B%2B17) +[![CMake](https://img.shields.io/badge/CMake-3.14+-green.svg)](https://cmake.org/) + +C++ implementation of ElevenLabs Conversational AI client + +## Features + +- **Real-time Audio Processing**: Full-duplex audio streaming with low-latency playback +- **WebSocket Integration**: Secure WSS connection to ElevenLabs Conversational AI platform +- **Cross-platform Audio**: PortAudio-based implementation supporting Windows, macOS, and Linux +- **Echo Suppression**: Built-in acoustic feedback prevention +- **Modern C++**: Clean, maintainable C++17 codebase with proper RAII and exception handling +- **Flexible Architecture**: Modular design allowing easy customization and extension + +## Architecture + +```mermaid +graph TB + subgraph "User Interface" + A[main.cpp] --> B[Conversation] + end + + subgraph "Core Components" + B --> C[DefaultAudioInterface] + B --> D[WebSocket Client] + C --> E[PortAudio] + D --> F[Boost.Beast + OpenSSL] + end + + subgraph "ElevenLabs Platform" + F --> G[WSS API Endpoint] + G --> H[Conversational AI Agent] + end + + subgraph "Audio Flow" + I[Microphone] --> C + C --> J[Base64 Encoding] + J --> D + D --> K[Audio Events] + K --> L[Base64 Decoding] + L --> C + C --> M[Speakers] + end + + subgraph "Message Types" + N[user_audio_chunk] + O[agent_response] + P[user_transcript] + Q[audio_event] + R[ping/pong] + end + + style B fill:#e1f5fe + style C fill:#f3e5f5 + style D fill:#e8f5e8 + style H fill:#fff3e0 +``` + +## Quick Start + +### Prerequisites + +- **C++17 compatible compiler**: GCC 11+, Clang 14+, or MSVC 2022+ +- **CMake** 3.14 or higher +- **Dependencies** (install via package manager): + +#### macOS (Homebrew) +```bash +brew install boost openssl portaudio nlohmann-json cmake pkg-config +``` + +#### Ubuntu/Debian +```bash +sudo apt update +sudo apt install build-essential cmake pkg-config +sudo apt install libboost-system-dev libboost-thread-dev +sudo apt install libssl-dev libportaudio2-dev nlohmann-json3-dev +``` + +#### Windows (vcpkg) +```bash +vcpkg install boost-system boost-thread openssl portaudio nlohmann-json +``` + +### Building + +```bash +# Clone the repository +git clone https://github.com/Jitendra2603/elevenlabs-convai-cpp.git +cd elevenlabs-convai-cpp + +# Build the project +mkdir build && cd build +cmake .. +cmake --build . --config Release +``` + +### Running + +```bash +# Set your agent ID (get this from ElevenLabs dashboard) +export AGENT_ID="your-agent-id-here" + +# Run the demo +./convai_cpp +``` + +The application will: +1. Connect to your ElevenLabs Conversational AI agent +2. Start capturing audio from your default microphone +3. Stream audio to the agent and play responses through speakers +4. Display conversation transcripts in the terminal +5. Continue until you press Enter to quit + +## 📋 Usage Examples + +### Basic Conversation +```bash +export AGENT_ID="agent_" +./convai_cpp +# Speak into your microphone and hear the AI agent respond +``` + + +## Configuration + +### Audio Settings + +The audio interface is configured for optimal real-time performance: + +- **Sample Rate**: 16 kHz +- **Format**: 16-bit PCM mono +- **Input Buffer**: 250ms (4000 frames) +- **Output Buffer**: 62.5ms (1000 frames) + +### WebSocket Connection + +- **Endpoint**: `wss://api.elevenlabs.io/v1/convai/conversation` +- **Protocol**: WebSocket Secure (WSS) with TLS 1.2+ +- **Authentication**: Optional (required for private agents) + +## Project Structure + +``` +elevenlabs-convai-cpp/ +├── CMakeLists.txt # Build configuration +├── README.md # This file +├── LICENSE # MIT license +├── CONTRIBUTING.md # Contribution guidelines +├── .gitignore # Git ignore rules +├── include/ # Header files +│ ├── AudioInterface.hpp # Abstract audio interface +│ ├── DefaultAudioInterface.hpp # PortAudio implementation +│ └── Conversation.hpp # Main conversation handler +└── src/ # Source files + ├── main.cpp # Demo application + ├── Conversation.cpp # WebSocket and message handling + └── DefaultAudioInterface.cpp # Audio I/O implementation +``` + +## Technical Details + +### Audio Processing Pipeline + +1. **Capture**: PortAudio captures 16-bit PCM audio at 16kHz +2. **Encoding**: Raw audio is base64-encoded for WebSocket transmission +3. **Streaming**: Audio chunks sent as `user_audio_chunk` messages +4. **Reception**: Server sends `audio_event` messages with agent responses +5. **Decoding**: Base64 audio data decoded back to PCM +6. **Playback**: Audio queued and played through PortAudio output stream + +### Echo Suppression + +The implementation includes a simple, effective echo suppression mechanism: + +- Microphone input is suppressed during agent speech playback +- Prevents acoustic feedback loops that cause the agent to respond to itself +- Uses atomic flags for thread-safe coordination between input/output + +### WebSocket Message Handling + +Supported message types: +- `conversation_initiation_client_data` - Session initialization +- `user_audio_chunk` - Microphone audio data +- `audio_event` - Agent speech audio +- `agent_response` - Agent text responses +- `user_transcript` - Speech-to-text results +- `ping`/`pong` - Connection keepalive + + + +## 📝 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/CPP/elevenlabs-convai-cpp-main/include/AudioInterface.hpp b/CPP/elevenlabs-convai-cpp-main/include/AudioInterface.hpp new file mode 100644 index 0000000..b92bd42 --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/include/AudioInterface.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +class AudioInterface { +public: + using AudioCallback = std::function&)>; + + virtual ~AudioInterface() = default; + + // Starts the audio interface. The callback will be invoked with raw 16-bit PCM mono samples at 16kHz. + virtual void start(AudioCallback inputCallback) = 0; + + // Stops audio I/O and releases underlying resources. + virtual void stop() = 0; + + // Play audio to the user; audio is 16-bit PCM mono 16kHz. + virtual void output(const std::vector& audio) = 0; + + // Immediately stop any buffered / ongoing output. + virtual void interrupt() = 0; +}; \ No newline at end of file diff --git a/CPP/elevenlabs-convai-cpp-main/include/Conversation.hpp b/CPP/elevenlabs-convai-cpp-main/include/Conversation.hpp new file mode 100644 index 0000000..04005e4 --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/include/Conversation.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "AudioInterface.hpp" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +class Conversation { +public: + using CallbackAgentResponse = std::function; + using CallbackAgentResponseCorrection = std::function; + using CallbackUserTranscript = std::function; + using CallbackLatencyMeasurement = std::function; + + Conversation( + const std::string& agentId, + bool requiresAuth, + std::shared_ptr audioInterface, + CallbackAgentResponse callbackAgentResponse = nullptr, + CallbackAgentResponseCorrection callbackAgentResponseCorrection = nullptr, + CallbackUserTranscript callbackUserTranscript = nullptr, + CallbackLatencyMeasurement callbackLatencyMeasurement = nullptr + ); + + ~Conversation(); + + void startSession(); + void endSession(); + std::string waitForSessionEnd(); + + void sendUserMessage(const std::string& text); + void registerUserActivity(); + void sendContextualUpdate(const std::string& content); + +private: + void run(); + void handleMessage(const nlohmann::json& message); + std::string getWssUrl() const; + + // networking members + boost::asio::io_context ioc_; + boost::asio::ssl::context sslCtx_{boost::asio::ssl::context::tlsv12_client}; + + using tcp = boost::asio::ip::tcp; + using websocket_t = boost::beast::websocket::stream< + boost::beast::ssl_stream>; + std::unique_ptr ws_; + + // general state + std::string agentId_; + bool requiresAuth_; + std::shared_ptr audioInterface_; + + CallbackAgentResponse callbackAgentResponse_; + CallbackAgentResponseCorrection callbackAgentResponseCorrection_; + CallbackUserTranscript callbackUserTranscript_; + CallbackLatencyMeasurement callbackLatencyMeasurement_; + + std::thread workerThread_; + std::atomic shouldStop_{false}; + std::string conversationId_; + + std::atomic lastInterruptId_{0}; +}; \ No newline at end of file diff --git a/CPP/elevenlabs-convai-cpp-main/include/DefaultAudioInterface.hpp b/CPP/elevenlabs-convai-cpp-main/include/DefaultAudioInterface.hpp new file mode 100644 index 0000000..39940e9 --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/include/DefaultAudioInterface.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "AudioInterface.hpp" +#include +#include +#include +#include +#include +#include + +class DefaultAudioInterface : public AudioInterface { +public: + static constexpr int INPUT_FRAMES_PER_BUFFER = 4000; // 250ms @ 16kHz + static constexpr int OUTPUT_FRAMES_PER_BUFFER = 1000; // 62.5ms @ 16kHz + + DefaultAudioInterface(); + ~DefaultAudioInterface() override; + + void start(AudioCallback inputCallback) override; + void stop() override; + void output(const std::vector& audio) override; + void interrupt() override; + +private: + static int inputCallbackStatic(const void* input, void* output, unsigned long frameCount, + const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, + void* userData); + + int inputCallbackInternal(const void* input, unsigned long frameCount); + + void outputThreadFunc(); + + PaStream* inputStream_{}; + PaStream* outputStream_{}; + + AudioCallback inputCallback_; + + std::queue> outputQueue_; + std::mutex queueMutex_; + std::condition_variable queueCv_; + + std::thread outputThread_; + std::atomic shouldStop_{false}; + std::atomic outputPlaying_{false}; +}; \ No newline at end of file diff --git a/CPP/elevenlabs-convai-cpp-main/src/Conversation.cpp b/CPP/elevenlabs-convai-cpp-main/src/Conversation.cpp new file mode 100644 index 0000000..be11e7b --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/src/Conversation.cpp @@ -0,0 +1,230 @@ +#include "Conversation.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using tcp = boost::asio::ip::tcp; +namespace ssl = boost::asio::ssl; +namespace websocket = boost::beast::websocket; +namespace beast = boost::beast; + +static std::string base64Encode(const std::vector& data) { + auto encodedSize = beast::detail::base64::encoded_size(data.size()); + std::string out(encodedSize, '\0'); + beast::detail::base64::encode(&out[0], data.data(), data.size()); + return out; +} + +static std::vector base64Decode(const std::string& str) { + auto decodedSize = beast::detail::base64::decoded_size(str.size()); + std::vector out(decodedSize); + auto result = beast::detail::base64::decode(out.data(), str.data(), str.size()); + out.resize(result.first); + return out; +} + +static std::string toString(const nlohmann::json& j){ + if(j.is_string()) return j.get(); + if(j.is_number_integer()) return std::to_string(j.get()); + return j.dump(); +} + +Conversation::Conversation(const std::string& agentId, bool requiresAuth, + std::shared_ptr audioInterface, + CallbackAgentResponse callbackAgentResponse, + CallbackAgentResponseCorrection callbackAgentResponseCorrection, + CallbackUserTranscript callbackUserTranscript, + CallbackLatencyMeasurement callbackLatencyMeasurement) + : agentId_(agentId), + requiresAuth_(requiresAuth), + audioInterface_(std::move(audioInterface)), + callbackAgentResponse_(std::move(callbackAgentResponse)), + callbackAgentResponseCorrection_(std::move(callbackAgentResponseCorrection)), + callbackUserTranscript_(std::move(callbackUserTranscript)), + callbackLatencyMeasurement_(std::move(callbackLatencyMeasurement)) { + + sslCtx_.set_default_verify_paths(); +} + +Conversation::~Conversation() { + endSession(); +} + +void Conversation::startSession() { + shouldStop_.store(false); + workerThread_ = std::thread(&Conversation::run, this); +} + +void Conversation::endSession() { + shouldStop_.store(true); + if (ws_) { + beast::error_code ec; + ws_->close(websocket::close_code::normal, ec); + } + if (audioInterface_) { + audioInterface_->stop(); + } + if (workerThread_.joinable()) { + workerThread_.join(); + } +} + +std::string Conversation::waitForSessionEnd() { + if (workerThread_.joinable()) { + workerThread_.join(); + } + return conversationId_; +} + +void Conversation::sendUserMessage(const std::string& text) { + if (!ws_) { + throw std::runtime_error("Session not started"); + } + nlohmann::json j = { + {"type", "user_message"}, + {"text", text} + }; + ws_->write(boost::asio::buffer(j.dump())); +} + +void Conversation::registerUserActivity() { + if (!ws_) throw std::runtime_error("Session not started"); + nlohmann::json j = {{"type", "user_activity"}}; + ws_->write(boost::asio::buffer(j.dump())); +} + +void Conversation::sendContextualUpdate(const std::string& content) { + if (!ws_) throw std::runtime_error("Session not started"); + nlohmann::json j = {{"type", "contextual_update"}, {"content", content}}; + ws_->write(boost::asio::buffer(j.dump())); +} + +std::string Conversation::getWssUrl() const { + // Hard-coded base env for demo; in production you'd call ElevenLabs env endpoint. + std::ostringstream oss; + oss << "wss://api.elevenlabs.io/v1/convai/conversation?agent_id=" << agentId_; + return oss.str(); +} + +void Conversation::run() { + try { + auto url = getWssUrl(); + std::string protocol, host, target; + unsigned short port = 443; + + // Very naive parse: wss://host[:port]/path?query + if (boost::starts_with(url, "wss://")) { + protocol = "wss"; + host = url.substr(6); + } else { + throw std::runtime_error("Only wss:// URLs supported in this demo"); + } + auto slashPos = host.find('/'); + if (slashPos == std::string::npos) { + target = "/"; + } else { + target = host.substr(slashPos); + host = host.substr(0, slashPos); + } + auto colonPos = host.find(':'); + if (colonPos != std::string::npos) { + port = static_cast(std::stoi(host.substr(colonPos + 1))); + host = host.substr(0, colonPos); + } + + tcp::resolver resolver(ioc_); + auto const results = resolver.resolve(host, std::to_string(port)); + + beast::ssl_stream stream(ioc_, sslCtx_); + boost::asio::connect(beast::get_lowest_layer(stream), results); + if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { + throw std::runtime_error("Failed to set SNI hostname on SSL stream"); + } + stream.handshake(ssl::stream_base::client); + + ws_ = std::make_unique(std::move(stream)); + ws_->set_option(websocket::stream_base::timeout::suggested(beast::role_type::client)); + ws_->handshake(host, target); + + // send initiation data + nlohmann::json init = { + {"type", "conversation_initiation_client_data"}, + {"custom_llm_extra_body", nlohmann::json::object()}, + {"conversation_config_override", nlohmann::json::object()}, + {"dynamic_variables", nlohmann::json::object()} + }; + ws_->write(boost::asio::buffer(init.dump())); + + // Prepare audio callback + auto inputCb = [this](const std::vector& audio) { + nlohmann::json msg = { + {"user_audio_chunk", base64Encode(audio)} + }; + ws_->write(boost::asio::buffer(msg.dump())); + }; + audioInterface_->start(inputCb); + + beast::flat_buffer buffer; + while (!shouldStop_.load()) { + beast::error_code ec; + ws_->read(buffer, ec); + if (ec) { + std::cerr << "Websocket read error: " << ec.message() << std::endl; + break; + } + auto text = beast::buffers_to_string(buffer.data()); + buffer.consume(buffer.size()); + try { + auto message = nlohmann::json::parse(text); + handleMessage(message); + } catch (const std::exception& ex) { + std::cerr << "JSON parse error: " << ex.what() << std::endl; + } + } + } catch (const std::exception& ex) { + std::cerr << "Conversation error: " << ex.what() << std::endl; + } +} + +void Conversation::handleMessage(const nlohmann::json& message) { + std::string type = message.value("type", ""); + if (type == "conversation_initiation_metadata") { + conversationId_ = message["conversation_initiation_metadata_event"]["conversation_id"].get(); + } else if (type == "audio") { + auto event = message["audio_event"]; + int eventId = std::stoi(toString(event["event_id"])); + if (eventId <= lastInterruptId_.load()) return; + auto audioBytes = base64Decode(event["audio_base_64"].get()); + audioInterface_->output(audioBytes); + } else if (type == "agent_response" && callbackAgentResponse_) { + auto event = message["agent_response_event"]; + callbackAgentResponse_(event["agent_response"].get()); + } else if (type == "agent_response_correction" && callbackAgentResponseCorrection_) { + auto event = message["agent_response_correction_event"]; + callbackAgentResponseCorrection_(event["original_agent_response"].get(), + event["corrected_agent_response"].get()); + } else if (type == "user_transcript" && callbackUserTranscript_) { + auto event = message["user_transcription_event"]; + callbackUserTranscript_(event["user_transcript"].get()); + } else if (type == "interruption") { + auto event = message["interruption_event"]; + lastInterruptId_.store(std::stoi(toString(event["event_id"]))); + audioInterface_->interrupt(); + } else if (type == "ping") { + auto event = message["ping_event"]; + nlohmann::json pong = {{"type", "pong"}, {"event_id", event["event_id"]}}; + ws_->write(boost::asio::buffer(pong.dump())); + if (callbackLatencyMeasurement_ && event.contains("ping_ms")) { + int latency = event["ping_ms"].is_number() ? event["ping_ms"].get() : std::stoi(event["ping_ms"].get()); + callbackLatencyMeasurement_(latency); + } + } + // Note: client tool call handling omitted for brevity. +} \ No newline at end of file diff --git a/CPP/elevenlabs-convai-cpp-main/src/DefaultAudioInterface.cpp b/CPP/elevenlabs-convai-cpp-main/src/DefaultAudioInterface.cpp new file mode 100644 index 0000000..9a0ca84 --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/src/DefaultAudioInterface.cpp @@ -0,0 +1,131 @@ +#include "DefaultAudioInterface.hpp" + +#include +#include + +DefaultAudioInterface::DefaultAudioInterface() { + PaError err = Pa_Initialize(); + if (err != paNoError) { + throw std::runtime_error("PortAudio initialization failed"); + } +} + +DefaultAudioInterface::~DefaultAudioInterface() { + if (!shouldStop_.load()) { + stop(); + } + Pa_Terminate(); +} + +void DefaultAudioInterface::start(AudioCallback inputCallback) { + inputCallback_ = std::move(inputCallback); + PaStreamParameters inputParams; + std::memset(&inputParams, 0, sizeof(inputParams)); + inputParams.channelCount = 1; + inputParams.device = Pa_GetDefaultInputDevice(); + inputParams.sampleFormat = paInt16; + inputParams.suggestedLatency = Pa_GetDeviceInfo(inputParams.device)->defaultLowInputLatency; + inputParams.hostApiSpecificStreamInfo = nullptr; + + PaStreamParameters outputParams; + std::memset(&outputParams, 0, sizeof(outputParams)); + outputParams.channelCount = 1; + outputParams.device = Pa_GetDefaultOutputDevice(); + outputParams.sampleFormat = paInt16; + outputParams.suggestedLatency = Pa_GetDeviceInfo(outputParams.device)->defaultLowOutputLatency; + outputParams.hostApiSpecificStreamInfo = nullptr; + + PaError err = Pa_OpenStream(&inputStream_, &inputParams, nullptr, 16000, INPUT_FRAMES_PER_BUFFER, paClipOff, + &DefaultAudioInterface::inputCallbackStatic, this); + if (err != paNoError) { + throw std::runtime_error("Failed to open input stream"); + } + + err = Pa_OpenStream(&outputStream_, nullptr, &outputParams, 16000, OUTPUT_FRAMES_PER_BUFFER, paClipOff, nullptr, nullptr); + if (err != paNoError) { + throw std::runtime_error("Failed to open output stream"); + } + + if ((err = Pa_StartStream(inputStream_)) != paNoError) { + throw std::runtime_error("Failed to start input stream"); + } + if ((err = Pa_StartStream(outputStream_)) != paNoError) { + throw std::runtime_error("Failed to start output stream"); + } + + shouldStop_.store(false); + outputThread_ = std::thread(&DefaultAudioInterface::outputThreadFunc, this); +} + +void DefaultAudioInterface::stop() { + shouldStop_.store(true); + queueCv_.notify_all(); + if (outputThread_.joinable()) { + outputThread_.join(); + } + + if (inputStream_) { + Pa_StopStream(inputStream_); + Pa_CloseStream(inputStream_); + inputStream_ = nullptr; + } + if (outputStream_) { + Pa_StopStream(outputStream_); + Pa_CloseStream(outputStream_); + outputStream_ = nullptr; + } +} + +void DefaultAudioInterface::output(const std::vector& audio) { + { + std::lock_guard lg(queueMutex_); + outputQueue_.emplace(audio); + } + queueCv_.notify_one(); +} + +void DefaultAudioInterface::interrupt() { + std::lock_guard lg(queueMutex_); + std::queue> empty; + std::swap(outputQueue_, empty); +} + +int DefaultAudioInterface::inputCallbackStatic(const void* input, void* /*output*/, unsigned long frameCount, + const PaStreamCallbackTimeInfo* /*timeInfo*/, PaStreamCallbackFlags /*statusFlags*/, + void* userData) { + auto* self = static_cast(userData); + return self->inputCallbackInternal(input, frameCount); +} + +int DefaultAudioInterface::inputCallbackInternal(const void* input, unsigned long frameCount) { + if (!input || !inputCallback_) { + return paContinue; + } + if (outputPlaying_.load()) { + // Suppress microphone input while playing output to avoid echo feedback. + return paContinue; + } + const size_t bytes = frameCount * sizeof(int16_t); + std::vector buffer(bytes); + std::memcpy(buffer.data(), input, bytes); + inputCallback_(buffer); + return paContinue; +} + +void DefaultAudioInterface::outputThreadFunc() { + while (!shouldStop_.load()) { + std::vector audio; + { + std::unique_lock lk(queueMutex_); + queueCv_.wait(lk, [this] { return shouldStop_.load() || !outputQueue_.empty(); }); + if (shouldStop_.load()) break; + audio = std::move(outputQueue_.front()); + outputQueue_.pop(); + } + if (!audio.empty() && outputStream_) { + outputPlaying_.store(true); + Pa_WriteStream(outputStream_, audio.data(), audio.size() / sizeof(int16_t)); + outputPlaying_.store(false); + } + } +} \ No newline at end of file diff --git a/CPP/elevenlabs-convai-cpp-main/src/main.cpp b/CPP/elevenlabs-convai-cpp-main/src/main.cpp new file mode 100644 index 0000000..f8522bd --- /dev/null +++ b/CPP/elevenlabs-convai-cpp-main/src/main.cpp @@ -0,0 +1,31 @@ +#include "Conversation.hpp" +#include "DefaultAudioInterface.hpp" + +#include +#include +#include + +int main() { + const char* agentIdEnv = std::getenv("AGENT_ID"); + if (!agentIdEnv) { + std::cerr << "AGENT_ID environment variable must be set" << std::endl; + return 1; + } + std::string agentId(agentIdEnv); + + auto audioInterface = std::make_shared(); + Conversation conv(agentId, /*requiresAuth*/ false, audioInterface, + [](const std::string& resp) { std::cout << "Agent: " << resp << std::endl; }, + [](const std::string& orig, const std::string& corrected) { + std::cout << "Agent correction: " << orig << " -> " << corrected << std::endl; }, + [](const std::string& transcript) { std::cout << "User: " << transcript << std::endl; }); + + conv.startSession(); + + std::cout << "Press Enter to quit..." << std::endl; + std::cin.get(); + conv.endSession(); + auto convId = conv.waitForSessionEnd(); + std::cout << "Conversation ID: " << convId << std::endl; + return 0; +} \ No newline at end of file diff --git a/Unreal/PS_AI_Agent/Content/test_AI_Actor.uasset b/Unreal/PS_AI_Agent/Content/test_AI_Actor.uasset index 748ebd54c4400a977eaff42982c5c4dab431f6f3..a4affbacdfdb7f48067bd653f8e7bc23f093c758 100644 GIT binary patch literal 157437 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

_nWv_%Pf1v^r|O7+OPp=Y%FsS@*Qy)GFm9<@My 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

e_78q(Z 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 D!;rT` diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsConversationalAgentComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsConversationalAgentComponent.cpp index c13d380..4e4e0f4 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsConversationalAgentComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsConversationalAgentComponent.cpp @@ -604,9 +604,15 @@ void UElevenLabsConversationalAgentComponent::OnMicrophoneDataCaptured(const TAr { if (!IsConnected() || !bIsListening) return; + // Echo suppression: skip sending mic audio while the agent is speaking. + // This prevents the agent from hearing its own voice through the speakers, + // which would confuse the server's VAD and STT. Matches the approach used + // in the official ElevenLabs C++ SDK (outputPlaying_ flag). + if (bAgentSpeaking) return; + // Convert this callback's samples to int16 bytes and accumulate. - // WASAPI fires every ~5ms (158 bytes at 16kHz). ElevenLabs needs ≥100ms - // (3200 bytes) per chunk for reliable VAD and STT. We hold bytes here + // WASAPI fires every ~5ms (158 bytes at 16kHz). ElevenLabs needs ≥250ms + // (8000 bytes) per chunk for reliable VAD and STT. We hold bytes here // until we have enough, then send the whole batch in one WebSocket frame. TArray PCMBytes = FloatPCMToInt16Bytes(FloatPCM); MicAccumulationBuffer.Append(PCMBytes); diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsWebSocketProxy.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsWebSocketProxy.cpp index 3f3215b..83f273d 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsWebSocketProxy.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Private/ElevenLabsWebSocketProxy.cpp @@ -491,6 +491,17 @@ void UElevenLabsWebSocketProxy::HandleAudioResponse(const TSharedPtrTryGetNumberField(TEXT("event_id"), EventId); + if (EventId > 0 && EventId <= LastInterruptEventId) + { + UE_LOG(LogElevenLabsWS, Verbose, TEXT("Discarding audio event_id=%d (interrupted at %d)."), EventId, LastInterruptEventId); + return; + } + FString Base64Audio; if (!(*AudioEvent)->TryGetStringField(TEXT("audio_base_64"), Base64Audio)) { @@ -591,7 +602,20 @@ void UElevenLabsWebSocketProxy::HandleAgentChatResponsePart(const TSharedPtr& Root) { - UE_LOG(LogElevenLabsWS, Log, TEXT("Agent interrupted (server ack received).")); + // Extract the interrupt event_id so we can filter stale audio frames. + // { "type": "interruption", "interruption_event": { "event_id": 42 } } + const TSharedPtr* InterruptEvent = nullptr; + if (Root->TryGetObjectField(TEXT("interruption_event"), InterruptEvent) && InterruptEvent) + { + int32 EventId = 0; + (*InterruptEvent)->TryGetNumberField(TEXT("event_id"), EventId); + if (EventId > LastInterruptEventId) + { + LastInterruptEventId = EventId; + } + } + + UE_LOG(LogElevenLabsWS, Log, TEXT("Agent interrupted (server ack, LastInterruptEventId=%d)."), LastInterruptEventId); OnInterrupted.Broadcast(); } diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsConversationalAgentComponent.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsConversationalAgentComponent.h index 05d0665..27f93b7 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsConversationalAgentComponent.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsConversationalAgentComponent.h @@ -400,5 +400,5 @@ private: // ElevenLabs needs at least ~100ms (3200 bytes) per chunk for reliable VAD/STT. // We accumulate here and only call SendAudioChunk once enough bytes are ready. TArray MicAccumulationBuffer; - static constexpr int32 MicChunkMinBytes = 3200; // 100ms @ 16kHz 16-bit mono + static constexpr int32 MicChunkMinBytes = 8000; // 250ms @ 16kHz 16-bit mono (4000 samples, matches ElevenLabs SDK recommendation) }; diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsWebSocketProxy.h b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsWebSocketProxy.h index 1cc0efc..fa1ee67 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsWebSocketProxy.h +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/Source/PS_AI_Agent_ElevenLabs/Public/ElevenLabsWebSocketProxy.h @@ -226,6 +226,13 @@ private: // Used to compute [T+Xs] session-relative timestamps in all log messages. double SessionStartTime = 0.0; + // ── Interrupt filtering (event_id approach, matching official SDK) ──────── + // When the server sends an "interruption" event it includes an event_id. + // Audio events whose event_id <= LastInterruptEventId belong to the cancelled + // generation and must be discarded. Only AUDIO is filtered — transcripts, + // agent_response, agent_chat_response_part etc. are always processed. + int32 LastInterruptEventId = 0; + public: // Set by UElevenLabsConversationalAgentComponent before calling Connect(). // Controls turn_timeout in conversation_initiation_client_data.