#!/bin/bash # Batch-process every video under a directory through process_meeting.py, # mirroring the input folder structure into the output base. # # Recursive and space-safe (paths come off a Windows mount, so they often have # spaces). Each video's run folder is auto-created by process_meeting.py inside # the mirrored output subfolder. # # Usage: # ctrl/batch.sh -i -o [-e "mkv mp4 ..."] [-n] \ # [-- ] # # Examples: # # Everything under a mounted share, default extraction flags forwarded # ctrl/batch.sh -i "/mnt/win/trainings" -o output/batch \ # -- --embed-images --scene-detection --diarize # # # Dry run: just show which videos map to which output folders # ctrl/batch.sh -i "/mnt/win/trainings" -o output/batch -n # # # Only mp4/mov, custom whisper model # ctrl/batch.sh -i "./recordings" -o output/batch -e "mp4 mov" \ # -- --run-whisper --whisper-model large # # A video at /2026/team a/session 1.mkv produces # /2026/team a//... # i.e. the run folder lands inside the mirrored subtree. set -euo pipefail PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" cd "$PROJECT_DIR" # python: honor $PYTHON, else prefer python3 PYTHON="${PYTHON:-}" if [ -z "$PYTHON" ]; then if command -v python3 >/dev/null 2>&1; then PYTHON=python3; else PYTHON=python; fi fi usage() { sed -n '2,32p' "$0"; } INPUT="" OUTPUT="" EXTS="mkv mp4 mov avi m4v webm wmv ogg mp3 wav m4a opus flac aac" DRY=false FORWARD=() while [[ $# -gt 0 ]]; do case "$1" in -i|--input) INPUT="$2"; shift 2 ;; -o|--output) OUTPUT="$2"; shift 2 ;; -e|--ext) EXTS="$2"; shift 2 ;; -n|--dry-run) DRY=true; shift ;; -h|--help) usage; exit 0 ;; --) shift; FORWARD=("$@"); break ;; *) echo "Unknown arg: $1" >&2; usage; exit 1 ;; esac done [ -n "$INPUT" ] || { echo "ERROR: -i/--input is required" >&2; exit 1; } [ -n "$OUTPUT" ] || { echo "ERROR: -o/--output is required" >&2; exit 1; } [ -d "$INPUT" ] || { echo "ERROR: input dir not found: $INPUT" >&2; exit 1; } # Absolute paths so relative-path math is stable regardless of where we cd'd. INPUT="$(realpath "$INPUT")" mkdir -p "$OUTPUT" OUTPUT="$(realpath "$OUTPUT")" # Build the find extension filter: \( -iname '*.mkv' -o -iname '*.mp4' ... \) find_expr=() for ext in $EXTS; do find_expr+=( -iname "*.${ext}" -o ) done unset 'find_expr[${#find_expr[@]}-1]' # drop the trailing -o echo "Input : $INPUT" echo "Output: $OUTPUT" echo "Exts : $EXTS" [ "$DRY" = true ] && echo "(dry run — nothing will be processed)" echo total=0 ok=0 fail=0 # Process substitution (not a pipe) so counters survive into the summary. while IFS= read -r -d '' video; do total=$((total + 1)) rel="${video#"$INPUT"/}" # path relative to the input root reldir="$(dirname "$rel")" if [ "$reldir" = "." ]; then outdir="$OUTPUT" # video sat directly in the input root else outdir="$OUTPUT/$reldir" # mirror the subfolder structure fi echo "[$total] $rel" echo " -> $outdir" if [ "$DRY" = true ]; then continue fi mkdir -p "$outdir" # stdin from /dev/null: process_meeting's children (ffmpeg/whisperx) otherwise # inherit the loop's stdin and eat the rest of the file list — which stalls the # batch after the first item. if "$PYTHON" "$PROJECT_DIR/process_meeting.py" "$video" \ --output-dir "$outdir" "${FORWARD[@]+"${FORWARD[@]}"}" &2 fail=$((fail + 1)) fi echo done < <(find "$INPUT" -type f \( "${find_expr[@]}" \) -print0 | sort -z) echo "----------------------------------------" if [ "$DRY" = true ]; then echo "Found $total video(s)." else echo "Done. $total video(s): $ok ok, $fail failed." fi [ "$fail" -eq 0 ]