Find and create GIFs from dialogue in your local video library
Find a file
Coyote-git 0f6d039ade Add Photos delivery and refuse poisoned MP4 exports.
iMessage attachment send is unreliable, so create --photos / gifhorse photos is the phone path. MP4 encode now maps video-only and deletes clips that inherit full-movie data-track duration.
2026-08-15 09:42:31 -06:00
.claude/skills/gifhorse Add fuzzy -S matching and franchise grouping 2026-07-09 14:28:57 -06:00
gifhorse Add Photos delivery and refuse poisoned MP4 exports. 2026-08-15 09:42:31 -06:00
gifhorse-ios iOS: Xcode project shell, thumbs API, gallery posters 2026-07-09 14:17:23 -06:00
scripts Add Photos delivery and refuse poisoned MP4 exports. 2026-08-15 09:42:31 -06:00
.gitignore iOS: Xcode project shell, thumbs API, gallery posters 2026-07-09 14:17:23 -06:00
activate.sh Add comprehensive documentation and remove personal info 2026-01-25 14:57:59 -07:00
CLAUDE.md Docs: refresh CLAUDE.md architecture for v3 franchises 2026-07-09 14:33:49 -06:00
DEVELOPER_NOTES.md Add comprehensive documentation and remove personal info 2026-01-25 14:57:59 -07:00
DEVELOPMENT_LOG.md Add comprehensive documentation and remove personal info 2026-01-25 14:57:59 -07:00
FEATURES.md Docs: fuzzy -S, franchises, and current roadmap 2026-07-09 14:33:40 -06:00
main.py Add Photos delivery and refuse poisoned MP4 exports. 2026-08-15 09:42:31 -06:00
QUICKSTART.md Rename tool from 'gifmaker' to 'gifhorse' 2026-01-18 09:47:49 -07:00
README.md Docs: iMessage/phone delivery tips in README and USAGE_GUIDE 2026-07-29 08:48:53 -06:00
requirements.txt Add Photos delivery and refuse poisoned MP4 exports. 2026-08-15 09:42:31 -06:00
ROADMAP.md Add Photos delivery and refuse poisoned MP4 exports. 2026-08-15 09:42:31 -06:00
setup.py Add FastAPI backend + import-exports for the app 2026-06-25 11:14:57 -06:00
TESTING.md Rename tool from 'gifmaker' to 'gifhorse' 2026-01-18 09:47:49 -07:00
USAGE_GUIDE.md Docs: iMessage/phone delivery tips in README and USAGE_GUIDE 2026-07-29 08:48:53 -06:00

GifHorse

"Don't look a gif horse in the mouth!"

Find and create GIFs from dialogue in your local video library. Search your favorite shows and movies by what was said, then export perfect reaction GIFs (or MP4s) with subtitles.

Features

  • Search by dialogue: Find quotes across your library (FTS5 ranked search, instant once indexed)
  • Fuzzy source filter (-S): Restrict to a show/movie without typing the full title — adv, alien, typos, etc.
  • Franchises: Group related titles (Alien + Aliens + Prometheus, Adventure Time + Fionna and Cake) so -S alien offers the whole franchise
  • Context search: See dialogue before/after matches
  • Preview before creating: Timing, duration, frame estimates, platform warnings
  • GIF and MP4 export: GIFs for reactions; MP4s for smaller files / longer clips
  • Subtitle substitution (-s) / clean replace (-r): Meme word swaps with or without strikethrough
  • Gif library: Every create is recorded, taggable, and browsable (gifhorse gifs, iOS app)
  • Auto subtitle downloading: Local .srt first, then online providers (Whisper optional fallback)
  • iMessage sending: --send / --send-to
  • Platform warnings: Size/frame limits for X, Discord, Slack, iMessage
  • Local & private: Your media, your machine (or your NAS) — no cloud corpus

Requirements

  • Python 3.8+
  • FFmpeg (for video processing)
  • Local video files (TV shows, movies, etc.)
  • Internet connection for subtitle downloading (default transcription method)
  • ~2GB disk space only if using --use-whisper for Whisper AI transcription

Installation

1. Install FFmpeg

macOS:

# Install ffmpeg-full (required for subtitle support)
brew install ffmpeg-full

# Add to PATH (required since ffmpeg-full is keg-only)
echo 'export PATH="/opt/homebrew/opt/ffmpeg-full/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

Linux:

sudo apt install ffmpeg  # Ubuntu/Debian
sudo yum install ffmpeg  # CentOS/RHEL

Windows: Download from ffmpeg.org

2. Install Python Package

# Navigate to the project directory
cd /path/to/gifhorse

# Create virtual environment
python3 -m venv venv

# Activate it (you'll need to do this each time you use the tool)
source venv/bin/activate

# Install the package
pip install -e .

This will install gifhorse as a command-line tool.

Quick tip: Use the activation helper script:

source activate.sh

Quick Start

1. Transcribe Your Videos

Point the tool at a folder containing your videos:

gifhorse transcribe ~/Videos/Adventure_Time

This will:

  • Find all video files (.mp4, .mkv, .avi, etc.)
  • Check for local .srt subtitle files first
  • Download subtitles from online providers if no local file exists
  • Save everything to transcriptions.db
  • Skip any already-transcribed videos

No Whisper model download, no waiting for AI — subtitle downloading is fast and accurate for most well-known content.

Already have .srt files? Use --use-subtitles to skip downloading and only use local files.

No subtitles available online? Use --use-whisper to transcribe with Whisper AI (takes ~1/3 of real-time, downloads a ~1GB model on first run).

2. Name your shows (once) + seed franchises

gifhorse backfill-sources    # parse show/movie from filenames
gifhorse franchise seed      # link Alien, Adventure Time, Dune, …

3. Search for Dialogue

gifhorse search "you didn't even do anything"

# Only inside one show (fuzzy — no need for the full title)
gifhorse search "mathematical" -S adv
# If several titles match, pick from the list (or use --source-pick):
gifhorse search "mathematical" -S adv --source-pick 1

# Whole franchise at once
gifhorse search "the spice" -S dune --source-pick 0

4. Create a GIF or MP4

# Create a GIF (default)
gifhorse create "you didn't even do anything" 1

# Same -S / --source-pick as search so result numbers stay aligned
gifhorse create "mathematical" 1 -S adv --source-pick 1

# MP4 (smaller file, better quality for longer clips)
gifhorse create "you didn't even do anything" 1 --format mp4

This creates exports/you_didnt_even_do_anything.gif (or .mp4) from result #1, with subtitles burned in.

Command Reference

transcribe

gifhorse transcribe [OPTIONS] DIRECTORY

Transcribe videos in a directory. By default, looks for local .srt files first, then downloads subtitles from online providers.

Options:

Option Default Description
--recursive/--no-recursive --recursive Search subdirectories
--use-subtitles off Use only local .srt files (no downloading, no Whisper)
--use-whisper off Use Whisper AI (skip subtitle downloading)
--force, -f off Re-transcribe videos even if already in database
--model [tiny|base|small|medium|large] base Whisper model size (only with --use-whisper)
--language CODE auto-detect Language code (e.g., en, es, fr)
--db PATH transcriptions.db Database file path

Examples:

# Default: download subtitles automatically
gifhorse transcribe ~/Videos/MyShows

# Use only local .srt files
gifhorse transcribe ~/Videos/Movies --use-subtitles

# Use Whisper AI with larger model
gifhorse transcribe ~/Videos/MyShows --use-whisper --model medium

# Re-transcribe everything (e.g., after getting better subtitles)
gifhorse transcribe ~/Videos/MyShows --force

# Non-recursive (only top level)
gifhorse transcribe ~/Videos --no-recursive
gifhorse search [OPTIONS] QUERY

Search for dialogue across all transcribed videos (default: ranked FTS5).

Options:

Option Default Description
--limit, -l N 100 Maximum number of results
--all, -a off Show all results (no limit)
--context N 0 Show N dialogue lines before/after each match
--exact off Substring (LIKE) match instead of FTS
-S, --source TEXT Fuzzy show/movie/franchise filter (see below)
--source-pick N Pick list index when -S is ambiguous (0 = whole franchise)
-y, --yes off Never prompt for ambiguous -S (fail with list instead)
--db PATH transcriptions.db Database file path

Examples:

gifhorse search "mathematical"
gifhorse search "mathematical" --context 2
gifhorse search "mathematical" -S adv
gifhorse search "the spice" -S dune --source-pick 0
gifhorse search "ice king" --all

Fuzzy -S / franchises

You do not need the exact official title:

You type Typical result
-S adv Pick list: Adventure Time, Fionna and Cake, or all
-S alien Full Alien franchise pick list
-S aliens Auto → Aliens (1986)
-S fionna / -S romulus Auto → that title
-S advnture Fuzzy → Adventure Time family

Pass the same -S / --source-pick / --exact to preview and create so result numbers stay aligned.

gifhorse franchise list          # see groups
gifhorse franchise seed          # link known franchises (safe to re-run)
gifhorse franchise add "My Saga" "Title A" "Title B"
gifhorse sources -q alien        # dry-run the matcher

preview

gifhorse preview [OPTIONS] QUERY RESULT_NUMBER

Preview what will be captured before creating output. Shows timing, duration, frame estimate, platform warnings, and all dialogue that will be included.

Options:

Option Default Description
--include-before N 0 Include N segments before match
--include-after N 0 Include N segments after match
--start-offset SECONDS 0.0 Adjust start time (+/-)
--end-offset SECONDS 0.0 Adjust end time (+/-)
--padding-before SECONDS 1.0 Time before dialogue
--padding-after SECONDS 1.0 Time after dialogue
--fps N 15 Frames per second (for frame count estimate)
-S, --source TEXT Same fuzzy filter as search
--source-pick N Same disambiguation as search
--exact off Must match how you ran search
-y, --yes off Never prompt for ambiguous -S
--db PATH transcriptions.db Database file path

Examples:

gifhorse preview "cells" 1
gifhorse preview "mathematical" 1 -S adv --source-pick 1
gifhorse preview "cells" 1 --include-before 1 --include-after 1

create

gifhorse create [OPTIONS] QUERY RESULT_NUMBER

Create a GIF or MP4 from a search result.

Timing Options:

Option Default Description
--include-before N 0 Include N segments before match
--include-after N 0 Include N segments after match
--start-offset SECONDS 0.0 Adjust start time (+/-)
--end-offset SECONDS 0.0 Adjust end time (+/-)
--padding-before SECONDS 1.0 Seconds before dialogue
--padding-after SECONDS 1.0 Seconds after dialogue
-d, --duration SECONDS auto Override clip duration

Subtitle Options:

Option Description
-s, --sub NUM OLD NEW Substitute words with strikethrough (repeatable)
-r, --replace NUM OLD NEW Replace words cleanly, no strikethrough (repeatable)
--no-subtitles Don't include subtitles

Output Options:

Option Default Description
-o, --output PATH auto-named Output file path
--format [gif|mp4] gif Output format (auto-detected from -o extension)
--width PIXELS 480 Output width in pixels
--fps N 15 Frames per second
--quality [low|medium|high] medium Output quality
--send off Send to configured phone number via iMessage
--send-to NUMBER Send to specific phone number (overrides config)
--tag NAME Tag the created gif (repeatable)
-S, --source TEXT Same fuzzy filter as search (keep numbers aligned)
--source-pick N Same disambiguation as search
--exact off Must match how you ran search
-y, --yes off Never prompt for ambiguous -S
--db PATH transcriptions.db Database file path

Examples:

# Basic GIF creation
gifhorse create "mathematical" 1

# Scoped to a show (same -S as search)
gifhorse create "mathematical" 1 -S adv --source-pick 1

# Create MP4 instead (smaller, better quality)
gifhorse create "mathematical" 1 --format mp4

# Include surrounding dialogue
gifhorse create "cells" 1 --include-before 1 --include-after 1

# Adjust timing to capture scene change
gifhorse create "cells" 1 --start-offset -2.0

# Custom output
gifhorse create "mathematical" 1 -o ~/Desktop/reaction.gif

# Higher quality, larger size
gifhorse create "mathematical" 1 --quality high --width 720 --fps 24

# Substitute words (strikethrough effect)
gifhorse create "the age of men" 1 -s 1 "men" "code reviews"

# Clean replace (no strikethrough)
gifhorse create "the age of men" 1 -r 1 "men" "code reviews"

# Create and send via iMessage
gifhorse create "mathematical" 1 --format mp4 --send

fetch-subtitles

gifhorse fetch-subtitles [OPTIONS] DIRECTORY

Download subtitle files (.srt) from online providers without importing into the database. Useful for batch downloading before transcribing, or when you want the .srt files on disk for other purposes.

Options:

Option Default Description
--recursive/--no-recursive --recursive Search subdirectories
--skip-existing/--no-skip-existing --skip-existing Skip videos that already have .srt files

Examples:

# Download subtitles for all videos
gifhorse fetch-subtitles ~/Videos/MyShows

# Force re-download even if .srt exists
gifhorse fetch-subtitles ~/Videos --no-skip-existing

remove

gifhorse remove [OPTIONS] PATTERN

Remove videos from the database by path pattern. Uses SQL LIKE syntax — % is a wildcard.

Options:

Option Default Description
--yes, -y off Skip confirmation prompt
--db PATH transcriptions.db Database file path

Examples:

# Remove all Adventure Time episodes
gifhorse remove "%Adventure Time%"

# Remove all Season 1 videos
gifhorse remove "%S01%"

# Remove a specific video
gifhorse remove "/path/to/video.mp4"

# Skip confirmation
gifhorse remove "%old_show%" -y

subtitle-status

gifhorse subtitle-status [OPTIONS] DIRECTORY

Scan a directory and report which videos have subtitle files and which don't.

Options:

Option Default Description
--recursive/--no-recursive --recursive Search subdirectories
--missing-only off Only show videos without subtitles

Examples:

# Full status report
gifhorse subtitle-status ~/Videos

# Only show what's missing
gifhorse subtitle-status ~/Videos --missing-only

config

gifhorse config [OPTIONS]

Manage GifHorse configuration, stored at ~/.config/gifhorse/config.json.

Options:

Option Description
--set-phone NUMBER Set phone number for iMessage sending
--show Show current configuration

Examples:

# Set phone number for --send
gifhorse config --set-phone "+15551234567"

# View current config
gifhorse config --show

stats / list / sources / gifs / tag

gifhorse stats
gifhorse list
gifhorse sources                 # all shows/movies
gifhorse sources -q alien        # fuzzy matcher dry-run
gifhorse gifs [query] [--source TITLE] [--tag NAME]
gifhorse tag <id|path|last> <tag> [tag ...]

franchise

gifhorse franchise list
gifhorse franchise seed
gifhorse franchise add "Franchise Name" "Source Title A" "Source Title B"

serve (API for the iOS app)

gifhorse serve [--host 0.0.0.0] [--port 8000]

Serves the gif library, dialogue search, tags, media, and thumbnails. Point the iOS app at http://<this-machine>:8000.

See gifhorse-ios/README.md.

Tips

Organizing Your Videos

Put shows in separate folders for easier management:

~/Videos/
  ├── Adventure_Time/
  ├── The_Office/
  └── Parks_and_Rec/

Transcribe each folder separately:

gifhorse transcribe ~/Videos/Adventure_Time
gifhorse transcribe ~/Videos/The_Office

Transcription Tips

  • Subtitle downloading is the default — fast, accurate, no model download needed
  • Use --use-subtitles if you already have .srt files and don't want any downloading
  • Use --use-whisper as a fallback when subtitles aren't available online
  • Check subtitle coverage with gifhorse subtitle-status ~/Videos before transcribing
  • Already-transcribed videos are skipped — safe to re-run
  • Use --force to re-transcribe after getting better subtitles

Output Size Optimization

If your GIFs are too large:

  • Use --format mp4 for dramatically smaller files at better quality
  • Lower --fps (try 10 or 12)
  • Use --quality low
  • Reduce --width (try 360 or 400)
  • Shorter clips (less padding)

If your GIFs are too small/choppy:

  • Increase --fps (try 20 or 24)
  • Use --quality high
  • Increase --width (try 720)

Platform Limits

GifHorse automatically warns when your output exceeds platform limits. Here's a quick reference:

Platform GIF Max GIF Frames MP4 Max
X/Twitter (web) 15 MB 300 512 MB
X/Twitter (mobile) 5 MB 300 512 MB
Slack 5 MB 1 GB
Discord (free) 10 MB 10 MB
Discord (Nitro) 50 MB 500 MB
iMessage 0.6 MB 150 MB

MP4 is almost always the better choice for anything longer than a few seconds.

iMessage / phone delivery

Warnings fire after create (real file size). Practical tips:

  • Prefer --format mp4 when sending to a phone (--send / --send-to) — GIFs blow past the ~0.6 MB limit fast.
  • If you need a GIF: drop --width / --fps / use --quality low, then optionally compress further:
    gifsicle -O3 --lossy=80 big.gif -o phone.gif
    
    Aim under the warned limit (often ~0.51 MB is the workable range on device).
  • Burn-in needs FFmpeg built with libass. Slim package builds often fail the subtitles filter — install a full build (e.g. Homebrew ffmpeg-full) and put it first on PATH.

Search Tips

  • Searches are case-insensitive
  • Partial matches work ("didn't do" will find "you didn't do anything")
  • Try variations if you don't find what you want
  • Use --all to see every match, or --limit to adjust the cap

Workflow Example

# 1. Transcribe your collection (one time)
gifhorse transcribe ~/Videos/Adventure_Time
gifhorse transcribe ~/Videos/Community

# 2. Check what you have
gifhorse stats
# Output: Videos transcribed: 87, Total dialogue segments: 15,234

# 3. Search when you need a reaction GIF
gifhorse search "that's the coolest thing"

# 4. Preview to nail the timing
gifhorse preview "that's the coolest thing" 2 --include-after 1

# 5. Create and send
gifhorse create "that's the coolest thing" 2 --format mp4 --send

Troubleshooting

"FFmpeg not found"

  • Install FFmpeg (see Installation section)
  • Make sure it's in your PATH

"Model download failed" (only when using --use-whisper)

  • First run downloads Whisper model (~1GB)
  • Needs internet connection
  • Check disk space

Subtitle download failures

  • Some videos (especially obscure or amateur content) won't have subtitles available online
  • Use --use-whisper as a fallback for those videos
  • Check gifhorse subtitle-status ~/Videos to see what's missing

"Video file not found" when creating GIF

  • Make sure original video file hasn't moved
  • Database stores full path to video
  • If you moved videos, re-transcribe with --force

GIF creation is slow

  • Normal for high-quality GIFs
  • Try lower quality/fps settings
  • First GIF may generate palette cache

Subtitle errors on macOS

  • If you see "No option name near" errors with subtitles
  • Make sure you installed ffmpeg-full (not regular ffmpeg)
  • Check that /opt/homebrew/opt/ffmpeg-full/bin is in your PATH
  • Regular ffmpeg on Homebrew lacks libass support needed for subtitles

Technical Details

  • Transcription: Subtitle downloading via subliminal (default), with Whisper AI fallback
  • Video processing: FFmpeg
  • Database: SQLite
  • Language: Python 3.8+
  • CLI: Click framework

License

MIT License - feel free to use and modify!

Credits

Built with Claude Code - because finding the perfect reaction GIF shouldn't require opening Premiere Pro.