Michael Loh·

Save Your Best Workflows: Skills in Hedra

Save Your Best Workflows: Skills in Hedra

At Hedra, we are building agents that help you ideate and execute your creative ideas. Our agent now can help you anywhere in the creative process. It can research competitors, brainstorm new campaign ideas, define a new creative direction, explore new directions visually, and when you’re ready, generate production ready assets. That experience is meant to feel simple on the surface, even when the request underneath is not.

Over time, many of you will find yourself typing the same kind of request again and again: a storyboard-style sequence, a social-ready export, a house style you want every generation to follow. That repetition is natural. The hard part is not the big idea. It is remembering the exact wording, the order of steps, and the little rules that made last week’s result look right.

This is why we are launching skills: a simple way to save those repeatable workflows so you can trigger them with a short command instead of rewriting the same brief every time. You do not need to be a programmer, and you do not need to know how “agents” work under the hood. Think of a skill as a saved playbook the assistant follows whenever you invoke it.

What skills are (and why they help)

A skill is a saved playbook for Hedra’s agent: a short name you type after /, a description of what it is for, and a body with the real instructions—goals, steps, examples, guardrails—written like a creative brief, not code. When you use it, you are not pasting that whole brief every time; you run the playbook and add only what changes for this run. That is faster than re-explaining a multi-step workflow from scratch, keeps recurring formats and house rules—lighting, aspect ratio, “keep the product hero in frame”—consistent, and turns the description into a quick note to Future You about what to type after the command. Solo or on a team, it is also an easy way to share “how we do things here” without maintaining a separate prompt document.

After the skill name, you keep chatting in plain language—what to polish, which story to tell shot by shot, how to trim or export the clip. The skill holds the part of the brief that stays the same; your message holds what is different this time. It is still a normal conversation, not a rigid form to fill out.

How Hedra combines what you type with the skill behind the scenes belongs in a separate, more technical blog post for readers who want that depth. The rest of this piece focuses on practical use and how to create a skill.

How to add a skill in Hedra

  1. In the chat where you talk to the assistant, type / (slash) to open the command menu.

  2. Select Create skill

  3. Fill in the fields:

    • Name — Short, memorable, and easy to type. This becomes your slash command (for example, polish-shot, shot-list, or post-process). Use lowercase letters and hyphens if you like; avoid spaces.

    • Description — One or two sentences a human would understand: what the skill does and when to use it. This text often appears when you browse or search for skills, so write it for yourself and your collaborators, not for machines.

    • Body — The full playbook: what the assistant should optimize for, how it should break the work into steps, what to avoid, and any examples. You can paste structured text from a template or from the examples below.

  4. Save the skill. After that, you can invoke it with /your-skill-name followed by whatever details belong to this specific job. A little further down, the Example commands subsection lists ready-made lines you can try with the three sample skills.

Three skills you can start with today

Here are 3 skills or varying complexity that the Hedra team finds useful and are excited about:

Start with polish-shot if you want a gentle first example: it is short, readable, and shows how a creative brief becomes a reusable slash command. The next two are shot list and post-process playbooks our internal team actually uses. They are much longer because they walk the assistant through multi-step workflows; keep them in mind when you are ready to level up.

Polish shot — Takes a lackluster still and nudges it toward something stylish, clean, and campaign-ready without changing what the photo is fundamentally about.

Shot list — Turns an idea (or a starting image) into up to six connected images that read like a simple storyboard, with continuity from shot to shot.

Post-process — Handles practical edits on media you already have: trim, speed change, resize for a platform, extract a frame or audio, stitch clips, fades, and similar operations—without asking the AI to “regenerate” the whole thing.

Example commands

After you created you skills you can test them by using these examples:

  • /polish-shot @image1

  • /shot-list of a woman getting ready for dinner in 4 shots

  • /post-process trim the first 5 seconds of the video

  • /post-process compress for twitter

@image1 is a stand-in for an image you already have in the thread; your client may label assets differently, but the idea is the same: point the skill at the still you want polished.

Copy name, description and Markdown Instructions inside each block per example and paste it into your Create skill dialog.

Example: Polish shot (polish-shot)

name: polish-shot

description: Improve a rough or flat photo into a stylish, clean, professional image while keeping the subject, story, and commercial intent intact.

Markdown Instructions:

## Arguments

Optional free text after the command (for example brand tone, what to fix, or the audience). Parse $ARGUMENTS as supplemental creative direction; it may be empty.

## Workflow

1. If the user did not attach an image and did not clearly reference an existing image asset, ask them to attach or pick one. Do not proceed without a source image.
2. Call analyze_asset on the input image when it helps you understand subject, composition, and what needs improvement.
3. Call modify_image once to produce a polished variant. Build the internal modification brief from Goals, Style direction, Priorities, Avoid, and Output expectation below, woven together with any notes from $ARGUMENTS.
4. Call check_generation_status(wait=true), then confirm the result in plain language.

Do not mention tool names, asset IDs, or technical parameters in your reply to the user.

## Goals

- Make the image look stylish, clean, and professional
- Preserve the original subject, composition intent, and core product/story
- Improve visual clarity, cohesion, and aesthetic quality
- Keep the result realistic and commercially usable

## Style direction

- Clean composition
- Premium lighting
- Tasteful contrast
- Balanced color grading
- Modern, minimal, editorial feel
- Crisp details without looking overprocessed
- Subtle, intentional background cleanup if needed

## Priorities

1. Preserve the identity of the main subject
2. Remove distracting clutter or visual noise
3. Improve lighting, tone, and overall balance
4. Make the result feel elevated and brand-ready
5. Keep the image natural and believable

## Avoid

- Overly artificial beauty retouching
- Heavy filters or exaggerated color shifts
- Unrealistic skin, textures, or materials
- Overly busy backgrounds
- Excessive sharpening or HDR-like processing
- Changing the subject, product, or core scene unless explicitly requested

## Output expectation

Return a polished version of the photo that looks more stylish, clean, and professional, suitable for marketing, brand, social, or campaign use.

Example: Shot List (shot-list)

name: shot-list
description: Turn your idea into a simple sequence of up to 6 connected images so that a story flows smoothly from one moment to the next.

Markdown Instructions:
## Allowed Tools

This skill uses ONLY: `analyze_asset`, `generate_image`, `modify_image`, `check_generation_status`, `ask_clarifying_questions`.

## Arguments

If no arguments are passed in exit the skill and request the user pass in more information. You can give them an example like,

"Try /shot-list of a nike shoe show a man grabbing a shoe for a run. Or try /shot-list of a woman getting ready for dinner in 4 shots"

Parse `$ARGUMENTS` as:

| Arg | Default | Description |
|-----|---------|-------------|
| `scenes` | (required) | Ordered list of 1 to 6 scene beats. The parsing LLM should infer and fill this list from the user's request. Each item should describe one shot in the sequence and how the scene progresses. |
| `aspect_ratio` | `""` | Optional aspect ratio override for all generated shots. If empty and a starting image exists, preserve that image's aspect ratio. Otherwise default to `16:9`. |

## GOAL

- Turn the user's request into a short sequence of images that reads like a cinematic storyboard.
- Use the parser-produced `scenes` list as the source of truth for the sequence.
- Keep strong continuity across the sequence: the same subject, objects, styling, and world should carry forward unless the user explicitly asks for a change.
- Every new shot after the opening shot must use the immediately previous shot as its only visual reference.

## Scenes Extrapolation EXAMPLE

Good `scenes` shape for "a nike shoe show a man grabbing a shoe for a run":

```text
[
  "Close setup on the nike shoe in the closet",
  "The man reaches toward the shoe",
  "The man ties the shoe before leaving",
  "The man runs through the park"
]
```

If the user request is too vague to infer a useful ordered list, use `ask_clarifying_questions` instead of inventing scenes.

## General Instruction For Which Asset To Use

Use only assets generated during this request unless the user explicitly attached or referenced an existing asset for this task.

There are only 2 circumstances when it is required you use previously existing assets as references for this current task:
1. When assets are attached by the user you can only use those attached image assets as possible starting references for this task.
2. When the user implicitly or explicitly references an existing image asset in the prompt, select the best matching image asset and only use that as the starting reference for this task.

Do not mix unrelated prior assets into the shot chain.

## Prompt Composition Rules

For each shot, write an internal prompt in this labeled format:

```text
Subject: ...
Composition: ...
Action: ...
Location: ...
Style: ...
Camera & Lighting: ...
Continuity Notes: ...
```

Rules:
- Every shot prompt must be self-contained and concrete.
- Each later shot should advance the scene by one beat, not jump wildly to a different world.
- Preserve identity, wardrobe, product details, environment logic, and mood unless the user asked to change them.
- If the sequence starts from an attached or existing image, describe the next beat as a natural progression from that image.
- `Continuity Notes` should explicitly say what must carry over from the previous shot.

## TASKS

### task identify_starting_reference

Determine how the sequence begins:

1. If the user attached image assets, use the single best attached image as the opening reference.
2. If the user referenced an existing image asset in the prompt, use that as the opening reference.
3. Otherwise there is no opening reference and you must create shot 1 from scratch.

If multiple attached or referenced images could plausibly be the starting point and the choice materially changes the sequence, use `ask_clarifying_questions`.

### task inspect_reference

If a starting reference exists, call `analyze_asset` on it.

Extract:
- main subject and important props
- current framing and crop
- environment and styling
- visual continuity details that must persist
- nearest supported aspect ratio

If `{{ aspect_ratio }}` is present, it overrides the reference aspect ratio. Otherwise preserve the reference aspect ratio.

### task validate_scenes

Before generating anything, validate that `scenes` is an ordered progression:

{% for scene in scenes %}
- Shot {{ loop.index }}: {{ scene }}
{% endfor %}

Validation rules:
- `scenes` must contain at least 1 item and no more than 6 items.
- Each item should represent one beat in the sequence.
- The first item should establish the opening shot.
- Later items should progress naturally from the previous item.
- The final item should feel like the payoff or end of the mini-sequence.

If the parsed list is clearly malformed or too vague to use, ask the user for clarification instead of guessing.

### task generate_sequence

Maintain an internal variable called `previous_shot_asset`.

{% for scene in scenes %}
#### Shot {{ loop.index }}

Scene beat: **{{ scene }}**

Write the internal labeled shot prompt for this beat using:
- the current scene beat
- continuity from the previous shot{% if loop.first %}{% if aspect_ratio %} and aspect ratio **{{ aspect_ratio }}**{% endif %}{% else %}
- continuity from `previous_shot_asset`
{% endif %}

{% if loop.first %}
If there is no starting reference:
- Call `generate_image` once for shot {{ loop.index }} using the full prompt for "{{ scene }}".
- Then call `check_generation_status(wait=true)` and store the result as `previous_shot_asset`.

If there is a starting reference:
- If the starting reference already cleanly serves as shot {{ loop.index }}, record it as shot {{ loop.index }} and set it as `previous_shot_asset` without regenerating it.
- Otherwise call `modify_image` once to turn the starting reference into shot {{ loop.index }} for "{{ scene }}".
- Then call `check_generation_status(wait=true)` and store the result as `previous_shot_asset`.
{% else %}
Use `modify_image` once for shot {{ loop.index }} with `previous_shot_asset` as the only reference input.

After the generation call:
- call `check_generation_status(wait=true)`
- store the completed image as the new `previous_shot_asset`
- only then continue to shot {{ loop.index + 1 if not loop.last else loop.index }}
{% endif %}
{% endfor %}

Do not parallelize these calls.
Do not skip `check_generation_status(wait=true)`.
Do not use any image older than `previous_shot_asset` once a newer shot exists. Each shot must chain directly from the immediately previous shot.

### task respond

Tell the user the shot list is ready and briefly summarize the sequence as a numbered list with one short line per shot.

Keep the response concise:
- mention the total shot count
- mention the opening beat and final payoff
- mention that each shot was chained from the previous image when generation occurred

Do not mention tool names, asset IDs, or JSON.

Once all tasks are DONE, this skill is DONE. Stop and wait.

## Guardrails

- This is a sequential continuity workflow, not a batch-variants workflow.
- `scenes` is the authoritative sequence definition. Do not invent extra shots outside that list.
- Never generate multiple shots from one prompt.
- Never generate multiple images in parallel.
- If the user wants radically different options rather than a continuous sequence, this is the wrong skill.
- Preserve continuity aggressively across subject identity, product details, wardrobe, and environment.
- If the user's prompt is too vague to infer meaningful scenes, use `ask_clarifying_questions` instead of guessing.
- If a starting asset is not an image or cannot support a visual continuity chain, ask the user for an image or offer to start from scratch from text.

Example: Post Process (post-process)

This playbook is longer because it covers many edit types and platform presets. Paste it the same way as above.

name: post-process
description: Trim, compress, speed up, extract frames, add fades, convert formats, stitch clips, resize for different platforms, and more.

Markdown Instructions:
## Arguments

Parse `$ARGUMENTS` as:

| Arg | Default | Description |
|-----|---------|-------------|
| `edit_request` | (required) | Natural language description of the post-processing operation. Can describe one or multiple operations to chain. |
| `platform` | `""` | Target platform for optimization. One of: `youtube`, `twitter`, `linkedin`, `instagram-feed`, `instagram-reels`, `tiktok`, `web`. Empty means general-purpose output. |

{% if not edit_request %}
## NO ARGUMENTS PROVIDED

STOP. Do not call any tools. Do not look for assets. Do not proceed.

Reply with a short message asking what the user wants to do. Give examples:

"What would you like to post-process? Try `/post-process trim the first 5 seconds` or `/post-process speed this up 2x` or `/post-process compress this for twitter`"

This skill is DONE. Stop and wait for the user's next message.
{% else %}
## Allowed Tools

This skill uses ONLY these tools:
- `run_code` — load assets, run ffmpeg/ffprobe commands, and save outputs (all in one call)
- `ask_clarifying_questions` — ask user for missing info
- `check_generation_status` — check if a referenced asset is ready
- `list_generations` — find recently generated assets

Use `run_code` with `language: "bash"` for all ffmpeg and ffprobe operations. Pass `input_asset_ids` to load assets into the sandbox, and `save_outputs` to save result files as Hedra assets.

Do NOT use `generate_video`, `generate_image`, `modify_image`, `modify_video`, or any AI generation tool — even if an asset failed or is not ready. If the asset is unavailable, tell the user and stop.

## GOAL

Process the user's post-processing request: **{{ edit_request }}**{% if platform %} optimized for **{{ platform }}**{% endif %}

This skill handles ALL deterministic media post-processing via ffmpeg in the code sandbox. No AI models are involved — this replaces the workflow of downloading an asset and processing it in an external tool.

{% if platform %}
## PLATFORM OPTIMIZATION: {{ platform | upper }}

These are safe upload defaults for organic content. Actual limits may vary by account type and surface.

{% if platform == "youtube" %}
YouTube re-encodes everything, so upload the highest quality you can:
- **Resolution**: up to 4K, prefer 1920x1080
- **Max size**: 256GB / 12 hours
- **Codec**: H.264 High profile, level 4.0
- **Audio**: AAC 192kbps, 48kHz
- **Flags**: `-profile:v high -level 4.0 -bf 2 -g 30 -movflags +faststart`
- **CRF**: 18 (high quality, YouTube will re-encode anyway)

For YouTube Shorts: resize to 1080x1920 (9:16 vertical).
{% endif %}

{% if platform == "twitter" %}
Twitter has strict limits:
- **Resolution**: max 1920x1200, prefer 1280x720
- **Max size**: 512MB / 140 seconds
- **Codec**: H.264 Main profile, level 3.1
- **Audio**: AAC 128kbps, 44.1kHz
- **Flags**: `-profile:v main -level 3.1 -movflags +faststart`
- **CRF**: 24 (balance quality vs file size)
- **Tip**: Use `-fs 15M` to cap file size for fast uploads
{% endif %}

{% if platform == "linkedin" %}
LinkedIn prefers MP4 with AAC audio:
- **Resolution**: max 4096x2304, prefer 1920x1080
- **Max size**: 5GB / 10 minutes
- **Codec**: H.264 Main profile
- **Audio**: AAC 192kbps, 48kHz
- **Flags**: `-profile:v main -movflags +faststart`
- **CRF**: 22
{% endif %}

{% if platform == "instagram-feed" %}
Instagram Feed:
- **Resolution**: max 1080x1350 (4:5 portrait works best)
- **Max size**: 4GB / 60 seconds
- **Audio**: AAC 48kHz
- **CRF**: 22
{% endif %}

{% if platform == "instagram-reels" or platform == "tiktok" %}
{{ platform | title }}:
- **Resolution**: 1080x1920 (9:16 vertical)
- **Max size**: {% if platform == "tiktok" %}287MB / 10 minutes{% else %}4GB / 90 seconds{% endif %}
- **Audio**: AAC 48kHz
- **CRF**: 22
{% endif %}

{% if platform == "web" %}
Optimize for fast loading and broad browser support:
- **Resolution**: 1280x720 (balance quality/bandwidth)
- **Codec**: H.264 Baseline profile, level 3.1
- **Audio**: AAC 128kbps
- **Flags**: `-profile:v baseline -level 3.1 -movflags +faststart`
- **CRF**: 26 (small file size priority)
{% endif %}

Apply these constraints to the output in addition to any edit operations. After the edit, re-encode to match the platform specs above.
{% endif %}

## FFMPEG RECIPE REFERENCE

Use these battle-tested recipes as building blocks. Pick the recipe(s) that match "{{ edit_request }}", adapt parameters as needed, and chain operations when the user asks for multiple edits.

### Trim / Cut

```bash
# Trim from start time for a duration (recommended, always re-encode)
ffmpeg -y -ss <start> -i "<input>" -t <duration> \\
  -map 0:v:0? -map 0:a:0? \\
  -c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p \\
  -c:a aac -b:a 128k -movflags +faststart \\
  /home/user/.flora/workspace/output.mp4

# Trim between two timestamps
ffmpeg -y -ss <start> -i "<input>" -to <end> \\
  -map 0:v:0? -map 0:a:0? \\
  -c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p \\
  -c:a aac -b:a 128k -movflags +faststart \\
  /home/user/.flora/workspace/output.mp4
```

Always re-encode when trimming. Stream copy (`-c copy`) snaps to keyframes and produces wrong durations.

### Speed Up / Slow Down

```bash
# With audio (pitch-corrected via atempo)
ffmpeg -y -i "<input>" \\
  -filter_complex "[0:v]setpts=<1/speed>*PTS[v];[0:a]atempo=<speed>[a]" \\
  -map "[v]" -map "[a]" \\
  -c:v libx264 -preset fast -crf 18 \\
  -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# Without audio (extreme speeds or silent clips)
ffmpeg -y -i "<input>" \\
  -filter:v "setpts=<1/speed>*PTS" -an \\
  -c:v libx264 -preset fast -crf 18 \\
  /home/user/.flora/workspace/output.mp4
```

`atempo` supports 0.5-100.0. For extreme speeds, chain filters:
- 0.25x: `atempo=0.5,atempo=0.5`
- 4x: `atempo=2.0,atempo=2.0`
- 8x: `atempo=2.0,atempo=2.0,atempo=2.0`

Calculate: `setpts` multiplier = `1 / speed`. E.g. 3x speed = `setpts=0.333*PTS`, `atempo=3.0`.

### Extract Frame

```bash
# Last frame
ffmpeg -y -sseof -0.1 -i "<input>" -frames:v 1 -q:v 2 \\
  /home/user/.flora/workspace/frame.jpg

# First frame
ffmpeg -y -ss 0 -i "<input>" -frames:v 1 -q:v 2 \\
  /home/user/.flora/workspace/frame.jpg

# Frame at specific timestamp
ffmpeg -y -ss <time> -i "<input>" -frames:v 1 -q:v 2 \\
  /home/user/.flora/workspace/frame.jpg
```

Output is always JPEG. Use `asset_type: image` when saving.

### Resize

```bash
# Letterbox (maintain aspect ratio, add black bars)
ffmpeg -y -i "<input>" \\
  -vf "scale=<w>:<h>:force_original_aspect_ratio=decrease,pad=<w>:<h>:(ow-iw)/2:(oh-ih)/2" \\
  -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# Crop to fill (maintain aspect ratio, crop edges)
ffmpeg -y -i "<input>" \\
  -vf "scale=<w>:<h>:force_original_aspect_ratio=increase,crop=<w>:<h>" \\
  -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# Scale to width, auto height (keeps ratio)
ffmpeg -y -i "<input>" \\
  -vf "scale=<w>:-2" \\
  -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4
```

Common dimensions: 16:9=1920x1080, 9:16=1080x1920, 1:1=1080x1080, 4:3=1440x1080, 4:5=1080x1350.

### Compress

| Use case | CRF | Preset | Notes |
|----------|-----|--------|-------|
| Archive/master | 18 | slow | Best quality, large file |
| Production | 20-22 | medium | Good balance |
| Web/social | 23-25 | fast | Smaller files |
| Draft/preview | 28+ | veryfast | Fast encoding, lower quality |

```bash
# General compression
ffmpeg -y -i "<input>" \\
  -c:v libx264 -crf <crf> -preset <preset> \\
  -c:a aac -b:a 128k -movflags +faststart \\
  /home/user/.flora/workspace/output.mp4
```

### Rotate / Flip

```bash
# 90 degrees clockwise
ffmpeg -y -i "<input>" -vf "transpose=1" \\
  -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# 90 degrees counter-clockwise
ffmpeg -y -i "<input>" -vf "transpose=2" \\
  -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# 180 degrees
ffmpeg -y -i "<input>" -vf "transpose=1,transpose=1" \\
  -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# Mirror (horizontal flip)
ffmpeg -y -i "<input>" -vf "hflip" \\
  -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# Vertical flip
ffmpeg -y -i "<input>" -vf "vflip" \\
  -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4
```

### Extract Audio

```bash
# To MP3
ffmpeg -y -i "<input>" -vn -acodec libmp3lame -q:a 2 \\
  /home/user/.flora/workspace/output.mp3

# To WAV (uncompressed)
ffmpeg -y -i "<input>" -vn \\
  /home/user/.flora/workspace/output.wav

# To AAC
ffmpeg -y -i "<input>" -vn -acodec aac -b:a 192k \\
  /home/user/.flora/workspace/output.m4a
```

Use `asset_type: audio` when saving.

### Convert Audio Formats

```bash
# M4A to MP3
ffmpeg -y -i "<input>" -codec:a libmp3lame -qscale:a 2 \\
  /home/user/.flora/workspace/output.mp3

# WAV to MP3
ffmpeg -y -i "<input>" -codec:a libmp3lame -b:a 192k \\
  /home/user/.flora/workspace/output.mp3

# Adjust volume (1.5 = 50% louder, 0.5 = 50% quieter)
# For video with audio:
ffmpeg -y -i "<input>" -af "volume=<factor>" \\
  -c:v copy -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4
# For audio-only input:
ffmpeg -y -i "<input>" -af "volume=<factor>" \\
  -codec:a libmp3lame -b:a 192k \\
  /home/user/.flora/workspace/output.mp3
```

### Convert to GIF

The platform does not support GIF assets. Create a short, silent MP4 clip instead (web players typically auto-loop short clips):

```bash
# Short silent MP4 clip (do NOT output .gif — not supported)
ffmpeg -y -i "<input>" -t 5 \\
  -vf "fps=15,scale=480:-1:flags=lanczos" \\
  -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -an -movflags +faststart \\
  /home/user/.flora/workspace/output.mp4
```

Save with `asset_type: "video"`. Tell the user this is a short video clip optimized for looping playback, not a true GIF file.

### Fade In / Fade Out

```bash
# Video fade (need total duration from ffprobe first)
ffmpeg -y -i "<input>" \\
  -vf "fade=t=in:st=0:d=<fade_seconds>,fade=t=out:st=<total_duration - fade_seconds>:d=<fade_seconds>" \\
  -c:v libx264 -preset fast -crf 18 -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# Audio fade only (keeps video unchanged — use on video files, not audio-only)
ffmpeg -y -i "<input>" \\
  -af "afade=t=in:st=0:d=<fade_seconds>,afade=t=out:st=<total_duration - fade_seconds>:d=<fade_seconds>" \\
  -c:v copy -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# Both video and audio fade
ffmpeg -y -i "<input>" \\
  -vf "fade=t=in:st=0:d=<fade_seconds>,fade=t=out:st=<total_duration - fade_seconds>:d=<fade_seconds>" \\
  -af "afade=t=in:st=0:d=<fade_seconds>,afade=t=out:st=<total_duration - fade_seconds>:d=<fade_seconds>" \\
  -c:v libx264 -preset fast -crf 18 \\
  -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4
```

### Concatenate / Stitch

For multiple clips, re-encode to a common format first, then concatenate:

```bash
# Step 1: Re-encode each clip to match the first clip's resolution
ffmpeg -y -i "<clip_N_path>" \\
  -vf "scale=<w>:<h>:force_original_aspect_ratio=decrease,pad=<w>:<h>:(ow-iw)/2:(oh-ih)/2,fps=30,setsar=1" \\
  -c:v libx264 -preset fast -crf 18 \\
  -c:a aac -b:a 192k -ar 44100 \\
  "/home/user/.flora/workspace/clip_N.mp4"

# Step 2: Create file list and concatenate
echo "file 'clip_1.mp4'" > /home/user/.flora/workspace/list.txt
echo "file 'clip_2.mp4'" >> /home/user/.flora/workspace/list.txt
ffmpeg -y -f concat -safe 0 -i /home/user/.flora/workspace/list.txt \\
  -c copy /home/user/.flora/workspace/output.mp4
```

### Reverse

```bash
# With audio
ffmpeg -y -i "<input>" \\
  -vf "reverse" -af "areverse" \\
  -c:v libx264 -preset fast -crf 18 \\
  -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# Without audio
ffmpeg -y -i "<input>" \\
  -vf "reverse" -an \\
  -c:v libx264 -preset fast -crf 18 \\
  /home/user/.flora/workspace/output.mp4
```

The `reverse` filter loads the entire video into memory. Warn users for videos over 30 seconds.

### Add Audio to Video

```bash
# Mix with existing audio
ffmpeg -y -i "<video>" -i "<audio>" \\
  -filter_complex "[0:a][1:a]amix=inputs=2:duration=shortest[a]" \\
  -map 0:v -map "[a]" \\
  -c:v copy -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4

# Replace audio entirely
ffmpeg -y -i "<video>" -i "<audio>" \\
  -map 0:v -map 1:a -shortest \\
  -c:v copy -c:a aac -b:a 192k \\
  /home/user/.flora/workspace/output.mp4
```

### Image Operations

Preserve the input format by default. Use `.png` for PNG/WebP inputs (keeps alpha/transparency), `.jpg` for JPEG inputs. Only convert format when the user explicitly asks.

```bash
# Rotate image 90 degrees clockwise (match input extension)
ffmpeg -y -i "<input>" -vf "transpose=1" /home/user/.flora/workspace/output.<ext>

# Resize image
ffmpeg -y -i "<input>" -vf "scale=<w>:<h>" /home/user/.flora/workspace/output.<ext>

# Convert format (e.g. PNG to JPEG — only when explicitly requested)
ffmpeg -y -i "<input>" /home/user/.flora/workspace/output.jpg
```

Use `asset_type: image` when saving image outputs.

## COMMON ISSUES

When ffmpeg fails, check the stderr for these patterns:

| Error | Cause | Fix |
|-------|-------|-----|
| "Stream map '0:a:0' matches no streams" | Video has no audio track | Use `-map 0:v:0? -map 0:a:0?` with `?` suffix, or add `-an` |
| "height not divisible by 2" | Odd pixel dimensions | Add `-vf "scale=trunc(iw/2)*2:trunc(ih/2)*2"` |
| "Invalid data found" | Corrupted input file | Tell user the file may be corrupted |
| "encoder not found" | Missing codec | Fall back to `-c:v libx264` |
| Output 0 bytes | Silent failure | Check full stderr output |
| Audio out of sync | Speed change without filter_complex | Use `filter_complex` with both video and audio streams |

Always validate the output exists and has non-zero size before saving.

## TASKS

### task identify_assets

Determine which assets the user wants to edit:
1. If the user attached assets, use them.
2. If the user references specific assets (@video1, @image2), use those.
3. If the user says "the last video" or "that image", find the most recent matching asset.
4. If ambiguous or no assets are available, use `ask_clarifying_questions`.

The user wants: **{{ edit_request }}**{% if platform %} (optimized for {{ platform }}){% endif %}

Figure out which recipe(s) from the FFMPEG RECIPE REFERENCE best match this request.

### task probe_assets

Call `run_code` with `language: "bash"` and `input_asset_ids` to download the asset(s) into the sandbox. The result's `input_assets[].sandbox_path` tells you the exact file path — use that path, do not guess the extension.

```bash
ffprobe -v quiet -print_format json -show_format -show_streams "<sandbox_path from input_assets>"
```

Extract: duration, width, height, codec, audio streams, file size. You need this to:
- Validate timestamps (for trim/extract operations)
- Determine current resolution (for resize operations)
- **Check for audio streams** — if no audio stream exists, use the "without audio" recipe variant (add `-an` or drop audio filters). This prevents "matches no streams" errors.
- Calculate speed factor durations
- Calculate fade timing (need total duration)

### task execute_edit

Pick the recipe(s) from the FFMPEG RECIPE REFERENCE that match "{{ edit_request }}". Adapt parameters based on the probe results.

{% if platform %}
After the main edit, apply the {{ platform }} optimization constraints from the PLATFORM OPTIMIZATION section above. This may mean re-encoding with specific profile/level/CRF settings.
{% endif %}

The sandbox persists across `run_code` calls — files loaded in the probe step are still available. You do NOT need to pass `input_asset_ids` again.

Call `run_code` with `language: "bash"` and the ffmpeg command(s). If chaining multiple operations:
- Prefer combining into ONE ffmpeg command with multiple filters (avoids intermediate files and quality loss)
- Only use sequential commands when operations fundamentally can't be combined (e.g. concat requires pre-normalized clips)

For long encodes (concat, reverse, heavy re-encodes on long videos), set `timeout_seconds: 120` (the sandbox default). The outer tool timeout is 180s, so sandbox commands beyond that will be cut off.

Pass `save_outputs` to save result files as assets:
- `asset_type: "video"` for video outputs
- `asset_type: "image"` for frame/image outputs
- `asset_type: "audio"` for audio extractions

After execution, check the `run_code` result in this order:
1. **Top-level `error`**: If non-null, the sandbox itself failed (e.g. asset download error). Tell the user and stop.
2. **`exit_code`**: If non-zero, ffmpeg failed. Check the COMMON ISSUES table and retry ONCE with the suggested fix.
3. **`saved_outputs[].error`**: If any entry has a non-null `error` (e.g. `AudioModerationError`), the ffmpeg command succeeded but the file could not be saved. Tell the user the edit worked but saving failed due to a platform issue.

### task respond

Tell the user their edit is ready. Briefly describe what was done (e.g. "trimmed to 0:05-0:15, sped up 2x, and compressed for web").{% if platform %} Mention it was optimized for {{ platform }}.{% endif %}

Do not mention tool names, sandbox paths, ffmpeg commands, or technical parameters.

Once all tasks are DONE, this skill is DONE. Stop and wait.

## Guardrails

- NEVER use `generate_video`, `generate_image`, `modify_image`, `modify_video`, or any AI generation tool. This is deterministic processing only. If an asset is not ready, failed, or unavailable, explain this to the user and stop. Do NOT attempt to regenerate or create new content.
- ALWAYS use `run_code` with `language: "bash"` for ffmpeg and ffprobe operations.
- Produce exactly what the user asked for. Do not generate extra variations or bonus outputs.
- Use `-preset fast -crf 18` for quality by default. Only use higher CRF when the user asks for compression or a platform requires it.
- Use `-pix_fmt yuv420p` and `-movflags +faststart` for video outputs to ensure browser compatibility.
- Use `-map 0:v:0? -map 0:a:0?` with the `?` suffix to handle missing audio streams gracefully.
- If the user's request is ambiguous, use `ask_clarifying_questions` rather than guessing.
- If a referenced asset is still generating, ask the user to wait and stop. Do not proceed until the asset is ready.
- For operations that load entire videos into memory (reverse, complex filters), warn users about long processing times on videos over 30 seconds.
- If ffmpeg fails, retry ONCE with adjusted parameters based on the COMMON ISSUES table. If the retry also fails, explain the error to the user and stop.
- When chaining operations, prefer a single ffmpeg command with multiple filters over sequential commands. This is faster and avoids quality loss from multiple re-encodes.
- Audio extraction may fail at save time due to platform moderation. If the `run_code` result shows a save error for an audio file, tell the user the ffmpeg extraction succeeded but saving was blocked by a platform issue.
- Always validate the output file exists and has non-zero size before including it in `save_outputs`.
{% endif %}