kiran 1.0.0

Kiran — AI-native game engine for AGNOS
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
//! Voice synthesis via svara, shabda, and prani
//!
//! Bridges the AGNOS voice stack with kiran's ECS:
//! - **svara** — Formant and vocal synthesis (glottal source, vocal tract, prosody)
//! - **shabda** — Grapheme-to-phoneme conversion (text → phoneme sequences)
//! - **prani** — Creature vocal synthesis (species-specific voices, emotion, fatigue)
//!
//! Core types:
//! - [`VoiceSource`] component for humanoid vocal synthesis
//! - [`CreatureVoiceSource`] component for creature vocalizations
//! - [`SpeechRequest`] event for triggering text-to-speech

use serde::{Deserialize, Serialize};

use crate::world::{Entity, World};

// ---------------------------------------------------------------------------
// svara — formant and vocal synthesis
// ---------------------------------------------------------------------------

/// Formant filter banks (resonant filtering for vowel/consonant shaping).
pub use svara::formant;
/// Glottal source models (pulse generation for voice excitation).
pub use svara::glottal;
/// Level-of-detail quality settings for synthesis.
pub use svara::lod as svara_lod;
/// Phoneme definitions and classification.
pub use svara::phoneme;
/// Synthesis thread pool.
pub use svara::pool as svara_pool;
/// Prosody contours (pitch, stress, intonation patterns).
pub use svara::prosody;
/// Batch rendering of phoneme sequences to audio buffers.
pub use svara::render as svara_render;
/// Phoneme sequencing and timing.
pub use svara::sequence;
/// Spectral analysis utilities.
pub use svara::spectral as svara_spectral;
/// Vocal tract modeling (nasal coupling, tract length, articulatory parameters).
pub use svara::tract;
/// Formant trajectory planning (smooth transitions between targets).
pub use svara::trajectory;
/// Voice profiles (speaker identity, effort, quality).
pub use svara::voice as svara_voice;

// ---------------------------------------------------------------------------
// shabda — grapheme-to-phoneme
// ---------------------------------------------------------------------------

/// G2P conversion engine (text → phoneme sequences).
pub use shabda::engine as g2p_engine;
/// Heteronym resolution (context-dependent pronunciation).
pub use shabda::heteronym;
/// Text normalization (numbers, abbreviations, punctuation).
pub use shabda::normalize;
/// Timing profiles for phoneme duration.
pub use shabda::prosody as g2p_prosody;
/// Phoneme rule sets.
pub use shabda::rules as g2p_rules;
/// SSML parsing for marked-up speech input.
pub use shabda::ssml;
/// Syllable segmentation.
pub use shabda::syllable;

// ---------------------------------------------------------------------------
// prani — creature vocal synthesis
// ---------------------------------------------------------------------------

/// Emotion state for vocal modulation.
pub use prani::emotion as creature_emotion;
/// Vocal fatigue modeling.
pub use prani::fatigue;
/// Voice presets for common species.
pub use prani::preset;
/// Call sequencing (bouts, phrases, patterns).
pub use prani::sequence as creature_sequence;
/// Species definitions and vocal tract parameters.
pub use prani::species;
/// Creature vocal tract modeling.
pub use prani::tract as creature_tract;
/// Vocalization types and call intents.
pub use prani::vocalization;
/// Creature voice configuration and synthesis.
pub use prani::voice as creature_voice;

// ---------------------------------------------------------------------------
// Voice source component (humanoid)
// ---------------------------------------------------------------------------

/// Humanoid voice source — text-to-speech driven by shabda + svara.
///
/// Attach to an NPC entity. When a [`SpeechRequest`] targets this entity,
/// the text is converted to phonemes (shabda) and synthesized (svara).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceSource {
    /// Speaker identity name (selects voice profile).
    pub profile_name: String,
    /// Speaking rate multiplier (1.0 = normal).
    pub rate: f32,
    /// Pitch shift in semitones (0.0 = natural).
    pub pitch_shift: f32,
    /// Output volume.
    pub volume: f32,
    /// Whether this voice is currently speaking.
    #[serde(skip)]
    pub speaking: bool,
}

impl VoiceSource {
    /// Create a new voice source with the given profile name.
    ///
    /// # Examples
    ///
    /// ```
    /// # #[cfg(feature = "voice")] {
    /// use kiran::voice::VoiceSource;
    ///
    /// let voice = VoiceSource::new("narrator");
    /// assert_eq!(voice.profile_name, "narrator");
    /// assert_eq!(voice.rate, 1.0);
    /// # }
    /// ```
    pub fn new(profile_name: impl Into<String>) -> Self {
        Self {
            profile_name: profile_name.into(),
            rate: 1.0,
            pitch_shift: 0.0,
            volume: 1.0,
            speaking: false,
        }
    }

    /// Set the speaking rate.
    pub fn with_rate(mut self, rate: f32) -> Self {
        self.rate = rate;
        self
    }

    /// Set the pitch shift in semitones.
    pub fn with_pitch_shift(mut self, semitones: f32) -> Self {
        self.pitch_shift = semitones;
        self
    }

    /// Set the output volume.
    pub fn with_volume(mut self, volume: f32) -> Self {
        self.volume = volume;
        self
    }
}

impl Default for VoiceSource {
    fn default() -> Self {
        Self::new("default")
    }
}

// ---------------------------------------------------------------------------
// Creature voice source component
// ---------------------------------------------------------------------------

/// Creature voice source — species-specific vocalizations driven by prani.
///
/// Attach to a creature entity. Vocalizations are influenced by the creature's
/// emotional state and fatigue level.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatureVoiceSource {
    /// Species identifier (selects vocal tract model).
    pub species_name: String,
    /// Current emotional arousal (0.0 = calm, 1.0 = agitated).
    pub arousal: f32,
    /// Current vocal fatigue (0.0 = fresh, 1.0 = exhausted).
    pub fatigue: f32,
    /// Output volume.
    pub volume: f32,
    /// Whether this creature is currently vocalizing.
    #[serde(skip)]
    pub vocalizing: bool,
}

impl CreatureVoiceSource {
    /// Create a new creature voice source for the given species.
    ///
    /// # Examples
    ///
    /// ```
    /// # #[cfg(feature = "voice")] {
    /// use kiran::voice::CreatureVoiceSource;
    ///
    /// let voice = CreatureVoiceSource::new("wolf");
    /// assert_eq!(voice.species_name, "wolf");
    /// assert!(!voice.vocalizing);
    /// # }
    /// ```
    pub fn new(species_name: impl Into<String>) -> Self {
        Self {
            species_name: species_name.into(),
            arousal: 0.0,
            fatigue: 0.0,
            volume: 1.0,
            vocalizing: false,
        }
    }

    /// Set the emotional arousal level.
    pub fn with_arousal(mut self, arousal: f32) -> Self {
        self.arousal = arousal.clamp(0.0, 1.0);
        self
    }

    /// Set the fatigue level.
    pub fn with_fatigue(mut self, fatigue: f32) -> Self {
        self.fatigue = fatigue.clamp(0.0, 1.0);
        self
    }

    /// Set the output volume.
    pub fn with_volume(mut self, volume: f32) -> Self {
        self.volume = volume;
        self
    }
}

// ---------------------------------------------------------------------------
// Speech request event
// ---------------------------------------------------------------------------

/// Event requesting an entity to speak text.
#[derive(Debug, Clone)]
pub struct SpeechRequest {
    /// Target entity with a [`VoiceSource`] component.
    pub entity: Entity,
    /// Text to speak.
    pub text: String,
}

impl SpeechRequest {
    /// Create a speech request for the given entity and text.
    pub fn new(entity: Entity, text: impl Into<String>) -> Self {
        Self {
            entity,
            text: text.into(),
        }
    }
}

// ---------------------------------------------------------------------------
// Vocalize request event
// ---------------------------------------------------------------------------

/// Event requesting a creature to vocalize.
#[derive(Debug, Clone)]
pub struct VocalizeRequest {
    /// Target entity with a [`CreatureVoiceSource`] component.
    pub entity: Entity,
    /// Intent of the vocalization (alarm, mating call, territorial, etc.).
    pub intent: String,
}

impl VocalizeRequest {
    /// Create a vocalize request for the given entity and intent.
    pub fn new(entity: Entity, intent: impl Into<String>) -> Self {
        Self {
            entity,
            intent: intent.into(),
        }
    }
}

// ---------------------------------------------------------------------------
// Systems
// ---------------------------------------------------------------------------

/// Process speech requests from the event bus, marking voice sources as speaking.
pub fn process_speech_requests(world: &mut World) {
    let requests = {
        let Some(bus) = world.get_resource_mut::<crate::world::EventBus>() else {
            return;
        };
        bus.drain::<SpeechRequest>()
    };

    let count = requests.len();
    for req in requests {
        if let Some(voice) = world.get_component_mut::<VoiceSource>(req.entity) {
            voice.speaking = true;
        }
    }

    if count > 0 {
        tracing::info!(count, "processed speech requests");
    }
}

/// Process vocalize requests from the event bus, marking creature voices as active.
pub fn process_vocalize_requests(world: &mut World) {
    let requests = {
        let Some(bus) = world.get_resource_mut::<crate::world::EventBus>() else {
            return;
        };
        bus.drain::<VocalizeRequest>()
    };

    let count = requests.len();
    for req in requests {
        if let Some(voice) = world.get_component_mut::<CreatureVoiceSource>(req.entity) {
            voice.vocalizing = true;
        }
    }

    if count > 0 {
        tracing::info!(count, "processed vocalize requests");
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::world::EventBus;

    #[test]
    fn voice_source_builder() {
        let v = VoiceSource::new("narrator")
            .with_rate(1.2)
            .with_pitch_shift(-2.0)
            .with_volume(0.8);
        assert_eq!(v.profile_name, "narrator");
        assert_eq!(v.rate, 1.2);
        assert_eq!(v.pitch_shift, -2.0);
        assert_eq!(v.volume, 0.8);
        assert!(!v.speaking);
    }

    #[test]
    fn voice_source_default() {
        let v = VoiceSource::default();
        assert_eq!(v.profile_name, "default");
        assert_eq!(v.rate, 1.0);
    }

    #[test]
    fn creature_voice_builder() {
        let v = CreatureVoiceSource::new("wolf")
            .with_arousal(0.7)
            .with_fatigue(0.3)
            .with_volume(0.9);
        assert_eq!(v.species_name, "wolf");
        assert_eq!(v.arousal, 0.7);
        assert_eq!(v.fatigue, 0.3);
        assert!(!v.vocalizing);
    }

    #[test]
    fn creature_voice_clamps() {
        let v = CreatureVoiceSource::new("bird")
            .with_arousal(5.0)
            .with_fatigue(-1.0);
        assert_eq!(v.arousal, 1.0);
        assert_eq!(v.fatigue, 0.0);
    }

    #[test]
    fn speech_request_system() {
        let mut world = World::new();
        world.insert_resource(EventBus::new());

        let npc = world.spawn();
        world
            .insert_component(npc, VoiceSource::new("guard"))
            .unwrap();

        {
            let bus = world.get_resource_mut::<EventBus>().unwrap();
            bus.publish(SpeechRequest::new(npc, "Halt!"));
        }

        process_speech_requests(&mut world);

        let voice = world.get_component::<VoiceSource>(npc).unwrap();
        assert!(voice.speaking);
    }

    #[test]
    fn vocalize_request_system() {
        let mut world = World::new();
        world.insert_resource(EventBus::new());

        let creature = world.spawn();
        world
            .insert_component(creature, CreatureVoiceSource::new("wolf"))
            .unwrap();

        {
            let bus = world.get_resource_mut::<EventBus>().unwrap();
            bus.publish(VocalizeRequest::new(creature, "howl"));
        }

        process_vocalize_requests(&mut world);

        let voice = world
            .get_component::<CreatureVoiceSource>(creature)
            .unwrap();
        assert!(voice.vocalizing);
    }

    #[test]
    fn voice_as_component() {
        let mut world = World::new();
        let e = world.spawn();
        world.insert_component(e, VoiceSource::new("bard")).unwrap();
        assert!(world.has_component::<VoiceSource>(e));
    }

    #[test]
    fn creature_voice_as_component() {
        let mut world = World::new();
        let e = world.spawn();
        world
            .insert_component(e, CreatureVoiceSource::new("cat"))
            .unwrap();
        assert!(world.has_component::<CreatureVoiceSource>(e));
    }
}