●GA — Gemini Omni Flash reached general availability on August 27 as gemini-omni-1.1-flash, the conversational video generation and editing model●EXTEND — You can now continue an existing clip by generating past its end, either through the extend task or straight from a prompt, working around the short duration limit●RESOLUTION — video_config gains a resolution parameter offering 360p, 720p, 1080p, and 4k. The two highest tiers are produced by upscaling, so results vary with the source●DEPRECATION — gemini-omni-flash-preview shuts down on September 30, so any production code still pointing at the preview endpoint needs to move over●TRANSCRIBE — Gemini 3.5 Transcribe and Transcribe Live went GA on August 26 with language detection across 85+ languages, speaker diarization, and word-level timestamps●SHUTDOWN — gemini-robotics-er-1.6-preview retires on August 31, two days from now, with the ER 2 line in public preview since July 30 as the migration path●GA — Gemini Omni Flash reached general availability on August 27 as gemini-omni-1.1-flash, the conversational video generation and editing model●EXTEND — You can now continue an existing clip by generating past its end, either through the extend task or straight from a prompt, working around the short duration limit●RESOLUTION — video_config gains a resolution parameter offering 360p, 720p, 1080p, and 4k. The two highest tiers are produced by upscaling, so results vary with the source●DEPRECATION — gemini-omni-flash-preview shuts down on September 30, so any production code still pointing at the preview endpoint needs to move over●TRANSCRIBE — Gemini 3.5 Transcribe and Transcribe Live went GA on August 26 with language detection across 85+ languages, speaker diarization, and word-level timestamps●SHUTDOWN — gemini-robotics-er-1.6-preview retires on August 31, two days from now, with the ER 2 line in public preview since July 30 as the migration path
Should Omni Flash Hand You the 4K, or Should You Upscale at the Last Step?
In Gemini Omni Flash, 1080p and 4K are upscaled outputs. Here is how to pick the upscale point by working backwards from your delivery target, plus a script that checks whether the detail matches the nominal resolution.
Every time a new iPhone ships, I add another branch to the resolution logic in my wallpaper apps. More points on screen means different real pixels in the file I hand over, so no matter how carefully the source is built, the last step is always cutting it to fit the bucket on the receiving end. On Android I once lost resources entirely to density splitting and had to move them into drawable-nodpi/ to get them back.
What decides the resolution of a delivered asset is not the source. It is the condition on the receiving side. As an indie developer, getting that order backwards turns straight into lost hours.
Now that gemini-omni-1.1-flash is generally available, you can pick a video output resolution anywhere from 360p to 4k. But the documentation is explicit that 1080p and 4K are produced by upscaling. Reaching for "4K, just to be safe" repeats exactly the mistake I made with wallpapers.
resolution lives in response_format, not video_config
The first thing to trip over is where the parameter goes. Resolution belongs in response_format, not in generation_config.video_config. What goes into video_config is task — one of text_to_video, image_to_video, reference_to_video, edit, or extend.
Put it in the wrong place and the request still succeeds. You simply get the default 720p back. There is no error to catch it, so run ffprobe on your very first output.
import base64from google import genaiclient = genai.Client()interaction = client.interactions.create( model="gemini-omni-1.1-flash", input="A drone shot of a mountain landscape at sunrise.", response_format={ "type": "video", "aspect_ratio": "9:16", # 16:9 is the default, so portrait must be explicit "resolution": "1080p", # here, not in video_config },)with open("hires.mp4", "wb") as f: f.write(base64.b64decode(interaction.output_video.data))
Here is what the values mean.
Value
Output
How it is produced
360p
360p
Native
720p
720p (default)
Native
1080p
1080p
Upscaled
4k
4K
Upscaled
So the native ceiling of this API is 720p. The 1080p and 4K options are names attached to a stretched version of that. The full table lives in the Gemini Omni Flash documentation.
What 4K adds, and what it does not
Knowing the word "upscaling" is not the same as knowing what it costs you. Before spending API calls, I checked the property on synthetic footage at my desk.
Three clips, all two seconds, 24fps, CRF 18, all with a nominal size of 3840x2160:
A frame full of fine stripes and hard edges, authored at 3840x2160 from the start (native 4K)
The same picture authored at 1280x720 and stretched to 3840x2160 with Lanczos (upscaled 4K)
A smooth gradient with almost no fine detail, authored at 3840x2160 (native 4K, but flat)
Read them with ffprobe and all three report 3840,2160. The container metadata gives you nothing. The bitrate, on identical encoder settings, splits them wide open.
Clip
Nominal size
Bitrate
File size (2s)
Native 4K (detailed)
3840x2160
17.19 Mbps
4,299,566 B
Upscaled 4K
3840x2160
7.25 Mbps
1,812,869 B
Native 4K (flat)
3840x2160
0.59 Mbps
147,756 B
At the same size and the same CRF, the upscaled clip lands at roughly 42% of the native bitrate. An encoder does not spend bits on detail that is not there, which is obvious in hindsight and still worth measuring. Asking for 4K increases dimensions, not information.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to lock the delivery requirement before choosing a resolution, so you never ship a 4K file that gained dimensions and nothing else
✦You will be able to tell, with ffmpeg and about 40 lines of Python, whether a returned mp4 actually carries detail worth its nominal resolution
✦You will be able to decide resolution by payload size rather than by picture quality, using the fact that upscaled output lands at under half the bitrate at the same CRF
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Checking whether the detail matches the nominal size
If the dimensions are identical, look at the frequency domain instead. Footage stretched up from 720p carries almost no energy above the 720p Nyquist frequency. Measure that band and you can tell whether the nominal size is backed by real detail.
"""Judge whether detail matches the nominal resolution, relative to a 720p reference.Usage: python3 native_check.py candidate.mp4 reference_720p.mp4"""import subprocessimport sysimport numpy as npdef grab_frame(path, at="00:00:01"): """Pull one frame at the given timestamp as raw grayscale.""" wh = subprocess.run( ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0", path], capture_output=True, text=True, check=True).stdout.strip().split(",") w, h = int(wh[0]), int(wh[1]) raw = subprocess.run( ["ffmpeg", "-v", "error", "-ss", at, "-i", path, "-frames:v", "1", "-pix_fmt", "gray", "-f", "rawvideo", "-"], capture_output=True, check=True).stdout return np.frombuffer(raw, np.uint8).reshape(h, w).astype(np.float64), w, hdef detail_density(frame, w, h): """High-frequency energy per pixel. Divide by area rather than by total energy so clips of different sizes stay comparable.""" win = np.hanning(h)[:, None] * np.hanning(w)[None, :] # kill edge discontinuity spec = np.abs(np.fft.fftshift(np.fft.fft2(frame * win))) ** 2 fy = (np.arange(h) - h // 2) / h fx = (np.arange(w) - w // 2) / w r = np.sqrt(fy[:, None] ** 2 + fx[None, :] ** 2) band = spec[(r > 0.25) & (r <= 0.5)].sum() # the band stretching leaves empty return float(band / (w * h))if __name__ == "__main__": cf, cw, ch = grab_frame(sys.argv[1]) rf, rw, rh = grab_frame(sys.argv[2]) cd = detail_density(cf, cw, ch) rd = detail_density(rf, rw, rh) gain = cd / rd if rd > 0 else float("inf") label = "upscaled" if gain < 3 else "detail matches size" print(f"{sys.argv[1]}: nominal {cw}x{ch} / detail ratio {gain:.2f}x -> {label}")
Run against the three clips, using a 720p render of the same picture as the reference:
Clip
Detail density vs 720p reference
Verdict
Native 4K (detailed)
44.72x
detail matches size
Upscaled 4K
0.46x
upscaled
Native 4K (flat)
0.01x
upscaled
Nearly a 97x spread between the detailed native clip and the upscaled one. That is a gap you can act on without squinting at frames.
My first version had no reference clip. It computed the share of total energy sitting above the cutoff and called anything under 0.01 an upscale. The results:
Clip
High-band share
Verdict at threshold 0.01
Native 4K (detailed)
0.00607
false positive
Upscaled 4K
0.00011
upscaled
Native 4K (flat)
0.00000
false positive
All three came back as upscaled, and the third one — a genuine native 4K render — pinned at zero. A flat picture has nothing in the high band whether it was authored large or not.
That pushed me to change the question. What this measurement answers is not "was it authored natively" but "does this file carry detail worth its size" — and in practice, the second question is the one I actually need answered. So the reference line comes from outside now. Generate one extra 720p clip and every later comparison can reuse it.
I have a habit of reaching for absolute thresholds, because a single number in a config file feels tidy. But putting a threshold on a metric that moves with the material builds something that misjudges quietly, forever. I wrote about a similar observability trap in why the AI Studio developer log should not be your source of truth.
My assumption about double upscaling was wrong
The other thing I wanted to check was whether splitting the upscale across two steps degrades the picture. When the delivery target is 1080p, there are two routes:
Ask the API for 4k, then shrink to 1080p yourself — an enlargement plus a reduction
Take 720p as-is and enlarge once, right before delivery
I expected route 1 to come out softer. Building both and comparing them, route 2 measured 1.02x the detail density of route 1, and the PSNR between the two frames was 40.79 dB. That is not a difference anyone will see.
So the choice of upscale point is not a picture-quality decision. I want to be straight about that, because my prior was wrong. What actually differs is payload and wait time.
Same material, same CRF: the 720p clip is 402,983 B, the nominal 4K clip is 1,812,869 B. 4.50x. The Interactions API returns generated video as base64, so that difference lands directly in your response body — roughly 2.3 MiB versus 0.5 MiB. That is for two seconds of footage. Multiply by the number of takes you burn through and it stops being a rounding error.
So what does decide it
If picture quality is off the table, three things are left:
The size your delivery target accepts. When a store's preview video spec makes dimensions a condition of acceptance, there is nothing to decide
Payload. While iterating on takes, receiving 720p and normalizing once downstream is simply lighter
Whether a post-processing step already exists. If you are running ffmpeg anyway, that is where the upscale belongs
Writing this out as configuration keeps the answer stable across runs:
NATIVE_CEILING_H = 720 # the highest the API renders nativelydef choose_resolution(target_h: int, has_postprocess: bool) -> tuple[str, str]: """Return (resolution to request, where to upscale). target_h is the vertical pixel count the delivery target requires. """ if target_h <= 360: return "360p", "none" # a downscale alone will do if target_h <= NATIVE_CEILING_H: return "720p", "none" # native territory if has_postprocess: # ffmpeg runs regardless, so take the lighter payload return "720p", "local" # no post-processing stage: let the API match the required size return ("1080p" if target_h <= 1080 else "4k"), "api"for target_h, post in [(720, False), (1080, True), (1080, False), (2160, False)]: res, point = choose_resolution(target_h, post) print(f"target={target_h}p postprocess={post} -> resolution={res} upscale_at={point}")
The point is that upscale_at always resolves to exactly one place. Leave it ambiguous across several scripts and you end up with a path where the API stretches to 4K and a later stage stretches again. The quality loss turned out to be mild, as measured — but transfer volume and generation wait stack up honestly.
Required dimensions on the App Store and Google Play do change. Keep those numbers out of your code and in per-target configuration, so a store spec change means editing one place. I collected the device-resolution handling from my wallpaper work in generating wallpaper variations with Gemini 3.2 Flash Image Output.
Fix the resolution before you start extending
Omni Flash can also extend a clip. Prompt it to continue the scene and it generates a 3 to 10 second continuation. Videos it generated earlier are referenced with previous_interaction_id; your own footage goes in through the Files API.
If extension is part of the plan, settle the resolution when you cut the first shot. Changing resolution midway adds a normalization step later, and because one segment was shrunk to match while another was not, the texture can visibly shift at the seam.
The sequence I use:
Iterate on framing and timing at 360p, where cost and latency are lowest
Once the structure holds, move to 720p and run extensions at the same setting
Only after every shot exists, upscale once to each delivery target
Extension constraints are still being updated in the docs. Reading them before you build saves a round of rework.
delivery: "uri" still returns base64 on GET
There is one more behavior worth knowing on the retrieval side. Even when an interaction was created with delivery: "uri", GET /v1beta/interactions/{id} returns the video as inline base64 in the data field. The uri field is only guaranteed on the initial creation response or in the SSE stream.
If your design generates asynchronously and fetches by ID later, that is where an unexpected payload arrives — and if you asked for 4K, a heavy one.
The fix is simple: if you need the URI, capture it at creation time. Do not write code that assumes you can go back for it.
interaction = client.interactions.create( model="gemini-omni-1.1-flash", input="A drone shot of a mountain landscape at sunrise.", response_format={"type": "video", "resolution": "720p"},)# grab the uri now — a later GET hands back base64 insteadvideo = interaction.output_videouri = getattr(video, "uri", None)if uri: save_uri_to_manifest(interaction.id, uri)else: save_bytes(interaction.id, base64.b64decode(video.data))
Put the migration deadline on the calendar
gemini-omni-flash-preview shuts down on September 30, 2026. It entered public preview on June 30, so its whole life is about three months. If production code still points at the preview endpoint, you have roughly a month.
The swap itself is usually just a model name. But now that the default resolution is documented as 720p, anything that leaned on undocumented preview behavior may come out different. After swapping, run ffprobe once and confirm the dimensions. Shutdown dates are listed on the Gemini API deprecations page.
I have already had code stop working quietly when an image generation model was retired. Doing the work while the date is still published turned out to be the cheaper option.
Where to start
Generate one clip at 720p and keep it as your reference. Both the detail check and the upscale-point decision get easier the moment that file exists.
Choosing a resolution is not an operation that improves the picture. It is an operation that matches dimensions to what the receiving end will accept. Keep that order and both the cases where 4k is right and the cases where it is not become easy calls.
I have been doing exactly this with wallpaper delivery for years, and a new API still had me reasoning from the source side first. Thanks for reading.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.