feat(live-atc): per-position controller personas with voice and pace

Each ATC position (session + airport + type + frequency) now gets a stable
persona: a controller-pool voice and a base pace of 1.1-1.3x, jittered +/-0.05
per transmission. Anything spoken without an explicit voice — the controller
reply path and AI-traffic ATC lines — uses the persona; pilot voices are
untouched. A new session reshuffles the shift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-07-19 10:48:19 +02:00
parent b359670d47
commit 3d5a0a4e59
3 changed files with 95 additions and 4 deletions

View File

@@ -5,9 +5,11 @@ import {
CONTROLLER_VOICES,
PILOT_VOICES,
RESERVED_VOICES,
controllerPersonaFor,
controllerVoiceFor,
fnv1a,
pilotVoiceFor,
transmissionSpeed,
voiceFromPool,
} from '~~/shared/utils/voicePool'
@@ -73,6 +75,42 @@ describe('voicePool — assignment', () => {
})
})
describe('voicePool — controller personas', () => {
it('is deterministic for the same position key', () => {
const first = controllerPersonaFor('sess1:EDDF:TWR:118.775')
for (let i = 0; i < 10; i++) {
assert.deepEqual(controllerPersonaFor('sess1:EDDF:TWR:118.775'), first)
}
})
it('uses a controller-partition voice and a base speed in [1.1, 1.3]', () => {
for (const key of ['s:EDDF:TWR', 's:EDDF:GND', 's:EDDM:APP', 's:EDDB:DEL', 'x:EDDF:TWR']) {
const persona = controllerPersonaFor(key)
assert.ok(CONTROLLER_VOICES.includes(persona.voice), `${key}${persona.voice}`)
assert.ok(persona.baseSpeed >= 1.1 && persona.baseSpeed <= 1.3, `${key}${persona.baseSpeed}`)
}
})
it('gives different sessions a different shift of controllers', () => {
const keys = ['EDDF:DEL', 'EDDF:GND', 'EDDF:TWR', 'EDDF:APP']
const shiftA = keys.map(k => controllerPersonaFor(`sessionA:${k}`))
const shiftB = keys.map(k => controllerPersonaFor(`sessionB:${k}`))
assert.notDeepEqual(shiftA, shiftB)
})
it('jitters the transmission speed within ±0.05 of base', () => {
for (let i = 0; i < 50; i++) {
const speed = transmissionSpeed(1.2)
assert.ok(speed >= 1.15 - 1e-9 && speed <= 1.25 + 1e-9, `jittered to ${speed}`)
}
})
it('transmission speeds actually vary', () => {
const speeds = new Set(Array.from({ length: 30 }, () => transmissionSpeed(1.2)))
assert.ok(speeds.size > 1, 'expected jitter, got a constant')
})
})
describe('voicePool — fnv1a', () => {
it('matches the reference vectors', () => {
assert.equal(fnv1a(''), 0x811c9dc5)