export const MIN_UTTERANCE_CHARS = 3; const SPOKEN_ACRONYMS = { qvac: 'Quantum Verse Automatic Computer', cpu: 'C P U', gpu: 'G P U', ram: 'R A M', ssd: 'S S D', hdd: 'H D D', usb: 'U S B', dns: 'D N S', isp: 'I S P', vpn: 'V P N', ssh: 'S S H', api: 'A P I', url: 'U R L', uri: 'U R I', http: 'H T T P', https: 'H T T P S', html: 'H T M L', json: 'J S O N', xml: 'X M L', os: 'O S', ip: 'I P', tts: 'T T S', asr: 'A S R', hud: 'heads up display', llm: 'L L M', cli: 'C L I', gui: 'G U I', ptt: 'P T T', vad: 'V A D', }; function spellAcronyms(text) { return String(text || '').replace(/\b([A-Za-z]{2,6})\b/g, (word) => { const spoken = SPOKEN_ACRONYMS[word.toLowerCase()]; return spoken || word; }); } function speakableAddresses(text) { return String(text || '') .replace(/\b(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\b/g, '$1 $2 $3 $4') .replace(/https?:\/\//gi, '') .replace(/\b([A-Za-z0-9-]+)\.(com|org|net|io|dev|local|lan)\b/gi, (_, host, tld) => `${host} dot ${tld}`) .replace(/\//g, ' slash ') .replace(/@/g, ' at ') .replace(/_/g, ' '); } export function speakableForTts(text) { return spellAcronyms(speakableAddresses(String(text || ''))) .replace(/[`]/g, "'") .replace(/[<>]/g, ' ') .replace(/\s+/g, ' ') .trim(); } export function isSpeakable(text) { const value = String(text || '').trim(); if (!value || value === '[object Object]') return false; if (/\[no speech detected\]|\[blank_audio\]/i.test(value)) return false; if (/^\[[^\]]+\]$/.test(value)) return false; return /[\p{L}\p{N}]/u.test(value); } export function isMeaningfulTranscript(text) { const value = String(text || '').trim(); if (!isSpeakable(value)) return false; return value.replace(/[^\p{L}\p{N}]/gu, '').length >= MIN_UTTERANCE_CHARS; } export class SentenceBuffer { constructor({ onSentence } = {}) { this.onSentence = onSentence; this.pending = ''; } push(text) { this.pending += String(text || ''); const sentences = []; let match; while ((match = this.pending.match(/^(.+?[.!?](?:["')\]]+)?)(?:\s+|$)/s))) { sentences.push(match[1].trim()); this.pending = this.pending.slice(match[0].length); } for (const sentence of sentences) this.onSentence?.(sentence); return sentences; } flush() { const value = this.pending.trim(); this.pending = ''; if (value) this.onSentence?.(value); return value; } clear() { this.pending = ''; } }