fix(gateway-chat): chunk by quoted section, not sentence (prosody)
Per-sentence chunking generated each sentence cold, flattening intonation/prosody that spans the whole quoted line. Chunk by QUOTED SECTION instead — each contiguous quote is generated whole (max_tokens 2400) so its prosody stays intact; multiple quotes in a reply still play serially on the shared clock. extractQuotes already returns exactly these spans; dropped splitSentences.
This commit is contained in:
@@ -160,16 +160,14 @@ function extractQuotes(text){
|
|||||||
while ((m = re.exec(text)) !== null){ const q = (m[1] || m[2] || '').trim(); if (q) out.push(q); }
|
while ((m = re.exec(text)) !== null){ const q = (m[1] || m[2] || '').trim(); if (q) out.push(q); }
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
function splitSentences(t){
|
// stream ONE quoted section (whole, so intonation/prosody across it is preserved) and queue its
|
||||||
return (t.match(/[^.!?…]+[.!?…]+["”’')\]]*\s*|[^.!?…]+$/g) || [t]).map(s => s.trim()).filter(Boolean);
|
// PCM onto the shared speechHead clock; resolves when its audio is fully received (it plays on
|
||||||
}
|
// while the NEXT quote starts generating). Chunk by QUOTE, not sentence — per-sentence lost prosody.
|
||||||
// stream ONE sentence and queue its PCM onto the shared speechHead clock; resolves when the
|
|
||||||
// sentence's audio is fully received (it plays on while the NEXT sentence starts generating).
|
|
||||||
async function streamOne(sentence, mine){
|
async function streamOne(sentence, mine){
|
||||||
const url = $('ttsUrl').value.trim().replace(/\/+$/,'').replace(/\/tts$/,'') + '/tts/stream';
|
const url = $('ttsUrl').value.trim().replace(/\/+$/,'').replace(/\/tts$/,'') + '/tts/stream';
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method:'POST', headers:{ 'Content-Type':'application/json' },
|
method:'POST', headers:{ 'Content-Type':'application/json' },
|
||||||
body: JSON.stringify({ text: sentence, voice: ($('ttsVoice').value.trim() || 'baddy'), max_tokens: 900 })
|
body: JSON.stringify({ text: sentence, voice: ($('ttsVoice').value.trim() || 'baddy'), max_tokens: 2400 })
|
||||||
});
|
});
|
||||||
if (!res.ok || !res.body) return false;
|
if (!res.ok || !res.body) return false;
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
@@ -195,14 +193,14 @@ async function speakQuotes(text, msg){
|
|||||||
if (!$('ttsOn').checked) return;
|
if (!$('ttsOn').checked) return;
|
||||||
const quotes = extractQuotes(text);
|
const quotes = extractQuotes(text);
|
||||||
if (!quotes.length) return;
|
if (!quotes.length) return;
|
||||||
const sentences = splitSentences(quotes.join(' ')); // pre-chunk quoted text by sentence
|
const chunks = quotes; // one chunk per QUOTED SECTION (keeps prosody)
|
||||||
const tag = document.createElement('span'); tag.textContent = ' 🔊';
|
const tag = document.createElement('span'); tag.textContent = ' 🔊';
|
||||||
tag.style.cursor = 'pointer'; tag.title = 'mOrpheus — ' + sentences.length + ' sentence(s), click to replay';
|
tag.style.cursor = 'pointer'; tag.title = 'mOrpheus — ' + chunks.length + ' quote(s), click to replay';
|
||||||
const play = async () => {
|
const play = async () => {
|
||||||
const mine = ++ttsGen; // supersede any in-flight playback
|
const mine = ++ttsGen; // supersede any in-flight playback
|
||||||
primeAudio(); if (audioCtx.state === 'suspended') await audioCtx.resume();
|
primeAudio(); if (audioCtx.state === 'suspended') await audioCtx.resume();
|
||||||
speechHead = audioCtx.currentTime + 0.12; // reset the shared clock for this reply
|
speechHead = audioCtx.currentTime + 0.12; // reset the shared clock for this reply
|
||||||
for (const s of sentences){ // play sentences serially, in order
|
for (const s of chunks){ // play quoted sections serially, in order
|
||||||
if (mine !== ttsGen) return;
|
if (mine !== ttsGen) return;
|
||||||
if (!(await streamOne(s, mine))){ tag.textContent = ' 🔇'; return; }
|
if (!(await streamOne(s, mine))){ tag.textContent = ' 🔇'; return; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,16 +160,14 @@ function extractQuotes(text){
|
|||||||
while ((m = re.exec(text)) !== null){ const q = (m[1] || m[2] || '').trim(); if (q) out.push(q); }
|
while ((m = re.exec(text)) !== null){ const q = (m[1] || m[2] || '').trim(); if (q) out.push(q); }
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
function splitSentences(t){
|
// stream ONE quoted section (whole, so intonation/prosody across it is preserved) and queue its
|
||||||
return (t.match(/[^.!?…]+[.!?…]+["”’')\]]*\s*|[^.!?…]+$/g) || [t]).map(s => s.trim()).filter(Boolean);
|
// PCM onto the shared speechHead clock; resolves when its audio is fully received (it plays on
|
||||||
}
|
// while the NEXT quote starts generating). Chunk by QUOTE, not sentence — per-sentence lost prosody.
|
||||||
// stream ONE sentence and queue its PCM onto the shared speechHead clock; resolves when the
|
|
||||||
// sentence's audio is fully received (it plays on while the NEXT sentence starts generating).
|
|
||||||
async function streamOne(sentence, mine){
|
async function streamOne(sentence, mine){
|
||||||
const url = $('ttsUrl').value.trim().replace(/\/+$/,'').replace(/\/tts$/,'') + '/tts/stream';
|
const url = $('ttsUrl').value.trim().replace(/\/+$/,'').replace(/\/tts$/,'') + '/tts/stream';
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method:'POST', headers:{ 'Content-Type':'application/json' },
|
method:'POST', headers:{ 'Content-Type':'application/json' },
|
||||||
body: JSON.stringify({ text: sentence, voice: ($('ttsVoice').value.trim() || 'baddy'), max_tokens: 900 })
|
body: JSON.stringify({ text: sentence, voice: ($('ttsVoice').value.trim() || 'baddy'), max_tokens: 2400 })
|
||||||
});
|
});
|
||||||
if (!res.ok || !res.body) return false;
|
if (!res.ok || !res.body) return false;
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
@@ -195,14 +193,14 @@ async function speakQuotes(text, msg){
|
|||||||
if (!$('ttsOn').checked) return;
|
if (!$('ttsOn').checked) return;
|
||||||
const quotes = extractQuotes(text);
|
const quotes = extractQuotes(text);
|
||||||
if (!quotes.length) return;
|
if (!quotes.length) return;
|
||||||
const sentences = splitSentences(quotes.join(' ')); // pre-chunk quoted text by sentence
|
const chunks = quotes; // one chunk per QUOTED SECTION (keeps prosody)
|
||||||
const tag = document.createElement('span'); tag.textContent = ' 🔊';
|
const tag = document.createElement('span'); tag.textContent = ' 🔊';
|
||||||
tag.style.cursor = 'pointer'; tag.title = 'mOrpheus — ' + sentences.length + ' sentence(s), click to replay';
|
tag.style.cursor = 'pointer'; tag.title = 'mOrpheus — ' + chunks.length + ' quote(s), click to replay';
|
||||||
const play = async () => {
|
const play = async () => {
|
||||||
const mine = ++ttsGen; // supersede any in-flight playback
|
const mine = ++ttsGen; // supersede any in-flight playback
|
||||||
primeAudio(); if (audioCtx.state === 'suspended') await audioCtx.resume();
|
primeAudio(); if (audioCtx.state === 'suspended') await audioCtx.resume();
|
||||||
speechHead = audioCtx.currentTime + 0.12; // reset the shared clock for this reply
|
speechHead = audioCtx.currentTime + 0.12; // reset the shared clock for this reply
|
||||||
for (const s of sentences){ // play sentences serially, in order
|
for (const s of chunks){ // play quoted sections serially, in order
|
||||||
if (mine !== ttsGen) return;
|
if (mine !== ttsGen) return;
|
||||||
if (!(await streamOne(s, mine))){ tag.textContent = ' 🔇'; return; }
|
if (!(await streamOne(s, mine))){ tag.textContent = ' 🔇'; return; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user