diff --git a/.daemon/logs/daemon.out.log b/.daemon/logs/daemon.out.log index dad8dc4c8..b7ca0e6e2 100644 --- a/.daemon/logs/daemon.out.log +++ b/.daemon/logs/daemon.out.log @@ -23638,3 +23638,28 @@ hint: See the 'Note about fast-forwards' in 'git push --help' for details. 23:27:43 INFO  [memory] Chunked memory saved {"groupId":"84bcb13d-3767-44a0-a967-1d38f1ceb3bd","chunkCount":0} 23:27:43 INFO  [watcher] Ingested memory file {"path":"/home/nicholai/.agents/memory/2026-02-24-ad-prediction-system-proposal-review.md","chunks":1,"sections":1,"filename":"2026-02-24-ad-prediction-system-proposal-review"} 23:27:43 INFO  [daemon] Imported existing memory files {"files":142,"chunks":320} +23:27:48 INFO  [git] Auto-committed {"message":"2026-02-25T23-27-48_auto_memory/memories.db-wal, memory/memories.db-wal, me","filesChanged":7} +23:28:16 INFO  [secrets] exec_with_secrets completed {"name":"OPENROUTER_API_KEY","code":0} +23:28:24 INFO  [secrets] exec_with_secrets completed {"name":"OPENROUTER_API_KEY","code":0} +23:28:49 INFO  [secrets] exec_with_secrets completed {"name":"OPENROUTER_API_KEY","code":0} +23:29:21 INFO  [secrets] Secret deleted {"name":"OPENROUTER_API_KEY"} +23:29:22 INFO  [secrets] Secret stored {"name":"OPENROUTER_API_KEY"} +23:29:42 INFO  [secrets] exec_with_secrets completed {"name":"OPENROUTER_API_KEY","code":0} +23:30:34 INFO  [secrets] exec_with_secrets completed {"name":"OPENROUTER_API_KEY","code":0} +23:30:56 INFO  [secrets] exec_with_secrets completed {"name":"OPENROUTER_API_KEY","code":0} +23:32:51 INFO  [git] Git push {"commits":789} +23:33:03 INFO  [skills] Fetching skills.sh catalog +23:33:03 INFO  [skills] Fetching ClawHub catalog +23:33:03 INFO  [skills] Cached 600 skills +23:33:04 ERROR [skills] ClawHub catalog fetch failed + Error: ClawHub returned 429 +23:37:42 INFO  [git] Git push {"commits":789} +23:42:42 INFO  [git] Git push {"commits":789} +23:47:42 INFO  [git] Git push {"commits":789} +23:52:42 INFO  [git] Git push {"commits":789} +23:57:42 INFO  [git] Git push {"commits":789} +00:00:13 INFO  [scheduler] Executing task: Find a bug and fix it {"taskId":"51f5d597-5c2d-4282-bad1-ac9010862650","runId":"47c819d5-bc20-46de-9c09-afe0ec264eec","harness":"claude-code"} +00:00:13 INFO  [scheduler] Spawning claude-code {"bin":"/home/nicholai/.local/share/../bin/claude","cwd":"/home/nicholai/signet/signetai/"} +00:00:13 INFO  [watcher] File changed {"path":"/home/nicholai/.agents/memory/memories.db-wal"} +00:00:13 INFO  [scheduler] Task Find a bug and fix it failed {"taskId":"51f5d597-5c2d-4282-bad1-ac9010862650","runId":"47c819d5-bc20-46de-9c09-afe0ec264eec","exitCode":1,"timedOut":false} +00:00:13 INFO  [watcher] File changed {"path":"/home/nicholai/.agents/memory/memories.db-wal"} diff --git a/.secrets/secrets.enc b/.secrets/secrets.enc index 2e6c37b18..a71ed4056 100644 --- a/.secrets/secrets.enc +++ b/.secrets/secrets.enc @@ -1,11 +1,6 @@ { "version": 1, "secrets": { - "OPENROUTER_API_KEY": { - "ciphertext": "WgC40fXcR4Z/fR+zMeVH8Rl8U5y/BokoGaLeI9Ak4DKoqMS5GYvJ0C1HHw==", - "created": "2026-02-18T21:15:08.588Z", - "updated": "2026-02-18T21:15:08.588Z" - }, "GOOGLE_AI_API_KEY": { "ciphertext": "1cyAwWgmCqZxQpVGTvPCyyv1C6mt/8YzPfFrTK1crKE2lupmnhCInm0d4PwJeBMAPF6XQbrJZgv4aMZT9utO70+hSkXIZcKPnA4iZYcRew==", "created": "2026-02-21T07:40:25.875Z", @@ -45,6 +40,11 @@ "ciphertext": "xN8VWOFvMNgmUuCkM44MRFsNf9atDvCvJm6qBis5+FBpJrMlRR9JpmkhSSDd6SZx11/N336IUXQM8LcxtprBiSk6GekDk5COmk3qBin+ry4rF+RZ2NQ2mj6WTnxzxfMgtaK9Gne482zlftCs7CzLLmKcyCW6vrpZzNI8t9CEeThhMJnNcVKbTQHtk/9WD+10bs+jacZe7vsM8RLRsMOI2cUJdiKRCO6b7/o8hzPuCmziUaR54jO0bxdlFBHzJH8hiURlNdZ3ZwQBiWIEcDK15L5FrJSl4hu2oAMCDHTBb5mDzl35fZsH6SPzWhA4FSlvE7SDytOcPQ==", "created": "2026-02-24T11:00:59.366Z", "updated": "2026-02-24T11:05:51.951Z" + }, + "OPENROUTER_API_KEY": { + "ciphertext": "yCu2nwjkBU+jCYRy9pKDX//fHjlQPaJiWFG5a9sCRXCzGki4oa+2nULx1nzCrHHBaNCne/sXew2x1XhV3No0aVux4mW2kSwcnMzbu6DJM61ENWA9ZJi5R18xVOeQAl9Bs2uI9VuhIvH9Sr9IJWDX91c=", + "created": "2026-02-25T23:29:22.944Z", + "updated": "2026-02-25T23:29:22.944Z" } } } \ No newline at end of file diff --git a/memory/memories.db-shm b/memory/memories.db-shm index f64a7c713..a99db276b 100644 Binary files a/memory/memories.db-shm and b/memory/memories.db-shm differ diff --git a/memory/memories.db-wal b/memory/memories.db-wal index 6b52f1b58..efa1f161a 100644 Binary files a/memory/memories.db-wal and b/memory/memories.db-wal differ diff --git a/scripts/speak.sh b/scripts/speak.sh index 5da8a5a0a..8b56b314a 100755 --- a/scripts/speak.sh +++ b/scripts/speak.sh @@ -1,40 +1,94 @@ #!/usr/bin/env bash set -euo pipefail -API_KEY=$(signet secret get OPENROUTER_API_KEY 2>/dev/null) VOICE="${VOICE:-ash}" -FORMAT="${FORMAT:-wav}" TEXT="$*" +DAEMON="http://localhost:3850" +TMPRAW=$(mktemp /tmp/speak-XXXX.raw) +TMPWAV=$(mktemp /tmp/speak-XXXX.wav) +trap "rm -f $TMPRAW $TMPWAV" EXIT if [ -z "$TEXT" ]; then echo "Usage: speak.sh " exit 1 fi -RESPONSE=$(curl -s https://openrouter.ai/api/v1/chat/completions \ - -H "Authorization: Bearer $API_KEY" \ +# The inner command streams the SSE response, extracts base64 audio chunks, +# concatenates and decodes them to raw PCM16. +# We use jq to safely embed the text into the JSON payload. +read -r -d '' INNER_CMD << 'INNEREOF' || true +PAYLOAD=$(jq -n \ + --arg text "$SPEAK_TEXT" \ + --arg voice "$SPEAK_VOICE" \ + '{ + model: "openai/gpt-audio-mini", + modalities: ["text", "audio"], + audio: { voice: $voice, format: "pcm16" }, + stream: true, + messages: [{ role: "user", content: $text }] + }') + +curl -sN https://openrouter.ai/api/v1/chat/completions \ + -H "Authorization: Bearer $OPENROUTER_API_KEY" \ -H "Content-Type: application/json" \ - -d "$(jq -n \ - --arg text "$TEXT" \ - --arg voice "$VOICE" \ - --arg fmt "$FORMAT" \ - '{ - model: "openai/gpt-audio-mini", - modalities: ["text", "audio"], - audio: { voice: $voice, format: $fmt }, - messages: [{ role: "user", content: $text }] - }')") + -d "$PAYLOAD" | \ +while IFS= read -r line; do + # Strip "data: " prefix from SSE + line="${line#data: }" + [ -z "$line" ] && continue + [ "$line" = "[DONE]" ] && continue + # Extract audio data chunk if present + chunk=$(echo "$line" | jq -r '.choices[0].delta.audio.data // empty' 2>/dev/null) + [ -n "$chunk" ] && printf '%s' "$chunk" +done +INNEREOF -# Extract audio data -AUDIO_DATA=$(echo "$RESPONSE" | jq -r '.choices[0].message.audio.data // empty') +# Execute the streaming command via daemon's exec_with_secrets. +# We pass SPEAK_TEXT and SPEAK_VOICE as additional env vars through the secrets map. +# The exec endpoint injects OPENROUTER_API_KEY; we also set our custom vars in the command. +INNER_WITH_VARS="export SPEAK_TEXT=$(printf '%q' "$TEXT"); export SPEAK_VOICE=$(printf '%q' "$VOICE"); $INNER_CMD" -if [ -z "$AUDIO_DATA" ]; then - echo "Error: No audio in response" - echo "$RESPONSE" | jq '.error // .choices[0].message.content // .' 2>/dev/null +EXEC_RESPONSE=$(curl -s "$DAEMON/api/secrets/OPENROUTER_API_KEY/exec" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg cmd "$INNER_WITH_VARS" '{ command: $cmd }')") + +# Extract the concatenated base64 audio from stdout +B64_AUDIO=$(echo "$EXEC_RESPONSE" | jq -r '.stdout // empty') + +if [ -z "$B64_AUDIO" ]; then + STDERR=$(echo "$EXEC_RESPONSE" | jq -r '.stderr // empty') + echo "Error: No audio data received" + [ -n "$STDERR" ] && echo "stderr: $STDERR" exit 1 fi -TMPFILE=$(mktemp /tmp/speak-XXXX.wav) -trap "rm -f $TMPFILE" EXIT -echo "$AUDIO_DATA" | base64 -d > "$TMPFILE" -ffplay -nodisp -autoexit -loglevel quiet "$TMPFILE" +# Decode base64 to raw PCM16 +echo "$B64_AUDIO" | base64 -d > "$TMPRAW" 2>/dev/null + +RAWSIZE=$(stat -c%s "$TMPRAW" 2>/dev/null || stat -f%z "$TMPRAW" 2>/dev/null) +if [ "$RAWSIZE" -lt 100 ]; then + echo "Error: Audio data too small ($RAWSIZE bytes)" + exit 1 +fi + +# Wrap raw PCM16 in a WAV header (24kHz, mono, 16-bit LE) +# WAV header is 44 bytes +DATASIZE=$RAWSIZE +FILESIZE=$((DATASIZE + 36)) +{ + printf 'RIFF' + printf "$(printf '\\x%02x\\x%02x\\x%02x\\x%02x' $((FILESIZE & 0xFF)) $(((FILESIZE >> 8) & 0xFF)) $(((FILESIZE >> 16) & 0xFF)) $(((FILESIZE >> 24) & 0xFF)))" + printf 'WAVEfmt ' + printf '\x10\x00\x00\x00' # chunk size 16 + printf '\x01\x00' # PCM format + printf '\x01\x00' # mono + printf '\xc0\x5d\x00\x00' # 24000 Hz sample rate + printf '\x80\xbb\x00\x00' # byte rate (24000 * 2) + printf '\x02\x00' # block align + printf '\x10\x00' # 16 bits per sample + printf 'data' + printf "$(printf '\\x%02x\\x%02x\\x%02x\\x%02x' $((DATASIZE & 0xFF)) $(((DATASIZE >> 8) & 0xFF)) $(((DATASIZE >> 16) & 0xFF)) $(((DATASIZE >> 24) & 0xFF)))" + cat "$TMPRAW" +} > "$TMPWAV" + +ffplay -nodisp -autoexit -loglevel quiet "$TMPWAV" diff --git a/signet-speak.skill b/signet-speak.skill new file mode 100644 index 000000000..425989cb5 Binary files /dev/null and b/signet-speak.skill differ diff --git a/skills/signet-speak/SKILL.md b/skills/signet-speak/SKILL.md new file mode 100644 index 000000000..52a994faf --- /dev/null +++ b/skills/signet-speak/SKILL.md @@ -0,0 +1,36 @@ +--- +name: signet-speak +description: Speak audibly to the user via text-to-speech using OpenAI's gpt-audio-mini model through OpenRouter. Use when the agent wants to talk out loud, greet the user verbally, deliver information audibly, or when spoken audio output would enhance the interaction. Also triggers when the user asks the agent to "say something", "speak", "talk to me", "use your voice", or "read this aloud". +--- + +# Signet Speak + +Speak out loud via TTS. Uses OpenRouter's `openai/gpt-audio-mini` with Signet daemon's `exec_with_secrets` for secure API key injection. + +## Quick Start + +```bash +~/.agents/skills/signet-speak/scripts/speak.sh "whatever you want to say" +``` + +Override voice (default: ash): + +```bash +VOICE=coral ~/.agents/skills/signet-speak/scripts/speak.sh "hello there" +``` + +## Voices + +alloy, **ash** (default), ballad, coral, echo, fable, onyx, nova, sage, shimmer, verse, marin, cedar + +## Prerequisites + +- Signet daemon running (`signet daemon start`) +- `OPENROUTER_API_KEY` in Signet secrets +- `ffplay`, `jq`, `curl` on PATH + +## Troubleshooting + +- **No audio data**: Check daemon (`signet status`), verify API key +- **401 error**: Re-store key with `signet secret put OPENROUTER_API_KEY` +- **No sound from speakers**: Test with `ffplay -f lavfi -i sine=f=440:d=1 -nodisp -autoexit` diff --git a/skills/signet-speak/scripts/speak.sh b/skills/signet-speak/scripts/speak.sh new file mode 100755 index 000000000..fa9cdecd9 --- /dev/null +++ b/skills/signet-speak/scripts/speak.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +set -euo pipefail + +VOICE="${VOICE:-ash}" +TEXT="$*" +DAEMON="http://localhost:3850" +TMPRAW=$(mktemp /tmp/speak-XXXX.raw) +TMPWAV=$(mktemp /tmp/speak-XXXX.wav) +trap "rm -f $TMPRAW $TMPWAV" EXIT + +if [ -z "$TEXT" ]; then + echo "Usage: speak.sh " + exit 1 +fi + +# Build the inner command that runs with OPENROUTER_API_KEY injected by the daemon +read -r -d '' INNER_CMD << 'INNEREOF' || true +PAYLOAD=$(jq -n \ + --arg text "$SPEAK_TEXT" \ + --arg voice "$SPEAK_VOICE" \ + '{ + model: "openai/gpt-audio-mini", + modalities: ["text", "audio"], + audio: { voice: $voice, format: "pcm16" }, + stream: true, + messages: [{ role: "user", content: $text }] + }') + +curl -sN https://openrouter.ai/api/v1/chat/completions \ + -H "Authorization: Bearer $OPENROUTER_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" | \ +while IFS= read -r line; do + line="${line#data: }" + [ -z "$line" ] && continue + [ "$line" = "[DONE]" ] && continue + chunk=$(echo "$line" | jq -r '.choices[0].delta.audio.data // empty' 2>/dev/null) + [ -n "$chunk" ] && printf '%s' "$chunk" +done +INNEREOF + +# Inject text and voice as shell-safe env vars, then run the inner command +# through the Signet daemon's exec_with_secrets endpoint +INNER_WITH_VARS="export SPEAK_TEXT=$(printf '%q' "$TEXT"); export SPEAK_VOICE=$(printf '%q' "$VOICE"); $INNER_CMD" + +EXEC_RESPONSE=$(curl -s "$DAEMON/api/secrets/OPENROUTER_API_KEY/exec" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg cmd "$INNER_WITH_VARS" '{ command: $cmd }')") + +# Extract concatenated base64 audio from stdout +B64_AUDIO=$(echo "$EXEC_RESPONSE" | jq -r '.stdout // empty') + +if [ -z "$B64_AUDIO" ]; then + STDERR=$(echo "$EXEC_RESPONSE" | jq -r '.stderr // empty') + echo "Error: No audio data received" + [ -n "$STDERR" ] && echo "stderr: $STDERR" + exit 1 +fi + +# Decode base64 to raw PCM16 +echo "$B64_AUDIO" | base64 -d > "$TMPRAW" 2>/dev/null + +RAWSIZE=$(stat -c%s "$TMPRAW" 2>/dev/null || stat -f%z "$TMPRAW" 2>/dev/null) +if [ "$RAWSIZE" -lt 100 ]; then + echo "Error: Audio data too small ($RAWSIZE bytes)" + exit 1 +fi + +# Wrap raw PCM16 in a WAV header (24kHz, mono, 16-bit LE) +DATASIZE=$RAWSIZE +FILESIZE=$((DATASIZE + 36)) +{ + printf 'RIFF' + printf "$(printf '\\x%02x\\x%02x\\x%02x\\x%02x' $((FILESIZE & 0xFF)) $(((FILESIZE >> 8) & 0xFF)) $(((FILESIZE >> 16) & 0xFF)) $(((FILESIZE >> 24) & 0xFF)))" + printf 'WAVEfmt ' + printf '\x10\x00\x00\x00' # chunk size 16 + printf '\x01\x00' # PCM format + printf '\x01\x00' # mono + printf '\xc0\x5d\x00\x00' # 24000 Hz + printf '\x80\xbb\x00\x00' # byte rate (24000 * 2) + printf '\x02\x00' # block align + printf '\x10\x00' # 16 bits per sample + printf 'data' + printf "$(printf '\\x%02x\\x%02x\\x%02x\\x%02x' $((DATASIZE & 0xFF)) $(((DATASIZE >> 8) & 0xFF)) $(((DATASIZE >> 16) & 0xFF)) $(((DATASIZE >> 24) & 0xFF)))" + cat "$TMPRAW" +} > "$TMPWAV" + +ffplay -nodisp -autoexit -loglevel quiet "$TMPWAV"