Disclaimer 1: This article was supposed to be a book, but ChatGPT quota is against it. If you didn’t get the joke, it’s probably the API latency.
Disclaimer 2: Johan Guterman has fully reviewed, edited and approved this piece. Any copyright breach claims are preposterous, but will be tolerated.
Our application development journey started with a bit of a lazy for Johan and my growing fascination for automation. We both started using .
Over time, our enthusiasm grew, and we found ourselves wanting something more tailored. Say, I watch some series or a movie and I enjoy the picture so much that I would like to take my photos with that same color effect that I see on the screen. More importantly, I want to get the result before post-production in the dedicated software.
Replicating the cinematic quality of film or digital post-processing in a JPEG recipe is an ambitious challenge, often requiring extensive effort. It requires certain experience, and for beginners it’s a lot of work even in the specialized apps like (although it gets easier each year with the
💭 Hm, so we need a form, collection of cameras parameters and some basic communication with AI.
“That it?” — I ask.
“Well, It’d be also great to share the link with other photo enthusiasts! So when they get to the page…”
“Yup, let’s pause here for a second!”
💭 We’ll also need a database to store and fetch recipe params and an image storage, apparently.
“Okay, certainly doable. You were saying?”
“Right, so other people could remix existing recipes!”
“Remix? How does that work?”
“You just click a button…”
“…a-ha and it just works, sure”
💭 Raising AI model temperature and tweaking some other params with the same input data should work.
“Aa-and a nice landing page, of course!”
“Naturally”
“In the Fuji-retro-style, with grids and splashy gradients…”
“We’ll see”
“Oh, and I can write testimonials!”
“Yep… wait, is this all about promoting your Instagram?!”
“Alter… komm schon”
The Implementation — Snap-Snap
Hey there,ChatGPT
To kick off, let’s prototype using a tool we already have at our disposal:
If we dig a bit deeper and extrapolate this to the application UX the process is not exactly that smooth. In our app we’ll be making a single request and getting a single response.
What we’re looking forward to is something like this:
dialogue to using
It’s not an exaggeration to note that the AI instructions is the most edited file in the project. Fine-tuning of the text part for request took somewhat the same time as for creating the whole UI for the app.
The full instruction takes around 200 lines and about 7000 characters, resulting in roughly 1800 tokens for every request. Adding images tokens (I admit, we should’ve started with images) to that number will give us around 3000–4000 tokens per request on average. Output tokens volume is somewhat negligible, we limit it to around 250 per recipe.
“Is that much?” — Johan chimes in to check the results.
“Well, it depends. For personal occasional usage it’s pretty much affordable, but for our free app we don’t want to get a hefty tokens bill out of a sudden”
“How hefty?”
“Any unpredictable credit is a risk, especially for a public app”
“We can add a «buy me a coffee» button…”
“Or we can try recently released . But we’ll need another round of testing.”
“I guess I’ll go take some photos then. And a coffee maybe.”
Pivot #1
Looking for alternative to Open AI’s token costs and release of Gemini AI aligned perfectly and resulted in the first turning point in development. In retrospect we can compare not only the price but also the output quality, which may be especially handy for other photo/development enthusiasts.
So how and what are we actually comparing?
Same conditions as before, text request is around 1800 tokens plus two high-res images on average interaction. However for different operators images cost varies.
🤖 Open AI
- model: gpt-4-vision-preview (currently several cheaper models are available, i.e. gpt-4o)
: $10/1M tokens ($2.5/1M for modern models)- user session cost: $0.12 💰 ($0.03 for modern models)
🤖 Gemini AI
- model: gemini-pro-vision, : “During processing, the Gemini API considers images to be a fixed size, so they consume a fixed number of tokens (currently 258 tokens), regardless of their display or file size.”
- tokens per user session: (1800 + 2 images x 258) x 3 retries = 6900
- free tokens — 1M tokens per minute and 1500 requests per day
seems to be more comprehensive and practically better organized. On the other hand, Gemini AI when the service is paid, it’s crucial to setup a safety net. Both have
The next step was implementing services to save and share recipes effectively. Practically speaking, we need a simple yet efficient database and an image storage.
The backbone of our app is built on , a Redis-based solution that fits our needs just great. The implementation is not so different from the docs, but just for posterity, let’s have a look at writing and retrieving saved data.
was the very first service that we utilized, and it lasted around 2 days of not that really intense testing. And of course it’s not the question of reliability or anything else. We simply getting back again at the question of
At first glance, the 250MB quota seemed sufficient, right?
emerged as the ideal alternative, despite its more involved setup process, including account and project setting. And if that’s not enough fun, you’ll also get a change to know the one and only anyway…
You never asked, but I don’t want to be left alone with this.
Here’s how we can upload the images using @aws-sdk (extra code skipped for brevity). The crucial piece is theid, which we figured out previously:
CODEimport { S3Client, S3ClientConfigType, PutObjectCommand } from '@aws-sdk/client-s3';
// proprietary util
import { stringToBlob } from '@/utils/stringToBlob';
const s3Config: S3ClientConfigType = {
region: S3_REGION,
credentials: {
accessKeyId: process.env.AWS_API_KEY as string,
secretAccessKey: process.env.AWS_API_SECRET as string,
},
};
const s3Client = new S3Client(s3Config);
const blobPromises = imageData.map(async ({ url }, idx) => {
const pathname = `${id}/${idx}`;
const [blob, contentType] = stringToBlob(url);
const putObject = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: pathname,
Body: blob,
ContentType: contentType,
});
return await s3Client.send(putObject);
});
await Promise.all(blobPromises);
Similarly, to retrieve the uploaded images we only need the
id:
CODEimport { S3Client, S3ClientConfigType, ListObjectsV2Command } from '@aws-sdk/client-s3';
const s3Config: S3ClientConfigType = {
region: S3_REGION,
credentials: {
accessKeyId: process.env.AWS_API_KEY as string,
secretAccessKey: process.env.AWS_API_SECRET as string,
},
};
const s3Client = new S3Client(s3Config);
const listObjects = new ListObjectsV2Command({
Bucket: S3_BUCKET,
Prefix: `${id}/`,
MaxKeys: 5, // Fuji X Studio images limit
});
const { Contents } = await s3Client.send(listObjects);
if (Contents?.length) {
imageUrls = Contents.filter(({ Size }) => !!Size).map(
({ Key }) => `https://${S3_BUCKET}.s3.${S3_REGION}.amazonaws.com/${Key}`
);
}
Now our app starts to get closer to it’s final state:
»"
“Yeah, I don’t really watch cartoons”
“Believe me, you do in a parallel universe, Johan”
The Implementation — Post Processing
At this point, the app is functioning, but somewhat not complete. Let’s have a brief look on couple of other nice-to-have features.
Kill-Switch
A simple kill-switch mechanism is needed to temporarily disable the service while leaving essential pages and metadata accessible. This can be handy for several reasons, especially when one of our crucial services is no longer available.
The maintenance kill-switch in our case is implemented via an env variable that disables main features on the client and prevents unsolicited requests on the server.
CODEconst isMaintenance = process.env.FLAG_MAINTENANCE === SETTING_TRUE;
if (isMaintenance) {
return new Response(
'Sorry, the service is not available at the moment',
{
status: 429,
},
);
}
Server handling is the last resort in this case, so we coupled it with the rate limit checks, hence the
Reporting
After a while Johan comes up with a new idea — the feedback loop for the recipes — to gather user insights and refine AI outputs. It makes a lot of sense, since we need more field data in order to improve our instructions. Furthermore, it’s sets up the stage for the new features, something like recipes library.
Fortunately, it’s also additive to our codebase.
Reports page will be located in the new admin area of the app. The most important piece of information is the recipe output. It needs to be easily accessible along with the input data and report information. We can browse reports and mark them as read or completed.
. The only tricky task to solve is how to identify and store the reports.
Since we don’t really want to use another DB or overhaul existing code, the simple solution is to create a new KV record based on the recipe ID. It naturally can be scaled further to accommodate more than 1 report per recipe, yet for our practical debugging needs it’s more than sufficient.
, which later became its theming backbone.
🚀
as usual.
🚀 , specifically the relevant package. It’s essentially the one used in Fuji X Studio for theming.
Also, big kudos to is entirely free — no ads, no signups, and no data sales. It’s designed for hobbyists and developers as an educational and creative tool. And it’s a fun project to work on.
🚀 and powered by and , and for images storage, for database. While additional services contribute to the project, this lineup offers a clear overview of the core stack.
NextJS 15? Chakra3?
In late October 2024, both Next.js and Chakra released major updates.
Coincidentally, just a couple of days before final polishing and launching the app, posing a precarious challenge of yet another tech update. After giving it a b/g/rief overview and estimating the migration effort it was decided to stay somewhat behind the bleeding edge for a while. It’s totally okay. Let it be.
Frame It
Fuji X Studio started as a spark of curiosity, and evolved through the proof of concept, researches, pivots, and finally emerged as a tool that bridges AI innovation with the practical experimental photography.
We certainly learned a lot through this journey, which resulted in this very article. Of course some parts were skipped otherwise the story would never end. Same goes for the app development, and can apply to the photo editing as well. You need to know when done is done. It’s the essential skill that gets honed with the every next project.
If you got some inspiration, had some fun, found something useful or new, please support the article via sharing or 💜. Thank you for reading!
“Hey, Johan, do you know what are going to do today?”
“Na ja, try to take over the world?”
“Wha…? Jeez. Let’s go take some photos first, shall we?”
↗ Original-Artikel auf dev.to lesenVollständiger Original-ArtikelDen kompletten Beitrag mit allen Details direkt auf dev.to lesen.
SOCIAL SHARE CARD GENERATOR