The upload form in Tiny Desk AI has three possible seats: lead singer, sax player, and keyboard player. You choose Solo, Duo, or Trio, then add one photo per person. The output is a short AI-generated desk-concert clip with a fixed sequence.
I'm working on this project. This post is about the photo form, because even with only three inputs, there are a few ways to lose someone's choices or assign a photo to the wrong seat.
Give each photo a role
The form stores photos by role:
const CAST_ROLES = ['lead', 'sax', 'keys'] as const;
type CastRole = (typeof CAST_ROLES)[number];
type Photos = Partial<Record<CastRole, File>>;
The selected cast size controls which roles are visible:
const seats = CAST_ROLES.slice(0, cast);
That leaves one ordering to use for rendering the inputs and building the upload list. The singer stays the singer when someone switches from Trio to Solo and back.
Switching the cast size doesn't delete the hidden photos. Removing a photo is a separate action. Someone can compare the options without having to pick the same files again.
There is a limit to this design: each cast is a prefix of the same list. It works for this product because Duo always means lead plus sax. If we added a lead-and-keyboard option, a count alone would no longer describe the selection.
A thumbnail isn't the upload
Each selected file gets an object URL for its preview. The component revokes that URL when the file changes or the component unmounts.
The form also checks that the browser can decode the image, then checks its dimensions and aspect ratio. A file picker accepting JPEG, PNG, and WebP only helps with selection; it doesn't prove that the file is usable. These browser checks give earlier feedback. Server-side validation still has to protect the upload endpoint.
After a successful upload, the form remembers both the File object and the returned upload ID for that role. On another submission attempt, an unchanged file can reuse its ID. Choosing a replacement clears the cached upload for that seat.
That distinction matters when one upload succeeds and a later one fails. There is no reason to send the first photo again while the page still has its successful upload recorded.
Restoring a draft can race with the user
Selected files are saved in IndexedDB. Session storage holds a reference for each role, and the saved records have a 24-hour validity window. This is local recovery in the current browser session, with no cross-device promise.
Loading those files is asynchronous. A person can select a new photo before restoration finishes. If the old draft then overwrites the new selection, the form has undone their work.
The component keeps a choice counter. Restoration only applies while that counter is still zero. Once someone starts choosing photos, their current choices take precedence.
The storage helper also waits for the IndexedDB transaction to complete before resolving a save. A successful put request alone isn't enough: the transaction can still abort.
What the form can't promise
A clear preview and a correct role assignment don't guarantee a good generated video. Faces can drift, especially through motion. The homepage shows the input photos alongside sample results and explains which parts of the clip are fixed, so people can judge the format before paying.
The frontend has a narrower job: keep the selected photos in the right seats, explain invalid inputs, and preserve work where it can. For this form, those details deserve as much attention as the generate button.
Top comments (1)
One option is to keep the role-keyed File state above the conditional inputs and key each input by role rather than position. When a photo is explicitly removed, clear that role’s state and upload ID, then reset its native file input (for example, via a ref). That also lets the same file be selected again. Switching Trio to Solo should hide the inputs without treating that as removal.
For async work, give each role a revision that increments on selection or removal. Capture the revision and File when validation or upload starts, then apply the result only if both still match the current role. That prevents a late result for an old photo from attaching an upload ID to its replacement. The same per-role check could make restoration less coarse than the existing global choice counter while still letting a new choice win. Test rapid Trio → Solo → Trio switches and replacement during an in-flight upload.