@openclaw/inworld-speech 2026.6.11 → 2026.7.1-beta.1
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/dist/tts.js +60 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +4 -4
package/dist/tts.js
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
|
+
import { MAX_AUDIO_BYTES } from "openclaw/plugin-sdk/media-runtime";
|
|
2
|
+
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
1
3
|
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
2
4
|
//#region extensions/inworld/tts.ts
|
|
3
5
|
const DEFAULT_INWORLD_BASE_URL = "https://api.inworld.ai";
|
|
4
6
|
const DEFAULT_INWORLD_VOICE_ID = "Sarah";
|
|
5
7
|
const DEFAULT_INWORLD_MODEL_ID = "inworld-tts-1.5-max";
|
|
8
|
+
const INWORLD_TTS_BODY_MAX_BYTES = MAX_AUDIO_BYTES * 2;
|
|
9
|
+
const INWORLD_VOICES_BODY_MAX_BYTES = MAX_AUDIO_BYTES;
|
|
10
|
+
const INWORLD_BODY_READ_IDLE_TIMEOUT_MS = 3e4;
|
|
11
|
+
const INWORLD_ERROR_BODY_MAX_BYTES = 8 * 1024;
|
|
12
|
+
const INWORLD_ERROR_BODY_MAX_CHARS = 400;
|
|
13
|
+
const INWORLD_ERROR_BODY_READ_IDLE_TIMEOUT_MS = 1e4;
|
|
14
|
+
var InworldErrorBodyOverflow = class extends Error {};
|
|
15
|
+
/**
|
|
16
|
+
* Reads a bounded, whitespace-collapsed diagnostic snippet from a non-OK
|
|
17
|
+
* response body. A misbehaving or hostile endpoint can stream an arbitrarily
|
|
18
|
+
* large error body, so this never buffers it whole: it reuses the shared
|
|
19
|
+
* `readResponseWithLimit` reader (which cancels the underlying stream on
|
|
20
|
+
* overflow and enforces an idle timeout) with a small cap. On overflow it
|
|
21
|
+
* returns a fixed marker instead of echoing attacker-controlled bytes into the
|
|
22
|
+
* thrown error. Kept local to this extension so it depends only on the
|
|
23
|
+
* already-exported `response-limit-runtime` entry and adds no shared plugin-SDK
|
|
24
|
+
* surface.
|
|
25
|
+
*/
|
|
26
|
+
async function readInworldErrorBodySnippet(response) {
|
|
27
|
+
let buffer;
|
|
28
|
+
try {
|
|
29
|
+
buffer = await readResponseWithLimit(response, INWORLD_ERROR_BODY_MAX_BYTES, {
|
|
30
|
+
chunkTimeoutMs: INWORLD_ERROR_BODY_READ_IDLE_TIMEOUT_MS,
|
|
31
|
+
onOverflow: () => new InworldErrorBodyOverflow()
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return error instanceof InworldErrorBodyOverflow ? "(error body exceeded diagnostic limit; truncated)" : "";
|
|
35
|
+
}
|
|
36
|
+
const collapsed = buffer.toString("utf8").replace(/\s+/g, " ").trim();
|
|
37
|
+
if (collapsed.length > INWORLD_ERROR_BODY_MAX_CHARS) return `${collapsed.slice(0, INWORLD_ERROR_BODY_MAX_CHARS)}…`;
|
|
38
|
+
return collapsed;
|
|
39
|
+
}
|
|
6
40
|
const INWORLD_TTS_MODELS = [
|
|
7
41
|
"inworld-tts-1.5-max",
|
|
8
42
|
"inworld-tts-1.5-mini",
|
|
@@ -55,11 +89,16 @@ async function inworldTTS(params) {
|
|
|
55
89
|
});
|
|
56
90
|
try {
|
|
57
91
|
if (!response.ok) {
|
|
58
|
-
const errorBody = await response
|
|
92
|
+
const errorBody = await readInworldErrorBodySnippet(response);
|
|
59
93
|
throw new Error(`Inworld TTS API error (${response.status}): ${errorBody}`);
|
|
60
94
|
}
|
|
61
|
-
const body = await response
|
|
95
|
+
const body = (await readResponseWithLimit(response, INWORLD_TTS_BODY_MAX_BYTES, {
|
|
96
|
+
chunkTimeoutMs: INWORLD_BODY_READ_IDLE_TIMEOUT_MS,
|
|
97
|
+
onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`Inworld TTS audio stream too large: ${size} bytes (limit: ${maxBytes} bytes)`),
|
|
98
|
+
onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`Inworld TTS audio stream stalled: no data received for ${chunkTimeoutMs}ms`)
|
|
99
|
+
})).toString("utf8");
|
|
62
100
|
const chunks = [];
|
|
101
|
+
let decodedAudioBytes = 0;
|
|
63
102
|
for (const line of body.split("\n")) {
|
|
64
103
|
const trimmed = line.trim();
|
|
65
104
|
if (!trimmed) continue;
|
|
@@ -70,7 +109,13 @@ async function inworldTTS(params) {
|
|
|
70
109
|
throw new Error(`Inworld TTS stream parse error: unexpected non-JSON line: ${trimmed.slice(0, 80)}`);
|
|
71
110
|
}
|
|
72
111
|
if (parsed.error) throw new Error(`Inworld TTS stream error (${parsed.error.code}): ${parsed.error.message}`);
|
|
73
|
-
if (parsed.result?.audioContent)
|
|
112
|
+
if (parsed.result?.audioContent) {
|
|
113
|
+
const chunk = Buffer.from(parsed.result.audioContent, "base64");
|
|
114
|
+
const nextDecodedAudioBytes = decodedAudioBytes + chunk.length;
|
|
115
|
+
if (nextDecodedAudioBytes > MAX_AUDIO_BYTES) throw new Error(`Inworld TTS decoded audio too large: ${nextDecodedAudioBytes} bytes (limit: ${MAX_AUDIO_BYTES} bytes)`);
|
|
116
|
+
decodedAudioBytes = nextDecodedAudioBytes;
|
|
117
|
+
chunks.push(chunk);
|
|
118
|
+
}
|
|
74
119
|
}
|
|
75
120
|
if (chunks.length === 0) throw new Error("Inworld TTS returned no audio data");
|
|
76
121
|
return Buffer.concat(chunks);
|
|
@@ -92,10 +137,20 @@ async function listInworldVoices(params) {
|
|
|
92
137
|
});
|
|
93
138
|
try {
|
|
94
139
|
if (!response.ok) {
|
|
95
|
-
const errorBody = await response
|
|
140
|
+
const errorBody = await readInworldErrorBodySnippet(response);
|
|
96
141
|
throw new Error(`Inworld voices API error (${response.status}): ${errorBody}`);
|
|
97
142
|
}
|
|
98
|
-
const
|
|
143
|
+
const voicesBody = (await readResponseWithLimit(response, INWORLD_VOICES_BODY_MAX_BYTES, {
|
|
144
|
+
chunkTimeoutMs: INWORLD_BODY_READ_IDLE_TIMEOUT_MS,
|
|
145
|
+
onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`Inworld voices response too large: ${size} bytes (limit: ${maxBytes} bytes)`),
|
|
146
|
+
onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`Inworld voices response stalled: no data received for ${chunkTimeoutMs}ms`)
|
|
147
|
+
})).toString("utf8");
|
|
148
|
+
let json;
|
|
149
|
+
try {
|
|
150
|
+
json = JSON.parse(voicesBody);
|
|
151
|
+
} catch {
|
|
152
|
+
throw new Error("Inworld voices API returned malformed JSON");
|
|
153
|
+
}
|
|
99
154
|
return Array.isArray(json.voices) ? json.voices.map((voice) => ({
|
|
100
155
|
id: voice.voiceId?.trim() ?? "",
|
|
101
156
|
name: voice.displayName?.trim() || void 0,
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/inworld-speech",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.7.1-beta.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/inworld-speech",
|
|
9
|
-
"version": "2026.
|
|
9
|
+
"version": "2026.7.1-beta.1"
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/inworld-speech",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.7.1-beta.1",
|
|
4
4
|
"description": "OpenClaw Inworld speech plugin.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
"minHostVersion": ">=2026.6.8"
|
|
19
19
|
},
|
|
20
20
|
"compat": {
|
|
21
|
-
"pluginApi": ">=2026.
|
|
21
|
+
"pluginApi": ">=2026.7.1-beta.1"
|
|
22
22
|
},
|
|
23
23
|
"build": {
|
|
24
|
-
"openclawVersion": "2026.
|
|
24
|
+
"openclawVersion": "2026.7.1-beta.1",
|
|
25
25
|
"bundledDist": false
|
|
26
26
|
},
|
|
27
27
|
"release": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"README.md"
|
|
40
40
|
],
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"openclaw": ">=2026.
|
|
42
|
+
"openclaw": ">=2026.7.1-beta.1"
|
|
43
43
|
},
|
|
44
44
|
"peerDependenciesMeta": {
|
|
45
45
|
"openclaw": {
|