Add browser-local Piper WebAssembly speech
Build and publish frontend / publish (push) Failing after 6s
Build and publish frontend / publish (push) Failing after 6s
This commit is contained in:
+38
-2
@@ -9,7 +9,7 @@ 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()} = {}) {
|
||||
function makeHarness({supported = true, tts = true, piper = false, onTranscript = () => Promise.resolve()} = {}) {
|
||||
const recognitions = [];
|
||||
const utterances = [];
|
||||
const states = [];
|
||||
@@ -25,6 +25,17 @@ function makeHarness({supported = true, tts = true, onTranscript = () => Promise
|
||||
class Utterance {
|
||||
constructor(text) { this.text = text; utterances.push(this); }
|
||||
}
|
||||
const audioSources = [];
|
||||
class AudioContext {
|
||||
constructor() { this.state = 'running'; this.destination = {}; }
|
||||
resume() { this.state = 'running'; return Promise.resolve(); }
|
||||
decodeAudioData() { return Promise.resolve({duration: 1}); }
|
||||
createBufferSource() {
|
||||
const source = {playbackRate: {value: 1}, detune: {value: 0}, connect() {}, start() { this.started = true; }, stop() { this.stopped = true; }};
|
||||
audioSources.push(source);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
const synthesis = tts ? {
|
||||
spoken: [], cancelled: 0,
|
||||
getVoices: () => [{name: 'Svensk', lang: 'sv-SE'}],
|
||||
@@ -37,6 +48,8 @@ function makeHarness({supported = true, tts = true, onTranscript = () => Promise
|
||||
speechSynthesis: synthesis,
|
||||
SpeechSynthesisUtterance: tts ? Utterance : undefined,
|
||||
SpeechRecognition: supported ? Recognition : undefined,
|
||||
AudioContext,
|
||||
GamePiper: piper ? {synthesize: () => Promise.resolve({arrayBuffer: () => Promise.resolve(new ArrayBuffer(8))})} : undefined,
|
||||
};
|
||||
vm.runInNewContext(source, {window, globalThis: window, Promise, String, Math, Object});
|
||||
const controller = window.GameVoice.createVoiceController({
|
||||
@@ -45,7 +58,7 @@ function makeHarness({supported = true, tts = true, onTranscript = () => Promise
|
||||
onTranscript,
|
||||
});
|
||||
return {
|
||||
controller, recognitions, utterances, synthesis, states, timers,
|
||||
controller, recognitions, utterances, synthesis, states, timers, audioSources,
|
||||
runTimer(delay) {
|
||||
const match = [...timerDelays.entries()].find(([, value]) => value === delay);
|
||||
if (!match) throw new Error(`No timer found with delay ${delay}`);
|
||||
@@ -223,6 +236,29 @@ test('narrator uses neutral pitch while character replies use their saved voice
|
||||
assert.ok(h.utterances[1].pitch < 1);
|
||||
});
|
||||
|
||||
test('Piper WASM output uses the unlocked audio context and starts only after local synthesis', async () => {
|
||||
const h = makeHarness({piper: true});
|
||||
assert.equal(h.controller.activateFromUserGesture(), true);
|
||||
assert.equal(h.controller.speak('Ett svar från Piper.', {role: 'narrator', voiceIdentity: 'neutral'}), true);
|
||||
assert.equal(h.controller.getState(), 'QUEUED');
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(h.audioSources.length, 1);
|
||||
assert.equal(h.audioSources[0].started, true);
|
||||
assert.equal(h.audioSources[0].detune.value, 0);
|
||||
assert.equal(h.controller.getState(), 'SPEAKING');
|
||||
h.audioSources[0].onended();
|
||||
assert.equal(h.controller.getState(), 'IDLE');
|
||||
});
|
||||
|
||||
test('Piper output keeps per-character pitch and rate stable', async () => {
|
||||
const h = makeHarness({piper: true});
|
||||
h.controller.activateFromUserGesture();
|
||||
h.controller.speak('NPC-svar.', {voiceIdentity: 'masculine', characterId: 'npc-1'});
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.ok(h.audioSources[0].detune.value < 0);
|
||||
assert.ok(h.audioSources[0].playbackRate.value < 1);
|
||||
});
|
||||
|
||||
test('TTS and microphone are mutually exclusive', () => {
|
||||
const h = makeHarness();
|
||||
h.controller.update({mode: 'game-voice'});
|
||||
|
||||
Reference in New Issue
Block a user