iobroker.assistant 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/admin/custom/.vite/manifest.json +18 -18
- package/admin/custom/assets/Components-x2Dfv8qE.js +2 -0
- package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-C5NPM0Of.js → _virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-B4YbqhBb.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cdyjt5KT.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-o4EU_Ut5.js} +10 -10
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared-Dsw5zWS8.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared--be2hWbV.js} +1 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-BsTz_EWE.js +122 -0
- package/admin/custom/assets/{hostInit-jJe8bIfV.js → hostInit-Blh7a-Qe.js} +1 -1
- package/admin/custom/assets/{lib-CL8mXvI6.js → lib-DBbNtolG.js} +1 -1
- package/admin/custom/assets/{mf-entry-bootstrap-0-ab227aa6.js → mf-entry-bootstrap-0-7abd30d3.js} +2 -2
- package/admin/custom/assets/{pendingShares-wrGtfliQ.js → pendingShares-U0D1MYK4.js} +1 -1
- package/admin/custom/customComponents.js +2 -2
- package/admin/custom/i18n/de.json +16 -0
- package/admin/custom/i18n/en.json +16 -0
- package/admin/custom/i18n/es.json +16 -0
- package/admin/custom/i18n/fr.json +16 -0
- package/admin/custom/i18n/it.json +16 -0
- package/admin/custom/i18n/nl.json +16 -0
- package/admin/custom/i18n/pl.json +16 -0
- package/admin/custom/i18n/pt.json +16 -0
- package/admin/custom/i18n/ru.json +16 -0
- package/admin/custom/i18n/uk.json +16 -0
- package/admin/custom/i18n/zh-cn.json +16 -0
- package/admin/custom/index.html +2 -2
- package/admin/custom/mf-manifest.json +1 -1
- package/admin/custom/mf-stats.json +1 -1
- package/admin/i18n/de.json +25 -2
- package/admin/i18n/en.json +25 -2
- package/admin/i18n/es.json +25 -2
- package/admin/i18n/fr.json +25 -2
- package/admin/i18n/it.json +25 -2
- package/admin/i18n/nl.json +25 -2
- package/admin/i18n/pl.json +25 -2
- package/admin/i18n/pt.json +25 -2
- package/admin/i18n/ru.json +25 -2
- package/admin/i18n/uk.json +25 -2
- package/admin/i18n/zh-cn.json +25 -2
- package/admin/jsonConfig.json +197 -13
- package/build/lib/credentials.js +11 -5
- package/build/lib/credentials.js.map +1 -1
- package/build/lib/llm.js +17 -3
- package/build/lib/llm.js.map +1 -1
- package/build/lib/nlu.js +249 -9
- package/build/lib/nlu.js.map +1 -1
- package/build/lib/piAgent.js +89 -0
- package/build/lib/piAgent.js.map +1 -0
- package/build/lib/systemAi.js +124 -0
- package/build/lib/systemAi.js.map +1 -0
- package/build/lib/voice/engines.js +31 -3
- package/build/lib/voice/engines.js.map +1 -1
- package/build/lib/voice/openrouter.js +220 -0
- package/build/lib/voice/openrouter.js.map +1 -0
- package/build/main.js +573 -59
- package/build/main.js.map +1 -1
- package/io-package.json +27 -17
- package/package.json +1 -1
- package/admin/custom/assets/Components-BSewCBfS.js +0 -2
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-SgSagTOv.js +0 -122
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The adapter `pi-agent` as the cloud tier ("harness"): instead of running its own tool loop, the assistant
|
|
4
|
+
* hands the question to a pi-agent instance via `sendTo(<instance>, 'ask', …)`. pi-agent stores every model
|
|
5
|
+
* and tool call as a checkpoint before it runs (an interrupted request continues after a restart, a write is
|
|
6
|
+
* never repeated), enforces its own write permissions and confirmations, and can use the system-wide AI
|
|
7
|
+
* settings of admin — none of which the built-in `LlmAgent` does.
|
|
8
|
+
*
|
|
9
|
+
* Each request source (satellite, `chat`, `telegram:<user>`) gets a conversation of its own in pi-agent
|
|
10
|
+
* (`conversation`), so two rooms do not see each other's follow-ups. The source-independent part of the
|
|
11
|
+
* prompt (style, follow-up marker, memory, device list) goes along as the `instructions` of that
|
|
12
|
+
* conversation; what changes every turn (time, weather, presence) is prepended to the question.
|
|
13
|
+
* pi-agent 0.3.0 and older ignore both fields and answer everything in their root conversation.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.PiAgentHarness = exports.PI_AGENT_TIMEOUT_SEC = exports.PI_AGENT_MAX_INSTRUCTIONS = void 0;
|
|
17
|
+
exports.composeInstructions = composeInstructions;
|
|
18
|
+
exports.conversationOf = conversationOf;
|
|
19
|
+
/** Longest instructions pi-agent accepts for a conversation; anything beyond is cut there. */
|
|
20
|
+
exports.PI_AGENT_MAX_INSTRUCTIONS = 20000;
|
|
21
|
+
/** Default for `piAgentTimeoutSec`: a tool loop with several reads can take a while. */
|
|
22
|
+
exports.PI_AGENT_TIMEOUT_SEC = 60;
|
|
23
|
+
/**
|
|
24
|
+
* Join instruction blocks, leaving out optional ones from the end until the result fits into pi-agent's
|
|
25
|
+
* limit — pi-agent would cut the text in the middle, and a half device list is worse than none.
|
|
26
|
+
*
|
|
27
|
+
* @param required blocks that always go along (style, follow-up marker)
|
|
28
|
+
* @param optional blocks in order of importance (memory before the device list)
|
|
29
|
+
*/
|
|
30
|
+
function composeInstructions(required, optional) {
|
|
31
|
+
const blocks = [...required.filter(Boolean)];
|
|
32
|
+
const extra = optional.filter(Boolean);
|
|
33
|
+
let dropped = 0;
|
|
34
|
+
while (extra.length) {
|
|
35
|
+
const text = [...blocks, ...extra].join('\n\n');
|
|
36
|
+
if (text.length <= exports.PI_AGENT_MAX_INSTRUCTIONS) {
|
|
37
|
+
return { text, dropped };
|
|
38
|
+
}
|
|
39
|
+
extra.pop();
|
|
40
|
+
dropped++;
|
|
41
|
+
}
|
|
42
|
+
return { text: blocks.join('\n\n').slice(0, exports.PI_AGENT_MAX_INSTRUCTIONS), dropped };
|
|
43
|
+
}
|
|
44
|
+
/** Conversation name for a request source; pi-agent uses "/" as separator in its keys. */
|
|
45
|
+
function conversationOf(source) {
|
|
46
|
+
return (source || 'default').replace(/\//g, '_').slice(0, 100);
|
|
47
|
+
}
|
|
48
|
+
class PiAgentHarness {
|
|
49
|
+
host;
|
|
50
|
+
instance;
|
|
51
|
+
timeoutMs;
|
|
52
|
+
seq = 0;
|
|
53
|
+
constructor(host,
|
|
54
|
+
/** e.g. `pi-agent.0` */
|
|
55
|
+
instance, timeoutMs = exports.PI_AGENT_TIMEOUT_SEC * 1000) {
|
|
56
|
+
this.host = host;
|
|
57
|
+
this.instance = instance;
|
|
58
|
+
this.timeoutMs = timeoutMs;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Ask pi-agent. Fails fast when the instance is not running (otherwise the caller would wait for the whole
|
|
62
|
+
* timeout), and turns pi-agent's `{error}` into an exception so the caller can fall back to the local model.
|
|
63
|
+
*/
|
|
64
|
+
async ask(req) {
|
|
65
|
+
const alive = await this.host.getForeignStateAsync(`system.adapter.${this.instance}.alive`);
|
|
66
|
+
if (!alive?.val) {
|
|
67
|
+
throw new Error(`${this.instance} is not running`);
|
|
68
|
+
}
|
|
69
|
+
// Unique per request: pi-agent answers a repeated requestId only once.
|
|
70
|
+
const requestId = `assistant-${Date.now().toString(36)}-${(++this.seq).toString(36)}`;
|
|
71
|
+
const result = (await this.host.sendToAsync(this.instance, 'ask', {
|
|
72
|
+
text: req.text,
|
|
73
|
+
conversation: conversationOf(req.conversation),
|
|
74
|
+
instructions: req.instructions,
|
|
75
|
+
requestId,
|
|
76
|
+
}, { timeout: this.timeoutMs }));
|
|
77
|
+
if (result?.error) {
|
|
78
|
+
const error = typeof result.error === 'string' ? result.error : JSON.stringify(result.error);
|
|
79
|
+
throw new Error(`${this.instance}: ${error}`);
|
|
80
|
+
}
|
|
81
|
+
const answer = typeof result?.answer === 'string' ? result.answer.trim() : '';
|
|
82
|
+
if (!answer) {
|
|
83
|
+
throw new Error(`${this.instance} gave no answer`);
|
|
84
|
+
}
|
|
85
|
+
return answer;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.PiAgentHarness = PiAgentHarness;
|
|
89
|
+
//# sourceMappingURL=piAgent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"piAgent.js","sourceRoot":"","sources":["../../src/lib/piAgent.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The adapter `pi-agent` as the cloud tier (\"harness\"): instead of running its own tool loop, the assistant\n * hands the question to a pi-agent instance via `sendTo(<instance>, 'ask', …)`. pi-agent stores every model\n * and tool call as a checkpoint before it runs (an interrupted request continues after a restart, a write is\n * never repeated), enforces its own write permissions and confirmations, and can use the system-wide AI\n * settings of admin — none of which the built-in `LlmAgent` does.\n *\n * Each request source (satellite, `chat`, `telegram:<user>`) gets a conversation of its own in pi-agent\n * (`conversation`), so two rooms do not see each other's follow-ups. The source-independent part of the\n * prompt (style, follow-up marker, memory, device list) goes along as the `instructions` of that\n * conversation; what changes every turn (time, weather, presence) is prepended to the question.\n * pi-agent 0.3.0 and older ignore both fields and answer everything in their root conversation.\n */\n\n/** Longest instructions pi-agent accepts for a conversation; anything beyond is cut there. */\nexport const PI_AGENT_MAX_INSTRUCTIONS = 20000;\n\n/** Default for `piAgentTimeoutSec`: a tool loop with several reads can take a while. */\nexport const PI_AGENT_TIMEOUT_SEC = 60;\n\n/** The part of the adapter this module needs (kept small so tests can fake it). */\nexport interface PiAgentHost {\n sendToAsync(instance: string, command: string, message: unknown, options?: { timeout?: number }): Promise<unknown>;\n getForeignStateAsync(id: string): Promise<ioBroker.State | null | undefined>;\n}\n\nexport interface PiAgentRequest {\n /** The text pi-agent answers: context lines + the question. */\n text: string;\n /** Conversation of the caller, e.g. the satellite name. */\n conversation: string;\n /** Instructions of that conversation. */\n instructions: string;\n}\n\n/**\n * Join instruction blocks, leaving out optional ones from the end until the result fits into pi-agent's\n * limit — pi-agent would cut the text in the middle, and a half device list is worse than none.\n *\n * @param required blocks that always go along (style, follow-up marker)\n * @param optional blocks in order of importance (memory before the device list)\n */\nexport function composeInstructions(required: string[], optional: string[]): { text: string; dropped: number } {\n const blocks = [...required.filter(Boolean)];\n const extra = optional.filter(Boolean);\n let dropped = 0;\n while (extra.length) {\n const text = [...blocks, ...extra].join('\\n\\n');\n if (text.length <= PI_AGENT_MAX_INSTRUCTIONS) {\n return { text, dropped };\n }\n extra.pop();\n dropped++;\n }\n return { text: blocks.join('\\n\\n').slice(0, PI_AGENT_MAX_INSTRUCTIONS), dropped };\n}\n\n/** Conversation name for a request source; pi-agent uses \"/\" as separator in its keys. */\nexport function conversationOf(source: string): string {\n return (source || 'default').replace(/\\//g, '_').slice(0, 100);\n}\n\nexport class PiAgentHarness {\n private seq = 0;\n\n public constructor(\n private readonly host: PiAgentHost,\n /** e.g. `pi-agent.0` */\n public readonly instance: string,\n private readonly timeoutMs: number = PI_AGENT_TIMEOUT_SEC * 1000,\n ) {}\n\n /**\n * Ask pi-agent. Fails fast when the instance is not running (otherwise the caller would wait for the whole\n * timeout), and turns pi-agent's `{error}` into an exception so the caller can fall back to the local model.\n */\n public async ask(req: PiAgentRequest): Promise<string> {\n const alive = await this.host.getForeignStateAsync(`system.adapter.${this.instance}.alive`);\n if (!alive?.val) {\n throw new Error(`${this.instance} is not running`);\n }\n // Unique per request: pi-agent answers a repeated requestId only once.\n const requestId = `assistant-${Date.now().toString(36)}-${(++this.seq).toString(36)}`;\n const result = (await this.host.sendToAsync(\n this.instance,\n 'ask',\n {\n text: req.text,\n conversation: conversationOf(req.conversation),\n instructions: req.instructions,\n requestId,\n },\n { timeout: this.timeoutMs },\n )) as { answer?: unknown; error?: unknown } | null | undefined;\n if (result?.error) {\n const error = typeof result.error === 'string' ? result.error : JSON.stringify(result.error);\n throw new Error(`${this.instance}: ${error}`);\n }\n const answer = typeof result?.answer === 'string' ? result.answer.trim() : '';\n if (!answer) {\n throw new Error(`${this.instance} gave no answer`);\n }\n return answer;\n }\n}\n"]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SYSTEM_AI_ID = void 0;
|
|
4
|
+
exports.systemAiSelection = systemAiSelection;
|
|
5
|
+
exports.systemAiFingerprint = systemAiFingerprint;
|
|
6
|
+
exports.usesSystemAi = usesSystemAi;
|
|
7
|
+
exports.readSystemAi = readSystemAi;
|
|
8
|
+
exports.resolveSystemLlm = resolveSystemLlm;
|
|
9
|
+
exports.resolveLlm = resolveLlm;
|
|
10
|
+
const credentials_1 = require("./credentials");
|
|
11
|
+
const llm_1 = require("./llm");
|
|
12
|
+
/** ID of the object with the system-wide AI settings */
|
|
13
|
+
exports.SYSTEM_AI_ID = 'system.ai';
|
|
14
|
+
/** Fields of `system.ai` the assistant uses — a change of the others (like admin's chat button) needs no restart */
|
|
15
|
+
const USED_FIELDS = ['provider', 'model', 'credentialId', 'baseUrl'];
|
|
16
|
+
/** Where to set them up, for the log */
|
|
17
|
+
const WHERE = 'admin → System settings → tab "AI"';
|
|
18
|
+
function text(value) {
|
|
19
|
+
return typeof value === 'string' || typeof value === 'number' ? String(value).trim() : '';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Provider, model, credential and address of `system.ai`; `null` if it names no provider the assistant knows.
|
|
23
|
+
*
|
|
24
|
+
* @param native the `native` of `system.ai`
|
|
25
|
+
*/
|
|
26
|
+
function systemAiSelection(native) {
|
|
27
|
+
const provider = text(native?.provider);
|
|
28
|
+
if (!provider || !Object.prototype.hasOwnProperty.call(llm_1.PROVIDER_PRESETS, provider)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
provider: provider,
|
|
33
|
+
model: text(native?.model),
|
|
34
|
+
credentialId: text(native?.credentialId),
|
|
35
|
+
baseUrl: provider === 'custom' ? text(native?.baseUrl) : '',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What of `system.ai` the assistant depends on; a change of it restarts the instance.
|
|
40
|
+
*
|
|
41
|
+
* @param native the `native` of `system.ai`
|
|
42
|
+
*/
|
|
43
|
+
function systemAiFingerprint(native) {
|
|
44
|
+
return JSON.stringify(USED_FIELDS.map(field => text(native?.[field])));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether the instance takes the cloud LLM from `system.ai`: provider `system`, or a provider that needs a key
|
|
48
|
+
* without one (neither in the adapter nor a credential) — the custom endpoint often works without a key.
|
|
49
|
+
*
|
|
50
|
+
* @param cfg the configuration of the instance
|
|
51
|
+
*/
|
|
52
|
+
function usesSystemAi(cfg) {
|
|
53
|
+
const provider = cfg.provider || 'openai';
|
|
54
|
+
if (provider === 'system') {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (provider === 'none' || provider === 'pi-agent' || provider === 'custom') {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return cfg.credentialType === 'manager' ? !text(cfg.credentialIdApiKey) : !text(cfg.apiKey);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read `system.ai`; a missing object (or one the adapter may not read) gives `undefined`.
|
|
64
|
+
*
|
|
65
|
+
* @param adapter the adapter
|
|
66
|
+
*/
|
|
67
|
+
async function readSystemAi(adapter) {
|
|
68
|
+
try {
|
|
69
|
+
const obj = await adapter.getForeignObjectAsync(exports.SYSTEM_AI_ID);
|
|
70
|
+
return obj?.native;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The cloud LLM of `system.ai`, or why there is none.
|
|
78
|
+
*
|
|
79
|
+
* @param adapter the adapter (reads the credential)
|
|
80
|
+
* @param native the `native` of `system.ai`
|
|
81
|
+
* @param explicit the provider `system` was chosen (otherwise the instance only has no key of its own)
|
|
82
|
+
*/
|
|
83
|
+
async function resolveSystemLlm(adapter, native, explicit) {
|
|
84
|
+
const sel = systemAiSelection(native);
|
|
85
|
+
if (!sel) {
|
|
86
|
+
return {
|
|
87
|
+
error: explicit
|
|
88
|
+
? `The system-wide AI settings have no provider — set one up in ${WHERE} or choose a provider here.`
|
|
89
|
+
: `No API key configured — enter one in the adapter settings or set up the system-wide AI settings in ${WHERE}.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (sel.provider === 'custom' && !sel.baseUrl) {
|
|
93
|
+
return { error: `The system-wide AI settings use an OpenAI-compatible server without address (${WHERE}).` };
|
|
94
|
+
}
|
|
95
|
+
const apiKey = (0, llm_1.effectiveApiKey)(sel.provider, await (0, credentials_1.readCentralKey)(adapter, sel.credentialId));
|
|
96
|
+
if (!apiKey) {
|
|
97
|
+
return { error: `The system-wide AI settings have no API key for ${sel.provider} (${WHERE}).` };
|
|
98
|
+
}
|
|
99
|
+
const preset = (0, llm_1.resolveProvider)(sel.provider, sel.baseUrl);
|
|
100
|
+
return {
|
|
101
|
+
provider: sel.provider,
|
|
102
|
+
apiKey,
|
|
103
|
+
model: sel.model || preset.defaultModel,
|
|
104
|
+
baseUrl: preset.baseUrl,
|
|
105
|
+
system: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The cloud LLM of the instance: its own provider and key, or those of `system.ai` (see {@link usesSystemAi}).
|
|
110
|
+
* Not for the providers `none` and `pi-agent`, which have no LLM of their own.
|
|
111
|
+
*
|
|
112
|
+
* @param adapter the adapter
|
|
113
|
+
* @param cfg the configuration of the instance
|
|
114
|
+
*/
|
|
115
|
+
async function resolveLlm(adapter, cfg) {
|
|
116
|
+
if (usesSystemAi(cfg)) {
|
|
117
|
+
return resolveSystemLlm(adapter, await readSystemAi(adapter), cfg.provider === 'system');
|
|
118
|
+
}
|
|
119
|
+
const provider = (cfg.provider || 'openai');
|
|
120
|
+
const apiKey = (0, llm_1.effectiveApiKey)(provider, await (0, credentials_1.resolveApiKey)(adapter, cfg));
|
|
121
|
+
const preset = (0, llm_1.resolveProvider)(provider, cfg.baseUrl);
|
|
122
|
+
return { provider, apiKey, model: cfg.model || preset.defaultModel, baseUrl: preset.baseUrl, system: false };
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=systemAi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"systemAi.js","sourceRoot":"","sources":["../../src/lib/systemAi.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The system-wide AI settings of admin (`system.ai`, System settings → tab \"AI\") as the source of the\n * cloud LLM: provider, model, key — always an entry of the central credential store — and the address of\n * a custom endpoint.\n *\n * Used with the provider `system`, and by an instance that has no key of its own: an assistant nobody\n * configured takes what was set up once for the whole system (the same rule as `@iobroker/ai-core`'s\n * `usesSystemAiSettings`). The maximum answer length stays the assistant's own (`maxTokens`), because a\n * spoken answer needs far fewer tokens than the admin chat.\n */\nimport type { AdapterConfig } from '../types';\nimport { readCentralKey, resolveApiKey } from './credentials';\nimport { PROVIDER_PRESETS, effectiveApiKey, resolveProvider, type Provider } from './llm';\n\n/** ID of the object with the system-wide AI settings */\nexport const SYSTEM_AI_ID = 'system.ai';\n\n/** Fields of `system.ai` the assistant uses — a change of the others (like admin's chat button) needs no restart */\nconst USED_FIELDS = ['provider', 'model', 'credentialId', 'baseUrl'] as const;\n\n/** Where to set them up, for the log */\nconst WHERE = 'admin → System settings → tab \"AI\"';\n\n/** Provider, model, credential and address out of `system.ai` */\nexport interface SystemAiSelection {\n provider: Provider;\n model: string;\n credentialId: string;\n /** Only for `custom` */\n baseUrl: string;\n}\n\n/** The cloud LLM the instance works with, whether configured here or in `system.ai` */\nexport interface ResolvedLlm {\n provider: Provider;\n apiKey: string;\n model: string;\n baseUrl: string;\n /** Taken from `system.ai` */\n system: boolean;\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' || typeof value === 'number' ? String(value).trim() : '';\n}\n\n/**\n * Provider, model, credential and address of `system.ai`; `null` if it names no provider the assistant knows.\n *\n * @param native the `native` of `system.ai`\n */\nexport function systemAiSelection(native: Record<string, unknown> | null | undefined): SystemAiSelection | null {\n const provider = text(native?.provider);\n if (!provider || !Object.prototype.hasOwnProperty.call(PROVIDER_PRESETS, provider)) {\n return null;\n }\n return {\n provider: provider as Provider,\n model: text(native?.model),\n credentialId: text(native?.credentialId),\n baseUrl: provider === 'custom' ? text(native?.baseUrl) : '',\n };\n}\n\n/**\n * What of `system.ai` the assistant depends on; a change of it restarts the instance.\n *\n * @param native the `native` of `system.ai`\n */\nexport function systemAiFingerprint(native: Record<string, unknown> | null | undefined): string {\n return JSON.stringify(USED_FIELDS.map(field => text(native?.[field])));\n}\n\n/**\n * Whether the instance takes the cloud LLM from `system.ai`: provider `system`, or a provider that needs a key\n * without one (neither in the adapter nor a credential) — the custom endpoint often works without a key.\n *\n * @param cfg the configuration of the instance\n */\nexport function usesSystemAi(\n cfg: Pick<AdapterConfig, 'provider' | 'credentialType' | 'apiKey' | 'credentialIdApiKey'>,\n): boolean {\n const provider = cfg.provider || 'openai';\n if (provider === 'system') {\n return true;\n }\n if (provider === 'none' || provider === 'pi-agent' || provider === 'custom') {\n return false;\n }\n return cfg.credentialType === 'manager' ? !text(cfg.credentialIdApiKey) : !text(cfg.apiKey);\n}\n\n/**\n * Read `system.ai`; a missing object (or one the adapter may not read) gives `undefined`.\n *\n * @param adapter the adapter\n */\nexport async function readSystemAi(adapter: ioBroker.Adapter): Promise<Record<string, unknown> | undefined> {\n try {\n const obj = await adapter.getForeignObjectAsync(SYSTEM_AI_ID);\n return obj?.native;\n } catch {\n return undefined;\n }\n}\n\n/**\n * The cloud LLM of `system.ai`, or why there is none.\n *\n * @param adapter the adapter (reads the credential)\n * @param native the `native` of `system.ai`\n * @param explicit the provider `system` was chosen (otherwise the instance only has no key of its own)\n */\nexport async function resolveSystemLlm(\n adapter: ioBroker.Adapter,\n native: Record<string, unknown> | null | undefined,\n explicit: boolean,\n): Promise<ResolvedLlm | { error: string }> {\n const sel = systemAiSelection(native);\n if (!sel) {\n return {\n error: explicit\n ? `The system-wide AI settings have no provider — set one up in ${WHERE} or choose a provider here.`\n : `No API key configured — enter one in the adapter settings or set up the system-wide AI settings in ${WHERE}.`,\n };\n }\n if (sel.provider === 'custom' && !sel.baseUrl) {\n return { error: `The system-wide AI settings use an OpenAI-compatible server without address (${WHERE}).` };\n }\n const apiKey = effectiveApiKey(sel.provider, await readCentralKey(adapter, sel.credentialId));\n if (!apiKey) {\n return { error: `The system-wide AI settings have no API key for ${sel.provider} (${WHERE}).` };\n }\n const preset = resolveProvider(sel.provider, sel.baseUrl);\n return {\n provider: sel.provider,\n apiKey,\n model: sel.model || preset.defaultModel,\n baseUrl: preset.baseUrl,\n system: true,\n };\n}\n\n/**\n * The cloud LLM of the instance: its own provider and key, or those of `system.ai` (see {@link usesSystemAi}).\n * Not for the providers `none` and `pi-agent`, which have no LLM of their own.\n *\n * @param adapter the adapter\n * @param cfg the configuration of the instance\n */\nexport async function resolveLlm(\n adapter: ioBroker.Adapter,\n cfg: AdapterConfig,\n): Promise<ResolvedLlm | { error: string }> {\n if (usesSystemAi(cfg)) {\n return resolveSystemLlm(adapter, await readSystemAi(adapter), cfg.provider === 'system');\n }\n const provider = (cfg.provider || 'openai') as Provider;\n const apiKey = effectiveApiKey(provider, await resolveApiKey(adapter, cfg));\n const preset = resolveProvider(provider, cfg.baseUrl);\n return { provider, apiKey, model: cfg.model || preset.defaultModel, baseUrl: preset.baseUrl, system: false };\n}\n"]}
|
|
@@ -37,6 +37,7 @@ exports.createSttEngine = createSttEngine;
|
|
|
37
37
|
exports.createTtsEngine = createTtsEngine;
|
|
38
38
|
exports.listVoices = listVoices;
|
|
39
39
|
exports.listSttModels = listSttModels;
|
|
40
|
+
exports.listTtsModels = listTtsModels;
|
|
40
41
|
/**
|
|
41
42
|
* Factory that builds the configured STT/TTS engine. Cloud engines (OpenAI/Azure/AWS) use resolved
|
|
42
43
|
* credentials; local engines (Vosk/Piper) download their models on demand into the data dir.
|
|
@@ -48,6 +49,7 @@ const azure_1 = require("./azure");
|
|
|
48
49
|
const aws_1 = require("./aws");
|
|
49
50
|
const vosk_1 = require("./vosk");
|
|
50
51
|
const piper_1 = require("./piper");
|
|
52
|
+
const openrouter_1 = require("./openrouter");
|
|
51
53
|
const path = __importStar(require("node:path"));
|
|
52
54
|
const ttsCache_1 = require("./ttsCache");
|
|
53
55
|
const fallback_1 = require("./fallback");
|
|
@@ -67,6 +69,11 @@ function requireOpenAi(creds) {
|
|
|
67
69
|
throw new Error('OpenAI speech needs an API key');
|
|
68
70
|
}
|
|
69
71
|
}
|
|
72
|
+
function requireOpenRouter(creds) {
|
|
73
|
+
if (!creds.openrouterKey) {
|
|
74
|
+
throw new Error('OpenRouter speech needs an API key');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
70
77
|
function createSttEngine(provider, ctx) {
|
|
71
78
|
const primary = { name: provider, engine: buildSttEngine(provider, ctx) };
|
|
72
79
|
const fallback = ctx.sttFallback && ctx.sttFallback !== provider ? ctx.sttFallback : '';
|
|
@@ -91,6 +98,9 @@ function buildSttEngine(provider, ctx) {
|
|
|
91
98
|
return new aws_1.AwsStt(ctx.creds.aws);
|
|
92
99
|
case 'vosk':
|
|
93
100
|
return new vosk_1.VoskStt(ctx.dataDir, ctx.voskModel, ctx.log);
|
|
101
|
+
case 'openrouter':
|
|
102
|
+
requireOpenRouter(ctx.creds);
|
|
103
|
+
return new openrouter_1.OpenRouterStt(ctx.creds.openrouterKey, ctx.openrouterSttModel);
|
|
94
104
|
case 'piper':
|
|
95
105
|
throw new Error('Piper is a text-to-speech engine, not speech-to-text');
|
|
96
106
|
default:
|
|
@@ -126,7 +136,9 @@ function cachedTts(provider, ctx) {
|
|
|
126
136
|
? ctx.voices.aws
|
|
127
137
|
: provider === 'piper'
|
|
128
138
|
? ctx.voices.piper
|
|
129
|
-
:
|
|
139
|
+
: provider === 'openrouter'
|
|
140
|
+
? `${ctx.openrouterTtsModel || openrouter_1.DEFAULT_OPENROUTER_TTS_MODEL}-${ctx.voices.openrouter}`
|
|
141
|
+
: ctx.voices.openai;
|
|
130
142
|
const dir = path.join(ctx.dataDir, 'ttscache', `${provider}-${(voice || 'default').replace(/[^\w.-]/g, '_')}`);
|
|
131
143
|
return new ttsCache_1.CachedTts(buildTtsEngine(provider, ctx), { dir, log: ctx.log });
|
|
132
144
|
}
|
|
@@ -142,6 +154,9 @@ function buildTtsEngine(provider, ctx) {
|
|
|
142
154
|
return new piper_1.PiperTts(ctx.dataDir, ctx.voices.piper, ctx.log);
|
|
143
155
|
case 'vosk':
|
|
144
156
|
throw new Error('Vosk is a speech-to-text engine, not text-to-speech');
|
|
157
|
+
case 'openrouter':
|
|
158
|
+
requireOpenRouter(ctx.creds);
|
|
159
|
+
return new openrouter_1.OpenRouterTts(ctx.creds.openrouterKey, ctx.openrouterTtsModel, ctx.voices.openrouter, ctx.decodeAudio);
|
|
145
160
|
default:
|
|
146
161
|
requireOpenAi(ctx.creds);
|
|
147
162
|
return new tts_1.OpenAiTts(ctx.creds.openaiKey, (ctx.voices.openai || 'alloy'), ctx.ttsModel || undefined);
|
|
@@ -160,12 +175,25 @@ async function listVoices(provider, ctx, language) {
|
|
|
160
175
|
return (0, aws_1.listPollyVoices)(ctx.creds.aws, (0, lang_1.isoToLocale)(language));
|
|
161
176
|
case 'piper':
|
|
162
177
|
return (0, piper_1.listPiperVoices)();
|
|
178
|
+
case 'openrouter': {
|
|
179
|
+
// public list, no key needed: the voices of the chosen model
|
|
180
|
+
const model = ctx.openrouterTtsModel || openrouter_1.DEFAULT_OPENROUTER_TTS_MODEL;
|
|
181
|
+
const models = await (0, openrouter_1.listOpenRouterSpeechModels)('speech');
|
|
182
|
+
return (0, openrouter_1.voicesForLanguage)(models.find(m => m.id === model)?.voices || [], language);
|
|
183
|
+
}
|
|
163
184
|
default:
|
|
164
185
|
return OPENAI_VOICES;
|
|
165
186
|
}
|
|
166
187
|
}
|
|
167
|
-
/** Suggested STT model names for the settings dropdown
|
|
168
|
-
function listSttModels(provider, language) {
|
|
188
|
+
/** Suggested STT model names for the settings dropdown: the local Vosk catalogue, or the models of OpenRouter. */
|
|
189
|
+
async function listSttModels(provider, language) {
|
|
190
|
+
if (provider === 'openrouter') {
|
|
191
|
+
return (await (0, openrouter_1.listOpenRouterSpeechModels)('transcription')).map(m => m.id);
|
|
192
|
+
}
|
|
169
193
|
return provider === 'vosk' ? (0, vosk_1.listVoskModels)(language) : [];
|
|
170
194
|
}
|
|
195
|
+
/** TTS model names for the settings dropdown — only OpenRouter has a choice worth listing. */
|
|
196
|
+
async function listTtsModels(provider) {
|
|
197
|
+
return provider === 'openrouter' ? (await (0, openrouter_1.listOpenRouterSpeechModels)('speech')).map(m => m.id) : [];
|
|
198
|
+
}
|
|
171
199
|
//# sourceMappingURL=engines.js.map
|
|
@@ -1 +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 * as path from 'node:path';\n\nimport { CachedTts } from './ttsCache';\nimport { FallbackStt, FallbackTts, type NamedEngine } from './fallback';\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 /** Optional second engine, used when the primary one fails ('' = none). */\n sttFallback?: SpeechProvider | '';\n ttsFallback?: SpeechProvider | '';\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 const primary: NamedEngine<SttEngine> = { name: provider, engine: buildSttEngine(provider, ctx) };\n const fallback = ctx.sttFallback && ctx.sttFallback !== provider ? ctx.sttFallback : '';\n if (!fallback) {\n return primary.engine;\n }\n try {\n return new FallbackStt([primary, { name: fallback, engine: buildSttEngine(fallback, ctx) }], ctx.log);\n } catch (e) {\n ctx.log.warn(`Speech-to-text fallback '${fallback}' unavailable: ${(e as Error).message}`);\n return primary.engine;\n }\n}\n\nfunction buildSttEngine(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 // Each engine gets the shared behaviour (disk cache, length limit, SSML) and its own cache directory,\n // so a fallback engine's voice can never be served later while the primary one is healthy again.\n const primary: NamedEngine<TtsEngine> = { name: provider, engine: cachedTts(provider, ctx) };\n const fallback = ctx.ttsFallback && ctx.ttsFallback !== provider ? ctx.ttsFallback : '';\n if (!fallback) {\n return primary.engine;\n }\n try {\n return new FallbackTts([primary, { name: fallback, engine: cachedTts(fallback, ctx) }], ctx.log);\n } catch (e) {\n // A fallback that cannot even be built (missing key, wrong provider) must not take the main one down.\n ctx.log.warn(`Text-to-speech fallback '${fallback}' unavailable: ${(e as Error).message}`);\n return primary.engine;\n }\n}\n\n/**\n * One provider binding plus the shared cache/length/SSML layer. One cache directory per provider+voice —\n * a voice change must not serve audio in the old voice.\n */\nfunction cachedTts(provider: SpeechProvider, ctx: EngineContext): TtsEngine {\n const voice =\n provider === 'azure'\n ? ctx.voices.azure\n : provider === 'aws'\n ? ctx.voices.aws\n : provider === 'piper'\n ? ctx.voices.piper\n : ctx.voices.openai;\n const dir = path.join(ctx.dataDir, 'ttscache', `${provider}-${(voice || 'default').replace(/[^\\w.-]/g, '_')}`);\n return new CachedTts(buildTtsEngine(provider, ctx), { dir, log: ctx.log });\n}\n\nfunction buildTtsEngine(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"]}
|
|
1
|
+
{"version":3,"file":"engines.js","sourceRoot":"","sources":["../../../src/lib/voice/engines.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,0CAYC;AAuBD,0CAeC;AAuDD,gCAmBC;AAGD,sCAKC;AAGD,sCAEC;AAtOD;;;;GAIG;AACH,+BAAkD;AAClD,+BAAoE;AACpE,mCAA8D;AAC9D,+BAAuE;AACvE,iCAAiD;AACjD,mCAAoD;AACpD,6CAOsB;AACtB,gDAAkC;AAElC,yCAAuC;AACvC,yCAAwE;AACxE,iCAAqC;AA8CrC,SAAS,YAAY,CAAC,KAAuB;IACzC,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACxE,CAAC;AACL,CAAC;AAED,SAAS,UAAU,CAAC,KAAuB;IACvC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACvF,CAAC;AACL,CAAC;AAED,SAAS,aAAa,CAAC,KAAuB;IAC1C,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACtD,CAAC;AACL,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAuB;IAC9C,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IAC1D,CAAC;AACL,CAAC;AAED,SAAgB,eAAe,CAAC,QAAwB,EAAE,GAAkB;IACxE,MAAM,OAAO,GAA2B,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC;IAClG,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IACxF,IAAI,CAAC,QAAQ,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC,MAAM,CAAC;IAC1B,CAAC;IACD,IAAI,CAAC;QACD,OAAO,IAAI,sBAAW,CAAC,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IAC1G,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACT,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,4BAA4B,QAAQ,kBAAmB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3F,OAAO,OAAO,CAAC,MAAM,CAAC;IAC1B,CAAC;AACL,CAAC;AAED,SAAS,cAAc,CAAC,QAAwB,EAAE,GAAkB;IAChE,QAAQ,QAAQ,EAAE,CAAC;QACf,KAAK,OAAO;YACR,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACxB,OAAO,IAAI,gBAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACnE,KAAK,KAAK;YACN,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACtB,OAAO,IAAI,YAAM,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACrC,KAAK,MAAM;YACP,OAAO,IAAI,cAAO,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5D,KAAK,YAAY;YACb,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC7B,OAAO,IAAI,0BAAa,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAC9E,KAAK,OAAO;YACR,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAC5E;YACI,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACzB,OAAO,IAAI,eAAS,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC;IAC7E,CAAC;AACL,CAAC;AAED,SAAgB,eAAe,CAAC,QAAwB,EAAE,GAAkB;IACxE,sGAAsG;IACtG,iGAAiG;IACjG,MAAM,OAAO,GAA2B,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC;IAC7F,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;IACxF,IAAI,CAAC,QAAQ,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC,MAAM,CAAC;IAC1B,CAAC;IACD,IAAI,CAAC;QACD,OAAO,IAAI,sBAAW,CAAC,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IACrG,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACT,sGAAsG;QACtG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,4BAA4B,QAAQ,kBAAmB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3F,OAAO,OAAO,CAAC,MAAM,CAAC;IAC1B,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,SAAS,SAAS,CAAC,QAAwB,EAAE,GAAkB;IAC3D,MAAM,KAAK,GACP,QAAQ,KAAK,OAAO;QAChB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK;QAClB,CAAC,CAAC,QAAQ,KAAK,KAAK;YAClB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG;YAChB,CAAC,CAAC,QAAQ,KAAK,OAAO;gBACpB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK;gBAClB,CAAC,CAAC,QAAQ,KAAK,YAAY;oBACzB,CAAC,CAAC,GAAG,GAAG,CAAC,kBAAkB,IAAI,yCAA4B,IAAI,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE;oBACtF,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC;IAClC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,QAAQ,IAAI,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/G,OAAO,IAAI,oBAAS,CAAC,cAAc,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;AAC/E,CAAC;AAED,SAAS,cAAc,CAAC,QAAwB,EAAE,GAAkB;IAChE,QAAQ,QAAQ,EAAE,CAAC;QACf,KAAK,OAAO;YACR,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACxB,OAAO,IAAI,gBAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACrF,KAAK,KAAK;YACN,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACtB,OAAO,IAAI,YAAM,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACrD,KAAK,OAAO;YACR,OAAO,IAAI,gBAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QAChE,KAAK,MAAM;YACP,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;QAC3E,KAAK,YAAY;YACb,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC7B,OAAO,IAAI,0BAAa,CACpB,GAAG,CAAC,KAAK,CAAC,aAAa,EACvB,GAAG,CAAC,kBAAkB,EACtB,GAAG,CAAC,MAAM,CAAC,UAAU,EACrB,GAAG,CAAC,WAAW,CAClB,CAAC;QACN;YACI,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACzB,OAAO,IAAI,eAAS,CAChB,GAAG,CAAC,KAAK,CAAC,SAAS,EACnB,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,IAAI,OAAO,CAAgB,EAC7C,GAAG,CAAC,QAAQ,IAAI,SAAS,CAC5B,CAAC;IACV,CAAC;AACL,CAAC;AAED,uDAAuD;AACvD,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;AAE5E,sFAAsF;AAC/E,KAAK,UAAU,UAAU,CAAC,QAAwB,EAAE,GAAkB,EAAE,QAAgB;IAC3F,QAAQ,QAAQ,EAAE,CAAC;QACf,KAAK,OAAO;YACR,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACxB,OAAO,IAAA,uBAAe,EAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,IAAA,kBAAW,EAAC,QAAQ,CAAC,CAAC,CAAC;QAC7F,KAAK,KAAK;YACN,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACtB,OAAO,IAAA,qBAAe,EAAC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,IAAA,kBAAW,EAAC,QAAQ,CAAC,CAAC,CAAC;QACjE,KAAK,OAAO;YACR,OAAO,IAAA,uBAAe,GAAE,CAAC;QAC7B,KAAK,YAAY,CAAC,CAAC,CAAC;YAChB,6DAA6D;YAC7D,MAAM,KAAK,GAAG,GAAG,CAAC,kBAAkB,IAAI,yCAA4B,CAAC;YACrE,MAAM,MAAM,GAAG,MAAM,IAAA,uCAA0B,EAAC,QAAQ,CAAC,CAAC;YAC1D,OAAO,IAAA,8BAAiB,EAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,EAAE,MAAM,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;QACvF,CAAC;QACD;YACI,OAAO,aAAa,CAAC;IAC7B,CAAC;AACL,CAAC;AAED,kHAAkH;AAC3G,KAAK,UAAU,aAAa,CAAC,QAAwB,EAAE,QAAgB;IAC1E,IAAI,QAAQ,KAAK,YAAY,EAAE,CAAC;QAC5B,OAAO,CAAC,MAAM,IAAA,uCAA0B,EAAC,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,IAAA,qBAAc,EAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/D,CAAC;AAED,8FAA8F;AACvF,KAAK,UAAU,aAAa,CAAC,QAAwB;IACxD,OAAO,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,IAAA,uCAA0B,EAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACxG,CAAC","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 {\n OpenRouterStt,\n OpenRouterTts,\n listOpenRouterSpeechModels,\n voicesForLanguage,\n DEFAULT_OPENROUTER_TTS_MODEL,\n type AudioDecoder,\n} from './openrouter';\nimport * as path from 'node:path';\n\nimport { CachedTts } from './ttsCache';\nimport { FallbackStt, FallbackTts, type NamedEngine } from './fallback';\nimport { isoToLocale } from './lang';\nimport type { VoiceLogger } from './download';\n\nexport type SpeechProvider = 'openai' | 'openrouter' | 'azure' | 'aws' | 'vosk' | 'piper';\n\n/** All speech credentials, resolved from config (or the central credential store). */\nexport interface VoiceCredentials {\n openaiKey: string;\n openrouterKey: 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 openrouter: string; // depends on the model, e.g. Kore, george\n}\n\n/** Everything the factory needs to build any engine. */\nexport interface EngineContext {\n creds: VoiceCredentials;\n voices: VoiceNames;\n /** Optional second engine, used when the primary one fails ('' = none). */\n sttFallback?: SpeechProvider | '';\n ttsFallback?: SpeechProvider | '';\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 /** OpenRouter models ('' → the defaults of openrouter.ts) */\n openrouterSttModel: string;\n openrouterTtsModel: string;\n /** Decoder for compressed speech (OpenRouter models that only return mp3); absent → those models fail */\n decodeAudio?: AudioDecoder;\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\nfunction requireOpenRouter(creds: VoiceCredentials): void {\n if (!creds.openrouterKey) {\n throw new Error('OpenRouter speech needs an API key');\n }\n}\n\nexport function createSttEngine(provider: SpeechProvider, ctx: EngineContext): SttEngine {\n const primary: NamedEngine<SttEngine> = { name: provider, engine: buildSttEngine(provider, ctx) };\n const fallback = ctx.sttFallback && ctx.sttFallback !== provider ? ctx.sttFallback : '';\n if (!fallback) {\n return primary.engine;\n }\n try {\n return new FallbackStt([primary, { name: fallback, engine: buildSttEngine(fallback, ctx) }], ctx.log);\n } catch (e) {\n ctx.log.warn(`Speech-to-text fallback '${fallback}' unavailable: ${(e as Error).message}`);\n return primary.engine;\n }\n}\n\nfunction buildSttEngine(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 'openrouter':\n requireOpenRouter(ctx.creds);\n return new OpenRouterStt(ctx.creds.openrouterKey, ctx.openrouterSttModel);\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 // Each engine gets the shared behaviour (disk cache, length limit, SSML) and its own cache directory,\n // so a fallback engine's voice can never be served later while the primary one is healthy again.\n const primary: NamedEngine<TtsEngine> = { name: provider, engine: cachedTts(provider, ctx) };\n const fallback = ctx.ttsFallback && ctx.ttsFallback !== provider ? ctx.ttsFallback : '';\n if (!fallback) {\n return primary.engine;\n }\n try {\n return new FallbackTts([primary, { name: fallback, engine: cachedTts(fallback, ctx) }], ctx.log);\n } catch (e) {\n // A fallback that cannot even be built (missing key, wrong provider) must not take the main one down.\n ctx.log.warn(`Text-to-speech fallback '${fallback}' unavailable: ${(e as Error).message}`);\n return primary.engine;\n }\n}\n\n/**\n * One provider binding plus the shared cache/length/SSML layer. One cache directory per provider+voice —\n * a voice change must not serve audio in the old voice.\n */\nfunction cachedTts(provider: SpeechProvider, ctx: EngineContext): TtsEngine {\n const voice =\n provider === 'azure'\n ? ctx.voices.azure\n : provider === 'aws'\n ? ctx.voices.aws\n : provider === 'piper'\n ? ctx.voices.piper\n : provider === 'openrouter'\n ? `${ctx.openrouterTtsModel || DEFAULT_OPENROUTER_TTS_MODEL}-${ctx.voices.openrouter}`\n : ctx.voices.openai;\n const dir = path.join(ctx.dataDir, 'ttscache', `${provider}-${(voice || 'default').replace(/[^\\w.-]/g, '_')}`);\n return new CachedTts(buildTtsEngine(provider, ctx), { dir, log: ctx.log });\n}\n\nfunction buildTtsEngine(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 case 'openrouter':\n requireOpenRouter(ctx.creds);\n return new OpenRouterTts(\n ctx.creds.openrouterKey,\n ctx.openrouterTtsModel,\n ctx.voices.openrouter,\n ctx.decodeAudio,\n );\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 case 'openrouter': {\n // public list, no key needed: the voices of the chosen model\n const model = ctx.openrouterTtsModel || DEFAULT_OPENROUTER_TTS_MODEL;\n const models = await listOpenRouterSpeechModels('speech');\n return voicesForLanguage(models.find(m => m.id === model)?.voices || [], language);\n }\n default:\n return OPENAI_VOICES;\n }\n}\n\n/** Suggested STT model names for the settings dropdown: the local Vosk catalogue, or the models of OpenRouter. */\nexport async function listSttModels(provider: SpeechProvider, language: string): Promise<string[]> {\n if (provider === 'openrouter') {\n return (await listOpenRouterSpeechModels('transcription')).map(m => m.id);\n }\n return provider === 'vosk' ? listVoskModels(language) : [];\n}\n\n/** TTS model names for the settings dropdown — only OpenRouter has a choice worth listing. */\nexport async function listTtsModels(provider: SpeechProvider): Promise<string[]> {\n return provider === 'openrouter' ? (await listOpenRouterSpeechModels('speech')).map(m => m.id) : [];\n}\n"]}
|
|
@@ -0,0 +1,220 @@
|
|
|
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.OpenRouterTts = exports.OpenRouterStt = exports.DEFAULT_OPENROUTER_TTS_MODEL = exports.DEFAULT_OPENROUTER_STT_MODEL = void 0;
|
|
37
|
+
exports.listOpenRouterSpeechModels = listOpenRouterSpeechModels;
|
|
38
|
+
exports.voicesForLanguage = voicesForLanguage;
|
|
39
|
+
exports.pcmFromResponse = pcmFromResponse;
|
|
40
|
+
/**
|
|
41
|
+
* Speech over OpenRouter: one key for the speech-to-text and text-to-speech models of many vendors (OpenAI,
|
|
42
|
+
* ElevenLabs, Google, Deepgram, Mistral …) behind the OpenAI audio API at https://openrouter.ai/api/v1.
|
|
43
|
+
* Model ids carry the vendor (`openai/whisper-large-v3-turbo`), and the voices depend on the model — the public
|
|
44
|
+
* model list names them (`supported_voices`), so the settings offer exactly the voices of the chosen model.
|
|
45
|
+
*
|
|
46
|
+
* Differences to OpenAI itself: transcription ignores `prompt` (no vocabulary hints), and speech comes back as
|
|
47
|
+
* `pcm` (most models, the default) or `mp3` (MiniMax only). The PCM rate is not part of the API — OpenRouter
|
|
48
|
+
* documents 24 kHz, the rate of OpenAI, Gemini and Azure — so a rate in the content type wins over that default.
|
|
49
|
+
*/
|
|
50
|
+
const openai_1 = __importStar(require("openai"));
|
|
51
|
+
const llm_1 = require("../llm");
|
|
52
|
+
const stt_1 = require("./stt");
|
|
53
|
+
/** Speech-to-text model when none is configured: Whisper large v3 turbo, multilingual and the cheapest Whisper */
|
|
54
|
+
exports.DEFAULT_OPENROUTER_STT_MODEL = 'openai/whisper-large-v3-turbo';
|
|
55
|
+
/**
|
|
56
|
+
* Text-to-speech model when none is configured: multilingual, PCM (no decoder needed) and fast — in a test about
|
|
57
|
+
* 0.8 s for a sentence where Gemini TTS took 2.5 s, which a spoken answer waits for.
|
|
58
|
+
*/
|
|
59
|
+
exports.DEFAULT_OPENROUTER_TTS_MODEL = 'elevenlabs/eleven-flash-v2.5';
|
|
60
|
+
/** Rate of the PCM OpenRouter returns when the content type does not say otherwise */
|
|
61
|
+
const PCM_RATE = 24000;
|
|
62
|
+
/** Vendors whose speech models only return mp3 (they answer `pcm` with a 400) */
|
|
63
|
+
const MP3_ONLY = ['minimax/'];
|
|
64
|
+
/** How long the public model list is kept */
|
|
65
|
+
const MODEL_LIST_TTL_MS = 60 * 60 * 1000;
|
|
66
|
+
function client(apiKey) {
|
|
67
|
+
return new openai_1.default({ apiKey, baseURL: llm_1.PROVIDER_PRESETS.openrouter.baseUrl, defaultHeaders: llm_1.OPENROUTER_HEADERS });
|
|
68
|
+
}
|
|
69
|
+
const modelLists = new Map();
|
|
70
|
+
/**
|
|
71
|
+
* The speech models of OpenRouter. The list is public (no key), cached for an hour; a failed request is not kept.
|
|
72
|
+
*
|
|
73
|
+
* @param kind `speech` = text-to-speech, `transcription` = speech-to-text
|
|
74
|
+
* @param fetchImpl fetch (tests)
|
|
75
|
+
*/
|
|
76
|
+
function listOpenRouterSpeechModels(kind, fetchImpl = fetch) {
|
|
77
|
+
const cached = modelLists.get(kind);
|
|
78
|
+
if (cached && Date.now() - cached.at < MODEL_LIST_TTL_MS) {
|
|
79
|
+
return cached.models;
|
|
80
|
+
}
|
|
81
|
+
const models = (async () => {
|
|
82
|
+
const res = await fetchImpl(`${llm_1.PROVIDER_PRESETS.openrouter.baseUrl}/models?output_modalities=${kind}`, {
|
|
83
|
+
headers: llm_1.OPENROUTER_HEADERS,
|
|
84
|
+
signal: AbortSignal.timeout(15000),
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
throw new Error(`OpenRouter model list: HTTP ${res.status}`);
|
|
88
|
+
}
|
|
89
|
+
const body = (await res.json());
|
|
90
|
+
return (body.data || [])
|
|
91
|
+
.filter(m => typeof m.id === 'string' && m.id)
|
|
92
|
+
.map(m => ({
|
|
93
|
+
id: m.id,
|
|
94
|
+
name: typeof m.name === 'string' ? m.name : m.id,
|
|
95
|
+
voices: Array.isArray(m.supported_voices)
|
|
96
|
+
? m.supported_voices.filter((v) => typeof v === 'string')
|
|
97
|
+
: [],
|
|
98
|
+
}));
|
|
99
|
+
})();
|
|
100
|
+
modelLists.set(kind, { at: Date.now(), models });
|
|
101
|
+
models.catch(() => modelLists.delete(kind));
|
|
102
|
+
return models;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The voices of a text-to-speech model. Voices named after a locale (`de-DE-Klaus:MAI-Voice-2`) are narrowed to
|
|
106
|
+
* the language, if that leaves any; other names (`Kore`, `george`) say nothing about the language.
|
|
107
|
+
*
|
|
108
|
+
* @param voices the voices of the model
|
|
109
|
+
* @param language ISO-639-1 language ('' = all)
|
|
110
|
+
*/
|
|
111
|
+
function voicesForLanguage(voices, language) {
|
|
112
|
+
const lang = (language || '').split('-')[0].toLowerCase();
|
|
113
|
+
if (!lang) {
|
|
114
|
+
return voices;
|
|
115
|
+
}
|
|
116
|
+
const matching = voices.filter(v => new RegExp(`^${lang}[-_][A-Z]{2}\\b`).test(v));
|
|
117
|
+
return matching.length ? matching : voices;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Raw PCM out of a speech response: WAV is unwrapped, the rate comes from the content type (`audio/pcm;rate=…`)
|
|
121
|
+
* or is the documented 24 kHz.
|
|
122
|
+
*
|
|
123
|
+
* @param audio the response body
|
|
124
|
+
* @param contentType the content type of the response
|
|
125
|
+
*/
|
|
126
|
+
function pcmFromResponse(audio, contentType) {
|
|
127
|
+
if (audio.length > 44 && audio.toString('latin1', 0, 4) === 'RIFF' && audio.toString('latin1', 8, 12) === 'WAVE') {
|
|
128
|
+
const sampleRate = audio.readUInt32LE(24);
|
|
129
|
+
// the data chunk is not always right after the 16-byte fmt chunk
|
|
130
|
+
let offset = 12;
|
|
131
|
+
while (offset + 8 <= audio.length) {
|
|
132
|
+
const id = audio.toString('latin1', offset, offset + 4);
|
|
133
|
+
const size = audio.readUInt32LE(offset + 4);
|
|
134
|
+
if (id === 'data') {
|
|
135
|
+
return { pcm: audio.subarray(offset + 8, Math.min(audio.length, offset + 8 + size)), sampleRate };
|
|
136
|
+
}
|
|
137
|
+
offset += 8 + size + (size % 2);
|
|
138
|
+
}
|
|
139
|
+
return { pcm: audio.subarray(44), sampleRate };
|
|
140
|
+
}
|
|
141
|
+
const rate = /(?:^|[;\s])(?:rate|sample_?rate)=(\d+)/i.exec(contentType || '');
|
|
142
|
+
return { pcm: audio, sampleRate: rate ? Number(rate[1]) : PCM_RATE };
|
|
143
|
+
}
|
|
144
|
+
/** Speech-to-text over OpenRouter (OpenAI-compatible multipart upload) */
|
|
145
|
+
class OpenRouterStt {
|
|
146
|
+
client;
|
|
147
|
+
model;
|
|
148
|
+
constructor(apiKey, model = '') {
|
|
149
|
+
this.client = client(apiKey);
|
|
150
|
+
this.model = model || exports.DEFAULT_OPENROUTER_STT_MODEL;
|
|
151
|
+
}
|
|
152
|
+
async transcribe(pcm, sampleRate, lang) {
|
|
153
|
+
const file = await (0, openai_1.toFile)((0, stt_1.pcmToWav)(pcm, sampleRate), 'audio.wav', { type: 'audio/wav' });
|
|
154
|
+
// no vocabulary hints: OpenRouter accepts `prompt`, but ignores it
|
|
155
|
+
const res = await this.client.audio.transcriptions.create({
|
|
156
|
+
file,
|
|
157
|
+
model: this.model,
|
|
158
|
+
language: (lang || '').split('-')[0] || undefined,
|
|
159
|
+
});
|
|
160
|
+
return (res.text || '').trim();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
exports.OpenRouterStt = OpenRouterStt;
|
|
164
|
+
/** Text-to-speech over OpenRouter */
|
|
165
|
+
class OpenRouterTts {
|
|
166
|
+
client;
|
|
167
|
+
model;
|
|
168
|
+
voice;
|
|
169
|
+
decode;
|
|
170
|
+
fetchImpl;
|
|
171
|
+
/**
|
|
172
|
+
* @param apiKey OpenRouter key
|
|
173
|
+
* @param model model id ('' = {@link DEFAULT_OPENROUTER_TTS_MODEL})
|
|
174
|
+
* @param voice voice of the model ('' = the first voice of the model for the language)
|
|
175
|
+
* @param decode decoder for the models that only return mp3
|
|
176
|
+
* @param fetchImpl fetch for the model list (tests)
|
|
177
|
+
*/
|
|
178
|
+
constructor(apiKey, model = '', voice = '', decode, fetchImpl) {
|
|
179
|
+
this.client = client(apiKey);
|
|
180
|
+
this.model = model || exports.DEFAULT_OPENROUTER_TTS_MODEL;
|
|
181
|
+
this.voice = voice;
|
|
182
|
+
this.decode = decode;
|
|
183
|
+
this.fetchImpl = fetchImpl;
|
|
184
|
+
}
|
|
185
|
+
/** The configured voice, or the first one the model offers for the language — most models need a voice */
|
|
186
|
+
async voiceFor(lang) {
|
|
187
|
+
if (this.voice) {
|
|
188
|
+
return this.voice;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const models = await listOpenRouterSpeechModels('speech', this.fetchImpl);
|
|
192
|
+
const voices = models.find(m => m.id === this.model)?.voices || [];
|
|
193
|
+
return voicesForLanguage(voices, lang)[0] || '';
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return '';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async synthesize(text, lang) {
|
|
200
|
+
const mp3 = MP3_ONLY.some(prefix => this.model.startsWith(prefix));
|
|
201
|
+
if (mp3 && !this.decode) {
|
|
202
|
+
throw new Error(`${this.model} only returns mp3, which needs ffmpeg`);
|
|
203
|
+
}
|
|
204
|
+
const voice = await this.voiceFor(lang);
|
|
205
|
+
const res = await this.client.audio.speech.create({
|
|
206
|
+
model: this.model,
|
|
207
|
+
input: text,
|
|
208
|
+
// `voice` is required by the OpenAI types; an empty one is left out (a model with a default voice)
|
|
209
|
+
...(voice ? { voice } : {}),
|
|
210
|
+
response_format: mp3 ? 'mp3' : 'pcm',
|
|
211
|
+
});
|
|
212
|
+
const audio = Buffer.from(await res.arrayBuffer());
|
|
213
|
+
if (mp3) {
|
|
214
|
+
return this.decode(audio);
|
|
215
|
+
}
|
|
216
|
+
return pcmFromResponse(audio, res.headers.get('content-type') || '');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
exports.OpenRouterTts = OpenRouterTts;
|
|
220
|
+
//# sourceMappingURL=openrouter.js.map
|