Speak dialogue with persistent character voices
Build and publish frontend / publish (push) Successful in 50s
Build and publish frontend / publish (push) Successful in 50s
This commit is contained in:
+35
-12
@@ -5,6 +5,9 @@ const test = require('node:test');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const source = fs.readFileSync(path.join(__dirname, '..', 'voice.js'), 'utf8');
|
||||
const apiWindow = {};
|
||||
vm.runInNewContext(source, {window: apiWindow, globalThis: apiWindow, Promise, String, Math, Object});
|
||||
const GameVoice = apiWindow.GameVoice;
|
||||
|
||||
function makeHarness({supported = true, tts = true, onTranscript = () => Promise.resolve()} = {}) {
|
||||
const recognitions = [];
|
||||
@@ -119,6 +122,26 @@ test('TTS selects Swedish fallback, applies rate, and cancellation invalidates c
|
||||
assert.equal(h.controller.getState(), 'IDLE');
|
||||
});
|
||||
|
||||
test('character voice identity gives stable distinct masculine, feminine, and neutral timbres', () => {
|
||||
const profile = GameVoice.characterVoiceProfile;
|
||||
const masculine = profile('masculine', 'character-1');
|
||||
const feminine = profile('feminine', 'character-1');
|
||||
const neutral = profile('neutral', 'character-1');
|
||||
assert.equal(profile('masculine', 'character-1').pitch, masculine.pitch);
|
||||
assert.ok(masculine.pitch < neutral.pitch);
|
||||
assert.ok(neutral.pitch < feminine.pitch);
|
||||
assert.notEqual(profile('masculine', 'character-2').pitch, masculine.pitch);
|
||||
});
|
||||
|
||||
test('narrator uses neutral pitch while character replies use their saved voice identity', () => {
|
||||
const h = makeHarness();
|
||||
h.controller.update({mode: 'game-voice'});
|
||||
h.controller.speak('Berättarröst', {role: 'narrator', voiceIdentity: 'feminine', characterId: 'narrator'});
|
||||
h.controller.speak('NPC-replik', {voiceIdentity: 'masculine', characterId: 'npc-1'});
|
||||
assert.equal(h.utterances[0].pitch, 1);
|
||||
assert.ok(h.utterances[1].pitch < 1);
|
||||
});
|
||||
|
||||
test('TTS and microphone are mutually exclusive', () => {
|
||||
const h = makeHarness();
|
||||
h.controller.update({mode: 'game-voice'});
|
||||
@@ -139,13 +162,13 @@ test('speech is disabled in screen-reader mode and gracefully degrades without s
|
||||
test('system commands require an explicit prefix; ordinary dialogue words are untouched', () => {
|
||||
const global = {};
|
||||
vm.runInNewContext(source, {window: global, globalThis: global, Promise, String, Math, Object});
|
||||
assert.equal(global.GameVoice.parseVoiceCommand('upprepa'), null);
|
||||
assert.equal(global.GameVoice.parseVoiceCommand('hjälp'), null);
|
||||
assert.equal(global.GameVoice.parseVoiceCommand('röstkommando upprepa.' ).command, 'repeat');
|
||||
assert.equal(global.GameVoice.parseVoiceCommand('röstkommando alternativ').command, 'settings');
|
||||
assert.equal(global.GameVoice.parseVoiceCommand('röstinställningar').command, 'settings');
|
||||
assert.equal(global.GameVoice.parseVoiceCommand('röstinställningar', {requirePrefix: true}), null);
|
||||
assert.equal(global.GameVoice.parseVoiceCommand('röstkommando alternativ', {requirePrefix: true}).command, 'settings');
|
||||
assert.equal(GameVoice.parseVoiceCommand('upprepa'), null);
|
||||
assert.equal(GameVoice.parseVoiceCommand('hjälp'), null);
|
||||
assert.equal(GameVoice.parseVoiceCommand('röstkommando upprepa.' ).command, 'repeat');
|
||||
assert.equal(GameVoice.parseVoiceCommand('röstkommando alternativ').command, 'settings');
|
||||
assert.equal(GameVoice.parseVoiceCommand('röstinställningar').command, 'settings');
|
||||
assert.equal(GameVoice.parseVoiceCommand('röstinställningar', {requirePrefix: true}), null);
|
||||
assert.equal(GameVoice.parseVoiceCommand('röstkommando alternativ', {requirePrefix: true}).command, 'settings');
|
||||
});
|
||||
|
||||
test('settings survive reload and invalid storage falls back to accessible defaults', () => {
|
||||
@@ -153,14 +176,14 @@ test('settings survive reload and invalid storage falls back to accessible defau
|
||||
vm.runInNewContext(source, {window: global, globalThis: global, Promise, String, Math, Object});
|
||||
const values = new Map();
|
||||
const storage = {getItem: key => values.get(key) || null, setItem: (key, value) => values.set(key, value)};
|
||||
const requested = {...global.GameVoice.DEFAULT_SETTINGS, mode: 'game-voice', autoSpeak: true, rate: 1.5, voiceName: 'Svensk'};
|
||||
assert.equal(global.GameVoice.saveSettings(storage, 'voice', requested), true);
|
||||
const loaded = global.GameVoice.loadSettings(storage, 'voice');
|
||||
const requested = {...GameVoice.DEFAULT_SETTINGS, mode: 'game-voice', autoSpeak: true, rate: 1.5, voiceName: 'Svensk'};
|
||||
assert.equal(GameVoice.saveSettings(storage, 'voice', requested), true);
|
||||
const loaded = GameVoice.loadSettings(storage, 'voice');
|
||||
assert.equal(loaded.mode, 'game-voice');
|
||||
assert.equal(loaded.autoSpeak, true);
|
||||
assert.equal(loaded.rate, 1.5);
|
||||
assert.equal(loaded.voiceName, 'Svensk');
|
||||
values.set('voice', '{trasig');
|
||||
assert.equal(global.GameVoice.loadSettings(storage, 'voice').mode, 'screen-reader');
|
||||
assert.equal(global.GameVoice.saveSettings({setItem() { throw new Error('blocked'); }}, 'voice', requested), false);
|
||||
assert.equal(GameVoice.loadSettings(storage, 'voice').mode, 'screen-reader');
|
||||
assert.equal(GameVoice.saveSettings({setItem() { throw new Error('blocked'); }}, 'voice', requested), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user