125 lines
4.0 KiB
JavaScript
125 lines
4.0 KiB
JavaScript
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]+?)(?<!\s)\*/g, '$1');
|
|
s = s.replace(/~~([\s\S]*?)~~/g, '$1');
|
|
s = s.replace(/\*{2}/g, '');
|
|
s = s.replace(/<\/?[A-Za-z][^>]*>/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 = ''; }
|
|
}
|