1Competent and anonymous
A model with the right product in frame still has no idea who it is working for. Every brand on the same engine gets the same lighting, the same grade, the same composition. The output is competent and interchangeable.
For a brand, interchangeable is its own failure. The question is narrow and answerable: out of every good image in the world, which ones are this brand's? That is not a prompting problem. It is a memory problem, so it got built as one.
2What one entry is
One entry is one look, stored as a few lines of text. The first line records that the look was liked or saved and carries the brief it came from. Then an aesthetic line, which is the image read back in prose. Then the lane it belongs to, a link to the image it was learned from, and when it happened.
The last two matter more than they look. The link is what lets a look be shown back to someone later, and what lets one entry be recognised as a re-save of a look already on file rather than a second copy of it. The date is written into the text itself, not kept beside it, so recency travels with the entry instead of needing a separate sort.
An entry records an outcome, not an intent. The brief says what someone asked for; the read says what actually came out.
Entries are stored and recalled in full. There is no length trim and no field cap, because the whole value of the read is the detail in it, and a trimmed read is a thinner brief downstream. The only bounds in the system are timeouts and fan-out limits, which are operational, not editorial.
3Why the read is prose, not a schema
The first version of the reader asked for a fourteen-slot JSON object: palette, light, mood, composition, and so on. It is the obvious design, and structured output is the right default for anything you intend to query.
It returned short hedged values that then had to be flattened back into a sentence before any image model could use them, and the flattening threw away most of what made the description worth having. Asking plainly for one paragraph you could paste straight into an image model rendered measurably better.
One field is still parsed: a single line naming what the subject is. That one is used for matching rather than for rendering, which is exactly the case structure is for.
The one guard rail
Without a constraint on the shape of the reply, a vision model answers like a chat assistant. Measured on one reference, the reply came back as a 4,157-character menu: Option 1, Option 2, Option 3, a bullet summary, and Midjourney flags. That is not a prompt. Nothing downstream can paste it into an image model.
So the ask ends with an explicit instruction: one flowing paragraph, no options, no preamble, no headings, no markdown, no commentary. It constrains the shape of the reply and never the content, which is the difference between this and the schema it replaced.
Different readers also answer this differently, and the choice is measured rather than assumed: one model returns the pasteable paragraph, another returns the menu on the same reference. Swapping the reader is a decision that needs re-measuring, not a version bump.
Every read is bounded and fails open. A read runs a few seconds warm, but a full token budget plus a cold start can pass a minute, so the bound is set well clear of that rather than tight enough to turn a slow read into a false miss. On any error it returns nothing at all, and the caller carries on: a read miss must never be able to fail a like or block a render.
4Four ways it gets taught
Picking, at the start. Onboarding offers a set of aesthetics, and each pick is written in as a plain fact. Cheap, coarse, and enough that the first shot is not made blind.
Liking, in two phases. A like writes immediately as text, and a background job then reads the actual image and writes the enriched entry. Both writes carry the same key, derived from the shot, and the store upserts on that key, so the two collapse into one entry rather than accumulating as two. Deleting the shot can then drop its taste by that same key.
The split exists because the read takes on the order of fifteen seconds. Inline, that is a like button that spins. Off the request path, the interface stays honest and the entry still ends up enriched.[3] If the read fails, the entry keeps the image link and loses only the description.
Bulk seeding. Up to thirty images at a time, from an upload or a scan of an existing site, each read and recorded the same way. A brand with ten years of campaigns should not start from zero.
Disliking. A dislike writes an explicit avoid note and then searches for the closest earlier positive entry it contradicts, forgetting that one if the two are genuinely the same look. The similarity floor is deliberately high and it only ever forgets the single closest match, because one dislike must not be able to clear a container.
This path exists because a store that only accumulates ossifies. Without it, taste drifts toward whatever was liked most a year ago and there is no way back.
5Where it lives
Taste is stored per scope: one container for a person, one for the workspace default that any member can build, and a curated house baseline underneath both. A write names its target explicitly, and the tag for that target is computed in one place.
That single place is load-bearing in an unusual way. The agent reads from these containers by tag. If the tag the writer computes and the tag the reader computes ever disagree, nothing errors: the write lands, the read comes back empty, and the feature is silently off. It is the kind of bug that survives a test suite and a code review, so the shared helper carries a comment saying the two must match byte for byte.
6Recall: list for coverage, rank for order
Recall started as a relevance search over the container, which is the obvious implementation and was wrong in a way that took a while to see. A bulk-seeded look scores weakly against a query like “favourite looks”, so search dropped seeded entries entirely. Recall read empty for users whose saved grid was visibly full.
The list gives coverage. The search gives order. Using the search for both silently loses whatever it ranks low.
So recall lists the container newest-first to get everything, then runs a vector search to decide the order, then merges: matched entries lead in similarity order and the rest keep their recency position as backfill. Nothing is dropped, and the looks that actually match the brief lead.
The ranking step fails open. No query, one entry, or a search error all fall back to recency order, because a worse order is a worse shot and a thrown error is no shot.
Two limits worth naming
The search endpoint caps its result count at one hundred. The document list has no such cap, and the pool size we list with is larger. Passing the list's number to the search got the request rejected, and because ranking fails open, the failure looked like nothing: recall simply returned recency order with no sign that the ranking had not run. Two constants now, one per endpoint, each with the reason written next to it.
Recall hands back twelve looks. An uncapped library made one result 76KB, which is a meaningful share of a context window spent on taste alone. Ordering runs before the cut, so twelve is the best twelve rather than an arbitrary slice, and the caller is told how many were left behind so it can ask for more instead of treating a page as the whole library. The settings grid stays uncapped, because that one is for a human scrolling.
| Bound | Value | Why this number |
|---|---|---|
| Looks returned per recall | 12 | An uncapped library made one recall result 76KB. Ordering runs first, so this is the best twelve, not a slice. |
| Entries listed to build the pool | 500 | Coverage. The list is what guarantees nothing on file is invisible to recall. |
| Search results used for ranking | 100 | The search endpoint's own cap. Sending the list's number instead got the request rejected, and ranking silently fell back to recency. |
| Images per bulk seed | 30 | One run a person watches finish. The settings panel refuses to queue past it rather than letting the backend drop the tail. |
| Taste images given to the director | 4 | Enough to read drape and grade that prose flattens, bounded so vision cost per turn stays predictable. |
| Lanes per workspace | 24 | A picker, not a taxonomy. Past this, choosing a lane is harder than describing the shot. |
| Entries forgotten per dislike | 1 | The single closest contradicted look, above a high similarity floor. One dislike must not be able to clear a container. |
7A lane narrows, it does not hint
A brand shoots more than one kind of thing. Editorial taste and packshot taste averaged together serve neither, so entries carry a lane and a shot can ask for one.
Lanes were first implemented as a sort: matching entries moved to the front and everything else followed. That makes a lane a hint. Asking for a specific night-party lane with four looks in it returned all twelve looks in the container, eight of them from an unrelated lane, and the shoot was grounded in the wrong reference.
A lane is an explicit narrowing, so it now narrows. An empty lane still falls through to the tier's general taste, because losing the lane must not mean losing grounding entirely.
But a silent fallback is the same substitution bug wearing a different hat.
When a lane is asked for and nothing on file carries it, recall says so explicitly, and the agent is told in words: these are their other taste, do not build from them, do not spend, ask which saved looks belong to this lane. Degrading quietly reads as the named lane and gets shot from, which costs a real render.
One function computes that miss for every return path. Written inline per-return it was set on some paths and forgotten on others, which is exactly the silent substitution it exists to report.
8Recreate, never copy
The director sees the taste images. The engine does not.
Those are two separate decisions and both are deliberate. The agent is given up to four of the top taste images as real vision input, because a written read flattens drape, fabric, layering and grade, and a model that can look at the reference writes a truer prompt than one working from the paragraph alone.
The engine gets the words. Every taste image url is dropped from the reference set by the function that assembles a render request, on every spending tool, before the request exists.
The detail that matters for anyone building the same thing: taste images are re-hosted to our own storage, so they pass the provenance check that exists to reject invented and expiring urls. They are dropped anyway, by an explicit check placed ahead of it. The guard that keeps taste out is separate from the guard that keeps junk out, because they are answering different questions.
A prompt instruction not to copy a reference is a request. The reference either reaches the engine or it does not, and that is decided by the code that builds the payload.
The alternative is easier and lands closer on the first attempt. It also means the output begins life as a derivative of one specific image somebody else made, which for a system whose purpose is producing a brand's own assets is the wrong default.
When taste stands down
If someone attaches a reference in the conversation, the taste images are not injected at all. An attachment overrides saved taste for that shot, and two sets of images competing as vision means the model grounds in a saved look instead of the photo the person just pointed at. The rule is not politeness about precedence; it is that the vision input has to have one subject.
9Taste and Wings together
Taste and the render pipeline solve different halves, and each one alone produces a specific, recognisable disappointment.
Taste without Wings gives you the right look holding an invented product. Wings without taste gives you a real product in a competent, anonymous frame. Neither is a usable campaign asset.
Because the engine never sees the taste image, the prompt has to be complete on its own. So the agent is told to transcribe the read in full rather than compress it to a phrase. A short brief like “layered bohemian crop” gives the engine almost nothing and it drifts; the words are the shot.
That is also why a multi-shot set is one call per look rather than one prompt with the count raised. Raising the count samples the same prompt repeatedly and every frame comes back the same, which is the opposite of a shot list.
The rendering half, and the bug that decided its shape, is a separate note.
10What the settings panel had to learn
Taste is editable in settings: drop images, name a lane, learn them, and prune the library afterwards. Most of what is interesting there is a correction.
The progress bar counts what landed. The job increments its attempted counter in a finally block, so it counts images tried, including the ones the reader could not read. The bar showed “9 / 9 learned” for a run where four images were dropped, and the grid underneath then had five. It now counts what actually landed, names the shortfall, and says the useful next thing: try uploading those again.
Forget-all is scoped to your own library. The workspace library is shared by the team, so one member cannot clear it in a click. They prune it look by look.
A selection is intersected with what is on screen. A selected look can leave the grid by a filter change or a removal. Deleting ids that are no longer visible is a wipe nobody watched themselves choose.
A deleted lane still shows its slug. A look filed under a lane that has since been removed falls back to the raw name rather than rendering a blank badge, because an entry that displays as nothing looks like data loss.
Uploads run one at a time. A settings action has no reason to open thirty parallel requests, and bounding concurrency to one is less code than bounding it to four.
11What this does not fix
Recreation converges slower. Withholding the reference image means the first attempt lands further from the target than a copy would. The trade is deliberate and it is paid on every shot.
It is thinnest exactly when it is being judged. A picked aesthetic is a label. What the system gets good at is the specific thing a brand keeps choosing, and that needs real use, so taste is weakest while someone is still deciding whether to trust it.
A read is an opinion. The description of a look is produced by a model looking at it, and it can be shallow or wrong. A wrong entry is not visibly wrong from the outside, and the only correction path is someone noticing and disliking it.
The lane filter in settings only sees loaded pages. The grid pages in, so filtering by a lane whose looks are further down reads as empty until more is loaded. Known, marked in the code, and left alone until a library is big enough for it to matter.
Almost every path fails open, including this one. A read miss, a search error and a store timeout all degrade quietly by design, so taste getting quietly worse looks exactly like taste working. The counters in the settings panel are the only place a shortfall is visible.
12Components
| Part | Runs on |
|---|---|
| Taste store and semantic recall | Our own Postgres, with pgvector |
| Reading an image back into words | fal, routing to the reader model |
| Background reads, off the request path | Inngest |
| Base render, real faces and real light | Higgsfield |
Taste is stored in our own Postgres database and searched with pgvector. That is what makes recall semantic rather than a list: what comes back is what is close to the brief being written.[1] The reads run on fal, which routes to the model that does the looking.[2]
What is ours is the entry format, the four write paths, the list-then-rank recall, the lane semantics, and the drop in section 8.
Questions
- Does Icarus copy the reference images I save?
- No. A saved look is read back into words, and only those words reach the image engine. The image itself is removed from the request by the code that assembles it, on every path that spends your balance, even though the file is hosted on our own storage and would otherwise pass every check. The engine recreates the look from the description; it never receives the pixels.
- How does Icarus learn my brand's style?
- Four ways. You pick aesthetics during onboarding, you like or save shots as you work, you can bulk upload up to thirty reference images at once or scan an existing site, and you can dislike a look to have it forgotten. Every one of those writes an entry describing what the image actually was, not the brief that produced it.
- Where is taste stored, and who can see it?
- In three separate scopes: your own, your workspace's shared default, and a house baseline underneath both. Recall uses the first one with anything on file. Your personal library is yours to clear in one action; the shared workspace one cannot be cleared by a single member, only pruned look by look.
- What happens if I ask for a shoot type I have no saved looks for?
- Icarus says so rather than quietly using your other taste. It shows you a few saved looks and asks which belong to that shoot type, and it does not spend your balance on a render until you answer. Silently substituting nearby taste is what produces a shot that is confidently the wrong thing.
- Do more reference images make the result better?
- For taste, more saved looks help, because recall ranks them and uses the closest ones. For pinning a specific product or face it is the opposite: one photograph per subject. Multiple angles pull the subject in different directions and it drifts across a set.
References
- [1]pgvector the vector search in our own Postgres that taste recall is built on.
- [2]fal runs the image and video reads, and the rest of the model calls.
- [3]Inngest durable background jobs; the enrichment read runs as one.
- [4]Vercel AI SDK, issue 8180: tool-result image parts are dropped why taste images are sent as message parts rather than returned from the tool.
- [5]Schema.org TechArticle the structured description this page publishes about itself.
