Inspiration
I enjoy taking photos but I don't have anyone to give me feedback, especially on technical points. I thought an agent could do this well, because a photograph arrives with its own evidence. The EXIF data says exactly what the camera was doing: the aperture, the shutter speed, the focal length, whether the flash fired. An agent that can read that and look at the pixels and render a version of what it's suggesting can say something specific and checkable — "you shot this at f/1.8 and the eyes fell outside the focal plane, here's what f/4 would have given you" — instead of vague praise. And the more of my work it sees, the more it should be able to tell me about myself rather than about photography in general.
What it does
Camrade automatically gives me feedback on photos I upload and generates examples of improvements. It also learns my photography style over time and give me things I can work on.
- It reads the frame. Upload a photo and it analyses it without being asked — measured capture settings plus what it can actually see, with every observation grounded in one or the other.
- Talk to it and it talks back. Ask for "more separation behind her" and it renders the edit while you're still talking, appending it to a non-destructive version chain so undo is free and branching costs nothing.
- Camrade learns. Every edit asked for is classified onto an axis such as warmth, contrast, blacks, crop and the profile is counted from those decisions, not summarized. Confidence is arithmetic: how much evidence exists multiplied by how consistent it is, so six contradictory edits don't read as confident.
- Camrade tells you what it doesn't know. It can see the shape of your whole library, so it can say "you have no evidence at all on how you handle backlight — go shoot five frames into the sun and show me."
How we built it
Three Gemini models were used: (1) gemini-3.5-flash makes every judgement. (2) gemini-3-pro-image (Nano Banana Pro) does every render. (3) gemini-live-2.5-flash handles voice and it is strictly a transport. It cannot see the photograph. No image is ever sent to it; it has to call analyze_photo and relay what the reasoning model found. That split is the load-bearing architectural decision in the whole project.
Voice tools dispatch over HTTP to the app's own actions, so speaking and typing share exactly one implementation of "apply an edit" rather than two that drift and the socket can do nothing its owner couldn't do through the interface.
Stack: TypeScript, React 19 and React Router 7 with SSR, Express 5 hosting both the app and the /live WebSocket on one port, Tailwind 4. exifr for EXIF and for pulling the embedded preview out of TIFF-based RAW files.
Google Cloud: Vertex AI for all inference (Application Default Credentials, there is no API key anywhere in the repo or the container), Cloud Storage for photo bytes, Firestore for photos, versions, critiques and profiles, Secret Manager for credentials, Cloud Run to serve it.
Infrastructure as code. All of it is Terraform including the Cloud Monitoring dashboard, log-based metrics, alert policies and billing budget. Deploys run from GitHub Actions using Workload Identity Federation, so no service-account key exists to leak.
Challenges we ran into
An early build told the voice model to "say what you see" without giving it eyes, and it confidently described a bottle that wasn't in the frame. That single bug produced the architecture: the live model now cannot see, must call a tool, and the system instruction says plainly that claiming to see something without calling that tool is the worst thing it can do because the photographer will believe it.
Multiple hours were spent debugging code that was never running. I read the SDK internals twice and swapped API calls based on careful reasoning and none of it worked, because Vite's hot reload covers the app bundle but not the raw Node files. The browser half of every fix hot-reloaded correctly while the server half kept running four-hour-old code. The evidence looked contradictory until I checked the process start time against the file modification time.
npm run devnow watches those files explicitly, and the failure is documented at the top ofserver.mjs.The voice agent fetched my profile without an auth header. That redirects to the sign-in page, which answers 200 with HTML, so
res.okwas true, the JSON parse threw, and a catch commented "no profile is a normal cold start" swallowed it. The agent had never once received the profile. Conflating "the lookup broke" with "there's nothing there yet" is what made it invisible.The "one render at a time per user" cap was a Set in process memory. Cloud Run runs up to three instances, each with its own memory, so a user hitting different instances got three concurrent renders. It's now a Firestore lease claimed in a transaction, with a TTL so a killed instance doesn't lock anyone out.
Cloud Run refuses request bodies over 32MB, which a 45MP RAW file comfortably exceeds and it rejects them at the edge, so none of our error messages ever appeared. Uploads now go straight to Cloud Storage on a resumable session and never pass through the app at all.
Accomplishments that we're proud of
The personalization is falsifiable, not asserted. There's a test that shows the same photograph to the model twice under two opposite taste profiles and asserts it gets opposite advice back: "warm up the tones / lift the black point" versus "crush the shadow tones / cool down the colour balance". Same photo, same model, opposite answers, purely because the profile differed. Anyone can claim their agent personalizes; this one can be checked.
Every route is scoped to its owner so ownership is a compile-time argument rather than a convention, OAuth with PKCE and single-use state, a session secret that refuses to fall back to the development value in production, per-user spend limits, and structured logging feeding a real monitoring dashboard.
Every edit is non-destructive. The parent's bytes are read, never written. Undo is a pointer move, branching is free, and the version history is what makes "here's what changed after those decisions" a checkable claim rather than the model's recollection.
What we learned
Probe, don't trust. Two expensive bugs came from believing documentation about which models exist and where they're served. Fifteen minutes of probing the actual project would have a lot of time to know where models are actually available.
Know where your reload boundary is. Hot reload covering some of your code is more dangerous than none, because half your changes take effect and the evidence stops making sense.
"No data" and "working fine" look identical on a dashboard. Every metric filter had to be checked against a log line the app genuinely emits, which caught one metric that would have counted nothing forever.
What's next for Camrade
I think Camrade could work great as a mobile app that gives you feedback before you take a photo so you can point the camera at something and it can give you real time feedback on the composition, lighting, angle, and so forth so you can make changes before and not later.
Some other ideas:
- Assign the exercises it already writes. The profile computes specific experiments ("you've shot nothing under f/4 in three weeks") and the agent currently only mentions them if the moment fits. It could message or email them to the user.
- Broader format support. HEIC is currently rejected rather than half-supported, which matters a lot for phone photography.
- Sharing. Right now a library belongs to one photographer with no way to hand it to a mentor or a class.
Built With
- artifact-registry
- cloud-build
- cloud-logging
- cloud-monitoring
- cloud-run
- cloud-storage
- exifr
- express.js
- firestore
- gemini3.5flash
- gemini3proimage
- geminiliveapi
- github
- google-gen-ai-sdk
- google-oauth
- node.js
- react
- react-router
- secret-manager
- tailwind
- terraform
- typescript
- vertexai
- ws
Log in or sign up for Devpost to join the conversation.