The shell '>' redirect creates ACTION_FILE immediately as an empty file. The poll loop checked [ -f ] (exists) instead of [ -s ] (non-empty), so it matched on the first 0.2s iteration, found no 'cancel' string, and broke out of the loop — leaving mpv running with no listener. [ -s ] only returns true once notify-send writes 'cancel' to the file (i.e. when the user actually clicks the Abbrechen action).
127 lines
3.8 KiB
Nix
127 lines
3.8 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="''${ELEVENLABS_API_KEY:?Set ELEVENLABS_API_KEY in your environment}"
|
|
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";
|
|
};
|
|
}
|