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

# Recast and continue video

> Carry a clip's cast into a new shot, swap its medium, or extend it into the next beat.

export const RecipeMeta = ({authors = [], notebook, raw, note}) => <div className="not-prose recipe-meta" style={{
  margin: "1.4rem 0 2.4rem"
}}>
    <div className="recipe-meta__band" style={{
  display: "flex",
  flexWrap: "wrap",
  alignItems: "center",
  justifyContent: "space-between",
  gap: "0.7rem 1.5rem",
  padding: "0.7rem 0.15rem"
}}>
      <span style={{
  display: "inline-flex",
  flexWrap: "wrap",
  alignItems: "center",
  gap: "0.5rem 1.3rem"
}}>
        {authors.map((a, i) => <a key={i} href={`https://github.com/${a.github}`} target="_blank" rel="noreferrer" className="recipe-meta__author" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.5rem",
  textDecoration: "none"
}}>
            <img src={`https://github.com/${a.github}.png?size=96`} alt={a.name} loading="lazy" style={{
  width: "1.4rem",
  height: "1.4rem",
  borderRadius: "0.35rem",
  objectFit: "cover"
}} />
            <span className="recipe-meta__author-name" style={{
  fontSize: "0.85rem",
  fontWeight: 550
}}>
              {a.name}
            </span>
          </a>)}
      </span>
      <span style={{
  display: "inline-flex",
  flexWrap: "wrap",
  alignItems: "center",
  gap: "0.4rem 1.4rem"
}}>
        {notebook && <a href={notebook} target="_blank" rel="noreferrer" className="recipe-meta__link" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  fontSize: "0.82rem",
  fontWeight: 550,
  textDecoration: "none"
}}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
              <path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-1.94c-3.2.7-3.87-1.54-3.87-1.54-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.19 1.76 1.19 1.03 1.75 2.69 1.25 3.35.95.1-.74.4-1.25.72-1.53-2.55-.29-5.23-1.28-5.23-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11.1 11.1 0 0 1 5.79 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.41-2.69 5.38-5.25 5.66.41.36.77 1.05.77 2.13v3.16c0 .3.21.67.8.55A11.5 11.5 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" />
            </svg>
            View on GitHub
          </a>}
        {raw && <a href={raw} className="recipe-meta__link" style={{
  display: "inline-flex",
  alignItems: "center",
  gap: "0.4rem",
  fontSize: "0.82rem",
  fontWeight: 550,
  textDecoration: "none"
}}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
              <polyline points="7 10 12 15 17 10" />
              <line x1="12" y1="15" x2="12" y2="3" />
            </svg>
            Download notebook
          </a>}
      </span>
    </div>
    {note && <p className="recipe-meta__note" style={{
  margin: "0.8rem 0 0",
  fontSize: "0.82rem",
  lineHeight: 1.5
}}>
        {note}
      </p>}
  </div>;

export const RecipeClip = ({src, caption, aspectRatio = "1280 / 704"}) => <figure className="not-prose recipe-clip" style={{
  margin: "1.5rem 0 2rem"
}}>
    <video controls playsInline preload="metadata" src={src} style={{
  display: "block",
  width: "100%",
  aspectRatio,
  borderRadius: "0.9rem",
  border: "1px solid rgba(72,106,88,0.18)",
  background: "#0c1512"
}} />
    {caption && <figcaption className="recipe-clip__caption">{caption}</figcaption>}
  </figure>;

<div className="cookbook-recipe" />

<RecipeMeta authors={[{ name: "Stephen Batifol", github: "stephen37" }]} notebook="https://github.com/black-forest-labs/bfl_cookbook/blob/main/video/03_edit_recast_continue.ipynb" raw="https://raw.githubusercontent.com/black-forest-labs/bfl_cookbook/main/video/03_edit_recast_continue.ipynb" />

Two request fields take a video, and they answer two different questions about it:

* **`reference_video`**: *keep the cast, build a new shot.* A brand-new clip carrying over
  the subjects - and as much of the staging as you re-describe - from yours.
* **`start_video`**: *keep going.* Continues your clip from its final frames.

`reference_video` is the more flexible of the two: re-stage the same scene on different
gear, or send the same cast somewhere new entirely, depending on how much of the source
your prompt re-describes.

This notebook generates one source clip with text-to-video, then runs both against it, so
it stands alone with just a `BFL_API_KEY`. Video inputs are mp4, up to 50 MB and 15 seconds;
the limit is on the file you send, not the output you request.

## 1. Setup

The same client as the previous recipes. Our 5-second source is comfortably inside the input
caps; for longer sources, trim to under 15 seconds before sending.

<Accordion title="Setup: the API client from the quickstart (run this first)">
  ```python theme={null}
  import base64
  import os, re, subprocess, time, requests
  from PIL import Image
  from IPython.display import Video, display
  from imageio_ffmpeg import get_ffmpeg_exe

  API_KEY = os.environ.get("BFL_API_KEY")
  assert API_KEY, "Set BFL_API_KEY - create one at https://dashboard.bfl.ai"
  BASE = "https://api.bfl.ai/v1"
  HEADERS = {"x-key": API_KEY}
  DONE = {"Ready", "Request Moderated", "Content Moderated", "Error", "Task not found"}
  FFMPEG = get_ffmpeg_exe()
  os.makedirs("outputs", exist_ok=True)

  def generate(payload, path, model="flux-3-preview-high"):
      """The whole client: submit, poll to a terminal status, save the file."""
      task = requests.post(f"{BASE}/{model}", headers=HEADERS, json=payload, timeout=60)
      task.raise_for_status()
      poll = task.json()["polling_url"]
      result = {"status": None}
      while result["status"] not in DONE:
          time.sleep(5)
          result = requests.get(poll, headers=HEADERS, timeout=30).json()
          print(f'  {os.path.basename(path)}: {result["status"]}', flush=True)
      assert result["status"] == "Ready", f'{result["status"]} - Error: resubmit. Moderated: reword.'
      video = requests.get(result["result"]["sample"], timeout=60).content
      open(path, "wb").write(video)          # the result URL expires ~2h after Ready
      return path

  def b64(path):
      """Local files travel inline: no upload service, just a data URL."""
      mime = "video/mp4" if path.endswith(".mp4") else "image/jpeg"
      return f"data:{mime};base64," + base64.b64encode(open(path, "rb").read()).decode()

  def seconds(path):
      """Clip duration, read from ffmpeg's banner."""
      info = subprocess.run([FFMPEG, "-i", path], capture_output=True, text=True).stderr
      h, m, s = re.search(r"Duration: (\d+):(\d+):([\d.]+)", info).groups()
      return int(h) * 3600 + int(m) * 60 + float(s)

  def strip(path, n=5):
      """One image: n frames sampled evenly across the clip."""
      subprocess.run([FFMPEG, "-y", "-i", path, "-frames:v", "1",
                      "-vf", f"fps={n}/{seconds(path)},scale=320:-1,tile={n}x1",
                      "/tmp/_strip.jpg"], capture_output=True, check=True)
      return Image.open("/tmp/_strip.jpg")
  ```
</Accordion>

## 2. A source clip

A source clip you plan to reference later has one job: give the next prompts something
concrete to name. Three or four fixed visual anchors (here: the brass body, the red
wind-up key, the single amber eye) plus one unmistakable motion (marching in a straight
line), described the same way every time.

One clause in this prompt earns its place: "it moves like a wound-up machine, not a
person". Mechanical subjects default to fluid, human-smooth movement unless you write the
imperfection in: stiff even steps, a tremor on each footfall, and a sound locked to the
motion.

```python theme={null}
source = generate({
    "prompt": (
        "Medium shot of a small brass wind-up tin robot marching in a straight line "
        "across a cluttered watchmaker's workbench at night, between loose gears and a "
        "magnifying lamp, its red wind-up key slowly turning in its back, its single "
        "amber eye lit. It moves like a wound-up machine, not a person: stiff even "
        "steps, a faint mechanical tremor on each footfall. The camera tracks smoothly "
        "alongside it, level with the bench. Audio: a steady clockwork ticking locked "
        "to its steps, the low hum of the lamp, a distant clock. One continuous "
        "unbroken shot, the marching never stops. No on-screen text."
    ),
    "aspect_ratio": "16:9",
    "duration": 5,
}, "outputs/03_source.mp4")
```

<RecipeClip src="/images/cookbook/clips/03_source.mp4" caption="The source clip the rest of this recipe works from." />

## 3. `reference_video`, take one: same scene, different medium

The source becomes a reference: its subjects carry over, and whatever staging you
re-describe carries with them. So to recast the *look* of a shot, restate the shot (the
blocking, the camera move, the path) and change only the recording medium.

The medium includes the sound, and this recast makes that concrete: a 1928 silent print
has no sync sound at all. The honest recast deletes the scene's own ticking and replaces
it with what the medium would actually carry, a piano accompaniment and projector
clatter. Name that swap explicitly or the model keeps the modern soundtrack under an
old-looking picture.

One constraint to know: **at the default `720p`, `reference_video` output caps at 15
seconds.** Ask for 16-20 and you get a `422` telling you to shorten it or drop to `480p`.

```python theme={null}
silent_film = generate({
    "prompt": (
        "The same brass wind-up tin robot from the source video, same red wind-up key "
        "turning in its back, same single amber eye, marches in the same straight line "
        "across the same watchmaker's bench, same tracking camera alongside it. The "
        "whole scene is now a scratched 1928 silent film print: soft black-and-white, "
        "heavy grain, flickering exposure, gate weave, dust and hairline scratches, a "
        "slightly sped-up hand-cranked judder. The sound is recast to the medium: the "
        "clockwork ticking is gone, replaced by a lively solo upright-piano march that "
        "hits in time with the robot's steps, under the soft clatter of a film "
        "projector running in a quiet room. No on-screen text, no intertitle cards."
    ),
    "reference_video": b64(source),
    "duration": 5,
}, "outputs/03_recast_silent.mp4")
```

Play the source and the recast back to back. Same robot, same march; the century of
the recording changes, picture and soundtrack together.

## 4. `reference_video`, take two: same cast, new shot

Don't restate the staging and the reference works the other way: the model carries the
subjects into a scene you write from scratch. New action, new location, new camera. Like
`reference_images` in the [previous recipe](/cookbook/video_start_from_images), but the
identity comes from footage. The single lever that decides between "recast this shot" and
"new shot, same character" is how much of the source your prompt repeats.

Note the `duration: 8`: the endpoint accepts any integer from 5 to 20, so size the clip
to the beat instead of rounding to 5 or 10.

```python theme={null}
chessboard = generate({
    "prompt": (
        "The same brass wind-up tin robot from the source video, same red wind-up key "
        "turning in its back, same single amber eye and riveted brass seams, now "
        "marches across a giant marble chessboard floor in a vast empty hall, between "
        "chess pieces taller than it is, heading toward a toppled white king lying in "
        "its path. Cold shafts of light fall from high windows. It moves like a "
        "wound-up machine, stiff even steps, the key turning. Low tracking shot close "
        "to the floor, following just behind it. Audio: its clockwork ticking, faint "
        "footstep taps echoing in the huge stone room, a distant draught. One "
        "continuous unbroken shot. No on-screen text."
    ),
    "reference_video": b64(source),
    "duration": 8,
}, "outputs/03_ref_chessboard.mp4")
```

## 5. `start_video`: continue the shot

Picks up from the source's final frames, so the prompt's first job is to name what those
frames show ("reaches the very edge of the workbench") before saying where the shot goes.
`duration` here is the length of the **new** segment, not source plus new, and
`aspect_ratio: "auto"` inherits the source's frame so the join is clean.

Carry the established sounds forward before you evolve them: the ticking continues
unbroken through the seam, then each new event gets its own sound. Audio continuity is
what sells a continuation as one shot rather than two clips glued together.

```python theme={null}
cont = generate({
    "prompt": (
        "Continue this video from its final frames: the brass wind-up robot reaches the "
        "very edge of the workbench, its front foot steps out over nothing, and it "
        "keeps marching forward on the empty air, descending an invisible staircase "
        "step by step down toward the floor, red key still turning, amber eye level "
        "and unbothered. It moves like a machine, the same stiff even steps, now "
        "stepping down through open space. The camera cranes down smoothly to follow "
        "it. Audio: the clockwork ticking continues unbroken, each mid-air step landing "
        "on a soft wooden tap as if a stair were there, the lamp hum falling away "
        "below. One continuous shot. No on-screen text."
    ),
    "start_video": b64(source),
    "duration": 5,
}, "outputs/03_continue.mp4")
```

<RecipeClip src="/images/cookbook/clips/03_continue.mp4" caption="start_video: the shot continues from its final frames." />

The two clips cut together back to back; the seam is the source's last frame:

```python theme={null}
open("/tmp/_join.txt", "w").write(
    f"file '{os.path.abspath(source)}'\nfile '{os.path.abspath(cont)}'\n")
subprocess.run([FFMPEG, "-y", "-f", "concat", "-safe", "0", "-i", "/tmp/_join.txt",
                "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac",
                "outputs/03_joined.mp4"], capture_output=True, check=True)
```

<RecipeClip src="/images/cookbook/clips/03_joined.mp4" caption="The source and its continuation cut together: one shot becomes two beats." />

## The decision, one more time

| Question about your clip                     | Field             | The prompt's job                          |
| -------------------------------------------- | ----------------- | ----------------------------------------- |
| Re-stage the shot, change the look or medium | `reference_video` | Restate the staging, change the medium    |
| Keep the cast, build a new shot              | `reference_video` | Describe a new scene from scratch         |
| Keep going from the end                      | `start_video`     | Name the final frames, then where it goes |

* One video input per request; both fields take mp4 as a public URL or base64, ≤ 50 MB, ≤ 15s.
* `reference_video` at `720p` caps output `duration` at 15 seconds (16-20 need `480p`).
* `start_video` has no such restriction, and `aspect_ratio: "auto"` follows the source clip.

## Where to next

`start_video` chaining is one way to build length. The other is cuts: real ones, written
into the prompt, plus a shot pipeline that generates scenes in parallel and stitches them.
That's [Multi-shot films](/cookbook/video_multishot_films).
