Decision 01
Describing what a model can do, instead of which model it is
Problem
Decision
Trade-off
Result
Case studyAI Product Engineering · Personal Project2026
A workspace for creating video with generative AI: refining a prompt, choosing and configuring a model, tracking the generation, and reviewing the result. Built end to end by me.
Prompt
A slow aerial pass over a coastline at first light.
Refined
editableAerial tracking shot, low sun, long shadows across wet sand, 24fps, shallow depth of field.
Capabilities resolved per model. The UI never branches on provider.
Configuration
Jobs
Interface recreated for portfolio purposes.
The complete case study in seven parts. The sections below provide the engineering detail behind this summary.
Problem
Context
Constraints
My role
Approach
Solution
Outcome
Open Fern is a workspace for creating video with generative AI. It covers the whole loop: turning an idea into a prompt, choosing and configuring a model, tracking the generation, reviewing the output, and keeping the assets and brand context that make the next attempt better than the last.
A single video pulls on several kinds of model at once (text for scripting and prompt refinement, voice for narration, video for the footage) and then has to assemble the results into something coherent. Current integrations span OpenAI, Claude and Gemini for text, ElevenLabs for voice, Kling for video, and Remotion for composition and editing.
01
Choose what to create
Scene, narration, full sequence
02
Select and configure a model
Only the controls it supports
03
Write and refine the prompt
Refinement is visible and editable
04
Generate
Accepted as a job, not awaited as a request
05
Track the generation
Real stages, survives a closed tab
06
Review the output
An intermediate result, not an asset
07
Refine or regenerate
Per scene, not the whole project
08
Save to assets
Only what the creator chooses to keep
Calling a generation API is a POST request. Building a product around generation is a different problem, and most of it has nothing to do with the model.
A creator does not experience a request and a response. They experience an intention, a configuration they half understand, a wait of unknown length, a result they have to judge, and a decision about whether to keep it or try again. Every one of those steps is interface work.
So the product has to hold model differences, configuration, asynchronous jobs, progress, failure, retries, several competing generations, output history, asset organisation, and, unavoidably, human judgement about whether the thing that came back is any good.
How do you hide the complexity of several AI systems without hiding the control people actually need?
What should exist, and who decides?
How should it behave?
How is it built?
How does the model reach the product?
OpenAI, Claude, and Gemini handle text; ElevenLabs handles voice; Kling handles video; and Remotion handles composition. The contribution covered the product around these models, not their training, fine-tuning, or hosting.
Providers disagree about inputs, parameters, capabilities and output formats. Two video models will not accept the same request, and the differences are not cosmetic.
The interface could not branch per provider without becoming several interfaces sharing a stylesheet.
Work takes seconds to minutes, and is accepted long before it is finished. Request → spinner → response is not a model of what is happening.
Generation had to be a stored job with observable stages, not a loading boolean.
Providers fail, jobs time out, and a generation can succeed technically while producing something unusable.
Retry and regeneration are part of the main path, not an error branch bolted on afterwards.
Exposing every model parameter makes the product powerful and intimidating at the same time.
Common controls surface by default; model-specific ones are revealed progressively.
Weeks later it should still be possible to answer what prompt, what refinement, what settings and what model produced a given output.
Generation history is stored as a first-class record, separate from the assets a creator chose to keep.
When someone starts a generation, the product translates the chosen model and its settings into a request that particular provider understands, while the interface keeps tracking one consistent job. That is what lets the experience stay the same regardless of which model was picked.
Creator
The person making something
Product UI
CreationJob stateAsset state
AI / API layer
Model descriptionProvider adapters
Providers
OpenAIClaudeGeminiElevenLabsKling
Generated output
Returned, stored, unreviewed
Review
A person decides
Asset library
Only what was kept
Each of these solved a real problem and cost something in return. The trade-off is stated alongside the decision, because a decision without one was not really a decision.
Decision 01
Problem
Decision
Trade-off
Result
Decision 02
Problem
Decision
Trade-off
Result
Decision 03
Problem
Decision
Trade-off
Result
Decision 04
Problem
Decision
Trade-off
Result
Conventional software is close enough to deterministic that the interface can promise an outcome: an input goes in, a known operation runs, a predictable result comes back. Error states are the exception.
Generative systems break that promise. The same prompt produces different output twice, quality varies, and 'succeeded' does not mean 'good'. The interface has to carry that difference honestly instead of pretending it away.
The boundaries that matter here are not component boundaries. They are about which kind of state owns which fact, because generation work outlives the components that started it.
Dialogs, selection, local interaction, what is expanded
Ephemeral and component-local. Never persisted, since it would be noise on reload.
The multi-step creation flow and its configuration
Survives navigation within a project, so a half-configured generation is not lost by clicking away.
Generation lifecycle and job status
The one boundary that had to be explicit. A job is a server fact, not a component's loading flag.
Models, jobs, assets, generation history
Cached and revalidated. The interface reads it; it never becomes the source of truth.
A generation is accepted, then happens somewhere else, then either produces something or doesn't. Modelling that as a boolean loses every state that matters to the person waiting.
Lifecycle: idle, then queued, then processing, which then ends in either completed or failed.
Model-specific controls give advanced users real power and give everyone else a wall of settings they cannot evaluate.
ChoiceExpose common controls by default and reveal model-specific options progressively, driven by what the model description says it supports.
A spinner is a few lines of code and communicates almost nothing about work that takes minutes and can fail halfway.
ChoiceModel the generation lifecycle explicitly and show the stage. More state to manage, and a far more honest product.
A fully generic interface is easier to reason about, and quietly erases the specific things that make one model worth choosing over another.
ChoiceNormalise the shared concepts and let the model description carry genuinely provider-specific capability. The right boundary remains an open product trade-off.
Open Fern is live and in active development. The part I would point at is the model description system: adding a provider is a description and an adapter, and the creation interface does not change.
As a portfolio piece its value is the reasoning rather than a metric. It is where I get to make the architectural decisions unilaterally, and be judged on them.
A model can be genuinely impressive and still useless until someone designs the workflow, the waiting, the failure and the review around it. Most of the work in this project was that gap.
In a generative tool, waiting, retrying and failing are the main path. Treating them as exceptional produced an interface that felt broken during ordinary use.
The first model description system anticipated providers that were not yet integrated, and the first two real integrations immediately disagreed with it. A better approach would start with two concrete integrations and extract the shared shape from actual requirements.
The goal was never maximum automation. Keeping the creator in the loop is what makes the output usable, and designing review as a first-class step rather than a safety net changed the structure of the product.
05 / Contact
Available for senior frontend, product engineering, and AI product engineering opportunities.
Location
Bangalore, India · Remote