""" End-to-end pipeline test: 1. Call /connect to get a room + token 2. Join the LiveKit room as a Python client 3. Publish TTS audio (pre-generated) as microphone input 4. Capture bot's audio response and save to file """ import asyncio import wave import struct import httpx import numpy as np from livekit import rtc PIPECAT_URL = "http://localhost:8882" TTS_URL = "http://host.docker.internal:8881" OUTPUT_FILE = "/tmp/bot_response.wav" SAMPLE_RATE = 48000 NUM_CHANNELS = 1 async def generate_tts_pcm(text: str) -> bytes: """Get WAV audio from Silero TTS, return raw PCM int16 bytes.""" async with httpx.AsyncClient(timeout=30) as c: r = await c.post(f"{TTS_URL}/v1/audio/speech", json={ "input": text, "voice": "onyx", "response_format": "wav" }) r.raise_for_status() # Skip WAV header (44 bytes) to get raw PCM return r.content[44:] async def main(): # Step 1 — create room print("[test] Creating room...") async with httpx.AsyncClient() as c: r = await c.post(f"{PIPECAT_URL}/connect") r.raise_for_status() creds = r.json() print(f"[test] Room: {creds['room']} URL: {creds['url']}") # Step 2 — generate test audio test_phrase = "Привет! Как тебя зовут?" print(f"[test] Generating TTS for: {test_phrase!r}") pcm_bytes = await generate_tts_pcm(test_phrase) print(f"[test] TTS PCM: {len(pcm_bytes)} bytes (~{len(pcm_bytes)//(SAMPLE_RATE*2):.1f}s)") # Step 3 — join room room = rtc.Room() received_frames: list[bytes] = [] @room.on("track_subscribed") def on_track(track, pub, participant): if track.kind == rtc.TrackKind.KIND_AUDIO and participant.identity == "pipecat-bot": print(f"[test] Subscribed to bot audio track") audio_stream = rtc.AudioStream(track, sample_rate=SAMPLE_RATE, num_channels=NUM_CHANNELS) asyncio.ensure_future(_collect_audio(audio_stream, received_frames)) ws_url = creds["url"].replace("https://", "wss://").replace("http://", "ws://") # Connect internally via host.docker.internal internal_url = "ws://host.docker.internal:7880" print(f"[test] Connecting to LiveKit at {internal_url}...") await room.connect(internal_url, creds["token"]) print(f"[test] Connected. Waiting for bot to join...") # Wait for bot participant for _ in range(20): if any(p.identity == "pipecat-bot" for p in room.remote_participants.values()): break await asyncio.sleep(0.5) print(f"[test] Participants: {[p.identity for p in room.remote_participants.values()]}") # Step 4 — publish audio as microphone print("[test] Publishing audio track...") source = rtc.AudioSource(SAMPLE_RATE, NUM_CHANNELS) local_track = rtc.LocalAudioTrack.create_audio_track("microphone", source) opts = rtc.TrackPublishOptions(source=rtc.TrackSource.SOURCE_MICROPHONE) await room.local_participant.publish_track(local_track, opts) # Send PCM in 20ms chunks chunk_samples = SAMPLE_RATE * 20 // 1000 # 960 samples per chunk chunk_bytes = chunk_samples * 2 # int16 print(f"[test] Sending {len(pcm_bytes) // chunk_bytes} audio chunks...") for i in range(0, len(pcm_bytes), chunk_bytes): chunk = pcm_bytes[i:i + chunk_bytes] if len(chunk) < chunk_bytes: chunk = chunk + b'\x00' * (chunk_bytes - len(chunk)) samples = np.frombuffer(chunk, dtype=np.int16) frame = rtc.AudioFrame( data=samples.tobytes(), sample_rate=SAMPLE_RATE, num_channels=NUM_CHANNELS, samples_per_channel=chunk_samples, ) await source.capture_frame(frame) await asyncio.sleep(0.02) print("[test] Audio sent. Waiting for bot response (up to 30s)...") await asyncio.sleep(30) await room.disconnect() # Step 5 — save response if received_frames: total = b"".join(received_frames) print(f"[test] Received {len(total)} bytes of bot audio ({len(total)//(SAMPLE_RATE*2):.1f}s)") with wave.open(OUTPUT_FILE, "wb") as wf: wf.setnchannels(NUM_CHANNELS) wf.setsampwidth(2) wf.setframerate(SAMPLE_RATE) wf.writeframes(total) print(f"[test] Saved to {OUTPUT_FILE}") else: print("[test] No audio received from bot!") async def _collect_audio(stream: rtc.AudioStream, buf: list): async for event in stream: buf.append(bytes(event.frame.data)) asyncio.run(main())