tts: punctuate unbounded chunks, strip URLs and paths, log chunk text

This commit is contained in:
russell@unturf.com 2026-05-24 10:20:52 -04:00
parent 29c400bdb4
commit 075db9154a
No known key found for this signature in database

View file

@ -73,28 +73,49 @@ const ttsCache = new Map();
const TTS_CHUNK_MAX = 2500; // upper bound per chunk (server-side comfortable)
const TTS_FIRST_CHUNK_MAX = 500; // smaller first chunk for low time-to-first-audio
// Strip patterns F5 would pronounce character-by-character (URLs, paths, emails).
// F5 has no spell-vs-speak heuristic — feeding it https://foo.bar produces audible
// garbage that users hear as "random chars trying to be pronounced".
function stripUnspeakable(text) {
return text
.replace(/https?:\/\/\S+/gi, "")
.replace(/\b[\w-]+@[\w.-]+\.\w+\b/g, "")
.replace(/\/[\w./_-]{4,}/g, "")
.replace(/[A-Z]:\\[\w\\._-]+/g, "")
.replace(/[ \t]+/g, " ")
.trim();
}
// Split one chunk off the head of `remaining`. Always ends the chunk on
// sentence punctuation when possible — F5-TTS hallucinates noise at the tail
// of unpunctuated fragments since it doesn't know where to stop. If no
// boundary exists in `limit` chars, split on a space and append a period so
// F5 has a clean stop signal.
function splitOne(remaining, limit) {
if (remaining.length <= limit) return [remaining, ''];
const slice = remaining.substring(0, limit);
let splitAt = -1;
for (let i = slice.length - 1; i > limit * 0.3; i--) {
for (let i = slice.length - 1; i > 0; i--) {
const ch = slice[i];
if ((ch === '.' || ch === '!' || ch === '?' || ch === '\n') &&
(i === slice.length - 1 || slice[i + 1] === ' ' || slice[i + 1] === '\n')) {
splitAt = i + 1;
break;
const splitAt = i + 1;
return [remaining.substring(0, splitAt).trim(), remaining.substring(splitAt).trim()];
}
}
if (splitAt === -1) splitAt = slice.lastIndexOf(' ');
let splitAt = slice.lastIndexOf(' ');
if (splitAt <= 0) splitAt = limit;
return [remaining.substring(0, splitAt).trim(), remaining.substring(splitAt).trim()];
const head = remaining.substring(0, splitAt).trim();
const tail = remaining.substring(splitAt).trim();
const punctuatedHead = /[.!?]$/.test(head) ? head : head + ".";
return [punctuatedHead, tail];
}
function splitTextIntoChunks(text) {
if (text.length <= TTS_FIRST_CHUNK_MAX) return [text];
const cleaned = stripUnspeakable(text);
if (cleaned.length <= TTS_FIRST_CHUNK_MAX) return [cleaned];
const chunks = [];
let remaining = text;
let remaining = cleaned;
let isFirst = true;
while (remaining.length > 0) {
const limit = isFirst ? TTS_FIRST_CHUNK_MAX : TTS_CHUNK_MAX;
@ -105,6 +126,8 @@ function splitTextIntoChunks(text) {
}
console.log(`TTS: split ${text.length} chars into ${chunks.length} chunks:`, chunks.map((c, i) => `chunk${i + 1}=${c.length}chars`));
// Diagnostic: log full text of each chunk so we can spot garbage being sent to F5.
chunks.forEach((c, i) => console.log(`TTS chunk ${i + 1} text:`, JSON.stringify(c)));
return chunks;
}