Compare commits
175 Commits
c2142f3e6b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f35c134aa | |||
| 086311da38 | |||
| c783613117 | |||
| d92e402722 | |||
| 231ca4145d | |||
| 087c83018c | |||
| f49a57f892 | |||
| 6d92ec3c26 | |||
| bcb6c44f57 | |||
| cf83a2302d | |||
| 5b3a11a733 | |||
| 2ca5643f32 | |||
| 4854a7fa9a | |||
| 41900991ef | |||
| efd9921fde | |||
| fee7d0c679 | |||
| 777ffa09a8 | |||
| ab2482a209 | |||
| c2ca2e3dbb | |||
| 8ae73bbacb | |||
| 6a8c22c77e | |||
| 7d99a89ac7 | |||
| a5b0d5db84 | |||
| cfade2a265 | |||
| 27d0906d21 | |||
| 89d555b734 | |||
| 7430a18e66 | |||
| ceaf89f836 | |||
| a1e34e6dfa | |||
| f338c0586e | |||
| 48d1543329 | |||
| 15871be9e9 | |||
| 6fcbdb2005 | |||
| e322a400be | |||
| fda2e4e32f | |||
| e32c53ffd6 | |||
| 66171cc0bd | |||
| c1afddc8b7 | |||
| a31ac1d782 | |||
| 661e8c66ac | |||
| b473fcda22 | |||
| c32aba9902 | |||
| 90bee03b44 | |||
| d9fb46bc96 | |||
| d471714fbd | |||
| af93194f48 | |||
| f5897b46cc | |||
| de3a5310f4 | |||
| 69b9844a4b | |||
| 78149fffcd | |||
| b60086d107 | |||
| d6325b373d | |||
| 011bfcf62a | |||
| 391a35ac2c | |||
| 44f7e860aa | |||
| 86d3ae118d | |||
| 9aaf16c655 | |||
| 98f0dbdce5 | |||
| 25abd59512 | |||
| 59a23e61db | |||
| 65b86e2fbd | |||
| 2588883a1c | |||
| e7c3598dce | |||
| 0c4c409ebc | |||
| 137aeb5953 | |||
| eb5113ea30 | |||
| 1799ba28c0 | |||
| 9d054cc46f | |||
| 2e04cb0334 | |||
| 909583a1bb | |||
| 5d5b85380a | |||
| a07ac36686 | |||
| 18996a7254 | |||
| e5a32f5997 | |||
| aea02abe89 | |||
| eaa52a5c5f | |||
| 28aed55cd3 | |||
| 4456dfa9dc | |||
| 2169c58cd7 | |||
| 5fad6376bc | |||
| 0b190c3149 | |||
| 56b072c45e | |||
| a8dd5a022f | |||
| 955c97e0dd | |||
| d60f8d8484 | |||
| 6d4ef21269 | |||
| c922fd304c | |||
| ca10689bb6 | |||
| 8d4065944c | |||
| 8ad66ae4d5 | |||
| e5f40c65ec | |||
| 9321e21a3b | |||
| fb641d5aa4 | |||
| 2e96e3c766 | |||
| 8bb4371a74 | |||
| acfae96420 | |||
| ae40153252 | |||
| ac2d40b67b | |||
| d035f5410a | |||
| 35d217f6ec | |||
| 8886e7a7a2 | |||
| 28964f0a40 | |||
| b5c52c9236 | |||
| 7f92dcab51 | |||
| 677f08e936 | |||
| 45ee0c6f7d | |||
| 1c4dbfc402 | |||
| 86b7d9744e | |||
| 5e18e7cc8c | |||
| 3952847ece | |||
| 76dd13944a | |||
| bf08bb67d9 | |||
| 215cb398fd | |||
| 3c4389a43d | |||
| 4d662ebada | |||
| 8a3304fea9 | |||
| ce84a5dc58 | |||
| daf79d0d89 | |||
| 0124de8b53 | |||
| c0c1b2cea4 | |||
| fc728454d0 | |||
| 6cac56fa06 | |||
| 5e1c50edf8 | |||
| 1990b828a2 | |||
| 913be3c631 | |||
| 84a4fd7e0e | |||
| 11255db576 | |||
| 9ee7960855 | |||
| e4cf657077 | |||
| 11c473f3ab | |||
| 0b07a0493f | |||
| 82b134bcc3 | |||
| 259a77f9f6 | |||
| 8175375c28 | |||
| 275065f5aa | |||
| 33ec54150f | |||
| 5fcd98ba73 | |||
| 8fcb8b6f30 | |||
| 09ca2215a0 | |||
| 5323da6908 | |||
| 350558ba50 | |||
| 21298e01b0 | |||
| 765da966a2 | |||
| 453450d7eb | |||
| 301efee982 | |||
| 92a8b70a7f | |||
| 23e216b211 | |||
| 6c081e1207 | |||
| c6aed472f9 | |||
| b1bac66256 | |||
| 88a824897e | |||
| f24c9355eb | |||
| a3adf10e6f | |||
| b3bfabc52d | |||
| eeb0b1172b | |||
| 8a4622f348 | |||
| fd0e2a2d58 | |||
| 9eca5a3cfe | |||
| 9d281c2e03 | |||
| d2e904144a | |||
| cdb118a83e | |||
| 8df6967ee5 | |||
| c3abc420cf | |||
| 81bcd67428 | |||
| 250ae5e04d | |||
| 7c235106d6 | |||
| 780101d389 | |||
| 7373959d8b | |||
| 88c175909e | |||
| f6541bd7e2 | |||
| 949efd5578 | |||
| f57bb65297 | |||
| e57be0a1d9 | |||
| aa3010bae8 | |||
| 6543bc6785 |
95
.claude/CLAUDE.md
Normal file
95
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# PS_AI_Agent — Notes pour Claude
|
||||||
|
|
||||||
|
## Workflow règles
|
||||||
|
|
||||||
|
**TOUJOURS demander avant de lancer un changement de code.** Avant d'appeler Edit/Write/sed sur un fichier source :
|
||||||
|
1. Expliquer ce qu'on s'apprête à faire (quel fichier, quelle logique, quel impact)
|
||||||
|
2. Attendre la confirmation explicite de l'utilisateur
|
||||||
|
3. Seulement après → effectuer le changement
|
||||||
|
|
||||||
|
Cela s'applique à : modifications de code (.cpp/.h), refactors, renames bulk, ajouts de nouvelles fonctions.
|
||||||
|
Cela ne s'applique PAS à : lecture de fichiers, recherches, git status/log/diff, builds, questions diagnostiques.
|
||||||
|
|
||||||
|
Exceptions où on peut agir directement :
|
||||||
|
- L'utilisateur dit explicitement "fais-le" / "go" / "lance" / "commit"
|
||||||
|
- L'utilisateur demande explicitement un changement précis ("renomme X en Y", "ajoute cette fonction")
|
||||||
|
- Annulation / revert demandé par l'utilisateur
|
||||||
|
|
||||||
|
## Plugins dans ce repo
|
||||||
|
|
||||||
|
### PS_AI_ConvAgent
|
||||||
|
- **Module**: `PS_AI_ConvAgent` / `PS_AI_ConvAgentEditor`
|
||||||
|
- **API macro**: `PS_AI_CONVAGENT_API`
|
||||||
|
- **Class prefix**: `PS_AI_ConvAgent_` (e.g. `UPS_AI_ConvAgent_PostureComponent`)
|
||||||
|
- **ElevenLabs-specific**: suffix `_ElevenLabs` (e.g. `UPS_AI_ConvAgent_ElevenLabsComponent`)
|
||||||
|
- **UI display**: `"PS AI ConvAgent"` (categories, DisplayName)
|
||||||
|
- **CoreRedirects**: DefaultEngine.ini handles both ElevenLabs* and PS_AI_Agent_* → PS_AI_ConvAgent_*
|
||||||
|
|
||||||
|
### PS_AI_Behavior
|
||||||
|
- **Module**: `PS_AI_Behavior` / `PS_AI_BehaviorEditor`
|
||||||
|
- **API macro**: `PS_AI_BEHAVIOR_API`
|
||||||
|
- **Class prefix**: `PS_AI_Behavior_` (e.g. `UPS_AI_Behavior_PersonalityComponent`)
|
||||||
|
- **UI display**: `"PS AI Behavior"` (categories, DisplayName)
|
||||||
|
- **Interface**: `IPS_AI_Behavior_Interface` — implements on host project Pawns
|
||||||
|
|
||||||
|
## PS_AI_Behavior — Architecture
|
||||||
|
|
||||||
|
### Core Design
|
||||||
|
- **Interface-driven**: `IPS_AI_Behavior_Interface` on Pawns decouples plugin from host project
|
||||||
|
- **Personality profiles**: DataAsset with 5 trait axes, thresholds, target priority, speed per state
|
||||||
|
- **NPCType enum**: Civilian, Enemy, Protector, Any (unified — no separate SplineCategory)
|
||||||
|
- **TeamIds**: Civilian=1, Enemy=2, Protector=3, auto-assigned in OnPossess
|
||||||
|
- **Hostile switch**: Enemy with IsBehaviorHostile=false → TeamId=1 (disguised), flips to 2 at runtime
|
||||||
|
|
||||||
|
### Blackboard
|
||||||
|
- Key `BehaviorState` = Enum type, path: `/Script/PS_AI_Behavior.EPS_AI_Behavior_State`
|
||||||
|
- Values: Idle(0), Patrol(1), Alerted(2), Combat(3), Fleeing(4), TakingCover(5), Dead(6)
|
||||||
|
- BB asset created manually in editor (runtime fallback exists but prefer asset)
|
||||||
|
- SetValueAsEnum / GetValueAsEnum (not Int)
|
||||||
|
|
||||||
|
### BT Pattern
|
||||||
|
- Services on Selector: UpdateThreat + EvaluateReaction
|
||||||
|
- Decorator Observer Aborts = Both on Combat/Flee branches
|
||||||
|
- Spline branch as fallback (lowest priority, no condition decorator)
|
||||||
|
- Attack task stays InProgress permanently, Decorator pulls out on state change
|
||||||
|
|
||||||
|
### Spline System
|
||||||
|
- SplinePath actors placed in level, SplineCategory = NPCType
|
||||||
|
- SplineNetwork WorldSubsystem auto-detects junctions
|
||||||
|
- SplineFollowerComponent on Pawns: uses Pawn's real velocity, debug green sphere
|
||||||
|
- Junction switching: 70% probability, direction filter (dot > -0.5)
|
||||||
|
|
||||||
|
### Perception
|
||||||
|
- Senses configured in BeginPlay (NOT constructor — NewObject crashes in CDO)
|
||||||
|
- RequestStimuliListenerUpdate() after ConfigureSenses
|
||||||
|
- Detect ALL affiliations, filter by Hostile attitude in CalculateThreatLevel
|
||||||
|
- Target priority from PersonalityProfile (combat targeting)
|
||||||
|
|
||||||
|
### Known Gotchas (UE 5.5)
|
||||||
|
- BB Enum picker doesn't list plugin enums — use string path
|
||||||
|
- NTFS junctions (not symlinks) for UE5 cross-project plugin linking
|
||||||
|
- SVN doesn't traverse junctions — robocopy for initial import
|
||||||
|
- Live Coding fails on header changes — full rebuild required
|
||||||
|
- BlueprintNativeEvent: UHT auto-generates _Implementation defaults, don't duplicate in .cpp
|
||||||
|
- FGenericTeamId::NoTeam comparison: always recalculate TeamId (BP CDOs may reset to 0)
|
||||||
|
|
||||||
|
### Testing Status (2026-03-26)
|
||||||
|
- ✅ Patrol, Spline following, Perception, Threat, Flee, Combat delegation, Hostile switch
|
||||||
|
- 🔲 Cover Points, EQS, Editor tools (SplineEdMode, CoverPoint placement)
|
||||||
|
|
||||||
|
## Posture System — Diagonal Tilt Bug (à corriger)
|
||||||
|
|
||||||
|
### Problème
|
||||||
|
Le tilt diagonal (ear-to-shoulder) est PLUS VISIBLE avec la neck bone chain qu'avec le mono-bone.
|
||||||
|
|
||||||
|
### Piste de fix
|
||||||
|
Faire le swing-twist **PER-BONE** : pour chaque bone de la chaîne, composer le FractionalRot avec le bone's own rotation, swing-twist autour du bone's own tilt axis.
|
||||||
|
|
||||||
|
### Fichiers concernés
|
||||||
|
- `AnimNode_PS_AI_ConvAgent_Posture.cpp` — Evaluate_AnyThread, section multi-bone chain
|
||||||
|
|
||||||
|
## Architecture Posture (ConvAgent)
|
||||||
|
- **PS_AI_ConvAgent_PostureComponent** (game thread) : cascade Eyes→Head→Body
|
||||||
|
- **AnimNode_PS_AI_ConvAgent_Posture** (anim thread) : applique rotation sur bone(s) + injecte curves
|
||||||
|
- Pipeline 100% FQuat, Thread safety via FCriticalSection
|
||||||
|
- ARKit eye curves normalisées par range fixe (40°/35°)
|
||||||
@@ -17,69 +17,70 @@
|
|||||||
## Plugins
|
## Plugins
|
||||||
| Plugin | Path | Purpose |
|
| Plugin | Path | Purpose |
|
||||||
|--------|------|---------|
|
|--------|------|---------|
|
||||||
| Convai (reference) | `<repo_root>/ConvAI/Convai/` | gRPC + protobuf streaming to Convai API. Has ElevenLabs voice type enum in `ConvaiDefinitions.h`. Used as architectural reference. |
|
| Convai (reference) | `<repo_root>/ConvAI/Convai/` | gRPC + protobuf streaming to Convai API. Used as architectural reference. |
|
||||||
| **PS_AI_Agent_ElevenLabs** | `<repo_root>/Unreal/PS_AI_Agent/Plugins/PS_AI_Agent_ElevenLabs/` | Our ElevenLabs Conversational AI integration. See `.claude/elevenlabs_plugin.md` for full details. |
|
| **PS_AI_ConvAgent** | `<repo_root>/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/` | Main plugin — ElevenLabs Conversational AI, posture, gaze, lip sync, facial expressions. |
|
||||||
|
|
||||||
## User Preferences
|
## User Preferences
|
||||||
- Plugin naming: `PS_AI_Agent_<Service>` (e.g. `PS_AI_Agent_ElevenLabs`)
|
- Plugin naming: `PS_AI_ConvAgent` (renamed from PS_AI_Agent_ElevenLabs)
|
||||||
- Save memory frequently during long sessions
|
- Save memory frequently during long sessions
|
||||||
- Goal: ElevenLabs Conversational AI integration — simpler than Convai, no gRPC
|
|
||||||
- Full original ask + intent: see `.claude/project_context.md`
|
|
||||||
- Git remote is a **private server** — no public exposure risk
|
- Git remote is a **private server** — no public exposure risk
|
||||||
|
- Full original ask + intent: see `.claude/project_context.md`
|
||||||
|
|
||||||
|
## Current Branch & Work
|
||||||
|
- **Branch**: `main`
|
||||||
|
- **Recent merges**: `feature/multi-player-shared-agent` merged to main
|
||||||
|
|
||||||
|
### Latency Debug HUD (just implemented)
|
||||||
|
- Separate `bDebugLatency` property + CVar `ps.ai.ConvAgent.Debug.Latency`
|
||||||
|
- All metrics anchored to `GenerationStartTime` (`agent_response_started` event)
|
||||||
|
- Metrics: Gen>Audio (LLM+TTS), Pre-buffer, Gen>Ear (user-perceived)
|
||||||
|
- Reset per turn in `HandleAgentResponseStarted()`
|
||||||
|
- `DrawLatencyHUD()` separate from `DrawDebugHUD()`
|
||||||
|
|
||||||
|
### Future: Server-Side Latency from ElevenLabs API
|
||||||
|
**TODO — high-value improvement parked for later:**
|
||||||
|
- `GET /v1/convai/conversations/{conversation_id}` returns:
|
||||||
|
- `conversation_turn_metrics` with `elapsed_time` per metric (STT, LLM, TTS breakdown!)
|
||||||
|
- `tool_latency_secs`, `step_latency_secs`, `rag_latency_secs`
|
||||||
|
- `time_in_call_secs` per message
|
||||||
|
- `ping` WS event has `ping_ms` (network round-trip) — could display on HUD
|
||||||
|
- `vad_score` WS event (0.0-1.0) — could detect real speech start client-side
|
||||||
|
- Docs: https://elevenlabs.io/docs/api-reference/conversations/get
|
||||||
|
|
||||||
|
### Multi-Player Shared Agent — Key Design
|
||||||
|
- **Old model**: exclusive lock (one player per agent via `NetConversatingPawn`)
|
||||||
|
- **New model**: shared array (`NetConnectedPawns`) + active speaker (`NetActiveSpeakerPawn`)
|
||||||
|
- Speaker arbitration: server-side with `SpeakerSwitchHysteresis` (0.3s) + `SpeakerIdleTimeout` (3.0s)
|
||||||
|
- In standalone (≤1 player): speaker arbitration bypassed, audio sent directly to WebSocket
|
||||||
|
- Internal mic (WASAPI thread): direct WebSocket send, no game-thread state access
|
||||||
|
- `GetCurrentBlendshapes()` thread-safe via `ThreadSafeBlendshapes` snapshot + `BlendshapeLock`
|
||||||
|
|
||||||
## Key UE5 Plugin Patterns
|
## Key UE5 Plugin Patterns
|
||||||
- Settings object: `UCLASS(config=Engine, defaultconfig)` inheriting `UObject`, registered via `ISettingsModule`
|
- Settings object: `UCLASS(config=Engine, defaultconfig)` inheriting `UObject`, registered via `ISettingsModule`
|
||||||
- Module startup: `NewObject<USettings>(..., RF_Standalone)` + `AddToRoot()`
|
|
||||||
- WebSocket: `FWebSocketsModule::Get().CreateWebSocket(URL, TEXT(""), Headers)`
|
- WebSocket: `FWebSocketsModule::Get().CreateWebSocket(URL, TEXT(""), Headers)`
|
||||||
- `WebSockets` is a **module** (Build.cs only) — NOT a plugin, don't put it in `.uplugin`
|
- Audio capture: `Audio::FAudioCapture::OpenAudioCaptureStream()` (UE 5.3+)
|
||||||
- Audio capture: `Audio::FAudioCapture::OpenAudioCaptureStream()` (UE 5.3+, replaces deprecated `OpenCaptureStream`)
|
- Callback arrives on **background thread** — marshal to game thread
|
||||||
- `AudioCapture` IS a plugin — declare it in `.uplugin` Plugins array
|
- Procedural audio playback: `USoundWaveProcedural` + `OnSoundWaveProceduralUnderflow`
|
||||||
- Callback type: `FOnAudioCaptureFunction` = `TFunction<void(const void*, int32, int32, int32, double, bool)>`
|
|
||||||
- Cast `const void*` to `const float*` inside — device sends float32 interleaved
|
|
||||||
- Procedural audio playback: `USoundWaveProcedural` + `OnSoundWaveProceduralUnderflow` delegate
|
|
||||||
- Audio capture callbacks arrive on a **background thread** — always marshal to game thread with `AsyncTask(ENamedThreads::GameThread, ...)`
|
|
||||||
- Resample mic audio to **16000 Hz mono** before sending to ElevenLabs
|
- Resample mic audio to **16000 Hz mono** before sending to ElevenLabs
|
||||||
- `TArray::RemoveAt(idx, count, EAllowShrinking::No)` — bool overload deprecated in UE 5.5
|
- `TArray::RemoveAt(idx, count, EAllowShrinking::No)` — bool overload deprecated in UE 5.5
|
||||||
|
|
||||||
## Plugin Status
|
|
||||||
- **PS_AI_Agent_ElevenLabs**: compiles cleanly on UE 5.5 Win64 (verified 2026-02-19)
|
|
||||||
- v1.5.0 — mic audio chunk size fixed: WASAPI 5ms callbacks accumulated to 100ms before sending
|
|
||||||
- v1.4.0 — push-to-talk fully fixed: bAutoStartListening now ignored in Client turn mode
|
|
||||||
- Binary WS frame handling implemented (ElevenLabs sends ALL frames as binary, not text)
|
|
||||||
- First-byte discrimination: `{` = JSON control message, else = raw PCM audio
|
|
||||||
- `SendTextMessage()` added to both WebSocketProxy and ConversationalAgentComponent
|
|
||||||
- `conversation_initiation_client_data` now sent immediately on WS connect (required for mic + latency)
|
|
||||||
|
|
||||||
## Audio Chunk Size — CRITICAL
|
|
||||||
- WASAPI fires mic callbacks every ~5ms → **158 bytes** at 16kHz 16-bit mono
|
|
||||||
- ElevenLabs VAD/STT requires **≥3200 bytes (100ms)** per chunk; smaller chunks are silently ignored
|
|
||||||
- Fix: `MicAccumulationBuffer` in component accumulates chunks; sends only when `>= MicChunkMinBytes` (3200)
|
|
||||||
- `StopListening()` flushes remainder so final partial chunk is never dropped before end-of-turn
|
|
||||||
|
|
||||||
## ElevenLabs WebSocket Protocol Notes
|
## ElevenLabs WebSocket Protocol Notes
|
||||||
- **ALL frames are binary** — bind ONLY `OnRawMessage`; NEVER bind `OnMessage` (text) — UE fires both for same frame → double audio bug
|
- **ALL frames are binary** — bind ONLY `OnRawMessage`; NEVER bind `OnMessage` (text)
|
||||||
- Binary frame discrimination: peek byte[0] → `'{'` (0x7B) = JSON, else = raw PCM audio
|
- Binary frame discrimination: peek byte[0] → `'{'` (0x7B) = JSON, else = raw PCM audio
|
||||||
- Fragment reassembly: accumulate into `BinaryFrameBuffer` until `BytesRemaining == 0`
|
|
||||||
- Pong: `{"type":"pong","event_id":N}` — `event_id` is **top-level**, NOT nested
|
- Pong: `{"type":"pong","event_id":N}` — `event_id` is **top-level**, NOT nested
|
||||||
- Transcript: type=`user_transcript`, key=`user_transcription_event`, field=`user_transcript`
|
- `user_transcript` arrives AFTER `agent_response_started` in Server VAD mode
|
||||||
- Client turn mode (`client_vad`): send `user_activity` **with every audio chunk** (not just once) — server needs continuous signal to know user is speaking; stopping chunks = silence detected = agent responds
|
- **MUST send `conversation_initiation_client_data` immediately after WS connect**
|
||||||
- Text input: `{"type":"user_message","text":"..."}` — agent replies with audio + text
|
|
||||||
- **MUST send `conversation_initiation_client_data` immediately after WS connect** — without it, server won't process client audio (mic appears dead)
|
|
||||||
- `conversation_initiation_client_data` payload: `conversation_config_override.agent.turn.mode`, `conversation_config_override.tts.optimize_streaming_latency`, `custom_llm_extra_body.enable_intermediate_response`
|
|
||||||
- `enable_intermediate_response: true` in `custom_llm_extra_body` reduces time-to-first-audio latency
|
|
||||||
|
|
||||||
## API Keys / Secrets
|
## API Keys / Secrets
|
||||||
- ElevenLabs API key is set in **Project Settings → Plugins → ElevenLabs AI Agent** in the Editor
|
- ElevenLabs API key: **Project Settings → Plugins → ElevenLabs AI Agent**
|
||||||
- UE saves it to `DefaultEngine.ini` under `[/Script/PS_AI_Agent_ElevenLabs.ElevenLabsSettings]`
|
- Saved to `DefaultEngine.ini` — **stripped before every commit**
|
||||||
- **The key is stripped from `DefaultEngine.ini` before every commit** — do not commit it
|
|
||||||
- Each developer sets the key locally; it does not go in git
|
|
||||||
|
|
||||||
## Claude Memory Files in This Repo
|
## Claude Memory Files in This Repo
|
||||||
| File | Contents |
|
| File | Contents |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `.claude/MEMORY.md` | This file — project structure, patterns, status |
|
| `.claude/MEMORY.md` | This file — project structure, patterns, status |
|
||||||
| `.claude/elevenlabs_plugin.md` | Plugin file map, ElevenLabs WS protocol, design decisions |
|
| `.claude/elevenlabs_plugin.md` | Plugin file map, ElevenLabs WS protocol, design decisions |
|
||||||
| `.claude/elevenlabs_api_reference.md` | Full ElevenLabs API reference (WS messages, REST, signed URL, Agent ID location) |
|
| `.claude/elevenlabs_api_reference.md` | Full ElevenLabs API reference (WS messages, REST, signed URL) |
|
||||||
| `.claude/project_context.md` | Original ask, intent, short/long-term goals |
|
| `.claude/project_context.md` | Original ask, intent, short/long-term goals |
|
||||||
| `.claude/session_log_2026-02-19.md` | Full session record: steps, commits, technical decisions, next steps |
|
| `.claude/session_log_2026-02-19.md` | Session record: steps, commits, technical decisions |
|
||||||
| `.claude/PS_AI_Agent_ElevenLabs_Documentation.md` | User-facing Markdown reference doc |
|
| `.claude/PS_AI_Agent_ElevenLabs_Documentation.md` | User-facing Markdown reference doc |
|
||||||
|
|||||||
30
.gitattributes
vendored
Normal file
30
.gitattributes
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Force consistent line endings across all machines
|
||||||
|
# LF in the repo, native (CRLF on Windows) in working tree
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Source code — always normalize
|
||||||
|
*.cpp text
|
||||||
|
*.h text
|
||||||
|
*.cs text
|
||||||
|
*.py text
|
||||||
|
*.ini text
|
||||||
|
*.json text
|
||||||
|
*.md text
|
||||||
|
*.xml text
|
||||||
|
*.yaml text
|
||||||
|
*.yml text
|
||||||
|
|
||||||
|
# Unreal assets — binary, no normalization
|
||||||
|
*.uasset binary
|
||||||
|
*.umap binary
|
||||||
|
*.uproject text
|
||||||
|
|
||||||
|
# Binaries — no normalization
|
||||||
|
*.dll binary
|
||||||
|
*.exe binary
|
||||||
|
*.pdb binary
|
||||||
|
*.lib binary
|
||||||
|
*.exp binary
|
||||||
|
*.so binary
|
||||||
|
*.dylib binary
|
||||||
|
*.a binary
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -5,17 +5,23 @@ Unreal/PS_AI_Agent/Intermediate/
|
|||||||
Unreal/PS_AI_Agent/Saved/
|
Unreal/PS_AI_Agent/Saved/
|
||||||
ConvAI/Convai/Binaries/
|
ConvAI/Convai/Binaries/
|
||||||
|
|
||||||
# All plugin build artifacts (Binaries + Intermediate for any plugin)
|
# Plugin build artifacts (regenerated)
|
||||||
Unreal/PS_AI_Agent/Plugins/*/Binaries/
|
Unreal/PS_AI_Agent/Plugins/*/Binaries/
|
||||||
Unreal/PS_AI_Agent/Plugins/*/Intermediate/
|
Unreal/PS_AI_Agent/Plugins/*/Intermediate/
|
||||||
|
|
||||||
|
# Live Coding patch files (temporary, regenerated each session)
|
||||||
|
*.patch_*
|
||||||
|
|
||||||
# UE5 generated solution files
|
# UE5 generated solution files
|
||||||
Unreal/PS_AI_Agent/*.sln
|
Unreal/PS_AI_Agent/*.sln
|
||||||
Unreal/PS_AI_Agent/*.suo
|
Unreal/PS_AI_Agent/*.suo
|
||||||
|
|
||||||
# Claude Code local session settings (machine-specific, memory files in .claude/ are kept)
|
# Claude Code local session settings (machine-specific, memory files in .claude/ are kept)
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
.claude/worktrees/
|
||||||
|
|
||||||
# Documentation generator script (dev tool, output .pptx is committed instead)
|
# Documentation generator script (dev tool, output .pptx is committed instead)
|
||||||
generate_pptx.py
|
generate_pptx.py
|
||||||
~$PS_AI_Agent_ElevenLabs_Documentation.pptx
|
~$PS_AI_Agent_ElevenLabs_Documentation.pptx
|
||||||
|
Unreal/PS_AI_Agent/Build/
|
||||||
|
Unreal/PS_AI_Agent/Builds/
|
||||||
|
|||||||
65
Build_Plugin.bat
Normal file
65
Build_Plugin.bat
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
:: ─── Configuration ──────────────────────────────────────────────────────────
|
||||||
|
set UE_ROOT=C:\Program Files\Epic Games\UE_5.5
|
||||||
|
set UPROJECT=E:\ASTERION\GIT\PS_AI_Agent\Unreal\PS_AI_Agent\PS_AI_Agent.uproject
|
||||||
|
set PLATFORM=Win64
|
||||||
|
set CONFIG=Development
|
||||||
|
set TARGET=PS_AI_AgentEditor
|
||||||
|
|
||||||
|
:: ─── Colors ─────────────────────────────────────────────────────────────────
|
||||||
|
set GREEN=[92m
|
||||||
|
set RED=[91m
|
||||||
|
set YELLOW=[93m
|
||||||
|
set RESET=[0m
|
||||||
|
|
||||||
|
:: ─── Banner ─────────────────────────────────────────────────────────────────
|
||||||
|
echo.
|
||||||
|
echo %GREEN%============================================%RESET%
|
||||||
|
echo PS_AI_Agent - Plugin Build
|
||||||
|
echo Target: %TARGET% %PLATFORM% %CONFIG%
|
||||||
|
echo %GREEN%============================================%RESET%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: ─── Verify paths ───────────────────────────────────────────────────────────
|
||||||
|
if not exist "%UE_ROOT%\Engine\Binaries\ThirdParty\DotNet\8.0.300\win-x64\dotnet.exe" (
|
||||||
|
echo %RED%ERROR: UE5.5 dotnet not found at %UE_ROOT%%RESET%
|
||||||
|
echo Check UE_ROOT path in this script.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%UPROJECT%" (
|
||||||
|
echo %RED%ERROR: .uproject not found: %UPROJECT%%RESET%
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ─── Build ──────────────────────────────────────────────────────────────────
|
||||||
|
echo %YELLOW%Building %TARGET% (%CONFIG%)...%RESET%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
"%UE_ROOT%\Engine\Binaries\ThirdParty\DotNet\8.0.300\win-x64\dotnet.exe" ^
|
||||||
|
"%UE_ROOT%\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.dll" ^
|
||||||
|
%TARGET% %PLATFORM% %CONFIG% ^
|
||||||
|
-Project="%UPROJECT%" ^
|
||||||
|
-WaitMutex ^
|
||||||
|
-FromMsBuild
|
||||||
|
|
||||||
|
set BUILD_EXIT=%ERRORLEVEL%
|
||||||
|
|
||||||
|
echo.
|
||||||
|
if %BUILD_EXIT% EQU 0 (
|
||||||
|
echo %GREEN%============================================%RESET%
|
||||||
|
echo BUILD SUCCEEDED
|
||||||
|
echo %GREEN%============================================%RESET%
|
||||||
|
) else (
|
||||||
|
echo %RED%============================================%RESET%
|
||||||
|
echo BUILD FAILED (exit code: %BUILD_EXIT%)
|
||||||
|
echo %RED%============================================%RESET%
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b %BUILD_EXIT%
|
||||||
152
CPP/elevenlabs-convai-cpp-main/.gitignore
vendored
152
CPP/elevenlabs-convai-cpp-main/.gitignore
vendored
@@ -1,152 +0,0 @@
|
|||||||
# 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/
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
# ElevenLabs Conversational AI - C++ Implementation
|
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
|
||||||
[](https://en.wikipedia.org/wiki/C%2B%2B17)
|
|
||||||
[](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.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class AudioInterface {
|
|
||||||
public:
|
|
||||||
using AudioCallback = std::function<void(const std::vector<char>&)>;
|
|
||||||
|
|
||||||
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<char>& audio) = 0;
|
|
||||||
|
|
||||||
// Immediately stop any buffered / ongoing output.
|
|
||||||
virtual void interrupt() = 0;
|
|
||||||
};
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "AudioInterface.hpp"
|
|
||||||
#include <boost/beast/core.hpp>
|
|
||||||
#include <boost/beast/websocket.hpp>
|
|
||||||
#include <boost/beast/ssl.hpp>
|
|
||||||
#include <boost/asio.hpp>
|
|
||||||
#include <boost/asio/ssl/stream.hpp>
|
|
||||||
#include <boost/asio/ip/tcp.hpp>
|
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
#include <thread>
|
|
||||||
#include <atomic>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
class Conversation {
|
|
||||||
public:
|
|
||||||
using CallbackAgentResponse = std::function<void(const std::string&)>;
|
|
||||||
using CallbackAgentResponseCorrection = std::function<void(const std::string&, const std::string&)>;
|
|
||||||
using CallbackUserTranscript = std::function<void(const std::string&)>;
|
|
||||||
using CallbackLatencyMeasurement = std::function<void(int)>;
|
|
||||||
|
|
||||||
Conversation(
|
|
||||||
const std::string& agentId,
|
|
||||||
bool requiresAuth,
|
|
||||||
std::shared_ptr<AudioInterface> 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<tcp::socket>>;
|
|
||||||
std::unique_ptr<websocket_t> ws_;
|
|
||||||
|
|
||||||
// general state
|
|
||||||
std::string agentId_;
|
|
||||||
bool requiresAuth_;
|
|
||||||
std::shared_ptr<AudioInterface> audioInterface_;
|
|
||||||
|
|
||||||
CallbackAgentResponse callbackAgentResponse_;
|
|
||||||
CallbackAgentResponseCorrection callbackAgentResponseCorrection_;
|
|
||||||
CallbackUserTranscript callbackUserTranscript_;
|
|
||||||
CallbackLatencyMeasurement callbackLatencyMeasurement_;
|
|
||||||
|
|
||||||
std::thread workerThread_;
|
|
||||||
std::atomic<bool> shouldStop_{false};
|
|
||||||
std::string conversationId_;
|
|
||||||
|
|
||||||
std::atomic<int> lastInterruptId_{0};
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "AudioInterface.hpp"
|
|
||||||
#include <portaudio.h>
|
|
||||||
#include <mutex>
|
|
||||||
#include <condition_variable>
|
|
||||||
#include <queue>
|
|
||||||
#include <thread>
|
|
||||||
#include <atomic>
|
|
||||||
|
|
||||||
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<char>& 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<std::vector<char>> outputQueue_;
|
|
||||||
std::mutex queueMutex_;
|
|
||||||
std::condition_variable queueCv_;
|
|
||||||
|
|
||||||
std::thread outputThread_;
|
|
||||||
std::atomic<bool> shouldStop_{false};
|
|
||||||
std::atomic<bool> outputPlaying_{false};
|
|
||||||
};
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
#include "Conversation.hpp"
|
|
||||||
|
|
||||||
#include <boost/beast/websocket/ssl.hpp>
|
|
||||||
#include <boost/beast/websocket.hpp>
|
|
||||||
#include <boost/beast/ssl.hpp>
|
|
||||||
#include <boost/beast/core/detail/base64.hpp>
|
|
||||||
#include <boost/asio/connect.hpp>
|
|
||||||
#include <boost/algorithm/string.hpp>
|
|
||||||
#include <iostream>
|
|
||||||
#include <sstream>
|
|
||||||
#include <openssl/ssl.h>
|
|
||||||
|
|
||||||
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<char>& 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<char> base64Decode(const std::string& str) {
|
|
||||||
auto decodedSize = beast::detail::base64::decoded_size(str.size());
|
|
||||||
std::vector<char> 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<std::string>();
|
|
||||||
if(j.is_number_integer()) return std::to_string(j.get<int64_t>());
|
|
||||||
return j.dump();
|
|
||||||
}
|
|
||||||
|
|
||||||
Conversation::Conversation(const std::string& agentId, bool requiresAuth,
|
|
||||||
std::shared_ptr<AudioInterface> 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<unsigned short>(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<tcp::socket> 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<websocket_t>(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<char>& 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<std::string>();
|
|
||||||
} 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<std::string>());
|
|
||||||
audioInterface_->output(audioBytes);
|
|
||||||
} else if (type == "agent_response" && callbackAgentResponse_) {
|
|
||||||
auto event = message["agent_response_event"];
|
|
||||||
callbackAgentResponse_(event["agent_response"].get<std::string>());
|
|
||||||
} else if (type == "agent_response_correction" && callbackAgentResponseCorrection_) {
|
|
||||||
auto event = message["agent_response_correction_event"];
|
|
||||||
callbackAgentResponseCorrection_(event["original_agent_response"].get<std::string>(),
|
|
||||||
event["corrected_agent_response"].get<std::string>());
|
|
||||||
} else if (type == "user_transcript" && callbackUserTranscript_) {
|
|
||||||
auto event = message["user_transcription_event"];
|
|
||||||
callbackUserTranscript_(event["user_transcript"].get<std::string>());
|
|
||||||
} 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<int>() : std::stoi(event["ping_ms"].get<std::string>());
|
|
||||||
callbackLatencyMeasurement_(latency);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Note: client tool call handling omitted for brevity.
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
#include "DefaultAudioInterface.hpp"
|
|
||||||
|
|
||||||
#include <cstring>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
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<char>& audio) {
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lg(queueMutex_);
|
|
||||||
outputQueue_.emplace(audio);
|
|
||||||
}
|
|
||||||
queueCv_.notify_one();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DefaultAudioInterface::interrupt() {
|
|
||||||
std::lock_guard<std::mutex> lg(queueMutex_);
|
|
||||||
std::queue<std::vector<char>> 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<DefaultAudioInterface*>(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<char> buffer(bytes);
|
|
||||||
std::memcpy(buffer.data(), input, bytes);
|
|
||||||
inputCallback_(buffer);
|
|
||||||
return paContinue;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DefaultAudioInterface::outputThreadFunc() {
|
|
||||||
while (!shouldStop_.load()) {
|
|
||||||
std::vector<char> audio;
|
|
||||||
{
|
|
||||||
std::unique_lock<std::mutex> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#include "Conversation.hpp"
|
|
||||||
#include "DefaultAudioInterface.hpp"
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <iostream>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
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<DefaultAudioInterface>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user