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 stripMarkdownForSpeech(text) { let s = String(text || ''); if (!s) return ''; s = s.replace(/```[\w-]*\r?\n?([\s\S]*?)```/g, (_, code) => ' ' + String(code || '').replace(/\s+/g, ' ').trim() + ' '); s = s.replace(/`([^`]+)`/g, '$1'); s = s.replace(/!\[[^\]]*\]\([^)]*\)/g, ' '); s = s.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1'); s = s.replace(/^\s{0,3}#{1,6}\s+/gm, ''); s = s.replace(/^\s{0,3}>\s?/gm, ''); s = s.replace(/^\s{0,3}(?:[-*+]|\d+[.)])\s+/gm, ''); s = s.replace(/^\s*\|.*\|$/gm, (row) => row.replace(/\|/g, ' ').replace(/:?-{3,}:?/g, ' ')); s = s.replace(/^\s*(?:[-*_]){3,}\s*$/gm, ''); s = s.replace(/(\*\*|__)([\s\S]*?)\1/g, '$2'); s = s.replace(/\*([^*\n]+)\*/g, '$1'); s = s.replace(/~~([\s\S]*?)~~/g, '$1'); s = s.replace(/<\/?[A-Za-z][^>]*>/g, ' '); return s.replace(/\n+/g, ' ').replace(/[ \t]{2,}/g, ' ').trim(); } export function forChatDisplay(text) { let s = String(text || ''); if (!s) return ''; s = s.replace(/```[\w-]*\r?\n?([\s\S]*?)```/g, '\n$1\n'); s = s.replace(/`([^`]+)`/g, '$1'); s = s.replace(/!\[[^\]]*\]\([^)]*\)/g, ''); s = s.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1'); s = s.replace(/^\s{0,3}#{1,6}\s+/gm, ''); s = s.replace(/^\s{0,3}>\s?/gm, ''); s = s.replace(/^\s{0,3}[*+]\s+/gm, '- '); s = s.replace(/(\*\*|__)([\s\S]*?)\1/g, '$2'); s = s.replace(/\*(?!\s)([^*\n]+?)(?]*>/g, ''); return s.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').replace(/[ \t]{2,}/g, ' ').replace(/ +$/gm, '').trim(); } export function speakableForTts(text) { return spellAcronyms(speakableAddresses(stripMarkdownForSpeech(text))) .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 = ''; } }