--- title: Video editing | Luma Agents description: Edit existing videos with Ray 3.2 — source.generation_id, source URLs and inline video, strength, auto-controls, per-signal conditioning controls, and single- or multi-keyframe guide frames. --- Modify existing videos with natural-language prompts using `ray-3.2`. This guide covers every parameter for `type: "video_edit"` requests. For generating new videos from text or anchor images, see [Video generation](/guides/videos/generation/index.md). A video edit re-renders a previous video generation under a new prompt. Output settings like `resolution` and `duration` live under `video`; the conditioning that controls how much the edit preserves the source lives under `video.edit`. ## When to use video editing | Scenario | Use | Why | | ----------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | Restyle an existing video while preserving its motion | `video_edit` with `source.generation_id` | Re-renders the source under a new prompt | | Edit a local/private video file | `video_edit` with `source.url` or `source.data` (+ `source.media_type`) | References a hosted video URL or uploads inline base64 video when you do not have a prior generation | | Generate a new video that opens from an anchor image | `video` + `video.start_frame` | This is not an edit — it’s a generation seeded by an image. See [Video generation](/guides/videos/generation#image-to-video-with-anchor-frames/index.md) | ## Basic request `auto_controls: true` lets the model derive the conditioning schedule from the source video — the recommended default. Reference a previous video by passing that completed generation’s top-level `id` as `source.generation_id`. - [Python](#tab-panel-54) - [TypeScript](#tab-panel-55) - [Go](#tab-panel-56) - [cURL](#tab-panel-57) ``` from luma_agents import Luma client = Luma() # The source is a previous completed video generation. previous = client.generations.get("generation-id") generation = client.generations.create( model="ray-3.2", type="video_edit", prompt="Transform the scene into moonlit 35mm film footage", source={"generation_id": previous.id}, video={ "resolution": "720p", "edit": { "auto_controls": True, }, }, ) ``` ``` import Luma from "luma-agents"; const client = new Luma(); // The source is a previous completed video generation. const previous = await client.generations.get("generation-id"); const generation = await client.generations.create({ model: "ray-3.2", type: "video_edit", prompt: "Transform the scene into moonlit 35mm film footage", source: { generation_id: previous.id }, video: { resolution: "720p", edit: { auto_controls: true, }, }, }); ``` ``` // The source is a previous completed video generation. previous, err := client.Generations.Get(ctx, "generation-id") if err != nil { return err } generation, err := client.Generations.New(ctx, lumaagents.GenerationNewParams{ Model: lumaagents.F(lumaagents.ModelRay3_2), Type: lumaagents.F(lumaagents.GenerationNewParamsTypeVideoEdit), Prompt: lumaagents.F("Transform the scene into moonlit 35mm film footage"), Source: lumaagents.F(lumaagents.ImageRefParam{ GenerationID: lumaagents.F(previous.ID), }), Video: lumaagents.F(lumaagents.VideoOptionsParam{ Resolution: lumaagents.F(lumaagents.VideoResolution720p), Edit: lumaagents.F(lumaagents.VideoEditOptionsParam{ AutoControls: lumaagents.F(true), }), }), }) ``` Terminal window ``` curl -X POST https://agents.lumalabs.ai/v1/generations \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "ray-3.2", "type": "video_edit", "prompt": "Transform the scene into moonlit 35mm film footage", "source": { "generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" }, "video": { "resolution": "720p", "edit": { "auto_controls": true } } }' ``` ## Referencing the source video A `video_edit` request needs exactly one source video. Provide it three ways: **1. A prior Luma generation** — pass the completed generation’s top-level `id` as `source.generation_id`. It must be completed, belong to the same client, and have a video output: ``` { "source": { "generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" } } ``` **2. A hosted video URL** — pass a publicly reachable URL as `source.url`, with a `video/*` `source.media_type` such as `"video/mp4"`: ``` { "source": { "url": "https://example.com/source.mp4", "media_type": "video/mp4" } } ``` **3. Inline base64 video** — pass the encoded bytes as `source.data`, with a `video/*` `source.media_type`: ``` { "source": { "data": "AAAAIGZ0eXBtcDQy...", "media_type": "video/mp4" } } ``` `source.url` and `source.data` both require a `video/*` `source.media_type` (such as `"video/mp4"`) — the route uses the MIME prefix to choose the video ingest path before fetching any bytes, so a missing or non-`video/*` value is rejected. Source videos must be 18 seconds or shorter. Use inline `source.data` only for small media files. For anything larger, reference a prior `source.generation_id` or host the file and pass `source.url` instead of inlining. `image_ref` is rejected on `type: "video_edit"`; guide the edit through the prompt, optional `video.start_frame`, and `video.edit`. ## Edit controls — `video.edit` The `video.edit` object carries the video-to-video conditioning. It has three conditioning fields — `strength`, `auto_controls`, and `controls` — plus two multi-keyframe anchoring fields, `keyframes` and `keyframe_indexes` (see [Guide frames](#guide-frames) below). ### `auto_controls` Boolean. When `true`, the model derives the full conditioning schedule from the source video — the recommended default. When omitted, supplying `strength` or `controls` implies manual mode. Start with `auto_controls: true`. Reach for `strength` or `controls` only when you need finer control than auto mode gives you. `controls` cannot be combined with `auto_controls: true`. ### `strength` A preservation-vs-reimagination preset (`VideoEditStrength`). Nine values across three bands; lower numbers within each band are lighter, higher numbers stronger. Supplying `strength` implies manual mode unless `auto_controls` is explicitly set. | Wire value | Band | Behaviour | | ------------- | --------- | ---------------------------------------------------------------------- | | `adhere_1` | Adhere | Closely preserves source motion, composition, and structure | | `adhere_2` | Adhere | | | `adhere_3` | Adhere | Strongest adherence in the band | | `flex_1` | Flex | Balanced — preserves motion, lets the prompt reshape style and content | | `flex_2` | Flex | Common default for most edits | | `flex_3` | Flex | | | `reimagine_1` | Reimagine | The prompt drives most of the output; source provides loose grounding | | `reimagine_2` | Reimagine | | | `reimagine_3` | Reimagine | Strongest reimagining | ``` { "video": { "edit": { "strength": "flex_2" } } } ``` ### `controls` — per-signal conditioning For workflows that need finer control than `strength`, set `video.edit.controls` with per-signal objects. Each control has an `enabled` flag plus its own tuning parameter; omit a field to use the model default. `controls` cannot be combined with `auto_controls: true`. | Control | Fields | Notes | | ------------ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | | `pose` | `enabled` (boolean), `strength` (`precise` / `coarse`) | Pose / skeleton conditioning | | `depth` | `enabled` (boolean), `blur` (float 0–1) | Depth / scene-geometry conditioning. Higher `blur` allows more geometric freedom | | `normals` | `enabled` (boolean), `augmentation` (float 0–1) | Surface-normals conditioning. Higher `augmentation` allows more reinterpretation of surface geometry | | `trajectory` | `enabled` (boolean), `sparsity` (float 0–1) | Motion-trajectory conditioning. Higher `sparsity` uses fewer motion anchors | | `face` | `enabled` (boolean) | Face-identity conditioning | - [Python](#tab-panel-58) - [TypeScript](#tab-panel-59) - [Go](#tab-panel-60) - [cURL](#tab-panel-61) ``` generation = client.generations.create( model="ray-3.2", type="video_edit", prompt="Transform the scene into moonlit 35mm film footage", source={"generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851"}, video={ "resolution": "720p", "edit": { "controls": { "pose": {"enabled": True, "strength": "precise"}, "depth": {"enabled": True, "blur": 0.2}, "face": {"enabled": True}, }, }, }, ) ``` ``` const generation = await client.generations.create({ model: "ray-3.2", type: "video_edit", prompt: "Transform the scene into moonlit 35mm film footage", source: { generation_id: "d290f1ee-6c54-4b01-90e6-d701748f0851" }, video: { resolution: "720p", edit: { controls: { pose: { enabled: true, strength: "precise" }, depth: { enabled: true, blur: 0.2 }, face: { enabled: true }, }, }, }, }); ``` ``` generation, err := client.Generations.New(ctx, lumaagents.GenerationNewParams{ Model: lumaagents.F(lumaagents.ModelRay3_2), Type: lumaagents.F(lumaagents.GenerationNewParamsTypeVideoEdit), Prompt: lumaagents.F("Transform the scene into moonlit 35mm film footage"), Source: lumaagents.F(lumaagents.ImageRefParam{ GenerationID: lumaagents.F("d290f1ee-6c54-4b01-90e6-d701748f0851"), }), Video: lumaagents.F(lumaagents.VideoOptionsParam{ Resolution: lumaagents.F(lumaagents.VideoResolution720p), Edit: lumaagents.F(lumaagents.VideoEditOptionsParam{ Controls: lumaagents.F(lumaagents.AdvancedControlsParam{ Pose: lumaagents.F(lumaagents.PoseControlParam{Enabled: lumaagents.F(true), Strength: lumaagents.F(lumaagents.PoseControlStrengthPrecise)}), Depth: lumaagents.F(lumaagents.DepthControlParam{Enabled: lumaagents.F(true), Blur: lumaagents.F(0.2)}), Face: lumaagents.F(lumaagents.FaceControlParam{Enabled: lumaagents.F(true)}), }), }), }), }) ``` Terminal window ``` curl -X POST https://agents.lumalabs.ai/v1/generations \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "ray-3.2", "type": "video_edit", "prompt": "Transform the scene into moonlit 35mm film footage", "source": { "generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" }, "video": { "resolution": "720p", "edit": { "controls": { "pose": {"enabled": true, "strength": "precise"}, "depth": {"enabled": true, "blur": 0.2}, "face": {"enabled": true} } } } }' ``` ## Guide frames You can anchor the edit to specific frames in two mutually exclusive ways: a single guide frame, or a multi-keyframe set. ### Single guide frame — `video.start_frame` On `type: "video_edit"`, `video.start_frame` is accepted as a single guide frame for the edit (an `ImageRef`). `end_frame` is generation-only and rejected on edits. ### Multiple keyframes — `video.edit.keyframes` For finer anchoring, pin guide-frame images at arbitrary positions in the source video’s frame grid using two parallel arrays under `video.edit`: - **`keyframes`** — a list of guide-frame images (up to 64), each taking the same `ImageRef` shape as `source` / `image_ref[]` (a `url`, inline `data`, or a `generation_id`). - **`keyframe_indexes`** — a parallel list of non-negative, unique frame positions in the source video’s frame grid where each `keyframes[i]` is anchored. The two arrays must be the same length, and you provide both or neither — one without the other is a `400`. `keyframes` is the multi-anchor generalization of `video.start_frame`: setting `video.start_frame: X` is equivalent to `keyframes: [X], keyframe_indexes: [0]`, so the two are mutually exclusive — setting both on the same request is a `400`. Use one or the other. - [Python](#tab-panel-62) - [TypeScript](#tab-panel-63) - [cURL](#tab-panel-64) ``` generation = client.generations.create( model="ray-3.2", type="video_edit", prompt="Transform the scene into moonlit 35mm film footage", source={"generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851"}, video={ "resolution": "720p", "edit": { "keyframes": [ {"url": "https://example.com/frame-0.jpg"}, {"url": "https://example.com/frame-90.jpg"}, ], "keyframe_indexes": [0, 90], }, }, ) ``` ``` const generation = await client.generations.create({ model: "ray-3.2", type: "video_edit", prompt: "Transform the scene into moonlit 35mm film footage", source: { generation_id: "d290f1ee-6c54-4b01-90e6-d701748f0851" }, video: { resolution: "720p", edit: { keyframes: [ { url: "https://example.com/frame-0.jpg" }, { url: "https://example.com/frame-90.jpg" }, ], keyframe_indexes: [0, 90], }, }, }); ``` Terminal window ``` curl -X POST https://agents.lumalabs.ai/v1/generations \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "ray-3.2", "type": "video_edit", "prompt": "Transform the scene into moonlit 35mm film footage", "source": { "generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" }, "video": { "resolution": "720p", "edit": { "keyframes": [ { "url": "https://example.com/frame-0.jpg" }, { "url": "https://example.com/frame-90.jpg" } ], "keyframe_indexes": [0, 90] } } }' ``` ## HDR on edits `video.hdr` and `video.exr_export` apply to edits the same way they do to generation. HDR requires `720p` or `1080p` (not `540p`); `exr_export` requires `hdr: true`. HDR edits bill at a higher rate. ## Aspect ratio on edits The output preserves the source video’s aspect ratio. Setting `aspect_ratio` on a `video_edit` request is silently ignored — same behaviour as on image edits. ## Validation rules | Rule | Constraint | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `model` | Must be `ray-3.2` | | `type` | Must be `"video_edit"` | | `prompt` | Required, 1–6,000 characters | | `source` | Required. Provide exactly one of `source.generation_id`, `source.url`, or `source.data` | | `source.generation_id` | UUID of a prior completed video generation owned by the same client. No upload size limit — references stored media | | `source.url` | Publicly reachable video URL. Requires `source.media_type` set to a `video/*` MIME (e.g. `"video/mp4"`). Max 200 MB, ≤18s | | `source.data` | Inline base64 video. Requires `source.media_type` set to a `video/*` MIME. Max 200 MB, ≤18s | | `image_ref` | Rejected on `video_edit` | | `video.resolution` | One of `360p`, `540p`, `720p`, `1080p`, or omitted (defaults `720p`). `360p` is the draft tier; `360p`/`540p` rejected with `hdr: true` | | `video.loop` | Rejected on `video_edit` — create-only | | `video.end_frame` | Rejected on `video_edit` — generation-only | | `video.start_frame` | Optional single guide frame (`ImageRef`). Mutually exclusive with `video.edit.keyframes` | | `video.edit.strength` | One of the 9 `VideoEditStrength` values, or omitted | | `video.edit.auto_controls` | Boolean. Cannot combine with `video.edit.controls` | | `video.edit.controls` | Per-signal objects (`pose`, `depth`, `normals`, `trajectory`, `face`). Cannot combine with `auto_controls: true` | | `video.edit.keyframes` | Up to 64 guide-frame `ImageRef`s. Provide with `keyframe_indexes` (same length). Mutually exclusive with `video.start_frame` | | `video.edit.keyframe_indexes` | Parallel list of non-negative, unique source-frame positions; same length as `keyframes` | | `video.hdr` / `exr_export` | `hdr` requires `720p`/`1080p`; `exr_export` requires `hdr: true` | | `aspect_ratio` | Silently ignored — derived from source | See [Error handling](/guides/error-handling/index.md) for the exact `detail` strings rejected requests produce. ## Response A successful submit returns HTTP 201 with the same envelope shape used for video generations. Poll `GET /v1/generations/{id}` until `state` is `completed` or `failed`. On `completed`, `output[].url` carries the presigned MP4 URL with a 1-hour expiry. Keep the generation’s top-level `id` for any further edits or reframes. ## Pricing Ray 3.2 video edits are priced by duration (separate 5s and 10s rates) at the video-edit tier, which differs from the generation tier. See [Pricing — `ray-3.2` per-video pricing](/guides/pricing#ray-32--per-video-pricing/index.md). ## Next steps - [**Video generation**](/guides/videos/generation/index.md) — Generate new videos from text or anchor images - [**Video reframing**](/guides/videos/reframing/index.md) — Reframe an existing video to a new aspect ratio - [**Models**](/guides/model/index.md) — Capability matrix for every model - [**Pricing**](/guides/pricing/index.md) — Per-video pricing for Ray 3.2 video - [**Error handling**](/guides/error-handling/index.md) — Every error code with troubleshooting steps - [**API Reference**](/api/index.md) — Complete endpoint specifications