10 KiB
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
ffmpegffprobeusually included with ffmpeg
Install on Debian/Ubuntu/Raspberry Pi OS:
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:
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:
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:
export JAMENDO_CLIENT_ID='your_client_id_here'
or in a local config file:
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:
cinematic punk rock
Override it with:
python3 movmaker.py input --music-genre "upbeat rock"
Input folder
Example:
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, ortitle.txt
Files are ordered by filename, so names like this work well:
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:
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:
TitleorName - location:
Location,Place, orWhere - date:
Date,Dates, orWhen - description:
Description,Desc, orSynopsis
If the date entry is omitted, movmaker uses the date from the first picture/video and displays it like:
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:
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/<job>/ - outputs:
out-dir/YYYYMMDD_title.mp4 - per-job state:
in-dir/<job>/.movmaker-state.json
The worker:
- scans remote
in-dir/ - computes a fingerprint from input filenames, sizes, and mtimes
- skips jobs whose fingerprint matches a completed state file and whose output exists
- copies changed jobs locally
- renders with
movmaker.py - uploads the MP4 atomically to remote
out-dir/ - removes the old output if the regenerated filename changed
- clears the web metadata cache
Run once:
./mvlog_worker.py
Process one job:
./mvlog_worker.py --job romo2026
Force regeneration:
./mvlog_worker.py --job romo2026 --force
Pass extra movmaker options:
./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:
python3 movmaker.py input
With options:
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:
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_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_DURATIONseconds by default (6 s), not counting crossfades - keeps the first image longer so the first transition starts after
FIRST_IMAGE_DURATION_MULTIPLIER * DEFAULT_IMAGE_DURATIONseconds (12 s with defaults) - includes video clips inline
- crossfades between media items over
DEFAULT_CROSSFADE_DURATIONseconds by default (3 s) - loops/trims music to match the video
- if no audio file exists in the input folder, searches Jamendo using
--music-genreand downloads one random Creative Commons track that allows non-commercial reuse and derivative works; default query iscinematic punk rock - reads the Jamendo client ID from
JAMENDO_CLIENT_IDor~/.config/movmaker/jamendo.env - fades music in over
DEFAULT_AUDIO_FADE_INseconds (2 s) and out overDEFAULT_AUDIO_FADE_OUTseconds (10 s) by default - builds a black intro still (matching the first media item’s resolution) when
--intro-logois provided, scales the logo toINTRO_LOGO_TARGET_HEIGHT_RATIOof the frame height (75 % by default), shows it atINTRO_LOGO_DEFAULT_ALPHAopacity (1.0 = fully opaque) from frame 0, and keeps it on screen foraudio_fade_in + INTRO_LOGO_EXTRA_HOLDseconds (with defaults this isDEFAULT_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 fades in after
INTRO_TITLE_FADE_DELAYseconds (2 s), stays visible forINTRO_TITLE_VISIBLEseconds (4 s), then fades out overINTRO_TITLE_FADE_LENGTHseconds (1 s) - fades the first real clip in with the
--video-fadeduration (defaultDEFAULT_VIDEO_FADE, currently 1 s) while leaving the logo intro instant-on whenINTRO_VIDEO_FADE_ENABLEDisFalse; 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 followsDEFAULT_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.Orgstamps - 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.mp4in the current directory by default; use--out-dir DIRto choose another output directory - takes
YYYYMMDDfrom 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/ffmpegandffmpeg/ffprobewhen 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.