109 lines
5.2 KiB
JavaScript
109 lines
5.2 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { once } from 'node:events';
|
||
import { WebSocketServer, WebSocket } from 'ws';
|
||
|
||
// Local upstreams exercise the real framing without provider credentials or network access.
|
||
test('voice turns stream dialogue and skip synthesis for confirmed quick actions', async () => {
|
||
const stt = new WebSocketServer({ port: 0 });
|
||
const tts = new WebSocketServer({ port: 0 });
|
||
await Promise.all([once(stt, 'listening'), once(tts, 'listening')]);
|
||
process.env.ELEVENLABS_API_KEY = 'test';
|
||
process.env.ELEVEN_STT_KEYTERMS = 'Aphex Twin, Boards of Canada';
|
||
process.env.LLM_MODE = 'echo';
|
||
process.env.ELEVEN_STT_URL = `ws://127.0.0.1:${stt.address().port}/stt`;
|
||
process.env.ELEVEN_TTS_URL = `ws://127.0.0.1:${tts.address().port}/tts`;
|
||
let input = Buffer.alloc(0), ttsText = '', recorded;
|
||
let transcript = 'Привет';
|
||
let syntheses = 0;
|
||
stt.on('connection', (ws, req) => {
|
||
const params = new URL(req.url, 'http://x').searchParams;
|
||
assert.equal(params.get('commit_strategy'), 'manual');
|
||
assert.equal(params.get('model_id'), 'scribe_v3_live');
|
||
assert.deepEqual(params.getAll('keyterms'), ['Aphex Twin', 'Boards of Canada']);
|
||
ws.on('message', data => {
|
||
const msg = JSON.parse(data);
|
||
input = Buffer.concat([input, Buffer.from(msg.audio_base_64, 'base64')]);
|
||
if (msg.commit) ws.send(JSON.stringify({ message_type: 'committed_transcript', text: transcript }));
|
||
});
|
||
});
|
||
tts.on('connection', ws => {
|
||
syntheses++;
|
||
ws.on('message', data => {
|
||
const msg = JSON.parse(data);
|
||
if (msg.inputs) ttsText += msg.inputs[0].text;
|
||
if (msg.close_socket) { ws.send(JSON.stringify({ audio: Buffer.from([1, 2, 3, 4]).toString('base64') })); ws.send(JSON.stringify({ is_final: true })); }
|
||
});
|
||
});
|
||
const { handleTurn } = await import('../server/turns.mjs');
|
||
const local = new WebSocketServer({ port: 0 });
|
||
await once(local, 'listening');
|
||
const store = { getSettings: () => ({ sttModel: 'scribe_v3_live', ttsModel: 'eleven_v4_turbo', voiceId: 'voice', systemPrompt: '' }), getHistory: () => [], recordTurn: (...args) => { recorded = args; } };
|
||
local.on('connection', ws => handleTurn(ws, store));
|
||
async function turn(reply, wake = false) {
|
||
const client = new WebSocket(`ws://127.0.0.1:${local.address().port}`);
|
||
const seen = [], audio = [], calls = [];
|
||
client.on('message', (data, binary) => {
|
||
if (binary) {
|
||
audio.push(...data);
|
||
return;
|
||
}
|
||
const msg = JSON.parse(data);
|
||
seen.push(msg.type);
|
||
if (msg.type === 'ready') {
|
||
client.send(JSON.stringify({ type: 'start', wake }));
|
||
client.send(Buffer.from([5, 6, 7, 8]));
|
||
client.send(JSON.stringify({ type: 'end' }));
|
||
} else if (msg.type === 'device_tool') {
|
||
calls.push([msg.name, msg.arguments]);
|
||
client.send(JSON.stringify({ type: 'device_tool_result', id: msg.id, result: reply }));
|
||
}
|
||
});
|
||
await once(client, 'close');
|
||
return { seen, audio, calls };
|
||
}
|
||
|
||
assert.deepEqual(await turn(), { seen: ['ready', 'transcript', 'done'], audio: [1, 2, 3, 4], calls: [] });
|
||
assert.deepEqual([...input], [5, 6, 7, 8]);
|
||
assert.equal(ttsText, 'Привет');
|
||
assert.deepEqual(recorded, ['Привет', 'Привет']);
|
||
|
||
transcript = 'Алиса, громкость пятьдесят процентов.';
|
||
assert.deepEqual(await turn({ ok: true, level: 50 }), {
|
||
seen: ['ready', 'transcript', 'device_tool', 'done'], audio: [], calls: [['station_volume', { level: 50 }]],
|
||
});
|
||
assert.deepEqual(recorded, [transcript, 'Громкость 50%.']);
|
||
|
||
transcript = 'Выключи музыку.';
|
||
assert.deepEqual(await turn({ accepted: true, status: 'paused' }), {
|
||
seen: ['ready', 'transcript', 'device_tool', 'done'], audio: [], calls: [['spotify_control', { action: 'pause' }]],
|
||
});
|
||
assert.deepEqual(recorded, [transcript, 'Музыка на паузе.']);
|
||
|
||
transcript = 'Алиса, выключи музыку.';
|
||
assert.deepEqual(await turn({ accepted: true, status: 'paused' }, true), {
|
||
seen: ['ready', 'transcript', 'device_tool', 'done'], audio: [], calls: [['spotify_control', { action: 'pause' }]],
|
||
});
|
||
assert.deepEqual(recorded, ['выключи музыку.', 'Музыка на паузе.']);
|
||
|
||
transcript = 'Алиса.';
|
||
assert.deepEqual(await turn(undefined, true), { seen: ['ready', 'done'], audio: [], calls: [] });
|
||
assert.deepEqual(recorded, ['выключи музыку.', 'Музыка на паузе.']);
|
||
assert.equal(syntheses, 1);
|
||
|
||
for (const [command, action, enabled, answer] of [
|
||
['включи', 'start', true, 'Звук с компьютера включён.'],
|
||
['выключи', 'stop', false, 'Звук с компьютера выключен.'],
|
||
]) {
|
||
transcript = `Алиса, ${command} звук с компьютера.`;
|
||
ttsText = '';
|
||
assert.deepEqual(await turn({ ok: true, enabled }, true), {
|
||
seen: ['ready', 'transcript', 'device_tool', 'done'],
|
||
audio: [1, 2, 3, 4],
|
||
calls: [['pc_audio', { action }]],
|
||
});
|
||
assert.equal(ttsText, answer);
|
||
}
|
||
await Promise.all([new Promise(r => local.close(r)), new Promise(r => stt.close(r)), new Promise(r => tts.close(r))]);
|
||
});
|