openai: add AI stack (litellm + langfuse + pipecat + silero-tts) and oO aliases
- LiteLLM proxy with langfuse callbacks, postgres backends, and OpenRouter fallbacks. - Langfuse observability UI. - Pipecat voice pipeline (LiveKit + STT + TTS + LLM) and Silero TTS build contexts. - Ollama tuned for GPU (OLLAMA_NUM_GPU=999, mem_limit=4g, max 2 loaded models). - open-webui wired to litellm + faster-whisper + silero for voice. - litellm-config.yaml publishes oO's model aliases (tip-generator, embedder, judge) pointing at the host ollama on :11434 so ml/serving can call them via LiteLLM. .env skipped (secrets). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
122
openai/pipecat/test_pipeline.py
Normal file
122
openai/pipecat/test_pipeline.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
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())
|
||||
Reference in New Issue
Block a user