Google shut down the Veo preview models on the Gemini API in October and pointed everyone at Omni instead. On November 5, Omni 1.2 Flash went generally available: clips up to 15 seconds with native audio, edits you make in conversation instead of re-rendering from scratch, and a 30 percent price cut. This post wires it into a Next.js app with code I typechecked against the real SDK, and works out what a finished clip actually costs you.

How this post was verified: the specs and prices come from Google's Omni developer guide and the official Gemini API pricing page, both read on November 7, 2026. Every code snippet below was installed next to Next.js 16.2.6 and typechecked against @google/genai 1.52.0, and one snippet from Google's own docs failed that check, which is section three. I did not run a live generation for this post, because no paid API key was available in my environment, so you will find no quality verdict here that I cannot back. Google published no benchmarks either. Judge the picture on your own prompts; judge the integration and the bill here.

What Gemini Omni 1.2 Flash actually is

It is Google's video generation and editing model for the Gemini API, model ID gemini-omni-1.2-flash, generally available since November 5, 2026 on the paid tier. You feed it text, images, or video, and it returns video with synchronized native audio: 3 to 15 seconds per generation, up to 6 reference images, and follow-up edits that build on the clip you already made.

The architectural point most launch coverage skipped: Omni does not run on generateContent like the Gemini text and image models you already know. It runs on the Interactions API, a stateful endpoint where every generation returns an interaction ID and later calls can reference it. That one decision shapes your whole integration. Generation, conversational editing, and clip extension are the same call with different inputs, and the thing you persist in your database is not a file, it is the interaction ID.

Against Omni 1.1 Flash, three things changed. Clips grew from a 10-second cap to 15 seconds, in one-second steps. Video output pricing dropped from $17.50 to $12 per million tokens, which is the 30 percent cut in the headlines. And if your code still calls gemini-omni-1.1-flash, nothing breaks on December 3, 2026: Google routes those requests to 1.2 automatically and bills them at the lower 1.2 rates.

Two honest caveats before the code. Google says character consistency, lip-sync, and camera control improved, and it published no benchmarks to support any of that, so treat those as claims to test, not results. And every output carries an invisible SynthID watermark, which matters if your client needs unwatermarked masters for broadcast work.

The setup: from paid API key to a playing clip

You need four pieces: a paid-tier API key (there is no free tier for this model), the @google/genai SDK, one route handler that creates the interaction, and one small helper that pulls the video out of the response. A second route handler adds conversational editing. Everything below is one minimal App Router project, and it typechecks as a set.

Step 1: Get a key that can actually call the model

Go to Google AI Studio and create an API key on a project with billing attached. I am putting this first because it is where most people will burn their first hour: the pricing page lists the free tier as "Not available" for both input and output on gemini-omni-1.2-flash, so a free-tier key fails on this model no matter how correct your code is. Put the key in .env.local:

GEMINI_API_KEY=your_paid_tier_key 

Step 2: Install the SDK

npm install @google/genai 

That is the only new dependency. The Omni calls live in ai.interactions, next to the ai.models surface you may already use for text and images.

Step 3: The generate route

This is the whole server side of text-to-video: validate the prompt, clamp the duration to the supported 3 to 15 second window, create the interaction, and hand the video back as base64 your page can play immediately.

import { GoogleGenAI } from '@google/genai'; import { NextRequest, NextResponse } from 'next/server'; import { extractVideo } from '@/lib/extract-video'; // The @google/genai SDK needs the Node.js runtime, not Edge. export const runtime = 'nodejs'; // A generation call runs until the clip is rendered, which is far longer // than a default serverless timeout. On Vercel, raise the limit here // (Pro plans and up), or move the call into a background job. export const maxDuration = 300; const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); type GenerateVideoBody = { prompt?: string; durationSeconds?: number; // whole seconds, 3 to 15, default 10 aspectRatio?: '16:9' | '9:16'; resolution?: '360p' | '720p' | '1080p' | '4K'; }; export async function POST(request: NextRequest) { const body = (await request.json()) as GenerateVideoBody; const prompt = body.prompt?.trim(); if (!prompt) { return NextResponse.json({ error: 'prompt is required' }, { status: 400 }); } const seconds = Math.min(15, Math.max(3, Math.round(body.durationSeconds ?? 10))); const interaction = await ai.interactions.create({ model: 'gemini-omni-1.2-flash', input: prompt, response_format: { type: 'video', duration: `${seconds}s`, aspect_ratio: body.aspectRatio ?? '16:9', resolution: body.resolution ?? '720p', }, }); const video = extractVideo(interaction); if (!video?.data) { return NextResponse.json( { error: 'model returned no video', interactionId: interaction.id }, { status: 502 }, ); } return NextResponse.json({ interactionId: interaction.id, mimeType: video.mime_type ?? 'video/mp4', videoBase64: video.data, }); } 

Three response_format fields do the real work. duration takes a string from "3s" to "15s" and defaults to "10s" when you leave it out; a value in response_format overrides any length you mention in the prompt text, so set it here and keep your prompts about the scene. aspect_ratio is "16:9" by default or "9:16" for portrait. resolution is where the money hides: 360p and 720p are the native renders, while 1080p and 4K are upsampled from them and billed at higher token rates, which the cost section prices out.

The extractVideo helper is not decoration. Your first attempt at this route will follow Google's JavaScript examples, and it will not typecheck. The next section shows why, and the fix is fifteen lines.

Step 4: Play the clip in the browser

The route returns base64, so the client is a form, a fetch, and a video element fed by a data URL. This is the complete page component from the same project:

'use client'; import { useState } from 'react'; type VideoResult = { interactionId: string; mimeType: string; videoBase64: string; }; export default function VideoForm() { const [prompt, setPrompt] = useState( 'A single continuous shot of a paper plane gliding over a library table, warm window light, gentle camera drift. No dialogue.', ); const [seconds, setSeconds] = useState(8); const [instruction, setInstruction] = useState('Change the lighting to dusk.'); const [result, setResult] = useState<VideoResult | null>(null); const [busy, setBusy] = useState(false); const [error, setError] = useState<string | null>(null); async function call(url: string, payload: Record<string, unknown>) { setBusy(true); setError(null); try { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await res.json(); if (!res.ok) throw new Error(data.error ?? `request failed: ${res.status}`); setResult(data as VideoResult); } catch (err) { setError(err instanceof Error ? err.message : 'something went wrong'); } finally { setBusy(false); } } return ( <main style={{ maxWidth: 760, margin: '2rem auto', fontFamily: 'system-ui' }}> <h1>Omni video studio</h1> <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} rows={3} style={{ width: '100%' }} /> <label> Seconds (3-15):{' '} <input type="number" min={3} max={15} value={seconds} onChange={(e) => setSeconds(Number(e.target.value))} /> </label> <button disabled={busy} onClick={() => call('/api/generate-video', { prompt, durationSeconds: seconds })} > {busy ? 'Generating...' : 'Generate clip'} </button> {error && <p role="alert">{error}</p>} {result && ( <> <video controls style={{ width: '100%', marginTop: '1rem' }} src={`data:${result.mimeType};base64,${result.videoBase64}`} /> <p>Interaction: {result.interactionId}</p> <input value={instruction} onChange={(e) => setInstruction(e.target.value)} style={{ width: '70%' }} /> <button disabled={busy} onClick={() => call('/api/edit-video', { interactionId: result.interactionId, instruction, }) } > Edit this clip </button> </> )} </main> ); } 

Base64 in JSON is the right call for a first integration and the wrong call at scale: it inflates the payload by about a third, and the docs recommend switching response_format to delivery: 'uri' for videos over 4 MB, which means anything above 720p. When you make that switch, the same content part carries a uri instead of data, and your job becomes downloading and storing the file rather than passing it through.

Step 5: Edit the clip in conversation

Editing is the feature that justifies this API's shape. The follow-up call passes the earlier interaction ID and a one-line instruction; you never re-upload the clip, and you get back a new interaction whose ID you keep for the next turn.

import { GoogleGenAI } from '@google/genai'; import { NextRequest, NextResponse } from 'next/server'; import { extractVideo } from '@/lib/extract-video'; // The @google/genai SDK needs the Node.js runtime, not Edge. export const runtime = 'nodejs'; export const maxDuration = 300; const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); type EditVideoBody = { interactionId?: string; instruction?: string; }; export async function POST(request: NextRequest) { const body = (await request.json()) as EditVideoBody; const instruction = body.instruction?.trim(); if (!body.interactionId || !instruction) { return NextResponse.json( { error: 'interactionId and instruction are required' }, { status: 400 }, ); } // Conversational editing: the follow-up call references the earlier // interaction instead of re-uploading the clip, and returns a new // interaction whose id you keep for the next turn. const interaction = await ai.interactions.create({ model: 'gemini-omni-1.2-flash', previous_interaction_id: body.interactionId, input: `${instruction} Keep everything else the same.`, }); const video = extractVideo(interaction); if (!video?.data) { return NextResponse.json( { error: 'model returned no video', interactionId: interaction.id }, { status: 502 }, ); } return NextResponse.json({ interactionId: interaction.id, mimeType: video.mime_type ?? 'video/mp4', videoBase64: video.data, }); } 

Google's prompt guide is blunt about how to write these instructions: one change per turn, kept simple, with "Keep everything else the same" appended, which is why the route adds it for you. A paragraph-long edit brief produces edits you did not ask for.

The production warning I would tattoo on this route

The create call waits for the render. That is fine in a demo and dangerous as a public endpoint: a user who double-clicks Generate has just bought two renders, and a platform timeout kills the request while the bill stands. Past the prototype stage, I would put this call behind a queue, store the interaction ID against the job, and let the user poll your own status endpoint. That is the same job pattern I built for BullMQ retries and dead-letter queues in NestJS, and the idempotency lesson transfers directly: one user action, one paid generation, no duplicates. One related switch to know about: the docs suggest store: false for faster synchronous calls, but a clip stored nowhere cannot be edited later by previous_interaction_id. Speed now or editability later; pick per feature, not by accident.

The trap in Google's own JavaScript examples

Google's Omni guide shows the finished video being read from interaction.output_video.data in the JavaScript tab. When I typechecked that pattern against @google/genai 1.52.0, it failed: the JavaScript SDK's Interaction type has no output_video property at all. That convenience field exists in the Python SDK. In JavaScript, the response carries an outputs array of content parts, and the video is the part whose type is 'video'.

So the extraction is a helper you write once and reuse in every route:

import type { Interactions } from '@google/genai'; type Interaction = Interactions.Interaction; type VideoContent = Interactions.VideoContent; // In the JavaScript SDK the finished video is not a top-level field on // the interaction. It is the content part with type 'video' inside // interaction.outputs - base64 in `data`, or a downloadable `uri` when // you asked for URI delivery. export function extractVideo(interaction: Interaction): VideoContent | null { const video = interaction.outputs?.find( (part): part is VideoContent => part.type === 'video', ); return video ?? null; } 

If you are calling the REST endpoint directly instead of the SDK, expect a third shape: the video sits in the steps array, in the model_output step, as a base64 video/mp4 content entry. Three surfaces, three shapes, one model. The working rule I take from this: vendor documentation is written per language and drifts between them, so I typecheck every vendor snippet against the installed package before it enters a codebase. The typecheck is five seconds; debugging a silent undefined in production is not.

What one clip really costs

Video output is billed per token, and the token count depends on resolution: 5,792 tokens per second at 720p, charged at $12 per million. That is $0.0695 per second, which Google rounds to about $0.07. A 15-second 720p clip is 86,880 output tokens, or about $1.04, before the small input charge for your prompt and references at $1.50 per million tokens.

Here is the full ladder from the pricing page, with the per-clip math done:

Resolution Tokens per second Per second 8-second clip 15-second clip
360p 1,931 $0.023 $0.19 $0.35
720p 5,792 $0.07 $0.56 $1.04
1080p (upsampled) 8,688 $0.104 $0.83 $1.56
4K (upsampled) 17,376 $0.209 $1.67 $3.13

If you want this math inside your own app, for budgets or usage-based billing, it is one small function:

// Rates from the official Gemini API pricing page, checked 2026-11-07. // Video output is billed per token; the token count per second depends // on resolution. 1080p and 4K are upsampled from the native render. export const VIDEO_TOKENS_PER_SECOND = { '360p': 1931, '720p': 5792, '1080p': 8688, '4K': 17376, } as const; export type VideoResolution = keyof typeof VIDEO_TOKENS_PER_SECOND; export const VIDEO_OUTPUT_USD_PER_MILLION_TOKENS = 12; export function clipCostUsd(resolution: VideoResolution, seconds: number): number { const tokens = VIDEO_TOKENS_PER_SECOND[resolution] * seconds; return (tokens * VIDEO_OUTPUT_USD_PER_MILLION_TOKENS) / 1_000_000; } 

Now the number that actually decides whether your feature makes money: the cost per accepted clip, not per generation. Generation is probabilistic. If one take in three is usable, your 15-second keeper at 720p cost you about $3.13, not $1.04. Two habits keep that survivable. Draft at 360p, where a 15-second take costs $0.35, and only render the winning prompt at 720p. And remember that a conversational edit is a fresh render at full price: editing saves you the re-briefing and keeps the scene consistent, but a 15-second edit at 720p is another $1.04, not a discount. There is also no Batch rate listed for this model on the pricing page as of November 7, so the usual 50 percent batch trick does not apply here.

Omni 1.2 Flash vs Veo 3.1 vs Runway Gen-4.5

If your app talks to the Gemini API, this comparison starts with a funeral: the Veo 3.1 preview models on that API shut down on October 22, 2026, and Omni is the replacement Google names. Veo still exists on Google Cloud's Gemini Enterprise Agent Platform, and Runway Gen-4.5 is the strongest option outside Google. So the real question is not which logo wins; it is which platform your app should live on.

Gemini Omni 1.2 Flash Veo 3.1 (Google Cloud) Veo 3.1 Fast (Google Cloud) Runway Gen-4.5
API route Gemini API, one API key Gemini Enterprise Agent Platform Gemini Enterprise Agent Platform Runway API
Longest single clip 15 seconds 8 seconds 8 seconds 10 seconds
Native audio Yes Yes Yes No
720p cost per second About $0.07 $0.40 $0.10 $0.12
Conversational editing Built in, via interaction IDs No No No
Status GA, November 5, 2026 GA on Cloud; Gemini API previews shut down October 22, 2026 GA on Cloud GA

Prices checked November 6-7, 2026 against each vendor's published rates. Runway bills 12 credits per second on its API at $0.01 per credit, which is where its $0.12 comes from.

Read the table by constraint, not by brand. Omni wins on clip length, editing loop, and price per second, and it is the only one of the four your existing Gemini API key can call today. Veo 3.1 standard tier is the quality lane for teams already running on Google Cloud, at nearly six times Omni's per-second price. Runway earns its place when your team already lives in its editor and the API is a delivery mechanism, not the product.

The tools around the model, for reference:

Tool What it does in this setup Link
Google AI Studio Get the paid API key, prototype prompts before writing code https://aistudio.google.com
Gemini API pricing pag...Source:https://dev.to/zforzain2000/gemini-omni-12-flash-in-nextjs-the-setup-and-the-real-cost-per-clip-kj5

Related posts