Designing a Reliable Two-Photo Upload Flow for AI Video Apps
A two-image upload sounds like a small UI problem, but it has a surprising number of failure modes. Users can select the same file twice, mix unsupported formats, leave one slot empty, or replace a file after a preview U
A two-image upload sounds like a small UI problem, but it has a surprising number of failure modes. Users can select the same file twice, mix unsupported formats, leave one slot empty, or replace a file after a preview URL has already been created.
This is the approach I use for a workflow where two portraits become a short music video. The same pattern works for comparison tools, before-and-after editors, and paired-image generators.
Model slots, not a generic file list
When the product needs exactly two inputs, represent that requirement directly:
const state = {
left: null,
right: null,
errors: {}
};
A generic array makes ordering and replacement harder to reason about. Named slots also make error messages clearer: βChoose the second photoβ is more useful than βTwo files are required.β
Validate before creating previews
Run cheap checks first, then create object URLs only for accepted files:
const ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
const MAX_BYTES = 10 * 1024 * 1024;
function validateImage(file) {
if (!file) return 'Choose an image.';
if (!ALLOWED_TYPES.has(file.type)) return 'Use JPG, PNG, or WebP.';
if (file.size > MAX_BYTES) return 'Keep the image under 10 MB.';
return null;
}
The server must repeat these checks. Browser MIME types and file extensions are only hints, not a security boundary.
Clean up object URLs
Repeated replacements can leak memory if old preview URLs are never released:
function replacePreview(slot, file) {
const previous = state[slot];
if (previous?.previewUrl) URL.revokeObjectURL(previous.previewUrl);
const error = validateImage(file);
state.errors[slot] = error;
state[slot] = error ? null : {
file,
previewUrl: URL.createObjectURL(file)
};
}
Also revoke both URLs when the component unmounts or when the upload finishes.
Detect accidental duplicates
Comparing names is not enough. Two different files can share a name, and the same file can be renamed. A lightweight first pass can compare size and lastModified; a stronger check hashes the bytes with crypto.subtle.digest.
async function sha256(file) {
const bytes = await file.arrayBuffer();
const digest = await crypto.subtle.digest('SHA-256', bytes);
return [...new Uint8Array(digest)]
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
}
Hashing very large files on the main thread can feel slow, so set a sensible upload limit and show progress if the check takes noticeable time.
Make submission state explicit
The Generate button should be enabled only when both slots are valid and no request is running:
const canSubmit =
state.left &&
state.right &&
!state.errors.left &&
!state.errors.right &&
!isSubmitting;
Disable replacement controls during the final request or snapshot the current files before starting it. Otherwise, the preview can show one pair while the request sends another.
Accessibility details matter
Each slot should have its own label, keyboard-accessible remove button, and a live error region. Keep the native file input available to assistive technology even if the visible interface uses a styled drop zone. Do not communicate success or failure by color alone.
Test the awkward paths
The most useful tests are rarely the happy path:
- only the first slot is filled;
- the same file is selected twice;
- an accepted file is replaced by an invalid one;
- a user removes a file while validation is running;
- the request fails and both previews remain available;
- object URLs are revoked after replacement and cleanup.
A real example of this interaction is the two-photo rap video workflow. The visible experience is simple, but the implementation benefits from treating the pair as a small state machine rather than two unrelated file inputs.
The broader lesson is simple: when a product has a fixed input contract, model that contract directly. Clear slot state, early validation, cleanup, and explicit submission rules eliminate most of the edge cases before they reach the server.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.