Files
nixpkgs/pkgs/talk/default.nix
T
m3ta-chiron e7765ee39f feat(talk): read API key from agenix file instead of env var
Primary: /run/agenix/elevenlabs-key (read via 'read -r' which strips
the trailing newline agenix files contain).
Fallback: ELEVENLABS_API_KEY env var (backwards compatible).

No need to export the key in shell environment anymore — if the
agenix secret exists, talk picks it up automatically.

Override path via ELEVENLABS_KEY_FILE env var if needed.
2026-07-02 20:14:45 +02:00

138 lines
4.3 KiB
Nix

{
lib,
stdenv,
writeShellScriptBin,
fetchurl,
curl,
python3,
mpv,
libnotify,
busybox,
}: let
icon = fetchurl {
url = "https://s3.m3tam3re.com/public/chiron.jpeg";
hash = "sha256-LIIR3aqJ5ePsZuvVQxJ70c6yd+L85fLE5uY/fLE5Ypo=";
};
script = writeShellScriptBin "talk" ''
#!/usr/bin/env bash
set -euo pipefail
# ── Toggle: VOICE_NOTIFY=0 deaktiviert alles ───────────────────
if [ "''${VOICE_NOTIFY:-1}" = "0" ]; then
exit 0
fi
# ── Config ──────────────────────────────────────────────────────
# API key: read from agenix file (preferred), fall back to env var.
# `read -r` strips the trailing newline that agenix files contain.
ELEVENLABS_KEY_FILE="''${ELEVENLABS_KEY_FILE:-/run/agenix/elevenlabs-key}"
if [ -f "$ELEVENLABS_KEY_FILE" ]; then
read -r API_KEY < "$ELEVENLABS_KEY_FILE"
elif [ -n "''${ELEVENLABS_API_KEY:-}" ]; then
API_KEY="''${ELEVENLABS_API_KEY}"
else
echo "Error: No ElevenLabs API key found." >&2
echo "Create $ELEVENLABS_KEY_FILE (agenix) or set ELEVENLABS_API_KEY env var." >&2
exit 1
fi
VOICE_ID="''${ELEVENLABS_VOICE_ID:-KXxZd16DiBqt82nbarJx}"
# Argument hat Vorrang, sonst stdin lesen wenn Pipe
if [ $# -ge 1 ]; then
TEXT="$1"
elif [ ! -t 0 ]; then
TEXT=$(cat)
else
echo "Usage: talk \"Text\" oder echo Text | talk" >&2
exit 1
fi
AUDIO_FILE="/tmp/voice-notify-$$.mp3"
ACTION_FILE="/tmp/voice-action-$$"
CURL="${lib.getExe curl}"
PYTHON="${lib.getExe python3}"
MPV="${lib.getExe mpv}"
NOTIFY="${lib.getExe libnotify}"
RM="${busybox}/bin/rm"
CAT="${busybox}/bin/cat"
GREP="${busybox}/bin/grep"
SLEEP="${busybox}/bin/sleep"
KILL="${busybox}/bin/kill"
STAT="${busybox}/bin/stat"
trap '$RM -f "$AUDIO_FILE" "$ACTION_FILE"' EXIT
# ── TTS generieren ──────────────────────────────────────────────
PAYLOAD=$("$PYTHON" -c '
import json, sys
print(json.dumps({
"text": sys.argv[1],
"model_id": "eleven_multilingual_v2",
"voice_settings": {"stability": 0.5, "similarity_boost": 0.75}
}))
' "$TEXT")
HTTP_CODE=$("$CURL" -s -o "$AUDIO_FILE" -w "%{http_code}" \
-X POST "https://api.elevenlabs.io/v1/text-to-speech/''${VOICE_ID}" \
-H "xi-api-key: ''${API_KEY}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD") || HTTP_CODE="000"
if [ "$HTTP_CODE" != "200" ]; then
ERR=$($CAT "$AUDIO_FILE" 2>/dev/null | head -c 300)
$RM -f "$AUDIO_FILE"
"$NOTIFY" -u critical "❌ Voice Fehler" "HTTP ''${HTTP_CODE}\n''${ERR}"
exit 1
fi
# ── Gleichzeitig: Audio + Notification ──────────────────────────
"$MPV" --no-video --no-terminal --really-quiet "$AUDIO_FILE" &
MPV_PID=$!
(
exec "$NOTIFY" \
--action=cancel="⛔ Abbrechen" \
--urgency=normal \
--icon=${icon} \
"🔊 KI Antwort" \
"$TEXT" > "$ACTION_FILE" 2>/dev/null
) &
NOTIFY_PID=$!
# ── Poll-Loop ───────────────────────────────────────────────────
while true; do
if ! $KILL -0 "$MPV_PID" 2>/dev/null; then
$KILL "$NOTIFY_PID" 2>/dev/null || true
break
fi
if [ -s "$ACTION_FILE" ]; then
if $GREP -q "cancel" "$ACTION_FILE" 2>/dev/null; then
$KILL "$MPV_PID" 2>/dev/null || true
fi
break
fi
$SLEEP 0.2
done
wait "$MPV_PID" 2>/dev/null || true
'';
in
stdenv.mkDerivation {
pname = "talk";
version = "0.1.0";
dontUnpack = true;
installPhase = ''
mkdir -p "$out/bin"
ln -s ${script}/bin/talk "$out/bin/talk"
'';
meta = with lib; {
description = "ElevenLabs TTS voice notifications with dismissable popup";
homepage = "https://code.m3ta.dev/m3tam3re/nixpkgs";
license = licenses.mit;
platforms = platforms.linux;
mainProgram = "talk";
};
}