@openclaw/inworld-speech 0.0.0 → 2026.6.9-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/README.md CHANGED
@@ -1,3 +1,12 @@
1
- # @openclaw/inworld-speech
1
+ # OpenClaw Inworld Plugin
2
2
 
3
- Reserved placeholder for the official OpenClaw plugin package. Use a published release version instead.
3
+ Official OpenClaw plugin for Inworld.
4
+
5
+ Install from OpenClaw:
6
+
7
+ ```bash
8
+ openclaw plugins install @openclaw/inworld-speech
9
+ openclaw gateway restart
10
+ ```
11
+
12
+ See <https://docs.openclaw.ai/providers/inworld> for setup and configuration.
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import { buildInworldSpeechProvider } from "./speech-provider.js";
2
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+ //#region extensions/inworld/index.ts
4
+ var inworld_default = definePluginEntry({
5
+ id: "inworld",
6
+ name: "Inworld Speech",
7
+ description: "Bundled Inworld speech provider",
8
+ register(api) {
9
+ api.registerSpeechProvider(buildInworldSpeechProvider());
10
+ }
11
+ });
12
+ //#endregion
13
+ export { inworld_default as default };
@@ -0,0 +1,166 @@
1
+ import { DEFAULT_INWORLD_MODEL_ID, INWORLD_TTS_MODELS, inworldTTS, listInworldVoices, normalizeInworldBaseUrl } from "./tts.js";
2
+ import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
3
+ import { asObject, parseSpeechDirectiveNumberOverride, trimToUndefined } from "openclaw/plugin-sdk/speech-core";
4
+ import { asFiniteNumberInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
5
+ //#region extensions/inworld/speech-provider.ts
6
+ function normalizeInworldTemperature(value) {
7
+ return asFiniteNumberInRange(value, {
8
+ min: 0,
9
+ minExclusive: true,
10
+ max: 2
11
+ });
12
+ }
13
+ function normalizeInworldProviderConfig(rawConfig) {
14
+ const raw = asObject(asObject(rawConfig.providers)?.inworld) ?? asObject(rawConfig.inworld);
15
+ return {
16
+ apiKey: normalizeResolvedSecretInputString({
17
+ value: raw?.apiKey,
18
+ path: "messages.tts.providers.inworld.apiKey"
19
+ }),
20
+ baseUrl: normalizeInworldBaseUrl(trimToUndefined(raw?.baseUrl)),
21
+ voiceId: trimToUndefined(raw?.voiceId) ?? "Sarah",
22
+ modelId: trimToUndefined(raw?.modelId) ?? "inworld-tts-1.5-max",
23
+ temperature: normalizeInworldTemperature(raw?.temperature)
24
+ };
25
+ }
26
+ function readInworldProviderConfig(config) {
27
+ const defaults = normalizeInworldProviderConfig({});
28
+ return {
29
+ apiKey: trimToUndefined(config.apiKey) ?? defaults.apiKey,
30
+ baseUrl: normalizeInworldBaseUrl(trimToUndefined(config.baseUrl) ?? defaults.baseUrl),
31
+ voiceId: trimToUndefined(config.voiceId) ?? defaults.voiceId,
32
+ modelId: trimToUndefined(config.modelId) ?? defaults.modelId,
33
+ temperature: normalizeInworldTemperature(config.temperature) ?? defaults.temperature
34
+ };
35
+ }
36
+ function readInworldOverrides(overrides) {
37
+ if (!overrides) return {};
38
+ return {
39
+ voiceId: trimToUndefined(overrides.voiceId ?? overrides.voice),
40
+ modelId: trimToUndefined(overrides.modelId ?? overrides.model),
41
+ temperature: normalizeInworldTemperature(overrides.temperature)
42
+ };
43
+ }
44
+ function parseDirectiveToken(ctx) {
45
+ switch (ctx.key) {
46
+ case "voice":
47
+ case "voiceid":
48
+ case "voice_id":
49
+ case "inworld_voice":
50
+ case "inworldvoice":
51
+ if (!ctx.policy.allowVoice) return { handled: true };
52
+ return {
53
+ handled: true,
54
+ overrides: { voiceId: ctx.value }
55
+ };
56
+ case "model":
57
+ case "modelid":
58
+ case "model_id":
59
+ case "inworld_model":
60
+ case "inworldmodel":
61
+ if (!ctx.policy.allowModelId) return { handled: true };
62
+ return {
63
+ handled: true,
64
+ overrides: { modelId: ctx.value }
65
+ };
66
+ case "temperature": return parseSpeechDirectiveNumberOverride({
67
+ ctx,
68
+ overrideKey: "temperature",
69
+ range: {
70
+ min: 0,
71
+ minExclusive: true,
72
+ max: 2
73
+ },
74
+ warning: (value) => `invalid Inworld temperature "${value}"`
75
+ });
76
+ default: return { handled: false };
77
+ }
78
+ }
79
+ function buildInworldSpeechProvider() {
80
+ return {
81
+ id: "inworld",
82
+ label: "Inworld",
83
+ autoSelectOrder: 30,
84
+ defaultModel: DEFAULT_INWORLD_MODEL_ID,
85
+ models: INWORLD_TTS_MODELS,
86
+ resolveConfig: ({ rawConfig }) => normalizeInworldProviderConfig(rawConfig),
87
+ parseDirectiveToken,
88
+ resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => {
89
+ const base = normalizeInworldProviderConfig(baseTtsConfig);
90
+ const resolvedApiKey = talkProviderConfig.apiKey === void 0 ? void 0 : normalizeResolvedSecretInputString({
91
+ value: talkProviderConfig.apiKey,
92
+ path: "talk.providers.inworld.apiKey"
93
+ });
94
+ return {
95
+ ...base,
96
+ ...resolvedApiKey === void 0 ? {} : { apiKey: resolvedApiKey },
97
+ ...trimToUndefined(talkProviderConfig.baseUrl) == null ? {} : { baseUrl: normalizeInworldBaseUrl(trimToUndefined(talkProviderConfig.baseUrl)) },
98
+ ...trimToUndefined(talkProviderConfig.voiceId) == null ? {} : { voiceId: trimToUndefined(talkProviderConfig.voiceId) },
99
+ ...trimToUndefined(talkProviderConfig.modelId) == null ? {} : { modelId: trimToUndefined(talkProviderConfig.modelId) },
100
+ ...normalizeInworldTemperature(talkProviderConfig.temperature) == null ? {} : { temperature: normalizeInworldTemperature(talkProviderConfig.temperature) }
101
+ };
102
+ },
103
+ resolveTalkOverrides: ({ params }) => ({
104
+ ...trimToUndefined(params.voiceId) == null ? {} : { voiceId: trimToUndefined(params.voiceId) },
105
+ ...trimToUndefined(params.modelId) == null ? {} : { modelId: trimToUndefined(params.modelId) },
106
+ ...normalizeInworldTemperature(params.temperature) == null ? {} : { temperature: normalizeInworldTemperature(params.temperature) }
107
+ }),
108
+ listVoices: async (req) => {
109
+ const config = req.providerConfig ? readInworldProviderConfig(req.providerConfig) : void 0;
110
+ const apiKey = req.apiKey || config?.apiKey || process.env.INWORLD_API_KEY;
111
+ if (!apiKey) throw new Error("Inworld API key missing");
112
+ return listInworldVoices({
113
+ apiKey,
114
+ baseUrl: req.baseUrl ?? config?.baseUrl
115
+ });
116
+ },
117
+ isConfigured: ({ providerConfig }) => Boolean(readInworldProviderConfig(providerConfig).apiKey || process.env.INWORLD_API_KEY),
118
+ synthesize: async (req) => {
119
+ const config = readInworldProviderConfig(req.providerConfig);
120
+ const overrides = readInworldOverrides(req.providerOverrides);
121
+ const apiKey = config.apiKey || process.env.INWORLD_API_KEY;
122
+ if (!apiKey) throw new Error("Inworld API key missing");
123
+ const useOpus = req.target === "voice-note";
124
+ const audioEncoding = useOpus ? "OGG_OPUS" : "MP3";
125
+ return {
126
+ audioBuffer: await inworldTTS({
127
+ text: req.text,
128
+ apiKey,
129
+ baseUrl: config.baseUrl,
130
+ voiceId: overrides.voiceId ?? config.voiceId,
131
+ modelId: overrides.modelId ?? config.modelId,
132
+ audioEncoding,
133
+ temperature: overrides.temperature ?? config.temperature,
134
+ timeoutMs: req.timeoutMs
135
+ }),
136
+ outputFormat: audioEncoding.toLowerCase(),
137
+ fileExtension: useOpus ? ".ogg" : ".mp3",
138
+ voiceCompatible: useOpus
139
+ };
140
+ },
141
+ synthesizeTelephony: async (req) => {
142
+ const config = readInworldProviderConfig(req.providerConfig);
143
+ const overrides = readInworldOverrides(req.providerOverrides);
144
+ const apiKey = config.apiKey || process.env.INWORLD_API_KEY;
145
+ if (!apiKey) throw new Error("Inworld API key missing");
146
+ const sampleRate = 22050;
147
+ return {
148
+ audioBuffer: await inworldTTS({
149
+ text: req.text,
150
+ apiKey,
151
+ baseUrl: config.baseUrl,
152
+ voiceId: overrides.voiceId ?? config.voiceId,
153
+ modelId: overrides.modelId ?? config.modelId,
154
+ audioEncoding: "PCM",
155
+ sampleRateHertz: sampleRate,
156
+ temperature: overrides.temperature ?? config.temperature,
157
+ timeoutMs: req.timeoutMs
158
+ }),
159
+ outputFormat: "pcm",
160
+ sampleRate
161
+ };
162
+ }
163
+ };
164
+ }
165
+ //#endregion
166
+ export { buildInworldSpeechProvider };
package/dist/tts.js ADDED
@@ -0,0 +1,111 @@
1
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
2
+ //#region extensions/inworld/tts.ts
3
+ const DEFAULT_INWORLD_BASE_URL = "https://api.inworld.ai";
4
+ const DEFAULT_INWORLD_VOICE_ID = "Sarah";
5
+ const DEFAULT_INWORLD_MODEL_ID = "inworld-tts-1.5-max";
6
+ const INWORLD_TTS_MODELS = [
7
+ "inworld-tts-1.5-max",
8
+ "inworld-tts-1.5-mini",
9
+ "inworld-tts-1-max",
10
+ "inworld-tts-1"
11
+ ];
12
+ function normalizeInworldBaseUrl(baseUrl) {
13
+ return (baseUrl?.trim())?.replace(/\/+$/, "") || DEFAULT_INWORLD_BASE_URL;
14
+ }
15
+ function ssrfPolicyFromInworldBaseUrl(baseUrl) {
16
+ try {
17
+ const parsed = new URL(baseUrl);
18
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
19
+ return { hostnameAllowlist: [parsed.hostname] };
20
+ } catch {
21
+ return;
22
+ }
23
+ }
24
+ /**
25
+ * Calls the Inworld streaming TTS endpoint and concatenates every audio chunk
26
+ * into a single buffer. The stream returns newline-delimited JSON, each line
27
+ * carrying base64 audio in `result.audioContent`.
28
+ */
29
+ async function inworldTTS(params) {
30
+ const baseUrl = normalizeInworldBaseUrl(params.baseUrl);
31
+ const url = `${baseUrl}/tts/v1/voice:stream`;
32
+ const requestBody = JSON.stringify({
33
+ text: params.text,
34
+ voiceId: params.voiceId ?? "Sarah",
35
+ modelId: params.modelId ?? "inworld-tts-1.5-max",
36
+ audioConfig: {
37
+ audioEncoding: params.audioEncoding ?? "MP3",
38
+ ...params.sampleRateHertz && { sampleRateHertz: params.sampleRateHertz }
39
+ },
40
+ ...params.temperature != null && { temperature: params.temperature }
41
+ });
42
+ const { response, release } = await fetchWithSsrFGuard({
43
+ url,
44
+ init: {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ Authorization: `Basic ${params.apiKey}`
49
+ },
50
+ body: requestBody
51
+ },
52
+ timeoutMs: params.timeoutMs,
53
+ policy: ssrfPolicyFromInworldBaseUrl(baseUrl),
54
+ auditContext: "inworld-tts"
55
+ });
56
+ try {
57
+ if (!response.ok) {
58
+ const errorBody = await response.text().catch(() => "");
59
+ throw new Error(`Inworld TTS API error (${response.status}): ${errorBody}`);
60
+ }
61
+ const body = await response.text();
62
+ const chunks = [];
63
+ for (const line of body.split("\n")) {
64
+ const trimmed = line.trim();
65
+ if (!trimmed) continue;
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(trimmed);
69
+ } catch {
70
+ throw new Error(`Inworld TTS stream parse error: unexpected non-JSON line: ${trimmed.slice(0, 80)}`);
71
+ }
72
+ if (parsed.error) throw new Error(`Inworld TTS stream error (${parsed.error.code}): ${parsed.error.message}`);
73
+ if (parsed.result?.audioContent) chunks.push(Buffer.from(parsed.result.audioContent, "base64"));
74
+ }
75
+ if (chunks.length === 0) throw new Error("Inworld TTS returned no audio data");
76
+ return Buffer.concat(chunks);
77
+ } finally {
78
+ await release();
79
+ }
80
+ }
81
+ async function listInworldVoices(params) {
82
+ const baseUrl = normalizeInworldBaseUrl(params.baseUrl);
83
+ const { response, release } = await fetchWithSsrFGuard({
84
+ url: `${baseUrl}/voices/v1/voices${params.language ? `?languages=${encodeURIComponent(params.language)}` : ""}`,
85
+ init: {
86
+ method: "GET",
87
+ headers: { Authorization: `Basic ${params.apiKey}` }
88
+ },
89
+ timeoutMs: params.timeoutMs,
90
+ policy: ssrfPolicyFromInworldBaseUrl(baseUrl),
91
+ auditContext: "inworld-voices"
92
+ });
93
+ try {
94
+ if (!response.ok) {
95
+ const errorBody = await response.text().catch(() => "");
96
+ throw new Error(`Inworld voices API error (${response.status}): ${errorBody}`);
97
+ }
98
+ const json = await response.json();
99
+ return Array.isArray(json.voices) ? json.voices.map((voice) => ({
100
+ id: voice.voiceId?.trim() ?? "",
101
+ name: voice.displayName?.trim() || void 0,
102
+ description: voice.description?.trim() || void 0,
103
+ locale: voice.langCode || void 0,
104
+ gender: voice.tags?.find((t) => t === "male" || t === "female") || void 0
105
+ })).filter((voice) => voice.id.length > 0) : [];
106
+ } finally {
107
+ await release();
108
+ }
109
+ }
110
+ //#endregion
111
+ export { DEFAULT_INWORLD_MODEL_ID, DEFAULT_INWORLD_VOICE_ID, INWORLD_TTS_MODELS, inworldTTS, listInworldVoices, normalizeInworldBaseUrl };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@openclaw/inworld-speech",
3
+ "version": "2026.6.9-beta.1",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@openclaw/inworld-speech",
9
+ "version": "2026.6.9-beta.1"
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "id": "inworld",
3
+ "activation": {
4
+ "onStartup": false
5
+ },
6
+ "enabledByDefault": true,
7
+ "name": "Inworld",
8
+ "description": "Inworld streaming text-to-speech (MP3, OGG_OPUS, PCM telephony).",
9
+ "setup": {
10
+ "providers": [
11
+ {
12
+ "id": "inworld",
13
+ "envVars": ["INWORLD_API_KEY"]
14
+ }
15
+ ]
16
+ },
17
+ "contracts": {
18
+ "speechProviders": ["inworld"]
19
+ },
20
+ "configSchema": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "properties": {
24
+ "apiKey": {
25
+ "type": "string",
26
+ "description": "Inworld API key. Must be the Base64 credential string from the Inworld dashboard (used as Authorization: Basic <apiKey>). Falls back to INWORLD_API_KEY env var."
27
+ },
28
+ "baseUrl": {
29
+ "type": "string",
30
+ "description": "Override Inworld API base URL (default https://api.inworld.ai)."
31
+ },
32
+ "voiceId": {
33
+ "type": "string",
34
+ "description": "Voice identifier (default Sarah)."
35
+ },
36
+ "modelId": {
37
+ "type": "string",
38
+ "description": "TTS model id (default inworld-tts-1.5-max)."
39
+ },
40
+ "temperature": {
41
+ "type": "number",
42
+ "minimum": 0,
43
+ "maximum": 2,
44
+ "description": "Sampling temperature 0..2."
45
+ }
46
+ }
47
+ }
48
+ }
package/package.json CHANGED
@@ -1,14 +1,50 @@
1
1
  {
2
2
  "name": "@openclaw/inworld-speech",
3
- "version": "0.0.0",
4
- "description": "Reserved package name for an official OpenClaw plugin.",
5
- "license": "MIT",
3
+ "version": "2026.6.9-beta.1",
4
+ "description": "OpenClaw Inworld speech plugin.",
6
5
  "repository": {
7
6
  "type": "git",
8
- "url": "git+https://github.com/openclaw/openclaw.git"
7
+ "url": "https://github.com/openclaw/openclaw"
9
8
  },
10
- "publishConfig": {
11
- "access": "public",
12
- "tag": "placeholder"
13
- }
9
+ "type": "module",
10
+ "openclaw": {
11
+ "extensions": [
12
+ "./index.ts"
13
+ ],
14
+ "install": {
15
+ "clawhubSpec": "clawhub:@openclaw/inworld-speech",
16
+ "npmSpec": "@openclaw/inworld-speech",
17
+ "defaultChoice": "npm",
18
+ "minHostVersion": ">=2026.6.8"
19
+ },
20
+ "compat": {
21
+ "pluginApi": ">=2026.6.9-beta.1"
22
+ },
23
+ "build": {
24
+ "openclawVersion": "2026.6.9-beta.1",
25
+ "bundledDist": false
26
+ },
27
+ "release": {
28
+ "publishToClawHub": true,
29
+ "publishToNpm": true
30
+ },
31
+ "runtimeExtensions": [
32
+ "./dist/index.js"
33
+ ]
34
+ },
35
+ "files": [
36
+ "dist/**",
37
+ "openclaw.plugin.json",
38
+ "npm-shrinkwrap.json",
39
+ "README.md"
40
+ ],
41
+ "peerDependencies": {
42
+ "openclaw": ">=2026.6.9-beta.1"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "openclaw": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "bundledDependencies": []
14
50
  }