diff --git a/scripts/dictate.sh b/scripts/dictate.sh new file mode 100755 index 0000000..37a7df6 --- /dev/null +++ b/scripts/dictate.sh @@ -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