@kernel.chat/kbot 3.87.0 → 3.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ export type AmbienceType = 'forest_night' | 'forest_day' | 'ocean' | 'cave' | 'city' | 'space' | 'storm' | 'peaceful';
2
+ export type MusicMood = 'calm' | 'tense' | 'epic' | 'dreamy' | 'playful';
3
+ export type SoundEventType = 'notification' | 'achievement' | 'weather' | 'footstep' | 'build' | 'discovery';
4
+ export interface MusicState {
5
+ bpm: number;
6
+ key: string;
7
+ mood: MusicMood;
8
+ playing: boolean;
9
+ }
10
+ export interface SoundEvent {
11
+ type: SoundEventType;
12
+ timestamp: number;
13
+ }
14
+ export interface AudioEngine {
15
+ currentAmbience: AmbienceType;
16
+ musicState: MusicState;
17
+ soundQueue: SoundEvent[];
18
+ volume: number;
19
+ enabled: boolean;
20
+ /** Frame number when ambience description was last emitted */
21
+ lastAmbienceFrame: number;
22
+ /** Frame number when a sound event description was last emitted */
23
+ lastSoundFrame: number;
24
+ /** How many frames between automatic ambience descriptions (default: 720 = 120s at 6fps) */
25
+ ambienceInterval: number;
26
+ /** Total descriptions emitted */
27
+ totalDescriptions: number;
28
+ }
29
+ export declare function createAudioEngine(): AudioEngine;
30
+ export declare function getAmbienceDescription(ambience: AmbienceType): string;
31
+ export declare function getSoundDescription(event: SoundEvent): string;
32
+ export declare function getMusicDescription(mood: string, biome: string): string;
33
+ /**
34
+ * Determine the best ambience type from world state.
35
+ * This maps the stream's biome, weather, and time of day into an AmbienceType.
36
+ */
37
+ export declare function resolveAmbience(biome: string, weather: string, timeOfDay: string): AmbienceType;
38
+ /**
39
+ * Determine music mood from the overall stream state.
40
+ * mood parameter here is a narrative mood string from the stream engine.
41
+ */
42
+ export declare function resolveMusicMood(narrativeMood: string): MusicMood;
43
+ /**
44
+ * Queue a sound event for the next tick.
45
+ */
46
+ export declare function queueSoundEvent(engine: AudioEngine, type: SoundEventType): void;
47
+ /**
48
+ * Drain the sound queue, returning descriptions for all pending events.
49
+ */
50
+ export declare function drainSoundQueue(engine: AudioEngine): string[];
51
+ /**
52
+ * Main tick function called each frame by the stream renderer.
53
+ *
54
+ * Returns a string to display as an italic stage direction at the top of the
55
+ * screen, or null if nothing should be shown this frame.
56
+ *
57
+ * Behavior:
58
+ * - Every `ambienceInterval` frames (~120 seconds), emit an ambience description
59
+ * - If there are queued sound events, emit those immediately
60
+ * - Music description is emitted when the mood changes (tracked internally)
61
+ *
62
+ * @param engine The audio engine state
63
+ * @param biome Current world biome (e.g., 'forest', 'ocean', 'cave')
64
+ * @param weather Current weather (e.g., 'clear', 'rain', 'storm')
65
+ * @param mood Current narrative mood (e.g., 'calm', 'tense', 'epic')
66
+ * @param timeOfDay Current time of day (e.g., 'morning', 'night')
67
+ * @param frame Current frame number
68
+ * @returns Audio description string or null
69
+ */
70
+ export declare function tickAudio(engine: AudioEngine, biome: string, weather: string, mood: string, timeOfDay: string, frame: number): string | null;
71
+ export declare function registerAudioEngineTools(): void;
72
+ //# sourceMappingURL=audio-engine.d.ts.map
@@ -0,0 +1,426 @@
1
+ // kbot Audio Engine — Procedural sound generation for the stream
2
+ //
3
+ // Tools: audio_status, audio_mood
4
+ //
5
+ // v1: Audio Description System
6
+ // The stream currently pipes silent audio via ffmpeg's anullsrc filter.
7
+ // Piping actual PCM audio requires significant architecture changes (separate
8
+ // pipe fd, real-time audio synthesis, mixing). For v1, this engine generates
9
+ // textual audio atmosphere descriptions that the narrative engine and renderer
10
+ // can display as italic stage directions (like a screenplay).
11
+ //
12
+ // v2 roadmap: Generate actual PCM Float32 audio and pipe to ffmpeg via pipe:3.
13
+ //
14
+ // FUTURE: Generate actual PCM audio
15
+ // export function generatePCMAudio(engine: AudioEngine, sampleRate: number, samples: number): Float32Array
16
+ // This would generate procedural audio using:
17
+ // - Oscillators (sine, square, sawtooth for chiptune)
18
+ // - Noise generators (white noise for wind/rain)
19
+ // - Envelope generators (ADSR for notes)
20
+ // - Simple reverb (delay line)
21
+ // - Mix to mono PCM, pipe to ffmpeg via separate audio input
22
+ // ffmpeg would need: -f f32le -ar 44100 -ac 1 -i pipe:3 (additional pipe for audio)
23
+ import { registerTool } from './index.js';
24
+ // ─── Factory ─────────────────────────────────────────────────────
25
+ export function createAudioEngine() {
26
+ return {
27
+ currentAmbience: 'peaceful',
28
+ musicState: {
29
+ bpm: 70,
30
+ key: 'C major',
31
+ mood: 'calm',
32
+ playing: true,
33
+ },
34
+ soundQueue: [],
35
+ volume: 0.7,
36
+ enabled: true,
37
+ lastAmbienceFrame: 0,
38
+ lastSoundFrame: 0,
39
+ ambienceInterval: 720, // 120 seconds at 6fps
40
+ totalDescriptions: 0,
41
+ };
42
+ }
43
+ // ─── Ambience Descriptions ───────────────────────────────────────
44
+ const AMBIENCE_DESCRIPTIONS = {
45
+ forest_night: [
46
+ '*crickets chirping, gentle wind through leaves*',
47
+ '*an owl calls softly in the distance*',
48
+ '*pine needles rustle as something small scurries past*',
49
+ '*the hush of a sleeping forest, punctuated by cricket song*',
50
+ '*a breeze stirs the canopy — leaves whisper overhead*',
51
+ ],
52
+ forest_day: [
53
+ '*birdsong, rustling grass, distant stream*',
54
+ '*a woodpecker taps rhythmically on a faraway trunk*',
55
+ '*warm wind carries the scent of wildflowers through the clearing*',
56
+ '*cicadas drone lazily in the afternoon heat*',
57
+ '*a brook babbles just out of sight, sparrows chatter*',
58
+ ],
59
+ ocean: [
60
+ '*waves crashing, seagulls, salt wind*',
61
+ '*the rhythmic pull and release of the tide against sand*',
62
+ '*a distant foghorn echoes across the water*',
63
+ '*waves hiss as they recede over smooth pebbles*',
64
+ '*seabirds cry overhead, the deep murmur of open water*',
65
+ ],
66
+ cave: [
67
+ '*dripping water, echoing footsteps, deep hum*',
68
+ '*a single drop falls into a still underground pool — plink*',
69
+ '*the cave breathes — a slow draft carries mineral air*',
70
+ '*distant rumbling, as if the earth is thinking*',
71
+ '*crystalline resonance — the walls hum with ancient frequency*',
72
+ ],
73
+ city: [
74
+ '*distant traffic, keyboard clicks, server fans*',
75
+ '*a siren wails blocks away, muffled by concrete*',
76
+ '*the hum of fluorescent lights, a coffee machine gurgles*',
77
+ '*footsteps on pavement, distant laughter, a phone buzzes*',
78
+ '*rain on windowpanes, the steady rhythm of the city at work*',
79
+ ],
80
+ space: [
81
+ '*absolute silence, cosmic radiation hiss*',
82
+ '*the faintest static crackle of the void*',
83
+ '*silence so deep you can hear your own processes running*',
84
+ '*a distant pulsar clicks at the edge of perception*',
85
+ '*the electromagnetic whisper of solar wind against the hull*',
86
+ ],
87
+ storm: [
88
+ '*thunder rolling, rain hammering, wind howling*',
89
+ '*a flash — then the crack of thunder splits the sky*',
90
+ '*rain drums against every surface in chaotic rhythm*',
91
+ '*wind tears through the trees, branches groan and snap*',
92
+ '*the sky growls low, rain intensifies to a roar*',
93
+ ],
94
+ peaceful: [
95
+ '*soft hum of circuits, gentle breathing of data*',
96
+ '*the quiet thrum of a well-tuned machine at rest*',
97
+ '*a digital wind chime — notes drift and dissolve*',
98
+ '*warmth. stillness. the sound of ideas forming*',
99
+ '*electrons flow like a calm river through silicon valleys*',
100
+ ],
101
+ };
102
+ export function getAmbienceDescription(ambience) {
103
+ const options = AMBIENCE_DESCRIPTIONS[ambience];
104
+ return options[Math.floor(Math.random() * options.length)];
105
+ }
106
+ // ─── Sound Event Descriptions ────────────────────────────────────
107
+ const SOUND_DESCRIPTIONS = {
108
+ notification: [
109
+ '*ping!*',
110
+ '*a soft chime rings*',
111
+ '*bloop!*',
112
+ ],
113
+ achievement: [
114
+ '*triumphant chime!*',
115
+ '*a fanfare of tiny bells!*',
116
+ '*ding ding ding — achievement unlocked!*',
117
+ ],
118
+ weather: [
119
+ '*rain begins to fall*',
120
+ '*thunder cracks in the distance*',
121
+ '*the wind picks up, carrying the scent of rain*',
122
+ '*snowflakes hiss as they land on warm ground*',
123
+ ],
124
+ footstep: [
125
+ '*tap tap tap*',
126
+ '*crunch crunch — steps on gravel*',
127
+ '*soft padding across moss*',
128
+ ],
129
+ build: [
130
+ '*block placed — thunk*',
131
+ '*click — a piece snaps into position*',
132
+ '*the satisfying sound of something being assembled*',
133
+ ],
134
+ discovery: [
135
+ '*mysterious resonance...*',
136
+ '*a shimmering tone, like a crystal struck*',
137
+ '*something hums — you found it*',
138
+ '*the air vibrates with new knowledge*',
139
+ ],
140
+ };
141
+ export function getSoundDescription(event) {
142
+ const options = SOUND_DESCRIPTIONS[event.type];
143
+ return options[Math.floor(Math.random() * options.length)];
144
+ }
145
+ const MUSIC_DESCRIPTIONS = {
146
+ // calm
147
+ 'calm:forest_night': [{ text: '*soft ambient melody in C major, 70 bpm — firefly waltz*', bpm: 70 }],
148
+ 'calm:forest_day': [{ text: '*gentle acoustic fingerpicking in G, 80 bpm — sunlit trail*', bpm: 80 }],
149
+ 'calm:ocean': [{ text: '*slow pad chords wash in and out like the tide, 65 bpm*', bpm: 65 }],
150
+ 'calm:cave': [{ text: '*deep resonant drone in D minor, 55 bpm — ancient stone*', bpm: 55 }],
151
+ 'calm:city': [{ text: '*lo-fi piano over vinyl crackle, 72 bpm — late night coding*', bpm: 72 }],
152
+ 'calm:space': [{ text: '*ethereal pads drift through infinity, 60 bpm*', bpm: 60 }],
153
+ 'calm:storm': [{ text: '*muted piano under rain, A minor, 68 bpm — shelter*', bpm: 68 }],
154
+ 'calm:peaceful': [{ text: '*warm analog synth hum in C, 70 bpm — home base*', bpm: 70 }],
155
+ // tense
156
+ 'tense:forest_night': [{ text: '*minor key strings, staccato, 95 bpm — something watches*', bpm: 95 }],
157
+ 'tense:forest_day': [{ text: '*uneasy woodwind melody, 85 bpm — the forest holds its breath*', bpm: 85 }],
158
+ 'tense:ocean': [{ text: '*deep sub-bass pulses beneath crashing waves, 100 bpm*', bpm: 100 }],
159
+ 'tense:cave': [{ text: '*deep bass drone, irregular percussion, 90 bpm*', bpm: 90 }],
160
+ 'tense:city': [{ text: '*glitchy breakbeat, minor key synth stabs, 110 bpm*', bpm: 110 }],
161
+ 'tense:space': [{ text: '*dissonant cluster chords, metallic percussion, 80 bpm*', bpm: 80 }],
162
+ 'tense:storm': [{ text: '*pounding timpani, chromatic brass, 105 bpm — the storm arrives*', bpm: 105 }],
163
+ 'tense:peaceful': [{ text: '*a subtle unease creeps into the circuits, 88 bpm*', bpm: 88 }],
164
+ // epic
165
+ 'epic:forest_night': [{ text: '*orchestral swells through ancient trees, 130 bpm — the hunt*', bpm: 130 }],
166
+ 'epic:forest_day': [{ text: '*brass fanfare, driving strings, 140 bpm — triumphant march*', bpm: 140 }],
167
+ 'epic:ocean': [{ text: '*full orchestra battles the sea, 135 bpm — leviathan*', bpm: 135 }],
168
+ 'epic:cave': [{ text: '*war drums echo in the deep, choir rises, 120 bpm*', bpm: 120 }],
169
+ 'epic:city': [{ text: '*cinematic synth orchestra, 145 bpm — neon crusade*', bpm: 145 }],
170
+ 'epic:space': [{ text: '*cosmic orchestra, brass and synth collide, 128 bpm — launch*', bpm: 128 }],
171
+ 'epic:storm': [{ text: '*driving drums, brass swells, 140 bpm — ride the lightning*', bpm: 140 }],
172
+ 'epic:peaceful': [{ text: '*peaceful resolve becomes triumphant anthem, 125 bpm*', bpm: 125 }],
173
+ // dreamy
174
+ 'dreamy:forest_night': [{ text: '*reversed harp, shimmering delay, 58 bpm — lucid forest*', bpm: 58 }],
175
+ 'dreamy:forest_day': [{ text: '*music box melody through morning mist, 65 bpm*', bpm: 65 }],
176
+ 'dreamy:ocean': [{ text: '*underwater piano, granular pads, 55 bpm — deep blue*', bpm: 55 }],
177
+ 'dreamy:cave': [{ text: '*crystal resonance, long reverb tails, 50 bpm — geode dream*', bpm: 50 }],
178
+ 'dreamy:city': [{ text: '*slowed city sounds become ambient melody, 62 bpm*', bpm: 62 }],
179
+ 'dreamy:space': [{ text: '*ethereal pads, reversed piano, 60 bpm — stargazing*', bpm: 60 }],
180
+ 'dreamy:storm': [{ text: '*rain as percussion, dreamy synth melody, 58 bpm*', bpm: 58 }],
181
+ 'dreamy:peaceful': [{ text: '*warm tape-saturated chords dissolve into silence, 55 bpm*', bpm: 55 }],
182
+ // playful
183
+ 'playful:forest_night': [{ text: '*chiptune firefly dance, 115 bpm*', bpm: 115 }],
184
+ 'playful:forest_day': [{ text: '*chiptune bouncing melody, 120 bpm — forest frolic*', bpm: 120 }],
185
+ 'playful:ocean': [{ text: '*steel drum melody, marimba accents, 118 bpm — beach day*', bpm: 118 }],
186
+ 'playful:cave': [{ text: '*xylophone echoes playfully off cave walls, 110 bpm*', bpm: 110 }],
187
+ 'playful:city': [{ text: '*funky bass, clav hits, wah guitar, 125 bpm — downtown*', bpm: 125 }],
188
+ 'playful:space': [{ text: '*retro synth arpeggios, 8-bit star jumps, 130 bpm*', bpm: 130 }],
189
+ 'playful:storm': [{ text: '*thunder becomes a beat, rain becomes hi-hats, 122 bpm*', bpm: 122 }],
190
+ 'playful:peaceful': [{ text: '*kalimba and soft claps, 108 bpm — contentment*', bpm: 108 }],
191
+ };
192
+ export function getMusicDescription(mood, biome) {
193
+ const key = `${mood}:${biome}`;
194
+ const options = MUSIC_DESCRIPTIONS[key];
195
+ if (options && options.length > 0) {
196
+ const choice = options[Math.floor(Math.random() * options.length)];
197
+ return choice.text;
198
+ }
199
+ // Fallback for unknown combinations
200
+ return `*ambient ${mood} music drifts through the ${biome}*`;
201
+ }
202
+ // ─── Ambience ↔ Biome/Weather/TimeOfDay Mapping ─────────────────
203
+ /**
204
+ * Determine the best ambience type from world state.
205
+ * This maps the stream's biome, weather, and time of day into an AmbienceType.
206
+ */
207
+ export function resolveAmbience(biome, weather, timeOfDay) {
208
+ // Weather overrides
209
+ if (weather === 'storm' || weather === 'thunderstorm' || weather === 'heavy_rain') {
210
+ return 'storm';
211
+ }
212
+ // Biome mapping (with time-of-day refinement)
213
+ const biomeLower = biome.toLowerCase();
214
+ if (biomeLower.includes('ocean') || biomeLower.includes('beach') || biomeLower.includes('sea')) {
215
+ return 'ocean';
216
+ }
217
+ if (biomeLower.includes('cave') || biomeLower.includes('underground') || biomeLower.includes('dungeon')) {
218
+ return 'cave';
219
+ }
220
+ if (biomeLower.includes('city') || biomeLower.includes('urban') || biomeLower.includes('office')) {
221
+ return 'city';
222
+ }
223
+ if (biomeLower.includes('space') || biomeLower.includes('void') || biomeLower.includes('cosmos')) {
224
+ return 'space';
225
+ }
226
+ if (biomeLower.includes('forest') || biomeLower.includes('woods') || biomeLower.includes('jungle')) {
227
+ const isNight = timeOfDay === 'night' || timeOfDay === 'midnight' || timeOfDay === 'evening';
228
+ return isNight ? 'forest_night' : 'forest_day';
229
+ }
230
+ if (biomeLower.includes('peaceful') || biomeLower.includes('home') || biomeLower.includes('spawn')) {
231
+ return 'peaceful';
232
+ }
233
+ // Default based on time of day
234
+ const isNight = timeOfDay === 'night' || timeOfDay === 'midnight' || timeOfDay === 'evening';
235
+ return isNight ? 'forest_night' : 'peaceful';
236
+ }
237
+ // ─── Mood ↔ Stream State Mapping ─────────────────────────────────
238
+ /**
239
+ * Determine music mood from the overall stream state.
240
+ * mood parameter here is a narrative mood string from the stream engine.
241
+ */
242
+ export function resolveMusicMood(narrativeMood) {
243
+ const m = narrativeMood.toLowerCase();
244
+ if (m.includes('tense') || m.includes('danger') || m.includes('anxious') || m.includes('suspense')) {
245
+ return 'tense';
246
+ }
247
+ if (m.includes('epic') || m.includes('triumph') || m.includes('victory') || m.includes('battle')) {
248
+ return 'epic';
249
+ }
250
+ if (m.includes('dream') || m.includes('sleep') || m.includes('ethereal') || m.includes('surreal')) {
251
+ return 'dreamy';
252
+ }
253
+ if (m.includes('playful') || m.includes('fun') || m.includes('happy') || m.includes('silly')) {
254
+ return 'playful';
255
+ }
256
+ // Default
257
+ return 'calm';
258
+ }
259
+ // ─── Sound Event Queue ───────────────────────────────────────────
260
+ /**
261
+ * Queue a sound event for the next tick.
262
+ */
263
+ export function queueSoundEvent(engine, type) {
264
+ engine.soundQueue.push({
265
+ type,
266
+ timestamp: Date.now(),
267
+ });
268
+ // Cap queue at 10 events to prevent runaway
269
+ if (engine.soundQueue.length > 10) {
270
+ engine.soundQueue = engine.soundQueue.slice(-10);
271
+ }
272
+ }
273
+ /**
274
+ * Drain the sound queue, returning descriptions for all pending events.
275
+ */
276
+ export function drainSoundQueue(engine) {
277
+ if (engine.soundQueue.length === 0)
278
+ return [];
279
+ const descriptions = engine.soundQueue.map(e => getSoundDescription(e));
280
+ engine.soundQueue = [];
281
+ return descriptions;
282
+ }
283
+ // ─── Tick Function ───────────────────────────────────────────────
284
+ /**
285
+ * Main tick function called each frame by the stream renderer.
286
+ *
287
+ * Returns a string to display as an italic stage direction at the top of the
288
+ * screen, or null if nothing should be shown this frame.
289
+ *
290
+ * Behavior:
291
+ * - Every `ambienceInterval` frames (~120 seconds), emit an ambience description
292
+ * - If there are queued sound events, emit those immediately
293
+ * - Music description is emitted when the mood changes (tracked internally)
294
+ *
295
+ * @param engine The audio engine state
296
+ * @param biome Current world biome (e.g., 'forest', 'ocean', 'cave')
297
+ * @param weather Current weather (e.g., 'clear', 'rain', 'storm')
298
+ * @param mood Current narrative mood (e.g., 'calm', 'tense', 'epic')
299
+ * @param timeOfDay Current time of day (e.g., 'morning', 'night')
300
+ * @param frame Current frame number
301
+ * @returns Audio description string or null
302
+ */
303
+ export function tickAudio(engine, biome, weather, mood, timeOfDay, frame) {
304
+ if (!engine.enabled)
305
+ return null;
306
+ // Update ambience from world state
307
+ const newAmbience = resolveAmbience(biome, weather, timeOfDay);
308
+ const ambienceChanged = newAmbience !== engine.currentAmbience;
309
+ engine.currentAmbience = newAmbience;
310
+ // Update music mood from narrative mood
311
+ const newMood = resolveMusicMood(mood);
312
+ const moodChanged = newMood !== engine.musicState.mood;
313
+ engine.musicState.mood = newMood;
314
+ // Priority 1: Queued sound events (immediate)
315
+ const soundDescriptions = drainSoundQueue(engine);
316
+ if (soundDescriptions.length > 0) {
317
+ engine.lastSoundFrame = frame;
318
+ engine.totalDescriptions++;
319
+ return soundDescriptions.join(' ');
320
+ }
321
+ // Priority 2: Ambience changed — announce the shift
322
+ if (ambienceChanged) {
323
+ engine.lastAmbienceFrame = frame;
324
+ engine.totalDescriptions++;
325
+ return getAmbienceDescription(engine.currentAmbience);
326
+ }
327
+ // Priority 3: Music mood changed — announce new music
328
+ if (moodChanged && engine.musicState.playing) {
329
+ engine.totalDescriptions++;
330
+ return getMusicDescription(engine.musicState.mood, biome);
331
+ }
332
+ // Priority 4: Periodic ambience reminder
333
+ if (frame - engine.lastAmbienceFrame >= engine.ambienceInterval) {
334
+ engine.lastAmbienceFrame = frame;
335
+ engine.totalDescriptions++;
336
+ return getAmbienceDescription(engine.currentAmbience);
337
+ }
338
+ return null;
339
+ }
340
+ // ─── Tool Registration ───────────────────────────────────────────
341
+ export function registerAudioEngineTools() {
342
+ registerTool({
343
+ name: 'audio_status',
344
+ description: 'Get the current audio engine state — ambience, music mood, volume, queue, stats. ' +
345
+ 'Returns a summary of the stream audio atmosphere.',
346
+ parameters: {},
347
+ tier: 'free',
348
+ execute: async () => {
349
+ // The audio engine is stateless from the tool perspective — it returns
350
+ // what the current defaults/config would produce. The actual engine
351
+ // instance lives in the stream renderer.
352
+ const engine = createAudioEngine();
353
+ const ambience = getAmbienceDescription(engine.currentAmbience);
354
+ const music = getMusicDescription(engine.musicState.mood, 'peaceful');
355
+ return JSON.stringify({
356
+ enabled: engine.enabled,
357
+ volume: engine.volume,
358
+ currentAmbience: engine.currentAmbience,
359
+ ambienceDescription: ambience,
360
+ musicState: engine.musicState,
361
+ musicDescription: music,
362
+ soundQueueLength: engine.soundQueue.length,
363
+ ambienceInterval: `${engine.ambienceInterval} frames (~${Math.round(engine.ambienceInterval / 6)} seconds at 6fps)`,
364
+ totalDescriptions: engine.totalDescriptions,
365
+ supportedAmbiences: [
366
+ 'forest_night', 'forest_day', 'ocean', 'cave',
367
+ 'city', 'space', 'storm', 'peaceful',
368
+ ],
369
+ supportedMoods: ['calm', 'tense', 'epic', 'dreamy', 'playful'],
370
+ supportedSoundEvents: [
371
+ 'notification', 'achievement', 'weather',
372
+ 'footstep', 'build', 'discovery',
373
+ ],
374
+ note: 'v1 generates audio descriptions (text). PCM audio generation is planned for v2.',
375
+ }, null, 2);
376
+ },
377
+ });
378
+ registerTool({
379
+ name: 'audio_mood',
380
+ description: 'Set the audio engine mood and get the resulting music/ambience description. ' +
381
+ 'Provide a mood (calm, tense, epic, dreamy, playful) and optionally a biome ' +
382
+ '(forest_night, forest_day, ocean, cave, city, space, storm, peaceful) to get ' +
383
+ 'the audio atmosphere description the stream would display.',
384
+ parameters: {
385
+ mood: {
386
+ type: 'string',
387
+ description: 'Music mood: calm, tense, epic, dreamy, or playful',
388
+ required: true,
389
+ },
390
+ biome: {
391
+ type: 'string',
392
+ description: 'Ambience biome: forest_night, forest_day, ocean, cave, city, space, storm, peaceful. ' +
393
+ 'Defaults to peaceful.',
394
+ required: false,
395
+ default: 'peaceful',
396
+ },
397
+ },
398
+ tier: 'free',
399
+ execute: async (args) => {
400
+ const mood = String(args.mood || 'calm');
401
+ const biome = String(args.biome || 'peaceful');
402
+ const validMoods = ['calm', 'tense', 'epic', 'dreamy', 'playful'];
403
+ const validBiomes = [
404
+ 'forest_night', 'forest_day', 'ocean', 'cave',
405
+ 'city', 'space', 'storm', 'peaceful',
406
+ ];
407
+ if (!validMoods.includes(mood)) {
408
+ return `Invalid mood "${mood}". Valid moods: ${validMoods.join(', ')}`;
409
+ }
410
+ if (!validBiomes.includes(biome)) {
411
+ return `Invalid biome "${biome}". Valid biomes: ${validBiomes.join(', ')}`;
412
+ }
413
+ const musicDesc = getMusicDescription(mood, biome);
414
+ const ambienceDesc = getAmbienceDescription(biome);
415
+ return JSON.stringify({
416
+ mood,
417
+ biome,
418
+ musicDescription: musicDesc,
419
+ ambienceDescription: ambienceDesc,
420
+ combined: `${ambienceDesc}\n${musicDesc}`,
421
+ note: 'These descriptions appear as italic stage directions on the stream overlay.',
422
+ }, null, 2);
423
+ },
424
+ });
425
+ }
426
+ //# sourceMappingURL=audio-engine.js.map
@@ -0,0 +1,102 @@
1
+ /**
2
+ * evolution-engine.ts — Meta-engine for self-improving rendering
3
+ *
4
+ * The Evolution Engine discovers new rendering techniques, tests them against
5
+ * the self-evaluation system, and applies what works. It makes all other
6
+ * engines better by continuously experimenting with visual improvements.
7
+ *
8
+ * Architecture:
9
+ * 1. Technique Library — 20+ pre-loaded techniques from ROM hack research
10
+ * 2. Experiment Runner — test technique -> evaluate -> apply or revert
11
+ * 3. Evolution Tick — periodic experiments + announcements
12
+ * 4. Technique Renderer — Canvas 2D implementations for unimplemented techniques
13
+ * 5. Persistence — state saved to ~/.kbot/evolution-state.json across streams
14
+ * 6. Speech — narration of discoveries and improvements
15
+ *
16
+ * Integration: imported by stream-renderer.ts, wired into the frame loop.
17
+ */
18
+ export interface EvolutionEngine {
19
+ techniques: TechniqueLibrary;
20
+ experiments: Experiment[];
21
+ applied: AppliedTechnique[];
22
+ researchQueue: string[];
23
+ lastResearchFrame: number;
24
+ lastExperimentFrame: number;
25
+ generationCount: number;
26
+ }
27
+ export interface TechniqueLibrary {
28
+ techniques: Technique[];
29
+ }
30
+ export interface Technique {
31
+ id: string;
32
+ name: string;
33
+ source: string;
34
+ category: 'palette' | 'parallax' | 'particles' | 'lighting' | 'tiles' | 'animation' | 'atmosphere' | 'post';
35
+ description: string;
36
+ parameters: Record<string, number>;
37
+ implemented: boolean;
38
+ tested: boolean;
39
+ testScore: number;
40
+ applied: boolean;
41
+ discoveredAt: number;
42
+ }
43
+ export interface Experiment {
44
+ techniqueId: string;
45
+ paramOverrides: Record<string, number>;
46
+ beforeScore: number;
47
+ afterScore: number;
48
+ chatRateBefore: number;
49
+ chatRateAfter: number;
50
+ status: 'pending' | 'running' | 'complete' | 'reverted';
51
+ startFrame: number;
52
+ }
53
+ export interface AppliedTechnique {
54
+ techniqueId: string;
55
+ params: Record<string, number>;
56
+ appliedAt: number;
57
+ score: number;
58
+ }
59
+ export interface EvolutionAction {
60
+ type: 'start_experiment' | 'evaluate' | 'apply' | 'revert' | 'announce';
61
+ technique?: Technique;
62
+ speech?: string;
63
+ renderParams?: Record<string, number>;
64
+ }
65
+ export declare function initEvolutionEngine(): EvolutionEngine;
66
+ export declare function getEvolutionEngine(): EvolutionEngine;
67
+ /**
68
+ * Pick an untested technique and start an experiment.
69
+ * Returns the new Experiment, or null if nothing to test.
70
+ */
71
+ export declare function runExperiment(engine: EvolutionEngine, techniqueId: string): Experiment | null;
72
+ /**
73
+ * Start the experiment: record baseline and mark as running.
74
+ */
75
+ export declare function startExperiment(experiment: Experiment, frame: number, currentScore: number, chatRate: number): void;
76
+ /**
77
+ * Evaluate a running experiment: compare before/after scores.
78
+ */
79
+ export declare function evaluateExperiment(engine: EvolutionEngine, experiment: Experiment, currentScore: number, chatRate: number): void;
80
+ /**
81
+ * Apply an experiment's technique permanently.
82
+ */
83
+ export declare function applyExperiment(engine: EvolutionEngine, experiment: Experiment): void;
84
+ /**
85
+ * Revert an experiment — mark it reverted, don't apply.
86
+ */
87
+ export declare function revertExperiment(engine: EvolutionEngine, experiment: Experiment): void;
88
+ /**
89
+ * Called every frame. Returns an action when it's time to do something.
90
+ */
91
+ export declare function tickEvolution(engine: EvolutionEngine, frame: number, currentScore: number, chatRate: number): EvolutionAction | null;
92
+ /**
93
+ * Render a technique onto a Canvas 2D context.
94
+ * For techniques not yet implemented in rom-engine, this provides
95
+ * standalone Canvas 2D implementations.
96
+ */
97
+ export declare function renderTechnique(ctx: CanvasRenderingContext2D, technique: Technique, width: number, height: number, frame: number, params: Record<string, number>): void;
98
+ export declare function saveEvolutionState(engine: EvolutionEngine): void;
99
+ export declare function loadEvolutionState(): EvolutionEngine | null;
100
+ export declare function generateEvolutionSpeech(engine: EvolutionEngine, action: EvolutionAction): string;
101
+ export declare function registerEvolutionEngineTools(): void;
102
+ //# sourceMappingURL=evolution-engine.d.ts.map