# 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_LOGO_DEFAULT_ALPHA` | Default opacity for the intro logo (1 = fully opaque, 0 = invisible). | `1.0` | | `INTRO_TITLE_START_OFFSET` | Seconds after start before the title appears (or starts fading in). | `4.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` | | `INTRO_TITLE_FADE_ENABLED` | If `False`, the title pops in at `INTRO_TITLE_START_OFFSET` instead of fading in. | `True` | | `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` | | `CLIP_AUDIO_FADE` | Seconds used to fade video clip audio in/out while ducking the soundtrack. | `0.75` | 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 - when a clip's "Use video file audio" option is enabled, movmaker crossfades (`CLIP_AUDIO_FADE` seconds) from the soundtrack to that clip's audio, ducks the music while it plays, then fades the music back in - 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 `INTRO_LOGO_DEFAULT_ALPHA` opacity (1.0 = fully opaque) 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; the same clip is appended at the end so the last real scene crossfades back into the logo-on-black frame - opening title text shows up at `INTRO_TITLE_START_OFFSET` seconds (4 s). When `INTRO_TITLE_FADE_IN_ENABLED` is `True` it fades in/out using `INTRO_TITLE_FADE_LENGTH` (1 s); otherwise it appears instantly and only fades out (if `INTRO_TITLE_FADE_LENGTH` > 0). It remains fully visible for `INTRO_TITLE_VISIBLE` seconds (4 s). - fades the first real clip in with the `--video-fade` duration (default `DEFAULT_VIDEO_FADE`, currently 1 s) while leaving the logo intro instant-on when `INTRO_VIDEO_FADE_ENABLED` is `False`; when a logo intro is present, the reusable outro clip removes the need for a final global fade-out, so the crossfade into the outro handles the closing visuals while audio still follows `DEFAULT_AUDIO_FADE_OUT` - 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`, `å` -> `aa`, `ø` -> `o`, `ä` -> `ae`, `ß` -> `ss` - 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. ## Web push notifications (MVLog web server) Movmaker itself does not send web push notifications. The MVLog web UI handles "Show" notifications server-side: when an authenticated admin enables the "Show" checkbox for a job the web server attempts to send a single web-push to all subscribers and records the send. This keeps the VAPID private key on the web host and centralizes notification logic. Files and behavior (deployed under the MVLog web root): - send_push.js — Node sender script (uses web-push). It reads subscriptions from stdin and accepts a push payload and a push config file path. - lib/send_push.php — server-side PHP helper used by new.php to call send_push.js and atomically update cache/push_notifications.json (the "shown" map). - api/enable_show.php — authenticated endpoint to toggle Show and trigger send (used by some integrations). It requires admin login. - new.php — the admin UI toggle now calls the server-side helper when Show is enabled (server-side send; non-fatal). - cache/push_subscriptions.json — stored subscriptions (array of subscription objects). - cache/push_notifications.json — recorded sends; the "shown" map prevents duplicate Show notifications. - push.json or /etc/mvlog/push.php — VAPID keys (private key must be kept secret and file permissions restricted). - log: /var/log/mvlog_notify.log — send attempts and errors are logged here. Deployment checklist 1) Install Node.js and the web-push package under the web root (or system-wide): npm install web-push --prefix /var/www/html/mvlog 2) Place VAPID keys where the webserver can read them (recommended: /etc/mvlog/push.php or /var/www/html/mvlog/push.json) and set restrictive permissions (600 or 640, owner www-data). Do NOT commit private keys to git. 3) Ensure /var/www/html/mvlog/cache exists and contains push_subscriptions.json and push_notifications.json (create {} if empty), owned by www-data and writable. 4) Ensure /var/log/mvlog_notify.log is writable by the webserver user. 5) The admin UI (new.php) requires login; enable Show from the admin UI to trigger a server-side send. Security notes - api/enable_show.php requires admin login; do not expose it publicly without strong auth. - Protect push private keys: file permission 600 and owned by web user. Do not store private keys in repository. - Consider adding CSRF protection, rate limiting, and periodic pruning of expired subscriptions (HTTP 410 responses). If you want deployment automation, alternative flows (worker-based sender or token-protected endpoint), or I should add a short admin checklist, tell me and I will update the docs.