Skip to main content

Title screens, maps and music

The clips are the film, but the cards around them are what make it read as a memory and not an FFmpeg concat of your camera roll:

  • Intro card: a blurred, darkened frame from the first clip behind white text, 3.5 seconds (title_screens.title_duration). Title and subtitle shrink together when the pair would pass 80 % of the frame height.
  • Month dividers: at each month change in a single-year film spanning four months or more. The first month gets none: the intro already said it. month_divider_threshold sizes a budget, and when the budget binds the first month changes win, so a thin January can keep its card while a busy November loses one.
  • Trip map: a satellite fly-over from home to the destination, in place of the intro. Off by default, see The map fly-over.
  • Location cards: the place name where a trip moves on. A hop of more than 30 km always gets one. A walking or cycling trip moves village to village well under that, so a change of town gets one too: at most one a day, never the same town twice in a row, never a town within 50 km of home. With map tiles on, each card flies from the town the last card named to the new one, then holds on its name. See Map moves.
  • Ending: a fade to white, no text. Every film keeps it; a short film squeezes it to 2 s rather than dropping it.

All of this works on a plain NAS. Titles come from templates, the special-day catalogue and your album names; a reader only rewrites people and occasion titles (see When a model names the film).

A configured LLM can write titles and choose music mood from the cut's text on every selection tier, including NAS. These requests send no pictures and need no local GPU. If the model is missing or fails, the template title and local music mood still work. Adding a text model does not opt you into using it to caption pictures.

title_screens.enabled: false turns every card off. The other keys are in the config reference.

Styles​

Every title is Montserrat, white on a dark palette. The date and place captions burned on clips use Outfit. Both ship inside the package (OFL-1.1).

StylePaletteCharacter
modern_warmWarm charcoal/stoneBold, semibold. Amber accents
elegant_minimalDeep navy/blackClean, medium weight. Cyan accents
vintage_charmWarm charcoalNostalgic. Amber/gold accents
playful_brightDeep tealEnergetic, semibold. Teal accents
soft_romanticDark amber-tintedGentle scale-in. Warm amber accents

style_mode: auto (the default) picks the palette from the film's mood: happy, nostalgic and romantic get warm_dark, calm and peaceful deep_teal, energetic and playful midnight, exciting cinematic_dark. style_mode: random picks a named style.

The renderer is picked for you: GPU kernels where Metal, CUDA or Vulkan start, Pillow elsewhere (static gradients, same text). Which one your box gets is on Hardware encoding.

Preview a card without running anything:

immich-memories titles test --year 2025 --style elegant_minimal
immich-memories titles test --month 6 --year 2025 --type month
immich-memories titles test --year 2025 --locale fr --orientation portrait

Every alphabet​

Montserrat draws Latin, Polish, Czech, Turkish and Vietnamese included. A letter it lacks comes from Noto Sans, word by word, so DEUX SEMAINES EN CRÈTE · Κρήτη keeps its French in Montserrat and its Greek in Noto. A word never switches typeface halfway through.

ScriptWhere the font comes from
Latin, Greek, Cyrillic, Vietnameseinside the package (Noto Sans)
Arabic, Hebrew, Thaana, the Indic scripts, Sinhala, Thai, Lao, Khmer, Myanmar, Armenian, Georgian, Ethiopictitles fonts --install (3.8 MB for all of them)
Chinese, Japanese, Koreantitles fonts --install (Noto Sans CJK, 39.5 MB)

The Docker image runs that install at build time, each file checked against a pinned SHA-256, so a Docker user has every alphabet out of the box. On pip or uv, run it once:

immich-memories titles fonts --install

That command is the only thing that ever downloads a font. A render never does: a letter no installed font has draws as a box, and the log names the command once. Where the files come from is on Privacy.

Arabic and Hebrew run right to left and Arabic letters join; the Indic scripts form their clusters. That is HarfBuzz and FriBiDi through Pillow. The Docker image has both. Elsewhere, install FriBiDi from the system (apt install libfribidi0, or brew install fribidi and start the app with DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib). Without it the letters still draw, unjoined.

Greek in capitals drops its stress accents, the way Greek signs print it (ΗΡΑΚΛΕΙΟ, and ΜΑΪΟΣ where the accent kept two vowels apart). Chinese characters use the Simplified forms unless the title holds kana (Japanese forms) or Hangul (Korean).

Languages​

title_screens.locale sets the language of everything the film prints: titles, months, weekdays, trip cards, holidays. auto (the default) follows the host's locale.

A film whose window is exactly a meteorological season opens on the season's name (Summer 2025, Été 2025, 2025年の夏), counted in your home's hemisphere: with trips.homebase_latitude south of the equator, 1 December to the end of February is Summer 2024–25. Without a home base, or for any other window, the months name it (June to August 2025).

The web UI's suggested title is the same template in the same language, so with no reader a French trip made in the web UI opens on "DEUX SEMAINES EN CRÈTE, GRÈCE, ÉTÉ 2025", exactly as the CLI would.

LanguageCodeTrip titles
Englishenfull: "A WEEK IN THE NETHERLANDS"
Frenchfrfull: "UNE SEMAINE AUX PAYS-BAS"
Dutchnlfull: "op Kreta", "in de Verenigde Staten"
Germandefull: "auf Kreta", "in der Schweiz"
Spanishesfull: "en el Reino Unido"
Italianitfull: "a Creta", "negli Stati Uniti"
Portuguese (Brazil)pt-BRfull: "na Itália", "nos Estados Unidos"
Portuguese (Portugal)pt-PTfull: "em França", "no Japão"
Polishplfull for the places it lists, the fallback design for the rest
Swedishsvfull: "på Kreta", "i Italien"
Russianrufallback design
Japanesejafallback design
Chinese (Simplified)zh-Hansfallback design
Koreankofallback design
Twelve of these catalogues were drafted by an AI

Months and weekdays come from CLDR, so they are right everywhere. The rest of the wording lives in one catalogue per language under src/immich_memories/locales/. English and French are written by hand. The other twelve catalogues, and the preposition rules for Dutch, German, Spanish, Italian, Portuguese, Polish and Swedish, were drafted by an AI and checked by tests, not yet by native speakers. Each file says so at the top. A fix is a one-line pull request.

"Fallback design" means the trip card never guesses a preposition: the place is the big line, the duration and date sit under it. A wrong preposition never reaches the screen.

Why these fourteen: Immich publishes no usage numbers per language, and 44 of its languages on Hosted Weblate are 95 % translated or more. The first ten are the Western European and American languages with the most speakers, plus Dutch; the last four are the largest non-Latin ones.

Trip titles: the place at the right scale​

A trip is named after the smallest place that holds 85 % of its located pictures, counted per picture: the city, else the island, else a town holding more than half of them, else the region, else two regions, else the country, else the countries in the order you crossed them. A day with forty pictures in one town weighs more than a travel day with two.

The places are Immich's own reverse geocoding (GeoNames, offline) plus a small bundled table of islands, because GeoNames files most islands under a region. No outside call.

ScaleEnglishFrench
A countryIN THE NETHERLANDS, IN ITALYAUX PAYS-BAS, EN ITALIE
An islandIN CRETE, GREECE, IN CYPRUSEN CRÈTE, GRÈCE, À CHYPRE
A regionIN APULIA, ITALYDANS LES POUILLES, ITALIE
Two regionsIN UTAH AND NEVADA, UNITED STATESDANS L'UTAH ET AU NEVADA, ÉTATS-UNIS
A cityIN LAS VEGAS, UNITED STATESÀ LAS VEGAS, ÉTATS-UNIS
Several countriesACROSS BELGIUM → SPAINno phrase, place first

The full title adds the length and the season or month: TWO WEEKS IN CRETE, GREECE, SUMMER 2025.

Immich stores places in English. Country, island and region names are translated offline (CLDR and the bundled tables). City names stay as Immich stored them unless you switch on network.geocoding, which asks Nominatim for the district each picture is in, in the film's language, one request per distinct place and kept for the next film. That also fixes the district Immich names after its neighbour (Wilrijk, not Hoboken) everywhere a place is named: story titles, captions, location cards, map pins and the report. What that sends is on Privacy.

Date and place captions​

Every film gets small translucent date and place captions unless you turn them off: defaults.add_date and defaults.add_place are on, and --no-add-date or --no-add-place drops one for a single film (the web render panel has the same two boxes). They are 48 px on a 1080p frame at 85 % opacity, each one appearing when it changes. Captions stay clear of dissolves so two never overlap, and a caption in any alphabet draws with the title fonts above, HDR included.

The date says only what is new: the weekday and day inside one month, the day and month inside one year, the full date across years. Each language writes it its own way (10. AUGUST, 10 DE AGOSTO, 10 SIERPNIA, 8月10日).

Places you are at all the time stay unlabelled (the name of your own town over every third clip is noise). A spot is familiar when it recurs within 250 m of the picture over many weeks in several years; a yearly summer holiday never qualifies. trips.homebase_latitude and trips.homebase_longitude mark home too, and the home country drops out of domestic captions. The scan reads metadata only, is cached for seven days under cache.directory/familiar-places/, and is skipped in privacy mode.

The map fly-over​

The fly-over needs satellite tiles from a third party, so it is off until you say so:

network:
map_tiles: true

Off, a trip film opens on the ordinary title card, location cards show the town name on the style's own background, and nothing about where you went leaves the machine. On, the camera starts over home at city zoom and flies out to the trip's stops. Long distances use a Van Wijk zoom (the d3 interpolateZoom path, which pulls out further the further apart the points are); short hops pan. Tiles come from ArcGIS World Imagery at server.arcgisonline.com, no key, a few hundred per map. A box that cannot reach it renders grey frames and the run carries on.

Map moves​

Every map in a trip film is one move: an eased flight, then at least 2 seconds sitting still on the destination with its name up. That goes for the intro and for each location card, the town-change cards of a walk included. A card flies from the place the previous card named (the trip's first place for the first card) to its own town, so a hike reads as village to village on the map.

A move lasts 6 seconds for a short hop and 8 for a long flight, scaled on the log of the distance: 30 km gets 6 s, 300 km about 7 s, 3000 km and more 8 s. The 2 s hold always stays; the flight gets the rest.

title_screens:
map_move_min_seconds: 6.0 # nearby place, hold included
map_move_max_seconds: 8.0 # far place, hold included

A card renders every flight frame once and the hold once, so a 7 s card at 30 fps is about 150 map renders. Tiles are cached for the whole card. The log line per card gives frames, renders and tiles.

Stops on the intro​

Every pin on the intro has a name. A point nobody can name gets no pin, since an unlabelled dot tells you nothing.

Close stops are grouped with the trip-leg rule: points that stay within 25 km of each other are one area of stay.

  • A group of 1 or 2 stops keeps each town's own name.
  • A group of 3 or more is one pin in its middle, named by the place its members share: the municipality, county, district, island, province or region, from the geocoder, in the film's language. With geocoding off, or no level in common, it is named "first → last" (Village A → Village G).

So a hike through seven villages in one valley is one stop with the valley's county on it, and the intro flies there as one hop. The intro never hops stop by stop: each location card already flies that leg at a watchable speed, and an 8-stop trip at 2 s a hop would open on 18 s of map before any picture.

Maps and film length​

Maps run on top of the length you asked for. The budget counts each map at the price of what it replaces (a location card at month_divider_duration, the intro at title_duration), so a 3-minute trip keeps its 3 minutes of pictures and regular cards, and the seconds the maps run past that are added on top. Three location cards and an intro add about 20 s.

The render log says it once the film is composed:

Final timeline: 163.5s content + 16.5s titles + 21.0s map extra

immich-memories runs show <run-id> prints the same line as Timeline, and immich-memories report carries it under run.timeline.

When a model names the film​

Make it better (optional): with a reader configured, the model names people and occasion films ("Ada and her grandparents" instead of three stacked full names). --llm-title adds trips, --no-llm-title pins the template everywhere, and --title always wins.

The title reader gets facts, never pictures and never coordinates: first names, birth dates and ages, the relations your people registry confirms, the special-day catalogue's words, the album that holds most of the cut, and the place names by day. A capitalised word found in none of those facts gets the title refused in favour of the template, and so does a country, island or region the facts do not name, even as the title's first word. A trip title has to name the trip's place. Refusing costs a plainer title, so the check leans towards refusing.

Where the title came from​

Every render stores the source of its opening title on the run. immich-memories runs show <run-id> prints it as Title From:

SourceThe title is
overridewhat you typed: --title, or your edit in the web UI
albuman album film's album name
occasiona holiday's name, or the special-day catalogue's title
modelwhat the title reader wrote
placea trip title, built from where it went
fallbackthe template: the year, the dates, the people

fallback on a film you expected the model to name means the reader was not asked, failed, or had its title refused.

Music​

Swipe sideways, or focus the diagram and use the arrow keys, to see more.

The mood comes from text only. With a reader configured, mood_for_cut sends the saved cut's text (story labels, captions of kept pictures) and gets back a mood, an energy, a tempo and up to five genres; no picture goes out. Without a reader, or when that call fails, the film gets the mood its clips carry, else calm.

A generator that fails falls through to a bundled track, and the swap comes back as a warning on the finished video and in the notification, so a dead backend never passes for working music.

The bundled tracks come with the music extra, which the Docker image and the all extra include: 28 royalty-free tracks in five moods (calm, energetic, happy, nostalgic, tender), about 30 s each, looped with a crossfade. They were generated locally with ACE-Step from nothing sampled; tempo, key and seed per track are in LICENSE-MUSIC. A pip install without the extra renders silent unless you pass a file. In a film with photos, the pick prefers a track whose beat lands within 0.2 beats of the photo cadence.

Ducking is a sidechain compressor keyed on the clips' audio, so speech, laughter and wind all lower the music. --music-volume (0.0 to 1.0, default 0.5) maps onto -20 dB to 0 dB before ducking. When a generator gave four stems, vocals duck most and drums keep their rhythm. None of the ducking constants is a config key.

WhereSwitch
CLI--music PATH, --no-music, --music-volume
Web UI, RenderMusic: Automatic (as configured), No music, a previewed track, an uploaded one, and Music volume
Configadvanced.ace_step.enabled, advanced.musicgen.enabled

Generated music​

Generated music (ACE-Step on a Mac or a GPU box, MusicGen on a server) is an add-on with its own install and memory needs: Generated music.

The music commands​

immich-memories music search --mood happy --genre acoustic --limit 5
immich-memories music add compilation.mp4 output.mp4 --music ~/Music/track.mp3 --fade-in 3 --fade-out 5
immich-memories music add compilation.mp4 output.mp4 --mood nostalgic

music search reads audio.local_music_dir (~/Music/Memories). music add mixes a track under a video you already have, with the same ducking. Without --music it picks a track from that folder by --mood, and by calm when you give none. A standalone video has no cut text to read, and no command here sends a frame to a model: pictures are read once, at ingest. To hear what a finished cut would get before you render it, music preview RUN generates its track (runs render). Every flag is in the CLI reference.