iobroker.assistant 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/admin/assistant.svg +19 -0
  4. package/admin/custom/.vite/manifest.json +169 -0
  5. package/admin/custom/assets/Components-DcnuVFM6.js +1 -0
  6. package/admin/custom/assets/_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-DogEjv3U.js +1 -0
  7. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-Dw5TkU1t.js +1 -0
  8. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-BEFjTs9S.js +1 -0
  9. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-C4r3udVz.js +1243 -0
  10. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__1__loadShare___mf_0_iobroker_mf_1_json_mf_2_config__loadShare__.js-DlOBHPL0.js +1494 -0
  11. package/admin/custom/assets/hostInit-DKMI-sLp.js +1 -0
  12. package/admin/custom/assets/index-B8wW0kQx.js +1 -0
  13. package/admin/custom/assets/lib-DpE8sc4h.js +3 -0
  14. package/admin/custom/assets/mf-entry-bootstrap-0-e7fa582d.js +36 -0
  15. package/admin/custom/assets/rolldown-runtime-C0FnF6B9.js +1 -0
  16. package/admin/custom/assets/virtualExposes-DOTO22UK.js +1 -0
  17. package/admin/custom/assets/virtual_mf-REMOTE_ENTRY_ID___mfe_internal__ConfigCustomAssistant__customComponents_js-CQa59yVi.js +1 -0
  18. package/admin/custom/assets/virtual_mf-exposes-ssr___mfe_internal__ConfigCustomAssistant__customComponents_js-9g2Fj8tk.js +2 -0
  19. package/admin/custom/assets/virtual_mf-exposes___mfe_internal__ConfigCustomAssistant__customComponents_js-BTUQTbYo.js +2 -0
  20. package/admin/custom/assets/vite-preload-helper-B7qeedMF.js +1 -0
  21. package/admin/custom/customComponents.js +1 -0
  22. package/admin/custom/customComponents.ssr.js +1 -0
  23. package/admin/custom/i18n/de.json +58 -0
  24. package/admin/custom/i18n/en.json +58 -0
  25. package/admin/custom/i18n/es.json +34 -0
  26. package/admin/custom/i18n/fr.json +34 -0
  27. package/admin/custom/i18n/it.json +34 -0
  28. package/admin/custom/i18n/nl.json +34 -0
  29. package/admin/custom/i18n/pl.json +34 -0
  30. package/admin/custom/i18n/pt.json +34 -0
  31. package/admin/custom/i18n/ru.json +34 -0
  32. package/admin/custom/i18n/uk.json +34 -0
  33. package/admin/custom/i18n/zh-cn.json +34 -0
  34. package/admin/custom/index.html +32 -0
  35. package/admin/custom/mf-manifest.json +1 -0
  36. package/admin/custom/mf-stats.json +1 -0
  37. package/admin/i18n/de.json +130 -0
  38. package/admin/i18n/en.json +130 -0
  39. package/admin/i18n/es.json +106 -0
  40. package/admin/i18n/fr.json +106 -0
  41. package/admin/i18n/it.json +106 -0
  42. package/admin/i18n/nl.json +106 -0
  43. package/admin/i18n/pl.json +106 -0
  44. package/admin/i18n/pt.json +106 -0
  45. package/admin/i18n/ru.json +106 -0
  46. package/admin/i18n/uk.json +106 -0
  47. package/admin/i18n/zh-cn.json +106 -0
  48. package/admin/jsonConfig.json +743 -0
  49. package/admin/sounds/alarm.wav +0 -0
  50. package/admin/sounds/timer.wav +0 -0
  51. package/build/lib/alarms.js +281 -0
  52. package/build/lib/alarms.js.map +1 -0
  53. package/build/lib/context.js +52 -0
  54. package/build/lib/context.js.map +1 -0
  55. package/build/lib/credentials.js +121 -0
  56. package/build/lib/credentials.js.map +1 -0
  57. package/build/lib/llm.js +303 -0
  58. package/build/lib/llm.js.map +1 -0
  59. package/build/lib/localLlm.js +221 -0
  60. package/build/lib/localLlm.js.map +1 -0
  61. package/build/lib/memory.js +190 -0
  62. package/build/lib/memory.js.map +1 -0
  63. package/build/lib/nlu.js +929 -0
  64. package/build/lib/nlu.js.map +1 -0
  65. package/build/lib/timers.js +245 -0
  66. package/build/lib/timers.js.map +1 -0
  67. package/build/lib/tools.js +299 -0
  68. package/build/lib/tools.js.map +1 -0
  69. package/build/lib/voice/aws.js +102 -0
  70. package/build/lib/voice/aws.js.map +1 -0
  71. package/build/lib/voice/azure.js +144 -0
  72. package/build/lib/voice/azure.js.map +1 -0
  73. package/build/lib/voice/download.js +76 -0
  74. package/build/lib/voice/download.js.map +1 -0
  75. package/build/lib/voice/engines.js +89 -0
  76. package/build/lib/voice/engines.js.map +1 -0
  77. package/build/lib/voice/lang.js +33 -0
  78. package/build/lib/voice/lang.js.map +1 -0
  79. package/build/lib/voice/piper.js +191 -0
  80. package/build/lib/voice/piper.js.map +1 -0
  81. package/build/lib/voice/protocol.js +35 -0
  82. package/build/lib/voice/protocol.js.map +1 -0
  83. package/build/lib/voice/stt.js +116 -0
  84. package/build/lib/voice/stt.js.map +1 -0
  85. package/build/lib/voice/tts.js +36 -0
  86. package/build/lib/voice/tts.js.map +1 -0
  87. package/build/lib/voice/voiceServer.js +249 -0
  88. package/build/lib/voice/voiceServer.js.map +1 -0
  89. package/build/lib/voice/vosk.js +260 -0
  90. package/build/lib/voice/vosk.js.map +1 -0
  91. package/build/lib/voice/wyoming.js +320 -0
  92. package/build/lib/voice/wyoming.js.map +1 -0
  93. package/build/lib/weather.js +423 -0
  94. package/build/lib/weather.js.map +1 -0
  95. package/build/main.js +3152 -0
  96. package/build/main.js.map +1 -0
  97. package/io-package.json +528 -0
  98. package/package.json +68 -0
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.downloadFile = downloadFile;
37
+ exports.extractArchive = extractArchive;
38
+ /**
39
+ * Small download + extract helpers for the local engines (Piper binary/voices, Vosk models).
40
+ * Files land in the writable instance data dir so they survive adapter upgrades.
41
+ */
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ const node_child_process_1 = require("node:child_process");
45
+ /** Download a file to `dest` (skips if it already exists). */
46
+ async function downloadFile(url, dest, log) {
47
+ if (fs.existsSync(dest)) {
48
+ return;
49
+ }
50
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
51
+ log.info(`Downloading ${path.basename(dest)} …`);
52
+ const res = await fetch(url);
53
+ if (!res.ok) {
54
+ throw new Error(`download ${url} failed: HTTP ${res.status}`);
55
+ }
56
+ const buf = Buffer.from(await res.arrayBuffer());
57
+ // Write to a temp file first so an interrupted download does not leave a truncated "valid" file.
58
+ const tmp = `${dest}.part`;
59
+ fs.writeFileSync(tmp, buf);
60
+ fs.renameSync(tmp, dest);
61
+ log.info(`Saved ${path.basename(dest)} (${(buf.length / 1e6).toFixed(1)} MB).`);
62
+ }
63
+ /** Extract a `.tar.gz`/`.tgz` (via `tar`) or `.zip` (via `unzip`) into `destDir`. */
64
+ function extractArchive(archive, destDir, log) {
65
+ fs.mkdirSync(destDir, { recursive: true });
66
+ const isZip = archive.endsWith('.zip');
67
+ const cmd = isZip ? 'unzip' : 'tar';
68
+ const args = isZip ? ['-o', archive, '-d', destDir] : ['-xzf', archive, '-C', destDir];
69
+ return new Promise((resolve, reject) => {
70
+ const child = (0, node_child_process_1.spawn)(cmd, args);
71
+ child.stderr?.on('data', d => log.debug(`${cmd}: ${String(d).trim()}`));
72
+ child.on('error', e => reject(new Error(`${cmd} failed: ${e.message} — is '${cmd}' installed?`)));
73
+ child.on('close', code => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${code}`))));
74
+ });
75
+ }
76
+ //# sourceMappingURL=download.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.js","sourceRoot":"","sources":["../../../src/lib/voice/download.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Small download + extract helpers for the local engines (Piper binary/voices, Vosk models).\n * Files land in the writable instance data dir so they survive adapter upgrades.\n */\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { spawn } from 'node:child_process';\n\nexport type VoiceLogger = Pick<ioBroker.Logger, 'info' | 'warn' | 'debug' | 'error'>;\n\n/** Download a file to `dest` (skips if it already exists). */\nexport async function downloadFile(url: string, dest: string, log: VoiceLogger): Promise<void> {\n if (fs.existsSync(dest)) {\n return;\n }\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n log.info(`Downloading ${path.basename(dest)} …`);\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`download ${url} failed: HTTP ${res.status}`);\n }\n const buf = Buffer.from(await res.arrayBuffer());\n // Write to a temp file first so an interrupted download does not leave a truncated \"valid\" file.\n const tmp = `${dest}.part`;\n fs.writeFileSync(tmp, buf);\n fs.renameSync(tmp, dest);\n log.info(`Saved ${path.basename(dest)} (${(buf.length / 1e6).toFixed(1)} MB).`);\n}\n\n/** Extract a `.tar.gz`/`.tgz` (via `tar`) or `.zip` (via `unzip`) into `destDir`. */\nexport function extractArchive(archive: string, destDir: string, log: VoiceLogger): Promise<void> {\n fs.mkdirSync(destDir, { recursive: true });\n const isZip = archive.endsWith('.zip');\n const cmd = isZip ? 'unzip' : 'tar';\n const args = isZip ? ['-o', archive, '-d', destDir] : ['-xzf', archive, '-C', destDir];\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args);\n child.stderr?.on('data', d => log.debug(`${cmd}: ${String(d).trim()}`));\n child.on('error', e => reject(new Error(`${cmd} failed: ${e.message} — is '${cmd}' installed?`)));\n child.on('close', code => (code === 0 ? resolve() : reject(new Error(`${cmd} exited with code ${code}`))));\n });\n}\n"]}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSttEngine = createSttEngine;
4
+ exports.createTtsEngine = createTtsEngine;
5
+ exports.listVoices = listVoices;
6
+ exports.listSttModels = listSttModels;
7
+ /**
8
+ * Factory that builds the configured STT/TTS engine. Cloud engines (OpenAI/Azure/AWS) use resolved
9
+ * credentials; local engines (Vosk/Piper) download their models on demand into the data dir.
10
+ * Adding a provider = one new engine class + one `case` here.
11
+ */
12
+ const stt_1 = require("./stt");
13
+ const tts_1 = require("./tts");
14
+ const azure_1 = require("./azure");
15
+ const aws_1 = require("./aws");
16
+ const vosk_1 = require("./vosk");
17
+ const piper_1 = require("./piper");
18
+ const lang_1 = require("./lang");
19
+ function requireAzure(creds) {
20
+ if (!creds.azureKey || !creds.azureRegion) {
21
+ throw new Error('Azure speech needs a subscription key and region');
22
+ }
23
+ }
24
+ function requireAws(creds) {
25
+ if (!creds.aws.accessKeyId || !creds.aws.secretAccessKey || !creds.aws.region) {
26
+ throw new Error('AWS speech needs an access key id, secret access key and region');
27
+ }
28
+ }
29
+ function requireOpenAi(creds) {
30
+ if (!creds.openaiKey) {
31
+ throw new Error('OpenAI speech needs an API key');
32
+ }
33
+ }
34
+ function createSttEngine(provider, ctx) {
35
+ switch (provider) {
36
+ case 'azure':
37
+ requireAzure(ctx.creds);
38
+ return new azure_1.AzureStt(ctx.creds.azureKey, ctx.creds.azureRegion);
39
+ case 'aws':
40
+ requireAws(ctx.creds);
41
+ return new aws_1.AwsStt(ctx.creds.aws);
42
+ case 'vosk':
43
+ return new vosk_1.VoskStt(ctx.dataDir, ctx.voskModel, ctx.log);
44
+ case 'piper':
45
+ throw new Error('Piper is a text-to-speech engine, not speech-to-text');
46
+ default:
47
+ requireOpenAi(ctx.creds);
48
+ return new stt_1.OpenAiStt(ctx.creds.openaiKey, ctx.sttModel || undefined);
49
+ }
50
+ }
51
+ function createTtsEngine(provider, ctx) {
52
+ switch (provider) {
53
+ case 'azure':
54
+ requireAzure(ctx.creds);
55
+ return new azure_1.AzureTts(ctx.creds.azureKey, ctx.creds.azureRegion, ctx.voices.azure);
56
+ case 'aws':
57
+ requireAws(ctx.creds);
58
+ return new aws_1.AwsTts(ctx.creds.aws, ctx.voices.aws);
59
+ case 'piper':
60
+ return new piper_1.PiperTts(ctx.dataDir, ctx.voices.piper, ctx.log);
61
+ case 'vosk':
62
+ throw new Error('Vosk is a speech-to-text engine, not text-to-speech');
63
+ default:
64
+ requireOpenAi(ctx.creds);
65
+ return new tts_1.OpenAiTts(ctx.creds.openaiKey, (ctx.voices.openai || 'alloy'), ctx.ttsModel || undefined);
66
+ }
67
+ }
68
+ /** OpenAI TTS voices are a fixed set (no list API). */
69
+ const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
70
+ /** List the available TTS voices for a provider (for the settings voice dropdown). */
71
+ async function listVoices(provider, ctx, language) {
72
+ switch (provider) {
73
+ case 'azure':
74
+ requireAzure(ctx.creds);
75
+ return (0, azure_1.listAzureVoices)(ctx.creds.azureKey, ctx.creds.azureRegion, (0, lang_1.isoToLocale)(language));
76
+ case 'aws':
77
+ requireAws(ctx.creds);
78
+ return (0, aws_1.listPollyVoices)(ctx.creds.aws, (0, lang_1.isoToLocale)(language));
79
+ case 'piper':
80
+ return (0, piper_1.listPiperVoices)();
81
+ default:
82
+ return OPENAI_VOICES;
83
+ }
84
+ }
85
+ /** Suggested STT model names for the settings dropdown. Only Vosk has a local model catalogue. */
86
+ function listSttModels(provider, language) {
87
+ return provider === 'vosk' ? (0, vosk_1.listVoskModels)(language) : [];
88
+ }
89
+ //# sourceMappingURL=engines.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engines.js","sourceRoot":"","sources":["../../../src/lib/voice/engines.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Factory that builds the configured STT/TTS engine. Cloud engines (OpenAI/Azure/AWS) use resolved\n * credentials; local engines (Vosk/Piper) download their models on demand into the data dir.\n * Adding a provider = one new engine class + one `case` here.\n */\nimport { OpenAiStt, type SttEngine } from './stt';\nimport { OpenAiTts, type OpenAiVoice, type TtsEngine } from './tts';\nimport { AzureStt, AzureTts, listAzureVoices } from './azure';\nimport { AwsStt, AwsTts, listPollyVoices, type AwsCreds } from './aws';\nimport { VoskStt, listVoskModels } from './vosk';\nimport { PiperTts, listPiperVoices } from './piper';\nimport { isoToLocale } from './lang';\nimport type { VoiceLogger } from './download';\n\nexport type SpeechProvider = 'openai' | 'azure' | 'aws' | 'vosk' | 'piper';\n\n/** All speech credentials, resolved from config (or the central credential store). */\nexport interface VoiceCredentials {\n openaiKey: string;\n azureKey: string;\n azureRegion: string;\n aws: AwsCreds;\n}\n\n/** Provider-specific voice ids (each provider names voices differently). */\nexport interface VoiceNames {\n openai: string; // alloy/echo/…\n azure: string; // e.g. de-DE-KatjaNeural\n aws: string; // e.g. Vicki\n piper: string; // e.g. de_DE-thorsten-medium\n}\n\n/** Everything the factory needs to build any engine. */\nexport interface EngineContext {\n creds: VoiceCredentials;\n voices: VoiceNames;\n /** Writable instance data dir (for the local engines' downloads). */\n dataDir: string;\n log: VoiceLogger;\n /** Local model overrides ('' → default per language). */\n voskModel: string;\n /** OpenAI STT model ('' → 'whisper-1'; e.g. 'gpt-4o-transcribe'). */\n sttModel: string;\n /** OpenAI TTS model ('' → 'tts-1'; e.g. 'gpt-4o-mini-tts'). */\n ttsModel: string;\n}\n\nfunction requireAzure(creds: VoiceCredentials): void {\n if (!creds.azureKey || !creds.azureRegion) {\n throw new Error('Azure speech needs a subscription key and region');\n }\n}\n\nfunction requireAws(creds: VoiceCredentials): void {\n if (!creds.aws.accessKeyId || !creds.aws.secretAccessKey || !creds.aws.region) {\n throw new Error('AWS speech needs an access key id, secret access key and region');\n }\n}\n\nfunction requireOpenAi(creds: VoiceCredentials): void {\n if (!creds.openaiKey) {\n throw new Error('OpenAI speech needs an API key');\n }\n}\n\nexport function createSttEngine(provider: SpeechProvider, ctx: EngineContext): SttEngine {\n switch (provider) {\n case 'azure':\n requireAzure(ctx.creds);\n return new AzureStt(ctx.creds.azureKey, ctx.creds.azureRegion);\n case 'aws':\n requireAws(ctx.creds);\n return new AwsStt(ctx.creds.aws);\n case 'vosk':\n return new VoskStt(ctx.dataDir, ctx.voskModel, ctx.log);\n case 'piper':\n throw new Error('Piper is a text-to-speech engine, not speech-to-text');\n default:\n requireOpenAi(ctx.creds);\n return new OpenAiStt(ctx.creds.openaiKey, ctx.sttModel || undefined);\n }\n}\n\nexport function createTtsEngine(provider: SpeechProvider, ctx: EngineContext): TtsEngine {\n switch (provider) {\n case 'azure':\n requireAzure(ctx.creds);\n return new AzureTts(ctx.creds.azureKey, ctx.creds.azureRegion, ctx.voices.azure);\n case 'aws':\n requireAws(ctx.creds);\n return new AwsTts(ctx.creds.aws, ctx.voices.aws);\n case 'piper':\n return new PiperTts(ctx.dataDir, ctx.voices.piper, ctx.log);\n case 'vosk':\n throw new Error('Vosk is a speech-to-text engine, not text-to-speech');\n default:\n requireOpenAi(ctx.creds);\n return new OpenAiTts(\n ctx.creds.openaiKey,\n (ctx.voices.openai || 'alloy') as OpenAiVoice,\n ctx.ttsModel || undefined,\n );\n }\n}\n\n/** OpenAI TTS voices are a fixed set (no list API). */\nconst OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];\n\n/** List the available TTS voices for a provider (for the settings voice dropdown). */\nexport async function listVoices(provider: SpeechProvider, ctx: EngineContext, language: string): Promise<string[]> {\n switch (provider) {\n case 'azure':\n requireAzure(ctx.creds);\n return listAzureVoices(ctx.creds.azureKey, ctx.creds.azureRegion, isoToLocale(language));\n case 'aws':\n requireAws(ctx.creds);\n return listPollyVoices(ctx.creds.aws, isoToLocale(language));\n case 'piper':\n return listPiperVoices();\n default:\n return OPENAI_VOICES;\n }\n}\n\n/** Suggested STT model names for the settings dropdown. Only Vosk has a local model catalogue. */\nexport function listSttModels(provider: SpeechProvider, language: string): string[] {\n return provider === 'vosk' ? listVoskModels(language) : [];\n}\n"]}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isoToLocale = isoToLocale;
4
+ /**
5
+ * Map an ISO-639-1 language code (or an already-qualified locale) to a BCP-47 locale,
6
+ * as required by Azure Speech and AWS (Polly/Transcribe). OpenAI/Whisper uses the bare
7
+ * ISO code, so those engines strip the region again.
8
+ */
9
+ const LOCALES = {
10
+ de: 'de-DE',
11
+ en: 'en-US',
12
+ ru: 'ru-RU',
13
+ fr: 'fr-FR',
14
+ it: 'it-IT',
15
+ es: 'es-ES',
16
+ nl: 'nl-NL',
17
+ pl: 'pl-PL',
18
+ pt: 'pt-PT',
19
+ uk: 'uk-UA',
20
+ zh: 'zh-CN',
21
+ };
22
+ function isoToLocale(lang, fallback = 'en-US') {
23
+ if (!lang) {
24
+ return fallback;
25
+ }
26
+ if (lang.includes('-')) {
27
+ // Already a locale (e.g. 'zh-cn') — normalise casing to 'zh-CN'.
28
+ const [l, r] = lang.split('-');
29
+ return `${l.toLowerCase()}-${r.toUpperCase()}`;
30
+ }
31
+ return LOCALES[lang.toLowerCase()] || fallback;
32
+ }
33
+ //# sourceMappingURL=lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lang.js","sourceRoot":"","sources":["../../../src/lib/voice/lang.ts"],"names":[],"mappings":";;AAmBA,kCAUC;AA7BD;;;;GAIG;AACH,MAAM,OAAO,GAA2B;IACpC,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;IACX,EAAE,EAAE,OAAO;CACd,CAAC;AAEF,SAAgB,WAAW,CAAC,IAAY,EAAE,QAAQ,GAAG,OAAO;IACxD,IAAI,CAAC,IAAI,EAAE,CAAC;QACR,OAAO,QAAQ,CAAC;IACpB,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,iEAAiE;QACjE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,GAAG,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;IACnD,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,QAAQ,CAAC;AACnD,CAAC","sourcesContent":["/**\n * Map an ISO-639-1 language code (or an already-qualified locale) to a BCP-47 locale,\n * as required by Azure Speech and AWS (Polly/Transcribe). OpenAI/Whisper uses the bare\n * ISO code, so those engines strip the region again.\n */\nconst LOCALES: Record<string, string> = {\n de: 'de-DE',\n en: 'en-US',\n ru: 'ru-RU',\n fr: 'fr-FR',\n it: 'it-IT',\n es: 'es-ES',\n nl: 'nl-NL',\n pl: 'pl-PL',\n pt: 'pt-PT',\n uk: 'uk-UA',\n zh: 'zh-CN',\n};\n\nexport function isoToLocale(lang: string, fallback = 'en-US'): string {\n if (!lang) {\n return fallback;\n }\n if (lang.includes('-')) {\n // Already a locale (e.g. 'zh-cn') — normalise casing to 'zh-CN'.\n const [l, r] = lang.split('-');\n return `${l.toLowerCase()}-${r.toUpperCase()}`;\n }\n return LOCALES[lang.toLowerCase()] || fallback;\n}\n"]}
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PiperTts = void 0;
37
+ exports.listPiperVoices = listPiperVoices;
38
+ /**
39
+ * Local TTS via Piper (https://github.com/rhasspy/piper). The binary + a voice model are downloaded
40
+ * into the instance data dir on first use, then `piper --output_raw` is spawned to synthesise 16-bit
41
+ * PCM. No cloud, no API key.
42
+ *
43
+ * ⚠️ Binary/voice download + spawn are platform-specific — validate on the target device.
44
+ */
45
+ const fs = __importStar(require("node:fs"));
46
+ const path = __importStar(require("node:path"));
47
+ const node_child_process_1 = require("node:child_process");
48
+ const download_1 = require("./download");
49
+ const PIPER_RELEASE = 'https://github.com/rhasspy/piper/releases/download/2023.11.14-2';
50
+ /** Release asset for the current platform/arch. */
51
+ function piperAsset() {
52
+ const p = process.platform;
53
+ const a = process.arch;
54
+ if (p === 'linux') {
55
+ if (a === 'arm64') {
56
+ return 'piper_linux_aarch64.tar.gz';
57
+ }
58
+ if (a === 'arm') {
59
+ return 'piper_linux_armv7l.tar.gz';
60
+ }
61
+ return 'piper_linux_x86_64.tar.gz';
62
+ }
63
+ if (p === 'darwin') {
64
+ return a === 'arm64' ? 'piper_macos_aarch64.tar.gz' : 'piper_macos_x64.tar.gz';
65
+ }
66
+ if (p === 'win32') {
67
+ return 'piper_windows_amd64.zip';
68
+ }
69
+ throw new Error(`Piper: unsupported platform ${p}/${a}`);
70
+ }
71
+ /** Reasonable default Piper voice per language (rhasspy/piper-voices). */
72
+ const DEFAULT_VOICE = {
73
+ de: 'de_DE-thorsten-medium',
74
+ en: 'en_US-lessac-medium',
75
+ ru: 'ru_RU-irina-medium',
76
+ fr: 'fr_FR-siwis-medium',
77
+ it: 'it_IT-riccardo-x_low',
78
+ es: 'es_ES-sharvard-medium',
79
+ nl: 'nl_NL-mls-medium',
80
+ pl: 'pl_PL-darkman-medium',
81
+ pt: 'pt_PT-tugão-medium',
82
+ uk: 'uk_UA-ukrainian_tts-medium',
83
+ zh: 'zh_CN-huayan-medium',
84
+ };
85
+ /** Build the two voice-file URLs (.onnx + .onnx.json) from a voice name like `de_DE-thorsten-medium`. */
86
+ function voiceUrls(voice) {
87
+ // name = <locale>-<speaker>-<quality>; path = <lang>/<locale>/<speaker>/<quality>/<name>.onnx
88
+ const [locale, speaker, quality] = voice.split('-');
89
+ const lang = locale.split('_')[0].toLowerCase();
90
+ const base = `https://huggingface.co/rhasspy/piper-voices/resolve/main/${lang}/${locale}/${speaker}/${quality}/${voice}`;
91
+ return { onnx: `${base}.onnx`, json: `${base}.onnx.json` };
92
+ }
93
+ class PiperTts {
94
+ dataDir;
95
+ voice;
96
+ log;
97
+ binaryPath = '';
98
+ ready = null;
99
+ constructor(dataDir,
100
+ /** Voice name (e.g. de_DE-thorsten-medium); '' → default per language. */
101
+ voice, log) {
102
+ this.dataDir = dataDir;
103
+ this.voice = voice;
104
+ this.log = log;
105
+ }
106
+ /** Download + unpack the binary once; concurrent calls share one init. */
107
+ ensure() {
108
+ if (!this.ready) {
109
+ this.ready = this.doEnsure().catch(e => {
110
+ this.ready = null; // allow a retry on the next call
111
+ throw e;
112
+ });
113
+ }
114
+ return this.ready;
115
+ }
116
+ async doEnsure() {
117
+ // 1. binary
118
+ const piperDir = path.join(this.dataDir, 'piper');
119
+ const exe = path.join(piperDir, 'piper', process.platform === 'win32' ? 'piper.exe' : 'piper');
120
+ if (!fs.existsSync(exe)) {
121
+ const asset = piperAsset();
122
+ const archive = path.join(piperDir, asset);
123
+ await (0, download_1.downloadFile)(`${PIPER_RELEASE}/${asset}`, archive, this.log);
124
+ await (0, download_1.extractArchive)(archive, piperDir, this.log);
125
+ if (process.platform !== 'win32') {
126
+ try {
127
+ fs.chmodSync(exe, 0o755);
128
+ }
129
+ catch {
130
+ /* best effort */
131
+ }
132
+ }
133
+ }
134
+ this.binaryPath = exe;
135
+ }
136
+ voiceFor(lang) {
137
+ const iso = (lang || 'en').split('-')[0].toLowerCase();
138
+ return this.voice || DEFAULT_VOICE[iso] || DEFAULT_VOICE.en;
139
+ }
140
+ async ensureVoice(voice) {
141
+ const dir = path.join(this.dataDir, 'piper-voices');
142
+ const onnx = path.join(dir, `${voice}.onnx`);
143
+ const json = path.join(dir, `${voice}.onnx.json`);
144
+ const urls = voiceUrls(voice);
145
+ await (0, download_1.downloadFile)(urls.onnx, onnx, this.log);
146
+ await (0, download_1.downloadFile)(urls.json, json, this.log);
147
+ let sampleRate = 22050;
148
+ try {
149
+ const cfg = JSON.parse(fs.readFileSync(json, 'utf8'));
150
+ sampleRate = cfg.audio?.sample_rate || 22050;
151
+ }
152
+ catch {
153
+ /* keep default */
154
+ }
155
+ return { onnx, sampleRate };
156
+ }
157
+ /** Warm-up: download the Piper binary + the voice for `lang` so the first command isn't slow. */
158
+ async prepare(lang) {
159
+ await this.ensure();
160
+ await this.ensureVoice(this.voiceFor(lang));
161
+ }
162
+ async synthesize(text, lang) {
163
+ await this.ensure();
164
+ const voice = this.voiceFor(lang);
165
+ const { onnx, sampleRate } = await this.ensureVoice(voice);
166
+ return new Promise((resolve, reject) => {
167
+ const child = (0, node_child_process_1.spawn)(this.binaryPath, ['--model', onnx, '--output_raw'], {
168
+ cwd: path.dirname(this.binaryPath), // finds bundled espeak-ng-data / libs
169
+ });
170
+ const chunks = [];
171
+ child.stdout.on('data', (d) => chunks.push(d));
172
+ child.stderr.on('data', (d) => this.log.debug(`piper: ${String(d).trim()}`));
173
+ child.on('error', e => reject(new Error(`piper failed: ${e.message}`)));
174
+ child.on('close', code => {
175
+ if (code === 0) {
176
+ resolve({ pcm: Buffer.concat(chunks), sampleRate });
177
+ }
178
+ else {
179
+ reject(new Error(`piper exited with code ${code}`));
180
+ }
181
+ });
182
+ child.stdin.end(text);
183
+ });
184
+ }
185
+ }
186
+ exports.PiperTts = PiperTts;
187
+ /** Voices offered in the settings dropdown for Piper (the built-in per-language defaults). */
188
+ function listPiperVoices() {
189
+ return Object.values(DEFAULT_VOICE);
190
+ }
191
+ //# sourceMappingURL=piper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piper.js","sourceRoot":"","sources":["../../../src/lib/voice/piper.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Local TTS via Piper (https://github.com/rhasspy/piper). The binary + a voice model are downloaded\n * into the instance data dir on first use, then `piper --output_raw` is spawned to synthesise 16-bit\n * PCM. No cloud, no API key.\n *\n * ⚠️ Binary/voice download + spawn are platform-specific — validate on the target device.\n */\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { spawn } from 'node:child_process';\nimport { downloadFile, extractArchive, type VoiceLogger } from './download';\nimport type { TtsEngine, TtsResult } from './tts';\n\nconst PIPER_RELEASE = 'https://github.com/rhasspy/piper/releases/download/2023.11.14-2';\n\n/** Release asset for the current platform/arch. */\nfunction piperAsset(): string {\n const p = process.platform;\n const a = process.arch;\n if (p === 'linux') {\n if (a === 'arm64') {\n return 'piper_linux_aarch64.tar.gz';\n }\n if (a === 'arm') {\n return 'piper_linux_armv7l.tar.gz';\n }\n return 'piper_linux_x86_64.tar.gz';\n }\n if (p === 'darwin') {\n return a === 'arm64' ? 'piper_macos_aarch64.tar.gz' : 'piper_macos_x64.tar.gz';\n }\n if (p === 'win32') {\n return 'piper_windows_amd64.zip';\n }\n throw new Error(`Piper: unsupported platform ${p}/${a}`);\n}\n\n/** Reasonable default Piper voice per language (rhasspy/piper-voices). */\nconst DEFAULT_VOICE: Record<string, string> = {\n de: 'de_DE-thorsten-medium',\n en: 'en_US-lessac-medium',\n ru: 'ru_RU-irina-medium',\n fr: 'fr_FR-siwis-medium',\n it: 'it_IT-riccardo-x_low',\n es: 'es_ES-sharvard-medium',\n nl: 'nl_NL-mls-medium',\n pl: 'pl_PL-darkman-medium',\n pt: 'pt_PT-tugão-medium',\n uk: 'uk_UA-ukrainian_tts-medium',\n zh: 'zh_CN-huayan-medium',\n};\n\n/** Build the two voice-file URLs (.onnx + .onnx.json) from a voice name like `de_DE-thorsten-medium`. */\nfunction voiceUrls(voice: string): { onnx: string; json: string } {\n // name = <locale>-<speaker>-<quality>; path = <lang>/<locale>/<speaker>/<quality>/<name>.onnx\n const [locale, speaker, quality] = voice.split('-');\n const lang = locale.split('_')[0].toLowerCase();\n const base = `https://huggingface.co/rhasspy/piper-voices/resolve/main/${lang}/${locale}/${speaker}/${quality}/${voice}`;\n return { onnx: `${base}.onnx`, json: `${base}.onnx.json` };\n}\n\nexport class PiperTts implements TtsEngine {\n private binaryPath = '';\n private ready: Promise<void> | null = null;\n\n constructor(\n private readonly dataDir: string,\n /** Voice name (e.g. de_DE-thorsten-medium); '' → default per language. */\n private readonly voice: string,\n private readonly log: VoiceLogger,\n ) {}\n\n /** Download + unpack the binary once; concurrent calls share one init. */\n private ensure(): Promise<void> {\n if (!this.ready) {\n this.ready = this.doEnsure().catch(e => {\n this.ready = null; // allow a retry on the next call\n throw e;\n });\n }\n return this.ready;\n }\n\n private async doEnsure(): Promise<void> {\n // 1. binary\n const piperDir = path.join(this.dataDir, 'piper');\n const exe = path.join(piperDir, 'piper', process.platform === 'win32' ? 'piper.exe' : 'piper');\n if (!fs.existsSync(exe)) {\n const asset = piperAsset();\n const archive = path.join(piperDir, asset);\n await downloadFile(`${PIPER_RELEASE}/${asset}`, archive, this.log);\n await extractArchive(archive, piperDir, this.log);\n if (process.platform !== 'win32') {\n try {\n fs.chmodSync(exe, 0o755);\n } catch {\n /* best effort */\n }\n }\n }\n this.binaryPath = exe;\n }\n\n private voiceFor(lang: string): string {\n const iso = (lang || 'en').split('-')[0].toLowerCase();\n return this.voice || DEFAULT_VOICE[iso] || DEFAULT_VOICE.en;\n }\n\n private async ensureVoice(voice: string): Promise<{ onnx: string; sampleRate: number }> {\n const dir = path.join(this.dataDir, 'piper-voices');\n const onnx = path.join(dir, `${voice}.onnx`);\n const json = path.join(dir, `${voice}.onnx.json`);\n const urls = voiceUrls(voice);\n await downloadFile(urls.onnx, onnx, this.log);\n await downloadFile(urls.json, json, this.log);\n let sampleRate = 22050;\n try {\n const cfg = JSON.parse(fs.readFileSync(json, 'utf8')) as { audio?: { sample_rate?: number } };\n sampleRate = cfg.audio?.sample_rate || 22050;\n } catch {\n /* keep default */\n }\n return { onnx, sampleRate };\n }\n\n /** Warm-up: download the Piper binary + the voice for `lang` so the first command isn't slow. */\n async prepare(lang: string): Promise<void> {\n await this.ensure();\n await this.ensureVoice(this.voiceFor(lang));\n }\n\n async synthesize(text: string, lang: string): Promise<TtsResult> {\n await this.ensure();\n const voice = this.voiceFor(lang);\n const { onnx, sampleRate } = await this.ensureVoice(voice);\n\n return new Promise<TtsResult>((resolve, reject) => {\n const child = spawn(this.binaryPath, ['--model', onnx, '--output_raw'], {\n cwd: path.dirname(this.binaryPath), // finds bundled espeak-ng-data / libs\n });\n const chunks: Buffer[] = [];\n child.stdout.on('data', (d: Buffer) => chunks.push(d));\n child.stderr.on('data', (d: Buffer) => this.log.debug(`piper: ${String(d).trim()}`));\n child.on('error', e => reject(new Error(`piper failed: ${e.message}`)));\n child.on('close', code => {\n if (code === 0) {\n resolve({ pcm: Buffer.concat(chunks), sampleRate });\n } else {\n reject(new Error(`piper exited with code ${code}`));\n }\n });\n child.stdin.end(text);\n });\n }\n}\n\n/** Voices offered in the settings dropdown for Piper (the built-in per-language defaults). */\nexport function listPiperVoices(): string[] {\n return Object.values(DEFAULT_VOICE);\n}\n"]}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * Voice-satellite UDP wire protocol — compatible with the Hannah satellite
4
+ * (`C:\iot\Hannah\satellite-pi\satellite.py`) so existing ESP/Python satellites keep working.
5
+ *
6
+ * One UDP socket, first byte is the packet type:
7
+ * 0x01 CONTROL — UTF-8 JSON message (both directions)
8
+ * 0x02 AUDIO — raw 16 kHz mono 16-bit signed LE PCM (satellite → server)
9
+ * 0x03 TTS — raw mono 16-bit signed LE PCM (server → satellite)
10
+ *
11
+ * ⚠️ KEEP IN SYNC with the copy in the standalone `@iobroker/assistant-satellite` repo.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.AUDIO_SAMPLE_RATE = exports.TYPE_TTS = exports.TYPE_AUDIO = exports.TYPE_CONTROL = void 0;
15
+ exports.encodeControl = encodeControl;
16
+ exports.encodeTts = encodeTts;
17
+ exports.decodePacket = decodePacket;
18
+ exports.TYPE_CONTROL = 0x01;
19
+ exports.TYPE_AUDIO = 0x02;
20
+ exports.TYPE_TTS = 0x03;
21
+ /** Audio on the wire from the satellite is always 16 kHz, mono, 16-bit signed little-endian PCM. */
22
+ exports.AUDIO_SAMPLE_RATE = 16000;
23
+ /** Prefix a control JSON message with the CONTROL type byte. */
24
+ function encodeControl(msg) {
25
+ return Buffer.concat([Buffer.from([exports.TYPE_CONTROL]), Buffer.from(JSON.stringify(msg), 'utf8')]);
26
+ }
27
+ /** Prefix a raw PCM chunk with the TTS type byte. */
28
+ function encodeTts(pcm) {
29
+ return Buffer.concat([Buffer.from([exports.TYPE_TTS]), pcm]);
30
+ }
31
+ /** Split an incoming datagram into its type byte and payload (a view onto the original buffer). */
32
+ function decodePacket(data) {
33
+ return { type: data[0], payload: data.subarray(1) };
34
+ }
35
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../../src/lib/voice/protocol.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AA2BH,sCAEC;AAGD,8BAEC;AAGD,oCAEC;AArCY,QAAA,YAAY,GAAG,IAAI,CAAC;AACpB,QAAA,UAAU,GAAG,IAAI,CAAC;AAClB,QAAA,QAAQ,GAAG,IAAI,CAAC;AAE7B,oGAAoG;AACvF,QAAA,iBAAiB,GAAG,KAAK,CAAC;AAmBvC,gEAAgE;AAChE,SAAgB,aAAa,CAAC,GAAgB;IAC1C,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAY,CAAC,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AAClG,CAAC;AAED,qDAAqD;AACrD,SAAgB,SAAS,CAAC,GAAW;IACjC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,gBAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,mGAAmG;AACnG,SAAgB,YAAY,CAAC,IAAY;IACrC,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;AACxD,CAAC","sourcesContent":["/**\n * Voice-satellite UDP wire protocol — compatible with the Hannah satellite\n * (`C:\\iot\\Hannah\\satellite-pi\\satellite.py`) so existing ESP/Python satellites keep working.\n *\n * One UDP socket, first byte is the packet type:\n * 0x01 CONTROL — UTF-8 JSON message (both directions)\n * 0x02 AUDIO — raw 16 kHz mono 16-bit signed LE PCM (satellite → server)\n * 0x03 TTS — raw mono 16-bit signed LE PCM (server → satellite)\n *\n * ⚠️ KEEP IN SYNC with the copy in the standalone `@iobroker/assistant-satellite` repo.\n */\n\nexport const TYPE_CONTROL = 0x01;\nexport const TYPE_AUDIO = 0x02;\nexport const TYPE_TTS = 0x03;\n\n/** Audio on the wire from the satellite is always 16 kHz, mono, 16-bit signed little-endian PCM. */\nexport const AUDIO_SAMPLE_RATE = 16000;\n\n/** Control messages sent by a satellite to the server. */\nexport type SatToServer =\n | { type: 'register'; device: string; room?: string; listen_port?: number }\n | { type: 'heartbeat'; device: string }\n | { type: 'audio_end'; device: string };\n\n/** Satellite lifecycle/status as reflected to the satellite (drives its LED) and to ioBroker states. */\nexport type SatelliteState = 'idle' | 'listening' | 'processing' | 'speaking';\n\n/** Control messages sent by the server to a satellite. */\nexport type ServerToSat =\n | { type: 'registered'; ok: boolean }\n | { type: 'heartbeat_ack' }\n | { type: 'reregister' }\n | { type: 'tts_end'; sample_rate: number }\n | { type: 'status'; state: SatelliteState };\n\n/** Prefix a control JSON message with the CONTROL type byte. */\nexport function encodeControl(msg: ServerToSat): Buffer {\n return Buffer.concat([Buffer.from([TYPE_CONTROL]), Buffer.from(JSON.stringify(msg), 'utf8')]);\n}\n\n/** Prefix a raw PCM chunk with the TTS type byte. */\nexport function encodeTts(pcm: Buffer): Buffer {\n return Buffer.concat([Buffer.from([TYPE_TTS]), pcm]);\n}\n\n/** Split an incoming datagram into its type byte and payload (a view onto the original buffer). */\nexport function decodePacket(data: Buffer): { type: number; payload: Buffer } {\n return { type: data[0], payload: data.subarray(1) };\n}\n"]}
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.OpenAiStt = void 0;
37
+ exports.hintsToPrompt = hintsToPrompt;
38
+ exports.pcmToWav = pcmToWav;
39
+ /**
40
+ * Speech-to-text engines. V1 ships the cloud engine (OpenAI Whisper); local engines
41
+ * (Vosk / whisper.cpp / Wyoming) plug in behind the same `SttEngine` interface later.
42
+ */
43
+ const openai_1 = __importStar(require("openai"));
44
+ /**
45
+ * Join vocabulary hints into a Whisper `prompt` string (a soft bias toward these terms), de-duplicated and
46
+ * length-bounded to stay within the model's ~224-token prompt budget. Returns '' when there are no hints.
47
+ */
48
+ function hintsToPrompt(hints, maxChars = 800) {
49
+ if (!hints?.length) {
50
+ return '';
51
+ }
52
+ const seen = new Set();
53
+ const parts = [];
54
+ let len = 0;
55
+ for (const h of hints) {
56
+ const term = (h || '').trim();
57
+ const key = term.toLowerCase();
58
+ if (!term || seen.has(key)) {
59
+ continue;
60
+ }
61
+ if (len + term.length + 2 > maxChars) {
62
+ break;
63
+ }
64
+ seen.add(key);
65
+ parts.push(term);
66
+ len += term.length + 2;
67
+ }
68
+ return parts.join(', ');
69
+ }
70
+ /** Wrap raw mono 16-bit-signed-LE PCM into a minimal 44-byte WAV container. */
71
+ function pcmToWav(pcm, sampleRate) {
72
+ const numChannels = 1;
73
+ const bitsPerSample = 16;
74
+ const blockAlign = (numChannels * bitsPerSample) / 8;
75
+ const byteRate = sampleRate * blockAlign;
76
+ const header = Buffer.alloc(44);
77
+ header.write('RIFF', 0);
78
+ header.writeUInt32LE(36 + pcm.length, 4);
79
+ header.write('WAVE', 8);
80
+ header.write('fmt ', 12);
81
+ header.writeUInt32LE(16, 16); // PCM fmt chunk size
82
+ header.writeUInt16LE(1, 20); // audio format = PCM
83
+ header.writeUInt16LE(numChannels, 22);
84
+ header.writeUInt32LE(sampleRate, 24);
85
+ header.writeUInt32LE(byteRate, 28);
86
+ header.writeUInt16LE(blockAlign, 32);
87
+ header.writeUInt16LE(bitsPerSample, 34);
88
+ header.write('data', 36);
89
+ header.writeUInt32LE(pcm.length, 40);
90
+ return Buffer.concat([header, pcm]);
91
+ }
92
+ /** Cloud STT via the OpenAI transcription API (Whisper). */
93
+ class OpenAiStt {
94
+ client;
95
+ model;
96
+ constructor(apiKey, model = 'whisper-1') {
97
+ this.client = new openai_1.default({ apiKey });
98
+ this.model = model;
99
+ }
100
+ async transcribe(pcm, sampleRate, lang, hints) {
101
+ const file = await (0, openai_1.toFile)(pcmToWav(pcm, sampleRate), 'audio.wav', { type: 'audio/wav' });
102
+ // A prompt biases Whisper toward the given domain vocabulary (device/room names) without excluding
103
+ // other words — improves recognition of proper nouns the NLU then needs to match.
104
+ const prompt = hintsToPrompt(hints);
105
+ const res = await this.client.audio.transcriptions.create({
106
+ file,
107
+ model: this.model,
108
+ // Whisper wants the bare ISO-639-1 code; strip any region (de-DE → de). '' → auto-detect.
109
+ language: (lang || '').split('-')[0] || undefined,
110
+ ...(prompt ? { prompt } : {}),
111
+ });
112
+ return (res.text || '').trim();
113
+ }
114
+ }
115
+ exports.OpenAiStt = OpenAiStt;
116
+ //# sourceMappingURL=stt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stt.js","sourceRoot":"","sources":["../../../src/lib/voice/stt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Speech-to-text engines. V1 ships the cloud engine (OpenAI Whisper); local engines\n * (Vosk / whisper.cpp / Wyoming) plug in behind the same `SttEngine` interface later.\n */\nimport OpenAI, { toFile } from 'openai';\n\nexport interface SttEngine {\n /**\n * Transcribe raw mono 16-bit PCM. `lang` is an ISO-639-1 hint ('' = auto-detect).\n *\n * `hints` is an optional domain vocabulary (device / room / scene names …) to bias recognition toward,\n * so the engine better transcribes those proper nouns. Only engines that support *soft* biasing use it\n * (OpenAI Whisper `prompt`, Azure phrase list); engines where a word list would be a hard constraint or\n * needs pre-registration (Vosk grammar, AWS custom vocabulary) ignore it.\n */\n transcribe(pcm: Buffer, sampleRate: number, lang: string, hints?: string[]): Promise<string>;\n /** Optional warm-up (install/download engine + model for `lang`) so the first request isn't slow. */\n prepare?(lang: string): Promise<void>;\n}\n\n/**\n * Join vocabulary hints into a Whisper `prompt` string (a soft bias toward these terms), de-duplicated and\n * length-bounded to stay within the model's ~224-token prompt budget. Returns '' when there are no hints.\n */\nexport function hintsToPrompt(hints: string[] | undefined, maxChars = 800): string {\n if (!hints?.length) {\n return '';\n }\n const seen = new Set<string>();\n const parts: string[] = [];\n let len = 0;\n for (const h of hints) {\n const term = (h || '').trim();\n const key = term.toLowerCase();\n if (!term || seen.has(key)) {\n continue;\n }\n if (len + term.length + 2 > maxChars) {\n break;\n }\n seen.add(key);\n parts.push(term);\n len += term.length + 2;\n }\n return parts.join(', ');\n}\n\n/** Wrap raw mono 16-bit-signed-LE PCM into a minimal 44-byte WAV container. */\nexport function pcmToWav(pcm: Buffer, sampleRate: number): Buffer {\n const numChannels = 1;\n const bitsPerSample = 16;\n const blockAlign = (numChannels * bitsPerSample) / 8;\n const byteRate = sampleRate * blockAlign;\n const header = Buffer.alloc(44);\n header.write('RIFF', 0);\n header.writeUInt32LE(36 + pcm.length, 4);\n header.write('WAVE', 8);\n header.write('fmt ', 12);\n header.writeUInt32LE(16, 16); // PCM fmt chunk size\n header.writeUInt16LE(1, 20); // audio format = PCM\n header.writeUInt16LE(numChannels, 22);\n header.writeUInt32LE(sampleRate, 24);\n header.writeUInt32LE(byteRate, 28);\n header.writeUInt16LE(blockAlign, 32);\n header.writeUInt16LE(bitsPerSample, 34);\n header.write('data', 36);\n header.writeUInt32LE(pcm.length, 40);\n return Buffer.concat([header, pcm]);\n}\n\n/** Cloud STT via the OpenAI transcription API (Whisper). */\nexport class OpenAiStt implements SttEngine {\n private readonly client: OpenAI;\n private readonly model: string;\n\n constructor(apiKey: string, model = 'whisper-1') {\n this.client = new OpenAI({ apiKey });\n this.model = model;\n }\n\n async transcribe(pcm: Buffer, sampleRate: number, lang: string, hints?: string[]): Promise<string> {\n const file = await toFile(pcmToWav(pcm, sampleRate), 'audio.wav', { type: 'audio/wav' });\n // A prompt biases Whisper toward the given domain vocabulary (device/room names) without excluding\n // other words — improves recognition of proper nouns the NLU then needs to match.\n const prompt = hintsToPrompt(hints);\n const res = await this.client.audio.transcriptions.create({\n file,\n model: this.model,\n // Whisper wants the bare ISO-639-1 code; strip any region (de-DE → de). '' → auto-detect.\n language: (lang || '').split('-')[0] || undefined,\n ...(prompt ? { prompt } : {}),\n });\n return (res.text || '').trim();\n }\n}\n"]}