#!/bin/bash # Toggle dictation: ⌘⇧D / Fn → запись с микрофона; повторное нажатие → Groq Whisper → текст в /tmp/groq-dictate.last # Hammerspoon Lua callback вставляет через hs.eventtap.keyStroke({"cmd"},"v") в активное окно. # Fallback: если Groq недоступен (нет сети / 429 / 5xx) → локальный whisper-cli (whisper-cpp tiny ru). # Hammerspoon при автозапуске даёт пустой PATH — добавляем явно (Intel + Apple Silicon brew пути) export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin" PID_FILE="/tmp/groq-dictate.pid" WAV_FILE="/tmp/groq-dictate.wav" WAV_16K="/tmp/groq-dictate-16k.wav" OUT_FILE="/tmp/groq-dictate.last" LOG="/tmp/groq-dictate.log" GROQ_KEY="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" WHISPER_MODEL="$HOME/.cache/whisper-cpp/ggml-tiny-q5_1.bin" log() { echo "[$(date +%H:%M:%S)] $*" >> "$LOG"; } notify() { osascript -e "display notification \"$1\" with title \"Groq Dictate\""; } groq_transcribe() { local response response=$(curl -sS -X POST "https://api.groq.com/openai/v1/audio/transcriptions" \ -H "Authorization: Bearer $GROQ_KEY" \ -F "file=@$WAV_FILE" \ -F "model=whisper-large-v3-turbo" \ -F "language=ru" \ -F "response_format=json" \ --max-time 15 2>/dev/null) || return 1 local code code=$(echo "$response" | jq -r '.error.code // empty' 2>/dev/null) [ -n "$code" ] && { log "Groq error: $response"; return 1; } echo "$response" | jq -r '.text // empty' 2>/dev/null | sed 's/^ *//;s/ *$//' } local_transcribe() { [ ! -f "$WHISPER_MODEL" ] && { log "no local model: $WHISPER_MODEL"; return 1; } ffmpeg -hide_banner -loglevel error -i "$WAV_FILE" -ar 16000 -ac 1 -y "$WAV_16K" 2>>"$LOG" || return 1 whisper-cli -m "$WHISPER_MODEL" -l ru -nt -np "$WAV_16K" 2>>"$LOG" | sed 's/^ *//;s/ *$//' | tr -d '\n' } if [ -f "$PID_FILE" ]; then # === STOP & TRANSCRIBE === PID=$(cat "$PID_FILE") log "STOP pid=$PID" kill -INT "$PID" 2>/dev/null for i in 1 2 3 4 5; do kill -0 "$PID" 2>/dev/null || break; sleep 0.1; done rm -f "$PID_FILE" if [ ! -s "$WAV_FILE" ]; then log "ERR empty wav"; notify "Запись пустая"; exit 1 fi log "POST size=$(stat -f%z "$WAV_FILE")B" TEXT=$(groq_transcribe) if [ -z "$TEXT" ]; then log "Groq fail → trying local whisper-cli" notify "☁️→💻 Groq не отвечает, локальная модель" TEXT=$(local_transcribe) [ -z "$TEXT" ] && { log "Local also empty"; notify "Не распознано"; exit 1; } log "LOCAL DONE: $TEXT" else log "GROQ DONE: $TEXT" fi printf '%s' "$TEXT" > "$OUT_FILE" else # === START RECORDING === rm -f "$WAV_FILE" "$WAV_16K" "$OUT_FILE" log "START → $WAV_FILE" ffmpeg -hide_banner -loglevel error -f avfoundation -i ":0" -ar 16000 -ac 1 -y "$WAV_FILE" >/dev/null 2>>"$LOG" & echo $! > "$PID_FILE" notify "🎙️ Говори" fi