990d166617
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.
200 lines
6.2 KiB
Bash
Executable File
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
|