fix(gateway-chat): honor UI endpoint/model/voice for TTS + standard-audio playback
The TTS path was hardwired to the parked zonos-gateway: it force-reverted the endpoint field back to zonos :8890 on load, hardcoded model=ext-tts, and decoded the response as Zonos-specific raw float32 PCM @ 44.1kHz. Result: quoted-text TTS was dead once zonos was parked, and pointing the field elsewhere silently failed. - Honor the interface: set endpoint/model/voice defaults only when a field is empty; never rewrite a user-typed value (removed the zonos auto-revert regex). - Add a TTS model field (ttsModel); send the UI's model instead of hardcoding. - Playback: request standard OpenAI /v1/audio/speech mp3 and decode via audioCtx.decodeAudioData (handles wav/mp3/ogg/flac from any endpoint). - Defaults: endpoint = LiteLLM ext-tts alias (fleet TTS gateway), voice = nova.
This commit is contained in:
@@ -67,11 +67,12 @@
|
|||||||
<div><label>Temperature</label><input id="temp" type="number" step="0.05" value="1"></div>
|
<div><label>Temperature</label><input id="temp" type="number" step="0.05" value="1"></div>
|
||||||
<div><label>Max tokens</label><input id="max" type="number" step="1" value="1024"></div>
|
<div><label>Max tokens</label><input id="max" type="number" step="1" value="1024"></div>
|
||||||
</div>
|
</div>
|
||||||
<label style="margin-top:.5rem">🔊 mOrpheus voice (quoted text)</label>
|
<label style="margin-top:.5rem">🔊 TTS (quoted text)</label>
|
||||||
<label class="hint" style="display:flex;gap:.45rem;align-items:center;margin:.15rem 0"><input id="ttsOn" type="checkbox" style="width:auto"> Auto-play quoted dialogue</label>
|
<label class="hint" style="display:flex;gap:.45rem;align-items:center;margin:.15rem 0"><input id="ttsOn" type="checkbox" style="width:auto"> Auto-play quoted dialogue</label>
|
||||||
<input id="ttsUrl" placeholder="http://10.100.79.3:8299/tts">
|
<input id="ttsUrl" placeholder="endpoint — http://10.250.50.70:4000/v1/audio/speech">
|
||||||
<input id="ttsVoice" placeholder="baddy">
|
<input id="ttsModel" placeholder="model — ext-tts">
|
||||||
<div class="hint">Settings persist locally. No <code>tools</code> field is ever sent. Reasoning models show their <code>reasoning_content</code> above the answer. With 🔊 on, quoted text in each reply auto-plays via mOrpheus (irv-ml1, internal).</div>
|
<input id="ttsVoice" placeholder="voice — nova">
|
||||||
|
<div class="hint">Settings persist locally. No <code>tools</code> field is ever sent. Reasoning models show their <code>reasoning_content</code> above the answer. With 🔊 on, quoted text auto-plays via the <b>endpoint + model + voice set above</b> (OpenAI <code>/v1/audio/speech</code>, mp3).</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main id="main">
|
<main id="main">
|
||||||
<div id="log"></div>
|
<div id="log"></div>
|
||||||
@@ -85,11 +86,15 @@
|
|||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const cfg = ['base','key','model','sys','temp','max','ttsUrl','ttsVoice'];
|
const cfg = ['base','key','model','sys','temp','max','ttsUrl','ttsModel','ttsVoice'];
|
||||||
cfg.forEach(k => { const v = localStorage.getItem('gc_'+k); if (v !== null) $(k).value = v; });
|
cfg.forEach(k => { const v = localStorage.getItem('gc_'+k); if (v !== null) $(k).value = v; });
|
||||||
if (!$('base').value) $('base').value = 'http://10.250.50.70:4000/v1';
|
if (!$('base').value) $('base').value = 'http://10.250.50.70:4000/v1';
|
||||||
if (!$('ttsUrl').value) $('ttsUrl').value = 'http://10.100.79.3:8299/tts';
|
// TTS: honor whatever endpoint/model/voice the interface holds. Set a default ONLY when a field is
|
||||||
if (!$('ttsVoice').value) $('ttsVoice').value = 'baddy';
|
// empty — never rewrite a value the user typed. Defaults: endpoint = the LiteLLM ext-tts alias (which
|
||||||
|
// fronts the fleet TTS gateway), model = ext-tts, voice = nova (an OpenAI voice name the gateway aliases).
|
||||||
|
if (!$('ttsUrl').value) { $('ttsUrl').value = 'http://10.250.50.70:4000/v1/audio/speech'; localStorage.setItem('gc_ttsUrl', $('ttsUrl').value); }
|
||||||
|
if (!$('ttsModel').value) { $('ttsModel').value = 'ext-tts'; localStorage.setItem('gc_ttsModel', $('ttsModel').value); }
|
||||||
|
if (!$('ttsVoice').value) { $('ttsVoice').value = 'nova'; localStorage.setItem('gc_ttsVoice', $('ttsVoice').value); }
|
||||||
cfg.forEach(k => $(k).addEventListener('input', e => localStorage.setItem('gc_'+k, e.target.value)));
|
cfg.forEach(k => $(k).addEventListener('input', e => localStorage.setItem('gc_'+k, e.target.value)));
|
||||||
// 🔊 auto-play toggle is a checkbox (persist .checked)
|
// 🔊 auto-play toggle is a checkbox (persist .checked)
|
||||||
$('ttsOn').checked = localStorage.getItem('gc_ttsOn') === '1';
|
$('ttsOn').checked = localStorage.getItem('gc_ttsOn') === '1';
|
||||||
@@ -144,7 +149,7 @@ function addMsg(who, cls){
|
|||||||
}
|
}
|
||||||
function showErr(text){ const m = addMsg('error','err'); m.body.className = 'err'; m.body.textContent = text; }
|
function showErr(text){ const m = addMsg('error','err'); m.body.className = 'err'; m.body.textContent = text; }
|
||||||
|
|
||||||
// --- mOrpheus TTS: streaming-decode auto-voice of quoted text (Web Audio, low TTFA) ---
|
// --- TTS: auto-voice quoted text via the configured OpenAI /v1/audio/speech endpoint (Web Audio) ---
|
||||||
let audioCtx = null; // shared AudioContext
|
let audioCtx = null; // shared AudioContext
|
||||||
let ttsGen = 0; // generation counter — a newer reply's stream supersedes older ones
|
let ttsGen = 0; // generation counter — a newer reply's stream supersedes older ones
|
||||||
let speechHead = 0; // shared playback clock — sentences queue onto it back-to-back
|
let speechHead = 0; // shared playback clock — sentences queue onto it back-to-back
|
||||||
@@ -160,50 +165,47 @@ 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;
|
||||||
}
|
}
|
||||||
// stream ONE quoted section (whole, so intonation/prosody across it is preserved) and queue its
|
// Speak ONE quoted section (whole, so prosody across it is preserved). Honors the endpoint, model and
|
||||||
// PCM onto the shared speechHead clock; resolves when its audio is fully received (it plays on
|
// voice set in the interface; requests standard OpenAI /v1/audio/speech mp3 and plays it via Web Audio.
|
||||||
// while the NEXT quote starts generating). Chunk by QUOTE, not sentence — per-sentence lost prosody.
|
// decodeAudioData handles wav/mp3/ogg/flac, so this works with whatever the configured endpoint returns.
|
||||||
async function streamOne(sentence, mine){
|
async function speakOne(sentence, mine){
|
||||||
const url = $('ttsUrl').value.trim().replace(/\/+$/,'').replace(/\/tts$/,'') + '/tts/stream';
|
const url = $('ttsUrl').value.trim().replace(/\/+$/,'');
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method:'POST', headers:{ 'Content-Type':'application/json' },
|
method:'POST',
|
||||||
body: JSON.stringify({ text: sentence, voice: ($('ttsVoice').value.trim() || 'baddy'), max_tokens: 2400 })
|
headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + $('key').value },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: ($('ttsModel').value.trim() || 'ext-tts'),
|
||||||
|
input: sentence,
|
||||||
|
voice: ($('ttsVoice').value.trim() || 'nova'),
|
||||||
|
response_format: 'mp3'
|
||||||
|
})
|
||||||
});
|
});
|
||||||
if (!res.ok || !res.body) return false;
|
if (!res.ok) return false;
|
||||||
const reader = res.body.getReader();
|
const raw = await res.arrayBuffer();
|
||||||
let leftover = new Uint8Array(0);
|
if (mine !== ttsGen) return true; // a newer reply superseded this one
|
||||||
for(;;){
|
let abuf;
|
||||||
const { done, value } = await reader.read();
|
try { abuf = await audioCtx.decodeAudioData(raw); }
|
||||||
if (done || mine !== ttsGen) break; // stop if a newer reply took over
|
catch (e){ return false; } // non-audio body (e.g. error JSON) or unsupported codec
|
||||||
let bytes = new Uint8Array(leftover.length + value.length);
|
if (mine !== ttsGen) return true;
|
||||||
bytes.set(leftover); bytes.set(value, leftover.length);
|
const src = audioCtx.createBufferSource(); src.buffer = abuf; src.connect(audioCtx.destination);
|
||||||
const n = bytes.length - (bytes.length % 2); // whole int16 samples only
|
if (speechHead < audioCtx.currentTime + 0.05) speechHead = audioCtx.currentTime + 0.05;
|
||||||
leftover = bytes.slice(n);
|
src.start(speechHead); speechHead += abuf.duration; // queue back-to-back on the shared clock
|
||||||
if (!n) continue;
|
|
||||||
const dv = new DataView(bytes.buffer, 0, n), N = n / 2, f32 = new Float32Array(N);
|
|
||||||
for (let i = 0; i < N; i++) f32[i] = dv.getInt16(i * 2, true) / 32768;
|
|
||||||
const abuf = audioCtx.createBuffer(1, N, 24000); abuf.getChannelData(0).set(f32);
|
|
||||||
const src = audioCtx.createBufferSource(); src.buffer = abuf; src.connect(audioCtx.destination);
|
|
||||||
if (speechHead < audioCtx.currentTime + 0.05) speechHead = audioCtx.currentTime + 0.05; // underrun catch-up
|
|
||||||
src.start(speechHead); speechHead += abuf.duration; // queue back-to-back on the shared clock
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async function speakQuotes(text, msg){
|
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 chunks = quotes; // one chunk per QUOTED SECTION (keeps prosody)
|
// Send ALL quoted dialogue as ONE request to the configured endpoint (one clip = consistent prosody).
|
||||||
|
const joined = quotes.join(' ');
|
||||||
const tag = document.createElement('span'); tag.textContent = ' 🔊';
|
const tag = document.createElement('span'); tag.textContent = ' 🔊';
|
||||||
tag.style.cursor = 'pointer'; tag.title = 'mOrpheus — ' + chunks.length + ' quote(s), click to replay';
|
tag.style.cursor = 'pointer'; tag.title = 'ext-tts — ' + quotes.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 chunks){ // play quoted sections serially, in order
|
if (mine !== ttsGen) return;
|
||||||
if (mine !== ttsGen) return;
|
if (!(await speakOne(joined, mine))){ tag.textContent = ' 🔇'; return; }
|
||||||
if (!(await streamOne(s, mine))){ tag.textContent = ' 🔇'; return; }
|
|
||||||
}
|
|
||||||
if (mine === ttsGen) tag.textContent = ' 🔈';
|
if (mine === ttsGen) tag.textContent = ' 🔈';
|
||||||
};
|
};
|
||||||
tag.onclick = () => play().catch(() => tag.textContent = ' 🔇');
|
tag.onclick = () => play().catch(() => tag.textContent = ' 🔇');
|
||||||
|
|||||||
+35
-43
@@ -67,11 +67,12 @@
|
|||||||
<div><label>Temperature</label><input id="temp" type="number" step="0.05" value="1"></div>
|
<div><label>Temperature</label><input id="temp" type="number" step="0.05" value="1"></div>
|
||||||
<div><label>Max tokens</label><input id="max" type="number" step="1" value="1024"></div>
|
<div><label>Max tokens</label><input id="max" type="number" step="1" value="1024"></div>
|
||||||
</div>
|
</div>
|
||||||
<label style="margin-top:.5rem">🔊 ext-tts voice (quoted text)</label>
|
<label style="margin-top:.5rem">🔊 TTS (quoted text)</label>
|
||||||
<label class="hint" style="display:flex;gap:.45rem;align-items:center;margin:.15rem 0"><input id="ttsOn" type="checkbox" style="width:auto"> Auto-play quoted dialogue</label>
|
<label class="hint" style="display:flex;gap:.45rem;align-items:center;margin:.15rem 0"><input id="ttsOn" type="checkbox" style="width:auto"> Auto-play quoted dialogue</label>
|
||||||
<input id="ttsUrl" placeholder="http://10.100.79.3:8890/v1/audio/speech">
|
<input id="ttsUrl" placeholder="endpoint — http://10.250.50.70:4000/v1/audio/speech">
|
||||||
<input id="ttsVoice" placeholder="Cora">
|
<input id="ttsModel" placeholder="model — ext-tts">
|
||||||
<div class="hint">Settings persist locally. No <code>tools</code> field is ever sent. Reasoning models show their <code>reasoning_content</code> above the answer. With 🔊 on, quoted text in each reply auto-plays via <code>ext-tts</code> (Zonos, streamed direct from zonos-gateway).</div>
|
<input id="ttsVoice" placeholder="voice — nova">
|
||||||
|
<div class="hint">Settings persist locally. No <code>tools</code> field is ever sent. Reasoning models show their <code>reasoning_content</code> above the answer. With 🔊 on, quoted text auto-plays via the <b>endpoint + model + voice set above</b> (OpenAI <code>/v1/audio/speech</code>, mp3).</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main id="main">
|
<main id="main">
|
||||||
<div id="log"></div>
|
<div id="log"></div>
|
||||||
@@ -85,17 +86,15 @@
|
|||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const cfg = ['base','key','model','sys','temp','max','ttsUrl','ttsVoice'];
|
const cfg = ['base','key','model','sys','temp','max','ttsUrl','ttsModel','ttsVoice'];
|
||||||
cfg.forEach(k => { const v = localStorage.getItem('gc_'+k); if (v !== null) $(k).value = v; });
|
cfg.forEach(k => { const v = localStorage.getItem('gc_'+k); if (v !== null) $(k).value = v; });
|
||||||
if (!$('base').value) $('base').value = 'http://10.250.50.70:4000/v1';
|
if (!$('base').value) $('base').value = 'http://10.250.50.70:4000/v1';
|
||||||
// ext-tts default = zonos-gateway direct (streams; bypasses LiteLLM audio buffering). Migrate any stale
|
// TTS: honor whatever endpoint/model/voice the interface holds. Set a default ONLY when a field is
|
||||||
// mOrpheus URL (:8299/:8210//tts) OR the LiteLLM URL (:4000) to the direct gateway, persisting so it sticks.
|
// empty — never rewrite a value the user typed. Defaults: endpoint = the LiteLLM ext-tts alias (which
|
||||||
if (!$('ttsUrl').value || /:(8299|8210)|\/tts(\/stream)?$|10\.250\.50\.70:4000/.test($('ttsUrl').value)){
|
// fronts the fleet TTS gateway), model = ext-tts, voice = nova (an OpenAI voice name the gateway aliases).
|
||||||
$('ttsUrl').value = 'http://10.100.79.3:8890/v1/audio/speech'; localStorage.setItem('gc_ttsUrl', $('ttsUrl').value);
|
if (!$('ttsUrl').value) { $('ttsUrl').value = 'http://10.250.50.70:4000/v1/audio/speech'; localStorage.setItem('gc_ttsUrl', $('ttsUrl').value); }
|
||||||
}
|
if (!$('ttsModel').value) { $('ttsModel').value = 'ext-tts'; localStorage.setItem('gc_ttsModel', $('ttsModel').value); }
|
||||||
if (!$('ttsVoice').value || $('ttsVoice').value.trim() === 'baddy'){
|
if (!$('ttsVoice').value) { $('ttsVoice').value = 'nova'; localStorage.setItem('gc_ttsVoice', $('ttsVoice').value); }
|
||||||
$('ttsVoice').value = 'Cora'; localStorage.setItem('gc_ttsVoice', $('ttsVoice').value);
|
|
||||||
}
|
|
||||||
cfg.forEach(k => $(k).addEventListener('input', e => localStorage.setItem('gc_'+k, e.target.value)));
|
cfg.forEach(k => $(k).addEventListener('input', e => localStorage.setItem('gc_'+k, e.target.value)));
|
||||||
// 🔊 auto-play toggle is a checkbox (persist .checked)
|
// 🔊 auto-play toggle is a checkbox (persist .checked)
|
||||||
$('ttsOn').checked = localStorage.getItem('gc_ttsOn') === '1';
|
$('ttsOn').checked = localStorage.getItem('gc_ttsOn') === '1';
|
||||||
@@ -150,7 +149,7 @@ function addMsg(who, cls){
|
|||||||
}
|
}
|
||||||
function showErr(text){ const m = addMsg('error','err'); m.body.className = 'err'; m.body.textContent = text; }
|
function showErr(text){ const m = addMsg('error','err'); m.body.className = 'err'; m.body.textContent = text; }
|
||||||
|
|
||||||
// --- mOrpheus TTS: streaming-decode auto-voice of quoted text (Web Audio, low TTFA) ---
|
// --- TTS: auto-voice quoted text via the configured OpenAI /v1/audio/speech endpoint (Web Audio) ---
|
||||||
let audioCtx = null; // shared AudioContext
|
let audioCtx = null; // shared AudioContext
|
||||||
let ttsGen = 0; // generation counter — a newer reply's stream supersedes older ones
|
let ttsGen = 0; // generation counter — a newer reply's stream supersedes older ones
|
||||||
let speechHead = 0; // shared playback clock — sentences queue onto it back-to-back
|
let speechHead = 0; // shared playback clock — sentences queue onto it back-to-back
|
||||||
@@ -166,45 +165,38 @@ 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;
|
||||||
}
|
}
|
||||||
// stream ONE quoted section (whole, so intonation/prosody across it is preserved) and queue its
|
// Speak ONE quoted section (whole, so prosody across it is preserved). Honors the endpoint, model and
|
||||||
// PCM onto the shared speechHead clock; resolves when its audio is fully received (it plays on
|
// voice set in the interface; requests standard OpenAI /v1/audio/speech mp3 and plays it via Web Audio.
|
||||||
// while the NEXT quote starts generating). Chunk by QUOTE, not sentence — per-sentence lost prosody.
|
// decodeAudioData handles wav/mp3/ogg/flac, so this works with whatever the configured endpoint returns.
|
||||||
async function streamOne(sentence, mine){
|
async function speakOne(sentence, mine){
|
||||||
// ext-tts via zonos-gateway (direct, streams — bypasses LiteLLM's audio buffering): ttsUrl is the full
|
|
||||||
// OpenAI /v1/audio/speech endpoint. The gateway relays Zonos's raw float32 PCM @ 44.1kHz — decode as
|
|
||||||
// little-endian float32 (NOT int16) at 44100 Hz. The Bearer header is sent but the gateway ignores it.
|
|
||||||
const url = $('ttsUrl').value.trim().replace(/\/+$/,'');
|
const url = $('ttsUrl').value.trim().replace(/\/+$/,'');
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method:'POST',
|
method:'POST',
|
||||||
headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + $('key').value },
|
headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + $('key').value },
|
||||||
body: JSON.stringify({ model: 'ext-tts', input: sentence, voice: ($('ttsVoice').value.trim() || 'Cora') })
|
body: JSON.stringify({
|
||||||
|
model: ($('ttsModel').value.trim() || 'ext-tts'),
|
||||||
|
input: sentence,
|
||||||
|
voice: ($('ttsVoice').value.trim() || 'nova'),
|
||||||
|
response_format: 'mp3'
|
||||||
|
})
|
||||||
});
|
});
|
||||||
if (!res.ok || !res.body) return false;
|
if (!res.ok) return false;
|
||||||
const reader = res.body.getReader();
|
const raw = await res.arrayBuffer();
|
||||||
let leftover = new Uint8Array(0);
|
if (mine !== ttsGen) return true; // a newer reply superseded this one
|
||||||
for(;;){
|
let abuf;
|
||||||
const { done, value } = await reader.read();
|
try { abuf = await audioCtx.decodeAudioData(raw); }
|
||||||
if (done || mine !== ttsGen) break; // stop if a newer reply took over
|
catch (e){ return false; } // non-audio body (e.g. error JSON) or unsupported codec
|
||||||
let bytes = new Uint8Array(leftover.length + value.length);
|
if (mine !== ttsGen) return true;
|
||||||
bytes.set(leftover); bytes.set(value, leftover.length);
|
const src = audioCtx.createBufferSource(); src.buffer = abuf; src.connect(audioCtx.destination);
|
||||||
const n = bytes.length - (bytes.length % 4); // whole float32 samples only (ext-tts PCM)
|
if (speechHead < audioCtx.currentTime + 0.05) speechHead = audioCtx.currentTime + 0.05;
|
||||||
leftover = bytes.slice(n);
|
src.start(speechHead); speechHead += abuf.duration; // queue back-to-back on the shared clock
|
||||||
if (!n) continue;
|
|
||||||
const dv = new DataView(bytes.buffer, 0, n), N = n / 4, f32 = new Float32Array(N);
|
|
||||||
for (let i = 0; i < N; i++) f32[i] = dv.getFloat32(i * 4, true); // Zonos float32 PCM, little-endian
|
|
||||||
const abuf = audioCtx.createBuffer(1, N, 44100); abuf.getChannelData(0).set(f32);
|
|
||||||
const src = audioCtx.createBufferSource(); src.buffer = abuf; src.connect(audioCtx.destination);
|
|
||||||
if (speechHead < audioCtx.currentTime + 0.05) speechHead = audioCtx.currentTime + 0.05; // underrun catch-up
|
|
||||||
src.start(speechHead); speechHead += abuf.duration; // queue back-to-back on the shared clock
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
async function speakQuotes(text, msg){
|
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;
|
||||||
// Streaming backend (zonos-gateway) → send ALL quoted dialogue as ONE streamed request;
|
// Send ALL quoted dialogue as ONE request to the configured endpoint (one clip = consistent prosody).
|
||||||
// the stream itself is the incremental playback, so no per-quote chunking is needed.
|
|
||||||
const joined = quotes.join(' ');
|
const joined = quotes.join(' ');
|
||||||
const tag = document.createElement('span'); tag.textContent = ' 🔊';
|
const tag = document.createElement('span'); tag.textContent = ' 🔊';
|
||||||
tag.style.cursor = 'pointer'; tag.title = 'ext-tts — ' + quotes.length + ' quote(s), click to replay';
|
tag.style.cursor = 'pointer'; tag.title = 'ext-tts — ' + quotes.length + ' quote(s), click to replay';
|
||||||
@@ -213,7 +205,7 @@ async function speakQuotes(text, msg){
|
|||||||
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
|
||||||
if (mine !== ttsGen) return;
|
if (mine !== ttsGen) return;
|
||||||
if (!(await streamOne(joined, mine))){ tag.textContent = ' 🔇'; return; }
|
if (!(await speakOne(joined, mine))){ tag.textContent = ' 🔇'; return; }
|
||||||
if (mine === ttsGen) tag.textContent = ' 🔈';
|
if (mine === ttsGen) tag.textContent = ' 🔈';
|
||||||
};
|
};
|
||||||
tag.onclick = () => play().catch(() => tag.textContent = ' 🔇');
|
tag.onclick = () => play().catch(() => tag.textContent = ' 🔇');
|
||||||
|
|||||||
Reference in New Issue
Block a user