-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvoice
More file actions
executable file
·299 lines (256 loc) · 9.33 KB
/
voice
File metadata and controls
executable file
·299 lines (256 loc) · 9.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
#!/usr/bin/env bash
# Voice-to-Claude: Aufnehmen, Transkribieren, in Claude Code einfügen + Enter.
# Usage: voice [de|en|fr]
# Features:
# - Clipboard-Text wird automatisch als Zitat vorangestellt
# - "Screenshot" am Anfang → Screenshot wird gemacht und in Claude eingefügt
# - Akustische Benachrichtigung wenn Claude Code fertig ist
# Steuerung: Enter/Space = Aufnahme starten/stoppen, Esc = Beenden
set -uo pipefail
MODEL="$HOME/.whisper-models/ggml-small.bin"
TMPFILE="/tmp/voice_recording.wav"
SCREENSHOT="/tmp/voice_screenshot.png"
BUSY_FILE="/tmp/voice_claude_busy"
BATCH_DIR="/tmp/voice_batch"
LANG_CODE="${1:-de}"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
RED='\033[0;31m'
GRAY='\033[0;90m'
NC='\033[0m'
MY_WINDOW=$(osascript -e 'tell application "Terminal" to get id of front window' 2>/dev/null || echo "")
focus_claude() {
osascript \
-e 'tell application "Terminal"' \
-e ' repeat with w in windows' \
-e ' if name of w contains "claude" then' \
-e ' set frontmost of w to true' \
-e ' set selected tab of w to selected tab of w' \
-e ' exit repeat' \
-e ' end if' \
-e ' end repeat' \
-e ' activate' \
-e 'end tell' \
-e 'delay 0.8' \
2>/dev/null
}
paste_clipboard() {
osascript \
-e 'tell application "System Events" to keystroke "v" using command down' \
2>/dev/null
}
press_enter() {
osascript \
-e 'delay 0.4' \
-e 'tell application "System Events" to key code 36' \
2>/dev/null
}
paste_to_claude() {
echo -n "$1" | pbcopy
focus_claude
paste_clipboard
press_enter
}
screenshot_and_paste() {
local text="$1"
rm -f "$SCREENSHOT"
screencapture "$SCREENSHOT" 2>/dev/null
if [[ ! -f "$SCREENSHOT" ]]; then
return 1
fi
paste_to_claude "${text}
Screenshot: ${SCREENSHOT}"
}
switch_back() {
[ -z "$MY_WINDOW" ] && return
osascript \
-e "tell application \"Terminal\"" \
-e " repeat with w in windows" \
-e " if id of w is $MY_WINDOW then" \
-e " set frontmost of w to true" \
-e " exit repeat" \
-e " end if" \
-e " end repeat" \
-e " activate" \
-e "end tell" \
2>/dev/null
}
read_key() {
local key
IFS= read -rsn1 key
if [[ "$key" == $'\x1b' ]]; then
echo "ESC"
return
fi
echo "$key"
}
# ── Hintergrund: Claude Code idle-Monitor ─────────────────────
rm -f "$BUSY_FILE"
rm -rf "$BATCH_DIR"
mkdir -p "$BATCH_DIR"
get_claude_cpu() {
# CPU-Auslastung des Claude Code CLI-Prozesses (nicht Claude Desktop)
ps aux | grep 'claude.*--dangerously\|claude.*--skip' | grep -v grep \
| awk '{sum += $3} END {printf "%.0f", sum+0}'
}
monitor_claude_idle() {
local idle_count=0
local CPU_THRESHOLD=3 # Unter 3% = idle
while true; do
sleep 2
# Claude ist idle — nächsten Batch senden falls vorhanden
if [[ ! -f "$BUSY_FILE" ]]; then
local next_batch
next_batch=$(ls "$BATCH_DIR"/*.txt 2>/dev/null | sort | head -1) || true
if [[ -n "$next_batch" ]]; then
local prompt
prompt=$(cat "$next_batch")
rm -f "$next_batch"
echo -n "$prompt" | pbcopy
focus_claude
paste_clipboard
press_enter
touch "$BUSY_FILE"
idle_count=0
fi
continue
fi
# CPU-basierte Idle-Erkennung
local cpu
cpu=$(get_claude_cpu)
if [[ "$cpu" -lt "$CPU_THRESHOLD" ]]; then
idle_count=$((idle_count + 1))
# 2 aufeinanderfolgende Checks unter Threshold = wirklich idle
# (verhindert false positives bei kurzen CPU-Pausen)
if [[ "$idle_count" -ge 2 ]]; then
rm -f "$BUSY_FILE"
idle_count=0
afplay /System/Library/Sounds/Glass.aiff 2>/dev/null &
fi
else
idle_count=0
fi
done
}
monitor_claude_idle </dev/null >/dev/null 2>&1 &
MONITOR_PID=$!
cleanup() {
kill $MONITOR_PID 2>/dev/null || true
rm -f "$BUSY_FILE"
rm -rf "$BATCH_DIR"
}
trap cleanup EXIT
# ── UI ────────────────────────────────────────────────────────
clear
printf '\033]0;Voice Input\007'
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} 🎙️ Voice-to-Claude${NC} ${GRAY}(${LANG_CODE})${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e " ${BLUE}Enter/Space${NC} = Aufnahme starten/stoppen"
echo -e " ${RED}Esc${NC} = Beenden"
echo -e " ${GRAY}Clipboard-Text wird als Zitat vorangestellt${NC}"
echo -e " ${GRAY}\"Screenshot\" am Anfang → Screenshot machen${NC}"
echo -e " ${GRAY}🔔 Sound wenn Claude Code fertig ist${NC}"
echo -e " ${GRAY}\"Batch\" → Batch-Modus an/aus (Nachrichten sammeln)${NC}"
echo ""
while true; do
BATCH_COUNT=$(ls "$BATCH_DIR"/*.txt 2>/dev/null | wc -l | tr -d ' ')
if [[ -f "$BATCH_DIR/.mode" ]]; then
echo -e "${BLUE}📦 BATCH MODUS${NC} ${GRAY}(${BATCH_COUNT} gespeichert) — Enter/Space zum Starten${NC}"
elif [[ "$BATCH_COUNT" -gt 0 ]]; then
echo -e "${GRAY}⏸ Bereit${NC} ${BLUE}(📦 ${BATCH_COUNT} Batches warten)${NC}"
else
echo -e "${GRAY}⏸ Bereit — Enter/Space zum Starten${NC}"
fi
KEY=$(read_key)
[[ "$KEY" == "ESC" ]] && { echo -e "\n${RED}👋 Beendet${NC}"; exit 0; }
# Clipboard-Inhalt vor Aufnahme sichern
CLIP=$(pbpaste 2>/dev/null || echo "")
echo -e "${GREEN}🎙️ Aufnahme...${NC} (${YELLOW}Enter/Space${NC} zum Stoppen)"
rec -q -r 48000 -c 1 -b 16 "$TMPFILE" 2>/dev/null &
REC_PID=$!
KEY=$(read_key)
kill $REC_PID 2>/dev/null; wait $REC_PID 2>/dev/null || true
[[ "$KEY" == "ESC" ]] && { rm -f "$TMPFILE"; echo -e "\n${RED}👋 Beendet${NC}"; exit 0; }
echo -e "${BLUE}⏳ Transkribiere...${NC}"
TEXT=$(whisper-cli -m "$MODEL" -l "$LANG_CODE" -nt -f "$TMPFILE" 2>/dev/null \
| sed 's/\[.*\]//g; s/^ *//; s/ *$//; /^$/d' \
| tr '\n' ' ' \
| sed 's/ */ /g; s/^ *//; s/ *$//')
rm -f "$TMPFILE"
if [ -z "$TEXT" ]; then
echo -e "${YELLOW}⚠️ Nichts erkannt${NC}\n"
continue
fi
echo -e "${GREEN}📝 $TEXT${NC}"
TEXT_LOWER=$(echo "$TEXT" | tr '[:upper:]' '[:lower:]')
# "Batch" allein → Batch-Modus umschalten
if [[ "$TEXT_LOWER" =~ ^(batch|batsch|badge|patsch|putsch)[.,!]?[[:space:]]*$ ]]; then
if [[ -f "$BATCH_DIR/.mode" ]]; then
rm -f "$BATCH_DIR/.mode"
TOTAL=$(ls "$BATCH_DIR"/*.txt 2>/dev/null | wc -l | tr -d ' ')
echo -e "${GREEN}📦 Batch-Modus AUS${NC} ${GRAY}(${TOTAL} in Warteschlange)${NC}\n"
else
touch "$BATCH_DIR/.mode"
echo -e "${BLUE}📦 Batch-Modus AN — Nachrichten werden gesammelt${NC}\n"
fi
continue
fi
# Keyword-Erkennung: Screenshot und/oder Paste (in beliebiger Reihenfolge)
HAS_SCREENSHOT=false
HAS_PASTE=false
REMAINING_LOWER="$TEXT_LOWER"
for _ in 1 2; do
if [[ "$REMAINING_LOWER" =~ ^screenshot[.,!]?[[:space:]]*(.*) ]]; then
HAS_SCREENSHOT=true
REMAINING_LOWER="${BASH_REMATCH[1]}"
elif [[ "$REMAINING_LOWER" =~ ^paste[d]?[.,!]?[[:space:]]*(.*) ]]; then
HAS_PASTE=true
REMAINING_LOWER="${BASH_REMATCH[1]}"
elif [[ "$REMAINING_LOWER" =~ ^paced[.,!]?[[:space:]]*(.*) ]]; then
HAS_PASTE=true
REMAINING_LOWER="${BASH_REMATCH[1]}"
fi
done
REMAINING="$REMAINING_LOWER"
# Screenshot aufnehmen falls nötig
SCREENSHOT_REF=""
if $HAS_SCREENSHOT; then
echo -e "${BLUE}📸 Screenshot wird aufgenommen...${NC}"
rm -f "$SCREENSHOT"
screencapture "$SCREENSHOT" 2>/dev/null
if [[ -f "$SCREENSHOT" ]]; then
SCREENSHOT_REF="
Screenshot: ${SCREENSHOT}"
else
echo -e "${YELLOW}⚠️ Screenshot fehlgeschlagen${NC}\n"
continue
fi
fi
# Prompt zusammenbauen
if $HAS_PASTE && [[ -n "$CLIP" ]]; then
echo -e "${GRAY}📋 Clipboard-Text als Zitat angehängt${NC}"
PROMPT="<eingefuegter-text>
${CLIP}
</eingefuegter-text>
${REMAINING:-$TEXT}${SCREENSHOT_REF}"
elif $HAS_SCREENSHOT; then
PROMPT="${REMAINING:-Beschreibe was du auf dem Screenshot siehst.}${SCREENSHOT_REF}"
else
PROMPT="$TEXT"
fi
# Senden oder in Batch-Queue speichern
if [[ -f "$BATCH_DIR/.mode" ]]; then
NEXT=$(printf "%03d" $(( $(ls "$BATCH_DIR"/*.txt 2>/dev/null | wc -l | tr -d ' ') + 1 )))
printf '%s' "$PROMPT" > "$BATCH_DIR/$NEXT.txt"
TOTAL=$(ls "$BATCH_DIR"/*.txt 2>/dev/null | wc -l | tr -d ' ')
echo -e "${BLUE}📦 Batch #${TOTAL} gespeichert${NC}\n"
else
paste_to_claude "$PROMPT"
touch "$BUSY_FILE"
sleep 0.5
switch_back
echo -e "${GREEN}✅ Gesendet!${NC}\n"
fi
done