Files
doctate/scripts/dictate.sh
T
Brummel 990d166617 Refactor case listing and silent recording handling
The case listing on the "My Cases" page is now grouped by local date,
with labels for "Heute", "Gestern", or the ISO date. This improves
readability and organization.

Additionally, the handling of silent recordings has been refined.
Previously, the absence of usable recordings would result in an error.
Now, the analysis worker gracefully handles this by writing a stub
document and skipping the LLM call. This prevents unnecessary errors and
provides a clearer status for silent cases.

The `dictate.sh` script has been updated to simplify the case directory
lookup logic. Instead of iterating through `open` and `done`
subdirectories, it now directly checks for the case ID and ensures it's
not marked as deleted. This simplifies the script and improves
efficiency.

The `server/Cargo.toml` and `server/Cargo.lock` have been updated to
include the `num_threads` dependency and enable additional features for
the `time` crate, which are necessary for proper local time zone
handling.
2026-04-16 01:10:52 +02:00

200 lines
6.2 KiB
Bash
Executable File

#!/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/"
}
# 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