Skip to content

Repository files navigation

CreatorFlow

Live demo: https://creatorflow-gray.vercel.app · no login, no setup

A creator-collaboration CRM and raw-footage library for Willow & Stone, a fictional US spa chain with five houses including San Jose.

Creators receive a comped VIP visit. In exchange they deliver agreed raw footage. CreatorFlow runs that exchange end to end — and, more importantly, turns what comes back into a library an editor can actually search six months later.

Searchable down to the second: every clip is indexed not just as a file but as a handful of timestamped moments, shelved by spa and by creator, and the library will draft the cut for you — which clips, in what order, and which few seconds of each.

The demo library holds 111 clips across all five houses — real video files, every one described, tagged and moment-indexed by an actual vision pass rather than by hand.


The four deliverables

Where
Working prototype Live: https://creatorflow-gray.vercel.app — or npm install && npm run dev, no API key, no setup. Guided walkthrough below.
Source code This repository, public. Run instructions immediately below.
Thinking document THINKING.md — two pages: problem, solution, where AI is and is not used, decisions, prioritisation, next steps.
AI work history docs/ai-work-history.md — every instruction given, the tool-use record, and the four cases where the model was confidently wrong and what was done about it.

Run it

npm install
npm run dev

Open http://localhost:3000. That is the whole setup: no environment file, no API key, no database to provision. The SQLite database is created and seeded automatically on first run.

AI features work immediately in demo mode, replaying the committed recordings of real model output. To run them live:

cp .env.example .env      # add ANTHROPIC_API_KEY, or OPENAI_COMPAT_API_KEY for a gateway
npm run dev

Either provider works. ANTHROPIC_API_KEY is the path the prompts were written for; OPENAI_COMPAT_API_KEY covers any gateway speaking the OpenAI chat-completions shape, which is what the committed recordings were captured through. They are not equivalent, and the difference turned out to be the interesting part — see Structured output is not a given below.

The sidebar always shows which mode you are in. Other commands:

Command What it does
npm run db:reset Wipe and re-seed the demo data
npm run media:build Re-download sources and rebuild the demo footage (output already committed)
npm run backlog:build Re-cut the library back-catalogue from those sources (output already committed)
npm run backlog:analyze Screen and catalogue the back-catalogue with the real vision pass — needs a key
npx tsx scripts/record-fixtures.ts Capture live model output as the demo responses — needs a key
npm run build Production build

The five-minute walkthrough

Start as the Partnerships Manager (default role).

1. The pipeline knows what is slipping. /pipeline opens on a board with a Needs attention rail at the top: footage five days overdue, a creator who has not replied in a week, a visit in two days. Every row states its exact reason — "Footage is 5 days past the agreed deadline" — and is labelled rules, no AI, because it is date arithmetic and should never be anything else.

2. The AI is willing to say no. Go to /creators and open Marcus Vale — 341,000 followers, a media kit full of superlatives. Click Parse the pasted text, then Assess fit. He scores 25/100, poor: engagement below 1% caps the score at 40 regardless of reach, his audience is in Los Angeles where we have no house, and his rate card rules out trade. Every one of those is quoted back at you as a cited reason, naming the rule it triggered. Compare with Sasha Lindqvist, 22k followers, who scores 94. A scoring feature that likes everybody has told you nothing.

3. The main event. Open /pipeline/COL-1044. Nadia delivered nine files called IMG_4471.MOV, RPReplay_Final1721654433.mp4 and similar. These are real video files — hover any thumbnail and it plays — and nobody knows what is in them. Click Run ingest analysis.

Cards fill in as each clip is catalogued — a real name, a description, tags, a quality grade, a verdict. The brief checklist on the left resolves to 5 of 9 delivered, and the four shortfalls are each a different kind of miss:

  • VID_20260731_184512.mp4 is a perfectly good clip of the pool deck. The review says plainly that it is not the cold plunge the brief asked for.
  • trim.9F3A1C22-8B0E-4D71.MOV is filed where the talking head should be and is an empty corridor — nobody appears in it at all. Right place, wrong shot, and it is the failure most likely to be waved through by a human skimming file names.
  • IMG_4490.MOV is graded weak on two counts: the pre-filter measured it soft before any model saw it, and the review adds that it puts two identifiable guests on camera with no release on file.
  • That same lounge clip is matched to the brief but does not count — 4.1 seconds against a 6-second minimum. The model matched it; the code disqualified it.

The gaps are pre-written for pasting. Draft a reshoot request turns them into an email that names the exact clips.

4. The editor's payoff. Switch role to Video Editor. /library opens on the whole back-catalogue — 111 clips from 8 creators across all five houses, spanning 13 scene types, 17 of them out of licence. That is small for a real library and already far too many to scroll.

Type "vertical sauna steam b-roll, no faces, cleared for paid ads, San Jose". Two things happen, and they are deliberately different things. The parts of the sentence the taxonomy can express become removable filter chips — spa, two scenes, orientation, people in frame, rights, licence validity — with a plain-English restatement of what was searched. Delete a chip and the grid updates, because the AI and the filter rail are the same system. The parts it cannot express — steam, b-roll — become ranking terms, and each card now says why it is there: the field that matched and the words that matched in it, highlighted. Clips that matched nothing are still on the page, below a divider, because a ranking that hides footage you own is worse than one that merely orders it badly.

Hover any card to play it without opening anything. Open one to see its provenance chain back to the signed agreement, and the keyframes the analysis actually read, each seekable in the player. Note the clips badged licence expired in red — the download is blocked in code, with the reason shown rather than the button quietly hidden.

5. Shelving. Every clip is the intersection of one spa and one creator's visit, so the library browses along either axis. Use Group by to shelve it by Spa → creator — how a partnerships manager thinks — or Creator → spa, which is how you check what somebody has delivered across houses. Grouping is a rearrangement of the result set in memory, not a new query, so switching it never changes what you are looking at.

6. Searching inside the clips. Search "the moment the water hits the stones" — a sentence with no taxonomy value in it anywhere. The clip grid still ranks, because it reads the descriptions and the per-frame labels, and the top cards show a timecode instead of a field name: they matched on a moment, so clicking one opens the player at that second rather than at the beginning.

Now switch Show from Clips to Moments. The results stop being files and become seconds: 0:02.2 — Ladle tilting, first water leaving the bowl, then 0:04.3 — Water striking the stones, steam bursts upward. Each says which of your words earned it. The taxonomy will never have a word for "ladle", and should not — this works because the vision pass wrote one line per keyframe it was shown, and the second is the measured time that frame was extracted from.

7. From a shelf to a cut. Click Plan an edit. Choose a target — say a 15-second paid ad, vertical — and draft the plan. Before any model is called, code removes the clips that cannot legally run in paid media and the ones shot the wrong way up; the panel reports exactly how many and why. What comes back is a shot list with in and out points anchored on those same moments ("Cut in on 'hands gripping the rail above the water'"), a runtime totalled in code against your target, and — where the shelf genuinely cannot cover the goal — the brief that would fix it, ready to copy. Which is the same flywheel as the gap analysis, arrived at from the editor's side.

8. The flywheel. Switch to Brand Director. /insights shows the coverage matrix and a cost-per-approved-clip figure. Click Analyse library gaps: the model reads the aggregate matrix and reports that San Jose has sauna footage but none of it is vertical, so none of it can run in paid social — then proposes the brief that fixes it.

9. Then take the API key away. Stop the server, remove ANTHROPIC_API_KEY, restart. Everything still works. The badge changes to AI: demo responses and the review screen runs exactly as before.


What is AI, what is not, and why

The organising principle is AI proposes, code disposes, a human approves.

Built with AI

Feature What the model does Where the human stays
Footage cataloguing (lib/ai/features/ingest.ts) Reads keyframes; produces the file name, description, tags, quality grade and verdict Every tag is editable, every verdict flippable
Moment labelling (same call) Writes one line per frame — what is on screen at that instant The timestamp is the measured extraction time, never the model's guess
Brief coverage (same file, tier 3) Decides which requirement each clip addresses Code decides whether it is satisfied
Library search (features/search.ts) Translates a sentence into filters Filters render as removable chips
Edit planning (features/editplan.ts) Chooses which clips, in what order, and which seconds of each Rights and orientation are filtered before it is called; the runtime is summed in code
Fit assessment (features/fit.ts) Scores a creator against an anchored rubric, citing evidence Advisory only — never gates a stage change
Message drafting (features/messages.ts) Writes the email and enumerates every fact it relies on Those facts become a checklist; nothing sends until each is ticked
Gap analysis (features/gaps.ts) Interprets the coverage matrix and proposes the next brief A person creates the collaboration

Deliberately not AI

This list is a design position, not an omission.

  1. Usage rights. Human-entered structured fields, code-computed expiry, snapshotted onto each asset at publish. A hallucinated "cleared for paid" is a legal problem, not a UX one.
  2. Stage transitions. An allow-list state machine in lib/pipeline.ts with business preconditions — you cannot book a comped visit against an unsigned agreement. A model never mutates CRM state.
  3. All arithmetic. Package values, deliverable counts, duration minimums, deadlines, dashboard aggregates. Trivial in code, and wrong occasionally in a model.
  4. The attention queue. Pure date rules. An LLM would add latency and hallucination surface to information that must be exactly right.
  5. The brief itself. Assembled from structured templates, not generated prose — it is the contract the AI is later judged against, so it has to be exact.
  6. Facet search. Plain Prisma. An editor is never blocked by an API outage.
  7. Publishing and approval. Human clicks. The library's trustworthiness is the product.
  8. Moment ranking. The model writes the labels once, at ingest. Matching a query against them months later is string comparison, so a moment result can always be explained by pointing at the words that matched — and it works with the key removed.
  9. Grouping. Shelving by spa or creator is a Map over the result set. Asking a model how to arrange a list it can already see sorted is theatre.

Considered and cut

  • Embedding search. Filter translation is inspectable and correctable; a vector match that surfaces the wrong clip gives an editor nothing to fix. Worth revisiting past ~10k assets.
  • Full video analysis. Feasible, but slow and expensive, and it does not change the product story. The UI is explicit that analysis reads keyframes, not video — see below.
  • An AI-written morning digest. The underlying signals are date arithmetic. Rebuilt as deterministic queues with reason strings.

Token economics

Analysing video is the easiest way to burn money. Four tiers, each filtering for the next:

Tier What runs Cost per clip
0 Real ffmpeg measurement — duration, resolution, frame rate, audio presence, sharpness, brightness, and scene-change detection to choose the keyframes 0 tokens — kills fragments and unusably soft clips for free
1 Triage on a cheap model, one 512px frame: usable or not ~500 tokens
2 Full vision on the main model, three keyframes chosen by scene change ~3,000 tokens
3 Brief matching and edit ordering — text only, no images, one call per submission ~2,000 tokens total

Tier 0 is not a description, it is src/lib/media.ts running against the real files. Keyframes are selected on scene change rather than at fixed intervals, because three genuinely different moments tell a vision model far more than twelve near-identical ones and cost the same.

The softness measure is worth describing, because the two obvious versions are both wrong and this library proved it. Measuring edge energy and calling a low number "soft" actually measures brightness and contrast — a sunlit pool deck scored sixteen times a dim sauna. Normalising by each frame's contrast range fixes that and introduces a worse confound: it then measures how much fine detail a scene contains, and a person against smooth water scored below a clip that had been deliberately blurred. What works is comparing each clip against a blurred copy of itself — a sharp frame loses most of its edges when blurred again, an already-soft one barely changes — because that ratio cancels out brightness, contrast and subject matter alike. It detects softness, not focus accuracy, so it flags for review rather than deciding.

A nine-clip submission costs roughly 30k tokens end to end. Sending thirty full-resolution frames per clip in one call — the obvious naive version — costs about fifteen times that for a worse answer.

The moment index rides along free, and that is the point of building it this way. The expensive part of looking at a clip is putting its frames in front of a vision model; that has already happened by the time the model is writing the description. Asking it for one extra line per frame adds perhaps sixty output tokens and no input at all, and it is what makes the library searchable by parts rather than only by files. Doing it as a second pass — sending the frames again to a "what happens when" prompt — would cost roughly the whole vision tier a second time for the same answer.

Edit planning is text only: no images, one call, about 3k tokens for a forty-clip shelf. It can afford to be cheap because the looking was done months earlier at ingest. That is the argument for cataloguing at ingest rather than at search time, in one number.

Also implemented: prompt caching on the system prompt (which carries the whole taxonomy and rubric and never changes), persisted results that are never recomputed unless re-run, and downscaled images. The review screen reports actual token usage and estimated cost for each run.


Honesty about what the model can see

The analysis reads three extracted keyframes, not the video. The prompt says so explicitly, tells the model that motion, continuity and audio are unverifiable from stills, and instructs it to use low confidence freely. The review screen carries a permanent banner saying the same thing to the person reviewing.

Duration, resolution and audio presence come from the file itself and are exact. Everything visual is a read on three frames. Building a feature that pretends otherwise would demo better and be worse.

You can check this yourself, which is the point. Every clip in the library is a real video file, and the asset panel shows the keyframes underneath the player with the timestamp each was taken from. Click one and the player jumps there. When the review says "the lens fogs by the third frame", you can go and see whether that is true.


The library is real data, including the boring part

A library of twenty clips can be browsed, so search never has to work. Everything interesting about this product only becomes visible at a size where scrolling stops being an option — so most of what is in /library is not the demo narrative. It is the back-catalogue: ordinary clips from past collaborations across all five houses, which is what the search has to cut through.

None of it is hand-written, and the pipeline that produced it is the product's own:

  1. fetch-archive-sources.ts gathers extra source footage by category from a free stock library. The curated sources in video-sources.ts were each watched and matched to a scene by hand; that does not scale, and for a haystack it does not need to.
  2. build-backlog.ts cuts each source into short windows taken from different parts of the file, forces a delivery orientation, and measures every result with ffprobe — duration, resolution, audio, sharpness, brightness. Breadth before depth: it takes the first window of every source before anybody's second, because four videos cut once each make a better library than one video cut four times.
  3. analyze-backlog.ts screens, then catalogues. Screening is the interesting half.

Why screening exists. A category listing will cheerfully hand you Niagara Falls for waterfall, the Burj Khalifa for luxury, a swimwear shoot for pool, and somebody applying skincare in their own bathroom for skin-care. Filed in a spa's media library those are not just off-brand, they are false — and a famous landmark sitting there tagged as the grounds of a house in San Jose discredits every clip next to it. So every gathered clip passes a one-frame triage before it is allowed in: the same cheap tier-1 gate the ingest pipeline uses on creator submissions, pointed at the demo data. Tier 0 runs first and for free, so anything the pre-filter would reject never reaches the model.

Of 234 windows cut from 161 sources, 146 were dropped — 31 by tier-0 measurement alone, 115 by the screen — and 88 were kept and catalogued. Roughly three in five gathered clips did not survive, which is about what you would expect from stock categories and is the reason the gate exists.

The first version of that screen was wrong, and wrong in an instructive direction. It was told to be strict, and it rejected 89 of 130 clips — including the hand-picked hammam ("a historic landmark, not a Willow & Stone property"), the cold plunges ("a wild lake, not a spa"), and the rooftop pool. Every rejection was defensible in isolation and the result was useless: it was rejecting footage for being unverifiable rather than for being wrong, and since no frame can prove which building it was shot in, that rule empties the library of exactly the ordinary material it exists to hold. The prompt now says so explicitly — reject what is wrong, not what is unproven, ambiguity is an accept — and hand-curated sources are not screened at all, because a model second-guessing a human who watched the video is a downgrade, not a safeguard. What each clip was judged to be, and why anything was dropped, is committed in prisma/seed-data/backlog-screen.json.

Then the real vision pass runs over what survived, and its output is committed to prisma/seed-data/backlog-analysis.json. This is the step that makes search mean anything. If those descriptions and moment labels had been written alongside the seed data, search would be a demo of a search — it would find things because somebody had already decided it should. Running the actual pipeline over the actual frames means a query only finds a clip when the model, looking at that clip, independently wrote a word that matches. When it fails, it fails honestly, and you can open the clip and see why.


Demo mode

Every AI feature has two implementations behind one interface (lib/ai/client.ts): a live Claude call and a demo response. With no API key set, the demo path runs.

This is not a mock for convenience:

  • A reviewer can clone the repo and experience the whole product with zero setup.
  • The UI was developable without spending tokens on every hot reload.
  • It is also the failure path. If a live call errors mid-demo, that entity falls back to its recorded response and the UI says so, instead of showing a broken screen.

Responses resolve in three steps, in this order:

  1. A recording. scripts/record-fixtures.ts runs every feature live and writes what came back to src/lib/ai/recorded/, keyed by stable identifiers — file name, handle, query — so a recording survives re-seeding. The 44 committed entries are genuine model output from Gemini 2.5 Flash: every clip in both submissions, all twelve fit assessments, eight searches, both coverage passes, the gap analysis and one message of each purpose. The back-catalogue adds 88 more, in prisma/seed-data/backlog-analysis.json. Pass a feature name — record-fixtures.ts search — to re-record one without touching the rest; ingest.json is what makes the flagship submission resolve to 5 of 9, and the model does not tag identically twice.

They also carry the model's mistakes, which is the point of recording rather than writing them — and one of those mistakes was still live when this was last verified. Twenty-two of the recorded tags reported their confidence as the string "high" where the schema declares a 0–1 float; the model had merged the tag's numeric confidence with the analysis's verbal one. Demo mode used to hand its payload straight to the caller, so that string reached Prisma and surfaced as an empty 500 the moment a reviewer clicked Run ingest analysis on the affected clip. The guard now runs on the demo path too, and checks types rather than only enums: a tag that is correct except for its confidence keeps its value and loses the number, while a tag naming something outside the taxonomy is dropped whole. 2. A deterministic path. Coverage matching, fit scoring, gap analysis and edit planning are computed live from the real data rather than replayed, so they stay correct if you approve clips, reject clips, change a creator's numbers or narrow the shelf mid-demo. The fit rubric's hard caps are applied in code exactly as the prompt states them. This is what you are seeing today, and it is why every screen still holds up under a filter nobody anticipated. 3. An honest refusal. A clip nobody recorded — one you upload yourself — gets an entry built from its measured metadata that says outright that no vision pass ran, rather than a description of footage nothing has looked at. Its moments carry the real extraction timestamps and a label saying they were never described.

The UI labels which of these it is showing. Nothing here ever claims a model ran when one did not.


Architecture

Next.js 15 (App Router) · TypeScript · Prisma · SQLite · Tailwind.

prisma/
  schema.prisma          Data model. Rights snapshot onto assets at publish.
  seed.ts                Dates relative to now; metadata read from the media manifest
  seed-data/             Branches, creators, collaborations, measured media manifest
  seed-data/backlog.ts   The archive: past collaborations, and who shot which clip
scripts/
  build-media.ts         Cut every seeded clip from source video, then measure it
  fetch-archive-sources.ts  Gather extra source footage by category
  build-backlog.ts       Cut the back-catalogue into windows and measure every one
  analyze-backlog.ts     Screen it, then catalogue it with the real vision pass
  record-fixtures.ts     Capture live model output as the demo responses
src/lib/
  media.ts               Tier 0: ffprobe, scene detection, sharpness, transcoding
  taxonomy.ts            One controlled vocabulary → the DB, the UI, and every AI schema
  pipeline.ts            The state machine
  rules.ts               Money, coverage counting, attention rules, rights expiry
  library.ts             One filter object; one where-clause; two ways to build it
  grouping.ts            Shelving by spa and by creator
  moments.ts             Timestamped index of what happens inside a clip
  search-index.ts        Relevance: what is visible in a clip, where it was shot, by whom
  ai/client.ts           Single door to Claude: forced tool calls, retry, demo fallback
  ai/features/           One file per capability
src/app/(app)/           The CRM (sidebar chrome)
src/app/upload/          The creator-facing page (no chrome — they do not work here)

Three decisions worth explaining:

The taxonomy is injected into every AI schema as a closed enum. The model physically cannot return a tag the library does not recognise. Taxonomy drift — the thing that quietly kills every media library after a few hundred assets — is prevented structurally rather than by asking nicely.

Rights are snapshotted, not referenced. Editing a collaboration's terms cannot retroactively re-license footage already in a campaign.

Natural-language search compiles to the same filter object the facet rail produces. One query path, so search cannot hallucinate a result and the rail keeps working when the model does not.

Filters decide what is eligible; relevance decides what comes first. Free text used to be part of the where-clause — every word an AND of LIKE %word%. That reads as thorough and behaves terribly: "water hitting the hot stones" is five conditions that must all hold in one row, so it returns nothing, and an empty grid is indistinguishable from "we do not own this footage". Now the words rank instead, in src/lib/search-index.ts, against the description and the per-frame moment labels the vision pass wrote at ingest — plus the spa, the creator and the file name, because "Nadia's San Jose plunge stuff" is how people actually ask. Ranking never removes a clip: what matched comes first, and what merely passed the filters stays on the page below a divider. Every card shows the field and the words it matched on.

A clip is indexed by its parts, and the parts have measured timestamps. The vision pass writes one label per frame it was shown; resolveMoments in src/lib/moments.ts pairs those labels with the seconds ffmpeg actually extracted them from and discards any index the clip does not have. So a moment can never point at a second that does not exist, and clicking a search result lands on the frame the model was genuinely looking at when it wrote the line — not near it.

The demo data has real defects, not labelled ones. Where the scenario says a creator delivered one soft clip, build-media.ts genuinely softens that file before encoding it. The pre-filter then measures a real defect and rejects it on the evidence. Labelling a perfectly sharp clip "blurry" in a fixture would make the entire quality pipeline decorative.


Structured output is not a given

The taxonomy design rests on one claim: the model cannot return a tag the database does not recognise, because the allowed values go into the response schema as a closed enum. That holds on the Anthropic API. Running the identical schema through a gateway, it did not, and the failure was silent.

Handed this app's real clip schema, Gemini 3 Pro returned peopleVisible: false where a string enum was required, invented verdict: "ACCEPT", and filled the tags array with four nulls. Through response_format: json_schema instead of tool calls it invented its own taxonomy outright — categories location and object, values ladle and sauna heater stones. Nothing in either response indicated the schema had been ignored.

Gemini 2.5 Flash, given the same schema by both mechanisms, returned exactly the declared shape with exactly the declared values, every time. So the smaller, cheaper model is the correct one here, and not as a compromise: schema compliance is not the same axis as capability, and a model that writes a better description into the wrong shape is worth nothing to a pipeline that has to store it. It is also a sixth of the cost — 0.02 credits against 0.13 for a keyframe call.

Three things came out of this, and all three are in the code:

  • src/lib/ai/schema-guard.ts enforces every enum and every declared type in code, on both the live and the demo path. Violations are fed back to the model as a named correction on the retry; anything still wrong is stripped rather than stored. The guarantee is now true regardless of who serves the model — but the reason it is true is the guard, not the provider. Types matter as much as enums and are ignored just as readily: a "high" in a numeric confidence field is not a taxonomy problem, so an enum-only check sails straight past it and the database rejects the write instead.
  • Nullable fields are translated at the boundary. { type: ['string','null'] } is ordinary JSON Schema that Gemini's function-calling subset rejects outright, which killed two features on the first live run. The schemas stay correct; the adapter makes a nullable field an optional one.
  • The model is no longer offered flags it cannot observe. Given 1280×720 clips it flagged LOW_RESOLUTION, and it flagged NO_AUDIO on silent b-roll where silence is the point. Both are measured in tier 0 before any model runs. Telling it not to is a request; removing the values from its enum is a guarantee, so the vision pass now sees only the flags that require looking at the picture.

Why keyframes, and not video

This is worth stating plainly because it looks like a shortcut and is not: the Claude API does not accept video input. Vision means images. So on Claude, "analyse the footage" necessarily means "analyse frames extracted from the footage" — the only real choice is which frames, and how honest the product is about the difference.

That is what tier 0 exists for. Frames are picked on scene change rather than at fixed intervals, three genuinely different moments per clip, and the UI shows you which frames the model saw with the timestamp each came from. The prompt tells the model it is looking at stills and to hedge on anything motion- or audio-dependent, and the review screen says the same thing to the reviewer.

Adding a model that watches video end-to-end (Gemini accepts video natively) would remove that hedge for the handful of requirements that actually need it — continuous takes, audio quality, "does the shot hold for six seconds". That is a real upgrade and it is on the next-steps list. It is not a substitute for tier 0: full-video analysis costs far more than three frames, so the pipeline would still pre-filter and triage first and reserve the expensive pass for clips where the brief hinges on something a still cannot show.

What I would build next

  1. Video-native analysis for the claims stills cannot settle. Keep tiers 0 and 1 exactly as they are, and add a video pass for clips whose brief requirement depends on motion, continuity or audio. This is also what would make the moment index dense rather than three points per clip: right now a moment exists wherever a keyframe was extracted, which is enough to find the beat but not enough to find every beat. Transcribing talking-head clips would put spoken words on the same timeline, which is the same feature again.
  2. Actual integrations. Direct-to-storage uploads with signed URLs, a real email provider, e-signature for the rights agreement.
  3. Learning from editors. Every download and every "used in project" is a quality signal. After a few hundred, the grading rubric should be calibrated against what editors actually reach for rather than what the model finds pretty.
  4. Embeddings at scale. Past ~10k assets, filter translation stops being enough. It should sit alongside filters, not replace them.
  5. Batch backfill. The existing archive of unlabelled footage is the biggest single win available, and the Batch API halves the cost of processing it.
  6. Repeat-partner mechanics. Nadia is on her third visit. Repeat creators are cheaper and better than new ones, and nothing in the product currently optimises for that.

Notes

Willow & Stone is invented. Every clip in the demo library is a real video file, cut from freely licensed spa footage (Pexels, Mixkit, Wikimedia Commons) and re-encoded as a small web proxy; the keyframes were extracted from those cuts with ffmpeg and are the exact images the analysis reads. Per-file attribution is in public/footage/CREDITS.md, regenerated by the media build. None of the footage depicts a real Willow & Stone location, because there is no such thing.

The source videos are not committed — npm run media:build re-downloads them into a git-ignored cache and rebuilds the proxies. Only the ~30 MB of generated clips and their keyframes ship in the repo.

Built as a 96-hour prototype. It is not production code: there is no auth, no test suite, no error boundary discipline, and files are not really stored. Those were conscious trades against showing the product thinking end to end.

About

Creator-collaboration CRM and raw-footage library: AI catalogues every clip at ingest, indexes it by the second, and drafts the cut.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages