Files

109 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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))]);
});