Alumira — Specification
Status: v1 — implemented. This document describes what Alumira does and why. The code is the reference: where they disagree, the code wins and this document is corrected.
0. Vision & principles
Alumira is a self-hosted media server for a household's films and series. It reads the files where they are, recognises them, and presents them as a streaming service of one's own would: artwork first, a profile for each person, suggestions, every version and language of a title. It plays on a computer, a phone, or a TV with its remote.
Two roles. The viewer is passive: they choose what to watch and watch it, nothing more. Nothing to set up, sort or fix: titles, artwork, versions, languages and subtitles come to them ready. The administrator is the editor. They have a hand on everything, to fine-tune the server down to the last detail: the file that serves each version, a title's text, artwork and age limit, the collections and the home page, the subtitles, the qualities, the hardware, who sees what.
Each file is delivered in one of two ways, chosen for its folder, then file by file:
- Pre-encoded: encoded once, ahead of time, into a ladder of qualities. Playback then only serves ready-made bytes: predictable quality, many viewers on modest hardware.
- Played directly: the file is served as it is, nothing stored twice. A device that cannot play it, or whose connection is too slow for it, gets qualities converted on the fly by the video chip.
Alumira runs on what a household already has: a NAS (Docker), a desktop computer (an app for macOS and Windows), any server. Other computers of the network can help it encode. It can be reached from outside in one click (Alumira Connect, §10.3).
Language models are optional helpers, chosen by the administrator: they translate subtitles, read picture subtitles into text, write subtitles by listening, name chapters, recognise the language of a track.
Guiding principles
- Media first. A very visual, simple, to-the-point UI; a dead simple UX. Its own identity: we do not imitate the streaming platforms.
- Passive viewers, an editor at the helm. Viewers decide nothing but what to watch: no setting stands between them and the picture. Every choice is the administrator's, and every one can be made, by hand, down to the detail; what Alumira does by itself is a first draft the administrator can take back.
- Plain words for viewers. No technical jargon on the viewer side (codecs, protocols, subtitle formats, rating systems): the usual consumer terms when a term is needed (HDR10, 4K), no tooltips to explain them. The administration keeps technical terms where they are the subject.
- Prepared ahead, rather than at playback. Whatever can be done before someone presses play is: encodes, subtitles, seek thumbnails, chapters, pictures. Transcoding at playback exists only for files played directly, only on the video chip, within a fixed number of places.
- Split stems, recombined when served. Every track (video per rendition, audio per language and layout, subtitle) is an independent stem; the client receives a multi-track / multi-bitrate manifest generated on the fly.
- Lazy evaluation rather than links. We store identities (fingerprints, rules) and resolve them when needed, rather than maintaining links that drift out of sync.
- Sources are sacred. A store is never a source; a source is never modified, except in explicit
inboxmode. - The library is local. Imported metadata (TMDB) belongs to the library once imported: edited in place, never overridden by a layer on top. Only an explicit refresh imports a title again.
- AI helps, never required. Every feature works without a model. Models are called by jobs, on demand, with what they use shown.
- Simple, imperative code. No OOP; favour build and iteration speed.
1. Scope
Today
- A server for Windows, macOS and Linux, with a built-in encoding worker; Docker images for NAS (amd64, arm64); a desktop app for macOS and Windows.
- A single web app, installable, for viewers and administrators (the administration is a section of it), usable with a TV remote.
- Films, series (layouts, versions, own cuts), versions of a title, extras, collections.
- Accounts with profiles (Kids profiles among them), age limits and audiences, suggestions, favorites, resume playback, downloads.
- Each file pre-encoded or played directly, transcoded on the fly when needed.
- Folder and drive sources.
- Additional encoding workers on the local network.
- Remote access: Alumira Connect, a domain of one's own (built-in ACME) or a reverse proxy.
- Optional language-model features.
Later
- Native clients: Android, iOS, Android TV. Web TV clients: LG webOS, Samsung Tizen (reusing the web client).
- Encoding workers over the Internet.
- Per-media ladder override exposed in the UI (the model supports it, see §6.12).
- Intro / credits detection (chapters + cross-episode audio fingerprint).
- Composing tracks from several sources (e.g. a French dub from a DVD over Blu-ray video, PAL speedup correction).
- Dolby Vision; HLS I-frame playlists for Apple's players.
- Videos downloaded from the web (yt-dlp) identified from the id in their name.
- Creating a title sheet by hand in one go.
- Playlists.
2. Glossary
| Term | Definition |
|---|---|
| Source | Location (folder/drive) scanned for original files. Read-only (watch) or consumed (inbox). |
| Delivery | How a file plays: pre-encoded (its stems) or played directly (the file itself, transcoded on the fly when needed). Set per file. |
| Store | Location where Alumira writes normalized stems. Unlimited number. A store is never a source. |
| Fingerprint | Identity of a source file, independent of its path. |
| Source catalogue | Index fingerprint → current paths (0..n). |
| Stem | A single normalized track, stored as one CMAF fMP4 file (e.g. "1080p H264 SDR video", "fr-FR AAC 2.0 audio", "English WebVTT subtitle"). |
| Rendition | A video quality level (resolution × codec × dynamic range × bit depth). One rendition = one video stem. |
| Rung | One step of the ladder (definition of a rendition to produce). |
| Ladder | Ordered set of rungs + audio/subtitle definitions to produce for a media item. |
| Encoding profile | Named, versioned ladder configuration, editable by the admin (Administration → Profiles). |
| Pass | Group of rungs encoded together in one chain; passes are ordered. |
| Work | A title: movie, series, season, episode. |
| Edition | A version of a work ("1977 Cut", "Blu-ray 2011", "Deluxe"…), served by one file. The first one is the main version. |
| Extra | Bonus material (making-of, interview…) attached to a work or an edition. |
| Group | A collection: manual or dynamic. |
| Layout | How a show is cut into seasons: TMDB's aired order or one of its episode groups. |
| Own cut | A version of a show cut into seasons and episodes of its own (a DVD box), not TMDB's. |
| Account | A person signed in: password, passkeys, devices, limits. It holds up to a few profiles. |
| Profile | One viewer of an account: history, favorites, languages, age; a Kids profile sees only titles for children. |
| Audience | Who a title is for: Child, Everyone or Adult. |
| Worker | Encoding process (built into the server or remote). |
| Encoder signature | Identity of an encoding configuration that guarantees consistent output (see §7.3). |
3. Overall architecture
┌───────────────────────────────────────────────┐
│ Server │
Sources ──scan──▶ Fingerprint ─▶ Queues ─▶ Scheduler ────┐ │
(watch/inbox) │ catalogue │ │ │
│ ├─ identification ▼ │
│ ├─ normalization Built-in worker
│ ├─ images │ │
│ ├─ QA │ │
│ └─ AI (models) ▼ │
│ SQLite ◀── metadata Stores (fMP4 stems)
│ │ │
│ HTTP: web app + API + HLS manifests ◀──┘ │
│ + files played directly, transcoded │
│ on the fly (video chip) │
└──────────────▲──────────────────────▲─────────┘
│ │
Web / TV client LAN workers
(source streamed
over HTTP)
- One server binary per platform; one worker binary (the server embeds the same worker logic).
- The server serves the web app, the API, HLS manifests, stems (byte ranges), files played directly and downloads.
- One database writer: writes wait their turn (never "database is locked"), readers read only. A job, a chunk or a picture is never left "running" with nothing doing it: leases, and transitions written in the same transaction as what follows them; invariants are checked every minute and repairs logged.
4. Sources, delivery & catalogue
4.1 Source modes
| Mode | Behaviour |
|---|---|
watch |
Strictly read-only. Alumira never modifies, moves or deletes anything (seedbox, archive drive). |
inbox |
Consumed drop folder: once a media is fully encoded, its source file is deleted, with the subtitles beside it and the folders it leaves empty. There is no trash. |
In an inbox folder, what is left of a file is what its encoding profile keeps:
- its encoded versions only;
- or, besides them, its tracks as they are, nothing re-encoded (the original copy, §6.4: the default for inbox folders).
The admin is warned that a profile changed later can only start from the best existing stem, or from the original copy.
When adding a folder, the admin chooses among three ways, said in plain words: Pre-encoded, Pre-encoded, then removed (inbox), Played directly, transcoded on the fly (a watch folder whose files are played directly).
4.2 Delivery
Each file has its own delivery: pre-encoded or played directly. A new file takes its folder's choice when it comes in, and keeps its own from then on. A file in an inbox folder is always pre-encoded.
The admin changes it in Administration → Delivery, file by file or in batches (filters: delivery, not transcodable, folder, path). Changing a folder's choice applies to the files to come; it then offers to apply it to the files already there.
A file switching keeps playing as before until the new way is ready:
- to played directly: its pending encodes are cancelled, it is prepared (below), then its renditions are removed;
- to pre-encoded: its first pass is queued (refused with a profile that makes no qualities); it plays directly until that pass is done.
The source of a file played directly is never deleted. When it is gone, the file is unplayable: Maintenance lists it as a missing source, blocking.
4.3 Files played directly
Nothing is encoded nor stored ahead, apart from small extras. Each file is prepared once, as it comes in:
- its video keyframes are read (packets only), and segments of 4 s at least are cut on them (a grid of 6 s when its keyframes cannot be found);
- its subtitles, seek thumbnails and chapters are made as for an encoded file. Thumbnails come from its keyframes only, decoded by the video chip when it can, an HDR file's turned into SDR;
- its audio tracks in AAC play as they are; any other codec (AC-3, DTS, TrueHD…) is converted into AAC once, by the processor (there is no audio hardware; it is small). A light stereo version of each track (AAC 64 kb/s) is made too, for the small qualities (§6.6).
It is playable as soon as it is identified and prepared, through HLS like the others. Its video and AAC tracks are virtual renditions whose segments ffmpeg copies from the file when asked, without encoding.
Each segment is read with the file's own times and cut exactly at its keyframes. Every track is delayed by the same few milliseconds its first frames decode ahead (times never below 0, in step): fragments follow on exactly, and a seek plays at once.
4.4 Transcoding on the fly
A device that cannot decode a file played directly, or whose connection is too slow for it, gets qualities transcoded on the fly.
Its ladder. Each encoding profile has a live ladder, set on the Profiles page like its encodes: a resolution, a codec (H.264 or HEVC) and a bitrate per rung. By default, in H.264: 1080p 8 Mb/s, 720p 4.5 Mb/s, 480p 1.8 Mb/s, 360p 900 kb/s, 240p 450 kb/s. Changing it re-encodes nothing.
Beside the file. Its qualities are virtual renditions beside the file itself, cut on the same segments, so a player switches between them seamlessly. Each segment is made in parts of 3 s at most, sent as they are made: a start or a jump plays within a few seconds. Never above the file: two rungs above its size give one, at its size; when the file itself plays, only rungs under three quarters of its bitrate are offered.
On the video chip only. VideoToolbox, NVENC, Quick Sync, AMF, VA-API: on NAS too (Synology, QNAP, Unraid with an Intel iGPU or an NVIDIA card). Decoded by the chip when it can, without B-frames; a server without a hardware encoder offers none. Its formats are tested one by one at start (codec and depth, frames read back from the chip).
On an Intel chip the picture never leaves it. It is scaled there as encodes are (§6.3): by halves at most, with the chip's best filter. Its colours are set there too (BT.709 limited range, or the HDR kept), so SD sources and untagged ones convert as well.
HDR. An HDR file stays HDR for a device that shows it: HEVC 10 bits, the file's PQ or HLG, whatever the ladder's codec, so brightness never changes when the player switches. Without a hardware encoder for it, the file alone is offered. Any other device gets it tone-mapped to SDR.
When it cannot. A file the chip cannot decode, and the processor cannot keep up with (only light files: 8 bits SD, 720p of the light codecs), offers itself only. The device is told why, and Maintenance lists it (Transcoding unavailable, with the devices that could not play it), to be pre-encoded.
Shared fairly. Segments are made when asked, the next one meanwhile, and kept in memory (512 MB) for seeks back and other viewers. One being made goes on as long as someone waits for it. A player has 3 pieces cut or made at a time (its video, its sound, the next one), the server 16 copies; a request beyond waits its turn.
A maximum of viewers are transcoded at once: 2 by default, 1 to 32, on the Profiles page (a hardware encoder is as busy at 100 kb/s as at 25 Mb/s). Beyond it nobody is served worse: a device that can play the file plays it as it is, one that cannot is told to try again in a few minutes or on another device. A place is freed 45 s after its last segment.
4.5 Detection
- Local folders are followed through file-system events: nothing is looked at until something changes there, so their disks sleep in peace. A lost event, or a folder that cannot be followed, falls back to the next point.
- Network folders (NFS, SMB, FUSE…: changes made by other machines raise no events) are looked at every hour by default (15 min to a day, or only on demand, in Sources), and whenever Alumira is reading there anyway (every 10 min at most).
- Admin pages never touch a disk: a source's state and a store's free space come from the last look.
- Stability: a file is only taken into account once its size and modification time have been stable for a configurable delay (a copy in progress).
4.6 Fingerprints
- Quick fingerprint:
size + hash(start) + hash(end) + hash(k sampled blocks). Used as the identity. - Full hash: computed lazily in the background, only when needed (verification, conflict).
4.7 Catalogue
- The catalogue maintains
fingerprint → {current paths}. A moved file = same fingerprint, new path: no re-import. - A media item only references the fingerprint of its source. The path is resolved in the catalogue when needed (re-encoding, etc.).
- Same fingerprint seen several times → imported once.
- Source disappearance: a pre-encoded media stays available (once normalized it no longer depends on its source).
- Inconsistencies (likely duplicates with different fingerprints, missing sources, etc.) surface in Maintenance (§13.2).
5. Pipeline & identification
detection ─▶ probe ─┬─▶ identification ─────────────┐
├─▶ normalization (passes 1..n) ├─▶ visible
│ or preparation (direct) │
└─▶ images (derivatives) ┘
QA (sampled VMAF) ─▶ maintenance
AI jobs (on demand)
- Independent queues: identification, normalization, images, QA, and a lane of its own for jobs calling a model (§13.3). Identification and normalization run in parallel; stems are attached to the source file (fingerprint), the edition is resolved by identification.
- Visibility: a media item is only visible to viewers once it is fully catalogued: identified, and its pass 1 done (pre-encoded) or prepared (played directly). A media item that cannot be identified stays invisible and is listed for identification.
- Priorities: pass 1 of every media item comes before later passes, which maximizes the number of visible media during the initial import. Background tasks run at a lower processor and disk priority: they give way to what someone waits for.
- Every job can be replayed and is idempotent; errors are visible in the administration.
5.1 Metadata providers
- TMDB is the main source: localized metadata, artwork, cast, collections, per-country ratings, episode groups. Each install provides its own API key, entered during onboarding (§13.4).
- IMDb is reached through the
imdb_idgiven by TMDB, and its public datasets give ratings (optional, in onboarding). - A title's rating is 1 to 5 stars, or none: IMDb's rating turned into stars when the title is imported or refreshed, then the title's own, editable in the catalogue. IMDb's weekly update no longer touches it.
- Once imported, a title's data is its own (principle 8): only Refresh from TMDB imports it again.
5.2 Identification
Each file is read from its name and folders: a film, an episode, an extra, which version (cut). It is then looked up on TMDB.
- Two series of one name (a remake: DuckTales 1987 and 2017): the year the file's name gives picks; when it gives none, TMDB is asked.
- A file whose series is known stays the series' even when no episode is found: it is never to identify again. Its episodes are refined on the series' page (§9.3), and Maintenance lists the series under Episode inconsistencies.
- A series chosen by hand for files makes them its own, those identified already as another included: they are identified again by it, not by their names.
- Identification is tried again only when it may work; files wait meanwhile.
Why a file is not identified is kept as a code, and said in words in every language: "No TMDB series is called “The Smurfs” (1981)", "Its name says S02E05, but TMDB has no season 2 for this show"…
The Identification page has four tabs: To identify, Identified, Fixed by hand, Everything.
- To identify, one by one: first one line per unknown series (how its name reads, its files, why), with one button, Identify, which searches TMDB's series; its episodes are then found by themselves. Then one file per line, a TMDB search filled in from its name; a choice is offered at once to the files read the same.
- To identify, as a list: the batch table, with its tools (episodes, film, extras).
- A file can also be entered by hand: title, year, version, overview.
- A poster passed over shows the title's card (its poster, what it is, its overview), to tell apart titles named alike.
6. Normalization
Everything in this section concerns pre-encoded files; files played directly share its subtitles, chapters, pictures and seek thumbnails.
6.1 Source preparation
Before encoding, in the filter chain:
- deinterlacing / IVTC (DVD, TV sources);
- constant frame rate (VFR sources);
- black bar detection and cropping;
- normalization of track start offsets (all stems share the same timeline, t=0).
6.2 Rung consistency rules
- H264 ⇒ 8 bit SDR only.
- HDR ⇒ 10 bit ⇒ HEVC or AV1.
- Never upscale: rungs above the source resolution are skipped.
- Supported HDR: HDR10 (and HLG passthrough). Dolby Vision is out of scope.
- HDR stems carry the source's HDR10 metadata (mastering display, light level:
mdcvandclliboxes), written when their chunks are joined: FFmpeg gives it to one output of a filter graph only. - SDR variants of an HDR master: real tone-mapping (libplacebo), never a plain conversion.
6.3 Downscale chain
A fundamental point: low resolutions must look flawless.
- A single decode per pass, then a cascading
filter_complex: each stage divides the resolution by at most 2 (e.g. 2160 → 1080 → 540 → 270), starting from the previous stage, not from the source. - We encode the stages we care about; intermediate stages only exist for downscale quality.
- Reference scaler: libplacebo (Vulkan; MoltenVK on macOS) or zscale in software. Hardware scalers (
scale_qsv,scale_cuda,scale_vt…) are not used by default (e.g. low-power QSV does nearest-neighbour on large downscales). Hardware is used for encoding. - HDR→SDR tone-mapping sits in the chain at the appropriate stage.
- The chain also feeds the seek thumbnails (§6.10).
6.4 Ordered passes
The rungs of a ladder are split into passes, encoded in order. Each pass = one decode + one chain. The manifest includes rungs as soon as they are ready.
Setup offers two starting points, described by how the library is watched rather than by codecs: Many viewers, full quality (the default ladder below) and At home, sometimes on the go (few qualities, far apart: 4K for the living room, very light versions for a trip). Either is then editable.
Default ladder:
| Pass | Content |
|---|---|
| 1 | 8 bit SDR H264 video: 1080p, 480p, 240p. Audio: every language, AAC 2.0 128 kb/s (downmix, loudnorm) + AAC 5.1 320 kb/s if the source is ≥ 5.1 + light AAC 2.0 64 kb/s. Subtitles. Seek thumbnails. Chapters. |
| 2 | 10 bit HEVC video (HDR kept if the source is HDR): 2160p, 1080p; 8 bit SDR HEVC rescue qualities: 480p, 240p (far lighter than H264 for the same picture on a weak connection). Audio: E-AC-3 5.1 448 kb/s if the source is ≥ 5.1, Opus 2.0 96 kb/s / 5.1 192 kb/s / 7.1 256 kb/s + light Opus 2.0 48 kb/s. |
| last | Original copy: the source's tracks remuxed as they are into the store (no re-encoding). Depends on nothing, slowest to finish. Per encoding profile: never, for inbox folders only (the default), or for every file. |
A quality a later pass of the same profile makes again replaces the earlier one: it is no longer offered, and deleted once nobody has the media open.
Supported codecs: video H264, HEVC, AV1; audio AAC-LC, Opus, E-AC-3; layouts 2.0, 5.1, 7.1.
6.5 Bitrate & keyframes
- Capped CRF (
crf+maxrate+bufsize) rather than a fixed bitrate: a cartoon does not use the bitrate of a grainy film. - Fixed, closed GOP, keyframes aligned across all renditions, scene-cut disabled (forced keyframes). A keyframe and a fragment every 3 s, distributed encodes included.
6.6 Audio
- AAC-LC is mandatory (the only universal codec: Apple, TV, web). Opus as a bonus (web, Android). E-AC-3 for multichannel on TVs / AV receivers.
- A stereo downmix is always produced, with EBU R128 loudness normalization (loudnorm).
- Night track (compressed dynamics, stereo): profile option, off by default. It adds about one stereo stem per language.
- Languages identified in BCP-47 (e.g.
fr-FR/fr-CAfor France / Québec French dubs).
Light audio. Each language has a light stereo version (light rung), in an audio group of its own (aac-light, opus-light). The manifest gives it to the video qualities of 480p and below, so that sound does not outweigh picture on a weak connection; it is used only when every language has one.
It bears the name of the stereo track it stands for: the player keeps the viewer's choice from one group to the other, and its menu only lists languages and layouts (the codec is left to the player).
Each source track on its own. Audio is never chunked (priming issues at the joins): it is encoded in one go, while the video chunks are. Each track has its own FFmpeg (sharing one, a mono track could stop early without any error), is kept as soon as it is made (never encoded again), and fails on its own.
A track that cannot be encoded is left out: the pass goes on, the media plays without it, Maintenance lists it (incomplete encodes), and a new try makes only the missing tracks.
6.7 Subtitles
| Source | Processing |
|---|---|
| Text (SRT, WebVTT, mov_text…) | → WebVTT. |
| Styled ASS/SSA | → WebVTT (universal fallback) + original kept for rich client-side rendering (libass wasm, JASSUB). |
| Image (PGS, VobSub) | Image overlay: subtitles kept as images, rendered by the client; or read into text by a language model, when the admin chooses one for it (§6.9). |
Flags are kept and exposed: forced, SDH (hearing impaired), BCP-47 language. Viewers see them in plain words: a styled track is not told apart in the player (it is the only one listed), and reads "styled" in downloads.
One track per language and use (complete, forced): the best one.
- SDH are complete subtitles, taken only without plain ones.
- Among the source's tracks: ASS, then plain text, then pictures (pictures before plain text when they are read into text: they keep their places on screen).
- A file that is not in the source (§6.8) replaces the track of its language and use, the latest one winning.
- A track removed by the admin, or replaced by a file, never comes back: a re-encode skips it, a scan leaves its file aside, the automatic search never downloads it again (a file chosen by hand is taken, even removed before).
- Media encoded before keep their tracks until their next encode, or Maintenance's Keep one per language.
Tracks in an unknown language (no tag in their file, und, mul): players show them as such, and they count for no language. Maintenance lists them, each with the language to give it (the title's original one first), and the first lines of each.
- A source's track keeps the language it is given by its stream: every encode to come uses it. A subtitle file keeps it on its row.
- The renditions already made take it at once and are renamed after it; for subtitles, the one-per-language rule then applies.
- Any track's language can be changed from the media's technical sheet, a wrong one too.
- With a model for subtitles, AI recognition names them in batches (§6.9).
Forced or not. A subtitle track is said forced or not from the media's technical sheet, with a toggle. It is kept by its stream, over its file's flag and title; a subtitle file's is kept over its name at scans.
6.8 Subtitles from elsewhere
Files that are not in the source: found next to it (<name>.<lang>[.forced|.sdh].srt, or in a Subs folder), or downloaded from OpenSubtitles.
- Each becomes a stem of its own, outside the passes: added once the title plays, without encoding anything again, and never touched by a re-encode or an upgrade.
- Text files in a legacy character set are read in the usual code page of their language.
- A WebVTT file is taken as it is (ffmpeg would lose the cues' places).
- In a media's details, every file added outside the passes (next to the source, downloaded, translated, read, heard) is in an Additions section after the passes.
OpenSubtitles (optional; each install brings its own API key, an account raises the daily download quota). Subtitles are wanted in the default subtitle languages, unless the title already has them, or sound in that language.
- Files made for the source's very hash are taken automatically once the title plays. Others are chosen by hand, the search covering every subtitle language of the platform (default ones and each account's).
- A title without any text subtitles is searched in the language spoken too (its original one, else its sound's), though one hears it: that is what translations start from. Such a file is taken only when nothing found for the missing languages can be translated from.
- Downloads are counted per day: what is left waits for the next day.
- Downloaded files stay in the data directory; the database keeps their record (OpenSubtitles id, release, hash or title match), enough to fetch them again.
6.9 Subtitles by language models
Each of these is on demand and optional, with a model chosen for its use in Settings (§13.3). Each result is a subtitle file like a downloaded one; its stems are marked as machine-made for the admin, never for viewers.
Translation. From a media's details, the admin picks the text subtitles to translate from (any but a translation; the best ones proposed first: complete, in the original language, then English) and the languages to translate into (those without subtitles; the default ones it lacks checked). In batch from Maintenance: the best subtitles, into every default language each media lacks.
- Short notes come first (names, terms, how characters address each other), then the cues by batches of 120, with the scene around them. The timing never goes through the model.
- Each batch done is kept: a stopped translation resumes where it was.
- The result is a
translationfile, its stems namedext.tr-….
Picture subtitles read into text. With a model chosen for it, every import reads its picture subtitles; the catalogue's are read on demand from Maintenance.
- Pictures go to the model by 24, on a neutral ground. Each one becomes a WebVTT cue timed as it was, at its place on screen (top, left, right; low and centred where the player puts subtitles), italics kept.
- Each batch read is kept. The result (
ocr, stems namedext.ocr-<stream>) replaces the pictures: it can be translated, and is never read again.
Subtitles from speech. Each audio track can have subtitles made by listening to it.
- The first track of a language (the one the media speaks first: in the title's original language, else its default one) makes that language's subtitles. They are offered as Create in the media's missing subtitles when it has none in that language, in place of a translation; Maintenance's Missing subtitles makes those of the language it speaks.
- Any other track of a language (a commentary, or whatever it is: no use is guessed) makes subtitles of its own, named after its title, a use of their own: they never replace the language's, nor are chosen by themselves in the player.
A speech model chosen for the use Listening hears the track (from the provider of the AI settings: xAI's Grok Voice Transcribe for now; billed by the hour of sound).
- Only its centre channel for a 5.1 mix or more (where the dialogue is), from the source, else its largest rendition.
- Sent as Opus (about 15 MB an hour), in pieces of about five minutes cut in silences, four at a time, each one's words put back at its place: a whole film takes the service longer than the proxies in front of it wait.
- Its language is given, hesitations left out, the characters' names given to hear them right.
Its words, each timed and its speaker told apart, are cut into cues as a subtitler would. What a model makes up over music, subtitle credits or a long phrase looping is left out.
- Cut at pauses, sentence ends, speaker changes and shot changes (measured as for chapters).
- Within 2 lines of 42 characters (13 to 16 signs in Japanese, Chinese, Korean), long enough to be read (17 characters a second; 4, 9, 12 for those), 5/6 s at least, 7 s at most.
- Lingering 0.3 s after the voice, starting with a shot that starts just before it, ending 2 frames before a shot change rather than over it.
- A quick exchange (two speakers, short lines, the first too short to be read before the second speaks) is one cue, a dashed line each.
Then the model of subtitles, when there is one, gets the cues by 80, four batches at a time, with the scene around them, the title, overview and characters' names. It writes what is said: misheard words and names corrected, hesitations and false starts out, condensed only past what the cue can hold in its time (given to it), the speakers' dashes kept, noise taken for speech emptied. The times never go through it.
The result is a transcription file (stems named ext.stt-<stream>), which translations can start from. It takes two jobs in the AI lane, listening and writing; what was heard and each batch rewritten are kept until the file is made.
Language recognition. From Maintenance's Unknown language, AI recognition sends the model for subtitles, for each track in an unknown language that can be read as text:
- its figures: lines an hour, share of the runtime covered, longest silence;
- about 40 lines taken from all along it.
The model answers each track's language and whether it is forced (a single line is enough to tell a language; one or a few lines are forced). Answers are applied as the admin's own; a track left unknown is logged.
6.10 Seek thumbnails & chapters
- Seek thumbnails are generated from a low-resolution stage of the chain (pass 1), as thumbnail sheets (web, Android). HLS I-frame playlists for Apple come later.
- Chapters are extracted from the source whenever possible, and shown in the player.
Chapters without names are those numbered by their authoring tool: "Chapter 01", "Scene 2", "第3章", a time, nothing. They are told apart in any language without a model: their titles, numbers, times and punctuation taken out, are the same. Three of them at least, or half the chapters, and the media is listed in Maintenance (and Missing in its details).
On demand, a language model (a use of its own) names them from the dialogue of the best text subtitles, in one call:
- 1 to 5 words each, as on a disc's menu, outlining the story without giving it away (no twist, no ending);
- in each language of the platform (defaults and accounts'); chapters with a name of their own (Recap, Credits) keep it, translated.
The names are kept beside the chapters with their start times (a re-encode extracting the same chapters keeps them), in the store's source.json too. The player shows those of its interface language. They can be removed from the media's details (Additions).
Chapters made where there are none, for a media of 15 minutes or more whose source has none, or a single one.
At each encode of the pass making its extras, its scene changes are measured on its smallest rendition and kept: cuts and their strength, black stretches, silences of an audio rendition (a minute or two of decoding).
On demand, the strongest become break points: a fade, black, a sharp cut, the more where nobody speaks and nothing sounds; at least 30 s apart, the start first. The model gets the timed dialogue, the stretches without any and the break points. It chooses about one chapter every 7 minutes, each starting at a break point (a silent scene can start one: the picture tells where it is), and names them as above.
Each start then goes back to where its scene starts: before the exchange that follows it (a question, not its answer), on the best mark at most 2 minutes before (out of a fade first, then after a silence, then a sharp cut). Marks where someone speaks over a cut, or between two close lines, hardly count. The chapters made are kept with their names, so a re-encode keeps them.
6.11 Quality control
- Sampled VMAF is measured on every rendition (3 samples, 5 for a media over 30 minutes). Anomalies surface in Maintenance.
- A second VMAF is measured on a 4K screen: source and rendition both scaled to 3840×2160, VMAF 4K model. Unlike the first one it counts what a smaller picture lost: a common yardstick to rank encodes of any height and codec, whatever the device's screen.
Playback offers each device only the renditions worth their bitrate. One that another playable rendition beats (or matches within 1 point) for less bitrate is dropped, on that yardstick; not measured yet, on the first VMAF at the same height, else the more efficient codec when also cheaper. HDR renditions (VMAF does not apply) only compete on codec.
More bitrate then always brings a better picture: an H264 at twice the bitrate of an HEVC that looks as good is never chosen.
6.12 Encoding profiles: versioning & resolution
Profiles are versioned, and a stem records the profile version that produced it. Editing a profile, or giving a media another one, makes its stems obsolete: an information in Maintenance, not an error (they still play).
Nothing is re-encoded by itself: the admin upgrades them, one media or a batch.
- The stems the current profile makes already are kept (moved to their pass in it); the others are removed at once.
- The passes then run in fill mode, encoding only what is missing; each pass starts from the source with its own downscale chain.
- Impossible once the source is gone.
The effective ladder of a media item is resolved lazily: global default → type (movie / episode / extra) → media override. The per-media override is in the model; its UI comes later.
The admin sees a size estimate while editing a profile. Pre-encoding is refused with a profile that makes no qualities; Profiles can give such a profile the balanced qualities back.
7. Workers & distribution
7.1 Workers
- The server embeds a worker. Additional workers can be added on the local network.
- Token pairing, TLS traffic.
- FFmpeg runs as a subprocess: the one installed on the machine, never linked (looked up through
-ffmpeg/ALUMIRA_FFMPEG, anffmpeg/directory next to the executable, then thePATH).
Capabilities are auto-detected at startup, through a one-second test encode with every known encoder of the build (being compiled in does not mean the hardware is there): codecs, hardware, 10 bit, HDR, and which formats the video chip decodes.
The result is cached until the FFmpeg build changes (a week at most); the administration can run the tests again. A test that fails slowly on a busy machine, or that contradicts the previous detection, is tried again; what stays inconclusive is tested again 10 minutes later.
7.2 Time-slice distribution
- The video of every pass is encoded in time chunks (one setting, 2 min by default; one chunk for a short media), aligned on keyframes, with or without remote workers: one path for every encode, initial or not, and for a rendition tried again.
- Each chunk runs the whole chain of the pass (every rung) on its slice. This server takes its share; remote workers sharing the job's signature take others (when allowed). Workers send a chunk while encoding the next.
- Audio, subtitles and the original copy are processed outside chunks (§6.6), each stage kept as it finishes.
Nothing done is lost.
- A finished chunk is kept: an interrupted encode (stop, crash, reboot) resumes from the chunks it lacks. Changing the chunk length starts the cutting again.
- A failed or cancelled encode keeps what it made (finished chunks, assembled renditions, audio tracks): trying it again from Maintenance resumes the same job and makes only the rest.
Assembly. The server concatenates the chunks into stems, each rendition once, freeing its chunks as it goes. A chunk is checked before it joins (its files cover its range): a short one is encoded again, alone.
One encode at a time per media (a pass, a rendition tried again): each replaces stems the other may use. The encodes of other media go on.
A running job reports its stage as key or key:detail (video:3/12: chunk 3 of 12), shown the same way in every page.
7.3 Encoder signature
To avoid uneven quality within one media item:
- Signature =
encoder implementation (libx265, hevc_nvenc…)for each rendition; the preset/parameters travel with the job. The FFmpeg build is not part of it. - Every chunk of a job is encoded with the same encoder. At assembly, chunks must share their codec settings (
avcC/hvcC…): otherwise the job fails instead of producing a broken stream. - Encoder of a job: the one most machines online can run (server and workers), the profile's mode deciding between equals. Alone, the server keeps its choice; with a PC online, a shared encoder (libx264…) wins.
- Failed chunk: reassigned to a worker with the same encoder; failing that, the whole job is restarted.
- Worker update (signature change): the worker drains its running jobs before updating.
- Per-profile choice:
quality(software) orspeed(hardware).
7.4 Source access
- The server streams the source to workers over HTTP (byte ranges). No shared file system, no path mapping between operating systems.
8. Storage & serving
8.1 Stores
- Unlimited number. A store is never a source.
- Default placement policy: ordered fill. Stores form an ordered list; Alumira writes to the first store that is not full, then moves on to the next.
- A store is full when Alumira's files reach its configurable maximum size (empty: the whole disk), or when its disk keeps only a 5 GB safety margin free.
- A stem is always written entirely on a single store. If the store fills up while writing, the stem is rewritten on the next store.
- The stems of one media item may be spread over several stores (lazy resolution).
- Other policies (spreading, SSD/HDD tiering) may come later.
A store describes itself: no correspondence file, no database needed to understand it.
- The stems of a source are in
media/<its fingerprint>/, each file named after what it is: pass, kind, track, language, settings, flags, profile (p1.video.1080p.hevc.10b.hdr10.crf22.8000k.e1.libx265.@default@3.mp4,p1.audio.t1.fr-FR.aac.2ch.128k.light.@default@3.mp4,ext.os-123456.fr.vtt…). Codec, dimensions and index are read back from the files themselves. - A
source.jsonnext to them keeps what only the source knew: its name, size, duration and chapters. - The
.alumira-storemarker names the store, the installation using it and when it last did (hourly).
Adoption. A store added with content (filled by another installation, or before a database was lost) is taken in by a background job. Every source folder becomes a media with its stems, after a check that each file is complete; it is playable once it has picture and sound, identified from the source's name. When the store's last user looks still active (less than two days), the admin is warned; nothing is refused.
Its sources then join. A scanned file whose fingerprint has a folder takes its place there: it is analysed, and only what is missing is encoded (nothing when its stems follow another profile: upgraded on request, §6.12). Renditions are measured again once the source is there. Sources without stems are encoded as usual; stems without sources keep playing (Maintenance: missing sources, nothing lost).
The same source in two stores (a copied disk): one copy plays, the other is listed in Maintenance, to delete or keep as a spare.
8.2 Stem format
- One CMAF fMP4 file per stem, with an index (
sidx), served with byte ranges. No file per segment: no thousands of small files, and stems are easy to move between stores.
8.3 Packaging
- HLS only (CMAF fMP4): AVPlayer requires it; ExoPlayer, TVs, Shaka and hls.js play it. No DASH.
- Manifests generated on the fly per session: available rungs, client capabilities, user restrictions, bandwidth cap (§11).
- Signed segment URLs (§10.4).
8.4 Downloads
- The viewer picks a version, a quality and the tracks (e.g. 720p SDR + French sound + English subtitles).
- The file is an on-the-fly remux of the chosen stems, without re-encoding, streamed while it is built. Two kinds, in plain words: Complete (MKV), the default, and Plays anywhere (MP4) (styled subtitles left out: their plain copy is offered). Each choice says the formats it takes (H.264 · AAC).
- What can be downloaded: the encoded qualities and sound, the source itself when it is an MP4, subtitles. A title offers a download only when one of its files can be (one with an encoded picture); the link is absent otherwise.
- Allowed per account (Kids profiles never download), subject to bandwidth control (§11). A prepared file is kept 3 hours.
8.5 Images
Formats. A dedicated queue makes derivatives (several sizes) in JPEG, plus AVIF when the machine encodes AV1, else WebP; the browser gets the best format it takes, served with aggressive caching.
Blur first. Each picture keeps a ThumbHash (about 25 bytes) sent with the cards and pages: its blur shows at once, the picture fading in over it (50 ms) once whole, never drawn as it arrives.
Never stale. A picture's URL carries its version (when its sizes were made): kept a year by the browser; a picture made again, or a new one under a reused id, has another URL. Where the version is not known, the browser asks again each time (an answer without the image while it is unchanged).
Artwork. Localized posters, backdrops, transparent logos, cast portraits; dominant colours extracted to theme the UI. TMDB images are fetched in their original resolution and sized from it (backdrops up to 3840 wide). Backdrops are chosen the sharpest first (by steps: 4K, 2560, 1920), then the most liked.
Behind a title (its page, the home's hero, the player): TMDB's backdrop, or a slideshow of pictures taken from the film (from a series' episodes for a show: its first season by itself).
- 6 s each, the next one fading in over half a second, in their order, common to its versions. TMDB's backdrop may lead it (8 s, then the film's pictures).
- Every slideshow follows one clock: picture, fade and glide are those of the current 6 s slot. It goes on without a jump from the home to the title page and the player, or after a page left and come back to. Each slot shows the blur, then a small size, then the full one, each once decoded.
Pictures picked by hand. In the catalogue, the admin finds a moment on the seek thumbnails, then the very frame (a frame or a second at a time), seen in full size, and takes it. As many as wanted, dragged to reorder.
- It is taken from the best there is: the source file while it is there (as the renditions show it: cropped, deinterlaced, at its display size), else the largest rendition (from the fragment holding the time, found with its index: quick even on a large file on a slow disk). An HDR picture is tone-mapped as the SDR renditions are.
- It is decoded by the video chip when there is one (on an Intel chip, only the frame asked is read back), by the processor when the chip fails. Scrubbing, only the picture asked last is taken.
- Black bars are left out, a file played directly's too: found on the seek thumbnails, as the rows and columns from the edges dark on nine in ten of them (a dark scene is dark on a few only).
- Each picture remembers what it was taken from. When a better one comes (a 4K encode done since, the source back), Maintenance says so and takes them again.
Pictures chosen automatically (no model): 6 pictures from the film's seek thumbnails, between 5 % and half of the film (nothing given away).
- Scored on exposure, sharpness, colour and contrast against the film's own; text (subtitles, credits) and a busy or bright lower left (where the title is written) held against them; a few minutes apart and of other shots.
- The best ones are taken at full size, the sharpest kept, in the film's order.
- From the film's sheet (it replaces its pictures, once its seek thumbnails are made), or in batches from Maintenance, which lists the films without pictures of their own, and the episodes without one.
9. Data model
Conceptual sketch (not an SQL schema).
SourceRoot (id, path, mode: watch|inbox, direct) -- direct: its files' delivery by default
SourceFile (quick_fingerprint, full_hash?, size, probe, moviehash?) -- catalogue, kept when the file is gone
SourcePath (fingerprint, source_root, path, seen_at) -- 0..n paths
Store (id, path, capacity, policy)
TrackLang (fingerprint, stream, language) -- set by the admin (or by a model)
TrackForced (fingerprint, stream, forced)
Work (id, kind: movie|series|season|episode, parent?, numbers,
audience: kids|all|adults, age?, rating? (stars), boost?)
Edition (id, work, name, media, position, sparse override fields)
SeriesVersion (show, position, name, own_cut) -- versions of a show
Layout (show, kind: aired|group, seasons and episodes) -- imported once
Extra (id, work|edition, kind, title)
MediaItem (id, edition|extra, source_fingerprint, delivery: encoded|direct)
Stem (id, media_item, kind: video|audio|sub|trickplay, spec, store,
path, profile@version, pass, state, origin: source|external, external_sub?)
ExternalSub (id, source_fingerprint, origin: file|opensubtitles|translation|ocr|transcription,
path, language, forced, sdh, opensubtitles_id?, release?, matched: hash|title)
Localized (entity, language, title, overview, …) -- multilingual metadata
Artwork (entity, kind, language?, file)
Person (id, name, …)
Credit (person, work|edition, role, character?, audio_language?, image_override?)
Group (id, kind: manual|dynamic, rules?, sort, parent?, pinned, landing_order)
GroupItem (group, work|edition|group, position) -- manual groups
Profile (id, version, rungs[], passes[], audio[], subs[], live[], keep_original) -- encoding
LadderOverride (scope: type|media, profile) -- planned, UI later
User (id, role, parent?, kids, birth_date, audio_langs[], sub_langs[],
sub_mode, ui_lang, code?) -- an account, or a profile of one (parent)
Limits (account, downloads, max_kbps, max_streams, max_profiles, suspended, …)
Session (token_hash, account, profile?, junior) -- "Who is watching?", Junior mode
Login (account, at, ip, agent, method) -- kept a year
Invitation (token, role, birth_date, expires)
Device (id, user, name, token) -- TV pairing
Progress (profile, edition|episode-in-edition, position, updated) -- per edition
Watched (profile, work) -- per work
Favorite (profile, work)
Impression (profile, work, shown) -- suggestions shown unopened
9.1 Editions
Each edition is served by one file of the work, chosen by the admin (Edition.media_id). The editions are ordered, and the first one is the main version: played by default, renamable, never removed.
A file may serve several editions, or none: it is then a spare copy, kept and encoded but offered to nobody ("Files not served" in the catalogue, "Likely duplicates" in Maintenance).
- A new file joins the edition its name tells. A cut not seen yet names the main version when it is the work's first file, else becomes a new edition.
- It serves its edition if that has no file yet; otherwise it is a spare copy.
- An edition losing its file takes the best spare copy. So does one served by a file that will never play (a damaged source, or one gone with nothing encoded), as soon as a spare copy of it plays.
Sparse override: an edition only stores what differs from the main version; reading = edition.field ?? canonical.field (title, overview, artwork, cast, age…).
Editions do not share a timeline: audio tracks belong to an edition, never shared. Viewers only see the editions that have a file; a single one shows no picker.
9.2 Cast
- A cast member's thumbnail can be specific to a work / edition (
Credit.image_override). - Dubbing depends on the audio language, not on the edition: a dubbing credit carries an
audio_language. The UI shows the voices of the viewer's audio language.
9.3 Series
Layouts. A show is cut into seasons by a layout: TMDB's aired order, or one of its episode groups (by year, as TheTVDB has it, by story arc…). Layouts are imported once with the show, and again only when the admin refreshes it from TMDB.
- A file is on a TMDB episode whatever the layout: changing it moves no file and loses nothing watched.
- The admin chooses the layout shown (automatically: the aired order, unless a season holds more than 100 episodes) and those offered. Each viewer chooses among those offered, on the show's page. Labels, resume, next episode and missing episodes follow it.
- Specials stay specials (season 0). Specials a layout leaves out keep their aired place; other episodes it leaves out go to Other episodes.
Reading file names. The numbering a show's file names follow is found from them: the aired order, or the layout under which most land on an episode, their titles (in any language) and lengths agreeing. This is how Sonarr's TheTVDB seasons are read. The admin can set it; changing it identifies the files again, those placed by hand aside. A layout is used to read names only if it holds every episode (an arc never).
Versions of a show (a French dub, Remastered…) are defined on the show, in the order viewers get them; the first one holding files is shown first. The main version needs no name. An episode has an edition per version it has a file in.
The viewer picks a version on the show page and only sees the seasons and episodes that version has: nothing falls back on another version. One version only shows no picker; one season only shows no tab; beyond three seasons, a menu.
Own cuts. A version can have its own cut: the same show cut otherwise, as on a DVD box. Its files leave TMDB's episodes for episodes of its own, one per file, in seasons of its own (never the main version). Viewers see its seasons as "Season n", its episodes as S1 · E1.
Files and episodes.
- A file covering several episodes (e.g. E01-E02) maps to a range: the viewer sees one block, played from its first episode; the other episodes of the range cannot be played on their own.
- Two files on the same episode of the same version: one too many, reported (put one in another version).
- Files on no episode, or whose title or length match another episode, make the series listed in Maintenance (Episode inconsistencies). Titles are compared only where most of the show's titles agree.
Episodes & versions (in the catalogue, for each show) edits all of this, in Identification's batch design (§13.1):
- The versions' table: what each holds (seasons, incomplete ones, episodes, missing, duplicates, files); order, names, own cut, new versions. A row unfolds its seasons, each edited there; an own cut's seasons are added and removed there.
- The files of the version worked on (chosen in a menu), episode by episode, the episodes without a file as places to drop one on; beside them, the files on no episode.
- Files are dragged onto an episode (one already there goes back among those to place), several filling the holes from there; dragged back to take them off; into an own cut as its last episode, and reordered in it.
- Files selected go to a tool in a dialog: from the names, a pattern (
{s}season,{e}episode,*anything, a space any separator; without{s}the season comes from the folders), season, shift, renumbering, version. Where each file would go is previewed by the server, then applied; recent changes can be undone. - An episode's row edits its title in place and opens its editor (TMDB's episodes and an own cut's alike); a file's technical sheet opens in a column.
9.4 Groups
- Manual (ordered list) or dynamic (rules, e.g.
studio = Disney AND year < 1967), nestable. - Can be pre-filled from provider collections (TMDB…); collections the library lacks films of are listed in Maintenance.
- Pinnable on the landing page (global, by the admin), in the order the admin sets.
- A group that is empty after filtering (age, audience, visibility) is not shown.
9.5 Progress
- Progress per edition (different timelines); watched status per work; both per profile.
- Continue watching shows where each title stopped, with a picture taken from the film. "Next episode" stays in the chosen version and layout of the show, with no fallback: the version stops where its files stop.
- Progress writes are buffered in memory and flushed periodically (SQLite write load).
10. Users, access & security
10.1 Accounts & profiles
Two roles: admin and user, in a single app whose administration is a restricted section. An admin can neither remove their own role, nor suspend or delete their own account.
An account holds profiles: 5 by default, itself counted (the admin changes it per account). Once signed in, the viewer chooses one: Who is watching?
- A profile has its own name, colour, photo, history, favorites and languages, and an age only ever lower than the account's.
- The account's limits, suspension and sign-in (password, passkeys, devices, sessions) are shared, and only the account changes them.
- Only the account creates, edits and deletes its profiles, from Who is watching? (behind its code when it has one). A profile deleted is signed out where it was chosen.
- A code of 4 digits may guard each profile, typed on a pad a remote reaches. Wrong codes are limited (5 in 15 minutes, 20 a day).
Kids profiles see only the titles for children and for everyone, within their age. They never download, cannot edit themselves, and leaving one takes the account's code (so the account needs one).
Junior mode: on one device (its sign-in session), an adult profile shows what a Kids profile would: the titles for children, and those for everyone rated for all ages. It applies everywhere (home, lists, search, Continue watching); the profile's history and tastes stay its own. It is switched from the profile menu; the app's name then reads Junior in light pastel letters. It has no lock: a Kids profile is what protects a child.
Users have no editorial control. They play, choose audio and subtitle tracks, add favorites, download (when allowed), and set their languages: audio, subtitles (never, forced only, always), metadata and UI. These drive the automatic choice of tracks.
10.2 Authentication
- Invitations (admin only): a link valid 7 days by default (60 at most), used once. The admin sets the role and birth date; the invitee creates a password or a passkey (WebAuthn; it needs a domain name, not an IP address).
- TV pairing by code: the TV shows a code (
XXX-XXX, 10 minutes); the viewer enters it at<server>/link. - Sessions last 90 days, renewed as they are used; only their hash is stored. Changing one's password signs out one's other browsers (not TVs).
- Rate limiting on sign-in, invitations, passkeys, password changes and pairing (per address and per account).
- Suspension refuses new sessions and ends the current ones.
- Sign-ins and visits (a session back after half an hour) are kept a year: when, from where (IP), with what, how.
10.3 Internet exposure
Alumira Connect, in one click: a name under alumira.app (martin.alumira.app) that leads home, in HTTPS.
- Its certificate comes from Let's Encrypt by DNS-01, for the name and
*.name. - The router is asked to open the port (UPnP). The addresses (IPv4 as the service sees it, the machine's own IPv6) are sent again every 10 minutes, and the whole is checked from the Internet.
- One port everywhere: 443 when the machine lets Alumira have it, else 7576, the same on the machine, the router and in the address (IPv6 translates no port).
- Nothing of the video goes through Connect: it only names. A local address written as a name (
192-168-1-31.martin.alumira.app) leads home in HTTPS without the Internet. - The service is in
internal/connect, Alumira's side ininternal/network. It runs in alumira.app's server (cmd/alumira-app: the zone's DNS server, which also serves the zone's own names — the site, its mail — from a zone file, and Connect's API).
Otherwise: built-in TLS with automatic ACME (Let's Encrypt) for a domain of one's own, or a reverse proxy in front.
- In HTTPS the session cookie is a
__Host-one: under a shared domain, a neighbour cannot shadow it.
10.4 Signed URLs
- Manifests, segments and downloads are reached through signed URLs (HMAC, valid 24 hours): native players cannot always send authentication headers.
10.5 Age limits & audience
Age limit. No rating systems are exposed: everything is translated into an age limit, shown as (-15) in a white circle, on the home's hero and the title's page only.
- The rating → age table is built into Alumira (per country), not a setting. A rating missing from it is reported in the log when a title carries it, and gets added to the table.
- At identification (or a refresh from TMDB), the rating of the reference country (else the US one, else the strictest mapped one) gives the title's age limit. Ratings themselves are not stored; the admin then edits the age.
- The age limit may differ per edition (e.g. an unrated version).
- A viewer's age comes from their birth date; a title above it is invisible. Unrated titles are invisible to minors.
Audience: who a title is for, set by the admin.
- Child: Kids profiles (and Junior mode) only. An adult's lists never show these titles; asked for by name (the search, after the others; a title's page; playing it), they are found, said so on the poster.
- Adult: never in a Kids profile.
- Everyone.
It is set once by a rule when a title is identified (family, animation or kids genres and a low age; TMDB's kids genre: Child), then only edited. A show's seasons and episodes follow the show.
Filtering is applied server-side, everywhere: lists, search, cast pages, groups, direct URL access, signed URL issuing, pictures, downloads. A dedicated admin page edits the age limit of every title, rated or not, with batch actions.
11. Bandwidth & sessions
Constraint: a VOD HLS manifest is loaded once by the player; a rung cannot be removed during playback.
- Global upload budget: measured during onboarding (§13.4), then confirmed or adjusted by the admin; editable later in Settings. The local network is never capped.
- Admission when a playback session opens: the server caps the highest rung in the issued manifest according to the budget.
- Per-session throttling (token bucket): under saturation, playbacks share the budget fairly, by what each actually uses, and the player steps down by itself.
- Playback before downloads: downloads only use what playbacks leave; beyond that they are queued (asked to come back).
- Per-account limits, set by the admin: simultaneous playbacks, bitrate, maximum resolution, codecs, HDR, stereo, downloads.
12. Web client & UX
12.1 Constraints
- The web client is also the future webOS / Tizen client: light bundle, conservative JS/CSS (TV browsers lag behind Chromium).
- D-pad navigation from day 1: the whole UI is usable with arrows + OK, focus always visible. A small, imperative in-house module moves focus spatially.
- Framework: SolidJS + TypeScript + Vite. No virtual DOM (fine-grained reactivity): better performance on weak TV CPUs, a bundle of a few KB. Conservative build target (ES2017).
- Long lists (20k movies): grids are always virtualized.
12.2 Landing page
A hero, then rows, a title appearing in one row only, in this order:
- The hero: drawn from the best suggestions with a backdrop, not those of the last visits.
- Continue watching: where each title stopped, with a picture taken from the film.
- For you.
- Recently added: the latest first, never the library's base. A title is new once it came in a week after half the library was there (a library imported at once has nothing new until something comes).
- Best rated: the best not already shown above.
- Pinned collections, in the admin's order.
- To discover.
Suggestions. Each title a profile may see is scored in points:
- its rating (two points a star; none: the library's mean);
- the genres the profile watches lately, episode by episode, the latest counting most;
- its original language, for a language the profile watches more than the library holds it;
- its collection, when as good as the mean; its freshness; the admin's boost;
- less how often it was shown unopened. A title being watched, or watched within the year, is left out.
The rows are drawn from these scores, weighted, and stay the same within a visit. The admin's Suggestions page tells why each title is where it is.
A show's card says how many seasons there are to watch ("2022 • 3 seasons").
12.3 Title page
- Full-bleed artwork or the slideshow (§8.5), logo, overview, stars, age limit.
- A version picker, when there is more than one (at the show's level for series); seasons as tabs, or a menu beyond three; a layout menu when the admin offers more than one.
- Play or Resume, From the beginning, favorite, download (only when possible: §8.4).
- Extras, cast (portraits) and voices of the viewer's audio language, then the details: original title, genres, director, studios, collections.
12.4 Player
- Player: hls.js on a bare
<video>, with an in-house player UI. HLS-focused (no DASH), light, widespread on TVs; supports ManagedMediaSource (iOS Safari). - Tracks: audio and subtitles chosen automatically from the profile's preferences, changeable during playback without losing what is buffered. Forced subtitles are applied automatically.
- Styled subtitles (libass in WebAssembly, JASSUB) and picture subtitles drawn as an overlay by our UI.
- Chapters (named in the interface's language) and seek thumbnails.
Choosing the quality (web/src/abr.ts, in place of hls.js's own): each fragment that came as fast as the link carries it is measured (its speed, its wait for the first byte).
- A steady link jumps at once to what it carries most of the time. A link of bursts and holes climbs as its buffer fills, up to its usual speed.
- It steps down under 20 s of buffer, at once under 10 s.
- It loads 40 s ahead at home, five minutes away from home.
A statistics panel (technical by design) shows each stream's codec, quality and bitrate, the segment loading and the speed. Admins can play any file over a page to see what it is, its progress not kept.
12.5 Localization
- The UI is written in English; other languages are dictionaries keyed by the English string, loaded on demand: French, German, Spanish, Italian, Portuguese, Dutch, Russian, Japanese, Korean, Chinese. Server messages shown in the UI are translated the same way. A new account follows the browser's language when Alumira has it.
- Metadata is localized in the viewer's language, falling back to the original language.
- Search covers the original title, translated and alternative titles, and people (SQLite FTS5). Names are sorted as read: accents, case and ligatures folded.
12.6 Installed app & desktop app
Installable web app: a manifest and its icons (one with Android's margin). Chrome and Edge offer "Install", Safari "Add to Dock" or "Add to Home Screen": Alumira opens in a window of its own, without the browser's bars (on an iPhone its cookies are its own: signing in once more).
Desktop app (cmd/alumira-desktop, macOS and Windows, Wails v3 over the system's web engine: WebKit, WebView2): the web app in a window of its own, and an icon in the menu bar or the notification area while it runs (a click: the window; a right click: its menu).
At its first start it asks:
- host the server here: started in the same program, on port 7575 (or
ALUMIRA_LISTEN), its data where the command line keeps them. Closing the window hides it; the server goes on for the other devices. Its menu gives the address on the network, starting with the computer, the log, and quitting (which stops it as an interrupt does); - or use a server on the network: those heard are listed, or an address is typed (checked through
/api/status).
Missing FFmpeg, a port taken, a server that does not answer: said in the window, with what to do and a button to try again. One instance at a time; started with the computer, the icon only. On macOS, the Dock shows it while its window is open.
Found on the network: a server listening beyond its own machine says so (mDNS / DNS-SD, _alumira._tcp, under its name, else its machine's); the desktop app lists those it hears. In Docker, only with the host's network.
Packages: a disk image for macOS (one app for Apple chips and Intel, macOS 13 and later) and an installer for Windows (NSIS: Program Files, Start menu, a firewall rule for the private network; uninstalling leaves the data), built by scripts/build-desktop.sh and part of scripts/release.sh.
Both carry Alumira's FFmpeg (jellyfin-ffmpeg with VMAF, built from github.com/ejulien/jellyfin-ffmpeg), in an ffmpeg folder beside the program, where Alumira looks first: nothing else to install. They are not signed: macOS asks to allow the app once, SmartScreen to run it anyway.
13. Administration
13.1 Pages
The menu, in its order:
- Normalization: what is encoding now and on which machine, what comes next, recently finished jobs, workers, stores (real time; no time estimate, too dependent on the workers).
- Identification: the files to identify, one by one or in batch (§5.2).
- Maintenance: what needs a hand (§13.2), with likely duplicates and the episodes and films the library lacks.
- Jobs: every background task (analysis, identification, encoding passes, quality checks, AI jobs), by state and kind, with cancel and retry; each AI job shows its tokens.
- Catalogue: every title, with its source under it (its file, or the folder its files share).
- A title is identified again from a magnifier, without leaving the list.
- Rating, age limit, audience and boost are edited in place, or for many titles at once (asked first for ten or more).
- Each title sheet is edited by hand: information, texts in each language, artwork, cast and crew, voices, versions and files, seasons and episodes, pictures from the film; Refresh from TMDB. For a show, Episodes & versions (§9.3).
- Collections: manual or dynamic, pinned, ordered on the landing page.
- Age limits: the age limit of any title, rated or not, one at a time or in batch.
- Suggestions: a profile's suggestions, best first, each title's score explained.
- Users: accounts with their profiles, and invitations.
- Filters (search, role, state, last seen) kept in the address; actions on many (suspend, reactivate, sign out everywhere, delete), never on one's own.
- Per account: role, birth date, limits (§11), profiles allowed.
- An information bubble tells its last visits, days active, time watched (in all and per title), devices and places (IP).
- Sources: add, mode, scan state.
- Delivery: each file pre-encoded or played directly (§4.2).
- Stores: add, capacity, placement.
- Profiles: encoding passes, the on-the-fly ladder and places, versioning, size estimate.
- Workers: pairing, capabilities, signatures, state, chunks at once, pause.
- Status: the machine (processor, video chip, memory) and the figures of the moment.
- What plays and runs right now; each playback: who, where it is in the title, its buffer, the qualities it was sent, its upload over the last two minutes.
- Charts from 10 minutes to 30 days, zoomed together: upload stacked by viewer, playbacks, tasks, the busiest hours.
- The server's life since it started: records, what it served and encoded, what was watched.
- Journal: every event of the server (what the terminal shows).
- Kept on disk as JSON lines, in files that turn (a size set in Settings, 100 MB by default, the oldest dropped).
- Each event has its grade (debug, info, warning, error), its system (server, sources, catalogue, encoding, playback, images, subtitles, AI, storage, accounts, jobs) and its lines.
- The page filters them by grade, system, text and period (kept in the URL), follows the new ones while its top is in view, and links the media named to their sheet. What is shown downloads as text; the whole journal as a zip.
- Debug events (every FFmpeg command among them) only when turned on in Settings, or with
ALUMIRA_DEBUG_FF=1.
- Backups: scheduled database snapshots (the most valuable asset: matching, editions, groups, edits), each downloadable; a backup on demand.
- Settings: catalogue, languages, OpenSubtitles, AI features, network access, sharing the upload, journal.
Batch editing looks the same everywhere: Identification's table, its selection header and its tool dialogs. Every file's thumbnail in the administration plays it.
13.2 Maintenance
In the first part of the menu: what needs a hand and what the library lacks, most urgent first, one tab per kind present, with batch actions (Select all reaching beyond the items listed). The page follows the jobs that change it.
- Failed normalizations.
- Incomplete encodes (audio tracks left out).
- Transcoding unavailable (§4.4).
- Missing sources that block an encode, or a file played directly.
- Sources too damaged to be encoded (to replace).
- Insufficient quality (a rendition under 85 VMAF).
- Likely duplicates.
- Unidentified files.
- Episode inconsistencies (§9.3).
- Tracks in an unknown language (a language to give each, or AI recognition).
- Unknown genres.
- Stems of another profile (§6.12).
- Missing episodes: aired episodes no file brings (some can be set aside).
- Episodes without a picture.
- Incomplete collections.
- Quality not measured.
- Missing subtitles in a default language (OpenSubtitles search within the day's quota, made by listening, translated).
- Several subtitles for one language.
- Picture subtitles (to read into text).
- Chapters without names; without chapters.
- Sharper pictures available; films without pictures of their own.
- Extra copies of a media in another store (§8.1).
- Missing sources with nothing lost.
13.3 AI features
In Settings: one provider (OpenAI, Anthropic, xAI's Grok, or any API compatible with OpenAI's, a local one included: Ollama, LM Studio, vLLM…) and a model for each use, chosen from the provider's list, or none:
- Subtitling: translating subtitles, writing those heard, recognising the language of tracks;
- Listening: subtitles from speech;
- Reading picture subtitles;
- Chaptering: naming chapters, and making them where there are none.
Models are only called by jobs, on demand, in a lane of their own that never waits for an encode nor holds one. How many run at a time is a setting (2 by default, 16 at most). The provider bills its account's owner for each call.
- A key is checked as soon as it is typed, by listing the provider's models.
- Each job shows the tokens it takes as they grow: read (those the service had cached among them) and written; the total stays in the jobs' history.
- Transient failures (rate limits, an overloaded service) are tried again. A call taking more than 20 s, or tried again, is told in the journal: when its answer began, when its text did, its reasoning tokens, why its tries failed.
13.4 Onboarding
On first launch the admin goes through an onboarding that asks the key questions to get a working install. Principles:
- Every step has sensible defaults; everything can be changed later in the administration.
- The onboarding can be resumed: its state is persisted, an interruption loses nothing.
- No scan or encode starts before the final confirmation.
Steps:
- Admin account: username, password or passkey, birth date, UI language (which also sets the default audio, subtitle and metadata languages, and the country of age ratings).
- Network access: Alumira Connect in one click; or, by hand, public URL, TLS mode (built-in ACME / reverse proxy / local network only), port.
- Stores: at least one store. Write check, free space, measured disk throughput. List order (§8.1).
- Sources: zero or more folders, each pre-encoded, pre-encoded then removed (
inbox), or played directly. Quick pre-scan (number and volume of files). - Encoding: capability detection of the built-in worker (test encodes),
quality/speedchoice. - Ladder: a starting point (§6.4), editable, whether to keep an original copy, and the storage estimate (source volume × ladder) against store capacity.
- Metadata: TMDB API key, with a link to the TMDB key creation page (opened in a new tab); metadata languages; country whose ratings set the age limits; IMDb ratings.
- Bandwidth: upload throughput measurement (opt-in, through an external speed test service; manual entry as a fallback), then the budget (§11).
- Default languages of new users (audio, subtitles and when to show them).
- Backups: location and frequency of database snapshots.
- Invitations: first users (optional).
- Summary → scanning and queues start.
14. Technical stack
| Element | Choice |
|---|---|
| Server & worker | Go, imperative style (structs + functions), no cgo → static binary, trivial cross-compilation. |
| Database | SQLite (WAL), pure Go driver (modernc.org/sqlite), FTS5 for search; one writer, readers in parallel. Thin access layer to allow Postgres later. |
| Media | FFmpeg / ffprobe as subprocesses (crash isolation; a separate program, never linked), the build installed on the machine. |
| Scaling / tone-mapping | libplacebo (Vulkan / MoltenVK), zscale; the video chip's own for transcoding on the fly. |
| Packaging | HLS CMAF fMP4, dynamic manifests. |
| Client | TypeScript, SolidJS, Vite, hls.js. |
| Desktop app | Wails v3 over the system's web engine. |
| Remote access | Alumira Connect: its own DNS server (miekg/dns), ACME through x/crypto/acme. |
15. Orders of magnitude
Alumira is built for a household's library, and sized so that a large one holds on modest hardware: 20,000 movies, 1,000 accounts.
| Topic | Order of magnitude | Consequence |
|---|---|---|
| Storage | 20k × 10–20 GB ≈ 200–400 TB (excluding series and original copies) | Multiple stores and placement are essential; playing files directly stores nothing twice. |
| Encoding | ~40,000 h of content | An initial import of several weeks/months → ordered passes and priorities are critical. |
| Streaming | 50–100 concurrent streams × ~15 Mbps (4K) ≈ 1–1.5 Gbps | Disk/network bound, not CPU → bandwidth control (§11); transcoding on the fly limited to a few places. |
| Database | ~100k media entries (episodes included) | SQLite is enough with one writer, buffered progress writes, and admin pages read in a few queries. |