iobroker.assistant 1.4.3 → 1.4.4
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.
- package/README.md +4 -0
- package/build/lib/voice/esphome.js +49 -4
- package/build/lib/voice/esphome.js.map +1 -1
- package/build/lib/voice/vad.js +42 -5
- package/build/lib/voice/vad.js.map +1 -1
- package/io-package.json +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -235,6 +235,10 @@ train it twice.
|
|
|
235
235
|
Placeholder for the next version (at the beginning of the line):
|
|
236
236
|
### **WORK IN PROGRESS**
|
|
237
237
|
-->
|
|
238
|
+
### 1.4.4 (2026-11-03)
|
|
239
|
+
* (@GermanBluefox) ESPHome satellites: a sound only counts as speech once it has lasted about 200 ms. After a false wake a single click opened the gate, and Whisper made "и все просят." of the amplified silence around it
|
|
240
|
+
* (@GermanBluefox) ESPHome satellites: when a device stops sending audio without ending the stream, the recording is closed after 2 seconds instead of the 30-second pipeline timeout, during which the satellite showed "listening"
|
|
241
|
+
|
|
238
242
|
### 1.4.3 (2026-11-03)
|
|
239
243
|
* (@GermanBluefox) An empty question sent with `sendTo('ask')` is rejected (`{error: 'empty question'}`) instead of going to the model with nothing but the time context, and the log names the sender once a minute. A script that asked on every answer looped about once a second and used pi-agent tokens. The log line of every `ask` now names the sender
|
|
240
244
|
|
|
@@ -136,6 +136,22 @@ const DEFAULT_SILENCE_MS = 900;
|
|
|
136
136
|
*/
|
|
137
137
|
const LONG_REQUEST_MS = 2500;
|
|
138
138
|
const LONG_PAUSE_FACTOR = 1.5;
|
|
139
|
+
/**
|
|
140
|
+
* Sound needed before a loud moment counts as speech. On vitanova (2026-11-03) a single 100 ms sound at RMS 197
|
|
141
|
+
* after a false wake opened the gate (threshold 150), and Whisper made "и все просят." of the amplified silence
|
|
142
|
+
* around it. The shortest real answer, "да", lasts about 250 ms.
|
|
143
|
+
*/
|
|
144
|
+
const MIN_SPEECH_MS = 200;
|
|
145
|
+
/**
|
|
146
|
+
* Close the utterance when the device stops sending audio without ending the stream. Seen on a ThirdReality
|
|
147
|
+
* speaker: the audio stopped after 2.7 s, and the satellite showed "listening" for the 30 s of
|
|
148
|
+
* {@link PIPELINE_TIMEOUT_MS}. A device streams every few tens of ms while the mic is open.
|
|
149
|
+
*/
|
|
150
|
+
const STREAM_STALL_MS = 2000;
|
|
151
|
+
/** The same before the first audio arrives: a device needs a moment to open the microphone. */
|
|
152
|
+
const STREAM_START_MS = 5000;
|
|
153
|
+
/** How often a running capture checks for a stalled stream. */
|
|
154
|
+
const STALL_CHECK_MS = 250;
|
|
139
155
|
/** Silence threshold as a share of the speech threshold — keeps the default pair 250/150. */
|
|
140
156
|
const SILENCE_SHARE = 0.6;
|
|
141
157
|
/**
|
|
@@ -599,9 +615,11 @@ class EsphomeConnection {
|
|
|
599
615
|
this.send(response);
|
|
600
616
|
const timer = setTimeout(() => {
|
|
601
617
|
this.opts.log.warn(`ESPHome ${this.deviceName}: no end of speech within ${PIPELINE_TIMEOUT_MS} ms`);
|
|
602
|
-
void this.finishCapture();
|
|
618
|
+
void this.finishCapture('pipeline-timeout');
|
|
603
619
|
}, PIPELINE_TIMEOUT_MS);
|
|
604
620
|
timer.unref();
|
|
621
|
+
const stallCheck = setInterval(() => this.checkStall(), STALL_CHECK_MS);
|
|
622
|
+
stallCheck.unref();
|
|
605
623
|
const silenceMs = this.opts.silenceMs || DEFAULT_SILENCE_MS;
|
|
606
624
|
this.capture = {
|
|
607
625
|
chunks: [],
|
|
@@ -617,10 +635,14 @@ class EsphomeConnection {
|
|
|
617
635
|
startTimeoutMs: phrase ? NO_SPEECH_MS : NO_SPEECH_FOLLOW_UP_MS,
|
|
618
636
|
minStartLevel: this.speechLevel || exports.DEFAULT_SPEECH_LEVEL,
|
|
619
637
|
minEndLevel: Math.round((this.speechLevel || exports.DEFAULT_SPEECH_LEVEL) * SILENCE_SHARE),
|
|
638
|
+
minSpeechMs: MIN_SPEECH_MS,
|
|
620
639
|
}),
|
|
621
640
|
conversationId: request.getConversationId(),
|
|
622
641
|
wakeWord: phrase || '',
|
|
623
642
|
timer,
|
|
643
|
+
stallCheck,
|
|
644
|
+
startedAt: Date.now(),
|
|
645
|
+
lastAudioAt: 0,
|
|
624
646
|
closed: false,
|
|
625
647
|
};
|
|
626
648
|
this.status('listening');
|
|
@@ -638,6 +660,7 @@ class EsphomeConnection {
|
|
|
638
660
|
}
|
|
639
661
|
const chunk = Buffer.from(audio.getData_asU8());
|
|
640
662
|
if (chunk.length) {
|
|
663
|
+
capture.lastAudioAt = Date.now();
|
|
641
664
|
capture.chunks.push(chunk);
|
|
642
665
|
const end = capture.detector.push(chunk);
|
|
643
666
|
if (end) {
|
|
@@ -646,8 +669,23 @@ class EsphomeConnection {
|
|
|
646
669
|
}
|
|
647
670
|
}
|
|
648
671
|
if (audio.getEnd()) {
|
|
649
|
-
void this.finishCapture();
|
|
672
|
+
void this.finishCapture('stream');
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/** End a capture whose device stopped sending audio without ending the stream. */
|
|
676
|
+
checkStall() {
|
|
677
|
+
const capture = this.capture;
|
|
678
|
+
if (!capture || capture.closed) {
|
|
679
|
+
return;
|
|
650
680
|
}
|
|
681
|
+
const now = Date.now();
|
|
682
|
+
const quietMs = capture.lastAudioAt ? now - capture.lastAudioAt : now - capture.startedAt;
|
|
683
|
+
if (quietMs < (capture.lastAudioAt ? STREAM_STALL_MS : STREAM_START_MS)) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const audioMs = Math.round(capture.chunks.reduce((sum, c) => sum + c.length, 0) / 2 / (MIC_SAMPLE_RATE / 1000));
|
|
687
|
+
this.opts.log.info(`ESPHome ${this.deviceName}: the device ${capture.lastAudioAt ? `stopped sending audio after ${(audioMs / 1000).toFixed(2)} s` : 'sent no audio'} without ending the stream — closing the utterance after ${(quietMs / 1000).toFixed(1)} s of nothing`);
|
|
688
|
+
void this.finishCapture('stall');
|
|
651
689
|
}
|
|
652
690
|
/** Hand a recording that never became a request to {@link EsphomeSatellitesOptions.onDiscarded}. */
|
|
653
691
|
discard(capture, pcm, reason, vad) {
|
|
@@ -679,11 +717,12 @@ class EsphomeConnection {
|
|
|
679
717
|
capture.closed = true;
|
|
680
718
|
this.capture = null;
|
|
681
719
|
clearTimeout(capture.timer);
|
|
720
|
+
clearInterval(capture.stallCheck);
|
|
682
721
|
// Send this first: the device streams until it sees it.
|
|
683
722
|
this.event(EVENT.STT_VAD_END);
|
|
684
723
|
const pcm = Buffer.concat(capture.chunks);
|
|
685
724
|
const seconds = pcm.length / 2 / MIC_SAMPLE_RATE;
|
|
686
|
-
const vad = { ...capture.detector.report(), end
|
|
725
|
+
const vad = { ...capture.detector.report(), end };
|
|
687
726
|
const startedAt = vad.speechStartMs !== undefined ? `, speech from ${(vad.speechStartMs / 1000).toFixed(2)} s` : '';
|
|
688
727
|
const endedBy = vad.end === 'speech-end' ? `ended after ${vad.silenceMs} ms of silence` : `ended by ${vad.end}`;
|
|
689
728
|
this.opts.log.debug(`ESPHome ${this.deviceName}: captured ${seconds.toFixed(2)} s ` +
|
|
@@ -692,7 +731,12 @@ class EsphomeConnection {
|
|
|
692
731
|
if (!pcm.length || !capture.detector.sawSpeech) {
|
|
693
732
|
const loudest = Math.round(capture.detector.analysedPeak);
|
|
694
733
|
const gate = Math.round(capture.detector.startLevel);
|
|
695
|
-
if (
|
|
734
|
+
if (capture.detector.longestSoundMs > 0) {
|
|
735
|
+
// Something crossed the gate, but too briefly for a word: a click, a knock, a cough.
|
|
736
|
+
this.opts.log.debug(`ESPHome ${this.deviceName}: only a short sound (${capture.detector.longestSoundMs} ms, ` +
|
|
737
|
+
`RMS ${loudest}) — too short for speech (${MIN_SPEECH_MS} ms), dropping the utterance`);
|
|
738
|
+
}
|
|
739
|
+
else if (pcm.length && loudest >= gate * QUIET_VOICE_SHARE) {
|
|
696
740
|
// Not an empty room: something came close to the gate. Typically a microphone that is quieter
|
|
697
741
|
// than the default threshold expects (a phone) — say where to change it.
|
|
698
742
|
this.opts.log.info(`ESPHome ${this.deviceName}: dropped the utterance — the loudest sound (RMS ${loudest}) stayed ` +
|
|
@@ -835,6 +879,7 @@ class EsphomeConnection {
|
|
|
835
879
|
this.capture = null;
|
|
836
880
|
capture.closed = true;
|
|
837
881
|
clearTimeout(capture.timer);
|
|
882
|
+
clearInterval(capture.stallCheck);
|
|
838
883
|
}
|
|
839
884
|
// ── announcements ───────────────────────────────────────────────────────────────────────────────
|
|
840
885
|
/**
|
|
@@ -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 SpeechEnd, type Bytes, type VadReport } from './vad';\nimport { prepareForStt } from './speechAudio';\nimport { pcmToWav } from './stt';\nimport { levelProfile, type DiscardedCapture, type 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. Only the analysis skips it: STT gets the audio\n * from 400 ms before the detected speech (`speechAudio.ts`), so a speaker who starts inside this window loses\n * 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 that ends an utterance when the configuration leaves it empty. */\nconst DEFAULT_SILENCE_MS = 900;\n/**\n * After this much speech a pause may be {@link LONG_PAUSE_FACTOR} times as long before the utterance ends: whoever\n * talks that long is dictating a request and pauses to think, while \"Licht an\" should still close quickly. Field\n * report 2026-11-03: \"Я хочу, чтоб ты создал задачу, чтобы утром говорить мне…\" was cut off right there.\n */\nconst LONG_REQUEST_MS = 2500;\nconst LONG_PAUSE_FACTOR = 1.5;\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: VadReport;\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 const silenceMs = this.opts.silenceMs || DEFAULT_SILENCE_MS;\n this.capture = {\n chunks: [],\n detector: new SpeechDetector({\n sampleRate: MIC_SAMPLE_RATE,\n silenceMs,\n longSilenceMs: Math.round(silenceMs * LONG_PAUSE_FACTOR),\n longAfterMs: LONG_REQUEST_MS,\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 /** Hand a recording that never became a request to {@link EsphomeSatellitesOptions.onDiscarded}. */\n private discard(capture: Capture, pcm: Bytes, reason: DiscardReason, vad: VadReport): void {\n if (!this.opts.onDiscarded || !pcm.length) {\n return;\n }\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 // The store writes its own level profile next to it.\n vad: { ...vad, levels: undefined },\n });\n } catch (e) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: keeping the recording failed — ${(e as Error).message}`);\n }\n }\n\n /** Stop the mic stream and run STT → answer → TTS for what was captured. */\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 const vad: VadReport = { ...capture.detector.report(), end: end ?? 'stream' };\n const startedAt =\n vad.speechStartMs !== undefined ? `, speech from ${(vad.speechStartMs / 1000).toFixed(2)} s` : '';\n const endedBy = vad.end === 'speech-end' ? `ended after ${vad.silenceMs} ms of silence` : `ended by ${vad.end}`;\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: captured ${seconds.toFixed(2)} s ` +\n `(peak RMS ${vad.peak}, noise floor ${vad.noiseFloor}, speech/silence gate ${vad.startLevel}/${vad.endLevel}` +\n `${startedAt}, ${endedBy})`,\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', vad);\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 speech = prepareForStt(pcm, MIC_SAMPLE_RATE, { startMs: vad.speechStartMs, endLevel: vad.endLevel });\n vad.stt = { fromMs: speech.fromMs, toMs: speech.toMs, gain: speech.gain };\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: speech recognition gets ${(speech.fromMs / 1000).toFixed(2)}–` +\n `${(speech.toMs / 1000).toFixed(2)} s${speech.gain > 1 ? `, amplified ×${speech.gain}` : ''}`,\n );\n const sttStart = Date.now();\n const text = (\n await this.opts.stt.transcribe(speech.pcm, MIC_SAMPLE_RATE, this.opts.language, hints)\n ).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', vad);\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 // Numbers only, no audio: shows where a request was cut off without keeping recordings.\n vad: { ...vad, levels: levelProfile(pcm, MIC_SAMPLE_RATE) },\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 CaptureEnd, type Bytes, type VadReport } from './vad';\nimport { prepareForStt } from './speechAudio';\nimport { pcmToWav } from './stt';\nimport { levelProfile, type DiscardedCapture, type 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. Only the analysis skips it: STT gets the audio\n * from 400 ms before the detected speech (`speechAudio.ts`), so a speaker who starts inside this window loses\n * 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 that ends an utterance when the configuration leaves it empty. */\nconst DEFAULT_SILENCE_MS = 900;\n/**\n * After this much speech a pause may be {@link LONG_PAUSE_FACTOR} times as long before the utterance ends: whoever\n * talks that long is dictating a request and pauses to think, while \"Licht an\" should still close quickly. Field\n * report 2026-11-03: \"Я хочу, чтоб ты создал задачу, чтобы утром говорить мне…\" was cut off right there.\n */\nconst LONG_REQUEST_MS = 2500;\nconst LONG_PAUSE_FACTOR = 1.5;\n/**\n * Sound needed before a loud moment counts as speech. On vitanova (2026-11-03) a single 100 ms sound at RMS 197\n * after a false wake opened the gate (threshold 150), and Whisper made \"и все просят.\" of the amplified silence\n * around it. The shortest real answer, \"да\", lasts about 250 ms.\n */\nconst MIN_SPEECH_MS = 200;\n/**\n * Close the utterance when the device stops sending audio without ending the stream. Seen on a ThirdReality\n * speaker: the audio stopped after 2.7 s, and the satellite showed \"listening\" for the 30 s of\n * {@link PIPELINE_TIMEOUT_MS}. A device streams every few tens of ms while the mic is open.\n */\nconst STREAM_STALL_MS = 2000;\n/** The same before the first audio arrives: a device needs a moment to open the microphone. */\nconst STREAM_START_MS = 5000;\n/** How often a running capture checks for a stalled stream. */\nconst STALL_CHECK_MS = 250;\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: VadReport;\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 /** Watches for a device that stops sending audio without ending the stream. */\n stallCheck: NodeJS.Timeout;\n startedAt: number;\n /** When the last audio arrived; 0 = none yet. */\n lastAudioAt: number;\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('pipeline-timeout');\n }, PIPELINE_TIMEOUT_MS);\n timer.unref();\n const stallCheck = setInterval(() => this.checkStall(), STALL_CHECK_MS);\n stallCheck.unref();\n\n const silenceMs = this.opts.silenceMs || DEFAULT_SILENCE_MS;\n this.capture = {\n chunks: [],\n detector: new SpeechDetector({\n sampleRate: MIC_SAMPLE_RATE,\n silenceMs,\n longSilenceMs: Math.round(silenceMs * LONG_PAUSE_FACTOR),\n longAfterMs: LONG_REQUEST_MS,\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 minSpeechMs: MIN_SPEECH_MS,\n }),\n conversationId: request.getConversationId(),\n wakeWord: phrase || '',\n timer,\n stallCheck,\n startedAt: Date.now(),\n lastAudioAt: 0,\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.lastAudioAt = Date.now();\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('stream');\n }\n }\n\n /** End a capture whose device stopped sending audio without ending the stream. */\n private checkStall(): void {\n const capture = this.capture;\n if (!capture || capture.closed) {\n return;\n }\n const now = Date.now();\n const quietMs = capture.lastAudioAt ? now - capture.lastAudioAt : now - capture.startedAt;\n if (quietMs < (capture.lastAudioAt ? STREAM_STALL_MS : STREAM_START_MS)) {\n return;\n }\n const audioMs = Math.round(capture.chunks.reduce((sum, c) => sum + c.length, 0) / 2 / (MIC_SAMPLE_RATE / 1000));\n this.opts.log.info(\n `ESPHome ${this.deviceName}: the device ${\n capture.lastAudioAt ? `stopped sending audio after ${(audioMs / 1000).toFixed(2)} s` : 'sent no audio'\n } without ending the stream — closing the utterance after ${(quietMs / 1000).toFixed(1)} s of nothing`,\n );\n void this.finishCapture('stall');\n }\n\n /** Hand a recording that never became a request to {@link EsphomeSatellitesOptions.onDiscarded}. */\n private discard(capture: Capture, pcm: Bytes, reason: DiscardReason, vad: VadReport): void {\n if (!this.opts.onDiscarded || !pcm.length) {\n return;\n }\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 // The store writes its own level profile next to it.\n vad: { ...vad, levels: undefined },\n });\n } catch (e) {\n this.opts.log.debug(`ESPHome ${this.deviceName}: keeping the recording failed — ${(e as Error).message}`);\n }\n }\n\n /** Stop the mic stream and run STT → answer → TTS for what was captured. */\n private async finishCapture(end: CaptureEnd): 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 clearInterval(capture.stallCheck);\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 const vad: VadReport = { ...capture.detector.report(), end };\n const startedAt =\n vad.speechStartMs !== undefined ? `, speech from ${(vad.speechStartMs / 1000).toFixed(2)} s` : '';\n const endedBy = vad.end === 'speech-end' ? `ended after ${vad.silenceMs} ms of silence` : `ended by ${vad.end}`;\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: captured ${seconds.toFixed(2)} s ` +\n `(peak RMS ${vad.peak}, noise floor ${vad.noiseFloor}, speech/silence gate ${vad.startLevel}/${vad.endLevel}` +\n `${startedAt}, ${endedBy})`,\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 (capture.detector.longestSoundMs > 0) {\n // Something crossed the gate, but too briefly for a word: a click, a knock, a cough.\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: only a short sound (${capture.detector.longestSoundMs} ms, ` +\n `RMS ${loudest}) — too short for speech (${MIN_SPEECH_MS} ms), dropping the utterance`,\n );\n } else 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', vad);\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 speech = prepareForStt(pcm, MIC_SAMPLE_RATE, { startMs: vad.speechStartMs, endLevel: vad.endLevel });\n vad.stt = { fromMs: speech.fromMs, toMs: speech.toMs, gain: speech.gain };\n this.opts.log.debug(\n `ESPHome ${this.deviceName}: speech recognition gets ${(speech.fromMs / 1000).toFixed(2)}–` +\n `${(speech.toMs / 1000).toFixed(2)} s${speech.gain > 1 ? `, amplified ×${speech.gain}` : ''}`,\n );\n const sttStart = Date.now();\n const text = (\n await this.opts.stt.transcribe(speech.pcm, MIC_SAMPLE_RATE, this.opts.language, hints)\n ).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', vad);\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 // Numbers only, no audio: shows where a request was cut off without keeping recordings.\n vad: { ...vad, levels: levelProfile(pcm, MIC_SAMPLE_RATE) },\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 clearInterval(capture.stallCheck);\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"]}
|
package/build/lib/voice/vad.js
CHANGED
|
@@ -61,6 +61,8 @@ class SpeechDetector {
|
|
|
61
61
|
silenceMs;
|
|
62
62
|
longSilenceMs;
|
|
63
63
|
longAfterMs;
|
|
64
|
+
minSpeechMs;
|
|
65
|
+
speechGapMs;
|
|
64
66
|
maxMs;
|
|
65
67
|
startTimeoutMs;
|
|
66
68
|
skipMs;
|
|
@@ -79,6 +81,14 @@ class SpeechDetector {
|
|
|
79
81
|
elapsedMs = 0;
|
|
80
82
|
/** Start of the frame that crossed {@link startLevel}, in ms from the start of the recording; -1 = not yet. */
|
|
81
83
|
speechStartMs = -1;
|
|
84
|
+
/** A crossing of {@link startLevel} that has not yet lasted {@link minSpeechMs}: its start, -1 = none. */
|
|
85
|
+
candidateMs = -1;
|
|
86
|
+
/** Quiet since the last loud frame of the candidate. */
|
|
87
|
+
candidateQuietMs = 0;
|
|
88
|
+
/** Sound above {@link endLevel} since the candidate (and then the speech) started, in ms. */
|
|
89
|
+
voicedMs = 0;
|
|
90
|
+
/** The longest sound that crossed {@link startLevel}, in ms — also one too short to count as speech. */
|
|
91
|
+
longestSoundMs = 0;
|
|
82
92
|
/** Audio analysed so far, in milliseconds (exposed for logging). */
|
|
83
93
|
totalMs = 0;
|
|
84
94
|
/** Loudest frame seen so far (exposed for logging — the usual clue when a mic is mis-configured). */
|
|
@@ -88,7 +98,7 @@ class SpeechDetector {
|
|
|
88
98
|
* it tells whether a dropped utterance was silence or a voice that was simply too quiet for the gate.
|
|
89
99
|
*/
|
|
90
100
|
analysedPeak = 0;
|
|
91
|
-
constructor({ sampleRate = 16000, frameMs = 30, startLevel = 700, endLevel = 400, silenceMs = 900, longSilenceMs = 0, longAfterMs = 0, maxMs = 12000, startTimeoutMs = 0, skipMs = 0, adaptive = false, minStartLevel = 250, minEndLevel = 150, startFactor = 3.5, endFactor = 2, warmupMs = 150, } = {}) {
|
|
101
|
+
constructor({ sampleRate = 16000, frameMs = 30, startLevel = 700, endLevel = 400, silenceMs = 900, longSilenceMs = 0, longAfterMs = 0, minSpeechMs = 0, speechGapMs = 300, maxMs = 12000, startTimeoutMs = 0, skipMs = 0, adaptive = false, minStartLevel = 250, minEndLevel = 150, startFactor = 3.5, endFactor = 2, warmupMs = 150, } = {}) {
|
|
92
102
|
this.frameBytes = Math.round((sampleRate * frameMs) / 1000) * 2;
|
|
93
103
|
this.frameMs = frameMs;
|
|
94
104
|
this.fixedStart = startLevel;
|
|
@@ -96,6 +106,8 @@ class SpeechDetector {
|
|
|
96
106
|
this.silenceMs = silenceMs;
|
|
97
107
|
this.longSilenceMs = longSilenceMs;
|
|
98
108
|
this.longAfterMs = longAfterMs;
|
|
109
|
+
this.minSpeechMs = minSpeechMs;
|
|
110
|
+
this.speechGapMs = speechGapMs;
|
|
99
111
|
this.maxMs = maxMs;
|
|
100
112
|
this.startTimeoutMs = startTimeoutMs;
|
|
101
113
|
this.skipMs = skipMs;
|
|
@@ -145,6 +157,7 @@ class SpeechDetector {
|
|
|
145
157
|
startLevel: Math.round(this.startLevel),
|
|
146
158
|
endLevel: Math.round(this.endLevel),
|
|
147
159
|
speechStartMs: this.speechSeen ? this.speechStartMs : undefined,
|
|
160
|
+
voicedMs: this.speechSeen ? this.voicedMs : undefined,
|
|
148
161
|
silenceMs: this.requiredSilenceMs,
|
|
149
162
|
};
|
|
150
163
|
}
|
|
@@ -184,7 +197,7 @@ class SpeechDetector {
|
|
|
184
197
|
? 0
|
|
185
198
|
: level < this.floor
|
|
186
199
|
? level
|
|
187
|
-
: this.speechSeen
|
|
200
|
+
: this.speechSeen || this.candidateMs >= 0
|
|
188
201
|
? this.floor
|
|
189
202
|
: this.floor + (level - this.floor) * NOISE_RISE;
|
|
190
203
|
if (this.totalMs < this.warmupMs) {
|
|
@@ -192,10 +205,33 @@ class SpeechDetector {
|
|
|
192
205
|
}
|
|
193
206
|
}
|
|
194
207
|
if (!this.speechSeen) {
|
|
195
|
-
|
|
208
|
+
// A loud frame opens a candidate; it becomes speech once it has lasted `minSpeechMs`, and is
|
|
209
|
+
// forgotten when it falls quiet for `speechGapMs` before that — a click is not a request.
|
|
210
|
+
if (this.candidateMs < 0) {
|
|
211
|
+
if (level >= this.startLevel) {
|
|
212
|
+
this.candidateMs = this.elapsedMs - this.frameMs;
|
|
213
|
+
this.candidateQuietMs = 0;
|
|
214
|
+
this.voicedMs = this.frameMs;
|
|
215
|
+
this.longestSoundMs = Math.max(this.longestSoundMs, this.voicedMs);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (level >= this.endLevel) {
|
|
219
|
+
this.voicedMs += this.frameMs;
|
|
220
|
+
this.candidateQuietMs = 0;
|
|
221
|
+
this.longestSoundMs = Math.max(this.longestSoundMs, this.voicedMs);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
this.candidateQuietMs += this.frameMs;
|
|
225
|
+
if (this.candidateQuietMs >= this.speechGapMs) {
|
|
226
|
+
this.candidateMs = -1;
|
|
227
|
+
this.voicedMs = 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (this.candidateMs >= 0 && this.voicedMs >= this.minSpeechMs) {
|
|
196
231
|
this.speechSeen = true;
|
|
197
|
-
this.speechStartMs = this.
|
|
198
|
-
|
|
232
|
+
this.speechStartMs = this.candidateMs;
|
|
233
|
+
// A gap inside the candidate already counts towards the end of the utterance.
|
|
234
|
+
this.silentMs = this.candidateQuietMs;
|
|
199
235
|
}
|
|
200
236
|
}
|
|
201
237
|
else if (level < this.endLevel) {
|
|
@@ -206,6 +242,7 @@ class SpeechDetector {
|
|
|
206
242
|
}
|
|
207
243
|
else {
|
|
208
244
|
this.silentMs = 0;
|
|
245
|
+
this.voicedMs += this.frameMs;
|
|
209
246
|
}
|
|
210
247
|
if (!this.speechSeen && this.startTimeoutMs > 0 && this.elapsedMs >= this.startTimeoutMs) {
|
|
211
248
|
verdict ||= 'no-speech';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vad.js","sourceRoot":"","sources":["../../../src/lib/voice/vad.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * End-of-speech detection for satellites that stream microphone audio until they are told to stop.\n *\n * The Hannah/UDP protocol has the satellite decide when an utterance ended (`audio_end`), and Wyoming\n * clients send `audio-stop` themselves. ESPHome voice satellites do neither: they keep streaming until\n * the server sends `STT_VAD_END`/`STT_END`, so the decision has to happen here.\n *\n * This is a plain energy gate, not a neural VAD — good enough to close an utterance, and it costs\n * nothing per frame. Speech must first rise above `startLevel`; after that, `silenceMs` worth of frames\n * below `endLevel` ends it. `maxMs` is the hard stop for rooms that are simply loud.\n *\n * Fixed thresholds only work when the microphone level is known, and on a real satellite it is not.\n * Measured on a ThirdReality Voice & Music Assistant: the device plays a wake-acknowledgement chirp\n * into its own microphone that peaks at full scale (RMS ~16000, clipped), while the speech that follows\n * arrives at RMS 358–469 — below the 700 default for `startLevel` and right on the 400 for `endLevel`.\n * That breaks the gate twice over: `sawSpeech` is satisfied by the chirp rather than by any voice, so\n * the \"nothing but speech-free audio\" guard never fires, and half the real speech frames count as\n * silence, so the utterance can be cut off mid-sentence.\n *\n * Hence two opt-in knobs, both enabled by the ESPHome transport:\n *\n * - `skipMs` drops the leading chirp from the *analysis* only. What reaches STT is cut later, from where\n * speech was detected minus a margin (`speechAudio.ts`) — a fast speaker may already be talking.\n * - `adaptive` derives the thresholds from a tracked noise floor instead of fixed levels. The floor\n * follows the quietest frame seen (instant down, very slow up), so it settles on room noise even when\n * someone starts talking immediately — which a mean or median over a calibration window would not.\n *\n * And one for longer requests: `longSilenceMs` after `longAfterMs` of speech. In a quiet room the silence\n * threshold sits at its minimum, and a voice a few metres away dips below it between syllables — on a\n * ThirdReality speaker a whole \"Мессенджер\" (0.7 s) stayed under 150. A pause for thought on top of that\n * ended a long request mid-sentence, while short commands should keep closing quickly.\n */\n\n/**\n * A buffer that may be a view onto any `ArrayBufferLike` — what `Buffer.subarray()` and\n * `Buffer.concat()` hand back. Plain `Buffer` (= `Buffer<ArrayBuffer>`) assigns to it, not the reverse.\n */\nexport type Bytes = Buffer<ArrayBufferLike>;\n\n/** Root-mean-square amplitude (0…32767) of a mono 16-bit signed LE PCM buffer. */\nexport function rms(pcm: Bytes): number {\n const samples = Math.floor(pcm.length / 2);\n if (!samples) {\n return 0;\n }\n let sum = 0;\n for (let i = 0; i < samples; i++) {\n const value = pcm.readInt16LE(i * 2);\n sum += value * value;\n }\n return Math.sqrt(sum / samples);\n}\n\nexport interface SpeechDetectorOptions {\n /** Sample rate of the incoming PCM (ESPHome satellites always capture at 16 kHz). */\n sampleRate?: number;\n /** Analysis window; 30 ms is the usual compromise between latency and stability. */\n frameMs?: number;\n /** RMS above which we consider speech to have started. Ignored when `adaptive` is on. */\n startLevel?: number;\n /** RMS below which a frame counts as silence. Ignored when `adaptive` is on. */\n endLevel?: number;\n /** Silence after speech that ends the utterance. */\n silenceMs?: number;\n /**\n * Silence that ends the utterance once the speaker has talked for {@link longAfterMs} (0 = always\n * {@link silenceMs}). Somebody dictating a longer request pauses to think, and a short command should\n * still close fast.\n */\n longSilenceMs?: number;\n /** Speech (from its start to the pause) after which {@link longSilenceMs} applies; 0 = never. */\n longAfterMs?: number;\n /** Hard cap on one utterance, in case the room never falls quiet. */\n maxMs?: number;\n /**\n * Give up when no speech has started this long after the stream began (the skipped lead-in included);\n * 0 = only {@link maxMs}. A false wake would otherwise keep the microphone open for the whole `maxMs`.\n */\n startTimeoutMs?: number;\n /**\n * Leading audio to leave out of the analysis — for devices that chirp into their own microphone\n * when the wake word fires. The audio itself is untouched; this only stops the chirp from being\n * mistaken for speech and from poisoning the noise floor.\n */\n skipMs?: number;\n /** Derive the thresholds from the tracked noise floor instead of using fixed levels. */\n adaptive?: boolean;\n /** Adaptive mode: `startLevel` is at least this, however quiet the room is. */\n minStartLevel?: number;\n /** Adaptive mode: `endLevel` is at least this. */\n minEndLevel?: number;\n /** Adaptive mode: speech threshold as a multiple of the noise floor. */\n startFactor?: number;\n /** Adaptive mode: silence threshold as a multiple of the noise floor. */\n endFactor?: number;\n /** Adaptive mode: settle the noise floor this long before speech may be declared. */\n warmupMs?: number;\n}\n\n/** Why an utterance ended: the speaker stopped, we hit the hard cap, or nobody started speaking at all. */\nexport type SpeechEnd = 'speech-end' | 'timeout' | 'no-speech';\n\n/** What the detector made of one recording — for the request trace and the diagnosis recordings. */\nexport interface VadReport {\n /** Loudest frame, the wake chirp included. */\n peak: number;\n /** Loudest frame after the skipped lead-in. */\n analysedPeak: number;\n noiseFloor: number;\n startLevel: number;\n endLevel: number;\n /** Where speech began, in ms from the start of the recording; absent when it never did. */\n speechStartMs?: number;\n /** Why the recording ended; 'stream' = the device stopped streaming by itself. */\n end?: SpeechEnd | 'stream';\n /** Silence that was needed to end it at that point (longer once the speaker talked for a while). */\n silenceMs?: number;\n /** RMS per 100 ms of the whole recording. */\n levels?: number[];\n /** What went to speech recognition: the recording cut to the speech, and the gain applied to it. */\n stt?: { fromMs: number; toMs: number; gain: number };\n}\n\n/**\n * How fast the tracked noise floor may follow a *rising* ambient level, per analysed frame (30 ms).\n * Fast enough that a genuinely noisy room is matched within about a second, slow enough that a voice\n * cannot pull the floor up to its own level before it has been recognised as speech.\n */\nconst NOISE_RISE = 0.02;\n\nexport class SpeechDetector {\n private readonly frameBytes: number;\n private readonly frameMs: number;\n private readonly fixedStart: number;\n private readonly fixedEnd: number;\n private readonly silenceMs: number;\n private readonly longSilenceMs: number;\n private readonly longAfterMs: number;\n private readonly maxMs: number;\n private readonly startTimeoutMs: number;\n private readonly skipMs: number;\n private readonly adaptive: boolean;\n private readonly minStartLevel: number;\n private readonly minEndLevel: number;\n private readonly startFactor: number;\n private readonly endFactor: number;\n private readonly warmupMs: number;\n\n private pending: Bytes = Buffer.alloc(0);\n private speechSeen = false;\n private silentMs = 0;\n /** Tracked room level in adaptive mode; null until the first analysed frame. */\n private floor: number | null = null;\n /** Audio seen so far *including* the skipped lead-in, in milliseconds. */\n private elapsedMs = 0;\n /** Start of the frame that crossed {@link startLevel}, in ms from the start of the recording; -1 = not yet. */\n speechStartMs = -1;\n /** Audio analysed so far, in milliseconds (exposed for logging). */\n totalMs = 0;\n /** Loudest frame seen so far (exposed for logging — the usual clue when a mic is mis-configured). */\n peak = 0;\n /**\n * Loudest frame after the skipped lead-in, i.e. without the wake chirp. Compared with {@link startLevel}\n * it tells whether a dropped utterance was silence or a voice that was simply too quiet for the gate.\n */\n analysedPeak = 0;\n\n constructor({\n sampleRate = 16000,\n frameMs = 30,\n startLevel = 700,\n endLevel = 400,\n silenceMs = 900,\n longSilenceMs = 0,\n longAfterMs = 0,\n maxMs = 12000,\n startTimeoutMs = 0,\n skipMs = 0,\n adaptive = false,\n minStartLevel = 250,\n minEndLevel = 150,\n startFactor = 3.5,\n endFactor = 2,\n warmupMs = 150,\n }: SpeechDetectorOptions = {}) {\n this.frameBytes = Math.round((sampleRate * frameMs) / 1000) * 2;\n this.frameMs = frameMs;\n this.fixedStart = startLevel;\n this.fixedEnd = endLevel;\n this.silenceMs = silenceMs;\n this.longSilenceMs = longSilenceMs;\n this.longAfterMs = longAfterMs;\n this.maxMs = maxMs;\n this.startTimeoutMs = startTimeoutMs;\n this.skipMs = skipMs;\n this.adaptive = adaptive;\n this.minStartLevel = minStartLevel;\n this.minEndLevel = minEndLevel;\n this.startFactor = startFactor;\n this.endFactor = endFactor;\n this.warmupMs = warmupMs;\n }\n\n /** True once any frame crossed {@link startLevel} — an utterance that never did is room noise. */\n get sawSpeech(): boolean {\n return this.speechSeen;\n }\n\n /** Tracked room level (adaptive mode), for logging. 0 before the first analysed frame. */\n get noiseFloor(): number {\n return this.floor ?? 0;\n }\n\n /** Current speech threshold — fixed, or derived from the noise floor in adaptive mode. */\n get startLevel(): number {\n return this.adaptive ? Math.max(this.minStartLevel, (this.floor ?? 0) * this.startFactor) : this.fixedStart;\n }\n\n /** Current silence threshold. Never above {@link startLevel}, so the gate keeps its hysteresis. */\n get endLevel(): number {\n if (!this.adaptive) {\n return this.fixedEnd;\n }\n return Math.min(this.startLevel, Math.max(this.minEndLevel, (this.floor ?? 0) * this.endFactor));\n }\n\n /**\n * Silence that ends the utterance right now: {@link silenceMs}, or {@link longSilenceMs} once the speaker\n * has talked for {@link longAfterMs} before the current pause.\n */\n get requiredSilenceMs(): number {\n if (!this.speechSeen || !this.longAfterMs || this.longSilenceMs <= this.silenceMs) {\n return this.silenceMs;\n }\n const spokenMs = this.elapsedMs - this.silentMs - this.speechStartMs;\n return spokenMs >= this.longAfterMs ? this.longSilenceMs : this.silenceMs;\n }\n\n /** The detector's view of the recording so far. */\n report(): VadReport {\n return {\n peak: Math.round(this.peak),\n analysedPeak: Math.round(this.analysedPeak),\n noiseFloor: Math.round(this.noiseFloor),\n startLevel: Math.round(this.startLevel),\n endLevel: Math.round(this.endLevel),\n speechStartMs: this.speechSeen ? this.speechStartMs : undefined,\n silenceMs: this.requiredSilenceMs,\n };\n }\n\n /** Feed one audio chunk (any size). Returns why the utterance ended, or null to keep listening. */\n push(chunk: Bytes): SpeechEnd | null {\n this.pending = this.pending.length ? Buffer.concat([this.pending, chunk]) : chunk;\n let verdict: SpeechEnd | null = null;\n\n while (this.pending.length >= this.frameBytes) {\n const frame = this.pending.subarray(0, this.frameBytes);\n this.pending = this.pending.subarray(this.frameBytes);\n this.elapsedMs += this.frameMs;\n\n const level = rms(frame);\n if (level > this.peak) {\n this.peak = level;\n }\n\n // The device's own wake chirp: loud, clipped, and not speech. Leaving it out of the\n // analysis is what makes `sawSpeech` mean anything, and keeps the noise floor honest.\n if (this.elapsedMs <= this.skipMs) {\n continue;\n }\n this.totalMs += this.frameMs;\n if (level > this.analysedPeak) {\n this.analysedPeak = level;\n }\n\n if (this.adaptive) {\n // Three rules, each there to defeat a specific way of getting this wrong:\n // - start at zero, i.e. maximally sensitive. Seeding from the first frame instead\n // lets someone who talks straight after the wake word install their own voice as\n // the room's noise floor, which shuts the gate for good — the same trap an average\n // or median over a calibration window falls into.\n // - fall instantly, so a room that went quiet is believed at once.\n // - rise slowly, and not at all once speech has been recognised: otherwise a long\n // utterance drags the floor, and with it `endLevel`, up to its own level and cuts\n // itself off mid-sentence. Falling stays on, so the pauses between words still count.\n this.floor =\n this.floor === null\n ? 0\n : level < this.floor\n ? level\n : this.speechSeen\n ? this.floor\n : this.floor + (level - this.floor) * NOISE_RISE;\n if (this.totalMs < this.warmupMs) {\n continue; // let the floor settle before anything may count as speech\n }\n }\n\n if (!this.speechSeen) {\n if (level >= this.startLevel) {\n this.speechSeen = true;\n this.speechStartMs = this.elapsedMs - this.frameMs;\n this.silentMs = 0;\n }\n } else if (level < this.endLevel) {\n this.silentMs += this.frameMs;\n if (this.silentMs >= this.requiredSilenceMs) {\n verdict ||= 'speech-end';\n }\n } else {\n this.silentMs = 0;\n }\n\n if (!this.speechSeen && this.startTimeoutMs > 0 && this.elapsedMs >= this.startTimeoutMs) {\n verdict ||= 'no-speech';\n }\n if (this.totalMs >= this.maxMs) {\n verdict ||= 'timeout';\n }\n }\n return verdict;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"vad.js","sourceRoot":"","sources":["../../../src/lib/voice/vad.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * End-of-speech detection for satellites that stream microphone audio until they are told to stop.\n *\n * The Hannah/UDP protocol has the satellite decide when an utterance ended (`audio_end`), and Wyoming\n * clients send `audio-stop` themselves. ESPHome voice satellites do neither: they keep streaming until\n * the server sends `STT_VAD_END`/`STT_END`, so the decision has to happen here.\n *\n * This is a plain energy gate, not a neural VAD — good enough to close an utterance, and it costs\n * nothing per frame. Speech must first rise above `startLevel`; after that, `silenceMs` worth of frames\n * below `endLevel` ends it. `maxMs` is the hard stop for rooms that are simply loud.\n *\n * Fixed thresholds only work when the microphone level is known, and on a real satellite it is not.\n * Measured on a ThirdReality Voice & Music Assistant: the device plays a wake-acknowledgement chirp\n * into its own microphone that peaks at full scale (RMS ~16000, clipped), while the speech that follows\n * arrives at RMS 358–469 — below the 700 default for `startLevel` and right on the 400 for `endLevel`.\n * That breaks the gate twice over: `sawSpeech` is satisfied by the chirp rather than by any voice, so\n * the \"nothing but speech-free audio\" guard never fires, and half the real speech frames count as\n * silence, so the utterance can be cut off mid-sentence.\n *\n * Hence two opt-in knobs, both enabled by the ESPHome transport:\n *\n * - `skipMs` drops the leading chirp from the *analysis* only. What reaches STT is cut later, from where\n * speech was detected minus a margin (`speechAudio.ts`) — a fast speaker may already be talking.\n * - `adaptive` derives the thresholds from a tracked noise floor instead of fixed levels. The floor\n * follows the quietest frame seen (instant down, very slow up), so it settles on room noise even when\n * someone starts talking immediately — which a mean or median over a calibration window would not.\n *\n * And one for longer requests: `longSilenceMs` after `longAfterMs` of speech. In a quiet room the silence\n * threshold sits at its minimum, and a voice a few metres away dips below it between syllables — on a\n * ThirdReality speaker a whole \"Мессенджер\" (0.7 s) stayed under 150. A pause for thought on top of that\n * ended a long request mid-sentence, while short commands should keep closing quickly.\n */\n\n/**\n * A buffer that may be a view onto any `ArrayBufferLike` — what `Buffer.subarray()` and\n * `Buffer.concat()` hand back. Plain `Buffer` (= `Buffer<ArrayBuffer>`) assigns to it, not the reverse.\n */\nexport type Bytes = Buffer<ArrayBufferLike>;\n\n/** Root-mean-square amplitude (0…32767) of a mono 16-bit signed LE PCM buffer. */\nexport function rms(pcm: Bytes): number {\n const samples = Math.floor(pcm.length / 2);\n if (!samples) {\n return 0;\n }\n let sum = 0;\n for (let i = 0; i < samples; i++) {\n const value = pcm.readInt16LE(i * 2);\n sum += value * value;\n }\n return Math.sqrt(sum / samples);\n}\n\nexport interface SpeechDetectorOptions {\n /** Sample rate of the incoming PCM (ESPHome satellites always capture at 16 kHz). */\n sampleRate?: number;\n /** Analysis window; 30 ms is the usual compromise between latency and stability. */\n frameMs?: number;\n /** RMS above which we consider speech to have started. Ignored when `adaptive` is on. */\n startLevel?: number;\n /** RMS below which a frame counts as silence. Ignored when `adaptive` is on. */\n endLevel?: number;\n /** Silence after speech that ends the utterance. */\n silenceMs?: number;\n /**\n * Silence that ends the utterance once the speaker has talked for {@link longAfterMs} (0 = always\n * {@link silenceMs}). Somebody dictating a longer request pauses to think, and a short command should\n * still close fast.\n */\n longSilenceMs?: number;\n /** Speech (from its start to the pause) after which {@link longSilenceMs} applies; 0 = never. */\n longAfterMs?: number;\n /**\n * Sound above the silence threshold needed before a crossing of the speech threshold counts as speech; 0 = the\n * first loud frame does. A click or a knock is over in 100 ms, the shortest \"да\" takes about 250.\n */\n minSpeechMs?: number;\n /** A candidate start that falls quiet this long before reaching {@link minSpeechMs} is forgotten. */\n speechGapMs?: number;\n /** Hard cap on one utterance, in case the room never falls quiet. */\n maxMs?: number;\n /**\n * Give up when no speech has started this long after the stream began (the skipped lead-in included);\n * 0 = only {@link maxMs}. A false wake would otherwise keep the microphone open for the whole `maxMs`.\n */\n startTimeoutMs?: number;\n /**\n * Leading audio to leave out of the analysis — for devices that chirp into their own microphone\n * when the wake word fires. The audio itself is untouched; this only stops the chirp from being\n * mistaken for speech and from poisoning the noise floor.\n */\n skipMs?: number;\n /** Derive the thresholds from the tracked noise floor instead of using fixed levels. */\n adaptive?: boolean;\n /** Adaptive mode: `startLevel` is at least this, however quiet the room is. */\n minStartLevel?: number;\n /** Adaptive mode: `endLevel` is at least this. */\n minEndLevel?: number;\n /** Adaptive mode: speech threshold as a multiple of the noise floor. */\n startFactor?: number;\n /** Adaptive mode: silence threshold as a multiple of the noise floor. */\n endFactor?: number;\n /** Adaptive mode: settle the noise floor this long before speech may be declared. */\n warmupMs?: number;\n}\n\n/** Why an utterance ended: the speaker stopped, we hit the hard cap, or nobody started speaking at all. */\nexport type SpeechEnd = 'speech-end' | 'timeout' | 'no-speech';\n\n/**\n * Why a recording ended, from the transport's view: what the detector said, the device ending the stream\n * ('stream'), the device going quiet without ending it ('stall'), or the transport's last-resort timer.\n */\nexport type CaptureEnd = SpeechEnd | 'stream' | 'stall' | 'pipeline-timeout';\n\n/** What the detector made of one recording — for the request trace and the diagnosis recordings. */\nexport interface VadReport {\n /** Loudest frame, the wake chirp included. */\n peak: number;\n /** Loudest frame after the skipped lead-in. */\n analysedPeak: number;\n noiseFloor: number;\n startLevel: number;\n endLevel: number;\n /** Where speech began, in ms from the start of the recording; absent when it never did. */\n speechStartMs?: number;\n /** Why the recording ended. */\n end?: CaptureEnd;\n /** Sound above the silence threshold since speech started, in ms. */\n voicedMs?: number;\n /** Silence that was needed to end it at that point (longer once the speaker talked for a while). */\n silenceMs?: number;\n /** RMS per 100 ms of the whole recording. */\n levels?: number[];\n /** What went to speech recognition: the recording cut to the speech, and the gain applied to it. */\n stt?: { fromMs: number; toMs: number; gain: number };\n}\n\n/**\n * How fast the tracked noise floor may follow a *rising* ambient level, per analysed frame (30 ms).\n * Fast enough that a genuinely noisy room is matched within about a second, slow enough that a voice\n * cannot pull the floor up to its own level before it has been recognised as speech.\n */\nconst NOISE_RISE = 0.02;\n\nexport class SpeechDetector {\n private readonly frameBytes: number;\n private readonly frameMs: number;\n private readonly fixedStart: number;\n private readonly fixedEnd: number;\n private readonly silenceMs: number;\n private readonly longSilenceMs: number;\n private readonly longAfterMs: number;\n private readonly minSpeechMs: number;\n private readonly speechGapMs: number;\n private readonly maxMs: number;\n private readonly startTimeoutMs: number;\n private readonly skipMs: number;\n private readonly adaptive: boolean;\n private readonly minStartLevel: number;\n private readonly minEndLevel: number;\n private readonly startFactor: number;\n private readonly endFactor: number;\n private readonly warmupMs: number;\n\n private pending: Bytes = Buffer.alloc(0);\n private speechSeen = false;\n private silentMs = 0;\n /** Tracked room level in adaptive mode; null until the first analysed frame. */\n private floor: number | null = null;\n /** Audio seen so far *including* the skipped lead-in, in milliseconds. */\n private elapsedMs = 0;\n /** Start of the frame that crossed {@link startLevel}, in ms from the start of the recording; -1 = not yet. */\n speechStartMs = -1;\n /** A crossing of {@link startLevel} that has not yet lasted {@link minSpeechMs}: its start, -1 = none. */\n private candidateMs = -1;\n /** Quiet since the last loud frame of the candidate. */\n private candidateQuietMs = 0;\n /** Sound above {@link endLevel} since the candidate (and then the speech) started, in ms. */\n voicedMs = 0;\n /** The longest sound that crossed {@link startLevel}, in ms — also one too short to count as speech. */\n longestSoundMs = 0;\n /** Audio analysed so far, in milliseconds (exposed for logging). */\n totalMs = 0;\n /** Loudest frame seen so far (exposed for logging — the usual clue when a mic is mis-configured). */\n peak = 0;\n /**\n * Loudest frame after the skipped lead-in, i.e. without the wake chirp. Compared with {@link startLevel}\n * it tells whether a dropped utterance was silence or a voice that was simply too quiet for the gate.\n */\n analysedPeak = 0;\n\n constructor({\n sampleRate = 16000,\n frameMs = 30,\n startLevel = 700,\n endLevel = 400,\n silenceMs = 900,\n longSilenceMs = 0,\n longAfterMs = 0,\n minSpeechMs = 0,\n speechGapMs = 300,\n maxMs = 12000,\n startTimeoutMs = 0,\n skipMs = 0,\n adaptive = false,\n minStartLevel = 250,\n minEndLevel = 150,\n startFactor = 3.5,\n endFactor = 2,\n warmupMs = 150,\n }: SpeechDetectorOptions = {}) {\n this.frameBytes = Math.round((sampleRate * frameMs) / 1000) * 2;\n this.frameMs = frameMs;\n this.fixedStart = startLevel;\n this.fixedEnd = endLevel;\n this.silenceMs = silenceMs;\n this.longSilenceMs = longSilenceMs;\n this.longAfterMs = longAfterMs;\n this.minSpeechMs = minSpeechMs;\n this.speechGapMs = speechGapMs;\n this.maxMs = maxMs;\n this.startTimeoutMs = startTimeoutMs;\n this.skipMs = skipMs;\n this.adaptive = adaptive;\n this.minStartLevel = minStartLevel;\n this.minEndLevel = minEndLevel;\n this.startFactor = startFactor;\n this.endFactor = endFactor;\n this.warmupMs = warmupMs;\n }\n\n /** True once any frame crossed {@link startLevel} — an utterance that never did is room noise. */\n get sawSpeech(): boolean {\n return this.speechSeen;\n }\n\n /** Tracked room level (adaptive mode), for logging. 0 before the first analysed frame. */\n get noiseFloor(): number {\n return this.floor ?? 0;\n }\n\n /** Current speech threshold — fixed, or derived from the noise floor in adaptive mode. */\n get startLevel(): number {\n return this.adaptive ? Math.max(this.minStartLevel, (this.floor ?? 0) * this.startFactor) : this.fixedStart;\n }\n\n /** Current silence threshold. Never above {@link startLevel}, so the gate keeps its hysteresis. */\n get endLevel(): number {\n if (!this.adaptive) {\n return this.fixedEnd;\n }\n return Math.min(this.startLevel, Math.max(this.minEndLevel, (this.floor ?? 0) * this.endFactor));\n }\n\n /**\n * Silence that ends the utterance right now: {@link silenceMs}, or {@link longSilenceMs} once the speaker\n * has talked for {@link longAfterMs} before the current pause.\n */\n get requiredSilenceMs(): number {\n if (!this.speechSeen || !this.longAfterMs || this.longSilenceMs <= this.silenceMs) {\n return this.silenceMs;\n }\n const spokenMs = this.elapsedMs - this.silentMs - this.speechStartMs;\n return spokenMs >= this.longAfterMs ? this.longSilenceMs : this.silenceMs;\n }\n\n /** The detector's view of the recording so far. */\n report(): VadReport {\n return {\n peak: Math.round(this.peak),\n analysedPeak: Math.round(this.analysedPeak),\n noiseFloor: Math.round(this.noiseFloor),\n startLevel: Math.round(this.startLevel),\n endLevel: Math.round(this.endLevel),\n speechStartMs: this.speechSeen ? this.speechStartMs : undefined,\n voicedMs: this.speechSeen ? this.voicedMs : undefined,\n silenceMs: this.requiredSilenceMs,\n };\n }\n\n /** Feed one audio chunk (any size). Returns why the utterance ended, or null to keep listening. */\n push(chunk: Bytes): SpeechEnd | null {\n this.pending = this.pending.length ? Buffer.concat([this.pending, chunk]) : chunk;\n let verdict: SpeechEnd | null = null;\n\n while (this.pending.length >= this.frameBytes) {\n const frame = this.pending.subarray(0, this.frameBytes);\n this.pending = this.pending.subarray(this.frameBytes);\n this.elapsedMs += this.frameMs;\n\n const level = rms(frame);\n if (level > this.peak) {\n this.peak = level;\n }\n\n // The device's own wake chirp: loud, clipped, and not speech. Leaving it out of the\n // analysis is what makes `sawSpeech` mean anything, and keeps the noise floor honest.\n if (this.elapsedMs <= this.skipMs) {\n continue;\n }\n this.totalMs += this.frameMs;\n if (level > this.analysedPeak) {\n this.analysedPeak = level;\n }\n\n if (this.adaptive) {\n // Three rules, each there to defeat a specific way of getting this wrong:\n // - start at zero, i.e. maximally sensitive. Seeding from the first frame instead\n // lets someone who talks straight after the wake word install their own voice as\n // the room's noise floor, which shuts the gate for good — the same trap an average\n // or median over a calibration window falls into.\n // - fall instantly, so a room that went quiet is believed at once.\n // - rise slowly, and not at all once speech has been recognised: otherwise a long\n // utterance drags the floor, and with it `endLevel`, up to its own level and cuts\n // itself off mid-sentence. Falling stays on, so the pauses between words still count.\n this.floor =\n this.floor === null\n ? 0\n : level < this.floor\n ? level\n : this.speechSeen || this.candidateMs >= 0\n ? this.floor\n : this.floor + (level - this.floor) * NOISE_RISE;\n if (this.totalMs < this.warmupMs) {\n continue; // let the floor settle before anything may count as speech\n }\n }\n\n if (!this.speechSeen) {\n // A loud frame opens a candidate; it becomes speech once it has lasted `minSpeechMs`, and is\n // forgotten when it falls quiet for `speechGapMs` before that — a click is not a request.\n if (this.candidateMs < 0) {\n if (level >= this.startLevel) {\n this.candidateMs = this.elapsedMs - this.frameMs;\n this.candidateQuietMs = 0;\n this.voicedMs = this.frameMs;\n this.longestSoundMs = Math.max(this.longestSoundMs, this.voicedMs);\n }\n } else if (level >= this.endLevel) {\n this.voicedMs += this.frameMs;\n this.candidateQuietMs = 0;\n this.longestSoundMs = Math.max(this.longestSoundMs, this.voicedMs);\n } else {\n this.candidateQuietMs += this.frameMs;\n if (this.candidateQuietMs >= this.speechGapMs) {\n this.candidateMs = -1;\n this.voicedMs = 0;\n }\n }\n if (this.candidateMs >= 0 && this.voicedMs >= this.minSpeechMs) {\n this.speechSeen = true;\n this.speechStartMs = this.candidateMs;\n // A gap inside the candidate already counts towards the end of the utterance.\n this.silentMs = this.candidateQuietMs;\n }\n } else if (level < this.endLevel) {\n this.silentMs += this.frameMs;\n if (this.silentMs >= this.requiredSilenceMs) {\n verdict ||= 'speech-end';\n }\n } else {\n this.silentMs = 0;\n this.voicedMs += this.frameMs;\n }\n\n if (!this.speechSeen && this.startTimeoutMs > 0 && this.elapsedMs >= this.startTimeoutMs) {\n verdict ||= 'no-speech';\n }\n if (this.totalMs >= this.maxMs) {\n verdict ||= 'timeout';\n }\n }\n return verdict;\n }\n}\n"]}
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "assistant",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"1.4.4": {
|
|
7
|
+
"en": "ESPHome satellites: a sound only counts as speech once it has lasted about 200 ms. After a false wake a single click opened the gate, and Whisper made \"и все просят.\" of the amplified silence around it\nESPHome satellites: when a device stops sending audio without ending the stream, the recording is closed after 2 seconds instead of the 30-second pipeline timeout, during which the satellite showed \"listening\"",
|
|
8
|
+
"de": "ESPHome-Satelliten: Ein Ton gilt erst nach etwa 200 ms als Sprache. Nach einem falschen Weckruf öffnete ein Klick das Tor, und Whisper machte aus der Stille um es herum „und“\nESPHome-Satelliten: Wenn ein Gerät den Ton nicht sendet, ohne den Stream zu beenden, wird die Aufnahme nach 2 Sekunden beendet, statt nach 30 Sekunden, in der der Satellit „zuhört“",
|
|
9
|
+
"ru": "Спутники ESPhome: звук считается речью только в том случае, если он длится около 200 мс. После ложного пробуждения один щелчок открыл ворота, и Whisper издал «и все прольет», а вокруг них усилилась тишина\nСпутники ESPhome: когда устройство перестает отправлять звук, не прерывая поток, запись закрывается через 2 секунды вместо 30-секундного тайм-аута, в течение которого спутник показывал «прослушивание»",
|
|
10
|
+
"pt": "Satélites ESPHome: um som só conta como fala quando dura cerca de 200 ms. Depois de um falso despertar, um único clique abriu o portão e Whisper fez “” do silêncio amplificado ao redor dele.\nSatélites ESPHome: quando um dispositivo para de enviar áudio sem encerrar o stream, a gravação é fechada após 2 segundos, em vez do tempo limite de 30 segundos, durante o qual o satélite está “ouvindo”",
|
|
11
|
+
"nl": "ESPhome-satellieten: een geluid telt pas als spraak als het ongeveer 200 ms heeft geduurd. Na een valse wake opende een enkele klik de poort, en Whisper maakte „.” van de versterkte stilte eromheen\nESPhomesatellieten: wanneer een apparaat stopt met het verzenden van audio zonder de stream te beëindigen, wordt de opname na 2 seconden afgesloten in plaats van de time-out van 30 seconden tijdens de pijplijn, waarbij de satelliet „luisterde”",
|
|
12
|
+
"fr": "Satellites ESPhome : un son ne compte comme parole qu'après environ 200 ms. Après un faux réveil, un simple clic a ouvert le portail, et Whisper a fait « и в спросстт ». Le silence amplifié qui l'entourait\nSatellites ESPhome : lorsqu'un appareil arrête d'envoyer du son sans mettre fin au stream, l'enregistrement est fermé au bout de 2 secondes au lieu du délai de 30 secondes, pendant lequel le satellite indiquait « en écoute »",
|
|
13
|
+
"it": "Satelliti ESPHome: un suono conta come voce solo dopo circa 200 ms. Dopo un falso risveglio, un solo clic aprì il cancello e Whisper generò «e» dal silenzio amplificato che lo circondava\nSatelliti ESPhome: quando un dispositivo smette di inviare audio senza terminare lo streaming, la registrazione si chiude dopo 2 secondi invece del timeout di 30 secondi, durante il quale il satellite è «in ascolto»",
|
|
14
|
+
"es": "Satélites ESPhome: un sonido solo cuenta como voz una vez que dura unos 200 ms. Tras una falsa estela, un solo clic abrió la puerta y Whisper hizo «\nSatélites ESPhome: cuando un dispositivo deja de enviar audio sin terminar la transmisión, la grabación se cierra a los 2 segundos en lugar del tiempo de espera de 30 segundos, durante el cual el satélite mostró «escucha»",
|
|
15
|
+
"pl": "Satelity ESPhome: dźwięk liczy się jako mowa tylko wtedy, gdy trwa około 200 ms. Po fałszywym przebudzeniu jednym kliknięciem otworzyło bramę, a Whisper zrobił „и все просят.” wzmocnionej ciszy wokół niej\nSatelity ESPhome: gdy urządzenie przestaje wysyłać dźwięk bez zakończenia strumienia, nagranie zostaje zamknięte po 2 sekundach zamiast 30-sekundowego limitu czasu rurociągu, podczas którego satelita pokazywał „słuchanie”",
|
|
16
|
+
"uk": "Супутники ESPHome: звук вважається мовою лише після того, як він тривав близько 200 мс. Після помилкового пробудження одним клацанням відкрили ворота, і Whisper зробив «и все просят.» посиленої тиші навколо нього\nСупутники ESPHome: коли пристрій перестає надсилати аудіо без закінчення потоку, запис закривається через 2 секунди замість 30-секундного тайм-ауту конвеєра, під час якого супутник показав «прослуховування»",
|
|
17
|
+
"zh-cn": "ESPhome 卫星:声音只有持续大约 200 毫秒后才算作语音。在虚假的醒来之后,轻点一下就打开了大门,然后 Whisper 把周围放大的沉默变成了 “q q.”\nESPhome 卫星:当设备在不结束直播的情况下停止发送音频时,录制将在 2 秒后关闭,而不是在 30 秒的管道超时时关闭,在此期间,卫星显示 “正在收听”"
|
|
18
|
+
},
|
|
6
19
|
"1.4.3": {
|
|
7
20
|
"en": "An empty question sent with `sendTo('ask')` is rejected (`{error: 'empty question'}`) instead of going to the model with nothing but the time context, and the log names the sender once a minute. A script that asked on every answer looped about once a second and used pi-agent tokens. The log line of every `ask` now names the sender",
|
|
8
21
|
"de": "Eine leere Frage mit `sendTo ('ask') `wird abgewiesen (` {error: 'empty question'} `), statt nur mit Zeitkontext zum Modell zu gehen, und das Log benennt den Absender einmal pro Minute. Ein Skript, das jede Antwort fragte, lief einmal pro Sekunde und nutzte Pi-Agent-Tokens. Die Logzeile jeder `Ask` nennt nun den Absender",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "Polecenia na później: „wyłącz światło w ciągu 2 godzin” lub „osłona do 50% o 22:00,” staje się wydarzeniem w kalendarzu (adapter pełnego kalendarza) zamiast działać teraz; sztuczna inteligencja może planować, wyświetlać i anulować wydarzenia. Wcześniej silnik reguł przełączał takie polecenia od razu. Wymaga pełnej wersji kalendarza z nowym interfejsem API zdarzeń (po wersji 3.0.5)\nUstawienia: stan wiersza obecności jest wybierany za pomocą przeglądarki obiektów, rodzaj wiersza obecności lub celu ogłoszenia jest rozwijaną\nNowy dostawca „System-wide AI settings (admin)”, domyślne dla nowych instancji: dostawca, model i klucz pochodzą z ustawień AI ustawień systemu administratora, zmiana tam ponownie uruchamia instancję. Instancja bez własnego klucza też je bierze\nNowy dostawca OpenRouter\nOpenRouter do rozpoznawania mowy i głosu: jeden klucz dla modeli mowy wielu dostawców (Whisper, Deepgram, ElevenLabs, Gemini TTS,...), modele i głosy są wymienione w ustawieniach (domyślny model głosowy: ElevenLabs Flash v2.5)\nTest głosowy na karcie Głos: wypowiedz zdanie testowe i przetestuj rozpoznawanie mowy za pomocą mikrofonu przeglądarki, pliku audio lub podróży w obie strony, wszystko z niezapisanymi ustawieniami\n„Potwierdź polecenia przełączania za pomocą tonu” obejmuje teraz również polecenia wykonywane przez AI (lub pi-agent)\nW poleceniach („Lautstärke auf fünfzig Prozent”, „dwadzieścia pięć procent”, „практидцать пять известтов”), a sztuczna inteligencja widzi głośność każdego satelity głosowego, więc zamiast tego nie wyświetla już urządzenia multimedialnego pomieszczenia\nNowy dostawca „Adapter pi-agent”: trwały agent adaptera pi-agent odpowiada na to, czego nie mogą lokalne poziomy, z jedną rozmową na satelitę/czat",
|
|
81
94
|
"uk": "Команди на потім: «вимкнути світло через 2 години» або «заблокує до 50% о 22:00» стає подією в календарі (адаптер fullcalendar) замість запуску зараз; AI може планувати, перераховувати та скасовувати події. Раніше двигун правил перемикав такі команди відразу. Потрібна повнокалендарна версія з новим API подій (після 3.0.5)\nПараметри: стан рядка присутності вибирається за допомогою браузера об'єктів, тип рядка присутності або цілі оголошення - випадаючий список\nНовий провайдер «System-wide AI settings (admin)», за замовчуванням для нових екземплярів: провайдер, модель і ключ надходять з налаштувань AI налаштувань системи адміністратора, зміна там перезавантажує екземпляр. Примірник без власного ключа також приймає їх\nНовий провайдер OpenRouter\nOpenRouter для розпізнавання мовлення та голосу: одна клавіша для мовних моделей багатьох постачальників (Whisper, Deepgram, ElevenLabs, Gemini TTS,...), моделі та голоси перераховані в налаштуваннях (голосова модель за замовчуванням: ElevenLabs Flash v2.5)\nГолосовий тест на вкладці Голос: проговоріть тестове речення та перевірте розпізнавання мовлення за допомогою мікрофона браузера, аудіофайлу або туди й назад, все з незбереженими налаштуваннями\n«Підтвердження команд перемикання тоном» тепер також охоплює команди, які виконуються AI (або pi-агентом)\nВідтворюються числа до 100 в командах («Lautstärke auf fünfzig Prozent», «двадцять п'ять відсотків», «двадцять пать процентов»), і ШІ бачить гучність кожного голосового супутника, тому він більше не виводить медіа-пристрій кімнати замість цього\nНовий провайдер «Адаптер pi-agent»: довговічний агент адаптера pi-agent відповідає на те, що локальні рівні не можуть, з однією розмовою на супутник/чат",
|
|
82
95
|
"zh-cn": "稍后命令:“2 小时后关灯” 或 “晚上 10 点盲到 50%” 将成为日历中的一个事件(完整日历适配器),而不是现在运行;AI 可以计划、列出和取消活动。以前,规则引擎会立即切换此类命令。需要带有新活动 API 的完整日历版本(3.0.5 之后)\n设置:在线状态由对象浏览器选择,在线状态行或公告目标的类型是下拉列表\n新提供商 “系统级 AI 设置(管理员)”,新实例的默认设置:提供商、型号和密钥来自管理系统设置的 AI 设置,此处的更改会重新启动实例。没有自己的密钥的实例也会使用它们\n新供应商 OpenRouter\n用于语音识别和语音的 OpenRouter:许多供应商(Whisper、Deepgram、ElevenLabs、Gemini TTS 等)的语音模型都是一把钥匙,设置中列出了模型和语音(默认语音模型:ElevenLabs Flash v2.5)\n在 “语音” 选项卡上进行语音测试:说出测试句子,使用浏览器麦克风、音频文件或往返测试语音识别,所有这些都使用未保存的设置\n“用提示音确认交换机命令” 现在还涵盖了人工智能(或 pi-agent)执行的命令\n在命令中拼出最多 100 的数字(“Lautstärke auf fünfzig Prozent”、“百分之二十五”、“百分之二十五”、“páázátán”),人工智能可以看到每颗语音卫星的音量,因此它不再显示房间的媒体设备\n新提供商 “Adapter pi-agent”:pi-agent 适配器的耐用代理可以回答本地层无法解决的问题,每个卫星/聊天只能进行一次对话"
|
|
83
|
-
},
|
|
84
|
-
"1.2.0": {
|
|
85
|
-
"en": "Own wake words for ESPHome satellites: upload a microWakeWord model in the satellite settings, the device downloads it when it is activated. Models the device cannot run (e.g. openWakeWord) are rejected on upload with the reason\nVoice answers come faster: the device list for the offline rule engine is built once and shared for a minute instead of up to three times per spoken command\nThe local LLM is now the fallback for when the cloud LLM fails; \"Ask the local model first\" restores the old order. It is stopped after a configurable timeout, answers shorter and leaves one CPU core free\nWhen an answer asks back, UDP satellites now keep the microphone open for the reply too (needs `@iobroker/assistant-satellite` 0.2.0)\n\"Louder\", \"quieter\", \"volume 5\" change the volume of the satellite that heard it - ESPHome, ioBroker-native and UDP satellites\n\"How's the weather tomorrow?\" is answered from the weather adapter without an LLM\nFixed: Russian questions with \"будет\" (\"will it be …\") were taken for alarm queries",
|
|
86
|
-
"de": "Eigene Weckwörter für ESPhome: Laden Sie ein MicroWakeWord-Modell in den Sat-Einstellungen hoch, wenn es aktiviert ist. Geräteunfähige Modelle (z. B. OpenWakeWord) werden beim Upload abgelehnt\nSprachantworten schneller: Die Geräteliste für die Offline-Regel-Engine wird einmal erstellt und eine Minute lang geteilt, statt dreimal per Sprachbefehl\nDas lokale LLM ist nun die Lösung für Cloud-LLM-Fehler. „Fragen Sie zuerst das lokale Model“ Es stoppt nach einem Timeout, antwortet kürzer und lässt einen CPU-Kern frei\nWenn eine Antwort fragt, lassen UDP-Satelliten jetzt auch das Mikrofon offen (benötigt `@iobroker /assistant-satellite` 0.2.0)\n„Lauter“, „leiser“, „Lautstärke 5“ ändern die Lautstärke des Satelliten, der es gehört hat — ESPhome, IOBroker-native und UDP-Satelliten\n„Wie ist das Wetter morgen?“ wird vom Wetteradapter ohne LLM beantwortet\nBehoben: Russische Fragen mit „Wird es...“ galten als Alarmanfragen",
|
|
87
|
-
"ru": "Собственные слова для пробуждения спутников EspHome: загрузите модель MicroWakeWord в настройках спутника, устройство загрузит ее при активации. Модели, которые устройство не может запускать (например, OpenWakeword), отклоняются при загрузке по уважительной причине\nГолосовые ответы приходят быстрее: список устройств для автономного движка правил создается один раз и делится им в течение минуты вместо трех раз за одну голосовую команду\nЛокальный LLM теперь является запасным вариантом на случай сбоя облачного LLM; принцип «Сначала задайте вопрос локальной модели» восстанавливает прежний порядок. Он останавливается по истечении настраиваемого тайм-аута, время ответа сокращается, а одно ядро процессора остается свободным\nКогда ответ возвращается, спутники UDP теперь также оставляют микрофон открытым для ответа (требуется `@iobroker /assistant-satellite` 0.2.0)\n«Громче», «тише», «громче 5» изменяйте громкость спутника, который его услышал — спутники ESPhome, IOBroker-native и UDP\n«Какая погода завтра?» отвечает погодный адаптер без LLM\nИсправлено: русскоязычные вопросы с буквой «будет ли это...» принимались за тревожные запросы",
|
|
88
|
-
"pt": "Sinais próprios para satélites ESPHome: carregue um modelo microwakeWord nas configurações do satélite e o aparelho o baixa. Modelos inoperantes (ex.: OpenWakeWord) são rejeitados no upload\nAs respostas por voz são mais rápidas: a lista de dispositivos do motor de regras offline é compartilhada por um minuto, em vez de até três.\nO LLM local agora substitui a falha do LLM na nuvem; “Ask the local model first” restaura a ordem antiga. Ele pára após um tempo limite configurável, responde menos e deixa um núcleo de CPU livre\nQuando uma resposta é respondida, os satélites UDP também mantêm o microfone aberto (requer `@iobroker /assistant-satellite` 0.2.0)\n“Mais alto”, “silencioso”, “volume 5\" alteram o volume do satélite que o ouviu - ESPHome, IObroker e UDP\n“Como está o tempo amanhã?” é atendido pelo adaptador climático sem LLM\nCorrigido: perguntas russas com “será...” eram usadas para consultas de alarme",
|
|
89
|
-
"nl": "Eigen activeringswoorden voor ESPhome-satellieten: upload een MicroWakeWord-model in de satellietinstellingen, het apparaat downloadt het wanneer het wordt geactiveerd. Modellen die niet kunnen worden uitgevoerd op het apparaat (bijvoorbeeld OpenWakeWord) worden bij het uploaden geweigerd met de reden\nGesproken antwoorden komen sneller: de lijst met apparaten voor de offline regelengine wordt één keer samengesteld en gedurende één minuut gedeeld in plaats van maximaal drie keer per gesproken opdracht\nDe lokale LLM is nu de noodoplossing voor wanneer de LLM in de cloud faalt; „Vraag het eerst aan het lokale model” herstelt de oude volgorde. Het wordt gestopt na een instelbare time-out, het antwoord wordt korter en er blijft één CPU-kern vrij\nAls een antwoord terugvraagt, houden de UDP-satellieten nu ook de microfoon open voor het antwoord (hiervoor is `@iobroker /assistant-satellite` 0.2.0 nodig)\n„Luider”, „stiller”, „volume 5\" veranderen het volume van de satelliet die het heeft gehoord: ESPhome-, IOBroker-native en UDP-satellieten\n„Hoe is het weer morgen?” wordt beantwoord via de weeradapter zonder LLM\nOpgelost: Russische vragen met „...” („wordt het...”) werden beantwoord voor vragen over alarmen",
|
|
90
|
-
"fr": "Propre réveil pour les satellites EspHome : chargez un modèle MicroWakeword dans les paramètres du satellite, l'appareil le télécharge lorsqu'il est activé. Les modèles non compatibles (OpenWakeWord, par exemple) sont rejetés pour cette raison\nLes réponses vocales sont plus rapides : la liste des appareils du moteur de règles hors ligne est créée une fois et partagée pendant une minute au lieu de trois\nLe LLM local est désormais la solution en cas d'échec du LLM cloud ; « Ask the local model first » rétablit l'ancien ordre. Il s'arrête après un délai paramétrable, répond plus vite et laisse un cœur libre\nLorsqu'une réponse est demandée, les satellites UDP gardent le micro ouvert (needs `@iobroker /assistant-satellite` 0.2.0)\n« Plus fort », « plus doux », « volume 5 » : modifiez le volume du satellite qui l'a entendu : ESPHome, IOBroker et UDP\n« Quel temps fera-t-il demain ? » reçoit la réponse de l'adaptateur météo sans LLM\nCorrigé : Les questions russes « будет » (« est-ce que ce sera... ») ont été prises pour des raisons d'alarme",
|
|
91
|
-
"it": "Wake word personali per i satelliti ESPhome: carica un modello MicroWakeWord nelle impostazioni satellitari, lo scarica. I modelli non eseguibili (ad es. OpenWakeWord) vengono caricati e il motivo\nLe risposte vocali sono più veloci: l'elenco dei dispositivi offline è creato una volta e condiviso per un minuto anziché tre volte\nL'LLM locale è ora il rimedio per i fallimenti del cloud; «Ask the local model first» ripristina il vecchio ordine. Si ferma dopo un timeout configurabile, risponde meno e lascia libero un core CPU\nQuando una risposta risponde, i satelliti UDP ora tengono aperto anche il microfono (serve `@iobroker /assistant-satellite` 0.2.0)\n«Più forte», «più basso», «volume 5\" cambia il volume del satellite che lo ha sentito: ESPHome, IOBroker-native e UDP\n«Com'è il tempo domani?» risponde dal Weather Adapter senza un LLM\nRisolto: le domande russe con «удет» («sarà...») erano usate come allarmi",
|
|
92
|
-
"es": "Palabras propias para satélites ESPhome: suba un modelo MicroWakeWord en los ajustes del satélite y el dispositivo lo descarga cuando se activa. Los modelos que el dispositivo no puede ejecutar (por ejemplo, OpenWakeWord) se rechazan al subirlos\nLas respuestas de voz son más rápidas: la lista de dispositivos del motor de reglas offline se crea una vez y se comparte un minuto en lugar de tres veces por comando\nEl LLM local es ahora la alternativa para cuando el LLM en la nube fracasa; «Pregunte primero al modelo local» restaura el viejo orden. Se detiene tras un tiempo de espera configurable, responde menos y deja un núcleo de CPU libre\nCuando responde, los satélites UDP ahora mantienen el micrófono abierto para responder (necesita `@iobroker /assistant-satellite` 0.2.0)\n«Más alto», «más bajo», «volumen 5\" cambie el volumen del satélite que lo oyó: satélites ESPhome, nativos de IOBroker y UDP\n«¿Qué tiempo hace mañana?» se responde desde el adaptador meteorológico sin un LLM\nCorregido: las preguntas en ruso con «хедес» («será...») se respondían para consultas de alarma",
|
|
93
|
-
"pl": "Własne słowa budzące dla satelitów ESPhome: prześlij model MicroWakeWord w ustawieniach satelity, urządzenie pobiera go po aktywacji. Modele, których urządzenie nie może uruchomić (np. OpenWakeWord) są odrzucane po przesłaniu z powodu\nOdpowiedzi głosowe przychodzą szybciej: lista urządzeń dla silnika reguł offline jest tworzona raz i udostępniana przez minutę zamiast do trzech razy na polecenie mówione\nLokalna LLM jest teraz rezerwą, gdy LLM w chmurze ulegnie awarii; „Najpierw zapytaj model lokalny” przywraca stary porządek. Zostaje zatrzymany po konfigurowalnym limitu czasu, odpowiada krótsze i pozostawia jeden rdzeń procesora wolny\nGdy odpowiedź odpowie, satelity UDP utrzymują teraz mikrofon otwarty dla odpowiedzi (wymaga `@iobroker /asystent-satelita` 0.2.0)\n„Głośniejszy”, „cichszy”, „głośność 5” zmieniają głośność satelity, który go słyszał - satelity EspHome, ioBroker-native i UDP\n„Jaka jest pogoda jutro?” otrzymuje odpowiedź z adaptera pogodowego bez LLM\nNaprawiono: pytania rosyjskie z „будет” („czy będzie...”) były przyjmowane do zapytań alarmowych",
|
|
94
|
-
"uk": "Власні слова пробудження для супутників ESPHome: завантажте модель MicroWakeWord у налаштуваннях супутника, пристрій завантажує її, коли вона активована. Моделі, які пристрій не може запустити (наприклад, OpenWakeWord) відхиляються при завантаженні з причиною\nГолосові відповіді надходять швидше: список пристроїв для механізму автономних правил будується один раз і ділиться протягом хвилини, а не до трьох разів на вимовлену команду\nЛокальний LLM тепер є резервним варіантом, коли хмарний LLM не працює; «Спочатку запитати локальну модель» відновлює старий порядок. Він зупиняється після настроюваного тайм-ауту, відповідає коротше і залишає одне ядро процесора вільним\nКоли відповідь запитує, супутники UDP тепер також тримають мікрофон відкритим для відповіді (потрібен `@iobroker /assistant-satelit` 0.2.0)\n«Голосніше», «тихіше», «гучність 5\" змінюють гучність супутника, який його почув - супутники ESPHome, IObroker-native і UDP\n«Яка погода завтра?» відповідає від погодного адаптера без LLM\nВиправлено: російські питання з «будет» («чи буде...») були прийняті для тривожних запитів",
|
|
95
|
-
"zh-cn": "Esphome 卫星的自带唤醒词:在卫星设置中上传一个 MicroWakeWord 模型,设备在激活时下载该模型。设备无法运行的模型(例如 OpenWakeWord)在上传时被拒绝,原因是\n语音答案来得更快:离线规则引擎的设备列表生成一次并共享一分钟,而不是每个口头命令最多共享三次\n本地 LLM 现在是云 LLM 失败时的备用方法;“先询问本地模型” 恢复了旧的顺序。它在可配置的超时后停止,响应时间缩短并留出一个 CPU 内核空闲时间\n当答案要求回复时,UDP 卫星现在也会打开麦克风以进行回复(需要 `@iobroker /assistant-satellite` 0.2.0)\n“更响亮”、“更安静”、“音量 5” 改变听到声音的卫星的音量——ESPhome、IOBroker-Native 和 UDP 卫星\n“明天的天气怎么样?”在没有 LLM 的情况下由天气适配器回答\n已修复:警报查询时使用了带有 “bád耔(“会不会是...”)的俄语问题"
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|