From 80233a715d269023fcd4bf880326014f11683b79 Mon Sep 17 00:00:00 2001 From: Nicholai Date: Wed, 25 Feb 2026 17:00:18 -0700 Subject: [PATCH] 2026-02-26T00-00-18_auto_memory/memories.db-wal, memory/memories.db-wal --- .daemon/logs/daemon.out.log | 25 +++++++ .secrets/secrets.enc | 10 +-- memory/memories.db-shm | Bin 32768 -> 32768 bytes memory/memories.db-wal | Bin 5285992 -> 5285992 bytes scripts/speak.sh | 100 +++++++++++++++++++++------ signet-speak.skill | Bin 0 -> 2368 bytes skills/signet-speak/SKILL.md | 36 ++++++++++ skills/signet-speak/scripts/speak.sh | 88 +++++++++++++++++++++++ 8 files changed, 231 insertions(+), 28 deletions(-) create mode 100644 signet-speak.skill create mode 100644 skills/signet-speak/SKILL.md create mode 100755 skills/signet-speak/scripts/speak.sh 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 f64a7c71308fd78d990ea9014de53d87e2289f6f..a99db276bab565aee3b8594ba4b3e55ff6df8532 100644 GIT binary patch delta 514 zcmZo@U}|V!s+V}A%K!pZK+MR%AaI$9fk9uJf#HhQiIgDUm=opC`ChE=YAS747hzzK ztp7%;da&U@;v&%8|40BTzOmkvTkZu5149lg1H%F!UJAq;fOsR6zmauwqoXY2<_V5` zBaWCn!O?j01&0gFj29+1)>Z delta 216 zcmZo@U}|V!s+V}A%K!ouK+MR%AaI(Afk9uJf#H+btCuf0S#!&u^SxN#)l@3Jhhe=; znAv$!)q@QO5+HN`BLS%R#(Gok&5ya?noRz{t-tw#!v*Hai(FQ0_HYej;$&cuV%Wg& ynn7c-V6f-r5AGLuCNFW(*!&{o93N2iugx1$8aROrzs+@dtGIv+mCZhtTi5_V>`&|f diff --git a/memory/memories.db-wal b/memory/memories.db-wal index 6b52f1b586d3ae502c0611f856178456f3b11999..efa1f161a55bd5e42fd80bcb6ba2ac338aa9daaa 100644 GIT binary patch delta 4233 zcmcguO>7&-6{aY=38_eI>a?!ww3^gOEz#WNa=9c$rb;Tya_js!K_VSuv;b#!hU7%c z9cE`%6oH~^r~?EIS|9-o*oXf3V!-I3ZqTB>=afqU9}*z9qRqhs(nE_J+Dn1HSxS_6 z2%tktN*~C5GxO%noAIwB#^)+=`eO;YV-%w}Olj@u5TWV1u^_2RyI;WmiRH^J+;gtGv4K$3p&d zbYhr-PygY<@ZLLn(al?>-~IdcukOr)ZD!#{YT?HHQfuLzg?Nw6%x~59jKy;=W>(=- zDYYu3Qm+{m&nAXruh@o*Ny)%o-6~ltRl8cTF0+6jjNGV=F#KwiIZ8T^@R13#k~b9q=ejMhd7G@*WoyTxsvxDU${5 zD?naIvD700?W0#;LwX+=!7`c31JnjX(>xRs;S^ zWwT~k*N)grRc*H{(>iLeY?kas#cGsSOvkDnoj>6xl{)DkP5H0?x^w!o?tk9T>XheB ze45IAdjF{hi&5_8yBBB3+LI?TtC=s_8+_e{W#jAn%OgKty7h~{-__Tb&fZGR&1G-t z3;cODek+~b+B&;WjIwrjk;&m4cwv@_3Wxcsdt?)I#!8jKuJwv*dljqVRNOMDRh@Fl zF|b=I8xAR9!!1>bQFUxmEmdq%cUm6r_IxTS8Sh)(v?1-)M#VNOR{b#ZiQTaqrJ5O+ zE6z105#41{APWV&ZzAmbsA@xDburTiL4@IUz=dRaD^PAMuiwFdWl1b1ML5sBuNbvn-$m>vV8<|ZAPaxspPu>Dv#|;GihYty=u@|7Vj)9*_}C9|(DrGlEjo^74}06V zO+`__o>XF1Bv^D@jze&QmfR>HK14YRe%aKS#gXI&Z~&NpQm$E}JnW|yXrUWpDRn-U z1UQF77|$VzU7abkzguz}qjd%3vjA?N5!qRW%tK#jOTrwmc8PU+Jk(i>f=IxQJ2->~ zDm`;q$I`_?1f@05L_^6ah!{0t>c(ydM^Iin^b&=#AaECqie^HQ5Xq5)lkK7iy&`a! zCrv4K;j(qbC3li|YPFzJnuySzF4YhZP!dX$_oR5<^jVj|Xc6r{%XrYEdLNm1a5)*k zxrGj16K}u6eL9-kMBLRyXhN&V5O&v7IP$rN{p6qt%WB@GArur$U^`3>l1m`GAGAYE zMoSKwCrLnj12qG_vD@>R$K+ZtfP0Jr2~iPLCI(!QM-5op>p-SmNIJ~E%7d$s@2^3a zp=8i1iS;rMH?Y?kJzRw1r(_e{h-Ol-S_>deLJoXdgu>mSfnJJYCPV~xr%!v(X8L&B zB1wwq@W>~@d5K&&={8!wk{k%iJ@REf-+hniME~wasB7VZ~SQQ&wnatrcdYYrE>RjpBz%X886Ll%^ZiC)8}WmGRI+4h~W3* z2q1u{`LE7z|8VE&aR8q{6ywkR3lA2z6V(}oavTLnLOBj;Ln!a3QWuH`FB-!qe)HDb zS5Bozp*&3GKF>WIN0K@WrL{UYdv<1DXwr|(E@sB`4=!)(B?m6gI{!HL`l*jTNW?F5 z7sM}fSBu{-Gx1(C4N7!obu}~p(`Wx@^4G#ZwudV8#qSI8p)A&&Ya3cZ8fe|e5us=u z-g%Jba1ONxG=rl6_?-YT2?iq=?9liVvjr6bYTC?Rr9!vo{xfJnN7p_iOR_F}QvokZ zn(SiDs(l7(=Y>F|iS8wh3ym`JCCuz52+lxr0`v$IBazVzC+LVX)?ZrNy0o$W%B2lN zgB=#~pbNdFDS=_vr)Ku3+)t`~JaxIBRQq`9azCl|@zmvhQtjiZ%l)L<$5WU4Nwtrs zF87maA5UHGC)GZly4+8yeLQu!pH%xed8_{a%l(AHkq`49efYzdUjv29fZPwY#0?MS zzIA3UJ9}nkq#={S_V!R1025FISb!4129yB~paQ4@YJe3$9nb(g3-~Tz74RJ3dBFDo oYk-S@b-*RS2H-N_`+y$+t^i&Dya;#+@G{^k;FbGB@#=Nwzgup6od5s; delta 5253 zcmeHLeT-E{6@Op5?CyK}7Ipz!C>2`Xmfgp|%-s2;@IYuwscqdTwtSbFxih6H1+-~2 zik5^cO|&95g&T>aH8pCC7Gj8trcJOKt0cDg8LF+4hO|fvq54M?Me4cl1JQp>|KFGV zUM4ql=A1d_ch3F2clP89g|mQNNj@ng#iW#UCEdxSq$lZ3`jT=|N&1s&GLTG8rX+*O zP%<@{mP}7RkX(~oo6JaNCf6milIxQXCbN?{$qmVk$=sxt3@0}w^OBKdG^rn*Kl=BX zvnS`^^QkwhZ;}fKHy--aTMw9JKOO9=?#bPjp1-$tV|oJD*IZM6qWjMN<++31)62ts z7rRbZXLNnHFtzxz@m*_b7y3&B^Kv4cy|l3+JzN+P>Gv-W)=C2>;MtMl(28_mS$%!_ zgZpdxx?@-S+I{-?{CRJjxZDR1@6G=>z3)W#tkL|Lg6jWb|K@(tU#YxaIab+Qi7Sf= zwlcZ=M)|qIy{!6l0dg=)I((xl;1v9EA?{35S*=Tq$RrSy1tXt*>| z{CFWf&FbydrVX|B>e*_ey*hE_+4XNNY(G1GPupVu<)PsP*&$r9*lDVf7J?!sX+R1~ zj(mu5V2&t3asOYM?!NK2!8z$uYwKNYLqfE125A$=h*{#13YsA>(Zt1GM;P}cO~bME zwYD=E6!V&*;FUs5NQSgG4spz}50*F`Mg6yAHV3+fh97OlC!I0EMu=$*UxviU`iLUQ zF(#5SCpqtyt(_)T0Y`{O+#^OcMw(KFqJTqeLu{ONJ&W5ri87XMR_p zoG^%Dqhx@cLWjOzw+>}S2?uqQ4kLz{M2bOLB=d^ONDJa~Km<<|1g<@Xgk!jDRsry4g3ysO zOwsb8ETr^%ki`~fcdY5GHkCR&eifdzPmCR9UZO}x5&~ib*~ob$yrFP?$73J5E;dhm zw$QO2TXMzP+BifWNrN@;B~r*(ju8ol6M>~8F)p*F_2$i;eih&l(ya= zMPtan%f?HfP!2ej5TmW}wD0H5`y3g(V#6%VJ7`sr%}6HWY~EoMu%?!9!D32!Ow(pB z?Ogr1s|~MdhRL8rjtLa0jFE~tq+lEavEXi6ScQ%3=Ix!_TAf`utEmJsDg`{0JSe3- zBiOi}Uo`zpwjJhIm(@Fdpoq*vlp1ovtOo+aI{;XWX}~y0s^iK?%a3&|;tF~M`UpMF_;Iuuczr}$I<0;adCCaN^_c7MOX_cVFX<*q(Om1fkqx<#$1Iot!6+k zx95bvk_F)i18Ya2p_u^MX&i;D$BYailJKIb_O|?R7)KtasdL%%fH|UocL+wA7s>@< zRoweZ)6e<$md))hHpy^cW34^l8Dl<%)N#k)a*PCcD{zj4j3J5E!3eRF-f20CV(Wzk zYGs3lQiWh%0TqPf8WTpHi~4PACv}DhC_9B&2KBc<8WtlenV>WZoGafwx5cyzymR-h z{|SjGrr~%iGD=$w*AC1g&JFVWp0P$^4&-Hx%A3{v+LYY4DzO z3^nGZ7p4!bOnu%p$kKX~e-`eO&b^VL^>_2s_OZ*q%4v{jxi{IK-(+`%4NNPk;tJTg4^=Qwg=JupWOb) zjz#d(7k=c8ukLR&N~P}U@XuBCU9>y}r7hEe!N!ZF${+Ga#!tROX6Do9-$m<7{byj$ zOQv=>{n651p6;|YIlg^bW3-gtn(k1&%hS)WUXiN1dS&|AJ9_2#t{IKxh3TXJCE(s$ zXTJ4X?&a6VcZm`!kMBH;UMf9y?qLL209XjX00N+Z2*3awAOI4e02;6ea5G>rU/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 0000000000000000000000000000000000000000..425989cb51b84728f12a347a423d2eeb15dc4329 GIT binary patch literal 2368 zcmZ{mXHXN05`{wv(v>DYdKHk6M5Ia;MJb^p%|uEFfzUz+K_px-(wm_pSSTt@N)QAT zP(dMp5ZVPUA)o{)(gV^S-Z%5+jdS1bk2AZobN+p2*3y)TnI8ZEumY6g9N}g=Kd%ij z0sx{w06^em8{~!dLj@}ZVNtH$5F1lt7!2&|PO$Cpqe9QLEht{!s+Bb1*b#$0$- zWp@EoOW3&vQ#OW+Xl-i%!TT#(p(Uk5w|97{35VZEkps`XguK2`n2qd&5GG2AcWouU z+7Zo#ba0Ij9F4u%Mh?pYPZ{KK3eRB_5kq`W)_V{^3VE*R&`^Q4m|=Y}T6*-%SopcD zN}2wLg{s9DmzSOL`X7sSKnyg<6-=)yEVVRPSLC<&FsEBjKU_>?zK}$#HY%BBD>Y|> z66Xbs5JHoz14jm049LhR3`uOiWxYgcP7GGgu8-g-Lh5JdJ0?tBO?D}JVC>*{({nKk zMFV?U*caxl#Jp$gQK4b7-O{lg95m-niPq99Z>gJDWl;+k_fvK(7naVRv= znke6b3t1~ymrlH60nTTZRx1ce&!`SYol@>nlY+#=?lLGbt6kqGg};wl((`2t zN=j9BQNjrp8RjY#yQIn4EeJ&V=1y=OEW5Yk8~H{Jt+G*CS38wNIk_9Lv}vSq?DlQ>>_7_7Q_qg#N>HsJhaU&XuzV_xs{)@jK!hZfckijF z+sW@bv#VYZ81h}8BH$y}59Jdz-0ma!yQgGR^;kLi))x5+dk0k*+JWbB*30~Lr9XpV z`UPPbYfu6Eifpq#)YhA2Z>x8jknpe0sHwo6R*y%GGyCagZ}XXn!+$!znoMpdf9fsf zinvE~=r6WUaxU1z^rrgW>$`{cnE)>MRL(eLSMipEaLd~yy$N^1MYKb|+6+@3*Nm-L z90n;8wjE5}ohYoakSu&K$EH2n!=S;pC-E^Eeq#cfe4%$(R+nLJc2#WL3VFCEP&Utb zst>QoyvZb;4!mc4d~qb6E6-fe(rH*9n?=~J6_yFRza$smz%2WhDckdTJ?)g?z@KYa zb%io|I>yEbgM%bo3V#SrI%2f^F9gCT5LC#abl@id6mgt@@H+%S$UrY_a1i8gB)~zQ zMRxlZlSXHb?j9#L%aUK?12U~`OMwbN=_lDTCPN0NEgQy#scw{l0?uE2z}GcXEe!_Rh6FCz+LU(S(l zDxjPCHKRji-qdr^D)+YOC0c5Y3E3dv1tj*m4` zg0);~YY(1JP!hbAwtHIQEJK=*zTs!Svn}e+L&s%UZghra+PuKQ~A^4@HO$qtR`MDrK39qR7d5mhUxTirAZjZsnM6KXwic7LCHim*(7F4Lw-0 zbjhomF9i6SZ#nd=;qvBKqUK?4j*3gbXF-InDFHOL_{kq2v!{at`dc#(lq9s~(_^1LCL$d%JMt35;~EQxc3Rh+ z-lA3v?^KUoISS95l5!1f7u=PD9%Q(zP!vXu5Vkd;=}h@6?uJtr!7)pWJCE9%N}?$& zQQyyJ$=3zd@HP$)CC(7-lNz1yhzgLmNi0G^$8E@Jr5hd^w_n*A$*QCU zNfsbr>uuAmUtDDjlg&h2YBJIy-*kP;d1{C5XdqPNDCE2ZjiVrGi_MR}I!=p=M6~^N ze2G*FS3_{$u`rPo9y}Yr`L7OHdW6PT1_bu<+ZPfc)%n674*2Vw!#a8B0nypb9n&~r z&K7)mqmK3N8-B7)LKm;-xj>Y65;*LfWcZN(A+q9$%Bn#3j4k9U$*o87DG8q=8TttB zq>yZlmD*}^LQKXRCgvv{c;b?v9^nIa&r_F7gHScHvwh3d2+DZXI{+P;95CuS<2MrK zZy`nuNRn|A#zgk3evjVqiE=!rvgwe-(G###Z-4FWO_N#w@Ot#*`^uMfb9budklyPB z;5+LZfkeluf!k3IF&@2&6aiawcQ?_OTd@4pskQ#2wn`!$()n}MAQ$yKm_2JY#f{4p zd@HBkX*v<%)wTC3#51SdpijJQj42pcr|mOZi4hiLGr1U(5uKo4Jp=}?7PHL!%=f{ z89yoAuRI62ad61u0kfqk10z4v|Nl--via|NhWjP|_^bS{#Xn{He+&SC6&#T1H;dns U-qIAv^6Q-Oq*G6{koDK}Z>DNZfB*mh literal 0 HcmV?d00001 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"