From 1a85a67e40bba2a77cc5951f785ad2024571e123 Mon Sep 17 00:00:00 2001 From: Jasper Bekkers Date: Thu, 12 Feb 2026 16:26:21 +0100 Subject: [PATCH] Add local video processing and tagging scripts - download-webdav.sh: Download files from WebDAV using rclone - tag-videos.sh: Interactive quadrant tagging with img2sixel previews - process-videos.sh: Batch process videos with FFmpeg compositing - process-videos.py: Python version for easier debugging - preview.sh: Quick video frame preview with img2sixel - detect-cuts.sh: Detect scene changes in videos - detect-slide-changes.sh: Detect slide changes in processed videos - find-talks.sh: Find talk boundaries using silence detection Co-Authored-By: Claude Opus 4.5 --- scripts/detect-cuts.sh | 36 +++++++ scripts/detect-slide-changes.sh | 67 +++++++++++++ scripts/download-webdav.sh | 44 +++++++++ scripts/find-talks.sh | 60 ++++++++++++ scripts/preview.sh | 17 ++++ scripts/process-videos.py | 163 +++++++++++++++++++++++++++++++ scripts/process-videos.sh | 168 ++++++++++++++++++++++++++++++++ scripts/tag-videos.sh | 142 +++++++++++++++++++++++++++ 8 files changed, 697 insertions(+) create mode 100644 scripts/detect-cuts.sh create mode 100644 scripts/detect-slide-changes.sh create mode 100644 scripts/download-webdav.sh create mode 100644 scripts/find-talks.sh create mode 100644 scripts/preview.sh create mode 100644 scripts/process-videos.py create mode 100644 scripts/process-videos.sh create mode 100644 scripts/tag-videos.sh diff --git a/scripts/detect-cuts.sh b/scripts/detect-cuts.sh new file mode 100644 index 0000000..2f755bc --- /dev/null +++ b/scripts/detect-cuts.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Detect scene changes / cut points in a video +# Usage: ./detect-cuts.sh [threshold] +# +# threshold: 0.0-1.0, lower = more sensitive (default: 0.3) +# Output: timestamps where scene changes occur + +VIDEO="${1:?Usage: $0 [threshold]}" +THRESHOLD="${2:-0.3}" + +if [ ! -f "$VIDEO" ]; then + echo "File not found: $VIDEO" + exit 1 +fi + +echo "Analyzing: $VIDEO" +echo "Threshold: $THRESHOLD (lower = more sensitive)" +echo "" +echo "Detecting scene changes..." +echo "" + +# Use FFmpeg's select filter with scene detection +# This outputs timestamps where scene change score exceeds threshold +ffmpeg -i "$VIDEO" -vf "select='gt(scene,$THRESHOLD)',showinfo" -vsync vfr -f null - 2>&1 | \ + grep showinfo | \ + sed -n 's/.*pts_time:\([0-9.]*\).*/\1/p' | \ + while read -r timestamp; do + # Convert to HH:MM:SS format + hours=$(echo "$timestamp / 3600" | bc) + mins=$(echo "($timestamp % 3600) / 60" | bc) + secs=$(echo "$timestamp % 60" | bc) + printf "%02d:%02d:%05.2f\n" "$hours" "$mins" "$secs" + done + +echo "" +echo "Done!" diff --git a/scripts/detect-slide-changes.sh b/scripts/detect-slide-changes.sh new file mode 100644 index 0000000..5f20d40 --- /dev/null +++ b/scripts/detect-slide-changes.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Detect major slide changes in the processed video's slide overlay area +# The slides appear as a 320px-height overlay in the bottom-right corner with 40px margin +# Usage: ./detect-slide-changes.sh [threshold] +# +# threshold: 0.0-1.0, higher = only major changes (default: 0.3) + +VIDEO="${1:?Usage: $0 [threshold]}" +THRESHOLD="${2:-0.3}" + +if [ ! -f "$VIDEO" ]; then + echo "File not found: $VIDEO" + exit 1 +fi + +# Get video dimensions +dimensions=$(ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 "$VIDEO" 2>/dev/null) +width=$(echo "$dimensions" | cut -d',' -f1) +height=$(echo "$dimensions" | cut -d',' -f2) + +# The slide overlay is 320px tall, aspect ratio ~16:9, so roughly 569x320 +# Positioned at bottom-right with 40px margin +# crop=w:h:x:y +crop_w=569 +crop_h=320 +crop_x=$((width - crop_w - 40)) +crop_y=$((height - crop_h - 40)) + +echo "========================================" +echo "Analyzing: $(basename "$VIDEO")" +echo "Video size: ${width}x${height}" +echo "Monitoring: Slide overlay area (${crop_w}x${crop_h} at ${crop_x},${crop_y})" +echo "Threshold: $THRESHOLD" +echo "========================================" +echo "" + +# Get video duration +duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$VIDEO" 2>/dev/null) +echo "Video duration: $(printf '%02d:%02d:%02d' $((${duration%.*}/3600)) $((${duration%.*}%3600/60)) $((${duration%.*}%60)))" +echo "" +echo "Detecting slide changes (this may take a while)..." +echo "" + +# Crop to the slide overlay area, then detect scene changes +ffmpeg -i "$VIDEO" \ + -vf "crop=${crop_w}:${crop_h}:${crop_x}:${crop_y},select='gt(scene,$THRESHOLD)',showinfo" \ + -vsync vfr -f null - 2>&1 | \ + grep showinfo | \ + sed -n 's/.*pts_time:\([0-9.]*\).*/\1/p' | \ + while read -r timestamp; do + hours=$(echo "$timestamp / 3600" | bc) + mins=$(echo "($timestamp % 3600) / 60" | bc) + secs=$(echo "$timestamp % 60" | bc) + printf "%02d:%02d:%05.2f\n" "$hours" "$mins" "$secs" + done | tee /tmp/slide_changes.txt + +count=$(wc -l < /tmp/slide_changes.txt) +echo "" +echo "========================================" +echo "Found $count potential talk boundaries" +echo "========================================" + +if [ "$count" -gt 0 ]; then + echo "" + echo "To preview each cut point:" + echo " ffmpeg -ss TIMESTAMP -i \"$VIDEO\" -vframes 1 -q:v 2 preview.jpg && img2sixel preview.jpg" +fi diff --git a/scripts/download-webdav.sh b/scripts/download-webdav.sh new file mode 100644 index 0000000..c9d9b37 --- /dev/null +++ b/scripts/download-webdav.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Download all files from a WebDAV server using rclone +# Usage: ./download-webdav.sh [output_dir] +# +# Requires: rclone +# Install with: curl https://rclone.org/install.sh | sudo bash + +set -e + +WEBDAV_URL="${1:?Usage: $0 [output_dir]}" +USERNAME="${2:?Username required}" +PASSWORD="${3:?Password required}" +OUTPUT_DIR="${4:-.}" + +# Remove trailing slash from URL +WEBDAV_URL="${WEBDAV_URL%/}" + +echo "Downloading from: $WEBDAV_URL" +echo "Output directory: $OUTPUT_DIR" + +mkdir -p "$OUTPUT_DIR" + +# Check if rclone is installed +if ! command -v rclone &> /dev/null; then + echo "rclone not found. Install with: curl https://rclone.org/install.sh | sudo bash" + exit 1 +fi + +# Use rclone with inline WebDAV config +# --webdav-url: WebDAV server URL +# --webdav-user: username +# --webdav-pass: password (obscured) +OBSCURED_PASS=$(rclone obscure "$PASSWORD") + +rclone copy \ + --webdav-url="$WEBDAV_URL" \ + --webdav-user="$USERNAME" \ + --webdav-pass="$OBSCURED_PASS" \ + --progress \ + --transfers=4 \ + ":webdav:/" \ + "$OUTPUT_DIR" + +echo "Download complete!" diff --git a/scripts/find-talks.sh b/scripts/find-talks.sh new file mode 100644 index 0000000..61cc284 --- /dev/null +++ b/scripts/find-talks.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Find talk boundaries in a conference video +# Looks for major scene changes combined with silence gaps +# Usage: ./find-talks.sh [min_gap_seconds] +# +# min_gap_seconds: minimum silence duration to consider a boundary (default: 2) + +VIDEO="${1:?Usage: $0 [min_gap_seconds]}" +MIN_GAP="${2:-2}" + +if [ ! -f "$VIDEO" ]; then + echo "File not found: $VIDEO" + exit 1 +fi + +echo "========================================" +echo "Analyzing: $(basename "$VIDEO")" +echo "Min silence gap: ${MIN_GAP}s" +echo "========================================" +echo "" + +# Get video duration +duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$VIDEO" 2>/dev/null) +echo "Video duration: $(printf '%02d:%02d:%02d' $((${duration%.*}/3600)) $((${duration%.*}%3600/60)) $((${duration%.*}%60)))" +echo "" + +# Detect silence periods (potential talk boundaries) +echo "Detecting silence gaps (this may take a while)..." +echo "" + +tmpfile=$(mktemp /tmp/silence_XXXXXX.txt) +trap "rm -f $tmpfile" EXIT + +# silencedetect finds periods of silence +# -50dB threshold, minimum duration of MIN_GAP seconds +ffmpeg -i "$VIDEO" -af "silencedetect=noise=-50dB:d=$MIN_GAP" -f null - 2>&1 | \ + grep -E "silence_(start|end)" > "$tmpfile" + +echo "Potential talk boundaries (silence gaps):" +echo "----------------------------------------" + +# Parse silence start/end pairs +grep "silence_start" "$tmpfile" | while read -r line; do + start=$(echo "$line" | sed -n 's/.*silence_start: \([0-9.]*\).*/\1/p') + if [ -n "$start" ]; then + hours=$(echo "$start / 3600" | bc) + mins=$(echo "($start % 3600) / 60" | bc) + secs=$(echo "$start % 60" | bc) + printf " %02d:%02d:%05.2f\n" "$hours" "$mins" "$secs" + fi +done + +echo "" +echo "========================================" +echo "" +echo "To preview a cut point, run:" +echo " ./preview.sh \"$VIDEO\" " +echo "" +echo "To extract a segment:" +echo " ffmpeg -ss START -to END -i \"$VIDEO\" -c copy output.mp4" diff --git a/scripts/preview.sh b/scripts/preview.sh new file mode 100644 index 0000000..6b1c727 --- /dev/null +++ b/scripts/preview.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Show a full-size preview of frame 0 of a video using img2sixel +# Usage: ./preview.sh + +VIDEO="${1:?Usage: $0 }" + +if [ ! -f "$VIDEO" ]; then + echo "File not found: $VIDEO" + exit 1 +fi + +tmpfile=$(mktemp /tmp/preview_XXXXXX.jpg) +trap "rm -f $tmpfile" EXIT + +ffmpeg -y -ss 0 -i "$VIDEO" -vframes 1 -q:v 2 "$tmpfile" 2>/dev/null + +img2sixel "$tmpfile" diff --git a/scripts/process-videos.py b/scripts/process-videos.py new file mode 100644 index 0000000..e377ec0 --- /dev/null +++ b/scripts/process-videos.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +""" +Process all tagged videos using FFmpeg with background compositing. + +Usage: ./process-videos.py [background_image] [output_dir] + +Reads: ~/videos/quadrant-tags.json +Requires: ffmpeg +""" + +import json +import os +import subprocess +import sys +from pathlib import Path + + +def get_crop(quadrant: str) -> str: + """Get FFmpeg crop parameters for a quadrant.""" + crops = { + "top-left": "1912:1072:4:4", + "top-right": "1912:1072:1924:4", + "bottom-left": "1912:1072:4:1084", + "bottom-right": "1912:1072:1924:1084", + } + if quadrant not in crops: + raise ValueError(f"Invalid quadrant: {quadrant}") + return crops[quadrant] + + +def build_filter(presenter_crop: str, slides_crop: str) -> str: + """Build FFmpeg filter complex. + + Output: composited video with slides large and presenter small in corner. + """ + return ( + f"[1:v]scale=2560:1440[bg]; " + f"[0:v]crop={slides_crop}[slides_cropped]; " + f"[slides_cropped]scale=1920:1080[slides]; " + f"[0:v]crop={presenter_crop}[presenter_raw]; " + f"[presenter_raw]scale=-1:320[presenter]; " + f"[slides]scale=1920:1080[slides_s]; " + f"[bg][slides_s]overlay=(W-w)/2:(H-h)/2[base]; " + f"[base][presenter]overlay=x=W-w-40:y=H-h-40[outv]" + ) + + +def process_video(input_path: str, output_path: str, bg_image: str, + presenter: str, slides: str) -> bool: + """Process a single video with FFmpeg.""" + try: + presenter_crop = get_crop(presenter) + slides_crop = get_crop(slides) + except ValueError as e: + print(f" Error: {e}") + return False + + filter_complex = build_filter(presenter_crop, slides_crop) + + cmd = [ + "ffmpeg", "-y", + "-i", input_path, + "-i", bg_image, + "-filter_complex", filter_complex, + "-map", "[outv]", + "-map", "0:a?", + "-c:v", "libx264", + "-crf", "18", + "-preset", "veryfast", + "-threads", "0", + "-c:a", "copy", + output_path + ] + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True + ) + if result.returncode != 0: + print(f" FFmpeg error: {result.stderr[-500:]}") + return False + return True + except Exception as e: + print(f" Error running FFmpeg: {e}") + return False + + +def main(): + # Parse arguments + bg_image = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser("~/gpc-bg.png") + output_dir = sys.argv[2] if len(sys.argv) > 2 else os.path.expanduser("~/videos/processed") + tags_file = os.path.expanduser("~/videos/quadrant-tags.json") + + # Check dependencies + if not Path(bg_image).exists(): + print(f"Background image not found: {bg_image}") + sys.exit(1) + + if not Path(tags_file).exists(): + print(f"Tags file not found: {tags_file}") + print("Run tag-videos.sh first to create it") + sys.exit(1) + + # Create output directory + Path(output_dir).mkdir(parents=True, exist_ok=True) + + # Load tags + with open(tags_file) as f: + tags = json.load(f) + + total = len(tags) + print("=" * 40) + print(f"Processing {total} video(s)") + print(f"Background: {bg_image}") + print(f"Output dir: {output_dir}") + print("=" * 40) + print() + + # Process each video + for i, (filename, data) in enumerate(tags.items(), 1): + presenter = data["presenter"] + slides = data["slides"] + input_path = data["path"] + + # Output filename (change extension to .mp4) + output_name = Path(filename).stem + ".mp4" + output_path = os.path.join(output_dir, output_name) + + # Skip if already processed + if Path(output_path).exists(): + print(f"[{i}/{total}] Skipping {filename} (already exists)") + continue + + print(f"[{i}/{total}] Processing: {filename}") + print(f" Input: {input_path}") + print(f" Presenter: {presenter}") + print(f" Slides: {slides}") + print(f" Output: {output_path}") + print(" Running FFmpeg...") + + if process_video(input_path, output_path, bg_image, presenter, slides): + print(" Done!") + else: + print(" FAILED!") + print() + + print("=" * 40) + print("Processing complete!") + print(f"Output directory: {output_dir}") + print("=" * 40) + + # Show summary + print() + print("Processed files:") + for f in Path(output_dir).glob("*.mp4"): + size_mb = f.stat().st_size / (1024 * 1024) + print(f" {f.name}: {size_mb:.1f} MB") + + +if __name__ == "__main__": + main() diff --git a/scripts/process-videos.sh b/scripts/process-videos.sh new file mode 100644 index 0000000..3a4d1d2 --- /dev/null +++ b/scripts/process-videos.sh @@ -0,0 +1,168 @@ +#!/bin/bash +# Process all tagged videos using FFmpeg with background compositing +# Usage: ./process-videos.sh [background_image] [output_dir] +# +# Reads: ~/videos/quadrant-tags.json +# Requires: ffmpeg, jq + +# Don't use set -e as it causes issues with while loops +# set -e + +BG_IMAGE="${1:-$HOME/gpc-bg.png}" +OUTPUT_DIR="${2:-$HOME/videos/processed}" +TAGS_FILE="$HOME/videos/quadrant-tags.json" + +# Check dependencies +if ! command -v ffmpeg &> /dev/null; then + echo "ffmpeg not found" + exit 1 +fi + +if ! command -v jq &> /dev/null; then + echo "jq not found. Install with: apt install jq" + exit 1 +fi + +# Check tags file exists +if [ ! -f "$TAGS_FILE" ]; then + echo "Tags file not found: $TAGS_FILE" + echo "Run tag-videos.sh first to create it" + exit 1 +fi + +# Check background image exists +if [ ! -f "$BG_IMAGE" ]; then + echo "Background image not found: $BG_IMAGE" + echo "Download it first or provide path as first argument" + exit 1 +fi + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +# Function to get crop parameters for a quadrant +# Video is 3840x2160 (4K), divided into 4 quadrants of 1920x1080 each +# We apply a 4px offset to trim borders +get_crop() { + local quadrant="$1" + case "$quadrant" in + "top-left") echo "1912:1072:4:4" ;; + "top-right") echo "1912:1072:1924:4" ;; + "bottom-left") echo "1912:1072:4:1084" ;; + "bottom-right") echo "1912:1072:1924:1084" ;; + *) + echo "Invalid quadrant: $quadrant" >&2 + return 1 + ;; + esac +} + +# Build FFmpeg filter complex +# Input 0: video, Input 1: background image +# Output: composited video with slides large and presenter small in corner +build_filter() { + local presenter_crop="$1" + local slides_crop="$2" + + # Filter explanation: + # 1. Scale background to 2560x1440 + # 2. Crop slides quadrant and scale to 1920x1080 (big, centered) + # 3. Crop presenter quadrant and scale to 320px height (small, corner) + # 4. Overlay slides centered on background + # 5. Overlay presenter in bottom-right corner with 40px margin + echo "[1:v]scale=2560:1440[bg]; \ +[0:v]crop=${slides_crop}[slides_cropped]; \ +[slides_cropped]scale=1920:1080[slides]; \ +[0:v]crop=${presenter_crop}[presenter_raw]; \ +[presenter_raw]scale=-1:320[presenter]; \ +[slides]scale=1920:1080[slides_s]; \ +[bg][slides_s]overlay=(W-w)/2:(H-h)/2[base]; \ +[base][presenter]overlay=x=W-w-40:y=H-h-40[outv]" +} + +# Count total videos +total=$(jq 'length' "$TAGS_FILE") +current=0 + +echo "========================================" +echo "Processing $total video(s)" +echo "Background: $BG_IMAGE" +echo "Output dir: $OUTPUT_DIR" +echo "========================================" +echo "" + +# Process each video - write entries to temp file to avoid subshell issues +tmpentries=$(mktemp) +jq -r 'to_entries[] | @base64' "$TAGS_FILE" 2>/dev/null | grep -v '^$' > "$tmpentries" + +while IFS= read -r entry; do + # Decode the entry - skip if decoding fails + decoded=$(echo "$entry" | base64 -d 2>/dev/null) + if [ -z "$decoded" ]; then + continue + fi + filename=$(echo "$decoded" | jq -r '.key' 2>/dev/null) || continue + [ -z "$filename" ] && continue + # Sanity check - filename should end in .mov + [[ "$filename" != *.mov ]] && continue + presenter=$(echo "$decoded" | jq -r '.value.presenter' 2>/dev/null) + slides=$(echo "$decoded" | jq -r '.value.slides' 2>/dev/null) + input_path=$(echo "$decoded" | jq -r '.value.path' 2>/dev/null) + + current=$((current + 1)) + + # Output filename (change extension to .mp4) + output_file="$OUTPUT_DIR/${filename%.*}.mp4" + + # Skip if already processed + if [ -f "$output_file" ]; then + echo "[$current/$total] Skipping $filename (already exists)" + continue + fi + + echo "[$current/$total] Processing: $filename" + echo " Input: $input_path" + echo " Presenter: $presenter" + echo " Slides: $slides" + echo " Output: $output_file" + + # Get crop parameters + presenter_crop=$(get_crop "$presenter") || { echo " Skipping - invalid presenter quadrant"; continue; } + slides_crop=$(get_crop "$slides") || { echo " Skipping - invalid slides quadrant"; continue; } + + # Build filter + filter=$(build_filter "$presenter_crop" "$slides_crop") + + # Run FFmpeg + echo " Running FFmpeg..." + if ffmpeg -y \ + -i "$input_path" \ + -i "$BG_IMAGE" \ + -filter_complex "$filter" \ + -map "[outv]" \ + -map "0:a?" \ + -c:v libx264 \ + -crf 18 \ + -preset veryfast \ + -threads 0 \ + -c:a copy \ + "$output_file" \ + 2>&1 | tail -5; then + echo " Done!" + else + echo " FAILED!" + fi + echo "" +done < "$tmpentries" + +rm -f "$tmpentries" + +echo "========================================" +echo "Processing complete!" +echo "Output directory: $OUTPUT_DIR" +echo "========================================" + +# Show summary +echo "" +echo "Processed files:" +ls -lh "$OUTPUT_DIR"/*.mp4 2>/dev/null || echo " No files processed" diff --git a/scripts/tag-videos.sh b/scripts/tag-videos.sh new file mode 100644 index 0000000..734567d --- /dev/null +++ b/scripts/tag-videos.sh @@ -0,0 +1,142 @@ +#!/bin/bash +# Tag video quadrants interactively using thumbnails displayed via img2sixel +# Usage: ./tag-videos.sh +# +# Requires: ffmpeg, img2sixel (from libsixel-bin) +# Output: ~/videos/quadrant-tags.json + +set -e + +VIDEO_DIR="${1:?Usage: $0 }" +OUTPUT_FILE="$HOME/videos/quadrant-tags.json" + +# Create output directory +mkdir -p "$HOME/videos" + +# Initialize JSON file if it doesn't exist +if [ ! -f "$OUTPUT_FILE" ]; then + echo "{}" > "$OUTPUT_FILE" +fi + +# Find all .mov files +mapfile -t videos < <(find "$VIDEO_DIR" -type f -name "*.mov" | sort) + +if [ ${#videos[@]} -eq 0 ]; then + echo "No .mov files found in $VIDEO_DIR" + exit 1 +fi + +echo "Found ${#videos[@]} video(s) to tag" +echo "" + +# Process each video +for video in "${videos[@]}"; do + filename=$(basename "$video") + + # Check if already tagged + if jq -e ".\"$filename\"" "$OUTPUT_FILE" > /dev/null 2>&1; then + echo "Skipping $filename (already tagged)" + continue + fi + + echo "========================================" + echo "Processing: $filename" + echo "========================================" + + # Create temp file for thumbnail + tmpfile=$(mktemp /tmp/thumb_XXXXXX.jpg) + trap "rm -f $tmpfile" EXIT + + # Get video duration and extract frame from middle + duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$video" 2>/dev/null | cut -d'.' -f1) + if [ -z "$duration" ] || [ "$duration" -lt 2 ]; then + seek=0 + else + seek=$((duration / 2)) + fi + ffmpeg -y -ss "$seek" -i "$video" -vframes 1 -q:v 2 "$tmpfile" 2>/dev/null + + # Display the thumbnail + echo "" + img2sixel -w 800 "$tmpfile" + echo "" + + # Show quadrant layout + echo "Quadrant layout:" + echo " +-------------+-------------+" + echo " | TL | TR |" + echo " | (top-left) | (top-right) |" + echo " +-------------+-------------+" + echo " | BL | BR |" + echo " | (bot-left) | (bot-right) |" + echo " +-------------+-------------+" + echo "" + + # Ask for presenter quadrant + while true; do + read -p "Presenter quadrant (TL/TR/BL/BR) or 's' to skip: " presenter + presenter=$(echo "$presenter" | tr '[:lower:]' '[:upper:]') + case "$presenter" in + TL) presenter="top-left"; break;; + TR) presenter="top-right"; break;; + BL) presenter="bottom-left"; break;; + BR) presenter="bottom-right"; break;; + S) presenter=""; break;; + *) echo "Invalid choice. Use TL, TR, BL, BR, or s to skip.";; + esac + done + + if [ -z "$presenter" ]; then + echo "Skipping $filename" + rm -f "$tmpfile" + continue + fi + + # Ask for slides quadrant + while true; do + read -p "Slides quadrant (TL/TR/BL/BR): " slides + slides=$(echo "$slides" | tr '[:lower:]' '[:upper:]') + case "$slides" in + TL) slides="top-left"; break;; + TR) slides="top-right"; break;; + BL) slides="bottom-left"; break;; + BR) slides="bottom-right"; break;; + *) echo "Invalid choice. Use TL, TR, BL, or BR.";; + esac + done + + # Warn if same quadrant selected + if [ "$presenter" = "$slides" ]; then + echo "Warning: Presenter and slides are the same quadrant!" + read -p "Continue anyway? (y/n): " confirm + if [ "$confirm" != "y" ]; then + rm -f "$tmpfile" + continue + fi + fi + + # Save to JSON + tmp_json=$(mktemp) + jq --arg file "$filename" \ + --arg pres "$presenter" \ + --arg slides "$slides" \ + --arg path "$video" \ + '.[$file] = {"presenter": $pres, "slides": $slides, "path": $path}' \ + "$OUTPUT_FILE" > "$tmp_json" + mv "$tmp_json" "$OUTPUT_FILE" + + echo "Saved: $filename -> presenter=$presenter, slides=$slides" + echo "" + + rm -f "$tmpfile" +done + +echo "========================================" +echo "Tagging complete!" +echo "Results saved to: $OUTPUT_FILE" +echo "========================================" + +# Show summary +echo "" +echo "Tagged videos:" +jq -r 'to_entries[] | " \(.key): presenter=\(.value.presenter), slides=\(.value.slides)"' "$OUTPUT_FILE"