iobroker.assistant 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +12 -0
  2. package/admin/custom/.vite/manifest.json +62 -62
  3. package/admin/custom/assets/{Components-x2Dfv8qE.js → Components-D15eOzOp.js} +2 -2
  4. package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-B4YbqhBb.js → _virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-C8qbMwoi.js} +1 -1
  5. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-IaosaymG.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__212234797318001__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-BWAFmFMj.js} +1 -1
  6. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-o4EU_Ut5.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__212234797318001__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Bh5IbNwb.js} +1 -1
  7. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared--be2hWbV.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__212234797318001__loadShare__shared-D8mhEL60.js} +1 -1
  8. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__212234797318001__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-T80cpMmm.js +122 -0
  9. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__212234797318001__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__--SURtz9t.js} +1 -1
  10. package/admin/custom/assets/hostInit-DpzPHTOk.js +1 -0
  11. package/admin/custom/assets/{lib-DBbNtolG.js → lib-70DV4zKj.js} +1 -1
  12. package/admin/custom/assets/{mf-entry-bootstrap-0-7abd30d3.js → mf-entry-bootstrap-0-0119ee25.js} +2 -2
  13. package/admin/custom/assets/{pendingShares-U0D1MYK4.js → pendingShares-CKvyybtG.js} +1 -1
  14. package/admin/custom/customComponents.js +2 -2
  15. package/admin/custom/index.html +2 -2
  16. package/admin/custom/mf-manifest.json +1 -1
  17. package/admin/custom/mf-stats.json +1 -1
  18. package/admin/i18n/de.json +6 -1
  19. package/admin/i18n/en.json +6 -1
  20. package/admin/i18n/es.json +6 -1
  21. package/admin/i18n/fr.json +6 -1
  22. package/admin/i18n/it.json +6 -1
  23. package/admin/i18n/nl.json +6 -1
  24. package/admin/i18n/pl.json +6 -1
  25. package/admin/i18n/pt.json +6 -1
  26. package/admin/i18n/ru.json +6 -1
  27. package/admin/i18n/uk.json +6 -1
  28. package/admin/i18n/zh-cn.json +6 -1
  29. package/admin/jsonConfig.json +38 -4
  30. package/build/lib/nlu.js +14 -0
  31. package/build/lib/nlu.js.map +1 -1
  32. package/build/lib/piAgent.js +5 -0
  33. package/build/lib/piAgent.js.map +1 -1
  34. package/build/lib/voice/captures.js +87 -0
  35. package/build/lib/voice/captures.js.map +1 -0
  36. package/build/lib/voice/engines.js +1 -1
  37. package/build/lib/voice/engines.js.map +1 -1
  38. package/build/lib/voice/esphome.js +81 -5
  39. package/build/lib/voice/esphome.js.map +1 -1
  40. package/build/lib/voice/ttsCache.js +49 -1
  41. package/build/lib/voice/ttsCache.js.map +1 -1
  42. package/build/lib/voice/vad.js +14 -1
  43. package/build/lib/voice/vad.js.map +1 -1
  44. package/build/main.js +173 -63
  45. package/build/main.js.map +1 -1
  46. package/io-package.json +29 -27
  47. package/package.json +5 -5
  48. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-BsTz_EWE.js +0 -122
  49. package/admin/custom/assets/hostInit-Blh7a-Qe.js +0 -1
  50. /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__212234797318001__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"esphome.js","sourceRoot":"","sources":["../../../src/lib/voice/esphome.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESPHome voice satellites — the fourth transport, next to the UDP server (`voiceServer.ts`) and the\n * Wyoming endpoint (`wyoming.ts`).\n *\n * Unlike those two, this one is a **client**: an ESPHome voice satellite is a *server* that waits on\n * TCP 6053 for the home-automation controller to connect and drive it. We therefore dial out to each\n * configured device and play the role Home Assistant normally plays. That covers the ThirdReality\n * Voice & Music Assistant (Dev Edition), the Home Assistant Voice PE and any box running\n * OHF-Voice/linux-voice-assistant.\n *\n * Flow for one utterance (verified against the firmware's `src/satellite/Satellite.cpp`):\n *\n * device → VoiceAssistantRequest{start, wake_word_phrase} wake word fired on the device\n * we → VoiceAssistantResponse{port:0} 0 = audio over the API, no UDP\n * we → RUN_START, WAKE_WORD_END, STT_START, STT_VAD_START\n * device → VoiceAssistantAudio … 16 kHz mono s16le, *until told to stop*\n * we → STT_VAD_END ← the device has no VAD of its own\n * we → STT_END{text}, INTENT_START, INTENT_END, TTS_START{text}, TTS_END{url}, RUN_END\n * device → fetches the URL over HTTP, plays it, VoiceAssistantAnnounceFinished\n *\n * Two consequences shape this file: end-of-speech detection is ours (`vad.ts`), and the spoken reply\n * has to be reachable over HTTP (`mediaServer.ts`) rather than streamed down the socket.\n */\nimport * as net from 'node:net';\nimport {\n pb,\n encode,\n decode,\n type PbMessage,\n type AuthenticationResponse,\n type DeviceInfoResponse,\n type HelloResponse,\n type VoiceAssistantAnnounceFinished,\n type VoiceAssistantAudio,\n type VoiceAssistantConfigurationResponse,\n type VoiceAssistantEventData,\n type VoiceAssistantRequestMsg,\n} from './esphomeProto';\nimport { EntityRegistry, isEntityStateMessage, isListEntityMessage, type EsphomeEntity } from './esphomeEntities';\nimport { SpeechDetector, type Bytes } from './vad';\nimport { pcmToWav } from './stt';\nimport type { SttEngine } from './stt';\nimport type { TtsEngine } from './tts';\nimport type { SatelliteState } from './protocol';\n\n/** `VoiceAssistantEvent` (api.proto) — the subset we emit. */\nconst EVENT = {\n ERROR: 0,\n RUN_START: 1,\n RUN_END: 2,\n STT_START: 3,\n STT_END: 4,\n INTENT_START: 5,\n INTENT_END: 6,\n TTS_START: 7,\n TTS_END: 8,\n WAKE_WORD_END: 10,\n STT_VAD_START: 11,\n STT_VAD_END: 12,\n} as const;\n\n/** `SubscribeVoiceAssistantRequest.flags`: audio over the API socket instead of a UDP side channel. */\nconst SUBSCRIBE_API_AUDIO = 1;\n/** ESPHome voice satellites always capture at 16 kHz mono 16-bit. */\nconst MIC_SAMPLE_RATE = 16000;\n/**\n * Leading audio to keep out of the end-of-speech analysis. These devices acknowledge the wake word\n * with a chirp through their own speaker and hear it back on their own microphone, far louder than\n * the speech that follows. Without this the chirp alone satisfies the detector's speech test — so\n * `sawSpeech` stops meaning \"somebody said something\" and a false wake is billed to STT — and it\n * drags the noise floor up with it.\n *\n * Measured over four captures from a ThirdReality Voice & Music Assistant (RMS per 50 ms, remarkably\n * repeatable across all four):\n *\n * ms 0 50 100 150 200 250 300 350 400 450 500 550 600\n * rms ~20 ~900 ~11k ~19k ~14k ~1.3k ~990 ~780 ~610 ~370 ~330 ~165 ~180\n *\n * so the chirp is still above the gate at 500 ms and only clears it around 550–600. 600 ms is the\n * smallest value that rejects a chirp-only wake while still detecting the speech in every one of the\n * four captures, with no change to when the utterance closes. The audio itself is untouched and still\n * goes to STT in full, so a speaker who starts inside this window loses nothing but a late gate open.\n */\nconst WAKE_CHIRP_MS = 600;\n/** Default API port of an ESPHome device. */\nconst DEFAULT_PORT = 6053;\n/** Keepalive interval; two missed replies count as a dead link. */\nconst PING_INTERVAL_MS = 20_000;\n/** Wait between reconnect attempts. Devices reboot, and a Wi-Fi speaker drops off now and then. */\nconst RECONNECT_MS = 10_000;\n/** Give up on a pipeline that produced no audio at all (device wedged mid-stream). */\nconst PIPELINE_TIMEOUT_MS = 30_000;\n/** How long we wait for the device to report that it finished playing before forcing it back to idle. */\nconst PLAYBACK_TIMEOUT_MS = 60_000;\n\nexport interface EsphomeDevice {\n /** Host name or IP of the satellite. */\n ip: string;\n /** API port; empty/0 → 6053. */\n port?: number;\n /** API password, if one is configured on the device. */\n password?: string;\n /** Room this satellite belongs to (drives `satellites.<room>.*` and the answer context). */\n room?: string;\n}\n\nexport interface EsphomeSatellitesOptions {\n devices: EsphomeDevice[];\n /** ISO-639-1 language hint for STT/TTS ('' = auto/provider default). */\n language: string;\n stt: SttEngine;\n tts: TtsEngine;\n /**\n * Produce the assistant reply for a transcribed utterance. `audioMs` is the length of the recording,\n * `vad` what the voice activity detection made of it (both for the request journal).\n */\n answer: (\n question: string,\n ctx: {\n device: string;\n room: string;\n wakeWord: string;\n sttMs: number;\n audioMs: number;\n vad: { peak: number; noiseFloor: number; startLevel: number; endLevel: number };\n },\n ) => Promise<string>;\n /** The reply was synthesised: how long that took and how long it plays (for the request journal). */\n onSpoken?: (device: string, info: { ms: number; audioMs: number }) => void;\n /** Optional STT vocabulary hints (device/room names) to bias recognition; re-read per utterance. */\n getHints?: () => string[] | Promise<string[]>;\n /** Where spoken replies are published for the device to fetch. */\n media: MediaHost;\n /**\n * Host the device should call back on. Empty → the local address of our socket to that device,\n * which is automatically the right interface on a multi-homed host.\n */\n mediaHost?: string;\n /** Silence that ends an utterance, in ms ('' / 0 → the detector default). */\n silenceMs?: number;\n log: ioBroker.Logger;\n /** Notified on every satellite state transition (idle/listening/processing/speaking/offline). */\n onStatus?: (device: string, room: string, state: SatelliteState | 'offline') => void;\n /** Notified with the device's wake-word configuration, on connect and after every change. */\n onWakeWords?: (device: string, room: string, config: WakeWordConfig) => void;\n /** Notified once per connect with everything the device exposes besides the voice pipeline. */\n onEntities?: (device: string, room: string, entities: EsphomeEntity[]) => void;\n /** Notified whenever one of those entities changes (and once per entity right after connecting). */\n onEntityState?: (device: string, room: string, entity: EsphomeEntity) => void;\n /** True when the reply asks something back, so the device should re-open its mic without a wake word. */\n expectsFollowUp?: (answer: string, device: string) => boolean;\n /**\n * Custom wake words to offer the devices, re-read for every configuration request. The device lists\n * them next to its own and downloads one (from the media server) only when it is selected.\n */\n externalWakeWords?: () => ExternalWakeWordOffer[];\n}\n\n/** A custom wake word as offered to the devices; the files are on the media server under `path`. */\nexport interface ExternalWakeWordOffer {\n id: string;\n phrase: string;\n languages: string[];\n /** Size of the `.tflite` in bytes. */\n size: number;\n /** SHA-256 of the `.tflite`, lower-case hex. */\n sha256: string;\n /** URL path of the JSON config, e.g. `/wakeword/hey_hannah_1a2b3c4d.json`; the model sits next to it. */\n path: string;\n}\n\n/** What a device reports about its wake words: what the firmware ships plus the custom ones we offered. */\nexport interface WakeWordConfig {\n /** Ids currently listening, e.g. `['okay_nabu']`. */\n active: string[];\n /** Every wake word the device can use, with the spoken phrase and the languages it was trained on. */\n available: { id: string; phrase: string; languages: string[] }[];\n /** How many may listen at once — 2 on the ThirdReality speaker, which has a sensitivity per slot. */\n max: number;\n}\n\n/** `VoiceAssistantTimerEvent` in api.proto. */\nexport const TIMER_EVENT = { started: 0, updated: 1, cancelled: 2, finished: 3 } as const;\n\n/** One assistant timer, as the device wants to hear about it. */\nexport interface TimerEvent {\n type: keyof typeof TIMER_EVENT;\n id: string;\n name: string;\n totalSeconds: number;\n secondsLeft: number;\n /** False once it has finished or been cancelled — the device clears its ring on that. */\n active: boolean;\n}\n\n/** The slice of `MediaServer` this module needs (kept narrow so tests can pass a stub). */\nexport interface MediaHost {\n port: number;\n publish(body: Buffer, contentType?: string, ttlMs?: number): string;\n}\n\n/**\n * All configured ESPHome satellites. Owns one connection per device and offers the same surface the\n * adapter already uses for the UDP server: `devices()` and `announce()`.\n */\nexport class EsphomeSatellites {\n private readonly connections: EsphomeConnection[] = [];\n\n constructor(private readonly opts: EsphomeSatellitesOptions) {}\n\n async start(): Promise<void> {\n for (const device of this.opts.devices) {\n if (!device.ip?.trim()) {\n continue;\n }\n const connection = new EsphomeConnection(device, this.opts);\n this.connections.push(connection);\n connection.start();\n }\n if (!this.connections.length) {\n this.opts.log.warn('ESPHome satellites enabled, but no device addresses are configured.');\n }\n return Promise.resolve();\n }\n\n async stop(): Promise<void> {\n await Promise.all(this.connections.map(c => c.stop()));\n this.connections.length = 0;\n }\n\n /** Device names of the satellites currently connected. */\n devices(): string[] {\n return this.connections.filter(c => c.online).map(c => c.deviceName);\n }\n\n /** Wake-word configuration of one connected satellite, or null if it is unknown/offline. */\n wakeWords(device: string): WakeWordConfig | null {\n return this.connections.find(c => c.online && c.deviceName === device)?.wakeWordConfig ?? null;\n }\n\n /**\n * Choose which wake words listen on one satellite. Ids come from {@link WakeWordConfig.available};\n * the device stores the selection itself, so it survives a restart of the adapter.\n */\n setWakeWords(device: string, ids: string[]): boolean {\n const target = this.connections.find(c => c.online && c.deviceName === device);\n if (!target) {\n return false;\n }\n target.setWakeWords(ids);\n return true;\n }\n\n /**\n * Offer the current custom wake words to every connected satellite again — after one was uploaded\n * or deleted. Each device answers with its new configuration (→ `onWakeWords`).\n */\n refreshWakeWords(): number {\n const online = this.connections.filter(c => c.online);\n online.forEach(c => c.requestWakeWordConfig());\n return online.length;\n }\n\n /** Everything one satellite exposes besides the voice pipeline (gain, noise suppression, …). */\n entities(device: string): EsphomeEntity[] {\n return this.connections.find(c => c.online && c.deviceName === device)?.entities.list() ?? [];\n }\n\n /**\n * Write one of those entities. Returns false when the satellite is offline, the entity is unknown\n * or read-only, or the value is not one the device accepts.\n */\n setEntity(device: string, objectId: string, value: unknown): boolean {\n const target = this.connections.find(c => c.online && c.deviceName === device);\n return target ? target.setEntity(objectId, value) : false;\n }\n\n /**\n * Mirror an assistant timer onto the satellites so they can show it on their own LED ring and ring\n * it themselves. `device` null broadcasts. Returns how many devices were told.\n */\n timerEvent(device: string | null, event: TimerEvent): number {\n const targets = this.connections.filter(c => c.online && (!device || c.deviceName === device));\n targets.forEach(c => c.sendTimerEvent(event));\n return targets.length;\n }\n\n /**\n * Play a ready PCM buffer on one satellite (by device name) or all of them. Returns how many\n * devices it was handed to.\n *\n * With `startConversation` the device opens its microphone as soon as the clip has played, without\n * a wake word — that is how a question gets its answer (see `PendingQuestions`).\n */\n async announce(device: string | null, pcm: Buffer, sampleRate: number, startConversation = false): Promise<number> {\n const targets = this.connections.filter(c => c.online && (!device || c.deviceName === device));\n if (!targets.length) {\n return 0;\n }\n await Promise.all(\n targets.map(c =>\n c\n .announce(pcm, sampleRate, startConversation)\n .catch(e => this.opts.log.warn(`announce to ${c.deviceName} failed: ${(e as Error).message}`)),\n ),\n );\n return targets.length;\n }\n}\n\n/** State of the utterance currently being captured. */\ninterface Capture {\n chunks: Buffer[];\n detector: SpeechDetector;\n conversationId: string;\n /** Wake word that opened this capture; '' when the mic was re-opened for a follow-up/question. */\n wakeWord: string;\n timer: NodeJS.Timeout;\n closed: boolean;\n}\n\n/** One satellite: socket lifecycle, handshake, and the voice pipeline. */\nclass EsphomeConnection {\n /** Device name as reported by the device; falls back to its address until the handshake answers. */\n deviceName: string;\n online = false;\n /** Last wake-word configuration the device reported; null until the handshake answers. */\n wakeWordConfig: WakeWordConfig | null = null;\n /**\n * One entry per configuration request in flight, in send order (the device answers each one, in\n * order): the ids a preceding `SetConfiguration` asked for, or null. The read-back is the only\n * failure signal the device gives.\n */\n private configChecks: (string[] | null)[] = [];\n /** Everything else the device exposes — rebuilt from scratch on every reconnect. */\n readonly entities = new EntityRegistry();\n\n private readonly host: string;\n private readonly port: number;\n private readonly room: string;\n\n private socket: net.Socket | null = null;\n private rx: Bytes = Buffer.alloc(0);\n private pingTimer: NodeJS.Timeout | null = null;\n private reconnectTimer: NodeJS.Timeout | null = null;\n private missedPings = 0;\n private stopping = false;\n private capture: Capture | null = null;\n private playbackTimer: NodeJS.Timeout | null = null;\n private readonly password: string;\n\n constructor(\n device: EsphomeDevice,\n private readonly opts: EsphomeSatellitesOptions,\n ) {\n this.host = device.ip.trim();\n this.port = device.port || DEFAULT_PORT;\n this.room = (device.room || '').trim();\n this.password = device.password || '';\n this.deviceName = this.host;\n }\n\n // ── lifecycle ───────────────────────────────────────────────────────────────────────────────────\n\n start(): void {\n this.connect();\n }\n\n async stop(): Promise<void> {\n this.stopping = true;\n this.clearTimers();\n this.abortCapture();\n const socket = this.socket;\n this.socket = null;\n if (socket) {\n try {\n socket.write(encode(new pb.DisconnectRequest()));\n } catch {\n /* socket already gone — nothing to say goodbye on */\n }\n socket.destroy();\n }\n this.setOffline();\n return Promise.resolve();\n }\n\n private connect(): void {\n if (this.stopping) {\n return;\n }\n this.rx = Buffer.alloc(0);\n this.missedPings = 0;\n // The device re-announces everything after a reconnect, and the keys are not stable across\n // reboots, so starting from a clean slate is the only safe option.\n this.entities.clear();\n this.configChecks = [];\n\n const socket = net.createConnection({ host: this.host, port: this.port }, () => {\n this.opts.log.debug(`ESPHome ${this.host}:${this.port} connected`);\n this.handshake();\n });\n this.socket = socket;\n socket.setNoDelay(true);\n\n socket.on('data', data => this.onData(data));\n socket.on('error', e => {\n // A satellite that is switched off is normal, so this is not an error-level event.\n this.opts.log.debug(`ESPHome ${this.host}: ${e.message}`);\n });\n socket.on('close', () => {\n if (this.socket === socket) {\n this.socket = null;\n this.onDisconnected();\n }\n });\n }\n\n private onDisconnected(): void {\n this.clearTimers();\n this.abortCapture();\n if (this.online) {\n this.opts.log.info(`ESPHome satellite offline: ${this.deviceName}`);\n }\n this.setOffline();\n this.scheduleReconnect();\n }\n\n private scheduleReconnect(): void {\n if (this.stopping || this.reconnectTimer) {\n return;\n }\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.connect();\n }, RECONNECT_MS);\n this.reconnectTimer.unref();\n }\n\n private clearTimers(): void {\n if (this.pingTimer) {\n clearInterval(this.pingTimer);\n this.pingTimer = null;\n }\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.playbackTimer) {\n clearTimeout(this.playbackTimer);\n this.playbackTimer = null;\n }\n }\n\n private setOffline(): void {\n if (this.online) {\n this.online = false;\n this.opts.onStatus?.(this.deviceName, this.room, 'offline');\n }\n }\n\n private status(state: SatelliteState): void {\n this.opts.onStatus?.(this.deviceName, this.room, state);\n }\n\n // ── protocol plumbing ───────────────────────────────────────────────────────────────────────────\n\n private send(message: PbMessage): void {\n const socket = this.socket;\n if (!socket || socket.destroyed) {\n return;\n }\n try {\n socket.write(encode(message));\n } catch (e) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: send failed — ${(e as Error).message}`);\n }\n }\n\n private onData(data: Buffer): void {\n this.rx = this.rx.length ? Buffer.concat([this.rx, data]) : data;\n let frames;\n try {\n ({ frames, rest: this.rx } = decode(this.rx));\n } catch (e) {\n this.opts.log.error(`ESPHome ${this.deviceName}: ${(e as Error).message}`);\n this.socket?.destroy();\n return;\n }\n for (const frame of frames) {\n if (frame.error) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: cannot parse ${frame.name}`);\n continue;\n }\n if (!frame.name || !frame.message) {\n continue; // unknown id — already skipped by its declared length\n }\n try {\n this.onMessage(frame.name, frame.message);\n } catch (e) {\n this.opts.log.warn(\n `ESPHome ${this.deviceName}: ${frame.name} handler failed — ${(e as Error).message}`,\n );\n }\n }\n }\n\n private handshake(): void {\n const hello = new pb.HelloRequest();\n hello.setClientInfo('ioBroker.assistant');\n hello.setApiVersionMajor(1);\n hello.setApiVersionMinor(10);\n this.send(hello);\n\n const auth = new pb.AuthenticationRequest();\n auth.setPassword(this.password);\n this.send(auth);\n\n this.send(new pb.DeviceInfoRequest());\n // Ask for the entity announcements *before* subscribing to their states: a state message whose\n // entity we have not seen yet has nothing to attach to and is dropped.\n this.send(new pb.ListEntitiesRequest());\n this.send(new pb.SubscribeStatesRequest());\n\n const subscribe = new pb.SubscribeVoiceAssistantRequest();\n subscribe.setSubscribe(true);\n subscribe.setFlags(SUBSCRIBE_API_AUDIO);\n this.send(subscribe);\n\n this.requestWakeWordConfig();\n\n this.pingTimer = setInterval(() => {\n if (++this.missedPings > 2) {\n this.opts.log.info(`ESPHome ${this.deviceName}: no reply to keepalive — reconnecting`);\n this.socket?.destroy();\n return;\n }\n this.send(new pb.PingRequest());\n }, PING_INTERVAL_MS);\n this.pingTimer.unref();\n }\n\n private onMessage(name: string, message: unknown): void {\n switch (name) {\n case 'HelloResponse': {\n const hello = message as HelloResponse;\n this.opts.log.debug(\n `ESPHome ${this.host}: ${hello.getServerInfo()} (API ${hello.getApiVersionMajor()}.${hello.getApiVersionMinor()})`,\n );\n return;\n }\n\n case 'AuthenticationResponse':\n if ((message as AuthenticationResponse).getInvalidPassword()) {\n this.opts.log.error(\n `ESPHome ${this.host}: API password rejected — check the device's password in the settings.`,\n );\n this.socket?.destroy();\n }\n return;\n\n case 'DeviceInfoResponse': {\n const info = message as DeviceInfoResponse;\n this.deviceName = info.getName() || this.host;\n this.online = true;\n this.opts.log.info(\n `ESPHome satellite online: ${this.deviceName} (${info.getModel()}) @ ${this.host}${this.room ? `, room ${this.room}` : ''}`,\n );\n this.status('idle');\n return;\n }\n\n case 'VoiceAssistantConfigurationResponse': {\n const config = message as VoiceAssistantConfigurationResponse;\n // A custom wake word the device already downloaded is listed twice: once from its own\n // model folder and once from our offer.\n const seen = new Set<string>();\n this.wakeWordConfig = {\n active: config.getActiveWakeWordsList(),\n available: config\n .getAvailableWakeWordsList()\n .map(w => ({\n id: w.getId(),\n phrase: w.getWakeWord(),\n languages: w.getTrainedLanguagesList(),\n }))\n .filter(w => !seen.has(w.id) && !!seen.add(w.id)),\n max: config.getMaxActiveWakeWords(),\n };\n const active = this.wakeWordConfig.active.join(', ') || '(none active)';\n const available = this.wakeWordConfig.available.map(w => w.id);\n this.opts.log.info(\n `ESPHome ${this.deviceName}: wake words ${active}${available.length ? ` (available: ${available.join(', ')})` : ''}`,\n );\n const requested = this.configChecks.shift();\n const missing = requested?.filter(id => !this.wakeWordConfig?.active.includes(id)) ?? [];\n if (missing.length) {\n // The device answers a model it cannot fetch or load with silence, not an error.\n this.opts.log.warn(\n `ESPHome ${this.deviceName}: could not activate wake word(s) ${missing.join(', ')}. ` +\n `For an uploaded wake word the device must reach ${this.mediaUrl('/')} and the model ` +\n `must be a microWakeWord .tflite.`,\n );\n }\n this.opts.onWakeWords?.(this.deviceName, this.room, this.wakeWordConfig);\n return;\n }\n\n case 'VoiceAssistantRequest':\n this.onPipelineRequest(message as VoiceAssistantRequestMsg);\n return;\n\n case 'VoiceAssistantAudio':\n this.onAudio(message as VoiceAssistantAudio);\n return;\n\n case 'VoiceAssistantAnnounceFinished':\n if (this.playbackTimer) {\n clearTimeout(this.playbackTimer);\n this.playbackTimer = null;\n }\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: playback finished (success=${(message as VoiceAssistantAnnounceFinished).getSuccess()})`,\n );\n this.status('idle');\n return;\n\n case 'PingRequest':\n this.send(new pb.PingResponse());\n return;\n\n case 'PingResponse':\n this.missedPings = 0;\n return;\n\n case 'GetTimeRequest': {\n // The device has no RTC and syncs its clock from us — its timer ring depends on it.\n const time = new pb.GetTimeResponse();\n time.setEpochSeconds(Math.floor(Date.now() / 1000));\n this.send(time);\n return;\n }\n\n case 'DisconnectRequest':\n this.send(new pb.DisconnectResponse());\n this.socket?.destroy();\n return;\n\n case 'ListEntitiesDoneResponse': {\n const list = this.entities.list();\n if (list.length) {\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: ${list.length} entities — ${list.map(e => e.objectId).join(', ')}`,\n );\n }\n this.opts.onEntities?.(this.deviceName, this.room, list);\n return;\n }\n\n default:\n if (isListEntityMessage(name)) {\n this.entities.addFromAnnouncement(name, message);\n } else if (isEntityStateMessage(name)) {\n const entity = this.entities.applyState(name, message);\n if (entity) {\n this.opts.onEntityState?.(this.deviceName, this.room, entity);\n }\n }\n return;\n }\n }\n\n /** Write one of the device's entities. See {@link EsphomeSatellites.setEntity}. */\n setEntity(objectId: string, value: unknown): boolean {\n const message = this.entities.command(objectId, value);\n if (!message) {\n const entity = this.entities.get(objectId);\n const known = this.entities\n .list()\n .map(e => e.objectId)\n .join(', ');\n const options = entity?.options ? ` (one of: ${entity.options.join(', ')})` : '';\n this.opts.log.warn(\n entity\n ? `ESPHome ${this.deviceName}: ${JSON.stringify(value)} is not a value \"${objectId}\" accepts${options}`\n : `ESPHome ${this.deviceName}: no entity \"${objectId}\" (have: ${known})`,\n );\n return false;\n }\n this.send(message);\n this.opts.log.debug(`ESPHome ${this.deviceName}: ${objectId} := ${JSON.stringify(value)}`);\n return true;\n }\n\n /** Push one assistant timer to the device so it can show and ring it itself. */\n sendTimerEvent(event: TimerEvent): void {\n const message = new pb.VoiceAssistantTimerEventResponse();\n message.setEventType(TIMER_EVENT[event.type]);\n message.setTimerId(event.id);\n message.setName(event.name);\n message.setTotalSeconds(Math.max(0, Math.round(event.totalSeconds)));\n message.setSecondsLeft(Math.max(0, Math.round(event.secondsLeft)));\n message.setIsActive(event.active);\n this.send(message);\n }\n\n // ── voice pipeline ──────────────────────────────────────────────────────────────────────────────\n\n private onPipelineRequest(request: VoiceAssistantRequestMsg): void {\n if (!request.getStart()) {\n this.abortCapture();\n return;\n }\n if (this.capture) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: pipeline already running — ignoring wake`);\n return;\n }\n\n const phrase = request.getWakeWordPhrase();\n this.opts.log.debug(`ESPHome ${this.deviceName}: wake \"${phrase || '(follow-up)'}\"`);\n\n const response = new pb.VoiceAssistantResponse();\n response.setPort(0); // 0 = stream the audio over this socket\n response.setError(false);\n this.send(response);\n\n const timer = setTimeout(() => {\n this.opts.log.warn(`ESPHome ${this.deviceName}: no end of speech within ${PIPELINE_TIMEOUT_MS} ms`);\n void this.finishCapture();\n }, PIPELINE_TIMEOUT_MS);\n timer.unref();\n\n this.capture = {\n chunks: [],\n detector: new SpeechDetector({\n sampleRate: MIC_SAMPLE_RATE,\n silenceMs: this.opts.silenceMs || undefined,\n // Microphone gain differs wildly between these devices, so fixed RMS levels do not\n // travel: derive them from the room instead, once the wake chirp is out of the way.\n skipMs: WAKE_CHIRP_MS,\n adaptive: true,\n }),\n conversationId: request.getConversationId(),\n wakeWord: phrase || '',\n timer,\n closed: false,\n };\n\n this.status('listening');\n this.event(EVENT.RUN_START);\n if (phrase) {\n this.event(EVENT.WAKE_WORD_END, { wake_word_phrase: phrase });\n }\n this.event(EVENT.STT_START);\n this.event(EVENT.STT_VAD_START);\n }\n\n private onAudio(audio: VoiceAssistantAudio): void {\n const capture = this.capture;\n if (!capture || capture.closed) {\n return;\n }\n const chunk = Buffer.from(audio.getData_asU8());\n if (chunk.length) {\n capture.chunks.push(chunk);\n if (capture.detector.push(chunk)) {\n void this.finishCapture();\n return;\n }\n }\n if (audio.getEnd()) {\n void this.finishCapture();\n }\n }\n\n /** Stop the mic stream and run STT → answer → TTS for what was captured. */\n private async finishCapture(): Promise<void> {\n const capture = this.capture;\n if (!capture || capture.closed) {\n return;\n }\n capture.closed = true;\n this.capture = null;\n clearTimeout(capture.timer);\n\n // Send this first: the device streams until it sees it.\n this.event(EVENT.STT_VAD_END);\n\n const pcm = Buffer.concat(capture.chunks);\n const seconds = pcm.length / 2 / MIC_SAMPLE_RATE;\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: captured ${seconds.toFixed(2)} s ` +\n `(peak RMS ${Math.round(capture.detector.peak)}, noise floor ${Math.round(capture.detector.noiseFloor)}, ` +\n `speech/silence gate ${Math.round(capture.detector.startLevel)}/${Math.round(capture.detector.endLevel)})`,\n );\n\n if (!pcm.length || !capture.detector.sawSpeech) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: nothing but silence — dropping the utterance`);\n this.event(EVENT.RUN_END);\n this.status('idle');\n return;\n }\n\n this.status('processing');\n try {\n const hints = this.opts.getHints ? await this.opts.getHints() : undefined;\n const sttStart = Date.now();\n const text = (await this.opts.stt.transcribe(pcm, MIC_SAMPLE_RATE, this.opts.language, hints)).trim();\n const sttMs = Date.now() - sttStart;\n this.event(EVENT.STT_END, { text });\n if (!text) {\n this.event(EVENT.RUN_END);\n this.status('idle');\n return;\n }\n\n this.event(EVENT.INTENT_START);\n const answer = (\n await this.opts.answer(text, {\n device: this.deviceName,\n room: this.room,\n wakeWord: capture.wakeWord,\n sttMs,\n audioMs: Math.round(seconds * 1000),\n vad: {\n peak: Math.round(capture.detector.peak),\n noiseFloor: Math.round(capture.detector.noiseFloor),\n startLevel: Math.round(capture.detector.startLevel),\n endLevel: Math.round(capture.detector.endLevel),\n },\n })\n ).trim();\n const followUp = !!answer && this.opts.expectsFollowUp?.(answer, this.deviceName) === true;\n this.event(EVENT.INTENT_END, followUp ? { continue_conversation: '1' } : undefined);\n\n if (!answer) {\n this.event(EVENT.RUN_END);\n this.status('idle');\n return;\n }\n\n const ttsStart = Date.now();\n const { pcm: ttsPcm, sampleRate } = await this.opts.tts.synthesize(answer, this.opts.language);\n this.opts.onSpoken?.(this.deviceName, {\n ms: Date.now() - ttsStart,\n audioMs: (ttsPcm.length / 2 / sampleRate) * 1000,\n });\n const url = this.publish(ttsPcm, sampleRate);\n\n this.status('speaking');\n this.event(EVENT.TTS_START, { text: answer });\n this.event(EVENT.TTS_END, { url });\n this.event(EVENT.RUN_END, followUp ? { continue_conversation: '1' } : undefined);\n this.armPlaybackTimeout();\n } catch (e) {\n const reason = (e as Error).message;\n this.opts.log.error(`ESPHome ${this.deviceName}: voice pipeline failed — ${reason}`);\n // Without this the device stays ducked and its LED keeps spinning.\n this.event(EVENT.ERROR, { code: 'pipeline-failed', message: reason });\n this.event(EVENT.RUN_END);\n this.status('idle');\n }\n }\n\n /**\n * Select which wake words listen on this device. Unknown ids are dropped and the list is trimmed\n * to what the firmware allows, because the device answers a rejected list with silence rather than\n * an error — better to send something valid and report what we changed.\n *\n * The device persists the selection and echoes it back in a `VoiceAssistantConfigurationResponse`,\n * so we ask for one instead of assuming the write took.\n */\n setWakeWords(ids: string[]): void {\n const known = this.wakeWordConfig?.available.map(w => w.id) ?? [];\n const wanted = [...new Set(ids.map(i => i.trim()).filter(Boolean))];\n const unknown = wanted.filter(i => !known.includes(i));\n if (unknown.length) {\n this.opts.log.warn(\n `ESPHome ${this.deviceName}: unknown wake word(s) ${unknown.join(', ')} — ignored. ` +\n `Available: ${known.join(', ') || '(none reported)'}`,\n );\n }\n let valid = wanted.filter(i => known.includes(i));\n const max = this.wakeWordConfig?.max || 0;\n if (max > 0 && valid.length > max) {\n this.opts.log.warn(\n `ESPHome ${this.deviceName}: device allows ${max} active wake word(s), keeping ${valid\n .slice(0, max)\n .join(', ')}`,\n );\n valid = valid.slice(0, max);\n }\n\n const message = new pb.VoiceAssistantSetConfiguration();\n message.setActiveWakeWordsList(valid);\n this.send(message);\n this.opts.log.info(`ESPHome ${this.deviceName}: wake words set to ${valid.join(', ') || '(none)'}`);\n // Read back, so the reported state is the device's and not our guess. The device resolves a\n // custom wake word (and downloads it) while handling the SetConfiguration, so this answer\n // already tells whether that worked.\n this.requestWakeWordConfig(valid);\n }\n\n /**\n * Ask the device for its wake-word configuration. The request always carries the current custom\n * wake words: the firmware replaces its list of offered models with every request, so one sent\n * without them would make an uploaded wake word vanish from the device until the next offer.\n *\n * @param check ids a SetConfiguration just asked for — warned about if the answer lacks them\n */\n requestWakeWordConfig(check: string[] | null = null): void {\n const request = new pb.VoiceAssistantConfigurationRequest();\n const offers = this.opts.externalWakeWords?.() ?? [];\n request.setExternalWakeWordsList(\n offers.map(offer => {\n const w = new pb.VoiceAssistantExternalWakeWord();\n w.setId(offer.id);\n w.setWakeWord(offer.phrase);\n w.setTrainedLanguagesList(offer.languages);\n w.setModelType('micro');\n w.setModelSize(offer.size);\n w.setModelHash(offer.sha256);\n w.setUrl(this.mediaUrl(offer.path));\n return w;\n }),\n );\n // A device that never answers must not let this grow without bound.\n if (this.configChecks.length < 16) {\n this.configChecks.push(check);\n }\n this.send(request);\n }\n\n /** Drop a half-finished capture (device went away, or told us to stop). */\n private abortCapture(): void {\n const capture = this.capture;\n if (!capture) {\n return;\n }\n this.capture = null;\n capture.closed = true;\n clearTimeout(capture.timer);\n }\n\n // ── announcements ───────────────────────────────────────────────────────────────────────────────\n\n /**\n * Speak a ready PCM buffer on this device (timers, alarms, `tts.text`, jingles). With\n * `startConversation` the microphone opens once the clip has played, to catch the answer to a question.\n */\n announce(pcm: Buffer, sampleRate: number, startConversation = false): Promise<void> {\n if (!this.online || !pcm.length) {\n return Promise.resolve();\n }\n const request = new pb.VoiceAssistantAnnounceRequest();\n request.setMediaId(this.publish(pcm, sampleRate));\n request.setText('');\n request.setPreannounceMediaId('');\n // Needs feature flag 32 (START_CONVERSATION); on a device without it the clip still plays and\n // only the microphone stays shut, so there is nothing to guard here.\n request.setStartConversation(startConversation);\n this.status('speaking');\n this.send(request);\n this.armPlaybackTimeout();\n return Promise.resolve();\n }\n\n /**\n * Publish PCM as a WAV on the media server and build the URL the device should fetch.\n *\n * The host matters: on a multi-homed ioBroker machine the device can usually only reach one of the\n * addresses, and the local end of *this* socket is by definition one it can talk to.\n */\n private publish(pcm: Buffer, sampleRate: number): string {\n return this.mediaUrl(this.opts.media.publish(pcmToWav(pcm, sampleRate), 'audio/wav'));\n }\n\n /** Absolute URL of a path on the media server, with a host this device can reach. */\n private mediaUrl(path: string): string {\n const local = (this.socket?.localAddress || '').replace(/^::ffff:/, '');\n const host = this.opts.mediaHost?.trim() || local || '127.0.0.1';\n // An IPv6 literal has to be bracketed in a URL.\n const authority = host.includes(':') ? `[${host}]` : host;\n return `http://${authority}:${this.opts.media.port}${path}`;\n }\n\n /** Fall back to `idle` if the device never reports that it finished playing. */\n private armPlaybackTimeout(): void {\n if (this.playbackTimer) {\n clearTimeout(this.playbackTimer);\n }\n this.playbackTimer = setTimeout(() => {\n this.playbackTimer = null;\n this.status('idle');\n }, PLAYBACK_TIMEOUT_MS);\n this.playbackTimer.unref();\n }\n\n private event(type: number, data?: Record<string, string>): void {\n const message = new pb.VoiceAssistantEventResponse();\n message.setEventType(type);\n const items: VoiceAssistantEventData[] = [];\n for (const [name, value] of Object.entries(data || {})) {\n const item = new pb.VoiceAssistantEventData();\n item.setName(name);\n item.setValue(value);\n items.push(item);\n }\n message.setDataList(items);\n this.send(message);\n }\n}\n"]}
1
+ {"version":3,"file":"esphome.js","sourceRoot":"","sources":["../../../src/lib/voice/esphome.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ESPHome voice satellites — the fourth transport, next to the UDP server (`voiceServer.ts`) and the\n * Wyoming endpoint (`wyoming.ts`).\n *\n * Unlike those two, this one is a **client**: an ESPHome voice satellite is a *server* that waits on\n * TCP 6053 for the home-automation controller to connect and drive it. We therefore dial out to each\n * configured device and play the role Home Assistant normally plays. That covers the ThirdReality\n * Voice & Music Assistant (Dev Edition), the Home Assistant Voice PE and any box running\n * OHF-Voice/linux-voice-assistant.\n *\n * Flow for one utterance (verified against the firmware's `src/satellite/Satellite.cpp`):\n *\n * device → VoiceAssistantRequest{start, wake_word_phrase} wake word fired on the device\n * we → VoiceAssistantResponse{port:0} 0 = audio over the API, no UDP\n * we → RUN_START, WAKE_WORD_END, STT_START, STT_VAD_START\n * device → VoiceAssistantAudio … 16 kHz mono s16le, *until told to stop*\n * we → STT_VAD_END ← the device has no VAD of its own\n * we → STT_END{text}, INTENT_START, INTENT_END, TTS_START{text}, TTS_END{url}, RUN_END\n * device → fetches the URL over HTTP, plays it, VoiceAssistantAnnounceFinished\n *\n * Two consequences shape this file: end-of-speech detection is ours (`vad.ts`), and the spoken reply\n * has to be reachable over HTTP (`mediaServer.ts`) rather than streamed down the socket.\n */\nimport * as net from 'node:net';\nimport {\n pb,\n encode,\n decode,\n type PbMessage,\n type AuthenticationResponse,\n type DeviceInfoResponse,\n type HelloResponse,\n type VoiceAssistantAnnounceFinished,\n type VoiceAssistantAudio,\n type VoiceAssistantConfigurationResponse,\n type VoiceAssistantEventData,\n type VoiceAssistantRequestMsg,\n} from './esphomeProto';\nimport { EntityRegistry, isEntityStateMessage, isListEntityMessage, type EsphomeEntity } from './esphomeEntities';\nimport { SpeechDetector, type SpeechEnd, type Bytes } from './vad';\nimport { pcmToWav } from './stt';\nimport type { DiscardedCapture, DiscardReason } from './captures';\nimport type { SttEngine } from './stt';\nimport type { TtsEngine } from './tts';\nimport type { SatelliteState } from './protocol';\n\n/** `VoiceAssistantEvent` (api.proto) — the subset we emit. */\nconst EVENT = {\n ERROR: 0,\n RUN_START: 1,\n RUN_END: 2,\n STT_START: 3,\n STT_END: 4,\n INTENT_START: 5,\n INTENT_END: 6,\n TTS_START: 7,\n TTS_END: 8,\n WAKE_WORD_END: 10,\n STT_VAD_START: 11,\n STT_VAD_END: 12,\n} as const;\n\n/** `SubscribeVoiceAssistantRequest.flags`: audio over the API socket instead of a UDP side channel. */\nconst SUBSCRIBE_API_AUDIO = 1;\n/** ESPHome voice satellites always capture at 16 kHz mono 16-bit. */\nconst MIC_SAMPLE_RATE = 16000;\n/**\n * Leading audio to keep out of the end-of-speech analysis. These devices acknowledge the wake word\n * with a chirp through their own speaker and hear it back on their own microphone, far louder than\n * the speech that follows. Without this the chirp alone satisfies the detector's speech test — so\n * `sawSpeech` stops meaning \"somebody said something\" and a false wake is billed to STT — and it\n * drags the noise floor up with it.\n *\n * Measured over four captures from a ThirdReality Voice & Music Assistant (RMS per 50 ms, remarkably\n * repeatable across all four):\n *\n * ms 0 50 100 150 200 250 300 350 400 450 500 550 600\n * rms ~20 ~900 ~11k ~19k ~14k ~1.3k ~990 ~780 ~610 ~370 ~330 ~165 ~180\n *\n * so the chirp is still above the gate at 500 ms and only clears it around 550–600. 600 ms is the\n * smallest value that rejects a chirp-only wake while still detecting the speech in every one of the\n * four captures, with no change to when the utterance closes. The audio itself is untouched and still\n * goes to STT in full, so a speaker who starts inside this window loses nothing but a late gate open.\n */\nconst WAKE_CHIRP_MS = 600;\n/** Default API port of an ESPHome device. */\nconst DEFAULT_PORT = 6053;\n/** Keepalive interval; two missed replies count as a dead link. */\nconst PING_INTERVAL_MS = 20_000;\n/** Wait between reconnect attempts. Devices reboot, and a Wi-Fi speaker drops off now and then. */\nconst RECONNECT_MS = 10_000;\n/** Give up on a pipeline that produced no audio at all (device wedged mid-stream). */\nconst PIPELINE_TIMEOUT_MS = 30_000;\n/** How long we wait for the device to report that it finished playing before forcing it back to idle. */\nconst PLAYBACK_TIMEOUT_MS = 60_000;\n/**\n * Default lowest RMS that counts as speech (the noise floor can only raise the gate above it). Fits the\n * ThirdReality speaker; quieter microphones get their own value per device ({@link EsphomeDevice.speechLevel}).\n */\nexport const DEFAULT_SPEECH_LEVEL = 250;\n/**\n * Close the microphone when nobody has started speaking this long after the wake word. Without it a false wake\n * kept the stream open for the full 12 s cap: on a ThirdReality speaker with a custom wake word that was ten\n * times in one night, each 12.6 s of an open mic (and of music ducked, if any played).\n */\nconst NO_SPEECH_MS = 5000;\n/** The same after a follow-up question re-opened the mic: the user may need a moment to think. */\nconst NO_SPEECH_FOLLOW_UP_MS = 8000;\n/** Silence threshold as a share of the speech threshold — keeps the default pair 250/150. */\nconst SILENCE_SHARE = 0.6;\n/**\n * A dropped utterance whose loudest frame reached this share of the speech threshold was probably a voice\n * that was too quiet, not an empty room — worth telling the user where to lower the threshold.\n */\nconst QUIET_VOICE_SHARE = 0.4;\n\n/** The configured speech threshold of a device, or 0 for the default. Table cells may arrive as strings. */\nexport function speechLevelOf(device: EsphomeDevice): number {\n const level = Math.round(Number(device.speechLevel));\n return Number.isFinite(level) && level > 0 ? level : 0;\n}\n\nexport interface EsphomeDevice {\n /** Host name or IP of the satellite. */\n ip: string;\n /** API port; empty/0 → 6053. */\n port?: number;\n /** API password, if one is configured on the device. */\n password?: string;\n /** Room this satellite belongs to (drives `satellites.<room>.*` and the answer context). */\n room?: string;\n /**\n * Lowest RMS that counts as speech on this device (empty/0 → {@link DEFAULT_SPEECH_LEVEL}). Microphone\n * levels differ a lot: a ThirdReality speaker hears normal speech at RMS 350–470, a phone running a\n * satellite app at under 200, so one fixed floor cannot fit both.\n */\n speechLevel?: number | string | null;\n}\n\nexport interface EsphomeSatellitesOptions {\n devices: EsphomeDevice[];\n /** ISO-639-1 language hint for STT/TTS ('' = auto/provider default). */\n language: string;\n stt: SttEngine;\n tts: TtsEngine;\n /**\n * Produce the assistant reply for a transcribed utterance. `audioMs` is the length of the recording,\n * `vad` what the voice activity detection made of it (both for the request journal).\n */\n answer: (\n question: string,\n ctx: {\n device: string;\n room: string;\n wakeWord: string;\n sttMs: number;\n audioMs: number;\n vad: { peak: number; noiseFloor: number; startLevel: number; endLevel: number };\n },\n ) => Promise<string>;\n /** The reply was synthesised: how long that took and how long it plays (for the request journal). */\n onSpoken?: (device: string, info: { ms: number; audioMs: number }) => void;\n /** Optional STT vocabulary hints (device/room names) to bias recognition; re-read per utterance. */\n getHints?: () => string[] | Promise<string[]>;\n /** Where spoken replies are published for the device to fetch. */\n media: MediaHost;\n /**\n * Host the device should call back on. Empty → the local address of our socket to that device,\n * which is automatically the right interface on a multi-homed host.\n */\n mediaHost?: string;\n /** Silence that ends an utterance, in ms ('' / 0 → the detector default). */\n silenceMs?: number;\n log: ioBroker.Logger;\n /** Notified on every satellite state transition (idle/listening/processing/speaking/offline). */\n onStatus?: (device: string, room: string, state: SatelliteState | 'offline') => void;\n /** Notified with the device's wake-word configuration, on connect and after every change. */\n onWakeWords?: (device: string, room: string, config: WakeWordConfig) => void;\n /** Notified once per connect with everything the device exposes besides the voice pipeline. */\n onEntities?: (device: string, room: string, entities: EsphomeEntity[]) => void;\n /** Notified whenever one of those entities changes (and once per entity right after connecting). */\n onEntityState?: (device: string, room: string, entity: EsphomeEntity) => void;\n /** True when the reply asks something back, so the device should re-open its mic without a wake word. */\n expectsFollowUp?: (answer: string, device: string) => boolean;\n /**\n * A recording that never became a request: dropped as silence, or returned empty by the speech recognition.\n * For diagnosis (the adapter stores it when asked to); absent → nothing is kept.\n */\n onDiscarded?: (capture: DiscardedCapture) => void;\n /**\n * Custom wake words to offer the devices, re-read for every configuration request. The device lists\n * them next to its own and downloads one (from the media server) only when it is selected.\n */\n externalWakeWords?: () => ExternalWakeWordOffer[];\n}\n\n/** A custom wake word as offered to the devices; the files are on the media server under `path`. */\nexport interface ExternalWakeWordOffer {\n id: string;\n phrase: string;\n languages: string[];\n /** Size of the `.tflite` in bytes. */\n size: number;\n /** SHA-256 of the `.tflite`, lower-case hex. */\n sha256: string;\n /** URL path of the JSON config, e.g. `/wakeword/hey_hannah_1a2b3c4d.json`; the model sits next to it. */\n path: string;\n}\n\n/** What a device reports about its wake words: what the firmware ships plus the custom ones we offered. */\nexport interface WakeWordConfig {\n /** Ids currently listening, e.g. `['okay_nabu']`. */\n active: string[];\n /** Every wake word the device can use, with the spoken phrase and the languages it was trained on. */\n available: { id: string; phrase: string; languages: string[] }[];\n /** How many may listen at once — 2 on the ThirdReality speaker, which has a sensitivity per slot. */\n max: number;\n}\n\n/** `VoiceAssistantTimerEvent` in api.proto. */\nexport const TIMER_EVENT = { started: 0, updated: 1, cancelled: 2, finished: 3 } as const;\n\n/** One assistant timer, as the device wants to hear about it. */\nexport interface TimerEvent {\n type: keyof typeof TIMER_EVENT;\n id: string;\n name: string;\n totalSeconds: number;\n secondsLeft: number;\n /** False once it has finished or been cancelled — the device clears its ring on that. */\n active: boolean;\n}\n\n/** The slice of `MediaServer` this module needs (kept narrow so tests can pass a stub). */\nexport interface MediaHost {\n port: number;\n publish(body: Buffer, contentType?: string, ttlMs?: number): string;\n}\n\n/**\n * All configured ESPHome satellites. Owns one connection per device and offers the same surface the\n * adapter already uses for the UDP server: `devices()` and `announce()`.\n */\nexport class EsphomeSatellites {\n private readonly connections: EsphomeConnection[] = [];\n\n constructor(private readonly opts: EsphomeSatellitesOptions) {}\n\n async start(): Promise<void> {\n for (const device of this.opts.devices) {\n if (!device.ip?.trim()) {\n continue;\n }\n const connection = new EsphomeConnection(device, this.opts);\n this.connections.push(connection);\n connection.start();\n }\n if (!this.connections.length) {\n this.opts.log.warn('ESPHome satellites enabled, but no device addresses are configured.');\n }\n return Promise.resolve();\n }\n\n async stop(): Promise<void> {\n await Promise.all(this.connections.map(c => c.stop()));\n this.connections.length = 0;\n }\n\n /** Device names of the satellites currently connected. */\n devices(): string[] {\n return this.connections.filter(c => c.online).map(c => c.deviceName);\n }\n\n /** Wake-word configuration of one connected satellite, or null if it is unknown/offline. */\n wakeWords(device: string): WakeWordConfig | null {\n return this.connections.find(c => c.online && c.deviceName === device)?.wakeWordConfig ?? null;\n }\n\n /**\n * Choose which wake words listen on one satellite. Ids come from {@link WakeWordConfig.available};\n * the device stores the selection itself, so it survives a restart of the adapter.\n */\n setWakeWords(device: string, ids: string[]): boolean {\n const target = this.connections.find(c => c.online && c.deviceName === device);\n if (!target) {\n return false;\n }\n target.setWakeWords(ids);\n return true;\n }\n\n /**\n * Offer the current custom wake words to every connected satellite again — after one was uploaded\n * or deleted. Each device answers with its new configuration (→ `onWakeWords`).\n */\n refreshWakeWords(): number {\n const online = this.connections.filter(c => c.online);\n online.forEach(c => c.requestWakeWordConfig());\n return online.length;\n }\n\n /** Everything one satellite exposes besides the voice pipeline (gain, noise suppression, …). */\n entities(device: string): EsphomeEntity[] {\n return this.connections.find(c => c.online && c.deviceName === device)?.entities.list() ?? [];\n }\n\n /**\n * Write one of those entities. Returns false when the satellite is offline, the entity is unknown\n * or read-only, or the value is not one the device accepts.\n */\n setEntity(device: string, objectId: string, value: unknown): boolean {\n const target = this.connections.find(c => c.online && c.deviceName === device);\n return target ? target.setEntity(objectId, value) : false;\n }\n\n /**\n * Mirror an assistant timer onto the satellites so they can show it on their own LED ring and ring\n * it themselves. `device` null broadcasts. Returns how many devices were told.\n */\n timerEvent(device: string | null, event: TimerEvent): number {\n const targets = this.connections.filter(c => c.online && (!device || c.deviceName === device));\n targets.forEach(c => c.sendTimerEvent(event));\n return targets.length;\n }\n\n /**\n * Play a ready PCM buffer on one satellite (by device name) or all of them. Returns how many\n * devices it was handed to.\n *\n * With `startConversation` the device opens its microphone as soon as the clip has played, without\n * a wake word — that is how a question gets its answer (see `PendingQuestions`).\n */\n async announce(device: string | null, pcm: Buffer, sampleRate: number, startConversation = false): Promise<number> {\n const targets = this.connections.filter(c => c.online && (!device || c.deviceName === device));\n if (!targets.length) {\n return 0;\n }\n await Promise.all(\n targets.map(c =>\n c\n .announce(pcm, sampleRate, startConversation)\n .catch(e => this.opts.log.warn(`announce to ${c.deviceName} failed: ${(e as Error).message}`)),\n ),\n );\n return targets.length;\n }\n}\n\n/** State of the utterance currently being captured. */\ninterface Capture {\n chunks: Buffer[];\n detector: SpeechDetector;\n conversationId: string;\n /** Wake word that opened this capture; '' when the mic was re-opened for a follow-up/question. */\n wakeWord: string;\n timer: NodeJS.Timeout;\n closed: boolean;\n}\n\n/** One satellite: socket lifecycle, handshake, and the voice pipeline. */\nclass EsphomeConnection {\n /** Device name as reported by the device; falls back to its address until the handshake answers. */\n deviceName: string;\n online = false;\n /** Last wake-word configuration the device reported; null until the handshake answers. */\n wakeWordConfig: WakeWordConfig | null = null;\n /**\n * One entry per configuration request in flight, in send order (the device answers each one, in\n * order): the ids a preceding `SetConfiguration` asked for, or null. The read-back is the only\n * failure signal the device gives.\n */\n private configChecks: (string[] | null)[] = [];\n /** Everything else the device exposes — rebuilt from scratch on every reconnect. */\n readonly entities = new EntityRegistry();\n\n private readonly host: string;\n private readonly port: number;\n private readonly room: string;\n\n private socket: net.Socket | null = null;\n private rx: Bytes = Buffer.alloc(0);\n private pingTimer: NodeJS.Timeout | null = null;\n private reconnectTimer: NodeJS.Timeout | null = null;\n private missedPings = 0;\n private stopping = false;\n private capture: Capture | null = null;\n private playbackTimer: NodeJS.Timeout | null = null;\n private readonly password: string;\n /** Configured speech threshold, 0 = {@link DEFAULT_SPEECH_LEVEL}. */\n private readonly speechLevel: number;\n\n constructor(\n device: EsphomeDevice,\n private readonly opts: EsphomeSatellitesOptions,\n ) {\n this.host = device.ip.trim();\n this.port = device.port || DEFAULT_PORT;\n this.room = (device.room || '').trim();\n this.password = device.password || '';\n this.speechLevel = speechLevelOf(device);\n this.deviceName = this.host;\n }\n\n // ── lifecycle ───────────────────────────────────────────────────────────────────────────────────\n\n start(): void {\n this.connect();\n }\n\n async stop(): Promise<void> {\n this.stopping = true;\n this.clearTimers();\n this.abortCapture();\n const socket = this.socket;\n this.socket = null;\n if (socket) {\n try {\n socket.write(encode(new pb.DisconnectRequest()));\n } catch {\n /* socket already gone — nothing to say goodbye on */\n }\n socket.destroy();\n }\n this.setOffline();\n return Promise.resolve();\n }\n\n private connect(): void {\n if (this.stopping) {\n return;\n }\n this.rx = Buffer.alloc(0);\n this.missedPings = 0;\n // The device re-announces everything after a reconnect, and the keys are not stable across\n // reboots, so starting from a clean slate is the only safe option.\n this.entities.clear();\n this.configChecks = [];\n\n const socket = net.createConnection({ host: this.host, port: this.port }, () => {\n this.opts.log.debug(`ESPHome ${this.host}:${this.port} connected`);\n this.handshake();\n });\n this.socket = socket;\n socket.setNoDelay(true);\n\n socket.on('data', data => this.onData(data));\n socket.on('error', e => {\n // A satellite that is switched off is normal, so this is not an error-level event.\n this.opts.log.debug(`ESPHome ${this.host}: ${e.message}`);\n });\n socket.on('close', () => {\n if (this.socket === socket) {\n this.socket = null;\n this.onDisconnected();\n }\n });\n }\n\n private onDisconnected(): void {\n this.clearTimers();\n this.abortCapture();\n if (this.online) {\n this.opts.log.info(`ESPHome satellite offline: ${this.deviceName}`);\n }\n this.setOffline();\n this.scheduleReconnect();\n }\n\n private scheduleReconnect(): void {\n if (this.stopping || this.reconnectTimer) {\n return;\n }\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.connect();\n }, RECONNECT_MS);\n this.reconnectTimer.unref();\n }\n\n private clearTimers(): void {\n if (this.pingTimer) {\n clearInterval(this.pingTimer);\n this.pingTimer = null;\n }\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.playbackTimer) {\n clearTimeout(this.playbackTimer);\n this.playbackTimer = null;\n }\n }\n\n private setOffline(): void {\n if (this.online) {\n this.online = false;\n this.opts.onStatus?.(this.deviceName, this.room, 'offline');\n }\n }\n\n private status(state: SatelliteState): void {\n this.opts.onStatus?.(this.deviceName, this.room, state);\n }\n\n // ── protocol plumbing ───────────────────────────────────────────────────────────────────────────\n\n private send(message: PbMessage): void {\n const socket = this.socket;\n if (!socket || socket.destroyed) {\n return;\n }\n try {\n socket.write(encode(message));\n } catch (e) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: send failed — ${(e as Error).message}`);\n }\n }\n\n private onData(data: Buffer): void {\n this.rx = this.rx.length ? Buffer.concat([this.rx, data]) : data;\n let frames;\n try {\n ({ frames, rest: this.rx } = decode(this.rx));\n } catch (e) {\n this.opts.log.error(`ESPHome ${this.deviceName}: ${(e as Error).message}`);\n this.socket?.destroy();\n return;\n }\n for (const frame of frames) {\n if (frame.error) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: cannot parse ${frame.name}`);\n continue;\n }\n if (!frame.name || !frame.message) {\n continue; // unknown id — already skipped by its declared length\n }\n try {\n this.onMessage(frame.name, frame.message);\n } catch (e) {\n this.opts.log.warn(\n `ESPHome ${this.deviceName}: ${frame.name} handler failed — ${(e as Error).message}`,\n );\n }\n }\n }\n\n private handshake(): void {\n const hello = new pb.HelloRequest();\n hello.setClientInfo('ioBroker.assistant');\n hello.setApiVersionMajor(1);\n hello.setApiVersionMinor(10);\n this.send(hello);\n\n const auth = new pb.AuthenticationRequest();\n auth.setPassword(this.password);\n this.send(auth);\n\n this.send(new pb.DeviceInfoRequest());\n // Ask for the entity announcements *before* subscribing to their states: a state message whose\n // entity we have not seen yet has nothing to attach to and is dropped.\n this.send(new pb.ListEntitiesRequest());\n this.send(new pb.SubscribeStatesRequest());\n\n const subscribe = new pb.SubscribeVoiceAssistantRequest();\n subscribe.setSubscribe(true);\n subscribe.setFlags(SUBSCRIBE_API_AUDIO);\n this.send(subscribe);\n\n this.requestWakeWordConfig();\n\n this.pingTimer = setInterval(() => {\n if (++this.missedPings > 2) {\n this.opts.log.info(`ESPHome ${this.deviceName}: no reply to keepalive — reconnecting`);\n this.socket?.destroy();\n return;\n }\n this.send(new pb.PingRequest());\n }, PING_INTERVAL_MS);\n this.pingTimer.unref();\n }\n\n private onMessage(name: string, message: unknown): void {\n switch (name) {\n case 'HelloResponse': {\n const hello = message as HelloResponse;\n this.opts.log.debug(\n `ESPHome ${this.host}: ${hello.getServerInfo()} (API ${hello.getApiVersionMajor()}.${hello.getApiVersionMinor()})`,\n );\n return;\n }\n\n case 'AuthenticationResponse':\n if ((message as AuthenticationResponse).getInvalidPassword()) {\n this.opts.log.error(\n `ESPHome ${this.host}: API password rejected — check the device's password in the settings.`,\n );\n this.socket?.destroy();\n }\n return;\n\n case 'DeviceInfoResponse': {\n const info = message as DeviceInfoResponse;\n this.deviceName = info.getName() || this.host;\n this.online = true;\n this.opts.log.info(\n `ESPHome satellite online: ${this.deviceName} (${info.getModel()}) @ ${this.host}${this.room ? `, room ${this.room}` : ''}`,\n );\n this.status('idle');\n return;\n }\n\n case 'VoiceAssistantConfigurationResponse': {\n const config = message as VoiceAssistantConfigurationResponse;\n // A custom wake word the device already downloaded is listed twice: once from its own\n // model folder and once from our offer.\n const seen = new Set<string>();\n this.wakeWordConfig = {\n active: config.getActiveWakeWordsList(),\n available: config\n .getAvailableWakeWordsList()\n .map(w => ({\n id: w.getId(),\n phrase: w.getWakeWord(),\n languages: w.getTrainedLanguagesList(),\n }))\n .filter(w => !seen.has(w.id) && !!seen.add(w.id)),\n max: config.getMaxActiveWakeWords(),\n };\n const active = this.wakeWordConfig.active.join(', ') || '(none active)';\n const available = this.wakeWordConfig.available.map(w => w.id);\n this.opts.log.info(\n `ESPHome ${this.deviceName}: wake words ${active}${available.length ? ` (available: ${available.join(', ')})` : ''}`,\n );\n const requested = this.configChecks.shift();\n const missing = requested?.filter(id => !this.wakeWordConfig?.active.includes(id)) ?? [];\n if (missing.length) {\n // The device answers a model it cannot fetch or load with silence, not an error.\n this.opts.log.warn(\n `ESPHome ${this.deviceName}: could not activate wake word(s) ${missing.join(', ')}. ` +\n `For an uploaded wake word the device must reach ${this.mediaUrl('/')} and the model ` +\n `must be a microWakeWord .tflite.`,\n );\n }\n this.opts.onWakeWords?.(this.deviceName, this.room, this.wakeWordConfig);\n return;\n }\n\n case 'VoiceAssistantRequest':\n this.onPipelineRequest(message as VoiceAssistantRequestMsg);\n return;\n\n case 'VoiceAssistantAudio':\n this.onAudio(message as VoiceAssistantAudio);\n return;\n\n case 'VoiceAssistantAnnounceFinished':\n if (this.playbackTimer) {\n clearTimeout(this.playbackTimer);\n this.playbackTimer = null;\n }\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: playback finished (success=${(message as VoiceAssistantAnnounceFinished).getSuccess()})`,\n );\n this.status('idle');\n return;\n\n case 'PingRequest':\n this.send(new pb.PingResponse());\n return;\n\n case 'PingResponse':\n this.missedPings = 0;\n return;\n\n case 'GetTimeRequest': {\n // The device has no RTC and syncs its clock from us — its timer ring depends on it.\n const time = new pb.GetTimeResponse();\n time.setEpochSeconds(Math.floor(Date.now() / 1000));\n this.send(time);\n return;\n }\n\n case 'DisconnectRequest':\n this.send(new pb.DisconnectResponse());\n this.socket?.destroy();\n return;\n\n case 'ListEntitiesDoneResponse': {\n const list = this.entities.list();\n if (list.length) {\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: ${list.length} entities — ${list.map(e => e.objectId).join(', ')}`,\n );\n }\n this.opts.onEntities?.(this.deviceName, this.room, list);\n return;\n }\n\n default:\n if (isListEntityMessage(name)) {\n this.entities.addFromAnnouncement(name, message);\n } else if (isEntityStateMessage(name)) {\n const entity = this.entities.applyState(name, message);\n if (entity) {\n this.opts.onEntityState?.(this.deviceName, this.room, entity);\n }\n }\n return;\n }\n }\n\n /** Write one of the device's entities. See {@link EsphomeSatellites.setEntity}. */\n setEntity(objectId: string, value: unknown): boolean {\n const message = this.entities.command(objectId, value);\n if (!message) {\n const entity = this.entities.get(objectId);\n const known = this.entities\n .list()\n .map(e => e.objectId)\n .join(', ');\n const options = entity?.options ? ` (one of: ${entity.options.join(', ')})` : '';\n this.opts.log.warn(\n entity\n ? `ESPHome ${this.deviceName}: ${JSON.stringify(value)} is not a value \"${objectId}\" accepts${options}`\n : `ESPHome ${this.deviceName}: no entity \"${objectId}\" (have: ${known})`,\n );\n return false;\n }\n this.send(message);\n this.opts.log.debug(`ESPHome ${this.deviceName}: ${objectId} := ${JSON.stringify(value)}`);\n return true;\n }\n\n /** Push one assistant timer to the device so it can show and ring it itself. */\n sendTimerEvent(event: TimerEvent): void {\n const message = new pb.VoiceAssistantTimerEventResponse();\n message.setEventType(TIMER_EVENT[event.type]);\n message.setTimerId(event.id);\n message.setName(event.name);\n message.setTotalSeconds(Math.max(0, Math.round(event.totalSeconds)));\n message.setSecondsLeft(Math.max(0, Math.round(event.secondsLeft)));\n message.setIsActive(event.active);\n this.send(message);\n }\n\n // ── voice pipeline ──────────────────────────────────────────────────────────────────────────────\n\n private onPipelineRequest(request: VoiceAssistantRequestMsg): void {\n if (!request.getStart()) {\n this.abortCapture();\n return;\n }\n if (this.capture) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: pipeline already running — ignoring wake`);\n return;\n }\n\n const phrase = request.getWakeWordPhrase();\n this.opts.log.debug(`ESPHome ${this.deviceName}: wake \"${phrase || '(follow-up)'}\"`);\n\n const response = new pb.VoiceAssistantResponse();\n response.setPort(0); // 0 = stream the audio over this socket\n response.setError(false);\n this.send(response);\n\n const timer = setTimeout(() => {\n this.opts.log.warn(`ESPHome ${this.deviceName}: no end of speech within ${PIPELINE_TIMEOUT_MS} ms`);\n void this.finishCapture();\n }, PIPELINE_TIMEOUT_MS);\n timer.unref();\n\n this.capture = {\n chunks: [],\n detector: new SpeechDetector({\n sampleRate: MIC_SAMPLE_RATE,\n silenceMs: this.opts.silenceMs || undefined,\n // Microphone gain differs wildly between these devices, so fixed RMS levels do not\n // travel: derive them from the room instead, once the wake chirp is out of the way.\n skipMs: WAKE_CHIRP_MS,\n adaptive: true,\n startTimeoutMs: phrase ? NO_SPEECH_MS : NO_SPEECH_FOLLOW_UP_MS,\n minStartLevel: this.speechLevel || DEFAULT_SPEECH_LEVEL,\n minEndLevel: Math.round((this.speechLevel || DEFAULT_SPEECH_LEVEL) * SILENCE_SHARE),\n }),\n conversationId: request.getConversationId(),\n wakeWord: phrase || '',\n timer,\n closed: false,\n };\n\n this.status('listening');\n this.event(EVENT.RUN_START);\n if (phrase) {\n this.event(EVENT.WAKE_WORD_END, { wake_word_phrase: phrase });\n }\n this.event(EVENT.STT_START);\n this.event(EVENT.STT_VAD_START);\n }\n\n private onAudio(audio: VoiceAssistantAudio): void {\n const capture = this.capture;\n if (!capture || capture.closed) {\n return;\n }\n const chunk = Buffer.from(audio.getData_asU8());\n if (chunk.length) {\n capture.chunks.push(chunk);\n const end = capture.detector.push(chunk);\n if (end) {\n void this.finishCapture(end);\n return;\n }\n }\n if (audio.getEnd()) {\n void this.finishCapture();\n }\n }\n\n /** Stop the mic stream and run STT → answer → TTS for what was captured. */\n /** Hand a recording that never became a request to {@link EsphomeSatellitesOptions.onDiscarded}. */\n private discard(capture: Capture, pcm: Bytes, reason: DiscardReason): void {\n if (!this.opts.onDiscarded || !pcm.length) {\n return;\n }\n const d = capture.detector;\n try {\n this.opts.onDiscarded({\n device: this.deviceName,\n room: this.room,\n wakeWord: capture.wakeWord,\n reason,\n pcm,\n sampleRate: MIC_SAMPLE_RATE,\n vad: {\n peak: Math.round(d.peak),\n analysedPeak: Math.round(d.analysedPeak),\n noiseFloor: Math.round(d.noiseFloor),\n startLevel: Math.round(d.startLevel),\n endLevel: Math.round(d.endLevel),\n },\n });\n } catch (e) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: keeping the recording failed — ${(e as Error).message}`);\n }\n }\n\n private async finishCapture(end?: SpeechEnd): Promise<void> {\n const capture = this.capture;\n if (!capture || capture.closed) {\n return;\n }\n capture.closed = true;\n this.capture = null;\n clearTimeout(capture.timer);\n\n // Send this first: the device streams until it sees it.\n this.event(EVENT.STT_VAD_END);\n\n const pcm = Buffer.concat(capture.chunks);\n const seconds = pcm.length / 2 / MIC_SAMPLE_RATE;\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: captured ${seconds.toFixed(2)} s ` +\n `(peak RMS ${Math.round(capture.detector.peak)}, noise floor ${Math.round(capture.detector.noiseFloor)}, ` +\n `speech/silence gate ${Math.round(capture.detector.startLevel)}/${Math.round(capture.detector.endLevel)})`,\n );\n\n if (!pcm.length || !capture.detector.sawSpeech) {\n const loudest = Math.round(capture.detector.analysedPeak);\n const gate = Math.round(capture.detector.startLevel);\n if (pcm.length && loudest >= gate * QUIET_VOICE_SHARE) {\n // Not an empty room: something came close to the gate. Typically a microphone that is quieter\n // than the default threshold expects (a phone) — say where to change it.\n this.opts.log.info(\n `ESPHome ${this.deviceName}: dropped the utterance — the loudest sound (RMS ${loudest}) stayed ` +\n `below the speech threshold ${gate}. If somebody spoke, lower \"Speech threshold\" for this ` +\n `device in the ESPHome satellites table (Voice tab), e.g. to ${Math.max(\n 30,\n Math.round((loudest * 0.6) / 10) * 10,\n )}.`,\n );\n } else {\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: nothing but silence${\n end === 'no-speech' ? ' (nobody started speaking)' : ''\n } — dropping the utterance`,\n );\n }\n this.discard(capture, pcm, end === 'no-speech' ? 'no-speech' : 'silence');\n this.event(EVENT.RUN_END);\n this.status('idle');\n return;\n }\n\n this.status('processing');\n try {\n const hints = this.opts.getHints ? await this.opts.getHints() : undefined;\n const sttStart = Date.now();\n const text = (await this.opts.stt.transcribe(pcm, MIC_SAMPLE_RATE, this.opts.language, hints)).trim();\n const sttMs = Date.now() - sttStart;\n this.event(EVENT.STT_END, { text });\n if (!text) {\n // Speech was heard, so the user waits for an answer: say why none comes. Typically a word in another\n // language than the STT listens in (a German \"Ja\" to a Russian STT comes back empty).\n this.opts.log.info(\n `Voice Q (${this.deviceName}): (empty) — speech recognition returned no text for ${seconds.toFixed(1)} s of audio`,\n );\n this.discard(capture, pcm, 'empty-stt');\n this.event(EVENT.RUN_END);\n this.status('idle');\n return;\n }\n\n this.event(EVENT.INTENT_START);\n const answer = (\n await this.opts.answer(text, {\n device: this.deviceName,\n room: this.room,\n wakeWord: capture.wakeWord,\n sttMs,\n audioMs: Math.round(seconds * 1000),\n vad: {\n peak: Math.round(capture.detector.peak),\n noiseFloor: Math.round(capture.detector.noiseFloor),\n startLevel: Math.round(capture.detector.startLevel),\n endLevel: Math.round(capture.detector.endLevel),\n },\n })\n ).trim();\n const followUp = !!answer && this.opts.expectsFollowUp?.(answer, this.deviceName) === true;\n this.event(EVENT.INTENT_END, followUp ? { continue_conversation: '1' } : undefined);\n\n if (!answer) {\n this.event(EVENT.RUN_END);\n this.status('idle');\n return;\n }\n\n const ttsStart = Date.now();\n const { pcm: ttsPcm, sampleRate } = await this.opts.tts.synthesize(answer, this.opts.language);\n this.opts.onSpoken?.(this.deviceName, {\n ms: Date.now() - ttsStart,\n audioMs: (ttsPcm.length / 2 / sampleRate) * 1000,\n });\n const url = this.publish(ttsPcm, sampleRate);\n\n this.status('speaking');\n this.event(EVENT.TTS_START, { text: answer });\n this.event(EVENT.TTS_END, { url });\n this.event(EVENT.RUN_END, followUp ? { continue_conversation: '1' } : undefined);\n this.armPlaybackTimeout();\n } catch (e) {\n const reason = (e as Error).message;\n this.opts.log.error(`ESPHome ${this.deviceName}: voice pipeline failed — ${reason}`);\n // Without this the device stays ducked and its LED keeps spinning.\n this.event(EVENT.ERROR, { code: 'pipeline-failed', message: reason });\n this.event(EVENT.RUN_END);\n this.status('idle');\n }\n }\n\n /**\n * Select which wake words listen on this device. Unknown ids are dropped and the list is trimmed\n * to what the firmware allows, because the device answers a rejected list with silence rather than\n * an error — better to send something valid and report what we changed.\n *\n * The device persists the selection and echoes it back in a `VoiceAssistantConfigurationResponse`,\n * so we ask for one instead of assuming the write took.\n */\n setWakeWords(ids: string[]): void {\n const known = this.wakeWordConfig?.available.map(w => w.id) ?? [];\n const wanted = [...new Set(ids.map(i => i.trim()).filter(Boolean))];\n const unknown = wanted.filter(i => !known.includes(i));\n if (unknown.length) {\n this.opts.log.warn(\n `ESPHome ${this.deviceName}: unknown wake word(s) ${unknown.join(', ')} — ignored. ` +\n `Available: ${known.join(', ') || '(none reported)'}`,\n );\n }\n let valid = wanted.filter(i => known.includes(i));\n const max = this.wakeWordConfig?.max || 0;\n if (max > 0 && valid.length > max) {\n this.opts.log.warn(\n `ESPHome ${this.deviceName}: device allows ${max} active wake word(s), keeping ${valid\n .slice(0, max)\n .join(', ')}`,\n );\n valid = valid.slice(0, max);\n }\n\n const message = new pb.VoiceAssistantSetConfiguration();\n message.setActiveWakeWordsList(valid);\n this.send(message);\n this.opts.log.info(`ESPHome ${this.deviceName}: wake words set to ${valid.join(', ') || '(none)'}`);\n // Read back, so the reported state is the device's and not our guess. The device resolves a\n // custom wake word (and downloads it) while handling the SetConfiguration, so this answer\n // already tells whether that worked.\n this.requestWakeWordConfig(valid);\n }\n\n /**\n * Ask the device for its wake-word configuration. The request always carries the current custom\n * wake words: the firmware replaces its list of offered models with every request, so one sent\n * without them would make an uploaded wake word vanish from the device until the next offer.\n *\n * @param check ids a SetConfiguration just asked for — warned about if the answer lacks them\n */\n requestWakeWordConfig(check: string[] | null = null): void {\n const request = new pb.VoiceAssistantConfigurationRequest();\n const offers = this.opts.externalWakeWords?.() ?? [];\n request.setExternalWakeWordsList(\n offers.map(offer => {\n const w = new pb.VoiceAssistantExternalWakeWord();\n w.setId(offer.id);\n w.setWakeWord(offer.phrase);\n w.setTrainedLanguagesList(offer.languages);\n w.setModelType('micro');\n w.setModelSize(offer.size);\n w.setModelHash(offer.sha256);\n w.setUrl(this.mediaUrl(offer.path));\n return w;\n }),\n );\n // A device that never answers must not let this grow without bound.\n if (this.configChecks.length < 16) {\n this.configChecks.push(check);\n }\n this.send(request);\n }\n\n /** Drop a half-finished capture (device went away, or told us to stop). */\n private abortCapture(): void {\n const capture = this.capture;\n if (!capture) {\n return;\n }\n this.capture = null;\n capture.closed = true;\n clearTimeout(capture.timer);\n }\n\n // ── announcements ───────────────────────────────────────────────────────────────────────────────\n\n /**\n * Speak a ready PCM buffer on this device (timers, alarms, `tts.text`, jingles). With\n * `startConversation` the microphone opens once the clip has played, to catch the answer to a question.\n */\n announce(pcm: Buffer, sampleRate: number, startConversation = false): Promise<void> {\n if (!this.online || !pcm.length) {\n return Promise.resolve();\n }\n const request = new pb.VoiceAssistantAnnounceRequest();\n request.setMediaId(this.publish(pcm, sampleRate));\n request.setText('');\n request.setPreannounceMediaId('');\n // Needs feature flag 32 (START_CONVERSATION); on a device without it the clip still plays and\n // only the microphone stays shut, so there is nothing to guard here.\n request.setStartConversation(startConversation);\n this.status('speaking');\n this.send(request);\n this.armPlaybackTimeout();\n return Promise.resolve();\n }\n\n /**\n * Publish PCM as a WAV on the media server and build the URL the device should fetch.\n *\n * The host matters: on a multi-homed ioBroker machine the device can usually only reach one of the\n * addresses, and the local end of *this* socket is by definition one it can talk to.\n */\n private publish(pcm: Buffer, sampleRate: number): string {\n return this.mediaUrl(this.opts.media.publish(pcmToWav(pcm, sampleRate), 'audio/wav'));\n }\n\n /** Absolute URL of a path on the media server, with a host this device can reach. */\n private mediaUrl(path: string): string {\n const local = (this.socket?.localAddress || '').replace(/^::ffff:/, '');\n const host = this.opts.mediaHost?.trim() || local || '127.0.0.1';\n // An IPv6 literal has to be bracketed in a URL.\n const authority = host.includes(':') ? `[${host}]` : host;\n return `http://${authority}:${this.opts.media.port}${path}`;\n }\n\n /** Fall back to `idle` if the device never reports that it finished playing. */\n private armPlaybackTimeout(): void {\n if (this.playbackTimer) {\n clearTimeout(this.playbackTimer);\n }\n this.playbackTimer = setTimeout(() => {\n this.playbackTimer = null;\n this.status('idle');\n }, PLAYBACK_TIMEOUT_MS);\n this.playbackTimer.unref();\n }\n\n private event(type: number, data?: Record<string, string>): void {\n const message = new pb.VoiceAssistantEventResponse();\n message.setEventType(type);\n const items: VoiceAssistantEventData[] = [];\n for (const [name, value] of Object.entries(data || {})) {\n const item = new pb.VoiceAssistantEventData();\n item.setName(name);\n item.setValue(value);\n items.push(item);\n }\n message.setDataList(items);\n this.send(message);\n }\n}\n"]}
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CachedTts = exports.DEFAULT_CACHE_BYTES = exports.MAX_CACHE_CHARS = exports.MAX_TTS_CHARS = void 0;
37
+ exports.parseTtsMaxChars = parseTtsMaxChars;
38
+ exports.spokenLengthHint = spokenLengthHint;
37
39
  exports.isSsml = isSsml;
38
40
  exports.stripSsml = stripSsml;
39
41
  exports.truncateForTts = truncateForTts;
@@ -59,6 +61,51 @@ const fs = __importStar(require("node:fs"));
59
61
  const path = __importStar(require("node:path"));
60
62
  /** Hard limit for a spoken text. Hannah's 400 characters ≈ 25 seconds of speech. */
61
63
  exports.MAX_TTS_CHARS = 400;
64
+ /**
65
+ * The configured length limit for spoken text: a number ≥ 0 (0 = no limit), or undefined for the default
66
+ * ({@link MAX_TTS_CHARS}) — also for installations from before the setting and for a field the user emptied.
67
+ */
68
+ function parseTtsMaxChars(value) {
69
+ if (value === '' || value === null || value === undefined) {
70
+ return undefined;
71
+ }
72
+ const n = Math.round(Number(value));
73
+ return Number.isFinite(n) && n >= 0 ? n : undefined;
74
+ }
75
+ /**
76
+ * Instruction for a model whose answer will be spoken and cut at `limit` characters: answer within it instead
77
+ * of being cut mid-thought. Models cannot count characters, so the limit also comes as a number of sentences.
78
+ * Also asks not to read out state ids and serial numbers — over a speaker they are a minute of digits.
79
+ * Empty when there is no limit.
80
+ */
81
+ function spokenLengthHint(limit, lang) {
82
+ if (!(limit > 0)) {
83
+ return '';
84
+ }
85
+ const n = Math.max(1, Math.round(limit / 120));
86
+ if (lang === 'ru') {
87
+ const sentences = n === 1
88
+ ? 'одно короткое предложение'
89
+ : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)
90
+ ? `${n} коротких предложения`
91
+ : `${n} коротких предложений`;
92
+ return (`Твой ответ зачитывается вслух и обрезается после ${limit} символов. Поэтому отвечай не длиннее ${limit} ` +
93
+ `символов (примерно ${sentences}), главное — в начале. Не зачитывай ID состояний, серийные номера и ` +
94
+ 'длинные числа, если пользователь сам об этом не просит. Если нужно сказать больше, кратко подведи итог ' +
95
+ 'и предложи рассказать подробнее.');
96
+ }
97
+ if (lang === 'de') {
98
+ const sentences = n === 1 ? 'einen kurzen Satz' : `${n} kurze Sätze`;
99
+ return (`Deine Antwort wird vorgelesen und nach ${limit} Zeichen abgeschnitten. Antworte deshalb in höchstens ` +
100
+ `${limit} Zeichen (etwa ${sentences}), das Wichtigste zuerst. Lies keine State-IDs, Seriennummern oder ` +
101
+ 'langen Zahlen vor, außer der Nutzer fragt ausdrücklich danach. Ist mehr zu sagen, fasse zusammen und ' +
102
+ 'biete an, Details zu nennen.');
103
+ }
104
+ const sentences = n === 1 ? 'one short sentence' : `${n} short sentences`;
105
+ return (`Your answer is read aloud and cut off after ${limit} characters. Keep it within ${limit} characters ` +
106
+ `(about ${sentences}), most important point first. Do not read out state ids, serial numbers or long ` +
107
+ 'numbers unless the user asks for them. If there is more to say, summarise and offer the details.');
108
+ }
62
109
  /** Texts up to this length are worth caching; a longer one is a one-off answer. */
63
110
  exports.MAX_CACHE_CHARS = 200;
64
111
  /** Default budget for the cache directory. ~1 MB per 20 s of 24 kHz 16-bit audio. */
@@ -136,7 +183,8 @@ class CachedTts {
136
183
  async synthesize(text, lang) {
137
184
  const wanted = this.maxChars > 0 ? truncateForTts(text, this.maxChars) : (text || '').trim();
138
185
  if (wanted.length < (text || '').trim().length) {
139
- this.opts.log.warn(`TTS text shortened from ${text.trim().length} to ${wanted.length} characters.`);
186
+ this.opts.log.warn(`TTS text shortened from ${text.trim().length} to ${wanted.length} characters ` +
187
+ `(limit ${this.maxChars}: "Longest spoken answer" in the Voice tab, 0 = no limit).`);
140
188
  }
141
189
  const cacheable = this.maxCacheChars === 0 || wanted.length <= this.maxCacheChars;
142
190
  const file = cacheable ? this.entryPath(wanted, lang) : '';
@@ -1 +1 @@
1
- {"version":3,"file":"ttsCache.js","sourceRoot":"","sources":["../../../src/lib/voice/ttsCache.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The plumbing every TTS engine benefits from: a disk cache, a length limit, and SSML handling.\n *\n * Wrapped around any {@link TtsEngine}, so each engine stays a thin provider binding:\n * - **cache** — the assistant says the same short things over and over (\"Ok.\", \"Timer abgelaufen\",\n * a trigger's announcement). Synthesising those again on every repeat costs a cloud round-trip and\n * a few hundred milliseconds of latency for audio that is byte-for-byte identical. Only short texts\n * are cached: a long one-off answer will never be asked for twice, so it would only fill the disk.\n * - **length limit** — an LLM can answer with three paragraphs. Spoken at a speaker that is a minute\n * of monologue nobody can interrupt, so the text is cut at the last sentence end that fits.\n * - **SSML** — Azure and Polly can speak `<speak>…</speak>` (pauses, emphasis); OpenAI and the local\n * engines cannot, so there the tags are stripped rather than read out loud.\n *\n * All three come from the Python original (`C:\\iot\\Hannah`, `core/hannah/tts.py`: `_TTSCache`,\n * `_truncate_for_tts`, `_strip_ssml_tags`), including the detail that the sample rate belongs in the\n * cache entry — serving 24 kHz audio as 16 kHz plays it back at the wrong pitch.\n */\nimport * as crypto from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { TtsEngine, TtsResult } from './tts';\nimport type { VoiceLogger } from './download';\n\n/** Hard limit for a spoken text. Hannah's 400 characters ≈ 25 seconds of speech. */\nexport const MAX_TTS_CHARS = 400;\n\n/** Texts up to this length are worth caching; a longer one is a one-off answer. */\nexport const MAX_CACHE_CHARS = 200;\n\n/** Default budget for the cache directory. ~1 MB per 20 s of 24 kHz 16-bit audio. */\nexport const DEFAULT_CACHE_BYTES = 64 * 1024 * 1024;\n\n/** Does this text ask for SSML? */\nexport function isSsml(text: string): boolean {\n return /^\\s*<speak[\\s>]/i.test(text || '');\n}\n\n/** Drop the XML tags, for an engine that cannot speak SSML (reading them aloud would be worse). */\nexport function stripSsml(text: string): string {\n return (text || '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n/**\n * Cut `text` to `maxChars`, preferring the last sentence end in the second half so the result does not\n * stop mid-thought; failing that, the last word boundary plus an ellipsis. SSML is left alone — cutting\n * it would break the markup, and it is written by us, not by a model.\n */\nexport function truncateForTts(text: string, maxChars = MAX_TTS_CHARS): string {\n const t = (text || '').trim();\n if (t.length <= maxChars || isSsml(t)) {\n return t;\n }\n const chunk = t.slice(0, maxChars);\n for (const sep of ['.', '!', '?', '…']) {\n const idx = chunk.lastIndexOf(sep);\n if (idx > maxChars / 2) {\n return chunk.slice(0, idx + 1);\n }\n }\n const lastSpace = chunk.lastIndexOf(' ');\n return `${(lastSpace > 0 ? chunk.slice(0, lastSpace) : chunk).trim()} …`;\n}\n\n/** A cache entry on disk: the sample rate first, so an entry can never be replayed at the wrong pitch. */\nfunction encodeEntry(result: TtsResult): Buffer {\n const head = Buffer.alloc(4);\n head.writeUInt32LE(result.sampleRate, 0);\n return Buffer.concat([head, result.pcm]);\n}\n\nfunction decodeEntry(data: Buffer): TtsResult | null {\n if (data.length < 8) {\n return null;\n }\n const sampleRate = data.readUInt32LE(0);\n // A plausible speech rate; anything else means the file is not one of ours (or is truncated).\n if (sampleRate < 8000 || sampleRate > 48000) {\n return null;\n }\n return { pcm: data.subarray(4), sampleRate };\n}\n\nexport interface TtsCacheOptions {\n /** Directory for this engine's entries (one per provider+voice, so a voice change cannot serve stale audio). */\n dir: string;\n log: VoiceLogger;\n /** Cut texts longer than this before synthesising (0 = no limit). */\n maxChars?: number;\n /** Only cache texts up to this length (0 = cache everything). */\n maxCacheChars?: number;\n /** Prune the oldest entries once the directory exceeds this (0 = no limit). */\n maxBytes?: number;\n}\n\n/**\n * A {@link TtsEngine} wrapper that truncates, serves repeats from disk and resolves SSML against what the\n * wrapped engine actually supports. Every cache operation is best-effort: a broken or read-only cache\n * directory must never stop the assistant from speaking.\n */\nexport class CachedTts implements TtsEngine {\n private readonly maxChars: number;\n private readonly maxCacheChars: number;\n private readonly maxBytes: number;\n private ready = false;\n\n constructor(\n private readonly engine: TtsEngine,\n private readonly opts: TtsCacheOptions,\n ) {\n this.maxChars = opts.maxChars ?? MAX_TTS_CHARS;\n this.maxCacheChars = opts.maxCacheChars ?? MAX_CACHE_CHARS;\n this.maxBytes = opts.maxBytes ?? DEFAULT_CACHE_BYTES;\n }\n\n prepare(lang: string): Promise<void> {\n return this.engine.prepare ? this.engine.prepare(lang) : Promise.resolve();\n }\n\n async synthesize(text: string, lang: string): Promise<TtsResult> {\n const wanted = this.maxChars > 0 ? truncateForTts(text, this.maxChars) : (text || '').trim();\n if (wanted.length < (text || '').trim().length) {\n this.opts.log.warn(`TTS text shortened from ${text.trim().length} to ${wanted.length} characters.`);\n }\n const cacheable = this.maxCacheChars === 0 || wanted.length <= this.maxCacheChars;\n const file = cacheable ? this.entryPath(wanted, lang) : '';\n if (file) {\n const hit = this.read(file);\n if (hit) {\n this.opts.log.debug(`TTS cache hit: \"${wanted.slice(0, 40)}\"`);\n return hit;\n }\n }\n const result = await this.speak(wanted, lang);\n if (file && result.pcm.length) {\n this.write(file, result);\n }\n return result;\n }\n\n /** Hand SSML to the engine only if it can speak it; otherwise speak the text without the markup. */\n private speak(text: string, lang: string): Promise<TtsResult> {\n if (!isSsml(text)) {\n return this.engine.synthesize(text, lang);\n }\n if (this.engine.synthesizeSsml) {\n return this.engine.synthesizeSsml(text, lang);\n }\n this.opts.log.debug('TTS engine cannot speak SSML — using the plain text.');\n return this.engine.synthesize(stripSsml(text), lang);\n }\n\n /** Is this text already on disk? Used by the warm-up to skip what it has. */\n has(text: string, lang: string): boolean {\n const wanted = this.maxChars > 0 ? truncateForTts(text, this.maxChars) : (text || '').trim();\n const file = this.entryPath(wanted, lang);\n try {\n return fs.existsSync(file);\n } catch {\n return false;\n }\n }\n\n /** Pre-synthesise frequently spoken phrases, so the first \"Ok.\" of the day is not a cloud call. */\n async warm(phrases: string[], lang: string): Promise<number> {\n let added = 0;\n for (const phrase of phrases) {\n const text = (phrase || '').trim();\n if (!text || this.has(text, lang)) {\n continue;\n }\n try {\n await this.synthesize(text, lang);\n added++;\n } catch (e) {\n this.opts.log.debug(`TTS warm-up failed for \"${text.slice(0, 30)}\": ${(e as Error).message}`);\n break; // the engine is unavailable — no point trying the rest\n }\n }\n if (added) {\n this.opts.log.info(`TTS cache warmed with ${added} phrase(s).`);\n }\n return added;\n }\n\n /** The language is part of the key: the same words are spoken differently in another language. */\n private entryPath(text: string, lang: string): string {\n const key = crypto.createHash('sha256').update(`${lang}\\u0000${text}`).digest('hex').slice(0, 20);\n return path.join(this.opts.dir, `${key}.pcm`);\n }\n\n private read(file: string): TtsResult | null {\n try {\n if (!fs.existsSync(file)) {\n return null;\n }\n const entry = decodeEntry(fs.readFileSync(file));\n if (entry) {\n // Touch it, so pruning drops what nobody asks for rather than what is merely old.\n const now = new Date();\n try {\n fs.utimesSync(file, now, now);\n } catch {\n /* a read-only cache is still a usable cache */\n }\n }\n return entry;\n } catch (e) {\n this.opts.log.debug(`TTS cache read failed: ${(e as Error).message}`);\n return null;\n }\n }\n\n private write(file: string, result: TtsResult): void {\n try {\n if (!this.ready) {\n fs.mkdirSync(this.opts.dir, { recursive: true });\n this.ready = true;\n }\n fs.writeFileSync(file, encodeEntry(result));\n this.prune();\n } catch (e) {\n this.opts.log.debug(`TTS cache write failed: ${(e as Error).message}`);\n }\n }\n\n /** Keep the directory under its budget by dropping the least recently used entries. */\n private prune(): void {\n if (this.maxBytes <= 0) {\n return;\n }\n try {\n const entries = fs\n .readdirSync(this.opts.dir)\n .filter(name => name.endsWith('.pcm'))\n .map(name => {\n const full = path.join(this.opts.dir, name);\n const stat = fs.statSync(full);\n return { full, size: stat.size, used: stat.mtimeMs };\n });\n let total = entries.reduce((sum, e) => sum + e.size, 0);\n if (total <= this.maxBytes) {\n return;\n }\n for (const entry of entries.sort((a, b) => a.used - b.used)) {\n if (total <= this.maxBytes) {\n break;\n }\n fs.unlinkSync(entry.full);\n total -= entry.size;\n }\n this.opts.log.debug(`TTS cache pruned to ${Math.round(total / 1024)} KiB.`);\n } catch (e) {\n this.opts.log.debug(`TTS cache prune failed: ${(e as Error).message}`);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"ttsCache.js","sourceRoot":"","sources":["../../../src/lib/voice/ttsCache.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The plumbing every TTS engine benefits from: a disk cache, a length limit, and SSML handling.\n *\n * Wrapped around any {@link TtsEngine}, so each engine stays a thin provider binding:\n * - **cache** — the assistant says the same short things over and over (\"Ok.\", \"Timer abgelaufen\",\n * a trigger's announcement). Synthesising those again on every repeat costs a cloud round-trip and\n * a few hundred milliseconds of latency for audio that is byte-for-byte identical. Only short texts\n * are cached: a long one-off answer will never be asked for twice, so it would only fill the disk.\n * - **length limit** — an LLM can answer with three paragraphs. Spoken at a speaker that is a minute\n * of monologue nobody can interrupt, so the text is cut at the last sentence end that fits.\n * - **SSML** — Azure and Polly can speak `<speak>…</speak>` (pauses, emphasis); OpenAI and the local\n * engines cannot, so there the tags are stripped rather than read out loud.\n *\n * All three come from the Python original (`C:\\iot\\Hannah`, `core/hannah/tts.py`: `_TTSCache`,\n * `_truncate_for_tts`, `_strip_ssml_tags`), including the detail that the sample rate belongs in the\n * cache entry — serving 24 kHz audio as 16 kHz plays it back at the wrong pitch.\n */\nimport * as crypto from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { TtsEngine, TtsResult } from './tts';\nimport type { VoiceLogger } from './download';\n\n/** Hard limit for a spoken text. Hannah's 400 characters ≈ 25 seconds of speech. */\nexport const MAX_TTS_CHARS = 400;\n\n/**\n * The configured length limit for spoken text: a number ≥ 0 (0 = no limit), or undefined for the default\n * ({@link MAX_TTS_CHARS}) — also for installations from before the setting and for a field the user emptied.\n */\nexport function parseTtsMaxChars(value: unknown): number | undefined {\n if (value === '' || value === null || value === undefined) {\n return undefined;\n }\n const n = Math.round(Number(value));\n return Number.isFinite(n) && n >= 0 ? n : undefined;\n}\n\n/**\n * Instruction for a model whose answer will be spoken and cut at `limit` characters: answer within it instead\n * of being cut mid-thought. Models cannot count characters, so the limit also comes as a number of sentences.\n * Also asks not to read out state ids and serial numbers — over a speaker they are a minute of digits.\n * Empty when there is no limit.\n */\nexport function spokenLengthHint(limit: number, lang: string): string {\n if (!(limit > 0)) {\n return '';\n }\n const n = Math.max(1, Math.round(limit / 120));\n if (lang === 'ru') {\n const sentences =\n n === 1\n ? 'одно короткое предложение'\n : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)\n ? `${n} коротких предложения`\n : `${n} коротких предложений`;\n return (\n `Твой ответ зачитывается вслух и обрезается после ${limit} символов. Поэтому отвечай не длиннее ${limit} ` +\n `символов (примерно ${sentences}), главное — в начале. Не зачитывай ID состояний, серийные номера и ` +\n 'длинные числа, если пользователь сам об этом не просит. Если нужно сказать больше, кратко подведи итог ' +\n 'и предложи рассказать подробнее.'\n );\n }\n if (lang === 'de') {\n const sentences = n === 1 ? 'einen kurzen Satz' : `${n} kurze Sätze`;\n return (\n `Deine Antwort wird vorgelesen und nach ${limit} Zeichen abgeschnitten. Antworte deshalb in höchstens ` +\n `${limit} Zeichen (etwa ${sentences}), das Wichtigste zuerst. Lies keine State-IDs, Seriennummern oder ` +\n 'langen Zahlen vor, außer der Nutzer fragt ausdrücklich danach. Ist mehr zu sagen, fasse zusammen und ' +\n 'biete an, Details zu nennen.'\n );\n }\n const sentences = n === 1 ? 'one short sentence' : `${n} short sentences`;\n return (\n `Your answer is read aloud and cut off after ${limit} characters. Keep it within ${limit} characters ` +\n `(about ${sentences}), most important point first. Do not read out state ids, serial numbers or long ` +\n 'numbers unless the user asks for them. If there is more to say, summarise and offer the details.'\n );\n}\n\n/** Texts up to this length are worth caching; a longer one is a one-off answer. */\nexport const MAX_CACHE_CHARS = 200;\n\n/** Default budget for the cache directory. ~1 MB per 20 s of 24 kHz 16-bit audio. */\nexport const DEFAULT_CACHE_BYTES = 64 * 1024 * 1024;\n\n/** Does this text ask for SSML? */\nexport function isSsml(text: string): boolean {\n return /^\\s*<speak[\\s>]/i.test(text || '');\n}\n\n/** Drop the XML tags, for an engine that cannot speak SSML (reading them aloud would be worse). */\nexport function stripSsml(text: string): string {\n return (text || '')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n/**\n * Cut `text` to `maxChars`, preferring the last sentence end in the second half so the result does not\n * stop mid-thought; failing that, the last word boundary plus an ellipsis. SSML is left alone — cutting\n * it would break the markup, and it is written by us, not by a model.\n */\nexport function truncateForTts(text: string, maxChars = MAX_TTS_CHARS): string {\n const t = (text || '').trim();\n if (t.length <= maxChars || isSsml(t)) {\n return t;\n }\n const chunk = t.slice(0, maxChars);\n for (const sep of ['.', '!', '?', '…']) {\n const idx = chunk.lastIndexOf(sep);\n if (idx > maxChars / 2) {\n return chunk.slice(0, idx + 1);\n }\n }\n const lastSpace = chunk.lastIndexOf(' ');\n return `${(lastSpace > 0 ? chunk.slice(0, lastSpace) : chunk).trim()} …`;\n}\n\n/** A cache entry on disk: the sample rate first, so an entry can never be replayed at the wrong pitch. */\nfunction encodeEntry(result: TtsResult): Buffer {\n const head = Buffer.alloc(4);\n head.writeUInt32LE(result.sampleRate, 0);\n return Buffer.concat([head, result.pcm]);\n}\n\nfunction decodeEntry(data: Buffer): TtsResult | null {\n if (data.length < 8) {\n return null;\n }\n const sampleRate = data.readUInt32LE(0);\n // A plausible speech rate; anything else means the file is not one of ours (or is truncated).\n if (sampleRate < 8000 || sampleRate > 48000) {\n return null;\n }\n return { pcm: data.subarray(4), sampleRate };\n}\n\nexport interface TtsCacheOptions {\n /** Directory for this engine's entries (one per provider+voice, so a voice change cannot serve stale audio). */\n dir: string;\n log: VoiceLogger;\n /** Cut texts longer than this before synthesising (0 = no limit). */\n maxChars?: number;\n /** Only cache texts up to this length (0 = cache everything). */\n maxCacheChars?: number;\n /** Prune the oldest entries once the directory exceeds this (0 = no limit). */\n maxBytes?: number;\n}\n\n/**\n * A {@link TtsEngine} wrapper that truncates, serves repeats from disk and resolves SSML against what the\n * wrapped engine actually supports. Every cache operation is best-effort: a broken or read-only cache\n * directory must never stop the assistant from speaking.\n */\nexport class CachedTts implements TtsEngine {\n private readonly maxChars: number;\n private readonly maxCacheChars: number;\n private readonly maxBytes: number;\n private ready = false;\n\n constructor(\n private readonly engine: TtsEngine,\n private readonly opts: TtsCacheOptions,\n ) {\n this.maxChars = opts.maxChars ?? MAX_TTS_CHARS;\n this.maxCacheChars = opts.maxCacheChars ?? MAX_CACHE_CHARS;\n this.maxBytes = opts.maxBytes ?? DEFAULT_CACHE_BYTES;\n }\n\n prepare(lang: string): Promise<void> {\n return this.engine.prepare ? this.engine.prepare(lang) : Promise.resolve();\n }\n\n async synthesize(text: string, lang: string): Promise<TtsResult> {\n const wanted = this.maxChars > 0 ? truncateForTts(text, this.maxChars) : (text || '').trim();\n if (wanted.length < (text || '').trim().length) {\n this.opts.log.warn(\n `TTS text shortened from ${text.trim().length} to ${wanted.length} characters ` +\n `(limit ${this.maxChars}: \"Longest spoken answer\" in the Voice tab, 0 = no limit).`,\n );\n }\n const cacheable = this.maxCacheChars === 0 || wanted.length <= this.maxCacheChars;\n const file = cacheable ? this.entryPath(wanted, lang) : '';\n if (file) {\n const hit = this.read(file);\n if (hit) {\n this.opts.log.debug(`TTS cache hit: \"${wanted.slice(0, 40)}\"`);\n return hit;\n }\n }\n const result = await this.speak(wanted, lang);\n if (file && result.pcm.length) {\n this.write(file, result);\n }\n return result;\n }\n\n /** Hand SSML to the engine only if it can speak it; otherwise speak the text without the markup. */\n private speak(text: string, lang: string): Promise<TtsResult> {\n if (!isSsml(text)) {\n return this.engine.synthesize(text, lang);\n }\n if (this.engine.synthesizeSsml) {\n return this.engine.synthesizeSsml(text, lang);\n }\n this.opts.log.debug('TTS engine cannot speak SSML — using the plain text.');\n return this.engine.synthesize(stripSsml(text), lang);\n }\n\n /** Is this text already on disk? Used by the warm-up to skip what it has. */\n has(text: string, lang: string): boolean {\n const wanted = this.maxChars > 0 ? truncateForTts(text, this.maxChars) : (text || '').trim();\n const file = this.entryPath(wanted, lang);\n try {\n return fs.existsSync(file);\n } catch {\n return false;\n }\n }\n\n /** Pre-synthesise frequently spoken phrases, so the first \"Ok.\" of the day is not a cloud call. */\n async warm(phrases: string[], lang: string): Promise<number> {\n let added = 0;\n for (const phrase of phrases) {\n const text = (phrase || '').trim();\n if (!text || this.has(text, lang)) {\n continue;\n }\n try {\n await this.synthesize(text, lang);\n added++;\n } catch (e) {\n this.opts.log.debug(`TTS warm-up failed for \"${text.slice(0, 30)}\": ${(e as Error).message}`);\n break; // the engine is unavailable — no point trying the rest\n }\n }\n if (added) {\n this.opts.log.info(`TTS cache warmed with ${added} phrase(s).`);\n }\n return added;\n }\n\n /** The language is part of the key: the same words are spoken differently in another language. */\n private entryPath(text: string, lang: string): string {\n const key = crypto.createHash('sha256').update(`${lang}\\u0000${text}`).digest('hex').slice(0, 20);\n return path.join(this.opts.dir, `${key}.pcm`);\n }\n\n private read(file: string): TtsResult | null {\n try {\n if (!fs.existsSync(file)) {\n return null;\n }\n const entry = decodeEntry(fs.readFileSync(file));\n if (entry) {\n // Touch it, so pruning drops what nobody asks for rather than what is merely old.\n const now = new Date();\n try {\n fs.utimesSync(file, now, now);\n } catch {\n /* a read-only cache is still a usable cache */\n }\n }\n return entry;\n } catch (e) {\n this.opts.log.debug(`TTS cache read failed: ${(e as Error).message}`);\n return null;\n }\n }\n\n private write(file: string, result: TtsResult): void {\n try {\n if (!this.ready) {\n fs.mkdirSync(this.opts.dir, { recursive: true });\n this.ready = true;\n }\n fs.writeFileSync(file, encodeEntry(result));\n this.prune();\n } catch (e) {\n this.opts.log.debug(`TTS cache write failed: ${(e as Error).message}`);\n }\n }\n\n /** Keep the directory under its budget by dropping the least recently used entries. */\n private prune(): void {\n if (this.maxBytes <= 0) {\n return;\n }\n try {\n const entries = fs\n .readdirSync(this.opts.dir)\n .filter(name => name.endsWith('.pcm'))\n .map(name => {\n const full = path.join(this.opts.dir, name);\n const stat = fs.statSync(full);\n return { full, size: stat.size, used: stat.mtimeMs };\n });\n let total = entries.reduce((sum, e) => sum + e.size, 0);\n if (total <= this.maxBytes) {\n return;\n }\n for (const entry of entries.sort((a, b) => a.used - b.used)) {\n if (total <= this.maxBytes) {\n break;\n }\n fs.unlinkSync(entry.full);\n total -= entry.size;\n }\n this.opts.log.debug(`TTS cache pruned to ${Math.round(total / 1024)} KiB.`);\n } catch (e) {\n this.opts.log.debug(`TTS cache prune failed: ${(e as Error).message}`);\n }\n }\n}\n"]}
@@ -55,6 +55,7 @@ class SpeechDetector {
55
55
  fixedEnd;
56
56
  silenceMs;
57
57
  maxMs;
58
+ startTimeoutMs;
58
59
  skipMs;
59
60
  adaptive;
60
61
  minStartLevel;
@@ -73,13 +74,19 @@ class SpeechDetector {
73
74
  totalMs = 0;
74
75
  /** Loudest frame seen so far (exposed for logging — the usual clue when a mic is mis-configured). */
75
76
  peak = 0;
76
- constructor({ sampleRate = 16000, frameMs = 30, startLevel = 700, endLevel = 400, silenceMs = 900, maxMs = 12000, skipMs = 0, adaptive = false, minStartLevel = 250, minEndLevel = 150, startFactor = 3.5, endFactor = 2, warmupMs = 150, } = {}) {
77
+ /**
78
+ * Loudest frame after the skipped lead-in, i.e. without the wake chirp. Compared with {@link startLevel}
79
+ * it tells whether a dropped utterance was silence or a voice that was simply too quiet for the gate.
80
+ */
81
+ analysedPeak = 0;
82
+ constructor({ sampleRate = 16000, frameMs = 30, startLevel = 700, endLevel = 400, silenceMs = 900, maxMs = 12000, startTimeoutMs = 0, skipMs = 0, adaptive = false, minStartLevel = 250, minEndLevel = 150, startFactor = 3.5, endFactor = 2, warmupMs = 150, } = {}) {
77
83
  this.frameBytes = Math.round((sampleRate * frameMs) / 1000) * 2;
78
84
  this.frameMs = frameMs;
79
85
  this.fixedStart = startLevel;
80
86
  this.fixedEnd = endLevel;
81
87
  this.silenceMs = silenceMs;
82
88
  this.maxMs = maxMs;
89
+ this.startTimeoutMs = startTimeoutMs;
83
90
  this.skipMs = skipMs;
84
91
  this.adaptive = adaptive;
85
92
  this.minStartLevel = minStartLevel;
@@ -125,6 +132,9 @@ class SpeechDetector {
125
132
  continue;
126
133
  }
127
134
  this.totalMs += this.frameMs;
135
+ if (level > this.analysedPeak) {
136
+ this.analysedPeak = level;
137
+ }
128
138
  if (this.adaptive) {
129
139
  // Three rules, each there to defeat a specific way of getting this wrong:
130
140
  // - start at zero, i.e. maximally sensitive. Seeding from the first frame instead
@@ -162,6 +172,9 @@ class SpeechDetector {
162
172
  else {
163
173
  this.silentMs = 0;
164
174
  }
175
+ if (!this.speechSeen && this.startTimeoutMs > 0 && this.elapsedMs >= this.startTimeoutMs) {
176
+ verdict ||= 'no-speech';
177
+ }
165
178
  if (this.totalMs >= this.maxMs) {
166
179
  verdict ||= 'timeout';
167
180
  }