Skip to content

Commit 01adb00

Browse files
committed
feat: Implement direct streaming TTS playback using HTMLAudioElement and update the Netlify function to support GET requests for audio parameters.
1 parent 2e79c67 commit 01adb00

File tree

1 file changed

+8
-88
lines changed

1 file changed

+8
-88
lines changed

src/services/ttsService.ts

Lines changed: 8 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -185,52 +185,17 @@ class TTSService {
185185
}
186186

187187
/**
188-
* Stop any currently playing audio and cancel queued sentences
188+
* Stop any currently playing audio and cancel queued playback
189189
*/
190190
stop() {
191191
this.cancelled = true;
192192
this.stopAudio();
193193
}
194194

195195
/**
196-
* Split text into speakable sentence chunks.
197-
* Splits on sentence-ending punctuation while keeping the punctuation attached.
198-
*/
199-
private splitIntoSentences(text: string): string[] {
200-
// Split on sentence boundaries (.!?) followed by space or end of string
201-
// Keep short fragments together to avoid tiny TTS calls
202-
const raw = text.match(/[^.!?]+[.!?]+[\s]*/g) || [text];
203-
const sentences: string[] = [];
204-
let buffer = '';
205-
206-
for (const chunk of raw) {
207-
buffer += chunk;
208-
// Only split if the buffer is long enough (>20 chars) to avoid tiny fragments
209-
if (buffer.trim().length >= 20) {
210-
sentences.push(buffer.trim());
211-
buffer = '';
212-
}
213-
}
214-
// Push any remaining buffer
215-
if (buffer.trim()) {
216-
if (sentences.length > 0) {
217-
// Attach short trailing fragment to last sentence
218-
sentences[sentences.length - 1] += ' ' + buffer.trim();
219-
} else {
220-
sentences.push(buffer.trim());
221-
}
222-
}
223-
224-
return sentences.filter(s => s.length > 0);
225-
}
226-
227-
/**
228-
* Speak text using sentence-level streaming for low latency.
229-
* Splits the text into sentences, speaks the first one immediately,
230-
* and queues the rest to play sequentially.
231-
*
232-
* onStart fires when the first sentence starts playing.
233-
* onEnd fires when the last sentence finishes.
196+
* Speak text using native streaming for lowest latency.
197+
* Passes the full text natively to the underlying TTS provider's streaming API
198+
* (e.g. ElevenLabs chunked REST API).
234199
*/
235200
async speakStreaming(
236201
text: string,
@@ -245,60 +210,15 @@ class TTSService {
245210
onError?: (error: Error) => void;
246211
}
247212
): Promise<void> {
248-
const sentences = this.splitIntoSentences(text);
249-
250-
if (sentences.length === 0) {
213+
if (!text || text.trim().length === 0) {
251214
options?.onEnd?.();
252-
return;
215+
return Promise.resolve();
253216
}
254217

255-
// If only one sentence, just use normal speak (no overhead)
256-
if (sentences.length === 1) {
257-
return this.speak(text, options);
258-
}
259-
260-
// Reset cancellation
261218
this.cancelled = false;
262219

263-
log.debug(`📝 Sentence streaming: ${sentences.length} chunks`, sentences.map(s => s.substring(0, 40) + '...'));
264-
265-
let started = false;
266-
267-
for (let i = 0; i < sentences.length; i++) {
268-
if (this.cancelled) {
269-
log.debug('⏹️ Sentence streaming cancelled');
270-
break;
271-
}
272-
273-
const isFirst = i === 0;
274-
const isLast = i === sentences.length - 1;
275-
const sentence = sentences[i];
276-
277-
try {
278-
await this.speak(sentence, {
279-
...options,
280-
onStart: () => {
281-
if (!started) {
282-
started = true;
283-
options?.onStart?.();
284-
}
285-
},
286-
onEnd: isLast ? options?.onEnd : undefined,
287-
onError: undefined, // Handle errors below
288-
});
289-
} catch (error) {
290-
log.error(`❌ Sentence ${i + 1}/${sentences.length} failed:`, error);
291-
// Skip this sentence and continue with the next
292-
if (isLast) {
293-
options?.onEnd?.();
294-
}
295-
}
296-
}
297-
298-
// If cancelled before any sentence played, still fire onEnd
299-
if (this.cancelled && !started) {
300-
options?.onEnd?.();
301-
}
220+
// Directly use native TTS provider streaming instead of custom sentence splitting
221+
return this.speak(text, options);
302222
}
303223

304224
/**

0 commit comments

Comments
 (0)