diff --git a/pkgs/default.nix b/pkgs/default.nix index c6c3f27..cfc906a 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -33,6 +33,7 @@ in { openwork = pkgs.callPackage ./openwork {}; zellij-ps = pkgs.callPackage ./zellij-ps {}; vibetyper = pkgs.callPackage ./vibetyper {}; + talk = pkgs.callPackage ./talk {}; # ── Pass-through packages ────────────────────────────────────────── # Imported directly from flake inputs. No local modifications. diff --git a/pkgs/talk/default.nix b/pkgs/talk/default.nix new file mode 100644 index 0000000..5c3f5d3 --- /dev/null +++ b/pkgs/talk/default.nix @@ -0,0 +1,113 @@ +{ + lib, + stdenv, + writeShellScriptBin, + curl, + python3, + mpv, + libnotify, + busybox, +}: let + 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}" + TEXT="''${1:?Usage: talk \"Text zum Sprechen\"}" + 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=audio-x-generic \ + "🔊 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 [ -f "$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"; + }; + }