Add social-reels and higgsfield-video-studio cabinets, wired to Gemini Video - #13
Add social-reels and higgsfield-video-studio cabinets, wired to Gemini Video#13oxedom wants to merge 11 commits into
Conversation
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.
Now carries both templates — #12 folded in here
They share the parts that matter, which is why reviewing them together is cheaper than reviewing them apart:
Scope: Higgsfield onlyNeither cabinet talks to Gemini Veo 3 or OpenAI directly. There is no video provider in 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 unverifiedNo 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.
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.
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:
PUTshot frontmatterPUTselected:DELETEPUTjob runEvery 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.mdis 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.yamlshipsenabled: falseand 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
/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.prompt+regenerate: true; the job generates whatever is flagged./api/tree— the tree whitelists frontmatter down totitle/icon/order/googleand dropsstatusandselected.>-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:
statuson disk and auto-commitsselectedmake-shotloads withenabled: false,timeout: 900Not 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.