Files
SciFi-XZBT/js/generative-experience.js

1372 lines
46 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* XZBT Generative Experience -- optional local AI narration layer.
*
* The deterministic engine remains authoritative at all times: this subsystem only
* VERBALIZES world state that audio.js / app.js already decided. Nothing here runs
* until the operator prepares it.
*
* NETWORK NOTE (documented exception to the zero-dependency rule in agents.md):
* prepareExperience() dynamically imports WebLLM and Kokoro from CDN. Those imports
* are lazy and guarded -- the rest of the application, including the packaged
* single-file build, runs fully offline and simply leaves this panel on STANDBY.
*/
const XZBT_COMMON_SPEECH_RULES = {
expandUnits: true,
preferSpokenNumbers: true,
units: {
'KM/S': 'kilometers per second',
KM: 'kilometers',
'M/S': 'meters per second',
M: 'meters',
MW: 'megawatts',
GW: 'gigawatts',
KW: 'kilowatts',
KHZ: 'kilohertz',
MHZ: 'megahertz',
GHZ: 'gigahertz',
HZ: 'hertz',
'%': 'percent',
C: 'degrees Celsius',
KPA: 'kilopascals',
MPA: 'megapascals'
},
numberStyle: {
bearings: 'digit-by-digit',
identifiers: 'digit-by-digit',
ordinaryValues: 'natural-number',
decimals: 'spoken-decimal'
},
fallbackRule:
'If compact notation would sound awkward when spoken, expand it into a natural plausible phrase that fits the line. Immersion is more important than strict real-world scientific precision.'
};
const speechProfile = (
role,
topics,
terms,
voice,
speed,
dsp,
prefer,
avoid,
examples,
announcementTemplates = {}
) => ({
role,
topics,
terms,
voice,
speed,
dsp,
speechRules: XZBT_COMMON_SPEECH_RULES,
lexicon: { prefer, avoid },
examples,
announcementTemplates
});
const XZBT_VOICE_AUTOMATION_DEFAULTS = {
starfleet: 0.84,
whataverse: 0.88,
industrial: 1.0,
bioships: 0.65,
retrofuture: 0.94,
military: 0.97,
deepspace: 0.78,
outlaw: 0.7,
spacestations: 0.9,
comedy: 0.72
};
const XZBT_SPEECH_PROFILES = {
starfleet: speechProfile(
'starship operations computer',
'navigation, sensor contacts, engineering status, traffic and safety',
'Calm, precise shipboard operations language. Polished, clinical, competent.',
'af_heart',
0.92,
'ship',
['nominal', 'variance', 'contact', 'bearing', 'power bus', 'drive field', 'sensor coverage'],
['okay', 'heads up', 'looks like', 'oops', 'I think', 'probably'],
{
routine: [
'Primary power distribution remains within nominal limits.',
'Long-range sensor coverage is operating normally.',
'Course correction complete. New bearing zero four two.',
'Drive field output stable at seventy-four percent.'
],
advisory: [
'Minor variance detected in the portside power bus.',
'Unidentified contact entering long-range sensor coverage.',
'Environmental flow has fallen four percent below nominal.',
'Navigation solution updated for local traffic.'
],
warning: [
'Warning. Drive field instability is increasing.',
'Collision threshold exceeded. Automatic correction engaged.',
'Forward structural load has risen above normal operating tolerance.',
'Sensor interference is degrading contact resolution.'
],
recovery: [
'Drive field stability restored.',
'Power distribution has returned to nominal limits.',
'Navigation correction complete.',
'Sensor resolution restored across the forward array.'
]
},
{
contact_detected: [
'Unidentified contact detected at {range} kilometers.',
'New contact. Bearing {bearing}. Range {range} kilometers.'
],
power_variance: ['{system} output has shifted {amount} percent from nominal.'],
course_change: ['Course correction initiated. New bearing {bearing}.']
}
),
whataverse: speechProfile(
'eccentric multidimensional machine status system',
'temporal stability, spatial displacement, mechanisms and navigation',
'An old, brilliant, temperamental multidimensional machine treating impossible mechanics as routine maintenance. Precise and strange, not jokey.',
'bf_emma',
0.96,
'machine',
[
'temporal index',
'dimensional shear',
'phase rotor',
'translation field',
'coordinate lock',
'chronometric drift',
'mechanical interlock'
],
['time travel', 'TARDIS', 'Doctor', 'Gallifrey', 'wibbly', 'magic', 'oops'],
{
routine: [
'Temporal index holding within three points of baseline.',
'Coordinate lock maintained across the translation field.',
'Phase rotors synchronized and carrying load.',
'Interior dimensional pressure remains comfortably impossible.'
],
advisory: [
'Chronometric drift detected in the outer reference frame.',
'Translation field is developing a minor phase asymmetry.',
'Mechanical interlock three is cycling more slowly than expected.',
'Coordinate solution revised after a local dimensional fold.'
],
warning: [
'Warning. Dimensional shear is rising across the translation envelope.',
'Temporal reference lock has fallen below stable tolerance.',
'Phase rotor synchronization is degrading.',
'Spatial compression limit approaching automatic correction threshold.'
],
recovery: [
'Temporal reference lock restored.',
'Dimensional shear has returned to background levels.',
'Phase rotors resynchronized.',
'Coordinate solution stabilized after correction.'
]
}
),
industrial: speechProfile(
'utilitarian transport vessel automation system',
'drive status, pressure, docking, mechanical load and environmental systems',
'Terse physical-plant language. Practical, mechanical and workmanlike.',
'am_adam',
0.9,
'industrial',
[
'manifold',
'load',
'pressure',
'pump',
'bearing temperature',
'bus voltage',
'seal',
'actuator',
'feed line'
],
['quantum', 'elegant', 'mysterious', 'awesome', 'oops'],
{
routine: [
'Main feed pressure steady at two hundred forty kilopascals.',
'Cargo lift actuators are parked and locked.',
'Drive coolant pumps are carrying normal load.',
'Docking clamps report full mechanical engagement.'
],
advisory: [
'Pump two is drawing six percent above normal current.',
'Port manifold pressure is drifting low.',
'Aft bearing temperature has increased three degrees Celsius.',
'Cargo bay ventilation is compensating for reduced flow.'
],
warning: [
'Warning. Drive coolant pressure is below operating tolerance.',
'Docking clamp three has not confirmed full engagement.',
'Main bus voltage is unstable under load.',
'Hydraulic return pressure is approaching shutdown threshold.'
],
recovery: [
'Coolant pressure restored.',
'Docking clamps confirmed secure.',
'Main bus voltage has stabilized.',
'Ventilation flow returned to scheduled output.'
]
}
),
bioships: speechProfile(
'living vessel autonomic awareness system',
'biological regulation, sensory conditions, metabolism, external organisms and stress',
'Clinical biological language. The vessel is alive; describe physiology rather than machinery. Never mystical.',
'bf_isabella',
0.88,
'organic',
[
'vascular flow',
'neural response',
'membrane',
'metabolic rate',
'sensory tissue',
'respiration',
'regeneration',
'stress response'
],
['reactor', 'engine room', 'power bus', 'computer core', 'magic', 'soul'],
{
routine: [
'Vascular flow remains even through the dorsal channels.',
'Metabolic rate is stable at sixty-eight percent of cruise demand.',
'Sensory membranes show no harmful external contact.',
'Respiratory exchange remains balanced.'
],
advisory: [
'Neural response has increased along the forward sensory ridge.',
'Minor tissue stress detected near the outer membrane.',
'Metabolic demand has risen five percent.',
'Regeneration activity is increasing around a healed abrasion.'
],
warning: [
'Warning. Vascular pressure is falling in the aft lobe.',
'Protective membrane stress exceeds normal tolerance.',
'Neural response is becoming irregular.',
'Respiratory exchange is below stable demand.'
],
recovery: [
'Vascular pressure restored.',
'Protective membrane stress is subsiding.',
'Neural response has returned to baseline.',
'Respiratory balance restored.'
]
}
),
retrofuture: speechProfile(
'mid-century spaceflight computer',
'course, fuel, cabin conditions, radio contacts and machinery',
'Compact formal space-age English with short declarative sentences and old-fashioned instrumentation terms.',
'am_michael',
0.86,
'retro',
[
'azimuth',
'range',
'radio beacon',
'fuel reserve',
'cabin pressure',
'gyro',
'automatic pilot',
'indicator'
],
['sensor fusion', 'neural', 'quantum mesh', 'heads up', 'awesome', 'oops'],
{
routine: [
'Automatic pilot holding course zero eight seven.',
'Cabin pressure steady.',
'Fuel reserve is seventy-one percent.',
'Radio beacon received at normal strength.'
],
advisory: [
'Gyro two shows a small azimuth error.',
'A weak radio contact has appeared ahead.',
'Cabin temperature has increased two degrees Celsius.',
'Fuel flow is slightly above cruise setting.'
],
warning: [
'Warning. Cabin pressure is falling.',
'Automatic pilot correction limit exceeded.',
'Main gyro signal is unstable.',
'Fuel feed pressure is below safe operating range.'
],
recovery: [
'Cabin pressure restored.',
'Automatic pilot is back on course.',
'Main gyro signal steady.',
'Fuel feed pressure normal.'
]
}
),
military: speechProfile(
'combat information and damage-control computer',
'contacts, readiness, propulsion, defensive systems and compartment condition',
'Clipped watch-floor reporting. Disciplined and unemotional. Never infer hostility or combat.',
'am_adam',
0.9,
'military',
[
'contact',
'track',
'readiness',
'compartment',
'damage control',
'propulsion',
'bearing',
'range',
'condition'
],
['enemy', 'hostile', 'weapons fire', 'attack', 'panic', 'probably'],
{
routine: [
'Propulsion readiness remains green.',
'Forward compartments report normal condition.',
'Track picture stable. No priority contacts.',
'Damage-control circuits are standing by.'
],
advisory: [
'New contact. Bearing two seven one. Range eighteen thousand kilometers.',
'Compartment six reports a minor pressure variance.',
'Propulsion reserve has decreased four percent.',
'Track quality is reduced in the aft sector.'
],
warning: [
'Warning. Compartment pressure is below operating tolerance.',
'Propulsion margin is approaching minimum reserve.',
'Multiple tracks have lost positive classification.',
'Damage-control isolation has engaged in section four.'
],
recovery: [
'Compartment pressure restored.',
'Propulsion margin recovered.',
'Track classification restored.',
'Section four isolation cleared.'
]
}
),
deepspace: speechProfile(
'deep-space exploration computer',
'astronomical observations, navigation, sensor data, environment and long-range contacts',
'Scientific observation, measured uncertainty and precise status language. Wonder comes from the data, not emotion.',
'af_nicole',
0.9,
'science',
[
'spectral',
'parallax',
'radiation flux',
'baseline',
'survey',
'transient',
'solution',
'long-range array'
],
['beautiful', 'amazing', 'scary', 'alien', 'probably', 'I think'],
{
routine: [
'Long-range array is collecting a clean spectral baseline.',
'Navigation solution remains stable against background stars.',
'Radiation flux is within survey limits.',
'No significant transient detected in the current field.'
],
advisory: [
'A weak transient has appeared beyond the primary survey range.',
'Parallax solution has shifted by zero point three degrees.',
'Background radiation has increased four percent.',
'Long-range contact resolution is improving.'
],
warning: [
'Warning. Radiation flux is above survey tolerance.',
'Navigation confidence has fallen below the preferred margin.',
'Sensor saturation is reducing spectral resolution.',
'Long-range array temperature is approaching its operating limit.'
],
recovery: [
'Radiation flux returned to baseline.',
'Navigation confidence restored.',
'Spectral resolution recovered.',
'Long-range array temperature stabilized.'
]
}
),
outlaw: speechProfile(
'independent freighter utility computer',
'fuel, cargo, navigation, nearby traffic, machinery and comms',
'Practical civilian shipboard language. Dry, economical and slightly worn, never theatrical.',
'am_michael',
0.94,
'utility',
[
'fuel reserve',
'cargo lock',
'drive temperature',
'traffic',
'relay',
'course',
'comms',
'service limit'
],
['command protocol', 'fleet', 'heroic', 'enemy', 'awesome', 'oops'],
{
routine: [
'Fuel reserve holding at sixty-three percent.',
'Cargo locks remain secure.',
'Drive temperature is steady.',
'No traffic conflict on the current course.'
],
advisory: [
'A nearby courier has crossed the forward traffic lane.',
'Cargo lock seven needs another confirmation cycle.',
'Drive temperature is up three degrees Celsius.',
'Comms relay quality is getting rough.'
],
warning: [
'Warning. Fuel feed pressure is below service limit.',
'Cargo lock seven has failed to confirm.',
'Drive temperature is approaching cutoff.',
'Traffic separation is below automatic comfort margin.'
],
recovery: [
'Fuel feed pressure is back in range.',
'Cargo lock seven confirmed.',
'Drive temperature is falling.',
'Traffic separation restored.'
]
}
),
spacestations: speechProfile(
'orbital station operations announcer',
'traffic, docking berths, approach corridors, maintenance and environmental status',
'Busy orbital traffic and facilities voice. Favor berths, corridors, docking queues, rotation sections, service crews and transfer decks.',
'af_sky',
0.91,
'pa',
[
'berth',
'approach corridor',
'traffic control',
'rotation section',
'dockmaster',
'service crew',
'transfer deck',
'pressure zone'
],
['bridge', 'warp core', 'captain', 'enemy', 'starfleet', 'oops'],
{
routine: [
'Approach corridor three is clear for scheduled traffic.',
'Berth twelve remains occupied by a cargo transfer.',
'Rotation section speed is stable.',
'Transfer deck environmental systems are operating normally.'
],
advisory: [
'Incoming shuttle traffic is being resequenced to corridor two.',
'Berth seven reports a delayed pressure check.',
'Service crew requested at the outer docking ring.',
'Rotation section load has increased five percent.'
],
warning: [
'Warning. Approach corridor four is temporarily closed.',
'Berth seven pressure check is outside tolerance.',
'Docking ring load is above scheduled limit.',
'Transfer deck pressure zone has entered automatic isolation.'
],
recovery: [
'Approach corridor four reopened.',
'Berth seven pressure check complete.',
'Docking ring load returned to schedule.',
'Transfer deck isolation cleared.'
]
}
),
comedy: speechProfile(
'competent but slightly quirky ship computer',
'routine operations, navigation, systems and harmless anomalies',
'Competent and factual with mild dry understatement and excessive administrative certainty. Humor comes only from phrasing.',
'af_bella',
0.96,
'clean',
[
'technically',
'within tolerance',
'administratively',
'minor inconvenience',
'scheduled',
'unlikely',
'adequate'
],
['punchline', 'ha ha', 'LOL', 'oops', 'random', 'wacky'],
{
routine: [
'Navigation remains on course, which is encouraging.',
'Life support is operating within its officially approved level of adequacy.',
'Drive output is stable and currently requires no paperwork.',
'No unusual contacts detected. The ordinary ones remain ordinary.'
],
advisory: [
'A minor course adjustment has become administratively necessary.',
'Power demand is four percent above the level everyone agreed was normal.',
'One sensor contact is unusual, but not yet interesting.',
'Environmental control is compensating for a small and entirely survivable variance.'
],
warning: [
'Warning. Drive output is now outside the range labeled reassuring.',
'Collision margin is below the value preferred by sensible navigation systems.',
'Power reserve has reached the officially inconvenient level.',
'Environmental control requires prompt attention rather than eventual attention.'
],
recovery: [
'Drive output has returned to the reassuring range.',
'Collision margin restored.',
'Power reserve is once again merely adequate.',
'Environmental control has resumed being someone elses problem.'
]
}
)
};
class XZBTGenerativeExperience {
constructor(opts = {}) {
this.getUniverse = opts.getUniverse || (() => null);
this.getPreset = opts.getPreset || (() => null);
this.isPlaying = opts.isPlaying || (() => false);
this.isObservation = opts.isObservation || (() => false);
this.onGenerated = opts.onGenerated || (() => {});
this.audioManager = opts.audioManager || null;
this.engine = null;
this.webllm = null;
this.modelId = 'Qwen3-1.7B-q4f16_1-MLC';
this.kokoro = null;
this.kokoroModule = null;
this.ttsReady = false;
this.preparing = null;
this.busy = false;
this.autoTimer = null;
this.history = [];
this.lastWorldState = null;
this.ui = {};
this.currentAudio = null;
this.audioUrl = null;
}
bindUI() {
const id = x => document.getElementById(x);
this.ui = {
badge: id('ai-status-badge'),
progress: id('ai-progress'),
output: id('ai-experience-output'),
prepare: id('btn-ai-prepare'),
generate: id('btn-ai-generate'),
auto: id('ai-auto-enabled'),
speech: id('ai-speech-enabled'),
voice: id('ai-kokoro-voice'),
robot: id('ai-robot-amount'),
robotValue: id('ai-robot-value'),
cadence: id('ai-cadence'),
cadenceValue: id('ai-cadence-value'),
llmState: id('ai-llm-state'),
ttsState: id('ai-tts-state')
};
if (!this.ui.prepare) return;
this.ui.prepare.addEventListener('click', () => this.prepareExperience());
this.ui.generate.addEventListener('click', () => this.generateAmbientMoment('manual-request'));
this.ui.auto.addEventListener('change', () => this.setAuto(this.ui.auto.checked));
if (this.ui.robot) {
this.ui.robot.addEventListener('input', () => this.syncRobotUI_());
this.applyThemeVoiceAutomation_();
}
this.ui.cadence.addEventListener('input', () => {
const b = Number(this.ui.cadence.value);
this.ui.cadenceValue.textContent = `${b}-${b * 2} SEC`;
if (this.ui.auto.checked) this.scheduleNext();
});
if (!navigator.gpu)
this.setStatus(
'NO WEBGPU',
'error',
'WebGPU is unavailable. Existing XZBT systems still work, but WebLLM will not load.'
);
}
setStatus(label, kind = '', detail = '') {
if (!this.ui.badge) return;
this.ui.badge.textContent = label;
this.ui.badge.className = `ai-status-badge ${kind}`.trim();
if (detail) this.ui.progress.textContent = detail;
}
async prepareExperience() {
if (this.engine && this.ttsReady) return true;
if (this.preparing) return this.preparing;
this.preparing = this._prepare();
try {
return await this.preparing;
} finally {
this.preparing = null;
}
}
async _prepare() {
if (!navigator.gpu) {
this.setStatus(
'NO WEBGPU',
'error',
'A WebGPU-capable Chrome/Edge browser is required for local language generation.'
);
return false;
}
try {
this.ui.prepare.disabled = true;
this.setStatus('PREPARING', 'busy', 'Loading local AI runtime...');
this.ui.llmState.textContent = 'LOAD';
this.webllm = this.webllm || (await import('https://esm.run/@mlc-ai/web-llm'));
const known = (this.webllm.prebuiltAppConfig?.model_list || []).some(
m => m.model_id === this.modelId
);
if (!known) throw Error(`Configured WebLLM model is unavailable: ${this.modelId}`);
if (!this.engine) {
this.engine = await this.webllm.CreateMLCEngine(this.modelId, {
initProgressCallback: r =>
this.setStatus('LOADING LLM', 'busy', r?.text || 'Loading language model...')
});
}
this.ui.llmState.textContent = 'READY';
this.ui.generate.disabled = false;
await this.prepareTTS();
this.setStatus('LOCAL AI READY', 'ready', `${this.modelId} + Kokoro prepared.`);
if (this.ui.auto.checked) this.scheduleNext();
return true;
} catch (e) {
console.error(e);
this.setStatus('AI ERROR', 'error', e?.message || String(e));
return false;
} finally {
this.ui.prepare.disabled = false;
}
}
async prepareTTS() {
if (this.ttsReady) return true;
try {
this.ui.ttsState.textContent = 'LOAD';
this.setStatus('LOADING VOICE', 'busy', 'Loading Kokoro neural voice model...');
this.kokoroModule =
this.kokoroModule || (await import('https://esm.sh/[email protected]?bundle'));
const { KokoroTTS } = this.kokoroModule;
let lastErr = null;
for (const cfg of [
{ dtype: 'fp32', device: 'webgpu' },
{ dtype: 'q8', device: 'wasm' }
]) {
try {
this.kokoro = await KokoroTTS.from_pretrained('onnx-community/Kokoro-82M-v1.0-ONNX', cfg);
break;
} catch (e) {
lastErr = e;
console.warn('Kokoro load attempt failed', cfg, e);
}
}
if (!this.kokoro) throw lastErr || Error('Kokoro failed to initialize');
this.populateKokoroVoices();
this.ttsReady = true;
this.ui.ttsState.textContent = 'READY';
return true;
} catch (e) {
console.warn(e);
this.ui.ttsState.textContent = 'FALLBACK';
this.ttsReady = false;
this.ui.progress.textContent =
'Kokoro unavailable; browser speech fallback remains available.';
return false;
}
}
populateKokoroVoices() {
if (!this.ui.voice || !this.kokoro) return;
const current = this.profile().voice;
let voices = this.kokoro.voices || {};
if (typeof this.kokoro.list_voices === 'function') {
try {
voices = this.kokoro.list_voices() || voices;
} catch (_) {}
}
this.ui.voice.innerHTML = '';
if (Array.isArray(voices)) {
voices.forEach(v => {
const key = typeof v === 'string' ? v : v.id || v.name;
if (!key) return;
const o = document.createElement('option');
o.value = key;
o.textContent = key;
this.ui.voice.appendChild(o);
});
} else {
Object.entries(voices).forEach(([key, val]) => {
const o = document.createElement('option');
o.value = key;
o.textContent = val?.name ? `${val.name} (${key})` : key;
this.ui.voice.appendChild(o);
});
}
if ([...this.ui.voice.options].some(o => o.value === current)) this.ui.voice.value = current;
}
profile() {
const id = this.getUniverse()?.id || 'starfleet';
return XZBT_SPEECH_PROFILES[id] || XZBT_SPEECH_PROFILES.starfleet;
}
voiceAutomationDefault_() {
const id = this.getUniverse()?.id || 'starfleet';
return XZBT_VOICE_AUTOMATION_DEFAULTS[id] ?? XZBT_VOICE_AUTOMATION_DEFAULTS.starfleet;
}
syncRobotUI_() {
if (!this.ui.robot) return;
const a = Math.max(0, Math.min(1, Number(this.ui.robot.value) || 0));
this.ui.robot.value = String(a);
if (this.ui.robotValue) this.ui.robotValue.textContent = `${Math.round(a * 100)}%`;
}
applyThemeVoiceAutomation_() {
if (!this.ui.robot) return;
this.ui.robot.value = String(this.voiceAutomationDefault_());
this.syncRobotUI_();
}
onThemeChanged() {
this.history = [];
this.lastWorldState = null;
this.applyThemeVoiceAutomation_();
if (this.ttsReady) this.populateKokoroVoices();
}
pick_(a) {
return Array.isArray(a) && a.length ? a[Math.floor(Math.random() * a.length)] : '';
}
examplesFor_(severity) {
const e = this.profile().examples || {},
shuffle = a =>
a
.map(x => ({ x, r: Math.random() }))
.sort((a, b) => a.r - b.r)
.map(v => v.x),
primary = shuffle([...(e[severity] || [])]).slice(0, 2),
other =
severity === 'routine'
? ['advisory']
: severity === 'advisory'
? ['routine', 'recovery']
: severity === 'recovery'
? ['routine', 'advisory']
: ['advisory', 'recovery'],
support = shuffle(other.flatMap(k => e[k] || [])).slice(0, 1);
return [...primary, ...support];
}
templatesFor_(severity) {
const t = this.profile().announcementTemplates || {};
return [...(t[severity] || []), ...(t.routine || [])]
.filter((x, i, a) => a.indexOf(x) === i)
.slice(0, 3);
}
makeWorldState(reason = 'ambient') {
const u = this.getUniverse() || {},
p = this.getPreset() || {},
profile = this.profile(),
topics = profile.topics.split(',').map(x => x.trim()),
severity = this.pick_(['routine', 'routine', 'routine', 'advisory', 'advisory', 'recovery']),
topic = this.pick_(topics),
topicKey = topic.toLowerCase(),
st = {
reason,
environment: u.name || u.id || 'unknown environment',
preset: p.name || 'active installation',
reportTopic: topic,
severity,
condition: this.pick_([
'nominal',
'stable',
'minor fluctuation',
'within tolerance',
'quiet watch',
'elevated activity'
]),
observation: !!this.isObservation(),
timestamp: new Date().toLocaleTimeString()
};
const needsNav =
/navigation|course|traffic|contact|sensor|radio|docking|approach|spatial displacement/.test(
topicKey
);
const needsPlant =
/engineering|power|drive|pressure|propulsion|environment|mechanism|maintenance|load|fuel|machinery/.test(
topicKey
);
const needsBio = /biolog|metabol|neural|membrane|vascular|respir/.test(topicKey);
const needsTemporal = /temporal|chronometric|dimensional/.test(topicKey);
if (needsNav) {
const rangeKm = Math.round((3000 + Math.random() * 87000) / 100) * 100;
st.bearing = String(Math.floor(Math.random() * 360)).padStart(3, '0');
st.range = `${rangeKm} KM`;
st.contact = this.pick_([
'routine traffic',
'unidentified contact',
'survey craft',
'courier',
'maintenance vehicle',
'distant transient'
]);
}
if (needsPlant)
st.subsystem = this.pick_([
'primary feed',
'translation field',
'phase rotor',
'environmental control',
'communications relay',
'drive assembly',
'mechanical interlock'
]);
if (needsBio)
st.bioRegion = this.pick_([
'dorsal channels',
'forward sensory ridge',
'outer membrane',
'aft lobe',
'respiratory folds',
'regenerative tissue'
]);
if (needsTemporal) {
st.temporalMetric = this.pick_([
'temporal index',
'reference lock',
'chronometric drift',
'translation coherence',
'phase alignment'
]);
st.variance = `${1 + Math.floor(Math.random() * 8)} points`;
}
this.lastWorldState = st;
return st;
}
speechRuleText_() {
const r = this.profile().speechRules || XZBT_COMMON_SPEECH_RULES;
return `Speech rules:
- Output natural TTS-ready spoken language.
- Expand all compact units and abbreviations. Never output KM, KM/S, MW, GW, kHz, MHz, GHz, %, or bare C for temperature.
- Speak bearings and identifiers digit by digit, preserving leading zeroes.
- Speak ordinary quantities as natural numbers.
- Speak decimal points aloud.
- ${r.fallbackRule}`;
}
systemPrompt() {
const p = this.profile(),
l = p.lexicon || { prefer: [], avoid: [] };
return `/no_think\nYou are the ${p.role} for an ambient science-fiction exhibition. The simulation engine is authoritative and supplies every fact. Report only supplied facts.\nReporting priorities: ${p.topics}.\nTheme voice: ${p.terms}\nPreferred vocabulary: ${(l.prefer || []).join(', ')}.\nAvoid vocabulary: ${(l.avoid || []).join(', ')}.\n${this.speechRuleText_()}\nUse the supplied examples as the strongest guide to voice, rhythm, vocabulary and sentence construction. Invent only harmless connective phrasing needed to verbalize supplied state. Never invent causes, intentions, emergencies, casualties, weapons fire, characters, franchises, lore or mission-changing events. Never expose chain-of-thought, reasoning, analysis or <think> tags. Produce exactly one concise spoken update, normally 8-28 words, with no quotation marks, labels, markdown or explanation.`;
}
userPrompt(st) {
const recent = this.history.slice(-12),
openings = recent.map(x => x.split(/\s+/).slice(0, 4).join(' ')),
examples = this.examplesFor_(st.severity),
templates = this.templatesFor_(st.severity);
return `/no_think
Generate one short spoken computer announcement about ONLY the selected report topic.
Theme examples (${st.severity}):
${examples.map(x => '- ' + x).join('\n')}
Reusable announcement patterns:
${templates.map(x => '- ' + x).join('\n') || '(none)'}
Current engine state:
${JSON.stringify(st)}
Recent announcements that MUST NOT be repeated or closely paraphrased:
${recent.join('\n') || '(none)'}
Avoid these opening constructions:
${openings.join('\n') || '(none)'}
Use only facts present in Current engine state. Do not borrow facts or numeric values from the examples. Match the examples only for voice and sentence style. If a fact is absent, do not mention it. Expand every unit and abbreviation. Return only the final spoken update.`;
}
repetitionKey_(text) {
return String(text || '')
.toLowerCase()
.replace(
/\b(?:zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty|thirty|forty|fifty|sixty|seventy|eighty|ninety|hundred|thousand|point)\b/g,
'#'
)
.replace(/[^a-z# ]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
contentTokens_(text) {
const stop = new Set([
'the',
'a',
'an',
'and',
'or',
'of',
'to',
'in',
'on',
'at',
'for',
'with',
'from',
'is',
'are',
'has',
'have',
'remains',
'remain',
'within',
'status',
'system',
'systems',
'condition',
'report',
'stable',
'nominal',
'normal',
'currently',
'still',
'#'
]);
return this.repetitionKey_(text)
.split(' ')
.filter(x => x && !stop.has(x));
}
similarity_(a, b) {
const aa = new Set(this.contentTokens_(a)),
bb = new Set(this.contentTokens_(b));
if (!aa.size || !bb.size) return 0;
let hit = 0;
aa.forEach(x => {
if (bb.has(x)) hit++;
});
return hit / Math.max(aa.size, bb.size);
}
hardDuplicate_(line) {
const key = this.repetitionKey_(line);
return !!key && this.history.slice(-16).some(prev => this.repetitionKey_(prev) === key);
}
repetitionScore_(line) {
let max = 0;
for (const prev of this.history.slice(-10)) max = Math.max(max, this.similarity_(line, prev));
return max;
}
tooRepetitive_(line) {
return this.hardDuplicate_(line) || this.repetitionScore_(line) >= 0.86;
}
fallbackAnnouncement_(st) {
const topic = String(st.reportTopic || 'systems')
.replace(/\s+/g, ' ')
.trim(),
condition = String(st.condition || 'stable').toLowerCase();
const phrase =
condition === 'minor fluctuation'
? 'showing a minor fluctuation'
: condition === 'within tolerance'
? 'within tolerance'
: condition === 'quiet watch'
? 'quiet and stable'
: condition === 'elevated activity'
? 'showing elevated activity'
: condition === 'nominal'
? 'nominal'
: 'stable';
const id = this.getUniverse()?.id || '';
if (id === 'military')
return `${topic.charAt(0).toUpperCase() + topic.slice(1)} ${phrase === 'nominal' ? 'reports nominal condition' : phrase === 'stable' ? 'reports stable condition' : phrase}.`;
if (id === 'comedy')
return `${topic.charAt(0).toUpperCase() + topic.slice(1)} ${phrase}, which remains administratively acceptable.`;
return `${topic.charAt(0).toUpperCase() + topic.slice(1)} ${phrase}.`;
}
numberWords_(n) {
n = Math.trunc(Math.abs(Number(n) || 0));
const o = [
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'ten',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
'sixteen',
'seventeen',
'eighteen',
'nineteen'
],
t = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
if (n < 20) return o[n];
if (n < 100) return t[Math.floor(n / 10)] + (n % 10 ? '-' + o[n % 10] : '');
if (n < 1000)
return (
o[Math.floor(n / 100)] + ' hundred' + (n % 100 ? ' ' + this.numberWords_(n % 100) : '')
);
if (n < 1000000)
return (
this.numberWords_(Math.floor(n / 1000)) +
' thousand' +
(n % 1000 ? ' ' + this.numberWords_(n % 1000) : '')
);
return String(n)
.split('')
.map(d => o[Number(d)])
.join(' ');
}
spokenNumber_(raw) {
const x = String(raw),
neg = x.startsWith('-'),
v = neg ? x.slice(1) : x;
if (v.includes('.')) {
const [a, b] = v.split('.');
return `${neg ? 'minus ' : ''}${this.numberWords_(Number(a))} point ${b
.split('')
.map(d => this.numberWords_(Number(d)))
.join(' ')}`;
}
return `${neg ? 'minus ' : ''}${this.numberWords_(Number(v))}`;
}
ttsSafe_(text) {
let s = String(text || ''),
units = this.profile().speechRules?.units || XZBT_COMMON_SPEECH_RULES.units;
// Pass 1: compact numeric measurements such as 482 km, 0.8 GHz and 74%.
for (const key of Object.keys(units).sort((a, b) => b.length - a.length)) {
const val = units[key];
if (key === '%') {
s = s.replace(/(-?\d+(?:\.\d+)?)\s*%/g, (_, n) => `${this.spokenNumber_(n)} ${val}`);
continue;
}
const esc = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
s = s.replace(
new RegExp(`(-?\\d+(?:\\.\\d+)?)\\s*${esc}\\b`, 'gi'),
(_, n) => `${this.spokenNumber_(n)} ${val}`
);
}
// Pass 2: unit aliases that may follow numbers the model already spelled out.
// Do not reconstruct the number words; only make the unit safe for speech.
const spokenUnitAliases = [
[/\bkm\s*\/\s*(?:s|sec|second)s?\b/gi, 'kilometers per second'],
[/\bkm\s+per\s+(?:s|sec|second)s?\b/gi, 'kilometers per second'],
[/\bkm\b/gi, 'kilometers'],
[/\bkilometres\b/gi, 'kilometers'],
[/\bm\s*\/\s*(?:s|sec|second)s?\b/gi, 'meters per second'],
[/\bmw\b/gi, 'megawatts'],
[/\bgw\b/gi, 'gigawatts'],
[/\bkw\b/gi, 'kilowatts'],
[/\bkhz\b/gi, 'kilohertz'],
[/\bmhz\b/gi, 'megahertz'],
[/\bghz\b/gi, 'gigahertz'],
[/\bhz\b/gi, 'hertz'],
[/\bkpa\b/gi, 'kilopascals'],
[/\bmpa\b/gi, 'megapascals']
];
spokenUnitAliases.forEach(([re, val]) => {
s = s.replace(re, val);
});
s = s.replace(
/\bbearing\s+([0-9]{1,3})\b/gi,
(_, n) =>
`bearing ${String(n)
.padStart(3, '0')
.split('')
.map(d => this.numberWords_(Number(d)))
.join(' ')}`
);
s = s.replace(
/\b(identifier|track)\s+([A-Z]?\d{2,})\b/gi,
(_, label, n) =>
`${label} ${n
.split('')
.map(ch => (/\d/.test(ch) ? this.numberWords_(Number(ch)) : ch))
.join(' ')}`
);
return s.replace(/\s+/g, ' ').trim();
}
sanitize(t) {
let s = String(t || '');
// Qwen3-class models may emit reasoning payloads even when only a short answer is wanted.
// Remove complete blocks first, then defensively discard malformed/unclosed reasoning prefixes.
s = s.replace(/<think(?:\s[^>]*)?>[\s\S]*?<\/think>/gi, ' ');
s = s.replace(/<analysis(?:\s[^>]*)?>[\s\S]*?<\/analysis>/gi, ' ');
s = s.replace(/<reasoning(?:\s[^>]*)?>[\s\S]*?<\/reasoning>/gi, ' ');
const lastThinkClose = s.toLowerCase().lastIndexOf('</think>');
if (lastThinkClose >= 0) s = s.slice(lastThinkClose + 8);
const openReasoning = s.search(/<(think|analysis|reasoning)(?:\s[^>]*)?>/i);
if (openReasoning >= 0) {
const before = s.slice(0, openReasoning).trim();
s = before; // never speak an unclosed reasoning payload
}
s = s
.replace(/<\/?(?:think|analysis|reasoning)(?:\s[^>]*)?>/gi, ' ')
.replace(/^```[a-z]*|```$/gim, '')
.replace(/^\s*(assistant|computer|announcement|status)\s*:\s*/i, '')
.replace(/["“”]/g, '')
.replace(/\s+/g, ' ')
.trim();
return this.ttsSafe_(s.slice(0, 360));
}
async generateAmbientMoment(reason = 'ambient') {
if (!this.engine) {
const ok = await this.prepareExperience();
if (!ok || !this.engine) return null;
}
if (this.busy) return null;
let state = this.makeWorldState(reason);
try {
this.busy = true;
this.ui.generate.disabled = true;
this.setStatus('GENERATING', 'busy', `Composing ${reason.replace(/-/g, ' ')} locally...`);
let line = '',
bestLine = '',
bestState = state,
bestScore = Infinity;
for (let attempt = 0; attempt < 4; attempt++) {
if (attempt > 0) state = this.makeWorldState(reason);
const retryNote = attempt
? `
VARIATION REQUIRED: choose a different sentence construction from recent announcements. The current engine state may also use a different report topic. Do not force familiar theme catchphrases unless they are needed by the supplied facts.`
: '';
const c = await this.engine.chat.completions.create({
messages: [
{ role: 'system', content: this.systemPrompt() },
{ role: 'user', content: this.userPrompt(state) + retryNote }
],
temperature: 0.76 + attempt * 0.06,
top_p: 0.94,
frequency_penalty: 0.82,
presence_penalty: 0.42,
max_tokens: 80
});
const candidate = this.sanitize(c?.choices?.[0]?.message?.content);
if (!candidate) continue;
const score = this.repetitionScore_(candidate);
if (!this.hardDuplicate_(candidate) && score < bestScore) {
bestLine = candidate;
bestState = state;
bestScore = score;
}
if (!this.tooRepetitive_(candidate)) {
line = candidate;
break;
}
}
if (!line && bestLine) {
line = bestLine;
state = bestState;
}
if (!line) {
line = this.sanitize(this.fallbackAnnouncement_(state));
if (this.hardDuplicate_(line)) {
this.ui.output.textContent = 'DUPLICATE CHATTER SUPPRESSED';
this.setStatus(
'LOCAL AI READY',
'ready',
'No sufficiently fresh line this cycle; continuing normally.'
);
return { line: null, state, suppressed: true };
}
}
this.history.push(line);
if (this.history.length > 24) this.history.shift();
this.ui.output.textContent = line;
this.setStatus(
'LOCAL AI READY',
'ready',
`${this.modelId}${state.timestamp}${bestScore >= 0.86 ? ' • relaxed variation' : ''}`
);
this.onGenerated(line, state);
if (this.ui.speech.checked) await this.speak(line);
return { line, state };
} catch (e) {
console.error(e);
this.setStatus('GEN ERROR', 'error', e?.message || String(e));
return null;
} finally {
this.busy = false;
this.ui.generate.disabled = !this.engine;
if (this.ui.auto.checked) this.scheduleNext();
}
}
async describeExistingEvent(type) {
if (!this.engine || !this.ui.auto.checked || this.busy || Math.random() > 0.58) return;
await this.generateAmbientMoment(`operator-event:${type}`);
}
roboticAmount() {
return Math.max(0, Math.min(1, Number(this.ui.robot?.value) || 0));
}
makeRobotCurve(amount) {
const n = 1024,
curve = new Float32Array(n),
drive = 1 + amount * 5;
for (let i = 0; i < n; i++) {
const x = (i * 2) / (n - 1) - 1;
curve[i] = Math.tanh(x * drive) / Math.tanh(drive);
}
return curve;
}
async playProcessedSpeech(a) {
const amount = this.roboticAmount();
const fnc = window.fncSystem;
if (fnc) fnc.hold('speech', true);
const releaseFnc = () => {
if (fnc) fnc.hold('speech', false);
};
a.addEventListener('ended', releaseFnc, { once: true });
a.addEventListener(
'pause',
() => {
if (!a.ended) releaseFnc();
},
{ once: true }
);
if (!this.audioManager || amount <= 0.001) {
a.volume = 0.92;
try {
await a.play();
} catch (e) {
releaseFnc();
throw e;
}
return;
}
await this.audioManager.resume();
const ctx = this.audioManager.ctx;
if (!ctx || !this.audioManager.compressor) {
a.volume = 0.92;
try {
await a.play();
} catch (e) {
releaseFnc();
throw e;
}
return;
}
const src = ctx.createMediaElementSource(a),
hp = ctx.createBiquadFilter(),
lp = ctx.createBiquadFilter(),
shape = ctx.createWaveShaper(),
dry = ctx.createGain(),
robot = ctx.createGain(),
robotOut = ctx.createGain(),
lfo = ctx.createOscillator(),
depth = ctx.createGain();
hp.type = 'highpass';
hp.frequency.value = 90 + amount * 120;
hp.Q.value = 0.7;
lp.type = 'lowpass';
lp.frequency.value = 9000 - amount * 3800;
lp.Q.value = 0.6;
shape.curve = this.makeRobotCurve(amount);
shape.oversample = '2x';
dry.gain.value = 1 - amount * 0.12;
robot.gain.value = 0;
robotOut.gain.value = amount * 0.9;
lfo.type = 'sine';
lfo.frequency.value = 18 + amount * 28;
depth.gain.value = 0.035 + amount * 0.18;
lfo.connect(depth);
depth.connect(robot.gain);
src.connect(hp);
hp.connect(lp);
lp.connect(dry);
dry.connect(this.audioManager.compressor);
lp.connect(shape);
shape.connect(robot);
robot.connect(robotOut);
robotOut.connect(this.audioManager.compressor);
lfo.start();
a.addEventListener(
'ended',
() => {
try {
lfo.stop();
} catch (_) {}
},
{ once: true }
);
a.addEventListener(
'pause',
() => {
if (a.ended) return;
try {
lfo.stop();
} catch (_) {}
},
{ once: true }
);
await a.play();
}
async speak(text) {
if (!text) return;
const p = this.profile(),
amount = this.roboticAmount();
if (this.ttsReady && this.kokoro) {
try {
const voice = this.ui.voice?.value || p.voice;
const raw = await this.kokoro.generate(text, { voice, speed: p.speed || 0.92 });
const blob = raw.toBlob();
if (this.audioUrl) URL.revokeObjectURL(this.audioUrl);
this.audioUrl = URL.createObjectURL(blob);
if (this.currentAudio) {
this.currentAudio.pause();
this.currentAudio = null;
}
const a = new Audio(this.audioUrl);
this.currentAudio = a;
await this.playProcessedSpeech(a);
return;
} catch (e) {
console.warn('Kokoro playback failed, using browser fallback', e);
}
}
if ('speechSynthesis' in window) {
try {
speechSynthesis.cancel();
const u = new SpeechSynthesisUtterance(text);
u.rate = (p.speed || 0.92) * (1 - amount * 0.04);
u.pitch = 0.88 - amount * 0.09;
u.volume = 0.9;
const fnc = window.fncSystem;
if (fnc) fnc.hold('speech', true);
u.onend = u.onerror = () => {
if (fnc) fnc.hold('speech', false);
};
speechSynthesis.speak(u);
} catch (e) {
console.warn(e);
if (window.fncSystem) window.fncSystem.hold('speech', false);
}
}
}
setAuto(on) {
clearTimeout(this.autoTimer);
this.autoTimer = null;
if (on) this.scheduleNext();
}
scheduleNext() {
clearTimeout(this.autoTimer);
this.autoTimer = null;
if (!this.ui.auto?.checked || !this.engine) return;
const b = Number(this.ui.cadence.value || 90),
secs = b + Math.random() * b;
this.autoTimer = setTimeout(async () => {
if (this.isPlaying()) await this.generateAmbientMoment('autonomous-watch');
else this.scheduleNext();
}, secs * 1000);
}
stop() {
clearTimeout(this.autoTimer);
this.autoTimer = null;
if (this.currentAudio) {
this.currentAudio.pause();
this.currentAudio = null;
}
if ('speechSynthesis' in window) speechSynthesis.cancel();
}
}
window.XZBTGenerativeExperience = XZBTGenerativeExperience;