This commit is contained in:
2025-12-07 21:45:34 -08:00
parent a93761b042
commit 33b541dbd8
5 changed files with 344 additions and 201 deletions

226
projects/scripts/record-audio.sh Normal file → Executable file
View File

@@ -1,93 +1,145 @@
#!/bin/sh
#!/usr/bin/env bash
# Version 1.2
# shoutout to https://gist.github.com/Cephian/f849e326e3522be9a4386b60b85f2f23 for the original script,
# https://github.com/xythh/ added the ankiConnect functionality
# toggle record computer audio (run once to start, run again to stop)
# dependencies: ffmpeg, pulseaudio, curl
# Toggle desktop audio recording and attach the result to the newest Anki note
# (as tagged by Yomichan). Run once to start recording, run again to stop.
# Dependencies: jq, curl, ffmpeg/ffprobe, pulseaudio (parec+pactl), bc, notify-send
# where recording gets saved, gets deleted after being imported to anki
DIRECTORY="$HOME/.cache/"
FORMAT="mp3" # ogg or mp3
# cut file since it glitches a bit at the end sometimes
CUT_DURATION="0.1"
#port used by ankiconnect
ankiConnectPort="8765"
# gets the newest created card, so make sure to create the card first with yomichan
newestNoteId=$(curl -s localhost:$ankiConnectPort -X POST -d '{"action": "findNotes", "version": 6, "params": { "query": "is:new"}}' | jq '.result[-1]')
#Audio field name
audioFieldName="SentenceAudio"
set -euo pipefail
#if there is no newest note, you either have a complete empty anki or ankiconnect isn't running
if [ "$newestNoteId" = "" ]; then
notify-send "anki connect not found"
exit 1
fi
ANKI_CONNECT_PORT="${ANKI_CONNECT_PORT:-8765}"
AUDIO_FIELD_NAME="${AUDIO_FIELD_NAME:-SentenceAudio}"
FORMAT="${FORMAT:-mp3}" # mp3 or ogg
CUT_DURATION="${CUT_DURATION:-0.1}"
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/record-audio"
RECORD_TIMEOUT="${RECORD_TIMEOUT:-60}"
ANKI_URL="http://localhost:${ANKI_CONNECT_PORT}"
if pgrep -f "parec"; then
pkill -f "parec"
else
time=$(date +%s)
name="$DIRECTORY/$time"
wav_file="$name.wav"
out_file="$name.$FORMAT"
if ! [ -d "$DIRECTORY" ]; then
mkdir "$DIRECTORY"
fi
notify-send -t 1000 "Audio recording started"
#timeout 1m arecord -t wav -f cd "$wav_file"
# just grabs last running source... may not always work if your pulseaudio setup is complicated
if ! timeout 1m parec -d"$(pactl list sinks | grep -B1 'State: RUNNING' | sed -nE 's/Sink #(.*)/\1/p' | tail -n 1)" --file-format=wav "$wav_file"; then
notify-send "Error recording " "most likely no audio playing"
rm "$wav_file"
exit 1
fi
input_duration=$(ffprobe -v error -select_streams a:0 -show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 "$wav_file")
output_duration=$(echo "$input_duration"-"$CUT_DURATION" | bc)
# encode file and delete OG
if [ $FORMAT = "ogg" ]; then
ffmpeg -i "$wav_file" -vn -codec:a libvorbis -b:a 64k -t "$output_duration" "$out_file"
elif [ $FORMAT = "mp3" ]; then
ffmpeg -i "$wav_file" -vn -codec:a libmp3lame -qscale:a 1 -t "$output_duration" "$out_file"
else
notify-send "Record Error" "Unknown format $FORMAT"
fi
rm "$wav_file"
# Update newest note with recorded audio
curl -s localhost:$ankiConnectPort -X POST -d '{
"action": "updateNoteFields",
"version": 6,
"params": {
"note": {
"id": '"$newestNoteId"',
"fields": {
"'$audioFieldName'": ""
},
"audio": [{
"path": "'"$out_file"'",
"filename": "'"$time"'.'$FORMAT'",
"fields": [
"'$audioFieldName'"
]
}]
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing dependency: $1" >&2
exit 1
}
}
}'
# opens changed note, comment if you don't want it.
curl -s localhost:$ankiConnectPort -X POST -d '{
"action": "guiBrowse",
"version": 6,
"params": {
"query": "nid:'"$newestNoteId"'"
}
}'
notify-send -t 1000 "Audio recording copied"
rm "$out_file"
fi
notify() {
# Best-effort notification; keep script running if notify-send is missing.
if command -v notify-send >/dev/null 2>&1; then
notify-send -t 1000 "$@"
fi
}
get_active_sink() {
pactl list sinks short 2>/dev/null | awk '$6=="RUNNING"{print $1; exit 0}'
}
get_newest_note_id() {
local response
response=$(curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' \
-d '{"action":"findNotes","version":6,"params":{"query":"is:new"}}')
jq -r '.result[-1] // empty' <<<"$response"
}
update_anki_note() {
local note_id="$1" audio_path="$2" filename="$3"
local payload
payload=$(jq -n --argjson noteId "$note_id" --arg field "$AUDIO_FIELD_NAME" \
--arg path "$audio_path" --arg filename "$filename" '
{action:"updateNoteFields",version:6,
params:{note:{id:$noteId,fields:{($field):""},
audio:[{path:$path,filename:$filename,fields:[$field]}]}}}')
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
}
open_note_in_browser() {
local note_id="$1"
local payload
payload=$(jq -n --argjson noteId "$note_id" '
{action:"guiBrowse",version:6,params:{query:("nid:" + ($noteId|tostring))}}')
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
}
record_audio() {
local note_id="$1"
local sink
sink=$(get_active_sink) || true
if [[ -z "$sink" ]]; then
notify "Record Error" "No running PulseAudio sink found"
exit 1
fi
mkdir -p "$CACHE_DIR"
local timestamp wav_file out_file
timestamp=$(date +%s)
wav_file="$CACHE_DIR/$timestamp.wav"
out_file="$CACHE_DIR/$timestamp.$FORMAT"
notify "Audio recording started"
if ! timeout "$RECORD_TIMEOUT" parec -d"$sink" --file-format=wav "$wav_file"; then
notify "Record Error" "No audio captured (timeout or sink issue)"
rm -f "$wav_file"
exit 1
fi
local input_duration output_duration
input_duration=$(ffprobe -v error -select_streams a:0 \
-show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 "$wav_file")
output_duration=$(echo "$input_duration - $CUT_DURATION" | bc -l)
# Guard against negative durations
if [[ $(echo "$output_duration < 0" | bc -l) -eq 1 ]]; then
output_duration="0"
fi
case "$FORMAT" in
ogg)
ffmpeg -nostdin -y -i "$wav_file" -vn -codec:a libvorbis -b:a 64k \
-t "$output_duration" "$out_file"
;;
mp3)
ffmpeg -nostdin -y -i "$wav_file" -vn -codec:a libmp3lame -qscale:a 1 \
-t "$output_duration" "$out_file"
;;
*)
notify "Record Error" "Unknown format: $FORMAT"
rm -f "$wav_file"
exit 1
;;
esac
rm -f "$wav_file"
update_anki_note "$note_id" "$out_file" "$timestamp.$FORMAT"
open_note_in_browser "$note_id"
notify "Audio recording copied"
rm -f "$out_file"
}
main() {
for cmd in curl jq ffmpeg ffprobe parec pactl bc; do
require_cmd "$cmd"
done
if pgrep -x parec >/dev/null 2>&1; then
pkill -x parec
notify "Audio recording stopped"
exit 0
fi
local newest_note
newest_note=$(get_newest_note_id)
if [[ -z "$newest_note" ]]; then
notify "Anki Connect" "No new notes found or AnkiConnect unavailable"
exit 1
fi
record_audio "$newest_note"
}
main "$@"

171
projects/scripts/screenshot-anki.sh Normal file → Executable file
View File

@@ -1,75 +1,112 @@
#!/bin/sh
#!/usr/bin/env bash
# Version 1.2
# click and drag to screenshot dragged portion
# click on specific window to screenshot window area
# dependencies: imagemagick, xclip,curl maybe xdotool (see comment below)
# shoutout to https://gist.github.com/Cephian/f849e326e3522be9a4386b60b85f2f23 for the original script,
# https://github.com/xythh/ added the ankiConnect functionality
# if anki is running the image is added to your latest note as a jpg, if anki is not running it's added to your clipboard as a png
time=$(date +%s)
tmp_file="$HOME/.cache/$time"
ankiConnectPort="8765"
pictureField="Picture"
quality="90"
# Capture a region with slurp+grim. If AnkiConnect is available, attach the
# JPEG to the newest note; otherwise copy a PNG to the clipboard.
# This gets your notes marked as new and returns the newest one.
newestNoteId=$(curl -s localhost:$ankiConnectPort -X POST -d '{"action": "findNotes", "version": 6, "params": { "query": "is:new"}}' | jq '.result[-1]')
set -euo pipefail
# you can remove these two lines if you don't have software which
# makes your mouse disappear when you use the keyboard (e.g. xbanish, unclutter)
# https://github.com/ImageMagick/ImageMagick/issues/1745#issuecomment-777747494
xdotool mousemove_relative 1 1
xdotool mousemove_relative -- -1 -1
ANKI_CONNECT_PORT="${ANKI_CONNECT_PORT:-8765}"
PICTURE_FIELD="${PICTURE_FIELD:-Picture}"
QUALITY="${QUALITY:-90}"
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/screenshot-anki"
ANKI_URL="http://localhost:${ANKI_CONNECT_PORT}"
REQUIREMENTS=(slurp grim wl-copy xdotool curl jq)
# if anki connect is running it will return your latest note id, and the following code will run, if anki connect is not running nothing is return.
if [ "$newestNoteId" != "" ]; then
if ! import -quality $quality "$tmp_file.jpg"; then
# most likley reason this returns a error, is for fullscreen applications that take full control which does not allowing imagemagick to select the area, use windowed fullscreen or if running wine use a virtual desktop to avoid this.
notify-send "Error screenshoting " "most likely unable to find selection"
exit 1
fi
notify() {
if command -v notify-send >/dev/null 2>&1; then
notify-send "$@"
fi
}
curl -s localhost:$ankiConnectPort -X POST -d '{
"action": "updateNoteFields",
"version": 6,
"params": {
"note": {
"id": '"$newestNoteId"',
"fields": {
"'$pictureField'": ""
},
"picture": [{
"path": "'"$tmp_file"'.jpg",
"filename": "paste-'"$time"'.jpg",
"fields": [
"'$pictureField'"
]
}]
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
notify "Missing dependency" "$1 is required"
exit 1
}
}'
}
#remove if you don't want anki to show you the card you just edited
curl -s localhost:$ankiConnectPort -X POST -d '{
"action": "guiBrowse",
"version": 6,
"params": {
"query": "nid:'"$newestNoteId"'"
}
}'
wiggle_mouse() {
# Avoid disappearing cursor on some compositors
xdotool mousemove_relative 1 1
xdotool mousemove_relative -- -1 -1
}
#you can comment this if you do not use notifcations.
notify-send "Screenshot Taken" "Added to note"
rm "$tmp_file.jpg"
else
if ! import -quality $quality "$tmp_file.png"; then
notify-send "Error screenshoting " "most likely unable to find selection"
exit 1
fi
# we use pngs when copying to clipboard because they have greater support when pasting.
xclip -selection clipboard -target image/png -i "$tmp_file.png"
rm "$tmp_file.png"
#you can comment this if you do not use notifcations.
notify-send "Screenshot Taken" "Copied to clipboard"
fi
capture_region() {
local fmt="$1" quality="$2" output="$3"
local geometry
geometry=$(slurp)
if [[ -z "$geometry" ]]; then
notify "Screenshot cancelled" "No region selected"
exit 1
fi
if [[ "$fmt" == "jpeg" ]]; then
grim -g "$geometry" -t jpeg -q "$quality" "$output"
else
grim -g "$geometry" -t png "$output"
fi
}
copy_to_clipboard() {
local file="$1"
if ! wl-copy <"$file"; then
notify "Error copying screenshot" "wl-copy failed"
exit 1
fi
}
get_newest_note_id() {
local response
response=$(curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' \
-d '{"action":"findNotes","version":6,"params":{"query":"is:new"}}')
jq -r '.result[-1] // empty' <<<"$response"
}
update_note_with_image() {
local note_id="$1" image_path="$2" filename="$3"
local payload
payload=$(jq -n --argjson noteId "$note_id" --arg field "$PICTURE_FIELD" \
--arg path "$image_path" --arg filename "$filename" '
{action:"updateNoteFields",version:6,
params:{note:{id:$noteId,fields:{($field):""},
picture:[{path:$path,filename:$filename,fields:[$field]}]}}}')
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
}
open_note_in_browser() {
local note_id="$1"
local payload
payload=$(jq -n --argjson noteId "$note_id" '
{action:"guiBrowse",version:6,params:{query:("nid:" + ($noteId|tostring))}}')
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
}
main() {
for cmd in "${REQUIREMENTS[@]}"; do
require_cmd "$cmd"
done
mkdir -p "$CACHE_DIR"
local timestamp base newest_note image_path
timestamp=$(date +%s)
base="$CACHE_DIR/$timestamp"
wiggle_mouse
newest_note=$(get_newest_note_id)
if [[ -n "$newest_note" ]]; then
image_path="$base.jpg"
capture_region "jpeg" "$QUALITY" "$image_path"
update_note_with_image "$newest_note" "$image_path" "paste-$timestamp.jpg"
open_note_in_browser "$newest_note"
notify -i "$image_path" "Screenshot Taken" "Added to Anki note"
rm -f "$image_path"
else
image_path="$base.png"
capture_region "png" "" "$image_path"
copy_to_clipboard "$image_path"
notify -i "$image_path" "Screenshot Taken" "Copied to clipboard"
rm -f "$image_path"
fi
}
main "$@"