@inetafrica/open-claudia 3.1.7 → 3.1.8
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/CHANGELOG.md +7 -0
- package/bin/cli.js +19 -0
- package/core/config.js +3 -1
- package/core/media.js +10 -1
- package/core/system-prompt.js +6 -2
- package/package.json +1 -1
- package/test-provider-language.js +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.1.8 — Hosted pods can actually send voice notes
|
|
4
|
+
|
|
5
|
+
- **FFMPEG is discovered on PATH.** Provisioned pods ship `/usr/bin/ffmpeg` but never set `FFMPEG=` in config, and `config.js` read only the explicit setting — so `FFMPEG` resolved empty and every TTS route (`pocketToVoice`, ElevenLabs, `say`) silently returned null: voice input got text-only replies. `FFMPEG` now falls back to PATH discovery via the existing `executableCandidate` helper; an explicit `FFMPEG=` still wins.
|
|
6
|
+
- **The system prompt's voice-notes flag reflects real TTS capability.** It gated on `WHISPER_CLI && FFMPEG` — the pre-gateway local-Whisper world — so whisper-less hosted pods told the model "Voice notes: disabled" and it refused user requests (the smart-euvy report), even with working gateway speech. New `media.canSpeak()` mirrors the actual pocket → ElevenLabs → macOS `say` fallback chain and now drives the flag; local Whisper is irrelevant to speaking.
|
|
7
|
+
- **New `open-claudia say "<text>"` CLI verb.** `send-voice` needs a ready-made ogg and the model had no way to synthesize one on request. `say` runs `textToVoice` and sends the result through the loopback voice path (same egress guardrail), and is documented in the Delivery prompt block.
|
|
8
|
+
- Regression coverage in `test-media-speech.js`: a whisper-less pod shape must report `canSpeak() === true`, the prompt gate must key on it, and FFMPEG resolution precedence (explicit > PATH discovery > empty) is asserted via subprocess config loads.
|
|
9
|
+
|
|
3
10
|
## v3.1.7 — CI spawn-race hardening (first published build of the v3.1.5 changes)
|
|
4
11
|
|
|
5
12
|
- v3.1.6 also never published. The loaded shared CI runner (node cold-boot 100-370ms vs ~50ms locally) keeps picking off the next-tightest spawn-timing test: this time `test-utility-provider-policy`'s shutdown-drain case, which flat-slept 100ms then cancelled — killing the fake agent before it booted, so its SIGTERM handler never wrote the expected signal capture (pipeline 10383; the v3.1.6 run-lifecycle fix itself passed there).
|
package/bin/cli.js
CHANGED
|
@@ -277,6 +277,24 @@ switch (command) {
|
|
|
277
277
|
break;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
case "say": {
|
|
281
|
+
const text = args.slice(1).join(" ").trim();
|
|
282
|
+
if (!text) {
|
|
283
|
+
console.error('Usage: open-claudia say "<text>"');
|
|
284
|
+
process.exit(2);
|
|
285
|
+
}
|
|
286
|
+
(async () => {
|
|
287
|
+
const { textToVoice } = require(path.join(botDir, "core", "media"));
|
|
288
|
+
const voicePath = await textToVoice(text);
|
|
289
|
+
if (!voicePath) {
|
|
290
|
+
console.error("No TTS route available — configure the speech gateway, ElevenLabs, or run on macOS.");
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
sendViaLoopback("send-voice", [voicePath]);
|
|
294
|
+
})();
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
|
|
280
298
|
case "tw":
|
|
281
299
|
case "transcript-window": {
|
|
282
300
|
require("./transcript-window").run(args.slice(1));
|
|
@@ -441,6 +459,7 @@ Send tools (only work inside an active bot-spawned task):
|
|
|
441
459
|
open-claudia send-file <path> [caption]
|
|
442
460
|
open-claudia send-photo <path> [caption]
|
|
443
461
|
open-claudia send-voice <path>
|
|
462
|
+
open-claudia say "<text>" Synthesize the text (TTS) and send it as a voice note
|
|
444
463
|
|
|
445
464
|
Memory tools:
|
|
446
465
|
open-claudia transcript-search <query> Ranked FTS5 search over indexed transcripts
|
package/core/config.js
CHANGED
|
@@ -137,7 +137,9 @@ const TRANSCRIPT_MAX_ENTRY_CHARS = parseInt(config.TRANSCRIPT_MAX_ENTRY_CHARS ||
|
|
|
137
137
|
const TRANSCRIPTS_DIR = config.TRANSCRIPTS_DIR || process.env.TRANSCRIPTS_DIR || path.join(CONFIG_DIR, "transcripts");
|
|
138
138
|
const WHISPER_CLI = config.WHISPER_CLI || "";
|
|
139
139
|
const WHISPER_MODEL = config.WHISPER_MODEL || "";
|
|
140
|
-
|
|
140
|
+
// Hosted pods ship /usr/bin/ffmpeg but never set FFMPEG=, so without PATH
|
|
141
|
+
// discovery every TTS route silently no-ops (smart-euvy "voice notes disabled").
|
|
142
|
+
const FFMPEG = config.FFMPEG || executableCandidate("ffmpeg") || "";
|
|
141
143
|
const ELEVENLABS_API_KEY = config.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY || "";
|
|
142
144
|
const ELEVENLABS_VOICE_ID = config.ELEVENLABS_VOICE_ID || process.env.ELEVENLABS_VOICE_ID || "EXAVITQu4vr4xnSDxMaL";
|
|
143
145
|
const ELEVENLABS_MODEL = config.ELEVENLABS_MODEL || process.env.ELEVENLABS_MODEL || "eleven_v3";
|
package/core/media.js
CHANGED
|
@@ -129,6 +129,15 @@ async function pocketToVoice(clean) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// True when at least one textToVoice route can produce audio. Mirrors the
|
|
133
|
+
// pocket -> ElevenLabs -> say fallback chain; local Whisper is deliberately
|
|
134
|
+
// irrelevant here (hosted pods speak via the gateway without it).
|
|
135
|
+
function canSpeak() {
|
|
136
|
+
if (!FFMPEG) return false;
|
|
137
|
+
if (TTS_PROVIDER !== "elevenlabs" && speechBearerToken() && SPEECH_SERVICE_URL) return true;
|
|
138
|
+
return Boolean(ELEVENLABS_API_KEY || TTS_CMD);
|
|
139
|
+
}
|
|
140
|
+
|
|
132
141
|
// Natural voice via ElevenLabs, falling back to macOS `say` only on no-key/error.
|
|
133
142
|
async function textToVoice(text) {
|
|
134
143
|
const clean = cleanForTTS(text);
|
|
@@ -194,4 +203,4 @@ async function synthSentenceMp3(text) {
|
|
|
194
203
|
}
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
module.exports = { transcribeAudio, transcribeAudioLocal, textToVoice, splitSentences, synthSentenceMp3, pocketToVoice, speechBearerToken, TTS_CMD };
|
|
206
|
+
module.exports = { transcribeAudio, transcribeAudioLocal, textToVoice, splitSentences, synthSentenceMp3, pocketToVoice, speechBearerToken, canSpeak, TTS_CMD };
|
package/core/system-prompt.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
-
const { SOUL_FILE, CRONS_FILE, VAULT_FILE, FILES_DIR, BOT_DIR,
|
|
6
|
+
const { SOUL_FILE, CRONS_FILE, VAULT_FILE, FILES_DIR, BOT_DIR, config } = require("./config");
|
|
7
|
+
const { canSpeak } = require("./media");
|
|
7
8
|
const CONFIG_DIR = require("../config-dir");
|
|
8
9
|
const { currentState, getProjectKey, getProviderSession } = require("./state");
|
|
9
10
|
const { currentAdapter, currentChannelId, currentUserId } = require("./context");
|
|
@@ -198,7 +199,9 @@ ${listing}`;
|
|
|
198
199
|
|
|
199
200
|
function buildCoreInstructionsUncached() {
|
|
200
201
|
const soul = loadSoul();
|
|
201
|
-
|
|
202
|
+
// Gate on actual TTS capability, not local Whisper: hosted pods hear and
|
|
203
|
+
// speak through the speech gateway with no whisper binary installed.
|
|
204
|
+
const hasVoice = canSpeak();
|
|
202
205
|
return `
|
|
203
206
|
${soul}
|
|
204
207
|
${buildPersonaBlock()}
|
|
@@ -230,6 +233,7 @@ Reply normally in your final answer. To send a file, image, or voice clip back t
|
|
|
230
233
|
- \`open-claudia send-file <path> [caption]\` — any document/binary
|
|
231
234
|
- \`open-claudia send-photo <path> [caption]\` — image with inline preview
|
|
232
235
|
- \`open-claudia send-voice <path>\` — ogg/opus voice note
|
|
236
|
+
- \`open-claudia say "<text>"\` — synthesize the text and send it as a voice note (use this when asked for a voice reply)
|
|
233
237
|
Never print or embed bot tokens in prompts, commands, logs, or messages.
|
|
234
238
|
|
|
235
239
|
## Background work
|
package/package.json
CHANGED
|
@@ -179,7 +179,14 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
179
179
|
// v3.1.6 run-lifecycle fix itself passed there). Audited all 13 fake-agent
|
|
180
180
|
// test files and hardened the three racy ones (utility-policy, enforcer,
|
|
181
181
|
// subagent): wait on boot captures / generous deadlines, never tight sleeps.
|
|
182
|
-
|
|
182
|
+
// 3.1.8 approved by Sumeet 2026-07-21 ("Yeh"): pod voice-note fix. Pods ship
|
|
183
|
+
// /usr/bin/ffmpeg but never set FFMPEG=, so every TTS route silently no-opped,
|
|
184
|
+
// and the system-prompt voice flag still gated on local Whisper — hosted bots
|
|
185
|
+
// told users voice notes were disabled despite a working speech gateway.
|
|
186
|
+
// Fix: FFMPEG PATH discovery in config, media.canSpeak() drives the prompt
|
|
187
|
+
// flag, and a new `open-claudia say "<text>"` verb synthesizes + sends a
|
|
188
|
+
// voice note on request.
|
|
189
|
+
assert.strictEqual(pkg.version, "3.1.8", "release version must remain unchanged without explicit approval");
|
|
183
190
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
184
191
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
185
192
|
}
|