/** * flow-upscale-finish.ts — the free Google Flow 1080p finish, applied to a clip * we just generated on a Flow route. * * ## Why this exists * * `providers/google-flow.ts` has had a working `upscaleFlowVideo()` since June * 2026 with **no production caller** — every Flow render shipped at the * resolution it was generated at while a free 1080p upsample sat one call away. * Google's Veo upsampler synthesizes real detail (skin, dust, edges) rather than * sharpening, so it beats any local filter on the same source, and it costs * nothing at 720p/1080p on a paid plan. * * ## The constraint that shapes everything here * * `POST /videos/upscale` accepts **only a mediaGenerationId Flow itself * generated**. Uploading a finished clip as an asset succeeds and then upscale * returns `400 INVALID_ARGUMENT` — so there is NO API route to upscale a * hand-edited cut, a stitched master, or a clip from Seedance/Runway/Dreamina. * That is why this runs **per clip at the generation stage** and why the * file-based `finish` lane (Topaz / Real-ESRGAN / Magnific) still exists for * everything else. The two are complementary, not alternatives. * * ## Rules this module encodes * * - **Never fatal.** A failed upscale keeps the original clip and reports the * reason. The render was already CHARGED; losing it to a free finishing step * would be the worst possible trade. * - **Idempotent.** Re-upscaling the same id returns Google's cached result and * costs nothing, so the resumable drivers can re-enter a scene safely. * - **Free tiers only by default.** 720p and 1080p cost 0 credits; `4K` costs 50 * and needs Ultra, so it is never the automatic choice. * - **A 360p clip can go straight to 1080p** — it does not have to stop at 720p. * * Spec: https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos-upscale */ import { type GoogleFlowUpscaleResolution } from './providers/google-flow.js'; import { type FlowMediaFetchLike } from './flow-media-url.js'; /** Default finish target: free on a paid plan, and reachable directly from 360p. */ export declare const FLOW_FINISH_DEFAULT_RESOLUTION: GoogleFlowUpscaleResolution; export interface FlowUpscaleFinishInput { /** Flow-GENERATED video id. An uploaded/edited clip cannot be upscaled. */ mediaGenerationId: string; /** Where the generated clip already sits; overwritten in place on success. */ outputPath: string; apiToken: string; /** Target resolution (default 1080p). '4K' spends 50 credits and needs Ultra. */ resolution?: GoogleFlowUpscaleResolution; /** * Injectable fetch. Typed as the media-fetch shape because this module both * POSTs the upscale and DOWNLOADS bytes; that interface is the superset (it * carries `arrayBuffer`), and it is cast down for the submit call. */ fetchImpl?: FlowMediaFetchLike; sleepImpl?: (ms: number) => Promise; /** Set false to skip entirely (env/flag opt-out). Default true. */ enabled?: boolean; /** * Injectable ffprobe. Used ONLY to confirm the upscale did not drop the audio * track — these are lip-sync lanes, and a silent HD clip is worse than a 720p * one with sound. Omit to use the real probe; a probe that is unavailable or * throws is treated as "cannot tell" and the swap stands. */ probeImpl?: (path: string) => Promise<{ hasAudio?: boolean; durationSec?: number; }>; } export interface FlowUpscaleFinishResult { /** True only when the file on disk was actually replaced with the upscale. */ upscaled: boolean; resolution?: GoogleFlowUpscaleResolution; /** The upscaled clip's own Flow id (chainable), when we got one. */ mediaGenerationId?: string; videoUrl?: string; /** Why it did not happen — 'disabled', 'no-media-id', or the failure text. */ skippedReason?: string; } /** * Read the opt-out. `VCLAW_FLOW_UPSCALE=0|false|no|off` disables the automatic * finish; anything else (including unset) leaves it ON, because the operator's * standing instruction is that every Flow clip gets the free upscale. */ export declare function flowUpscaleEnabled(env?: NodeJS.ProcessEnv): boolean; /** Read the target from `VCLAW_FLOW_UPSCALE_RESOLUTION`, defaulting to 1080p. */ export declare function resolveFlowFinishResolution(env?: NodeJS.ProcessEnv): GoogleFlowUpscaleResolution; /** * The target for an AUTOMATIC post-render finish — always a free tier. * * `4K` costs 50 credits and needs Ultra, so it must never ride in on an * environment variable: one `VCLAW_FLOW_UPSCALE_RESOLUTION=4k` left in a shell * profile would otherwise spend 50 credits on every clip of every subsequent * render, unprompted and invisible in the plan. Asking for 4K is a decision, and * it belongs at the explicit `vclaw video finish --flow-resolution 4K` call * site, where `--confirm-spend` gates it. */ export declare function resolveAutomaticFlowFinishResolution(env?: NodeJS.ProcessEnv): GoogleFlowUpscaleResolution; /** True when the target costs no credits (720p/1080p) — i.e. needs no spend gate. */ export declare function isFreeFlowUpscale(resolution: GoogleFlowUpscaleResolution): boolean; /** * Upscale a just-generated Flow clip and overwrite it in place. * * NEVER throws for an upscale failure: the caller has a charged, valid clip on * disk and must keep it. The result says whether the swap happened, so a caller * that records per-clip dimensions can report a mixed-resolution timeline rather * than assuming every clip rose together. */ export declare function finishFlowClip(input: FlowUpscaleFinishInput): Promise; /** A flag-validation failure carrying the CLI error code the handler should use. */ export interface FlowFinishFlagError extends Error { code?: 'missing_required_flag' | 'invalid_flag_value'; details?: Record; } /** * Validate the `vclaw video finish` flags for (and against) the `google-flow` * backend, returning the resolved upscale target. * * The asymmetry it enforces: google-flow takes `--media-id` and NO `--input`, * because Google upscales a clip Flow itself generated, addressed by id — an * uploaded or stitched file is rejected with 400 INVALID_ARGUMENT. Every other * backend is file-based and must not accept `--media-id`. Mismatched flags are * rejected rather than ignored, matching gen-image's strict handling of * backend-incompatible flags. */ export declare function validateFlowFinishFlags(input: { backend: string; mediaIdBackend: boolean; input?: string | undefined; output?: string | undefined; mediaId?: string | undefined; resolutionFlag?: string | undefined; }): GoogleFlowUpscaleResolution | undefined; /** * Does a `finish --backend google-flow` run actually spend credits? * * Only `4K` does (50 credits, Ultra-only). 720p and 1080p are free on a paid * plan, and gating a free operation behind `--confirm-spend` would just train * the operator to pass the flag reflexively — which is how a real spend gate * stops meaning anything. */ export declare function flowFinishCostsCredits(resolution: GoogleFlowUpscaleResolution | undefined): boolean; //# sourceMappingURL=flow-upscale-finish.d.ts.map