<!-- SOLUV · part 656 -->
vatureEnding,zf as ZeroFactor,Oi as ZeroSlopeEnding,Pm as ZeroStencilOp,pp as createCanvasElement,Lt as error,lg as getConsoleFunction,xr as log,og as setConsoleFunction,ut as warn,$n as warnOnce};


===== FILE: public/soluv/world/voices.js =====
/*
 * voices.js: Playa's TRANSCEIVER for chat walls (/soluv, /soluv/N, /soluv/space). The King's word: male is male, female is female.
 * - Reads NEW lines aloud (never the backlog), each speaker in the voice THAT USER chose (Male / Female), saved on the server
 *   (GET/POST /api/soluv-voice, soluv-voice.js) so everyone hears them the same way. Never guessed from a name. Unset = device default.
 * - "My voice" saves YOUR choice with your own login + CSRF (you can only set your own).
 * - A listener may override any speaker on this device only (localStorage soluv.voiceOverrides.v1), e.g. soluvKeys.voiceFor(name, 'male').
 * - Mic: dictates into the composer (never sends by itself) where the browser has speech recognition.
 * No silent failures: problems are shown in the panel and console.error'd. Every button is >= 48 px.
 */
const LS_ON = 'soluv.readAloud.v1', LS_OVR = 'soluv.voiceOverrides.v1';
const MALE = ['daniel', 'alex', 'fred', 'aaron', 'arthur', 'gordon', 'rishi', 'oliver', 'thomas', 'google uk english male', 'microsoft david', 'microsoft guy', 'microsoft mark', 'microsoft ryan', 'male'];
const FEMALE = ['samantha', 'karen', 'moira', 'tessa', 'victoria', 'allison', 'ava', 'susan', 'serena', 'fiona', 'google us english', 'google uk english female', 'microsoft zira', 'microsoft aria', 'microsoft jenny', 'microsoft libby', 'female'];
const PITCH = { male: 0.8, female: 1.25, default: 1 };
const lower = s => String(s || '').trim().toLowerCase();

function readLS(k, d) { const v = localStorage.getItem(k); if (v == null) return d; try { return JSON.parse(v); } catch (e) { console.error('[soluv-voice] unreadable ' + k + ': ' + e.message); return d; } }

export function createTransceiver(opts) {
  const origin = opts.origin || '';
  const whoAmI = opts.whoAmI;                     // () => Promise<{name, username, king, csrf}>
  const host = opts.container;                    // where the panel lives
  const synth = typeof window !== 'undefined' && window.speechSynthesis ? window.speechSynthesis : null;
  const SR = typeof window !== 'undefined' ? (window.SpeechRecognition || window.webkitSpeechRecognition) : null;
  const T = {
    on: readLS(LS_ON, false) === true, overrides: readLS(LS_OVR, {}) || {}, server: { byId: {}, byName: {} }, asked: new Map(),
    mine: null, me: null, floor: Infinity, lastSpeaker: null, problem: synth ? null : 'This browser cannot read aloud (no speechSynthesis)',
    picked: { male: null, female: null }, spoken: 0, recent: [], listening: false,
  };

  /* ---------- device voices: distinct male / female, by name, then English, then pitch ---------- */
  function pickVoices() {
    if (!synth) return;
    const all = synth.getVoices() || []; if (!all.length) return;
    const en = all.filter(v => /^en/i.test(v.lang));
    const pool = en.length ? en : all;
    const find = list => { for (const n of list) { const v = pool.find(x => lower(x.name).includes(n)); if (v) return v; } return null; };
    let m = find(MALE), f = find(FEMALE);
    if (!m) m = pool.find(v => v !== f) || null; if (!f) f = pool.find(v => v !== m) || null;
    T.picked = { male: m, female: f };
    renderPanel();
  }
  if (synth) { pickVoices(); synth.addEventListener('voiceschanged', pickVoices); }

  /* ---------- whose voice: listener override > that user's own saved choice > default ---------- */
  const idOf = m => { const x = /^uid:(\d+)$/.exec(m.user || ''); return x ? x[1] : null; };
  function voiceOf(m) {
    const name = lower(m.from), first = name.split(/\s+/)[0];
    const o = T.overrides[name] || T.overrides[first]; if (o) return { kind: o, why: 'your setting' };
    const id = idOf(m); if (id && T.server.byId[id]) return { kind: T.server.byId[id], why: 'their choice' };
    const un = !id && m.user && !/^(bot|guest):/.test(m.user) ? lower(m.user) : null;
    const s = (un && T.server.byName[un]) || T.server.byName[name]; if (s) return { kind: s, why: 'their choice' };
    return { kind: 'default', why: 'not set' };
  }
  async function fetchVoices(msgs) {
    const ids = new Set(), names = new Set(), now = Date.now();
    for (const m of msgs) {
      const id = idOf(m); if (id) { if (!(T.asked.get('i' + id) > now - 60000)) ids.add(id); continue; }
      if (m.user && /^(bot|guest):/.test(m.user)) continue;
      const n = lower(m.user || m.from); if (n && !(T.asked.get('n' + n) > now - 60000)) names.add(n);
    }
    if (!ids.size && !names.size) return;
    const q = new URLSearchParams(); if (ids.size) q.set('ids', [...ids].slice(0, 100).join(',')); if (names.size) q.set('names', [...names].slice(0, 100).join(','));
    const r = await fetch(origin + '/api/soluv-voice?' + q, { cache: 'no-store', credentials: 'same-origin' 