Skip to content

Commit 726901c

Browse files
authored
feat(voice): speech to speech mode for deployed chat (#467)
* finished barebones, optimized speech to speech chat deploy * better visualization, interruption still not good * fixed some turn detection, still not ideal. have to press mute + unmute to process successive queries * improvements * removed MediaSource in favor of blob, simplified echo cancellation and overall logic * simplified * cleanups * ack PR comments
1 parent 49a8a05 commit 726901c

File tree

14 files changed

+1946
-1106
lines changed

14 files changed

+1946
-1106
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type { NextRequest } from 'next/server'
2+
import { env } from '@/lib/env'
3+
import { createLogger } from '@/lib/logs/console-logger'
4+
5+
const logger = createLogger('ProxyTTSStreamAPI')
6+
7+
export async function POST(request: NextRequest) {
8+
try {
9+
const body = await request.json()
10+
const { text, voiceId, modelId = 'eleven_turbo_v2_5' } = body
11+
12+
if (!text || !voiceId) {
13+
return new Response('Missing required parameters', { status: 400 })
14+
}
15+
16+
const apiKey = env.ELEVENLABS_API_KEY
17+
if (!apiKey) {
18+
logger.error('ELEVENLABS_API_KEY not configured on server')
19+
return new Response('ElevenLabs service not configured', { status: 503 })
20+
}
21+
22+
const endpoint = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}/stream`
23+
24+
const response = await fetch(endpoint, {
25+
method: 'POST',
26+
headers: {
27+
Accept: 'audio/mpeg',
28+
'Content-Type': 'application/json',
29+
'xi-api-key': apiKey,
30+
},
31+
body: JSON.stringify({
32+
text,
33+
model_id: modelId,
34+
// Maximum performance settings
35+
optimize_streaming_latency: 4,
36+
output_format: 'mp3_22050_32', // Fastest format
37+
voice_settings: {
38+
stability: 0.5,
39+
similarity_boost: 0.8,
40+
style: 0.0,
41+
use_speaker_boost: false,
42+
},
43+
enable_ssml_parsing: false,
44+
apply_text_normalization: 'off',
45+
// Use auto mode for fastest possible streaming
46+
// Note: This may sacrifice some quality for speed
47+
use_pvc_as_ivc: false, // Use fastest voice processing
48+
}),
49+
})
50+
51+
if (!response.ok) {
52+
logger.error(`Failed to generate Stream TTS: ${response.status} ${response.statusText}`)
53+
return new Response(`Failed to generate TTS: ${response.status} ${response.statusText}`, {
54+
status: response.status,
55+
})
56+
}
57+
58+
if (!response.body) {
59+
logger.error('No response body received from ElevenLabs')
60+
return new Response('No audio stream received', { status: 422 })
61+
}
62+
63+
// Create optimized streaming response
64+
const { readable, writable } = new TransformStream({
65+
transform(chunk, controller) {
66+
// Pass through chunks immediately without buffering
67+
controller.enqueue(chunk)
68+
},
69+
flush(controller) {
70+
// Ensure all data is flushed immediately
71+
controller.terminate()
72+
},
73+
})
74+
75+
const writer = writable.getWriter()
76+
const reader = response.body.getReader()
77+
78+
;(async () => {
79+
try {
80+
while (true) {
81+
const { done, value } = await reader.read()
82+
if (done) {
83+
await writer.close()
84+
break
85+
}
86+
// Write immediately without waiting
87+
writer.write(value).catch(logger.error)
88+
}
89+
} catch (error) {
90+
logger.error('Error during Stream streaming:', error)
91+
await writer.abort(error)
92+
}
93+
})()
94+
95+
return new Response(readable, {
96+
headers: {
97+
'Content-Type': 'audio/mpeg',
98+
'Transfer-Encoding': 'chunked',
99+
'Cache-Control': 'no-cache, no-store, must-revalidate',
100+
Pragma: 'no-cache',
101+
Expires: '0',
102+
'X-Content-Type-Options': 'nosniff',
103+
'Access-Control-Allow-Origin': '*',
104+
Connection: 'keep-alive',
105+
// Stream headers for better streaming
106+
'X-Accel-Buffering': 'no', // Disable nginx buffering
107+
'X-Stream-Type': 'real-time',
108+
},
109+
})
110+
} catch (error) {
111+
logger.error('Error in Stream TTS:', error)
112+
113+
return new Response(
114+
`Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
115+
{ status: 500 }
116+
)
117+
}
118+
}

0 commit comments

Comments
 (0)