Ctrl K
ComunidadGuías…

test/say-voice.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Which macOS voice Billion speaks with, and how fast (server/voice.js,
// SAY_VOICE and SAY_RATE): spawn is mocked, `say -v '?'` answers with
// tools.voices, nothing is really spoken.

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EventEmitter } from 'events';
import { writeFileSync } from 'fs';

const tools = { voices: '', calls: [] };

vi.mock('child_process', async (importOriginal) => ({
  ...(await importOriginal()),
  spawn: vi.fn((cmd, args) => {
    tools.calls.push([cmd, args]);
    const child = new EventEmitter();
    child.stdout = new EventEmitter();
    child.stderr = new EventEmitter();
    process.nextTick(() => {
      if (cmd === 'say' && args[1] === '?') child.stdout.emit('data', tools.voices);
      if (cmd === 'ffmpeg') writeFileSync(args[args.length - 1], 'OGG');
      child.emit('close', 0);
    });
    return child;
  }),
}));

const LIST = [
  'Albert              en_US    # Hello! My name is Albert.',
  'Samantha            en_US    # Hello! My name is Samantha.',
  'Daniel (Enhanced)   en_GB    # Hello! My name is Daniel.',
  'Ava (Enhanced)      en_US    # Hello! My name is Ava.',
  'Ava (Premium)       en-US    # Hello! My name is Ava.',
  'Amélie (Premium)    fr_CA    # Bonjour! Je m’appelle Amélie.',
  'Eddy (English (UK)) en_GB    # Hello! My name is Eddy.',
].join('\n');

let log;
beforeEach(() => {
  vi.resetModules();   // a fresh voice cache per test
  tools.calls = [];
  log = vi.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(() => log.mockRestore());

const speak = async (voices, env) => {
  tools.voices = voices;
  const { synthesize } = await import('../server/voice.js');
  await synthesize('hello', env);
  await synthesize('again', env);
  const says = tools.calls.filter(([cmd, args]) => cmd === 'say' && args[1] !== '?');
  expect(tools.calls.filter(([, args]) => args[1] === '?')).toHaveLength(1);   // listed once, cached
  return says.map(([, args]) => args.slice(0, args.indexOf('-o')));
};

describe('SAY_VOICE and the auto-picked voice', () => {
  it('honours SAY_VOICE when it is installed', async () => {
    expect(await speak(LIST, { SAY_VOICE: 'Samantha' })).toEqual([['-v', 'Samantha'], ['-v', 'Samantha']]);
    expect(log).not.toHaveBeenCalled();
  });

  it('a bare SAY_VOICE takes that voice\'s best quality', async () => {
    expect((await speak(LIST, { SAY_VOICE: 'Ava' }))[0]).toEqual(['-v', 'Ava (Premium)']);
  });

  it('SAY_VOICE by its bare name matches the listed "Name (English (UK))"', async () => {
    expect((await speak(LIST, { SAY_VOICE: 'eddy' }))[0]).toEqual(['-v', 'Eddy (English (UK))']);
  });

  it('a missing SAY_VOICE is logged once, naming it, and falls back to the best voice', async () => {
    expect(await speak(LIST, { SAY_VOICE: 'Nobody' })).toEqual([['-v', 'Ava (Premium)'], ['-v', 'Ava (Premium)']]);
    expect(log).toHaveBeenCalledTimes(1);
    expect(log.mock.calls[0][0]).toContain('"Nobody"');
  });

  it('prefers English Premium over Enhanced over the default', async () => {
    expect((await speak(LIST, {}))[0]).toEqual(['-v', 'Ava (Premium)']);   // a hyphenated locale too
    vi.resetModules(); tools.calls = [];
    const noPremium = LIST.split('\n').filter(l => !l.startsWith('Ava (Premium)')).join('\n');
    expect((await speak(noPremium, {}))[0]).toEqual(['-v', 'Ava (Enhanced)']);
    vi.resetModules(); tools.calls = [];
    expect((await speak('Albert              en_US    # Hello!', {}))[0]).toEqual([]);
  });

  it('no voices parsed means no -v flag', async () => {
    expect(await speak('', {})).toEqual([[], []]);
  });
});

describe('SAY_RATE', () => {
  const rates = async env => {
    tools.voices = LIST;
    const { synthesize } = await import('../server/voice.js');
    await synthesize('hello', env);
    await synthesize('again', env);
    return tools.calls.filter(([cmd, args]) => cmd === 'say' && args[1] !== '?')
      .map(([, args]) => args.includes('-r') ? args[args.indexOf('-r') + 1] : null);
  };

  it('unset means no -r, the voice speaks at its own system rate', async () => {
    expect(await rates({})).toEqual([null, null]);
    expect(log).not.toHaveBeenCalled();
  });

  it('honours SAY_RATE', async () => {
    expect(await rates({ SAY_RATE: ' 180 ' })).toEqual(['180', '180']);
  });

  it('clamps it to 120–300', async () => {
    expect(await rates({ SAY_RATE: '900' })).toEqual(['300', '300']);
    vi.resetModules(); tools.calls = [];
    expect(await rates({ SAY_RATE: '50' })).toEqual(['120', '120']);
    expect(log).not.toHaveBeenCalled();
  });

  it('ignores garbage with one log line and no -r', async () => {
    expect(await rates({ SAY_RATE: 'fast' })).toEqual([null, null]);
    expect(log).toHaveBeenCalledTimes(1);
    expect(log.mock.calls[0][0]).toContain('"fast"');
  });
});