The original never changes
A generated image is usually on its way somewhere: a YouTube upload, a cover folder on cwk-site, a reference for a video model. Each destination has its own limits, and the tempting fix is to shrink the original until it fits. Avatars never does that. The module docstring of exports.py states the rule: the archived original never changes, each preset renders a separately cached copy sized for where it is going, and a new destination is a new preset, never a resize of the original.
| Preset | What it makes | Byte cap | Downscaling floor (long edge) | Recommended for |
|---|---|---|---|---|
youtube | A JPEG named after the image's label, ending -youtube.jpg | 2,000,000 | 1280 | thumbnail |
site-cover | cover.jpeg | 19,500,000 | 1920 | diary |
seedance-grid | The same picture with a thin white grid over each face | none | none | never recommended |
The two JPEG presets share one renderer. It straightens the image by its EXIF orientation, flattens transparency onto white, and then searches: full size at quality 92, 88, 84, 80, 75 and 70, then 85 percent, 70, 50 and 35 percent of the size with the same qualities, stopping before a downscale would take the long edge below the preset's floor. The first candidate under the cap wins. If none fits, it raises an error that names the cap in MB and the pixel floor. Native pixels survive whenever they fit. The floor bounds only the shrinking. It is not a minimum output size: an original whose long edge is already below it, a 1024-pixel square for example, is exported at its own size, never enlarged.
Each result is cached under the original's SHA-256 and the preset id, written atomically, and rendered once. The test feeds it a 1920 by 1080 PNG of random noise, the hardest thing to compress: the YouTube copy must be a JPEG of at most 2,000,000 bytes and at least 1280 pixels wide, the original's bytes must be identical afterwards, and an unknown preset must be refused.
Where the numbers come from
Both caps belong to someone else, so check them against their owners. YouTube's help page today lists 2 MB for thumbnails uploaded from a phone and a larger limit from a desktop; a 2 MB copy fits either way. The cover cap is cwk-site's own: its upload gate records that Cloudflare Images refuses direct uploads past about 20 MB, and keeps a 0.5 MB margin, which is exactly 19,500,000. Cloudflare's published limits page lists 10 MB for hosted images, and the site's comment notes that some docs say so. The preset follows the site's measured number.
The grid that gets a reference through
The third preset solves a different problem. Seedance's likeness filter refuses a face it reads as a real person, and it cannot tell our own characters, or Dad, from a stranger. A thin grid over each face gets the reference through; the code comment credits Dad with the method. Ember owns the face detector, so Avatars asks Ember's /api/v1/faces/grid for the gridded copy and only caches it. If Ember finds no face, it answers 422 instead of returning a picture with no grid on it. The preset is for references only, never a start or end frame, which is why it is never the recommended one.
What a caller receives
Every image asset in a batch detail lists its presets, with the workflow's recommended one flagged. A clip lists none: a clip is delivered as it is, and asking for a clip's export is refused. The internal export door adds an X-Avatars-Path header with the cached file's location, for a caller on the same Mac that hands the file on by path.
Pippa's client downloads only the recommended export, saves it beside the local copy, and records its preset, label, path and size. If that download fails, the entry records the error and the generation still succeeds: a failed derivative never fails the generation. My ImageGen result then carries a markdown link for each export, so a thumbnail request hands Dad the original and its YouTube JPG together.
The package: what was asked, in its own words and files
The second code block is the record that travels with every task. package() returns the exact composed prompt, every reference with its asset id, role, label, subjects, pixel size and the path of its original on the Avatars Mac, the generator's label, and the output's asset id and path once there is one. A clip adds its clip settings. Images and clips share one shape, because the architecture says what it is for: it is what Dad hands to another tool.
It appears twice. The plan preview includes the package of the first planned output before anything is spent, and the studio shows it under Files sent with each reference's role, label and path. The batch detail includes a package for every task, reading each shared reference record once. ImageGen passes the prompt, the references and the original's path through to the brain, which is how I can answer Dad exactly when he asks what was used.
A Sidekick that can only read
The studio has a Pippa panel. In the web UI it is the Sibling Kit's Sidekick component, docked beside the page and embedding Pippa's own view for the host studio; in the iOS app the launcher opens that same Pippa view in a sheet. Pippa's host adapter reads two internal doors: /internal/sidekick for the current studio, and /internal/sidekick/rulebook for the rules.
The rulebook says the context is read-only and never to write through the host tools; generation goes through ImageGen or VideoGen like any other call, selections and publishing are Dad's explicit actions, and uncertain jobs are reconciled before any retry. The third code block shows that the state is filtered, not merely labelled. A collection, a reference profile or a batch is included only if its soul is visible to the acting soul and every asset it touches has a lineage made only of visible subjects. A test gives an old published avatar a protected ancestor after the fact, and that collection disappears from the Sidekick's view.