Skip to content

Commit e7b9ac4

Browse files
committed
feat: stream audio response
1 parent 34e6742 commit e7b9ac4

File tree

2 files changed

+79
-26
lines changed

2 files changed

+79
-26
lines changed

app/api/tts/route.ts

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,70 @@
1-
import { getLatestVoice } from "@/lib/db/queries"
2-
import { auth } from "@/app/(auth)/auth"
3-
import { NextResponse } from "next/server"
1+
import { getLatestVoice } from "@/lib/db/queries";
2+
import { auth } from "@/app/(auth)/auth";
3+
import { NextResponse } from "next/server";
4+
const https = require("https");
45

56
export async function POST(request: Request) {
67
try {
7-
const { text, voice } = await request.json()
8+
const { text, voice } = await request.json();
89
const session = await auth();
910
const user_id = session?.user?.id ?? "";
10-
const latestVoice = await getLatestVoice(user_id)
11+
const latestVoice = await getLatestVoice(user_id);
1112

12-
const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${latestVoice?.voice_id ?? voice}`, {
13+
const options = {
14+
hostname: "api.elevenlabs.io",
15+
path: `/v1/text-to-speech/${latestVoice?.voice_id ?? voice}/stream`,
1316
method: "POST",
1417
headers: {
1518
"Content-Type": "application/json",
1619
"xi-api-key": process.env.ELEVENLABS_API_KEY!,
1720
},
18-
body: JSON.stringify({
19-
text,
20-
model_id: "eleven_monolingual_v1",
21-
voice_settings: {
22-
stability: 0.5,
23-
similarity_boost: 0.5,
24-
},
25-
}),
26-
})
21+
};
2722

28-
console.log("TTS response", response)
23+
const requestBody = JSON.stringify({
24+
text,
25+
model_id: "eleven_monolingual_v1",
26+
voice_settings: {
27+
stability: 0.5,
28+
similarity_boost: 0.5,
29+
},
30+
});
31+
32+
const stream = new ReadableStream({
33+
async start(controller) {
34+
const req = https.request(options, (res: any) => {
35+
if (res.statusCode !== 200) {
36+
controller.error(new Error("Failed to generate speech"));
37+
return;
38+
}
39+
40+
res.on("data", (chunk: any) => {
41+
controller.enqueue(chunk);
42+
});
2943

30-
if (!response.ok) {
31-
throw new Error("Failed to generate speech")
32-
}
44+
res.on("end", () => {
45+
controller.close();
46+
});
47+
});
3348

34-
const audioBuffer = await response.arrayBuffer()
49+
req.on("error", (e: any) => {
50+
controller.error(e);
51+
});
52+
53+
req.write(requestBody);
54+
req.end();
55+
},
56+
});
3557

36-
return new NextResponse(audioBuffer, {
58+
return new NextResponse(stream, {
3759
headers: {
3860
"Content-Type": "audio/mpeg",
3961
},
40-
})
62+
});
4163
} catch (error) {
42-
console.error("Error:", error)
43-
return NextResponse.json({ error: "Failed to generate speech" }, { status: 500 })
64+
console.error("Error:", error);
65+
return NextResponse.json(
66+
{ error: "Failed to generate speech" },
67+
{ status: 500 }
68+
);
4469
}
45-
}
70+
}

components/chat.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,29 @@ export function Chat({
120120
return;
121121
}
122122

123-
const audioBlob = await response.blob();
123+
const reader: any = response.body?.getReader();
124+
if (!reader) {
125+
toast.error("Failed to read audio stream");
126+
return;
127+
}
128+
129+
const stream = new ReadableStream({
130+
start(controller) {
131+
function push() {
132+
reader.read().then(({ done, value }: any) => {
133+
if (done) {
134+
controller.close();
135+
return;
136+
}
137+
controller.enqueue(value);
138+
push();
139+
});
140+
}
141+
push();
142+
},
143+
});
144+
145+
const audioBlob = await new Response(stream).blob();
124146
const url = URL.createObjectURL(audioBlob);
125147
setAudioUrl(url);
126148

@@ -132,6 +154,12 @@ export function Chat({
132154
toast.error("Failed to play audio");
133155
});
134156
}
157+
158+
if (!response.ok) {
159+
console.error(response);
160+
toast.error("Failed to generate speech");
161+
return;
162+
}
135163
},
136164
onError: (error) => {
137165
console.log(error);

0 commit comments

Comments
 (0)