Skip to content

Automation scripts reference

Moved out of README.md (see development.md for running from source and an overview, and the README for installing).

Disk Scanner (my_scripts/tools/disk_scanner.py)

Archived (item: Super Manager) - superseded by super_manager.py --mode scan below, still fully functional and directly runnable, just no longer listed in the main script list (see "Archive tab" further down). Recursively scans a drive/folder (default: your home folder), read-only - never modifies, moves, or deletes anything. Runs ffprobe on every recognized media file (codec, bitrate, duration, resolution), finds duplicate files via a tiered hash strategy (size match -> quick partial hash -> full SHA-256 only for real candidates, so it doesn't fully hash every large video on a multi-TB drive), and flags common junk files (Thumbs.db, desktop.ini, 0-byte files, corrupted media). Writes everything to a single data/report.json.

uv run python my_scripts/tools/disk_scanner.py --target_dir "D:\Videos" --workers 4
A full scan of a large, heavily-populated drive can take a while (ffprobe runs per media file) - keep an eye on the progress bar in the console/GUI log. Junctions/symlinks (e.g. those .pnpm-store/node_modules create) are detected and skipped rather than followed, to avoid double-counting and infinite recursion on a broken link.

Also produces a formatted report.xlsx (summary/duplicates/junk/media sheets, RTL, styled headers) and a lightweight single-page report.html summary (top duplicate groups/junk files, quick to open in any browser) alongside the JSON by default - pass --no-excel/--no-html to skip either. To convert an existing report.json without re-scanning, use my_scripts/tools/report_to_excel.py.

Clip Detection & Cutting (my_scripts/video/story_cutter.py)

Scans long gameplay recordings in a folder, finds candidate highlight moments by audio loudness peaks (dBFS), filters out ones with no accompanying visual motion (OpenCV frame-diff), then sends surviving candidates to Gemini AI to confirm they're an actual interesting moment and identify the game - clips are cut and sorted into clips/<game name>/.

Audio peaks alone miss highlights that have no loud sound behind them (e.g. no commentary, or a game whose important moments are shown only as on-screen text). Pass --use-ocr to additionally scan the entire video at a fixed interval for HUD keywords (Victory, Eliminated, Headshot, MVP, etc. - see OCR_HIGHLIGHT_KEYWORDS in utils/media/ocr_highlights.py) independent of audio - these OCR-confirmed points skip the visual-motion filter (matched text is already strong confirmation) and get merged with the audio-based ones. Requires Tesseract-OCR installed separately (not bundled, unlike ffmpeg) - utils/media/tesseract_locator.py looks for the standard Windows install path or falls back to PATH, and the script exits with a clear error if --use-ocr is passed without it:

uv run python my_scripts/video/story_cutter.py --input_dir "D:\Gameplay" --use-ocr
Run once with and once without the flag on the same input to compare how many extra clips OCR catches - the log prints OCR-MATCH/SKIP-VISUAL per candidate so you can see which detection path produced each clip. --ocr-keywords "word1,word2" adds extra keywords on top of the generic list for games without a kill-feed HUD (narrative/dialogue-driven games) - e.g. --ocr-keywords "wheatley,glados,explosion,alarm" for Portal 2.

AI judge: Gemini vs. local (--ai-provider gemini|local, default gemini) - each surviving candidate gets judged by an AI model that confirms it's a real interesting moment and identifies the game. gemini mode calls the Gemini API and automatically falls back to a local Ollama model (see below) if Gemini fails or its quota is exhausted, instead of blindly auto-approving. local mode skips Gemini entirely and always judges through Ollama - zero API calls, zero cost, but noticeably less accurate at identifying which game is being played (Gemini reliably named "Portal 2"; the local llava:7b sometimes guessed "Fallout 3" from similar-looking corridors). Requires Ollama installed and running locally with a vision model pulled (ollama pull llava:7b, ~4.7GB) - utils/media/local_vision_judge.py handles the HTTP calls (no new heavy Python dependency; Ollama runs the model itself), and --local-fallback-model picks which pulled model to use (default llava:7b).

This choice is also exposed in the GUI: select story_cutter.py in the sidebar and a "Judge clips using" dropdown appears in its Settings tab (Gemini / local), persisted in config.json.

Gemini's free tier caps out fast for long recordings - it's a daily request quota (not just per-minute), so a single ~3-hour video with a talkative game can exhaust it in the first few candidates, after which every remaining clip used to get silently auto-approved. The paid tier removes that cap and is inexpensive in practice (roughly $2-6 for tens of hours of footage at typical candidate-detection rates, using Gemini Flash pricing as of 2026) - see Gemini API pricing for current rates.

Loud quota warnings (utils/media/gemini_quota.py) - when a Gemini call fails with a quota error (429 / RESOURCE_EXHAUSTED) story_cutter.py prints one [WARNING] saying what happens next (local Ollama judging, or auto-approval without judging when Ollama is missing), prints a [WARNING] for every clip approved without real judging, and ends with a summary line. --gemini-daily-limit N adds an early warning at 80 percent of the limit you know. Startup health check - utils/diagnostics.print_startup_health() runs when the web server starts (ffmpeg/ffprobe, disk, workspace, a read-only parse of data/*.json, and a clear Hebrew warning if GEMINI_API_KEY is missing); it never blocks startup and never writes to data/.

Timelapse Maker (my_scripts/video/timelapse_maker.py)

Archived (item: Archive tab) - superseded by video_editor.py --target-seconds (identical underlying speed/atempo logic, plus watermark/captions/LUT/everything else this script doesn't offer), still fully functional and directly runnable, just no longer listed in the main script list. Compresses long recordings (even multiple hours) into a short, very-fast-motion clip by speeding up the whole video with ffmpeg's setpts filter - not highlight detection like story_cutter.py, everything in the source plays, just compressed into a fixed target duration (60s by default). Produces both a horizontal (1920x1080) and a vertical (1080x1920, center-cropped) version per source video, written to timelapses/:

uv run python my_scripts/video/timelapse_maker.py --input_dir "D:\Gameplay" --target-seconds 60
Audio is dropped by default (-an) - at the speed factors this implies for multi-hour sources (hundreds of times normal speed), sped-up audio is just noise. Pass --keep-audio to keep it anyway (chained atempo filters handle arbitrarily high factors). --fps sets the output framerate (default 30).

Highlight Reel Maker (my_scripts/video/highlight_reel_maker.py)

Merges several clips into one summary video - e.g. point it at the folder approval_cog.py moves Discord-approved clips into, and get one combined reel instead of manually stitching them elsewhere:

uv run python my_scripts/video/highlight_reel_maker.py --input_dir "D:\ApprovedClips" --max-clips 20
Each clip is normalized (scaled/padded to a shared resolution, resampled audio) via ffmpeg's concat filter rather than the concat demuxer, since clips from different games/recordings essentially never share identical codec/resolution/framerate - the demuxer requires that and just fails otherwise, the filter re-encodes everything to match first (utils/media/highlight_reel.py builds the filter graph, kept separate from the script so it's testable without invoking ffmpeg). A clip with no audio track gets a synthesized silent one instead of breaking the merge (concat requires every input to carry the same stream layout). Picks the --max-clips most-recently-modified clips (default 20) from the folder; --vertical switches to the 1080x1920 crop instead of 1920x1080; output goes to workspace/highlight_reels/vertical/ or workspace/highlight_reels/horizontal/ accordingly (the orientation is also encoded in the filename now, e.g. highlight_reel_5clips_vertical.mp4 - a re-run with the other orientation used to silently overwrite the previous file since neither the folder nor the filename distinguished them).

ffprobe caching: video_editor.py, timelapse_maker.py, and highlight_reel_maker.py all read a clip's duration/resolution/audio-presence via utils/media/ffprobe_cache.py's probe_video() instead of calling ffprobe directly - one combined probe per file (duration+resolution+audio together) instead of a separate ffprobe subprocess per piece of information, cached both in-process and to data/ffprobe_cache.json (gitignored) keyed by path+mtime+size, so the same unchanged file probed again later - even from a different script in a chained pipeline (item 15) - doesn't re-invoke ffprobe at all. A capped, insertion-order eviction (2000 entries) keeps the cache file from growing forever.

Duplicate Scene Detector (my_scripts/video/duplicate_scene_detector.py)

Finds scenes that repeat within a single video - a loading screen that shows up twice, a segment accidentally recorded twice - as opposed to duplicate_cleaner.py below, which finds whole duplicate files. Read-only, writes only a JSON report (workspace/reports/disk/duplicate_scenes_report.json by default):

uv run python my_scripts/video/duplicate_scene_detector.py --input_dir "D:\Clips" --interval-seconds 3 --hash-threshold 8
Samples one frame every --interval-seconds (default 3s), reduces each to an 8x8 grayscale average hash (utils/media/duplicate_scene_detector.py - cheap, no extra dependency beyond the opencv-python/Pillow already in use elsewhere), and flags pairs of samples whose hashes are within --hash-threshold Hamming distance (default 8 of 64 bits, ~12.5%) and at least --min-gap-seconds apart (default 5s) - the gap requirement is what distinguishes a genuinely repeated scene from two adjacent samples of the same static moment. Reuses utils/media/ffprobe_cache.py (item 20) for each clip's duration. - Cross-file duplicate clip detection - beyond scenes repeating within one file, also flags pairs of separate files in the folder that are probably the same clip (e.g. a highlight exported/imported twice under different names). Reuses the same already-sampled per-file hashes (no extra ffmpeg calls) - compute_frame_match_fraction() checks what fraction of one file's frames have a close match somewhere in the other's (not position-aligned, so clips starting at a different offset still match), and find_cross_file_duplicate_pairs() flags a pair when the symmetric average of both directions clears match_fraction_threshold (0.8 by default). Results land in the console output, the RESULT: line's cross_file_duplicates_found, and a _cross_file_duplicates list in the JSON report.

Duplicate Cleaner (my_scripts/tools/duplicate_cleaner.py)

Archived (item: Super Manager) - superseded by super_manager.py --mode clean below, still fully functional and directly runnable, just no longer listed in the main script list. Reads the duplicate groups from an existing report.json, keeps one copy per group (oldest by default, --keep newest to flip that) and moves the rest to the Recycle Bin only (via send2trash - never a permanent delete). Running it with no flags (including from the GUI's Run button, which can't pass custom flags) is always a dry-run plan that moves nothing; add --execute from the command line to actually move files:

uv run python my_scripts/tools/duplicate_cleaner.py --execute --keep oldest

Super Manager - מנהל-העל (my_scripts/tools/super_manager.py)

Unifies Disk Scanner + Duplicate Cleaner + report_to_excel.py (converts an existing report.json to a formatted Excel without re-scanning - never had its own README section) into one script with a --mode scan|report|clean selector, the same way video_editor.py unifies many independent video operations - except unlike video_editor.py's single composed ffmpeg pass, these are three genuinely separate jobs with three separate outputs (a scan produces report.json, clean reads one and moves files, report reads one and writes an .xlsx), so a mode here selects one job rather than composing several into one. Each mode is a thin dispatch to the original script's own main() (dynamically loaded via importlib, sys.argv filtered to just that mode's own recognized flags via parse_known_args()'s own leftover list - not a hand-rolled sys.argv filter, which would leak the --mode value itself in as a stray positional argument) - behavior is byte-for-byte identical to running the original script directly, this is purely a UI/discoverability unification, not a rewrite:

uv run python my_scripts/tools/super_manager.py --mode scan --target_dir "D:\Videos"
uv run python my_scripts/tools/super_manager.py --mode report --input workspace/reports/disk/report.json
uv run python my_scripts/tools/super_manager.py --mode clean --execute
In both UIs, selecting this script shows a mode dropdown with only the relevant fields for the chosen mode (scan: target folder + Excel/HTML export toggles; clean: keep-policy + an explicit "actually execute" checkbox, mirroring the original's dry-run-by-default safety; report: no extra fields, uses the default data/report.json/.xlsx paths). Item: scoped deliberately - file_renamer.py/audio_converter.py/check_pos.py are genuinely unrelated single-purpose utilities (generic bulk rename, WAV→OGG conversion, a mouse-position finder) with no natural pipeline connection to scan→report→clean, so merging them in would've been arbitrary bundling rather than a real unification; they're left as their own separate scripts. migrate_secrets_to_vault.py (a one-time .env→Credential Manager migration) was archived alongside this batch for an unrelated reason - not redundant because of a super-script, just because its purpose is already spent for anyone who's already run it once.

Super Streamer - משדר-העל (my_scripts/stream/super_streamer.py)

Unifies Twitch Clip Import + OBS Replay Watcher + Capture Folder Watcher into one script with a --mode twitch|obs|folder selector - all three ultimately do the same thing ("get clips into the Import folder"), just from a different capture source, so unlike Super Manager's three-separate-outputs design this is closer to video_editor.py's spirit (one underlying goal, several interchangeable ways to reach it) even though the mechanics still don't compose into one pass: twitch is a one-shot batch download that runs and exits, while obs/folder are long-running listeners that run until stopped (same "🛑 עצור" button in both UIs that already stops any long-running script). Same dispatch mechanism as Super Manager - obs_replay_watcher.py/capture_folder_watcher.py got a small refactor (their existing if __name__ == "__main__": block moved into a proper def main():, identical behavior, just matching twitch_clip_import.py's existing convention) so all three can be dispatched to uniformly:

uv run python my_scripts/stream/super_streamer.py --mode twitch --limit 10
uv run python my_scripts/stream/super_streamer.py --mode obs
uv run python my_scripts/stream/super_streamer.py --mode folder
The mode dropdown in both UIs only shows the "how many clips to download" field for twitch mode - obs/folder have no extra fields of their own (they read capture_watch_dir/clip_hotkey from the existing Rocket Shorts settings, unchanged).

Cleanup Old Sources (my_scripts/video/cleanup_published_sources.py)

Moves (never deletes) source clips into an Archived_Sources subfolder once they're older than both --retention_days and the last successful rocket_shorts_publisher.py run recorded in job_history - a file that predates the last publish has almost certainly already been processed (every run scans the whole Import folder). Runs from the GUI's scheduler or manually; with no --retention_days (and none set in rocket_settings.json's cleanup_retention_days) it's a no-op. - Undo last move batch (utils/shared/move_undo_log.py) - every successful (non-dry-run) run records the exact source→destination pairs it moved to data/last_move_batch.json (gitignored, only the most recent run - not a full history). An "↩️ בטל פעולה אחרונה" button appears in both UIs' history panel (desktop ui/stats_tab.py, web web_app.py's /api/undo/status + /api/undo/last_move_batch) whenever a batch is pending, moving every file back to where it came from. A file whose archived copy no longer exists, or whose original location is now occupied by something else, is skipped rather than overwritten or erroring out the whole undo. This is scoped to this one script - duplicate_cleaner.py below moves files to the Recycle Bin via send2trash, which already has its own OS-level undo (restore from Recycle Bin) and no simple cross-platform "restore to this exact original path" API to build a matching feature on.

Periodic Recap (my_scripts/video/weekly_compilation.py)

Concatenates the YouTube (or TikTok, --source tiktok) clips published in the last N days into a single recap video, plus a <output>.chapters.txt sidecar - one timestamp name line per clip, starting at 0:00, ready to paste into the YouTube description so YouTube auto-detects it as a chapters list (needs at least 3 clips of 10+ seconds each to actually activate on YouTube's side; not enforced here, the file is generated either way). --period week|month|year is a convenience for --days 7|30|365 respectively (an explicit --days always overrides --period); the output filename reflects the period (Weekly_Recap_.../Monthly_Recap_.../Yearly_Recap_...), and a previous recap's own output is always excluded from being swept up as an input clip into the next one. Tries a fast -c copy concat first, falling back to a full re-encode if the clips don't share identical codec parameters.

Automatic scheduling (desktop ui/rocket_settings_tab.py, web web_app.py's Rocket Shorts settings): enabling "ריקאפ אוטומטי" runs this via cogs/scheduler_cog.py's daily maintenance loop, with a period selector (week/month/year) alongside the existing day-of-week picker - week triggers on the chosen weekday, month/year trigger on a fixed date (the 1st of the month / January 1st) and ignore the weekday picker entirely. A failed automatic run DMs the bot owner, same as the other scheduled maintenance tasks.

Annual "Wrapped" Video (my_scripts/video/wrapped_generator.py)

A Spotify-Wrapped-style yearly recap (item: annual "Wrapped" video) - distinct from Periodic Recap above in both scope and time range: --period year there means "the last 365 rolling days" and just concatenates every clip; --year here (default: the current calendar year) means "your 2026", and opens with a handful of stat title cards before the clips themselves. Runs like any other script (auto-discovered in both the desktop sidebar and the web app's script list under "🎬 פייפליין וידאו" - no dedicated settings panel, since a once-a-year action with sensible defaults doesn't need one) with --source youtube|tiktok selecting the same published-clips folder Periodic Recap reads from.

Stats are computed by utils/media/wrapped_stats.py (pure logic, independently unit-tested) from data that already exists elsewhere in the project - clips published this year (folder scan, same as Periodic Recap's own clip-gathering, just calendar-year-bounded instead of a rolling window), successful render runs and total render hours (utils/shared/job_history.py), the year's single most common AI tag (workspace/.tags_cache/, matched by the mtime embedded in each cache filename - works even for a clip that's since been moved/deleted, since the cache file itself is all that's needed), and hours played per game this year (utils/media/game_playtime.py's new game_playtime_daily table - a day-bucketed sibling of the all-time-cumulative table the Personal Stats Dashboard reads, added specifically so a calendar-year total is actually derivable; a Wrapped generated soon after this feature shipped will honestly show a small number here, the same documented limitation the dashboard's own hours-played stat already has). A stat with no real data (e.g. no tags were ever generated) simply gets no card, rather than showing a misleading zero.

Each stat becomes a short (3s) title card - rendered as a PNG via PIL (font size scales with the video's own resolution, shrinking further if a line is still too wide, so the same card layout works for both horizontal/YouTube and vertical/TikTok output) and converted to a video segment via ffmpeg, then prepended to a highlight reel: up to 12 clips evenly sampled across the whole year (not just the first 12 chronologically) so the highlights actually represent the full year. Everything concatenates with the same fast -c copy (falling back to re-encode) approach as Periodic Recap, and a previous year's own Wrapped_<year>.mp4 is excluded from being swept up as an input clip into a new one.