Files
knowledge-base/snippets/mac-dictation/groq-dictate.sh
dttb 265d99b378 Mac dictation: Hammerspoon + Groq Whisper решение
- decisions/2026-05-05-mac-dictation-groq-hammerspoon.md: полный план,
  грабли с раскладкой, fallback на whisper-cpp, восстановление на новом Mac
- notes/ru-geoblocked-services.md: реестр CDN с RU-блоком
  (cdn.spokenly, dl.wisprflow и пр.) + принципы обхода
- snippets/mac-dictation/: рабочая версия скриптов и init.lua

Триггер — одиночный Fn, Groq cloud first → tiny local fallback,
вставка через hs.eventtap.event keycode 9 (минует ru-keymap warnings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 16:27:17 +03:00

71 lines
2.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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).
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