#!/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" local d="$DATA_PATH/$SLUG/$case_id" if [ -d "$d" ] && [ ! -f "$d/.deleted" ]; then echo "$d" return 0 fi 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/cases" } # 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