> ## Documentation Index
> Fetch the complete documentation index at: https://docs.summerengine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How to make cutscenes for your game with AI: on-model shots, a voice line and a music cue

> Make a game cutscene with AI that keeps your character on-model: a character reference picture, a style frame, image-to-video shots, sound made from each finished shot, a voice line, a music cue and sound accents, then assemble the shots. Exact CLI, API and MCP steps and what it costs.

The hard part of an AI cutscene is not making a good shot. It is making three shots in a row where the hero has the same face, the same cloak and the same light, so the player believes it is one moment. Ask a video model for each shot from words alone and you get three different people. Ask for one long take and the face drifts after six seconds. The fix is the method film crews use: lock the look in still pictures first, then animate each picture as its own short shot, then cut them together with a voice line and a music cue. This guide makes a 15-second story beat that way, from nothing to a finished MP4.

## The chain

| Step | Model | Why this model | Cost |
| - | - | - | - |
| 1. Character reference picture | [GPT Image 2.5](https://docs.summerengine.com/api/models/image/gpt-image-2-5.md) `image/gpt-image-2.5` | Follows a long character brief closely and keeps the whole figure on a plain background: the one picture every later frame is edited from. | \$0.053 |
| 2. Style frame (shot 1) | [GPT Image 2.5](https://docs.summerengine.com/api/models/image/gpt-image-2-5.md) `image/gpt-image-2.5` | Faithful edits: puts the same character into the scene and sets the light and palette that every other shot copies. | \$0.069 |
| 3. Start frames, later shots x 2 | [Nano Banana 2.1](https://docs.summerengine.com/api/models/image/nano-banana-2-1.md) `image/nano-banana-2.1` | Keeps the same look across many images when it edits from one picture, at a low price per frame. | \$0.08 |
| 4. Image-to-video shots x 3 | [Kling 4.0](https://docs.summerengine.com/api/models/video/kling-4.md) `video/kling-4` | Holds a character steadiest from its start frame to the last frame; the sound comes next, made from the finished clip. | \$2.355 |
| 5. Sound for each shot x 3 | [Kling Video to Audio](https://docs.summerengine.com/api/models/video/kling-video-to-audio.md) `video/kling-video-to-audio` | Watches the finished shot and makes the sound of what happens in it, in sync with the picture: the wind, the lantern, her steps. | \$0.105 |
| 6. Voice line | [Expressive v4](https://docs.summerengine.com/api/models/voice/expressive-v4.md) `voice/expressive-v4` | Reads stage words in square brackets, so one line can drop to a whisper on the word that matters. | \$0.0103 |
| 7. Music cue | [Music v2](https://docs.summerengine.com/api/models/music/music-v2.md) `music/music-v2` | Follows a timed arc and named instruments, so the cue fits the scene's length and swells on the last shot. | \$0.0425 |
| 8. Sound accents x 2 | [Sound Effects v2](https://docs.summerengine.com/api/models/sound/sfx-v2.md) `sound/sfx-v2` | Adds the one or two sounds the scene must have exactly on the cut, which the shot sound does not place for you. | \$0.006 |
| | | **Total** | **\$2.721** |

**\$2.721 for the whole chain**, at each model's list price:

* Character reference picture: \$0.053 (\$0.053 per image)
* Style frame (shot 1): \$0.069 (\$0.053 per image)
* Start frames, later shots x 2: \$0.08 (\$0.04 per image, 2 jobs)
* Image-to-video shots x 3: \$2.355 (\$0.157 per second, 3 jobs)
* Sound for each shot x 3: \$0.105 (\$0.035 per clip, 3 jobs)
* Voice line: \$0.0103 (\$0.08 per 1,000 characters)
* Music cue: \$0.0425 (\$0.0025 per second)
* Sound accents x 2: \$0.006 (\$0.002 per second, 2 jobs)

A failed job costs nothing. Before you run anything, price each step exactly and for free: `summer api estimate <model> --input '<json>'`, or `POST /v1/estimate` with the same body as the job. Prices come from [`GET /v1/models`](https://docs.summerengine.com/api/reference/get-models) and change only when the provider's price does.

## Before you start

Sign in once, so your agent or your terminal can run jobs on your Summer account:

```bash theme={null}
npx -y summer-engine@latest login
```

Your browser opens the Summer sign-in. Pick a spend limit for this computer and choose **Allow**. Using a coding agent? Paste this guide's URL and say "make this cutscene for my game"; the [quickstart for your agent](https://docs.summerengine.com/api/agents.md) connects it in one command.

Then write the **shot list**. A cutscene longer than about 6 seconds is several shots, never one take. One line per shot: framing, the one action, the one camera move.

| Shot | Framing | Action | Camera |
| - | - | - | - |
| 1 | Wide | Wren raises her lantern at the edge of the dark village | Slow push-in |
| 2 | Medium close-up | Her face in lantern light, she turns toward the fog | Static |
| 3 | Over the shoulder | Something moves in the fog at the end of the street | Slow push past her shoulder |

Three 5-second shots make a 15-second beat. A still costs a small part of what a shot costs, so do all the fixing on stills.

## Step 1: a character reference picture

This picture is the character's identity for the whole cutscene. Every later frame is an edit of it or of a picture made from it. Make it like a casting photo: the full figure, front view, plain background, even light, and every detail named in words ("moss-green hooded wool cloak", "brass oil lantern"). Words carry colours and materials into the edits better than pixels alone.

```bash CLI theme={null}
npx -y summer-engine@latest api run image/gpt-image-2.5 \
  --input '{"prompt":"Character reference picture: WREN, the lantern keeper, a young woman in her twenties, freckled face, short dark auburn hair, moss-green hooded wool cloak over a cream linen tunic, brown leather satchel, holding a brass oil lantern with a warm amber flame. Full body, front view, standing straight, whole figure in frame. Plain light grey background, soft even light. Painted storybook 3D look, soft shapes, rich but muted colours. No text.","aspectRatio":"3:4","title":"Wren reference"}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"image/gpt-image-2.5","input":{"prompt":"Character reference picture: WREN, the lantern keeper, a young woman in her twenties, freckled face, short dark auburn hair, moss-green hooded wool cloak over a cream linen tunic, brown leather satchel, holding a brass oil lantern with a warm amber flame. Full body, front view, standing straight, whole figure in frame. Plain light grey background, soft even light. Painted storybook 3D look, soft shapes, rich but muted colours. No text.","aspectRatio":"3:4","title":"Wren reference"}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "image/gpt-image-2.5",
    "input": {
      "prompt": "Character reference picture: WREN, the lantern keeper, a young woman in her twenties, freckled face, short dark auburn hair, moss-green hooded wool cloak over a cream linen tunic, brown leather satchel, holding a brass oil lantern with a warm amber flame. Full body, front view, standing straight, whole figure in frame. Plain light grey background, soft even light. Painted storybook 3D look, soft shapes, rich but muted colours. No text.",
      "aspectRatio": "3:4",
      "title": "Wren reference"
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "image/gpt-image-2.5", "input": {"prompt": "Character reference picture: WREN, the lantern keeper, a young woman in her twenties, freckled face, short dark auburn hair, moss-green hooded wool cloak over a cream linen tunic, brown leather satchel, holding a brass oil lantern with a warm amber flame. Full body, front view, standing straight, whole figure in frame. Plain light grey background, soft even light. Painted storybook 3D look, soft shapes, rich but muted colours. No text.", "aspectRatio": "3:4", "title": "Wren reference"}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "image/gpt-image-2.5",
  input: {
    prompt: "Character reference picture: WREN, the lantern keeper, a young woman in her twenties, freckled face, short dark auburn hair, moss-green hooded wool cloak over a cream linen tunic, brown leather satchel, holding a brass oil lantern with a warm amber flame. Full body, front view, standing straight, whole figure in frame. Plain light grey background, soft even light. Painted storybook 3D look, soft shapes, rich but muted colours. No text.",
    aspectRatio: "3:4",
    title: "Wren reference"
  },
  out: "assets",
})
```

If the character is already in your game, start from a screenshot or the concept art instead: [upload the file](https://docs.summerengine.com/api/jobs.md#send-a-picture) and use its `fileUrl` in step 2. The 3D [character guide](https://docs.summerengine.com/api/guides/3d-character.md) makes a concept picture that works here as it is.

## Step 2: the style frame

The style frame is shot 1's first frame, and it sets the look of the whole scene: the light, the palette, the lens. Edit it from the character reference with `referenceImageUrl`, and say "same face, hair, cloak and lantern as the reference".

```bash CLI theme={null}
npx -y summer-engine@latest api run image/gpt-image-2.5 \
  --input '{"prompt":"Wide cinematic film still of this same character, Wren, standing at the edge of a misty village at dusk, holding her brass lantern up, cobblestone street behind her with dark unlit lanterns on posts, cold blue fog, her lantern the only warm light. Same face, hair, cloak and lantern as the reference. Painted storybook 3D look, soft shapes, rich but muted colours. 16:9, room around her, no text.","referenceImageUrl":"<fileUrl of character reference picture>","aspectRatio":"16:9","title":"Wren intro, style frame"}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"image/gpt-image-2.5","input":{"prompt":"Wide cinematic film still of this same character, Wren, standing at the edge of a misty village at dusk, holding her brass lantern up, cobblestone street behind her with dark unlit lanterns on posts, cold blue fog, her lantern the only warm light. Same face, hair, cloak and lantern as the reference. Painted storybook 3D look, soft shapes, rich but muted colours. 16:9, room around her, no text.","referenceImageUrl":"<fileUrl of character reference picture>","aspectRatio":"16:9","title":"Wren intro, style frame"}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "image/gpt-image-2.5",
    "input": {
      "prompt": "Wide cinematic film still of this same character, Wren, standing at the edge of a misty village at dusk, holding her brass lantern up, cobblestone street behind her with dark unlit lanterns on posts, cold blue fog, her lantern the only warm light. Same face, hair, cloak and lantern as the reference. Painted storybook 3D look, soft shapes, rich but muted colours. 16:9, room around her, no text.",
      "referenceImageUrl": "<fileUrl of character reference picture>",
      "aspectRatio": "16:9",
      "title": "Wren intro, style frame"
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "image/gpt-image-2.5", "input": {"prompt": "Wide cinematic film still of this same character, Wren, standing at the edge of a misty village at dusk, holding her brass lantern up, cobblestone street behind her with dark unlit lanterns on posts, cold blue fog, her lantern the only warm light. Same face, hair, cloak and lantern as the reference. Painted storybook 3D look, soft shapes, rich but muted colours. 16:9, room around her, no text.", "referenceImageUrl": "<fileUrl of character reference picture>", "aspectRatio": "16:9", "title": "Wren intro, style frame"}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "image/gpt-image-2.5",
  input: {
    prompt: "Wide cinematic film still of this same character, Wren, standing at the edge of a misty village at dusk, holding her brass lantern up, cobblestone street behind her with dark unlit lanterns on posts, cold blue fog, her lantern the only warm light. Same face, hair, cloak and lantern as the reference. Painted storybook 3D look, soft shapes, rich but muted colours. 16:9, room around her, no text.",
    referenceImageUrl: "<fileUrl of character reference picture>",
    aspectRatio: "16:9",
    title: "Wren intro, style frame"
  },
  out: "assets",
})
```

Make it 16:9, or 9:16 for a phone game. An image-to-video shot keeps the framing of its start frame, so a square start frame gives you a square shot whatever `aspectRatio` says. Look at the style frame for a full minute before you go on. A wrong colour here is wrong in every shot.

## Step 3: start frames for the other shots

Make each later shot's first frame as an edit of the **style frame**, not of the character reference. The style frame already holds the character and the light, so both carry into the close-up and the over-the-shoulder shot.

```bash CLI theme={null}
npx -y summer-engine@latest api run image/nano-banana-2.1 \
  --input '{"prompt":"Medium close-up of the same woman from this picture, Wren, three-quarter view, her face lit from below by the amber lantern, worried eyes looking past the camera into the fog. Same face, hair, cloak, lantern, light and colours as this picture. 16:9 film still, no text.","referenceImageUrl":"<fileUrl of style frame (shot 1)>","aspectRatio":"16:9","title":"Wren intro, shot 2 frame"}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"image/nano-banana-2.1","input":{"prompt":"Medium close-up of the same woman from this picture, Wren, three-quarter view, her face lit from below by the amber lantern, worried eyes looking past the camera into the fog. Same face, hair, cloak, lantern, light and colours as this picture. 16:9 film still, no text.","referenceImageUrl":"<fileUrl of style frame (shot 1)>","aspectRatio":"16:9","title":"Wren intro, shot 2 frame"}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "image/nano-banana-2.1",
    "input": {
      "prompt": "Medium close-up of the same woman from this picture, Wren, three-quarter view, her face lit from below by the amber lantern, worried eyes looking past the camera into the fog. Same face, hair, cloak, lantern, light and colours as this picture. 16:9 film still, no text.",
      "referenceImageUrl": "<fileUrl of style frame (shot 1)>",
      "aspectRatio": "16:9",
      "title": "Wren intro, shot 2 frame"
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "image/nano-banana-2.1", "input": {"prompt": "Medium close-up of the same woman from this picture, Wren, three-quarter view, her face lit from below by the amber lantern, worried eyes looking past the camera into the fog. Same face, hair, cloak, lantern, light and colours as this picture. 16:9 film still, no text.", "referenceImageUrl": "<fileUrl of style frame (shot 1)>", "aspectRatio": "16:9", "title": "Wren intro, shot 2 frame"}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "image/nano-banana-2.1",
  input: {
    prompt: "Medium close-up of the same woman from this picture, Wren, three-quarter view, her face lit from below by the amber lantern, worried eyes looking past the camera into the fog. Same face, hair, cloak, lantern, light and colours as this picture. 16:9 film still, no text.",
    referenceImageUrl: "<fileUrl of style frame (shot 1)>",
    aspectRatio: "16:9",
    title: "Wren intro, shot 2 frame"
  },
  out: "assets",
})
```

Run the step once per shot, with that shot's framing and action from your list. The second frame, for shot 3, is: "Over-the-shoulder shot from behind the same woman, Wren, her raised lantern in the foreground, looking down the foggy street where a tall shadow stands between the dark lanterns." Keep the closing sentence ("same face, hair, cloak, lantern, light and colours as this picture") in every frame prompt, word for word.

## Step 4: animate each shot

Now each start frame becomes a 5-second shot. The prompt describes only what moves: one action and one camera move. The picture already says what everything looks like, so do not describe the character again; repeated description makes the model redraw her.

```bash CLI theme={null}
npx -y summer-engine@latest api run video/kling-4 \
  --input '{"prompt":"She slowly raises the brass lantern and looks down the empty street, fog drifting past her, the lantern flame flickering. Slow push-in.","imageUrl":"<fileUrl of style frame (shot 1)>","duration":5,"negativePrompt":"changing face, extra fingers, warped hands, text, subtitles","aspectRatio":"16:9"}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"video/kling-4","input":{"prompt":"She slowly raises the brass lantern and looks down the empty street, fog drifting past her, the lantern flame flickering. Slow push-in.","imageUrl":"<fileUrl of style frame (shot 1)>","duration":5,"negativePrompt":"changing face, extra fingers, warped hands, text, subtitles","aspectRatio":"16:9"}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "video/kling-4",
    "input": {
      "prompt": "She slowly raises the brass lantern and looks down the empty street, fog drifting past her, the lantern flame flickering. Slow push-in.",
      "imageUrl": "<fileUrl of style frame (shot 1)>",
      "duration": 5,
      "negativePrompt": "changing face, extra fingers, warped hands, text, subtitles",
      "aspectRatio": "16:9"
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "video/kling-4", "input": {"prompt": "She slowly raises the brass lantern and looks down the empty street, fog drifting past her, the lantern flame flickering. Slow push-in.", "imageUrl": "<fileUrl of style frame (shot 1)>", "duration": 5, "negativePrompt": "changing face, extra fingers, warped hands, text, subtitles", "aspectRatio": "16:9"}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "video/kling-4",
  input: {
    prompt: "She slowly raises the brass lantern and looks down the empty street, fog drifting past her, the lantern flame flickering. Slow push-in.",
    imageUrl: "<fileUrl of style frame (shot 1)>",
    duration: 5,
    negativePrompt: "changing face, extra fingers, warped hands, text, subtitles",
    aspectRatio: "16:9"
  },
  out: "assets",
})
// It answers `next` while the job runs: follow it to the end.
summer_api_job({ jobId: "<jobId>", wait: true, out: "assets" })
```

Run it three times, once per start frame, and change the prompt to that shot's action. What works:

* **One verb for the camera.** `Slow push-in`, `static`, `slow pan left`. Complex moves through a crowd fall apart.
* **5 seconds.** Faces, hands and physics drift with every second past about six. Two 5-second shots look better than one 10-second shot and cost the same.
* **Describe only the picture.** The next step makes the sound from the finished shot, so the prompt spends every word on the action, the light and the camera.
* **A negative prompt for the usual faults.** `changing face, extra fingers, warped hands, text, subtitles`.

A video job takes a few minutes. The CLI follows it and saves the MP4; over HTTP you read `GET /v1/jobs/{id}` until `state` is `done` ([jobs](https://docs.summerengine.com/api/jobs.md)). Try a new shot first on a cheap model such as [MiniMax H3 Max Turbo](https://docs.summerengine.com/api/models/video/minimax-h3-max-turbo.md) (\$0.026 per second) to check the motion, then make the final.

**Other models for this step.** [Kling 3.0 Pro](https://docs.summerengine.com/api/models/video/kling-3-pro.md) makes sound with the picture in one job (\$0.168 per second): ask for it in the prompt (`ambient sound only: soft wind, a lantern creaking`, then `no speech, no music`) and skip step 5. [Veo 3.1](https://docs.summerengine.com/api/models/video/veo3-1.md) gives the most coherent shots, also with sound, for the one shot that must be perfect (\$0.40 per second). [HappyHorse 1.1](https://docs.summerengine.com/api/models/video/happy-horse-1-1.md) makes the character speak with lip sync when you write the line into the video prompt: the voice then comes from the video model, not from your voice cast. To keep a character the same across many shots without a start frame, the reference models take up to several pictures of her: [Kling 4.0 References](https://docs.summerengine.com/api/models/video/kling-4-ref.md) (\$0.157 per second).

## Step 5: sound for each shot

Send each finished shot back as `videoUrl` and describe what should be heard. The model watches the clip and makes sound that follows it: a step lands when her foot does, the flame crackles when it flares. Ask for the place and its things, and keep `No speech, no music`: the voice and the music come from their own jobs.

```bash CLI theme={null}
npx -y summer-engine@latest api run video/kling-video-to-audio \
  --input '{"prompt":"Soft wind through an empty street, a brass lantern creaking on its handle, fog, distant water. No speech, no music.","videoUrl":"<fileUrl of image-to-video shots>"}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"video/kling-video-to-audio","input":{"prompt":"Soft wind through an empty street, a brass lantern creaking on its handle, fog, distant water. No speech, no music.","videoUrl":"<fileUrl of image-to-video shots>"}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "video/kling-video-to-audio",
    "input": {
      "prompt": "Soft wind through an empty street, a brass lantern creaking on its handle, fog, distant water. No speech, no music.",
      "videoUrl": "<fileUrl of image-to-video shots>"
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "video/kling-video-to-audio", "input": {"prompt": "Soft wind through an empty street, a brass lantern creaking on its handle, fog, distant water. No speech, no music.", "videoUrl": "<fileUrl of image-to-video shots>"}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "video/kling-video-to-audio",
  input: {
    prompt: "Soft wind through an empty street, a brass lantern creaking on its handle, fog, distant water. No speech, no music.",
    videoUrl: "<fileUrl of image-to-video shots>"
  },
  out: "assets",
})
// It answers `next` while the job runs: follow it to the end.
summer_api_job({ jobId: "<jobId>", wait: true, out: "assets" })
```

The answer is the same clip with its new soundtrack. [ThinkSound](https://docs.summerengine.com/api/models/video/thinksound.md) (\$0.01 per second) reasons about the scene before it makes the sound, and suits slow, quiet shots where every small sound counts; it takes longer. [Kling Video to Audio](https://docs.summerengine.com/api/models/video/kling-video-to-audio.md) (\$0.035 per clip) is fast and fits most shots.

## Step 6: the voice line

The line is its own job, so you choose the voice, the delivery and the timing. Pick the voice from the model's list (`npx -y summer-engine@latest api models voice/expressive-v4` prints every `voiceId`). Alice, a clear and calm voice, fits a lantern keeper who has seen something.

```bash CLI theme={null}
npx -y summer-engine@latest api run voice/expressive-v4 \
  --input '{"text":"[quietly] The lanterns went out at midnight. [pause] All of them. [whispering] Whatever came through the fog... it is still here.","voiceId":"Xb7hH8MSUJpSbSDYk0k2"}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"voice/expressive-v4","input":{"text":"[quietly] The lanterns went out at midnight. [pause] All of them. [whispering] Whatever came through the fog... it is still here.","voiceId":"Xb7hH8MSUJpSbSDYk0k2"}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "voice/expressive-v4",
    "input": {
      "text": "[quietly] The lanterns went out at midnight. [pause] All of them. [whispering] Whatever came through the fog... it is still here.",
      "voiceId": "Xb7hH8MSUJpSbSDYk0k2"
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "voice/expressive-v4", "input": {"text": "[quietly] The lanterns went out at midnight. [pause] All of them. [whispering] Whatever came through the fog... it is still here.", "voiceId": "Xb7hH8MSUJpSbSDYk0k2"}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "voice/expressive-v4",
  input: {
    text: "[quietly] The lanterns went out at midnight. [pause] All of them. [whispering] Whatever came through the fog... it is still here.",
    voiceId: "Xb7hH8MSUJpSbSDYk0k2"
  },
  out: "assets",
})
```

Stage words in square brackets set the delivery: `[quietly]`, `[pause]`, `[whispering]`. Full stops make clear pauses, and an ellipsis trails off. A video model does not move lips to a separate voice file. So play the line over shots where the mouth is small or hidden: the wide shot and the over-the-shoulder shot here. The [voice lines guide](https://docs.summerengine.com/api/guides/voice-lines.md) covers casting and delivery in depth.

## Step 7: the music cue

Time the cue to the scene: 15 seconds of picture plus a 2-second ring-out, so the last chord fades under the cut back to gameplay. At 60 BPM one bar is 4 seconds, so events on bar lines (0:08, 0:12) land where you ask more often. The swell starts under shot 2 and peaks inside shot 3.

```bash CLI theme={null}
npx -y summer-engine@latest api run music/music-v2 \
  --input '{"prompt":"Linear cutscene cue at 60 BPM, D minor, 4/4, exactly 15 seconds plus a 2 second ring-out: a low cello drone and sparse felt piano notes, uneasy and quiet, a soft string swell starts at 0:08 and peaks at 0:12, ends on an unresolved held chord. Instrumental, no vocals, no drums.","durationSeconds":17}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"music/music-v2","input":{"prompt":"Linear cutscene cue at 60 BPM, D minor, 4/4, exactly 15 seconds plus a 2 second ring-out: a low cello drone and sparse felt piano notes, uneasy and quiet, a soft string swell starts at 0:08 and peaks at 0:12, ends on an unresolved held chord. Instrumental, no vocals, no drums.","durationSeconds":17}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "music/music-v2",
    "input": {
      "prompt": "Linear cutscene cue at 60 BPM, D minor, 4/4, exactly 15 seconds plus a 2 second ring-out: a low cello drone and sparse felt piano notes, uneasy and quiet, a soft string swell starts at 0:08 and peaks at 0:12, ends on an unresolved held chord. Instrumental, no vocals, no drums.",
      "durationSeconds": 17
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "music/music-v2", "input": {"prompt": "Linear cutscene cue at 60 BPM, D minor, 4/4, exactly 15 seconds plus a 2 second ring-out: a low cello drone and sparse felt piano notes, uneasy and quiet, a soft string swell starts at 0:08 and peaks at 0:12, ends on an unresolved held chord. Instrumental, no vocals, no drums.", "durationSeconds": 17}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "music/music-v2",
  input: {
    prompt: "Linear cutscene cue at 60 BPM, D minor, 4/4, exactly 15 seconds plus a 2 second ring-out: a low cello drone and sparse felt piano notes, uneasy and quiet, a soft string swell starts at 0:08 and peaks at 0:12, ends on an unresolved held chord. Instrumental, no vocals, no drums.",
    durationSeconds: 17
  },
  out: "assets",
})
```

Make two or three takes and keep the one whose peak lands nearest 0:12. Then move the cut a few frames to the music, not the music to the cut. For a score that shares one theme across the menu, the cutscenes and combat, see [music for your game's cutscenes](https://docs.summerengine.com/api/guides/cutscene-music.md).

## Step 8: sound accents on the cut

The shot sound gives you the air of each shot. It does not put a sound on an exact frame. For the one or two moments that must hit (the lantern flaring on the cut to shot 3, a distant bell), make them as sound effects and place them yourself.

```bash CLI theme={null}
npx -y summer-engine@latest api run sound/sfx-v2 \
  --input '{"text":"A brass oil lantern flame flares up brightly with a soft whoosh and a crackle, close, dry, no music, no voices.","durationSeconds":1.5}' \
  --download .
```

```bash curl theme={null}
curl https://www.summerengine.com/v1/jobs \
  -H "Authorization: Bearer $SUMMER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Prefer: wait=60" \
  -d '{"model":"sound/sfx-v2","input":{"text":"A brass oil lantern flame flares up brightly with a soft whoosh and a crackle, close, dry, no music, no voices.","durationSeconds":1.5}}'
```

```ts TypeScript theme={null}
const res = await fetch("https://www.summerengine.com/v1/jobs", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUMMER_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({
    "model": "sound/sfx-v2",
    "input": {
      "text": "A brass oil lantern flame flares up brightly with a soft whoosh and a crackle, close, dry, no music, no voices.",
      "durationSeconds": 1.5
    }
  }),
});
const { job } = await res.json();
console.log(job.state, job.id);
```

```python Python theme={null}
import os, uuid, requests

res = requests.post(
    "https://www.summerengine.com/v1/jobs",
    headers={
        "Authorization": f"Bearer {os.environ['SUMMER_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
        "Prefer": "wait=60",
    },
    json={"model": "sound/sfx-v2", "input": {"text": "A brass oil lantern flame flares up brightly with a soft whoosh and a crackle, close, dry, no music, no voices.", "durationSeconds": 1.5}},
)
job = res.json()["job"]
print(job["state"], job["id"])
```

```js MCP theme={null}
// Summer's MCP server: npx -y summer-engine@latest setup <agent> --yes
summer_api_run({
  model: "sound/sfx-v2",
  input: {
    text: "A brass oil lantern flame flares up brightly with a soft whoosh and a crackle, close, dry, no music, no voices.",
    durationSeconds: 1.5
  },
  out: "assets",
})
```

For a long scene, add one `loop: true` ambience per location under the shots, at the length of the scene, so the air never drops out between cuts.

## Assemble the shots

Cut on action: end shot 1 as she starts to turn, open shot 2 mid-turn. Then lay the sound in four layers: shot sound low, the voice on top, the music under it, the accents on their frames. In any video editor that is four tracks. From a shell, two commands do it:

```bash theme={null}
printf "file 'shot_01.mp4'\nfile 'shot_02.mp4'\nfile 'shot_03.mp4'\n" > shots.txt
ffmpeg -f concat -safe 0 -i shots.txt -c:v libx264 -c:a aac picture.mp4

ffmpeg -i picture.mp4 -i wren_line.mp3 -i cue.mp3 -filter_complex \
  "[0:a]volume=0.35[amb];[1:a]adelay=2000|2000[vo];[2:a]volume=0.5[mus];[amb][vo][mus]amix=inputs=3:duration=first:normalize=0[a]" \
  -map 0:v -map "[a]" -c:v copy -c:a aac cutscene.mp4
```

The voice starts 2 seconds in (`adelay=2000`), the shots' own sound sits at 35 percent and the music at half volume under the voice. The concat step needs every shot to have a sound track; with a silent video model, drop `[0:a]` and mix your ambience loop in its place. Add the accents the same way, each with its own `adelay`.

## Put it in your game

* **Summer Engine.** Import the MP4 by its `assetId` or URL; the import converts it to `.ogv`, the video format the engine plays, and names the new file. Play it in a full-screen `VideoStreamPlayer` with a short fade from and to black. The [cinematic cutscene skill](https://docs.summerengine.com/skills/cinematic-cutscene.md) builds the scene, the fade and the hand-back to gameplay for you.
* **Any other engine.** The MP4 plays in any video player node. Pause gameplay, fade in, play, fade out, and let the player skip.
* **Keep the stills.** The character reference and the style frame are now the canon for this character. The next cutscene edits from them, so Wren looks the same in chapter five.

## Make every cutscene in the game

Once the first scene works, the next one is a batch. Write every start frame for the scene into one JSONL file and send it with `summer api run --batch frames.jsonl --download cutscenes/ch2`. Review the stills, then send the shots as a second batch, each with its frame's `fileUrl` as `imageUrl`. A batch is priced up front and runs all or none ([batches](https://docs.summerengine.com/api/jobs.md#run-many-jobs-batches)).

## Common problems

| You see | Do this |
| - | - |
| The character's face changes between shots | Edit every start frame from the style frame, keep the "same face" sentence, and keep shots at 5 seconds |
| The shot comes out square or vertical | Make the start frame in the framing you want; an image-to-video shot keeps it |
| The character talks gibberish in the shot sound | Add `No speech` to the sound prompt |
| Lips do not match the voice line | Play the line over wide or back-facing shots, or use [HappyHorse 1.1](https://docs.summerengine.com/api/models/video/happy-horse-1-1.md) with the line in the prompt |
| Hands melt or the lantern changes shape | Add them to `negativePrompt` and make the action smaller |
| The camera move falls apart | Use one simple move per shot: push-in, pan or static |
| The cutscene feels silent | Raise the shot sound and add an ambience loop under it |

## Questions

**Can I make a cutscene from my own game's art?** Yes. Upload a screenshot or the concept art ([send a picture](https://docs.summerengine.com/api/jobs.md#send-a-picture)) and start at step 2 with its `fileUrl`.

**How long can one shot be?** `duration` goes up to 20 seconds, and each model renders its nearest supported length. For a character on screen, 5 seconds per shot holds the look best.

**Can I add sound to a video I already have?** Yes. Upload the clip (at most 20 seconds) and run step 5 on it with its `fileUrl` as `videoUrl`. That works for gameplay captures too.

**Can I use the cutscene in a commercial game?** Yes. Every model on the Summer API allows commercial use of what it makes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.