Skip to content

Add social-reels and higgsfield-video-studio cabinets, wired to Gemini Video - #13

Open
oxedom wants to merge 11 commits into
mainfrom
feat/social-reels
Open

Add social-reels and higgsfield-video-studio cabinets, wired to Gemini Video#13
oxedom wants to merge 11 commits into
mainfrom
feat/social-reels

Conversation

@oxedom

@oxedom oxedom commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

A reel is a list of shots. A shot is a folder holding every take ever generated for it. The dashboard is where you throw most of them away.

Purely additive — one new cabinet directory, 21 files, 164KB.

Why a dashboard and not a gallery

Most AI-generated video is not good. The work isn't generating, it's choosing. So the three cheap actions are instant and the expensive one is deliberate:

Action Mechanism Cost Latency
Approve / reject PUT shot frontmatter free instant
Switch take PUT selected: free instant
Delete take DELETE free instant
Regenerate PUT job run credits minutes

Every free action goes through /api/pages, which auto-commits — so every click is undoable from git history.

Three decisions that shape everything

One writer per file. A shot's index.md is written only by the dashboard, acting for the person clicking. The agent is forbidden from touching it, even to clear the flag it just acted on. Video takes minutes; without this rule an approval made at 09:31 gets silently reverted by an agent that read the file at 09:29 and wrote it back at 09:33. To a normal user that reads as "the app forgets what I click."

Nothing runs on a timer. There is no routine here and there should never be one. make-shot.yaml ships enabled: false and is fired only by the button — executeJob() ignores the flag, so manual dispatch works while the daemon never fires it. Every clip is something a person asked for, on purpose.

One dispatch is one shot. The dashboard fires a run per shot rather than one per campaign: the 900s timeout is per run, a failure costs one shot instead of six, and concurrent job agents are known to OOM this machine.

Notes on the implementation

  • Not /api/agents/headless. It hardcodes a 120s timeout and terminates the agent. For video that means the user is charged and the clip is stranded. Job runs are fire-and-forget with their own timeout.
  • The prompt travels through the file, not the request, since the run action takes no prompt override. The dashboard writes prompt + regenerate: true; the job generates whatever is flagged.
  • Shot state is fetched raw, not read from /api/tree — the tree whitelists frontmatter down to title/icon/order/google and drops status and selected.
  • Frontmatter parsing handles block scalars. The API serializes with a real YAML writer and folds long strings onto continuation lines, so reading only the first line captures a literal >- and destroys the prompt. This was invisible on the first save and only bit on the second.

The example campaign

A made-up wedding company, three shots, colour-card clips (38KB total), in three deliberate states: one approved, one rejected and re-shot, one with two takes awaiting a decision.

The imperfection is the point. A seed campaign where everything is approved would teach nobody what the dashboard is for.

Verified against a running app

Not a mock — installed into a live data dir and driven in a browser:

  • Tree types it correctly (cabinet / app / video)
  • Dashboard renders 3 shots, 3 videos, 4 take-switch buttons, correct state pills
  • Approve flips status on disk and auto-commits
  • Second save preserves the folded prompt and selected
  • make-shot loads with enabled: false, timeout: 900

Not verified

Regenerate has never produced a real clip. Higgsfield isn't connected on the test machine, so the dispatch is proven but the round trip isn't. A smoke test on a connected account is the right gate before merge.

Related: #12 (higgsfield-video-studio), which shares the job-dispatch pattern.

oxedom added 5 commits August 18, 2026 12:21
A sibling to higgsfield-studio, but request-driven rather than brief-driven:
the Studio page carries the text box, and pressing Send dispatches a one-shot
Video Maker run against the user's own Higgsfield account.

Higgsfield returns a link rather than a file, so the agent downloads the clip
into videos/<slug>-<stamp>.mp4 and writes a .txt beside it holding the prompt,
model, duration, credits and the original URL. The folder listing stays the
whole data model — nothing is overwritten, and the .txt is what lets a clip be
re-run with one thing changed. If the download fails the .txt is still written:
the credits are spent and the link is the only way back to the clip.

Video is roughly a hundred times the cost of a still image and a free trial
starts at 10 credits, so the persona's spending rules come before everything
else in the file: one clip per request, about five seconds, cheapest video
model that does the job, 2 credits per run, one retry on an outright failure
and never a second take because the first could be better. Not connected, out
of credits, or over the ceiling all write nothing at all and report the numbers.
The weekly routine ships switched off for the same reason.

The page is a fork of gemini-image-studio's chassis-2 with one behavioural
change: /api/agents/headless caps at 120s and clips regularly run longer, so an
abort is no longer reported as a failure. It would tell someone their clip died
while their credits were already committed. Instead the page says "Still
generating", keeps the prompt text, and re-lists the folder until it appears.

No cover.jpg yet — the entries are added to both cover scripts, but those
hardcode paths that only resolve on the maintainer's machine.
The Send button called /api/agents/headless, which hardcodes timeoutMs 120_000
and, when it expires, calls terminateChildProcess(proc). The agent is killed —
the run does NOT continue in the background as the page claimed.

An image usually beats that clock. A clip usually does not. So the shipped page
told anyone whose clip took longer than two minutes "Still generating. The clip
will appear here when it is ready" and then polled for ten minutes for a file
that was never coming, while Higgsfield had already charged them for the
generation. Paid, killed, and reassured — the exact failure the persona's
spending rules exist to prevent.

Send now does two writes and waits for neither:

  PUT /api/pages/<cabinet>/requests/<stamp>   the prompt, status: pending
  PUT /api/agents/video-maker/jobs/on-demand-clip   {action: "run"}

Job runs are fire-and-forget and carry their own timeout, so on-demand-clip.yaml
sets 900s and the agent lives long enough to finish. executeJob() does not check
`enabled`, so the job ships switched off — never fired by the daemon — while the
button still works. The weekly routine is untouched; it always had its own 900s
budget and was never affected.

requests/ has exactly one writer. The job prompt forbids the agent from editing
those files at all, so an edit made while a clip generates cannot be clobbered
by an agent that read the file three minutes earlier.

Also corrects two claims the persona could not back:

- Model choice. Higgsfield's docs say the model is picked from the prose of the
  prompt, not a validated parameter, so "take the cheapest model" was advice the
  tool does not enforce. The persona now requires naming the model explicitly in
  the prompt and recording it, since an unnamed model is priced at whatever the
  service picks.
- Result shape. The persona asserted Higgsfield returns a link to download. Their
  documentation only says clips "land in your Higgsfield workspace", and
  generation is asynchronous with polling tools. It now handles a URL, a job id
  to poll, or neither — and is forbidden from inventing a link.
A smoke test from 2026-08-12 settles what the docs left vague. The tools do
expose a credit preflight (z_image 0.15 vs nano_banana 1.00) and models are real
ids passed as parameters, not prose the service interprets. Generations return a
job id first and a CloudFront URL on completion.

That makes the 2-credit ceiling an actual check rather than an intention, so the
persona now requires pricing a generation before committing to it.
A reel is a list of shots; a shot is a folder holding every take ever generated
for it. Approve, reject, switch takes and delete are local file writes through
/api/pages — free, instant, and auto-committed to git, so every click is
undoable. Regenerate is the only action that spends.

Three decisions shape everything else.

ONE WRITER PER FILE. A shot's index.md is written only by the dashboard, acting
for the person clicking. The agent is forbidden from touching it, even to clear
the flag it acted on. Video takes minutes, so without this rule an approval made
at 09:31 would be silently reverted by an agent that read the file at 09:29 and
wrote it back at 09:33 — a class of bug that reads to a normal user as "the app
forgets what I click".

NOTHING RUNS ON A TIMER. There is no routine here and there should never be one.
make-shot.yaml ships enabled:false and is fired only by the button; executeJob()
ignores the flag, so manual dispatch works while the daemon never fires it. Every
clip is something a person asked for on purpose.

ONE DISPATCH IS ONE SHOT. The dashboard fires a run per shot rather than one run
per campaign: the 900s timeout is per run, a failure costs one shot instead of
six, and concurrent job agents are known to OOM this machine.

Not /api/agents/headless: it hardcodes a 120s timeout and terminates the agent,
which for video means the user is charged and the clip is stranded.

The prompt travels through the file rather than the request, since the run action
takes no prompt override — the dashboard writes prompt + regenerate:true, the job
reads whatever is flagged.

Shot state is fetched raw rather than read from /api/tree, which whitelists
frontmatter down to title/icon/order/google and drops status and selected.

Frontmatter parsing handles block scalars. The API serializes with real YAML and
folds long strings onto continuation lines, so reading only the first line would
capture a literal ">-" and drop the prompt. Verified against the running app: the
fold survives a second save intact. That bug was invisible on the first save.

Ships with a made-up wedding campaign — colour-card clips, 38KB total — in three
states: one approved, one rejected and re-shot, one with two takes awaiting a
decision. Most generated video is not good, and a seed campaign where everything
is approved would teach nobody what the dashboard is for.
higgsfield-video-studio (one clip from a text box) and social-reels (a campaign
dashboard for triaging takes) share the same dispatch pattern and the same
findings about what Higgsfield can be trusted to do. Reviewing them apart meant
reading the same reasoning twice.
@oxedom oxedom changed the title Add social-reels: a campaign dashboard for triaging generated video Add two video-generation cabinets: higgsfield-video-studio and social-reels Aug 18, 2026
@oxedom

oxedom commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Now carries both templates — #12 folded in here

feat/higgsfield-video-studio has been merged into this branch, so this PR is the single review surface for both cabinets. #12 is closed as superseded, not abandoned — every commit from it is in this history.

higgsfield-video-studio — a text box. Describe a clip, get one video file. Single-clip, single-user, no campaign structure.

social-reels — campaigns of shots, with a dashboard for approving, rejecting, switching takes and regenerating.

They share the parts that matter, which is why reviewing them together is cheaper than reviewing them apart:

  • Dispatch through a job run, never /api/agents/headless (which kills the agent at 120s and strands a paid-for clip)
  • Jobs ship enabled: false and are fired by a button — executeJob() ignores the flag, so manual dispatch works while the daemon never fires them
  • The prompt travels through a file, since the run action takes no prompt override
  • Artifacts are immutable, timestamped, never overwritten, each with a .txt recording prompt / model id / credits / URL
  • The persona preflights cost, names models by id, and caps a run at 2 credits
  • Both ship EXAMPLE content so the page is populated before any connection exists

Scope: Higgsfield only

Neither cabinet talks to Gemini Veo 3 or OpenAI directly. There is no video provider in src/lib/image-gen/providers.ts and no connector for either — that engine work is not in this PR.

Higgsfield does carry Veo 3.1 and Sora 2 among its ~30 models, so they are reachable through Higgsfield by naming the model id. But that is one vendor's account and one credit balance, not a Gemini or OpenAI integration.

Still unverified

No clip has ever come back from Higgsfield through either path — it is not connected on the test machine. Dispatch, file writing, state and rendering are all verified against a running app; the generation round trip is not. A smoke test on a connected account remains the right gate before merge.

cabinet-app gains a first-party Veo tool on the plain Gemini API key, so the
cabinet is no longer Higgsfield-only. The agent now prefers it and falls back.

The preference is not arbitrary. gemini_generate_video takes `path` and writes
the file into the cabinet itself, so the whole poll-then-download dance
disappears — the step most likely to strand a clip that has already been paid
for. It also takes real parameters (aspect_ratio, duration_seconds, resolution)
instead of Higgsfield's prose-level model choice.

What the persona now says about cost differs per connector, because the two bill
differently and pretending otherwise would be a lie in one of them. Higgsfield
prices a generation before you commit, so preflight and the 2-credit ceiling
apply there. Gemini has no preflight — it bills per second, so the duration
asked for IS the estimate, and the rule becomes "shortest duration that serves
the shot" instead.

A Gemini paid-tier error is called out as its own stop condition: billing is not
enabled, and no amount of retrying turns billing on. That failure is reachable
with a key that generates images perfectly well, which is exactly the case a
user would otherwise read as "the cabinet is broken".

index.md now documents both connectors, Gemini Video first, and says plainly
that video needs the paid tier.
@oxedom oxedom changed the title Add two video-generation cabinets: higgsfield-video-studio and social-reels Add social-reels and higgsfield-video-studio cabinets, wired to Gemini Video Aug 18, 2026
oxedom added 5 commits August 18, 2026 16:21
The page wore the registry's parchment skin — cream paper, serif display,
terracotta. That is right for reading and wrong for this: video is judged
against a neutral dark surround, which is why every edit suite and grading room
is painted that way. A cream page around a clip misreports its own contents, so
this one commits to dark in both colour schemes rather than following the house
style into a lie.

The rest is borrowed from film review rather than invented. Teal and orange —
the most recognisable grade in cinema — carry the only two things that matter:
the action you can take, and the take you chose. Data is monospaced because
slates, timecodes and take numbers are.

The signature is the circle. A script supervisor rings the take to be printed in
red china marker and strikes the ones that are no good, so the mark already
encodes exactly the state this dashboard tracks. It is drawn on the footage,
hand-shaped rather than a clean ellipse, and strokes on when you circle a take.
Rejected shots dim instead of shouting, and come back on hover. Buttons say
"Circle it" and "No good" because that is what the mark means; "Generating"
stays plain English because jargon helps nobody watch a progress state.

No webfonts. Cabinet runs local-first and a page whose personality depends on
fonts.googleapis.com has none on a plane. Weight, tracking and case do the work.

Each card now credits the model that made the take it is showing, with the
provider's own mark served from the app's logos. That reads from the take's .txt
sidecar, so switching takes refetches it — rendering from the copy already in
memory showed the previous take's model beside the new clip, which is worse than
showing nothing.

Seed sidecars carry realistic model ids so the marks have something to show. The
files still say plainly that nothing was generated and nothing was spent.
The dark bay is gone. Paper is the house style, and it suits the subject better
anyway: a contact sheet IS paper, and ringing the chosen take in red china
marker is what a person does to one. The mark now sits on a white card the way
it would sit on a print.

Full three-state theming, matching the other registry pages — an explicit
data-scheme wins in both directions, the media query applies only when nothing
is stamped. The clip wells stay black regardless, because footage is judged
against neutral and that is not a theme preference.

Two colours, two jobs, never swapped: red is a decision you made, blue is an
action you can take. Georgia for headings as the house style has it, monospace
for take numbers, model ids and prices because slates genuinely are monospaced.

A rejected take now drains to greyscale instead of fading the whole card.
Fading turned its black video well into grey mush on paper, which read as
'failed to load' rather than 'set aside', and it dimmed the text you need in
order to remember why you rejected it. Hover restores the colour.

Delete take drops its border to read as tertiary. Losing a take you paid for
should not sit shoulder to shoulder with the things you do all day.
The cards were landscape boxes with portrait clips letterboxed inside them, so
roughly half of every card was dead black and the eye landed on the bars instead
of the footage. Reels are vertical; the cards are now vertical too, and the clip
fills the frame. That costs a crop at the edges and buys a page that looks like
what it produces.

Native video controls are gone. A grey browser control bar across the bottom of
every card, three in a row, reads as a form rather than a wall of footage. Hover
plays the clip muted and looping, click toggles so touch and keyboard are not
left out, and prefers-reduced-motion gets click-only. Duration is drawn from
loadedmetadata rather than trusted from the sidecar.

The direction now reads as the line of direction it is and only becomes a field
when clicked. Its box grows to its text, because a scrollbar inside a card is
the detail that makes a page look like an admin panel.

Three layout bugs the screenshots caught:

- auto-fill with 1fr kept a full-width empty track when a row was short, leaving
  a hole on the right. Letting cards stretch instead would render a three-shot
  campaign as three enormous portraits, so the track is bounded and a card stays
  a card however many there are.
- The cost was truncating to '0…'. A model id shortened to 'veo-3.1-lite…' still
  says what made it; a price shortened to '0…' says nothing, so the price never
  truncates and the id yields instead.
- Four controls would not fit across a 232px card and wrapped mid-word. The
  judgement pair now shares a row, the thing that spends money gets its own row
  at full width, and delete is a small mark at the end of the pair.
The wall now fits four cards on a desktop row and the example campaign has four
shots to fill it — three cards in a four-track grid left a hole on the right,
and a three-shot reel was a thin demo of a format that is normally four or more
beats. The new shot is The toast, sitting between the first dance and the call
to action, unreviewed with a single take.

Two up on a phone rather than one. A single column turns a four-shot campaign
into four screens of scrolling, and comparing takes is the entire job — you
cannot judge a shot against its neighbour if you can never see both at once.
The card needed no reflowing to get there: it is already a portrait video, which
is the shape a phone wants. The work was trimming the chrome around it.

Hover does not exist on touch, which is why playback was always click-toggleable
and the play affordance is drawn rather than implied by a cursor.

Below 400px the judgement pair keeps the row and delete drops beneath it, where
a lone glyph would read as an orphan — so at that width it takes the words it
now has room for. A touch target that destroys a take you paid for should say
what it destroys; the aria-label already carried that for screen readers.

Verified at 1280 (four 266px tracks, full row) and at 390 (two 175px tracks, no
horizontal overflow).
The ring was a script supervisor's mark — in film you circle the take to be
printed — and it did not read as anything of the sort to someone looking at the
page. A visual device whose whole justification is that it communicates
instantly has failed when it needs a paragraph of explanation, so it is gone
rather than defended.

An approved take is now edged in green and a rejected one still drains to grey.
Both are conventions people already know from every gallery and inbox they use,
which beats a cleverer mark nobody has seen before.

The vocabulary goes plain with it: Approve, Reject, Regenerate, and states that
read Approved / Rejected / Not reviewed / Generating. "Circle it" and "No good"
only made sense next to the mark that explained them, and this cabinet is for
people making wedding ads, not for a film crew.

The example banner is gone too. The campaign is literally named EXAMPLE-… in the
heading directly above it, so the banner said the same thing a second time, in a
box, as the loudest element on the page. Dead .seeded styles removed with it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant