Add dictate.sh script
This script provides a command-line interface for recording dictations, uploading them to a server, and retrieving transcripts. It allows for continuing an existing case, starting a new one, or refreshing the status of the current case. The script uses `ffmpeg` for audio recording and `curl` for server communication. It manages the current case state using a temporary file `/tmp/doctate-current-case`. Configuration details like `DATA_PATH` and `API_KEY` are read from the server's `.env` and `users.toml` files.
This commit is contained in:
Executable
+200
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test helper: record a dictation via ffmpeg, upload it to a running
|
||||
# doctate-server, then display transcript + oneliner. Stand-in for the
|
||||
# smartwatch until hardware arrives.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/dictate.sh interactive (c/n/r/q)
|
||||
# ./scripts/dictate.sh -c continue current case (record)
|
||||
# ./scripts/dictate.sh -n new case (record)
|
||||
# ./scripts/dictate.sh -r refresh — show current case state, no recording
|
||||
#
|
||||
# State: /tmp/doctate-current-case (transient, fine for testing).
|
||||
set -euo pipefail
|
||||
|
||||
STATE_FILE="/tmp/doctate-current-case"
|
||||
SERVER_URL="${SERVER_URL:-http://localhost:3000}"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
ENV_FILE="$REPO_ROOT/server/.env"
|
||||
USERS_FILE="$REPO_ROOT/server/users.toml"
|
||||
TRANSCRIPT_TIMEOUT_SECS=120
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
|
||||
# -- Config: DATA_PATH + first api_key/slug from the server's config --
|
||||
[ -f "$ENV_FILE" ] || die ".env not found at $ENV_FILE"
|
||||
[ -f "$USERS_FILE" ] || die "users.toml not found at $USERS_FILE"
|
||||
|
||||
DATA_PATH=$(grep -E '^DATA_PATH=' "$ENV_FILE" | head -1 | cut -d= -f2-)
|
||||
[ -n "$DATA_PATH" ] || die "DATA_PATH not set in $ENV_FILE"
|
||||
|
||||
if [ -n "${API_KEY:-}" ]; then
|
||||
SLUG=$(awk -v key="$API_KEY" '
|
||||
/^\[\[users\]\]/ { cur_slug=""; cur_key=""; next }
|
||||
/^slug[[:space:]]*=/ { gsub(/"/, "", $3); cur_slug=$3 }
|
||||
/^api_key[[:space:]]*=/ { gsub(/"/, "", $3); cur_key=$3; if (cur_key==key) { print cur_slug; exit } }
|
||||
' "$USERS_FILE")
|
||||
[ -n "$SLUG" ] || die "API_KEY not found in $USERS_FILE"
|
||||
else
|
||||
SLUG=$(awk '/^slug[[:space:]]*=/ { gsub(/"/, "", $3); print $3; exit }' "$USERS_FILE")
|
||||
API_KEY=$(awk '/^api_key[[:space:]]*=/ { gsub(/"/, "", $3); print $3; exit }' "$USERS_FILE")
|
||||
fi
|
||||
|
||||
# Locate a case directory under open/ or done/.
|
||||
case_dir_for() {
|
||||
local case_id="$1"
|
||||
for sub in open done; do
|
||||
if [ -d "$DATA_PATH/$SLUG/$sub/$case_id" ]; then
|
||||
echo "$DATA_PATH/$SLUG/$sub/$case_id"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Print everything we know about a case: recordings, transcripts, oneliner.
|
||||
show_case() {
|
||||
local case_id="$1"
|
||||
local dir
|
||||
dir=$(case_dir_for "$case_id") || { echo "Case $case_id: no files on disk yet."; return; }
|
||||
|
||||
local recs
|
||||
recs=$(ls "$dir"/*.m4a 2>/dev/null | wc -l)
|
||||
echo "Case $case_id ($recs recording$([ "$recs" = 1 ] || echo s))"
|
||||
echo "Dir: $dir"
|
||||
echo
|
||||
|
||||
echo "=== Transcripts ==="
|
||||
if ls "$dir"/*.transcript.txt >/dev/null 2>&1; then
|
||||
for t in "$dir"/*.transcript.txt; do
|
||||
echo "--- $(basename "$t") ---"
|
||||
cat "$t"
|
||||
echo
|
||||
done
|
||||
else
|
||||
echo "(none yet)"
|
||||
fi
|
||||
|
||||
echo "=== Oneliner ==="
|
||||
if [ -f "$dir/oneliner.txt" ]; then
|
||||
cat "$dir/oneliner.txt"
|
||||
echo
|
||||
else
|
||||
echo "(noch nicht generiert — später mit -r erneut prüfen)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Browse: $SERVER_URL/web/"
|
||||
}
|
||||
|
||||
# Record + upload for the given case, then display the case state.
|
||||
# Bails out (non-zero) on recording/upload errors so the loop can continue.
|
||||
record_and_show() {
|
||||
local case_id="$1"
|
||||
local audio="/tmp/doctate-dictate-$case_id-$(date +%s).m4a"
|
||||
echo
|
||||
echo "Recording to $audio — press Ctrl-C to stop."
|
||||
ffmpeg -loglevel error -f pulse -i default -c:a aac -b:a 64k "$audio" || true
|
||||
if [ ! -s "$audio" ]; then
|
||||
echo "error: recording is empty — skipped" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "Recorded $(du -h "$audio" | cut -f1)."
|
||||
|
||||
local recorded_at
|
||||
recorded_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
echo "Uploading to $SERVER_URL/api/upload ..."
|
||||
local resp
|
||||
resp=$(curl -sS -X POST "$SERVER_URL/api/upload" \
|
||||
-H "X-API-Key: $API_KEY" \
|
||||
-F "case_id=$case_id" \
|
||||
-F "recorded_at=$recorded_at" \
|
||||
-F "audio=@$audio")
|
||||
echo "Response: $resp"
|
||||
|
||||
local stamp
|
||||
stamp=$(echo "$recorded_at" | tr ':' '-')
|
||||
local case_dir
|
||||
case_dir=$(case_dir_for "$case_id") || { echo "error: case dir not found" >&2; return 1; }
|
||||
local transcript="$case_dir/$stamp.transcript.txt"
|
||||
|
||||
echo "Waiting for transcript ..."
|
||||
local i
|
||||
for i in $(seq 1 $((TRANSCRIPT_TIMEOUT_SECS / 2))); do
|
||||
[ -f "$transcript" ] && break
|
||||
sleep 2
|
||||
done
|
||||
if [ ! -f "$transcript" ]; then
|
||||
echo "error: timeout waiting for transcript (>${TRANSCRIPT_TIMEOUT_SECS}s)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo
|
||||
show_case "$case_id"
|
||||
}
|
||||
|
||||
# Execute one action: c=continue, n=new, r=refresh.
|
||||
run_action() {
|
||||
local action="$1"
|
||||
case "$action" in
|
||||
c)
|
||||
[ -f "$STATE_FILE" ] || { echo "error: no current case — start a new one" >&2; return 1; }
|
||||
local case_id
|
||||
case_id=$(cat "$STATE_FILE")
|
||||
echo "Continuing case $case_id"
|
||||
record_and_show "$case_id"
|
||||
;;
|
||||
n)
|
||||
local case_id
|
||||
case_id=$(uuidgen)
|
||||
echo "$case_id" > "$STATE_FILE"
|
||||
echo "New case $case_id"
|
||||
record_and_show "$case_id"
|
||||
;;
|
||||
r)
|
||||
[ -f "$STATE_FILE" ] || { echo "error: no current case" >&2; return 1; }
|
||||
show_case "$(cat "$STATE_FILE")"
|
||||
;;
|
||||
*) echo "error: invalid choice: $action" >&2; return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Interactive menu prompt. Echoes the chosen mode letter on stdout.
|
||||
interactive_prompt() {
|
||||
local current count dir mode
|
||||
if [ -f "$STATE_FILE" ]; then
|
||||
current=$(cat "$STATE_FILE")
|
||||
count=0
|
||||
dir=$(case_dir_for "$current") && count=$(ls "$dir"/*.m4a 2>/dev/null | wc -l)
|
||||
echo "Current case: $current ($count recordings)" >&2
|
||||
read -rp "[c] continue, [n] new, [r] refresh, [q] quit → " mode
|
||||
else
|
||||
echo "No current case — starting a new one." >&2
|
||||
mode="n"
|
||||
fi
|
||||
echo "$mode"
|
||||
}
|
||||
|
||||
# -- Parse flag (single-shot when given; interactive loop otherwise) --
|
||||
case "${1:-}" in
|
||||
-c|--continue) run_action c; exit $? ;;
|
||||
-n|--new) run_action n; exit $? ;;
|
||||
-r|--refresh) run_action r; exit $? ;;
|
||||
-h|--help)
|
||||
sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
|
||||
exit 0
|
||||
;;
|
||||
"") ;;
|
||||
*) die "unknown flag: $1" ;;
|
||||
esac
|
||||
|
||||
# -- Interactive loop --
|
||||
while true; do
|
||||
mode=$(interactive_prompt)
|
||||
case "$mode" in
|
||||
q|"") exit 0 ;;
|
||||
c|n|r) run_action "$mode" || true ;;
|
||||
*) echo "error: invalid choice: $mode" >&2 ;;
|
||||
esac
|
||||
echo
|
||||
done
|
||||
Reference in New Issue
Block a user