When generating images with modern AI models, one of the most frustrating limitations is character and composition drift. You enter a prompt, get an interesting portrait, change one word to alter the outfit or background, and the face completely changes.
To solve this, I designed and built Photo2Photo AI — a browser-based image-to-image studio focused specifically on consistent character rendering, sketch-to-photo conversion, and high-fidelity photo restyling without losing the subject's identity.
In this article, I want to walk through the system architecture, the dual-tier AI inference pipeline, and the engineering decisions behind edge-rendering on Cloudflare Pages.
🏗️ 1. Architecture Overview
Here is the high-level architecture:
[Browser Client] │ (Client-side WebP compression < 4MB) ▼ [Cloudflare Pages Edge Network] ├─ Static Assets & ISR Pages ├─ Turnstile Human Verification └─ Cloudflare D1 (Serverless SQLite Ledger) & Cloudflare R2 (Object Storage) │ ▼ [Multi-Engine Inference Gateway] ├─ Fast Tier (ByteDance Seedream 5.0 Flash) — ~12s rapid turnaround └─ Studio Pro (FLUX.1 Kontext Pro) — High commercial depth & 4K upscaling Why Cloudflare Pages + Next.js 15?
We opted for Next.js 15 compiled via @cloudflare/next-on-pages. This gives us:
- Zero Cold-Start Worldwide: Cloudflare's global edge serves SSR and API routes within 50ms from almost any country.
- Built-in Bot Protection: Cloudflare Turnstile validates human interaction seamlessly without annoying CAPTCHA puzzles.
- Decentralized Storage: Uploaded reference photos land directly in Cloudflare R2 with automatic lifecycle expiration rules (free user outputs purge after 24 hours).
⚡ 2. The Core Challenge: Preserving Identity in Image-to-Image
Traditional text-to-image models hallucinate geometry from scratch. To achieve true identity lock:
- We allow users to provide up to 4 reference photos of the same subject.
- The reference embeddings are mapped to spatial cross-attention layers.
- We support dedicated operational presets for distinct real-world workflows:
- Consistent Character AI Studio: Preserving facial bone structure, skin tone, and hair textures while altering background and lighting.
- Sketch to Image AI Converter: Translating black-and-white architectural wireframes or concept pencil sketches into hyper-realistic scenes.
- Old Photo Restoration AI: Repairing scratches, tears, and dust from vintage portraits while colorizing authentic historical skin tones.
- AI Product Photography Studio: Staging tabletop packaging onto luxury studio pedestals with realistic contact shadows.
- Free Image-to-Image Generator: Low-barrier playground for quick style transfer and creative iterations.
🚀 3. Dual-Tier Inference: Balancing Speed vs. Commercial Quality
Different users have fundamentally different latency and quality budgets:
| Tier | Engine / Model | Latency | Credit Cost | Best For |
|---|---|---|---|---|
| Fast | ByteDance Seedream 5.0 Flash | ~10–12 sec | 1 Credit | Quick drafts, concept validation, social posts |
| Studio Pro | FLUX.1 Kontext Pro | ~20–25 sec | 3 Credits | Commercial advertising, realistic skin textures, 4K export |
By decoupling the pipeline into Fast and Studio Pro, users can iterate rapidly on the Fast tier before spending compute on the Studio Pro engine for final 4K renders.
🔒 4. Client-Side WebP Compression: Never Upload Heavy Payloads
Rather than letting users upload 25MB RAW or uncompressed PNG files to the server (clogging bandwidth and slowing upload queues), we process all incoming images in the browser before dispatching:
// Compress image client-side to WebP (max edge 2048px, quality 0.88) export async function compressImageToWebP(file: File): Promise<Blob> { const img = new Image(); img.src = URL.createObjectURL(file); await img.decode(); const maxEdge = 2048; let { width, height } = img; if (width > maxEdge || height > maxEdge) { if (width > height) { height = Math.round((height * maxEdge) / width); width = maxEdge; } else { width = Math.round((width * maxEdge) / height); height = maxEdge; } } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d")!; ctx.drawImage(img, 0, 0, width, height); return new Promise((resolve) => { canvas.toBlob((blob) => resolve(blob!), "image/webp", 0.88); }); } This simple optimization reduced median upload times from 4.2 seconds down to 320ms.
🎁 5. No Subscription & Free Trial Philosophy
We decided against recurring monthly subscriptions. You can try 3 generations for free without even signing up:
- Check out the live studio: https://photo2photo.io
- Or review our transparent pricing tiers: Photo2Photo Pricing
Feedback and technical critiques are very welcome in the comments!