# movmaker Simple Python + ffmpeg movie maker. Drop pictures, videos, music, and optionally one simple `data.txt` file into a single flat input directory. Then run one command to create an MP4 video. No YAML. No required subdirectories. ## Requirements - Python 3 - `ffmpeg` - `ffprobe` usually included with ffmpeg Install on Debian/Ubuntu/Raspberry Pi OS: ```bash sudo apt install ffmpeg ``` ## Fonts The default title/stamp styling uses these fonts: - Bebas Neue for the opening title, date, and location - Noto Sans Bold for the bottom-left/bottom-right stamps - IBM Plex Sans SemiBold for per-file captions Both fonts support common Danish and Croatian characters such as `æ ø å Æ Ø Å č ć ž š đ Č Ć Ž Š Đ`. Install them locally for the current user: ```bash mkdir -p ~/.local/share/fonts/google curl -L -o ~/.local/share/fonts/google/BebasNeue-Regular.ttf \ 'https://github.com/google/fonts/raw/main/ofl/bebasneue/BebasNeue-Regular.ttf' sudo apt install fonts-noto-core fonts-ibm-plex fc-cache -f ``` Check that fonts are available: ```bash fc-match 'Bebas Neue' fc-match 'Noto Sans' fc-match 'IBM Plex Sans' ``` ## Optional Jamendo music download If the input folder has no audio file, movmaker can download one random track from Jamendo. Save your Jamendo client ID in either an environment variable: ```bash export JAMENDO_CLIENT_ID='your_client_id_here' ``` or in a local config file: ```bash mkdir -p ~/.config/movmaker echo 'JAMENDO_CLIENT_ID=your_client_id_here' > ~/.config/movmaker/jamendo.env chmod 600 ~/.config/movmaker/jamendo.env ``` The default music search query is: ```text cinematic punk rock ``` Override it with: ```bash python3 movmaker.py input --music-genre "upbeat rock" ``` ## Input folder Example: ```text input/ IMG_001.jpg IMG_002.png holiday_clip.mp4 music.mp3 data.txt ``` Supported file types: - Images: `.jpg`, `.jpeg`, `.png`, `.webp`, `.bmp`, `.tif`, `.tiff` - Videos: `.mp4`, `.mov`, `.mkv`, `.avi`, `.webm`, `.m4v` - Audio: `.mp3`, `.wav`, `.m4a`, `.aac`, `.flac`, `.ogg` - Data: `data.txt`, `info.txt`, or `title.txt` Files are ordered by filename, so names like this work well: ```text 001.jpg 002.jpg 003.mp4 004.jpg ``` ## data.txt `data.txt` is optional, but if present every non-empty, non-comment line must use mandatory `key: value` syntax. Plain positional lines are not supported. Movie metadata keys: ```text Title: Paris Trip Location: Paris, France Date: May 2024 Description: Longer description shown in MVLog and embedded in final MP4 metadata. ``` Supported metadata key aliases: - title: `Title` or `Name` - location: `Location`, `Place`, or `Where` - date: `Date`, `Dates`, or `When` - description: `Description`, `Desc`, or `Synopsis` If the date entry is omitted, movmaker uses the date from the first picture/video and displays it like: ```text May 2026 ``` If the location entry is omitted and GPS coordinates are found in the first picture/video that has them, movmaker reverse geocodes them and uses that as the location. It tries to return only `city/town/village, country`; if no city/town/village is found, it uses just the country. Any other `key: value` line is treated as a per-file caption where the key is the exact media filename: ```text IMG_001.jpg: Eiffel Tower IMG_002.jpg: Louvre IMG_003.jpg: First line | Second line ``` ## MVLog worker `mvlog_worker.py` can be run from this machine by cron or a systemd timer to process MVLog jobs stored on the web machine. Default paths: - remote host: `192.168.0.204` - remote web root: `/var/www/html/mvlog` - inputs: `in-dir//` - outputs: `out-dir/YYYYMMDD_title.mp4` - per-job state: `in-dir//.movmaker-state.json` The worker: 1. scans remote `in-dir/` 2. computes a fingerprint from input filenames, sizes, and mtimes 3. skips jobs whose fingerprint matches a completed state file and whose output exists 4. copies changed jobs locally 5. renders with `movmaker.py` 6. uploads the MP4 atomically to remote `out-dir/` 7. removes the old output if the regenerated filename changed 8. clears the web metadata cache Run once: ```bash ./mvlog_worker.py ``` Process one job: ```bash ./mvlog_worker.py --job romo2026 ``` Force regeneration: ```bash ./mvlog_worker.py --job romo2026 --force ``` Pass extra movmaker options: ```bash ./mvlog_worker.py --movmaker-arg=--preview ``` Recommended scheduling is a systemd timer or cron with a separate lock, e.g. every `MVLOG_WORKER_RECOMMENDED_INTERVAL_MINUTES` minutes (5 minutes; see `mvlog_worker.py`). ## Usage Basic: ```bash python3 movmaker.py input ``` With options: ```bash python3 movmaker.py input \ --out-dir . \ --image-duration 6 \ --fade 3 \ --audio-fade 10 \ --audio-fade-in 2 \ --video-fade 1.5 \ --music-genre "cinematic punk rock" \ --resolution 1920x1080 \ --fps 30 ``` Fast preview render: ```bash python3 movmaker.py input --preview ``` ## Configuration knobs The most common visual timing/styling tweaks are exposed as constants near the top of `movmaker.py`: | Constant | Purpose | Default | | --- | --- | --- | | `INTRO_LOGO_TARGET_HEIGHT_RATIO` | Portion of the video height used for the intro logo before padding (e.g. `0.75` = 75 % of frame height). | `0.75` | | `INTRO_LOGO_EXTRA_HOLD` | Extra seconds the synthesized intro still remains before the first crossfade. | `6.0` | | `INTRO_LOGO_FADE_OFFSET` | Minimum time (s) the logo stays fully visible before the first xfade is allowed to start. | `3.0` | | `INTRO_TITLE_FADE_DELAY` | Seconds after start before the main title begins fading in. | `2.0` | | `INTRO_TITLE_FADE_LENGTH` | Seconds the fade-in/out lasts. | `1.0` | | `INTRO_TITLE_VISIBLE` | Seconds the title stays fully visible between fade in/out. | `4.0` | | `DEFAULT_IMAGE_DURATION` | Default fully visible time per still image before fades are added. | `6.0` | | `DEFAULT_CROSSFADE_DURATION` | Default crossfade duration between media items. | `3.0` | | `FIRST_IMAGE_DURATION_MULTIPLIER` | Multiplier applied to the first still’s `--image-duration` to delay the first transition. | `2.0` | | `DEFAULT_AUDIO_FADE_IN` | Default audio fade-in duration. | `2.0` | | `DEFAULT_AUDIO_FADE_OUT` | Default audio fade-out duration. | `10.0` | | `DEFAULT_VIDEO_FADE` | Default video fade-in/out duration for the first/last real clips. | `1.0` | | `INTRO_VIDEO_FADE_ENABLED` | Whether the synthesized intro/logo pad gets the global fade-in effect (`False` keeps it fully visible from frame 0). | `False` | Adjust these constants and rerun `movmaker.py` to change the intro behavior without touching the rest of the pipeline. ## Current behavior - scans one flat input directory - supports an optional intro logo overlay via `--intro-logo` - supports AJAX save for input-dir create, edit, delete with toast notifications and redirect cleanup - auto-detects media/audio/data files by extension - shows each image fully visible for `DEFAULT_IMAGE_DURATION` seconds by default (6 s), not counting crossfades - keeps the first image longer so the first transition starts after `FIRST_IMAGE_DURATION_MULTIPLIER * DEFAULT_IMAGE_DURATION` seconds (12 s with defaults) - includes video clips inline - crossfades between media items over `DEFAULT_CROSSFADE_DURATION` seconds by default (3 s) - loops/trims music to match the video - if no audio file exists in the input folder, searches Jamendo using `--music-genre` and downloads one random Creative Commons track that allows non-commercial reuse and derivative works; default query is `cinematic punk rock` - reads the Jamendo client ID from `JAMENDO_CLIENT_ID` or `~/.config/movmaker/jamendo.env` - fades music in over `DEFAULT_AUDIO_FADE_IN` seconds (2 s) and out over `DEFAULT_AUDIO_FADE_OUT` seconds (10 s) by default - builds a black intro still (matching the first media item’s resolution) when `--intro-logo` is provided, scales the logo to `INTRO_LOGO_TARGET_HEIGHT_RATIO` of the frame height (75 % by default), shows it at full opacity from frame 0, and keeps it on screen for `audio_fade_in + INTRO_LOGO_EXTRA_HOLD` seconds (with defaults this is `DEFAULT_AUDIO_FADE_IN + INTRO_LOGO_EXTRA_HOLD`, i.e. 8 s) before allowing the first crossfade - opening title text fades in after `INTRO_TITLE_FADE_DELAY` seconds (2 s), stays visible for `INTRO_TITLE_VISIBLE` seconds (4 s), then fades out over `INTRO_TITLE_FADE_LENGTH` seconds (1 s) - fades the first real clip in and the final clip out with the `--video-fade` duration (default `DEFAULT_VIDEO_FADE`, currently 1 s); the black intro/logo pad respects this fade-in only when `INTRO_VIDEO_FADE_ENABLED` is `True` - displays optional per-file captions at bottom center with slightly off-white text and a soft shadow; use `|` to split caption lines - adds persistent bottom-left title/date/place and bottom-right `Bubulescu.Org` stamps - preserves special characters in rendered overlays and metadata - uses safe ASCII only for generated filenames, so special letters become readable equivalents like `Č` -> `C`, `å` -> `a`, `ø` -> `o` - embeds MP4 metadata: title, date, location/place, QuickTime location name, artist, comment, and creation_time - writes an MP4 file named like `YYYYMMDD_title.mp4` in the current directory by default; use `--out-dir DIR` to choose another output directory - takes `YYYYMMDD` from the first picture/video metadata, falling back to file modification date - keeps console output minimal during rendering and writes detailed ffmpeg output to timestamped logs in `.logs/` inside the input directory - uses local `ffmpeg/ffmpeg` and `ffmpeg/ffprobe` when present, otherwise system tools ## Notes This is an early version. The intended workflow is deliberately simple: dump files into one folder and run the script.