Skip to content
Back to skills

Image To Cinematic Video

ASecurity

Turn a prompt or an existing image into a polished multi-scene cinematic short video using Seedance for clip generation, uguu.se for file hosting, and FFmpeg for last-frame extraction and crossfade stitching. Supports two modes — PARALLEL (multiple scenes from the same reference image, concurrent, ~3 min total) and CHAIN (each scene starts on the last frame of the previous scene, sequential, ~3 min per scene, produces one continuous long take). Use this skill whenever the user wants to "make ...

  • 547 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added May 26, 2026
toolsjavascriptgojavaapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 7 files and shows the line behind each finding

Scanned May 27, 2026

npx -y skills add agnt-gg/agnt --skill image-to-cinematic-video --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Image To Cinematic Video?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Image To Cinematic Video
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agnt-gg-image-to-cinematic-video/badge)](https://www.skillsdirectory.com/skills/agnt-gg-image-to-cinematic-video)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: image-to-cinematic-video
description: >-
  Turn a prompt or an existing image into a polished multi-scene cinematic short video
  using Seedance for clip generation, uguu.se for file hosting, and FFmpeg for last-frame
  extraction and crossfade stitching. Supports two modes — PARALLEL (multiple scenes from
  the same reference image, concurrent, ~3 min total) and CHAIN (each scene starts on the
  last frame of the previous scene, sequential, ~3 min per scene, produces one continuous
  long take). Use this skill whenever the user wants to "make a video from an image",
  "animate this picture", "generate video scenes", "create a cinematic short", "stitch AI
  videos together", "make a long continuous video", "chain AI clips", or any workflow
  that goes from one reference image to a multi-clip stitched video. Also trigger when
  the user mentions Seedance, image-to-video, video stitching, crossfades between clips,
  frame chaining, last-frame continuity, or wants to turn a generated image into motion.
  Covers the full pipeline end-to-end — generate image with the chat-level
  `generate_image` tool (Gemini Nano Banana Pro by default) → pull the real bytes from
  `/api/images/:id` → upload to uguu → Seedance (with the relative-filename quirk
  handled) → copy output into the project folder → extract last frame → upload → chain
  into next scene → FFmpeg xfade stitch → upload final → write manifest.
---

# Image → Cinematic Video Pipeline

A battle-tested, end-to-end workflow for turning one prompt (or one
starting image) into a polished multi-scene stitched video. This skill
only documents techniques that **actually worked** in production.

## ⚠ NON-NEGOTIABLE RULES

1. **Use `generate_image` as the primary source-image generator.**
   The image-ref system has been fixed — every `{{IMAGE_REF:<id>}}`
   returned by `generate_image` is now backed by real bytes available at
   `GET http://localhost:3333/api/images/<id>` (Bearer-auth with
   `process.env.AGNT_AUTH_TOKEN`). Use `fetchImageRefBytes(ref, outPath)`
   from `save_generated_image.js` to pull those bytes onto disk in one
   line. Default model: **Gemini Nano Banana Pro**
   (`provider: 'gemini'`, `model: 'nano-banana-pro-preview'`).
   `saveGeneratedImage()` still exists as a direct-REST-API fallback
   when you want to bypass the chat tool entirely.

2. **Every run gets its own project folder.** Call `createProjectDir(slug)`
   at the top. It returns an absolute path under
   `C:\Users\Studio\AppData\Roaming\AGNT\projects\<timestamp>_<slug>\`
   with `scenes/` and `frames/` subdirs pre-created. Every artifact
   (source image, each scene mp4, each last-frame jpg, final mp4,
   manifest.json) goes into that folder. One folder per run. No
   scattering files across the AGNT root.

3. **Upload only to uguu.se.** Catbox, Litterbox, 0x0.st, tmpfiles.org,
   transfer.sh, and file.io were all tested in this environment and
   failed (truncated files, 403s on GET, timeouts). `uploadUguu()` is
   the only host that reliably serves both small JPGs and large MP4s
   AND is reachable by the Seedance worker. Files live ~3 hours on
   uguu — fine for a run that takes ~15 min.

4. **Seedance `filename` MUST be relative.** The plugin prepends
   `C:\Users\Studio\AppData\Roaming\AGNT\plugin-data\seedance\<userId>\`
   to whatever you pass. If you pass an absolute path, the resulting
   file lives at a double-prefixed path and ffmpeg will ENOENT. Always
   pass a plain filename like `scifi_scene1.mp4`, then immediately copy
   the output into your project folder with `copySeedanceOutput()`.

5. **CHAIN mode: extract and upload the last frame BETWEEN every scene.**
   If you ever call scene N+1 with the original reference image as
   `firstFrameUrl`, you've broken the chain. Use
   `extractAndUploadLastFrame(sceneMp4, projectDir, label)` after
   every scene (except optionally the last), and pass its `frameUrl`
   as the next scene's `firstFrameUrl`.

6. **Write a `manifest.json` at the end.** Record mode, every prompt,
   every local path, every uguu URL, durations, costs, wall time.
   Keeps the run reproducible.

## The files that do the work

```
scripts/
├── save_generated_image.js          ← fetchImageRefBytes()  ← PRIMARY
│                                      saveGeneratedImage()  ← fallback
│                                      createProjectDir(),
│                                      writeProjectManifest()
├── upload_uguu.js                   ← uploadUguu()  — the only upload host
├── copy_seedance_output.js          ← copySeedanceOutput()  — handle the
│                                      relative-filename quirk
├── extract_and_upload_last_frame.js ← extractAndUploadLastFrame(),
│                                      extractLastFrame()
└── stitch_xfade.js                  ← stitchXfade()  — the exact ffmpeg
                                       command that worked
references/
└── seedance-prompt-library.md       ← 20+ proven shot prompts
```

## Mode choice

| User's goal | Mode |
|---|---|
| Continuous walk-and-talk, long pan, evolving action | **CHAIN** |
| Trailer-style single-breath mini-film | **CHAIN** |
| Narrative >10s with continuity | **CHAIN** |
| 3-shot montage / highlight reel / social post | PARALLEL |
| Different camera angles of the same subject | PARALLEL |
| Fastest turnaround | PARALLEL |

## The proven CHAIN pipeline (what actually worked)

All tool calls below are AGNT tool calls made in the conversation — not
inside `execute_javascript_code` (which can't invoke AGNT tools). The
code blocks are what goes INSIDE `execute_javascript_code`.

### Step 1 — generate the source image (chat-level tool, Gemini Nano Banana Pro)

```
generate_image(
  prompt:       "A lone astronaut in a scuffed white spacesuit at the
                 threshold of a massive derelict alien spaceship
                 half-buried in orange desert dunes. Anamorphic lens
                 flare, golden-hour rim light, cyan glyphs on the hull,
                 35mm film grain, Denis Villeneuve.",
  provider:     "gemini",
  model:        "nano-banana-pro-preview",
  aspectRatio:  "16:9"
)
```

Capture the returned ref id, e.g.
`{{IMAGE_REF:img-toolu_01ABC...-0}}` → `img-toolu_01ABC...-0`.

### Step 2 — pull the bytes onto disk + upload to uguu

```js
const path = require('path');
const {
  fetchImageRefBytes, createProjectDir,
} = require('C:/Users/Studio/.agnt/skills/image-to-cinematic-video/scripts/save_generated_image.js');
const { uploadUguu } = require('C:/Users/Studio/.agnt/skills/image-to-cinematic-video/scripts/upload_uguu.js');

const projectDir = createProjectDir('scifi_chain');
const srcImg = path.join(projectDir, 'source.jpg');

// One call: Bearer-auth GET /api/images/:id and write bytes to disk.
await fetchImageRefBytes('img-toolu_01ABC...-0', srcImg);

const imageUrl = await uploadUguu(srcImg);
require('fs').writeFileSync(path.join(projectDir, 'image_url.txt'), imageUrl);
console.log('PROJECT_DIR=' + projectDir);
console.log('IMAGE_URL=' + imageUrl);
```

### Step 3 — Scene 1 (Seedance, AGNT tool call)

Pass a **relative** filename. Pass the uguu URL as `firstFrameUrl`.

```
seedance_api(
  prompt:        "Slow cinematic dolly-in on the astronaut as she raises
                  a gloved hand to the hull — ancient cyan glyphs flare
                  under her touch. Anamorphic lens flare, 35mm grain.",
  firstFrameUrl: "<imageUrl from step 2>",
  filename:      "scifi_scene1.mp4",              ← RELATIVE
  duration:      5,
  aspectRatio:   "16:9",
  resolution:    "720p"
)
```

### Step 4 — copy scene 1 into project + extract/upload its last frame

```js
const { copySeedanceOutput } = require('C:/Users/Studio/.agnt/skills/image-to-cinematic-video/scripts/copy_seedance_output.js');
const { extractAndUploadLastFrame } = require('C:/Users/Studio/.agnt/skills/image-to-cinematic-video/scripts/extract_and_upload_last_frame.js');

const scene1 = copySeedanceOutput('scifi_scene1.mp4',
                                  path.join(projectDir, 'scenes/scene1.mp4'));
const { frameUrl: frame1Url } =
  await extractAndUploadLastFrame(scene1, projectDir, 'scene1');
console.log('FRAME1_URL=' + frame1Url);
```

### Step 5 — Scene 2 (Seedance, using frame1Url)

```
seedance_api(
  prompt:        "Continuous: the glyphs erupt into a cascade of runes,
                  a seam cracks open, warm white light pours out. Camera
                  pushes toward the widening doorway. 35mm grain.",
  firstFrameUrl: "<frame1Url>",
  filename:      "scifi_scene2.mp4",              ← RELATIVE
  duration:      5,
  aspectRatio:   "16:9",
  resolution:    "720p"
)
```

### Step 6 — copy + last-frame for scene 2

```js
const scene2 = copySeedanceOutput('scifi_scene2.mp4',
                                  path.join(projectDir, 'scenes/scene2.mp4'));
const { frameUrl: frame2Url } =
  await extractAndUploadLastFrame(scene2, projectDir, 'scene2');
```

### Step 7 — Scene 3 (Seedance, using frame2Url)

```
seedance_api(
  prompt:        "Continuous: camera pushes through the doorway into a
                  cathedral-scale interior of crystalline pillars pulsing
                  with light. Astronaut silhouette walks forward.
                  Volumetric god-rays, 35mm grain.",
  firstFrameUrl: "<frame2Url>",
  filename:      "scifi_scene3.mp4",              ← RELATIVE
  duration:      5,
  aspectRatio:   "16:9",
  resolution:    "720p"
)
```

### Step 8 — copy scene 3 + extract scene 3's last frame (thumbnail only — no upload needed)

```js
const { extractLastFrame } = require('C:/Users/Studio/.agnt/skills/image-to-cinematic-video/scripts/extract_and_upload_last_frame.js');
const scene3 = copySeedanceOutput('scifi_scene3.mp4',
                                  path.join(projectDir, 'scenes/scene3.mp4'));
extractLastFrame(scene3, path.join(projectDir, 'frames/scene3_last.jpg'));
```

### Step 9 — stitch + upload final + manifest

```js
const { stitchXfade } = require('C:/Users/Studio/.agnt/skills/image-to-cinematic-video/scripts/stitch_xfade.js');
const { writeProjectManifest } = require('C:/Users/Studio/.agnt/skills/image-to-cinematic-video/scripts/save_generated_image.js');

const finalPath = path.join(projectDir, 'final.mp4');
const { durationSec, sizeBytes } = await stitchXfade({
  clips: [scene1, scene2, scene3],
  outputPath: finalPath,
  fadeDuration: 0.15,                // chain mode — short fade
  transition: 'fade',
  crf: 18,
});

const finalUrl = await uploadUguu(finalPath);

writeProjectManifest(projectDir, {
  generatedAt: new Date().toISOString(),
  mode: 'chain',
  sourceImage: { provider: 'gemini', model: 'nano-banana-pro-preview' },
  scenes: [
    { n: 1, path: scene1, lastFrameUrl: frame1Url },
    { n: 2, path: scene2, lastFrameUrl: frame2Url },
    { n: 3, path: scene3, lastFrameUrl: null },
  ],
  stitch: { fadeDuration: 0.15, transition: 'fade', crf: 18 },
  final: { path: finalPath, url: finalUrl, durationSec, sizeBytes },
  totals: { cost: 2.268, resolution: '720p', aspectRatio: '16:9' },
});

console.log('FINAL_URL=' + finalUrl);
```

### Step 10 — reply to the user

```html
<video src="<finalUrl>" controls autoplay loop muted playsinline
       style="width:100%;max-width:720px;border-radius:12px;
              box-shadow:0 8px 32px rgba(0,0,0,0.4);background:#000;"></video>
```

Plus a crisp summary (mode, duration, resolution, cost, wall time) and
the absolute path to the project folder.

## PARALLEL mode (lighter variant)

Same as above except:

- All N seedance calls go into ONE tool-call block with
  `_executeAsync: true` so they run concurrently.
- Every scene uses the SAME `firstFrameUrl` (the source image URL).
- No last-frame extraction between scenes.
- Use `fadeDuration: 0.5` in `stitchXfade` (longer decorative fades
  make sense when seams visibly reset).

## Seedance parameters (both modes)

| Param          | Value                                            |
|----------------|--------------------------------------------------|
| `aspectRatio`  | `16:9` (landscape) or `9:16` (Reels/Shorts)      |
| `duration`     | `5` seconds (sweet spot for cost and fidelity)   |
| `resolution`   | `720p` ($0.151/s = $0.756 per 5s clip)           |
| `firstFrameUrl`| uguu URL — shared (parallel) or chained (chain)  |
| `filename`     | **RELATIVE** only, e.g. `scifi_scene1.mp4`       |

Prompt formula:
`[camera move] [subject] [action] [environment] [lighting] [style]`

20+ proven prompts: `references/seedance-prompt-library.md`.

## Image generation: which path to use

| Situation | Path |
|---|---|
| Default — best quality, agentic flow | **`generate_image` chat tool → `fetchImageRefBytes()`** |
| You want a specific provider available in the chat tool | `generate_image` (set `provider`/`model`) → `fetchImageRefBytes()` |
| You're running headless inside `execute_javascript_code` only and don't want to round-trip through the chat tool | `saveGeneratedImage()` (Pollinations / Gemini REST / OpenAI REST) |
| You already have an image URL (user upload, etc.) | Skip generation, just `uploadUguu()` it |

`fetchImageRefBytes(ref, outputPath)` does an authenticated
`GET /api/images/:ref` against the local AGNT server, validates the
response is binary image data (rejects HTML SPA fallbacks), and writes
the bytes to disk. Returns `{ path, sizeBytes, contentType }`.

## Verified result from the canyon-runes production run

| | Value |
|---|---|
| Source image | Gemini Nano Banana Pro via `generate_image` (776 KB JPG, 16:9) |
| Scenes | 5 (chained) |
| Scene duration | 5s each |
| Total after stitch | 24.6s |
| Resolution | 720p, 16:9 |
| Total Seedance cost | $3.78 |
| Final mp4 | 13.6 MB (libx264 CRF 18, 0.15s xfade) |
| Hosted at | `https://d.uguu.se/GAjyPqKB.mp4` |

Project folder layout after a successful run:

```
<PROJECTS_ROOT>/<stamp>_<slug>/
├── source.jpg                 ← bytes from /api/images/:id
├── image_url.txt              ← uguu URL of source.jpg
├── scenes/
│   ├── scene1.mp4
│   ├── scene2.mp4
│   └── scene3.mp4
├── frames/
│   ├── scene1_last.jpg        ← → became scene 2's firstFrameUrl
│   ├── scene2_last.jpg        ← → became scene 3's firstFrameUrl
│   └── scene3_last.jpg        ← thumbnail / poster
├── final.mp4                  ← stitched output
├── final_url.txt              ← uguu URL of final.mp4
└── manifest.json              ← complete run record
```

## Common failure modes (observed)

- **ENOENT on seedance output** → you passed an absolute filename.
  Use a relative one, then `copySeedanceOutput()`.
- **"Invalid base64 image url" from Seedance** → the upload host you
  used isn't serving the file. Use `uploadUguu()`.
- **Stitched video is three disconnected cuts instead of one take** →
  you forgot to swap `firstFrameUrl` between scenes. Use
  `extractAndUploadLastFrame()` after every scene except the last.
- **Frame extraction gives a tiny/empty jpg** → the video has no
  keyframe at exactly `-0`. The `-sseof -0.1` offset in
  `extract_and_upload_last_frame.js` already handles this.
- **`fetchImageRefBytes()` returns HTML instead of an image** → the
  `Authorization: Bearer ${AGNT_AUTH_TOKEN}` header was dropped, so
  the server fell back to serving the SPA. The helper detects this
  (content-type sniff + magic-byte check) and throws — re-check the
  header and the ref id.

Files in this skill

  • SKILL.md15.2 KB
  • references/seedance-prompt-library.md7.2 KB
  • scripts/copy_seedance_output.js2.4 KB
  • scripts/extract_and_upload_last_frame.js2.9 KB
  • scripts/save_generated_image.js11 KB
  • scripts/stitch_xfade.js3.8 KB
  • scripts/upload_uguu.js4.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.