OpenRomeo Video Studio: A Cookie-Free, Blob-Counted Quota for a Per-Clip-Priced Video Model

Romi Nur Ismanto
Independent AI Research Lab, Jakarta, Indonesia
hello@rominur.com
September 2026

Abstract

OpenRomeo Video Studio, deployed at video.rominur.com, turns one sentence or one image into a ten-second cinematic clip at up to 1080p in six aspect ratios, with audio. The generating model is priced per clip rather than per token, which makes the product's central engineering question the same as its central business question: how to let anyone with a Google account try it while making the worst possible day cost a known amount. The answer is small enough to describe completely. The application is one static HTML file and three serverless functions sharing a 142-line helper, with a single dependency. There is no session cookie and no session store: the browser obtains a Google ID token and presents it as a bearer credential on every call, the server verifies it against Google and caches the verdict, and the token's own expiry is the session's. The per-account quota is not a counter but a count: each accepted job writes one small blob under a salted hash of the account's email, the quota used is the number of blobs, and a refund is a deletion — no email, no counter to corrupt, no transaction to lose. Before a job is submitted, a vision-capable chat model rewrites the user's idea into a 45–90-word English cinematic prompt with camera movement, lens, lighting and a three-beat motion arc, faithful to the reference image when there is one; the rewritten prompt is shown to the user, the original is kept, and a refusal or malformed rewrite silently falls back to the user's words. The video model's identity never reaches the browser: the server whitelists the fields of every job object it forwards. The client polls every twenty seconds, keeps the job in local storage so that a reload or a lost connection resumes monitoring rather than resubmitting, refuses to show 100% until the provider says completed, and downloads the result from a presigned URL with a server-side streaming proxy as fallback so the provider key is never exposed. Failed jobs return their quota automatically.

Keywords: text-to-video, image-to-video, generative video, prompt refinement, Google Identity Services, ID tokens, stateless sessions, quota enforcement, object storage, serverless functions, polling, presigned URLs, cost governance, OpenRouter, Vercel Blob

1. Introduction

Generative video is the most expensive inference a small web product can expose to the public. A ten-second clip at 1080p costs a fixed, non-trivial sum every time the button is pressed, and the button is the entire product. Everything else in this application — the sign-in, the quota, the prompt refinement, the polling, the download path — exists to make that button safe to put on the open internet.

The constraints were chosen to be legible. Four clips per Google account. Ten seconds each. Six aspect ratios from ultra-wide to vertical, two resolutions, optional audio. A landing page that states all of this before asking anyone to sign in, with a showcase of the kinds of shot the studio produces: a neon city drive, a product hero, a portrait reel, a drone over a coastline, a macro of dew on a leaf. Behind the landing page the whole system is one HTML document with an inline script, three Vercel functions of twenty to fifty-five lines each, and a shared helper. This paper describes the decisions inside those files.

2. The Session Is the Token

The application sets no cookies and keeps no session table. Sign-in uses Google Identity Services in the browser with nothing but a client ID; the ID token Google returns is decoded on the client for a first sanity check — a verified email, an audience matching this application — stored in local storage together with its expiry, and sent as Authorization: Bearer on every API call. Fetches are made with credentials: 'omit', so there is no cookie to protect and no cross-site request forgery surface to reason about.

The server does not trust the client's decoding. Each token is verified against Google's tokeninfo endpoint, and the audience, the verified-email flag and the expiry are checked again. Because the same token accompanies every request during its roughly one-hour life, verified tokens are cached in a per-instance map keyed by the token string, cleared when it grows past five hundred entries; a token is thus verified once per warm instance rather than once per request. When the token expires the client removes it a minute early, the server answers 401, and the interface asks for sign-in again. The trade-off is explicit: sessions are exactly as long as Google makes them, and there is no way to revoke one server-side other than to wait — acceptable for a tool whose only privileged action is spending one of four credits.

3. Quota as a Count of Objects

A counter in a database invites the usual failures: lost updates under concurrency, a counter that drifts from the events it summarises, a refund path that must reverse an increment it cannot see. The studio avoids the counter altogether.

Table 1. The quota store: two prefixes in one object bucket.
ObjectWritten whenContainsMeaning
quota/<owner>/<jobId>.jsonA job id comes back from the providerTimestamp, ratio, resolutionOne credit spent. Quota used = number of objects under the owner prefix.
jobs/<jobId>.jsonSame momentOwner hash, timestampReverse index, so a job id alone is enough to find whose credit to return.

The owner is sha256("openromeo:" + email), truncated: no email address is ever stored. Reading the quota is a prefix listing, which for a ceiling of four objects is trivially cheap. Consuming a credit is one put, performed only after the provider has returned a job id, so a request the provider rejects costs nothing. Refunding is one del: when the status endpoint observes a terminal failure — failed, cancelled or expired — it resolves the owner through the reverse index, deletes the credit object, and reports refunded: true to the client, which tells the user their credit came back. The bookkeeping cannot disagree with itself, because the events and the count are the same objects.

4. Prompt Refinement, Shown and Reversible

Users write ideas, not shot lists. Before submission the server passes the idea — and the reference image, when the mode is image-to-video — to a vision-capable chat model under a system prompt that asks for exactly one polished English prompt of 45 to 90 words: the user's subject and named details preserved, plus camera movement, shot size, lens feel, lighting, palette, mood and a start-middle-end motion arc for ten seconds, faithful to whatever is visible in the image and free of text overlays and unsafe content. Reasoning is disabled and the output capped at 320 tokens so the step is fast.

Three properties keep the refinement honest. It falls back: a failure of the vision model retries a text model, and if the result is shorter than twenty characters, longer than 1,200, or begins like a refusal, the user's original words are sent instead and the response says enhanced: false. It is shown: the reply carries both the original and the final prompt, and the interface renders the final one in a collapsible block labelled “Arahan dipertajam otomatis” with the original underneath, so no one wonders why the clip has a dolly move they did not ask for. And it is ordered correctly: the quota is checked before refinement, so an account with no credits left never pays for a rewrite it cannot use.

5. The Model Has No Name Here

The generating model is chosen by an environment variable on the server, and the browser never learns it. A publicJob function whitelists the fields forwarded from the provider's job object — id, status, timestamps, error, polling and download URLs, usage, duration, resolution, aspect ratio — and nothing else; error objects are flattened to a message. The options offered in the interface (aspect ratios, resolutions, whether audio is available) are read from the provider's model catalogue on the server and sent as plain lists, with a static fallback if the catalogue call fails. Swapping the engine is a configuration change that the front end cannot observe.

idea (+ image) → quota check → refine prompt → submit job → consume credit → poll every 20 s → completed → presigned MP4 (proxy fallback)

6. A Client That Can Lose the Connection

A video job takes minutes, and a browser tab is not a reliable place to wait. The client is written on that assumption. The job returned by submission is written to local storage immediately, with its start time and an estimate; polling runs every twenty seconds for up to 180 attempts — an hour — and each poll merges the latest job state back into storage. If the page is reloaded, the studio finds the stored job and offers to resume monitoring without submitting anything new. If the user stops monitoring, the card says so plainly — the job is still running on the server — and the button becomes “Cek status lagi”, which resumes with the original start time so the elapsed clock stays truthful.

The progress bar is deliberately conservative. Its estimate is a heuristic — sixty seconds plus nine per second of video, plus forty-five for 1080p, clamped between 45 and 420 seconds — and the bar is allowed to creep toward, but never reach, completion on the estimate alone; 100% is written only when the provider's status is completed. The code carries its own audit trail: comments prefixed with fix identifiers record why the running status was corrected from in_progress to processing, why the start time is preserved across retries, and why a frozen “processing 74%” card after monitoring stopped was judged misleading and replaced.

7. Downloading Without Exposing the Key

When a job completes the provider may return presigned URLs. If the first is an HTTPS URL the browser fetches it directly, with no credentials, and receives the MP4 without the server touching a byte. If there is no such URL, or the direct fetch fails, the client falls back to /api/video-content, which requests the clip from the provider with the server's key and streams the body through with the upstream content type and length preserved, via Readable.fromWeb(…).pipe(res), so a hundred-megabyte clip is never buffered in the function. The result is turned into a scoped object URL for playback and download; the previous object URL is revoked when a new one is created.

8. Input Discipline

Table 2. Validation on the submission path.
InputRule
PromptRequired; at most 2,000 characters.
First-frame imageMust be a data:image/(png|jpeg|webp);base64 URL of at most 4 MB; the client first downscales anything over 900 KB to 1,600 px JPEG.
Aspect ratio, resolutionAccepted only from fixed whitelists; anything else is dropped rather than forwarded.
Job idMust match ^[A-Za-z0-9_\-:.]{4,128}$ before it is interpolated into an upstream URL.
Provider errorsMapped by status to Indonesian messages — credit exhausted, guardrail refusal, busy, provider fault — with upstream detail truncated to 200 characters and every 5xx collapsed to a 502.

9. Deployment

The whole application is a Vercel project deployed from the command line: a public/ directory holding the one HTML file, an api/ directory holding the three functions with a sixty-second ceiling, a lib/ helper, and one dependency. API responses carry Cache-Control: no-store. The README records the setup state truthfully, including a detail most projects would omit: the Google OAuth client's list of authorised origins is full, so sign-in works only on the custom domain and not on the platform's preview hostnames.

10. Limits

The quota check and the credit consumption are not atomic. Two submissions from the same account arriving within the same second could both observe one credit remaining and both be accepted; for a four-credit demo the exposure is one extra clip, but a larger allowance would want a conditional write or a lock. Refunds depend on someone polling: a job that fails after the user closes the tab keeps its credit until the next time that job's status is requested, which for an abandoned job may be never.

The status and content endpoints are not gated by the Google token; the job identifier — long, provider-generated and unguessable — is the capability. That is a common and defensible pattern, but it means a leaked job id lets anyone watch, and download, that clip. Sessions cannot be revoked before the ID token expires. The quota objects are public and deterministically named; they hold no personal data, only a salted hash whose salt is a constant in the source, so the protection against enumerating accounts rests on the bucket hostname and object names not being known rather than on the hash. And the page itself sends no hardening headers, since it is served as a plain static file.

11. Conclusion

The studio is small because its problem is small once it is stated correctly: let anyone try an expensive model, four times, and make every path — sign-in, refusal, failure, reload, download — cost the right amount and nothing more. The token is the session, the objects are the ledger, the prompt rewrite is shown and reversible, the model is anonymous to the browser, and the client is built to be closed. None of these ideas is specific to video; all of them apply wherever the unit of cost is one API call whose price is felt.

References

  1. Google. “Sign In With Google for Web: Verify the Google ID Token on Your Server Side.” Google Identity documentation, 2026.
  2. Sakimura, N., et al. “OpenID Connect Core 1.0.” OpenID Foundation, 2014.
  3. Jones, M., Bradley, J., and Sakimura, N. “JSON Web Token (JWT).” RFC 7519, 2015.
  4. Sheffer, Y., Hardt, D., and Jones, M. “JSON Web Token Best Current Practices.” RFC 8725, 2020.
  5. Jones, M., and Hardt, D. “The OAuth 2.0 Authorization Framework: Bearer Token Usage.” RFC 6750, 2012.
  6. OpenRouter. “Video Generation API: Jobs, Status, and Content.” Platform documentation, 2026.
  7. OpenRouter. “Chat Completions with Image Inputs.” Platform documentation, 2026.
  8. Vercel. “Vercel Blob: put, list, head, and del.” Product documentation, 2026.
  9. Vercel. “Vercel Functions for the Node.js Runtime.” Platform documentation, 2026.
  10. Node.js Foundation. “Stream: Readable.fromWeb.” Node.js documentation, 2026.
  11. WHATWG. “Fetch Standard — Request Credentials Mode.” 2026.
  12. WHATWG. “HTML Living Standard — Web Storage.” 2026.
  13. W3C. “File API — Blob URLs and URL.createObjectURL.” Working Draft, 2026.
  14. Amazon Web Services. “Sharing Objects Using Presigned URLs.” Documentation, 2026.
  15. Fielding, R., et al. “HTTP Semantics.” RFC 9110, 2022.
  16. Ho, J., et al. “Video Diffusion Models.” NeurIPS, 2022.
  17. Blattmann, A., et al. “Stable Video Diffusion: Scaling Latent Video Diffusion Models to Large Datasets.” arXiv:2311.15127, 2023.
  18. Ismanto, R. N. “Jaipong: A Two-Stage Text-to-Song Studio and the Case for a Line-Oriented Plan Format.” 2026.
  19. Ismanto, R. N. “Pikapiku: Client-Side Chunking and a No-Retention Schema for Long-Form Transcription on Serverless Infrastructure.” 2026.