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. |
||
|---|---|---|
| .claude/skills/gifhorse | ||
| gifhorse | ||
| gifhorse-ios | ||
| scripts | ||
| .gitignore | ||
| activate.sh | ||
| CLAUDE.md | ||
| DEVELOPER_NOTES.md | ||
| DEVELOPMENT_LOG.md | ||
| FEATURES.md | ||
| main.py | ||
| QUICKSTART.md | ||
| README.md | ||
| requirements.txt | ||
| ROADMAP.md | ||
| setup.py | ||
| TESTING.md | ||
| USAGE_GUIDE.md | ||
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 alienoffers 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
.srtfirst, 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-whisperfor 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
search
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.
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-subtitlesif you already have .srt files and don't want any downloading - Use
--use-whisperas a fallback when subtitles aren't available online - Check subtitle coverage with
gifhorse subtitle-status ~/Videosbefore transcribing - Already-transcribed videos are skipped — safe to re-run
- Use
--forceto re-transcribe after getting better subtitles
Output Size Optimization
If your GIFs are too large:
- Use
--format mp4for 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 mp4when 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:
Aim under the warned limit (often ~0.5–1 MB is the workable range on device).gifsicle -O3 --lossy=80 big.gif -o phone.gif - Burn-in needs FFmpeg built with libass. Slim package builds often fail the
subtitlesfilter — install a full build (e.g. Homebrewffmpeg-full) and put it first onPATH.
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
--allto see every match, or--limitto 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-whisperas a fallback for those videos - Check
gifhorse subtitle-status ~/Videosto 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 regularffmpeg) - Check that
/opt/homebrew/opt/ffmpeg-full/binis 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.