149 lines
4.9 KiB
Bash
Executable File
149 lines
4.9 KiB
Bash
Executable File
#!/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 <input-dir> -o <output-dir> [-e "mkv mp4 ..."] \
|
|
# [-l <list-file>] [-n] [-- <process_meeting.py flags>]
|
|
#
|
|
# 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
|
|
#
|
|
# # Only the meetings doocus found (relative paths), re-rooted at a new mount,
|
|
# # output into the doocus folder for later packaging:
|
|
# ctrl/batch.sh -i "/mnt/drive" -o docs-output -l docs-output/meetings.txt \
|
|
# -- --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 <input>/2026/team a/session 1.mkv produces
|
|
# <output>/2026/team a/<YYYYMMDD-NNN-session 1>/...
|
|
# 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
|
|
LIST="" # optional newline list of RELATIVE paths (re-rooted at INPUT)
|
|
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 ;;
|
|
-l|--list) LIST="$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; }
|
|
[ -z "$LIST" ] || [ -f "$LIST" ] || { echo "ERROR: list file not found: $LIST" >&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"
|
|
[ -n "$LIST" ] && echo "List : $LIST (relative paths, re-rooted at input)" || echo "Exts : $EXTS"
|
|
[ "$DRY" = true ] && echo "(dry run — nothing will be processed)"
|
|
echo
|
|
|
|
# Producer: either the given list of relative paths (re-rooted at INPUT) or a
|
|
# recursive find by extension. Both emit null-delimited absolute paths.
|
|
producer() {
|
|
if [ -n "$LIST" ]; then
|
|
while IFS= read -r rel || [ -n "$rel" ]; do
|
|
[ -z "$rel" ] && continue
|
|
printf '%s\0' "$INPUT/$rel"
|
|
done < "$LIST"
|
|
else
|
|
find "$INPUT" -type f \( "${find_expr[@]}" \) -print0 | sort -z
|
|
fi
|
|
}
|
|
|
|
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
|
|
|
|
if [ ! -f "$video" ]; then # list may name a path missing under this root
|
|
echo "[$total] $rel !! not found under input (skipped)" >&2
|
|
fail=$((fail + 1))
|
|
continue
|
|
fi
|
|
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[@]}"}" </dev/null; then
|
|
ok=$((ok + 1))
|
|
else
|
|
echo " !! FAILED (continuing)" >&2
|
|
fail=$((fail + 1))
|
|
fi
|
|
echo
|
|
done < <(producer)
|
|
|
|
echo "----------------------------------------"
|
|
if [ "$DRY" = true ]; then
|
|
echo "Found $total video(s)."
|
|
else
|
|
echo "Done. $total video(s): $ok ok, $fail failed."
|
|
fi
|
|
[ "$fail" -eq 0 ]
|