@kernel.chat/kbot 3.88.0 → 3.94.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.
- package/dist/tools/audio-engine.d.ts +72 -0
- package/dist/tools/audio-engine.js +426 -0
- package/dist/tools/coordination-engine.d.ts +127 -0
- package/dist/tools/coordination-engine.js +543 -0
- package/dist/tools/evolution-engine.d.ts +102 -0
- package/dist/tools/evolution-engine.js +746 -0
- package/dist/tools/foundation-engines.d.ts +111 -0
- package/dist/tools/foundation-engines.js +520 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/living-world.d.ts +161 -0
- package/dist/tools/living-world.js +1054 -0
- package/dist/tools/narrative-engine.d.ts +58 -0
- package/dist/tools/narrative-engine.js +681 -0
- package/dist/tools/render-engine.js +5 -5
- package/dist/tools/research-engine.d.ts +58 -0
- package/dist/tools/research-engine.js +550 -0
- package/dist/tools/rom-engine.d.ts +130 -0
- package/dist/tools/rom-engine.js +1208 -0
- package/dist/tools/social-engine.d.ts +100 -0
- package/dist/tools/social-engine.js +540 -0
- package/dist/tools/sprite-engine.js +40 -26
- package/dist/tools/stream-brain.js +4 -4
- package/dist/tools/stream-intelligence.d.ts +6 -0
- package/dist/tools/stream-intelligence.js +239 -49
- package/dist/tools/stream-renderer.js +805 -639
- package/dist/tools/stream-self-eval.d.ts +96 -0
- package/dist/tools/stream-self-eval.js +764 -0
- package/dist/tools/tile-world.d.ts +40 -0
- package/dist/tools/tile-world.js +1070 -0
- package/package.json +1 -1
|
@@ -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,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* coordination-engine.ts — Master engine that manages all other engines.
|
|
3
|
+
*
|
|
4
|
+
* The Coordination Engine prevents engines from stepping on each other by:
|
|
5
|
+
* 1. Priority-based speech queue — only the most important message shows
|
|
6
|
+
* 2. Mood stack — highest priority mood wins
|
|
7
|
+
* 3. Blackboard — engines communicate through key/value store with TTLs
|
|
8
|
+
* 4. Engine status tracking — self-reported health from every engine
|
|
9
|
+
* 5. Resource budgets — frame timing, speech slot caps
|
|
10
|
+
*
|
|
11
|
+
* Priority levels (by convention):
|
|
12
|
+
* 95: System alerts (stream offline, engine crash)
|
|
13
|
+
* 90: Follower/subscriber celebrations
|
|
14
|
+
* 80: Chat responses (someone talked to kbot)
|
|
15
|
+
* 70: Evolution discoveries (new technique applied)
|
|
16
|
+
* 60: Brain tool execution results
|
|
17
|
+
* 50: Narrative observations
|
|
18
|
+
* 40: Exploration narration (walking, examining)
|
|
19
|
+
* 30: Autonomous idle behavior
|
|
20
|
+
* 20: Audio atmosphere descriptions
|
|
21
|
+
* 10: Inner thoughts
|
|
22
|
+
*
|
|
23
|
+
* Integration: imported by stream-renderer.ts, wired into the frame loop.
|
|
24
|
+
* Does NOT import or modify any other engine file.
|
|
25
|
+
*/
|
|
26
|
+
export interface CoordinationEngine {
|
|
27
|
+
speechQueue: SpeechItem[];
|
|
28
|
+
currentSpeech: SpeechItem | null;
|
|
29
|
+
currentSpeechExpiry: number;
|
|
30
|
+
moodStack: MoodRequest[];
|
|
31
|
+
engineStatus: Map<string, EngineStatus>;
|
|
32
|
+
blackboard: Map<string, BlackboardMessage>;
|
|
33
|
+
frameCount: number;
|
|
34
|
+
resourceBudget: ResourceBudget;
|
|
35
|
+
}
|
|
36
|
+
export interface SpeechItem {
|
|
37
|
+
text: string;
|
|
38
|
+
mood: string;
|
|
39
|
+
priority: number;
|
|
40
|
+
duration: number;
|
|
41
|
+
source: string;
|
|
42
|
+
timestamp: number;
|
|
43
|
+
}
|
|
44
|
+
export interface MoodRequest {
|
|
45
|
+
mood: string;
|
|
46
|
+
priority: number;
|
|
47
|
+
source: string;
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
}
|
|
50
|
+
export interface EngineStatus {
|
|
51
|
+
name: string;
|
|
52
|
+
active: boolean;
|
|
53
|
+
lastTick: number;
|
|
54
|
+
ticksPerSecond: number;
|
|
55
|
+
errors: number;
|
|
56
|
+
outputCount: number;
|
|
57
|
+
}
|
|
58
|
+
export interface BlackboardMessage {
|
|
59
|
+
key: string;
|
|
60
|
+
value: unknown;
|
|
61
|
+
source: string;
|
|
62
|
+
timestamp: number;
|
|
63
|
+
ttl: number;
|
|
64
|
+
}
|
|
65
|
+
export interface ResourceBudget {
|
|
66
|
+
frameBudgetMs: number;
|
|
67
|
+
speechSlots: number;
|
|
68
|
+
activeEngines: number;
|
|
69
|
+
}
|
|
70
|
+
export interface CoordinationOutput {
|
|
71
|
+
speech: string | null;
|
|
72
|
+
mood: string;
|
|
73
|
+
shouldWalk: boolean;
|
|
74
|
+
walkTarget: number | null;
|
|
75
|
+
effects: string[];
|
|
76
|
+
announcements: string[];
|
|
77
|
+
}
|
|
78
|
+
export declare function initCoordination(): CoordinationEngine;
|
|
79
|
+
/**
|
|
80
|
+
* Add a speech item to the priority queue.
|
|
81
|
+
* Queue is capped at MAX_SPEECH_QUEUE — lowest priority items are evicted.
|
|
82
|
+
*/
|
|
83
|
+
export declare function queueSpeech(coord: CoordinationEngine, text: string, mood: string, priority: number, duration: number, source: string): void;
|
|
84
|
+
/**
|
|
85
|
+
* Dequeue the highest priority speech when the current one expires.
|
|
86
|
+
* Returns the speech to display or null if nothing is ready.
|
|
87
|
+
*/
|
|
88
|
+
export declare function tickSpeech(coord: CoordinationEngine, frame: number): {
|
|
89
|
+
text: string;
|
|
90
|
+
mood: string;
|
|
91
|
+
} | null;
|
|
92
|
+
/**
|
|
93
|
+
* Request a mood. Highest priority mood wins on resolve.
|
|
94
|
+
* Duration is in frames.
|
|
95
|
+
*/
|
|
96
|
+
export declare function requestMood(coord: CoordinationEngine, mood: string, priority: number, source: string, duration: number): void;
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the current winning mood. Expires stale entries.
|
|
99
|
+
* Returns the mood string of the highest-priority active request,
|
|
100
|
+
* or DEFAULT_MOOD if the stack is empty.
|
|
101
|
+
*/
|
|
102
|
+
export declare function resolveMood(coord: CoordinationEngine, frame: number): string;
|
|
103
|
+
/**
|
|
104
|
+
* Post a message to the blackboard. Engines communicate through here.
|
|
105
|
+
* TTL is in frames. Overwrites existing key from any source.
|
|
106
|
+
*/
|
|
107
|
+
export declare function postToBlackboard(coord: CoordinationEngine, key: string, value: unknown, source: string, ttl: number): void;
|
|
108
|
+
/**
|
|
109
|
+
* Read the latest value for a key from the blackboard.
|
|
110
|
+
* Returns undefined if the key does not exist or has expired.
|
|
111
|
+
*/
|
|
112
|
+
export declare function readBlackboard(coord: CoordinationEngine, key: string): unknown;
|
|
113
|
+
/**
|
|
114
|
+
* Engines self-report their health status each tick.
|
|
115
|
+
*/
|
|
116
|
+
export declare function reportEngineStatus(coord: CoordinationEngine, name: string, active: boolean, errors: number): void;
|
|
117
|
+
/**
|
|
118
|
+
* Master coordination tick. Called once per frame.
|
|
119
|
+
* Returns what should be displayed/acted on this frame.
|
|
120
|
+
*/
|
|
121
|
+
export declare function tickCoordination(coord: CoordinationEngine, frame: number): CoordinationOutput;
|
|
122
|
+
export declare function serializeCoordination(coord: CoordinationEngine): string;
|
|
123
|
+
export declare function deserializeCoordination(json: string): CoordinationEngine;
|
|
124
|
+
/** Reset the singleton (for testing or re-init) */
|
|
125
|
+
export declare function resetCoordination(): void;
|
|
126
|
+
export declare function registerCoordinationEngineTools(): void;
|
|
127
|
+
//# sourceMappingURL=coordination-engine.d.ts.map
|