Dev.to AI 🤖 Ai 👁 0 📖 17 min read

Gemini Omni 1.2 Flash in Next.js: The Setup and the Real Cost Per Clip

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

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 page The source for every rate quoted in this post https://ai.google.dev/gemini-api/docs/pricing
Gemini Omni developer guide Official patterns for generation, editing, and extension https://ai.google.dev/gemini-api/docs/omni
Google Cloud console The route to Veo 3.1 if you outgrow the API key https://console.cloud.google.com
Runway The non-Google alternative, Gen-4.5 on API credits https://runway.com/pricing

{/* MONETIZATION NOTE: no affiliate links are live in this table yet. If affiliate or partner programs for Google Cloud or Runway are joined later, swap the plain URLs above for tracked ones at these rows. */}

The limits that will bite you

None of these are in the launch headlines, and every one of them is in the official guide's limitations section, which is worth more of your trust than the announcement:

  • First and last frame interpolation only supports 3 to 10 seconds. Ask for a longer clip with two frames and you get an invalid argument error, not a longer clip.
  • Extension adds 3 to 10 seconds per turn, up to a 30-second total, and it only appends to the end, using the last 10 seconds as context. There is no prepending and no mid-clip extension.
  • Reference rules shift with duration: clips of 11 to 15 seconds accept one reference video (up to 3 seconds), clips of 3 to 10 seconds accept up to three, and the 6 reference images are allowed at any duration.
  • Audio references are not supported in the API today, and neither is voice editing. You can generate dialogue; you cannot hand the model a voice to clone or fix a line of speech after the fact.
  • There are no negative prompts, temperature, top_p, or system instructions on this model. Put exclusions in the prompt itself, in plain words.
  • Editing or extending uploaded videos is unavailable to users in the EEA, Switzerland, and the UK; editing model-generated videos works in every supported region.
  • Google's own stated weak spots are fast sports action, rapid script changes, and complex physics. If your product concept lives in one of those, budget for a much lower keeper rate than the math above assumes.

My verdict: what I would ship

If I were adding video generation to a Next.js product this week, I would build it on Gemini Omni 1.2 Flash, and it is not close. It is the only video model a Gemini API key can call now that the Veo previews are gone, it gives you the longest single clips of the four, the editing loop is a genuine workflow advantage rather than a demo trick, and at about $0.07 a second it is the cheapest 720p generation here. For a product demo generator, a UGC ad variant tool, or social clips inside a SaaS, this is the default pick.

The answer flips in three situations. If your company already runs on Google Cloud and needs Veo's request-level controls and multiple outputs per call, stay on the platform and pay Veo's prices. If the video is the product and your team works in a creative editor all day, Runway's ecosystem matters more than a five-cent-per-second gap. And if your concept needs unbroken footage longer than 15 seconds as a single render, none of these four is your tool yet; you are stitching extensions or waiting.

One last honesty note, because it decides how much of this post you should weight: the integration above is typechecked and the prices are from Google's own page, but I have not seen this model's output with my own eyes, and Google shipped no benchmarks for you to lean on either. Run your ten hardest prompts through it at 360p for about two dollars total before you design pricing around it. That experiment is worth more than any launch post, including this one.

If you would rather hand this integration to someone who has already mapped the traps, that is the work I take on: production Next.js and NestJS builds where the API billing math is done before the first deploy, not after the first invoice. Tell me about your project.

FAQ

What is the model ID for Gemini Omni 1.2 Flash?

gemini-omni-1.2-flash. It went generally available on November 5, 2026 on the paid tier of the Gemini API, and you call it through the Interactions API, not generateContent.

Is there a free tier for Gemini Omni 1.2 Flash?

No. Google's pricing page lists the free tier as not available for both input and output on this model. You need a Gemini API key attached to a paid billing setup before the first call succeeds.

How much does a 15-second clip cost?

About $1.04 of video output at 720p: 15 seconds at 5,792 tokens per second is 86,880 tokens, billed at $12 per million. The same clip costs about $0.35 at 360p and $1.56 upsampled to 1080p. Input tokens for your prompt and references come on top.

Does Omni use the generateContent API like other Gemini models?

No. Omni runs on the Interactions API. In the @google/genai SDK that is ai.interactions.create, and the finished video comes back inside interaction.outputs as the content part with type video, not as a top-level field.

Can I edit a clip after generating it instead of starting over?

Yes. Call ai.interactions.create again with previous_interaction_id set to the earlier interaction and a plain-language instruction like 'Change the lighting to dusk. Keep everything else the same.' Each edit is a fresh render, so it bills as a new generation.

What happens to code using gemini-omni-1.1-flash on December 3, 2026?

Nothing breaks. Google automatically routes requests to gemini-omni-1.1-flash to gemini-omni-1.2-flash from December 3, 2026 and bills them at the 1.2 rates, with no code change required.

📰 Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.