# FFmpeg Desk > Paste the ffmpeg command you are about to run. A real command-line reader parses it in your > browser — free, offline, no account — and then one of four AI lanes reads it, walks its filter > graph, sorts out its subtitles or rewrites it for delivery. Live at: https://ffmpeg-desk.skillsafe.ai/ API tutorial: https://ffmpeg-desk.skillsafe.ai/api.html Token panel: https://ffmpeg-desk.skillsafe.ai/tokens.html ## The task this app exists for Take the ffmpeg command you are about to run on a real render — the whole command line, `-filter_complex` graph and all — and get it correct, explained and delivery-ready before you burn an hour of encode time. ## What is free, offline and needs no account Everything below runs in the visitor's own browser tab. Nothing is uploaded and nothing is executed. **A shell-accurate tokeniser.** Single quotes are literal; double quotes honour backslash escapes; a backslash-newline is a line continuation and not an argument; a pasted `$` shell prompt is stripped; an unquoted `;`, `|` or `&` is recorded as the command-ending hazard it is, because an unquoted filtergraph is not a filtergraph — it is two shell commands, and the second one runs. Every token records whether it was quoted, and the rules read that back. **An explicit option arity table.** ffmpeg exposes no grammar from the outside, so the table of which options take a value is the parser's spine: getting one entry wrong shifts every following token by one and produces a confident, wrong answer. An option the reader does not recognise is *reported* (rule R30) rather than guessed at silently. **The three regions ffmpeg actually reads.** `ffmpeg [global] {[input opts] -i INPUT}... {[output opts] OUTPUT}...`. Each option is assigned to the region it decorates. This is the whole point, because the same flag means different things in different places: `-ss` in front of an input is a fast input seek and after it is a decode-and-discard output seek; `-b:v` in front of the first `-i` is read as a demuxer option and does nothing at all. No regex over the string can establish either. **The filter graph, parsed as a graph.** Chains split on `;`, filters split on `,`, both respecting single-quoted sections and backslash escapes. Pad labels are read off both ends of each filter, links inside a chain are inferred where labels are absent, and a stream reference like `0:v` is resolved back to a real input. Then the structural questions are asked: is a label consumed twice (one pad can only feed one place — that needs a `split`), defined twice, or consumed but never defined; is a labelled output pad left unmapped, which stops ffmpeg before the first frame. The pad arity of the filters whose count is computed rather than fixed — `split`, `asplit`, `concat`, `amix`, `amerge`, `hstack`, `vstack`, `xstack`, `overlay`, `blend`, `paletteuse` — is checked too. **The graph as a picture and as a `.dot` file.** A deterministic layered layout (longest path from a source), drawn inline as SVG, and exported for Graphviz. **Every `-map` resolved** against the declared inputs and the graph's labels, with the ones that point at nothing named. **Thirty-two checks**, each reported as pass, fail or not-applicable with its evidence: | Rule | What it checks | | --- | --- | | R01 | it is an ffmpeg command at all | | R02 | there is an output | | R03 | `-y` or `-n` decides the overwrite prompt | | R04 | no output-only option is stranded in front of an input | | R05 | stream copy is not combined with filtering | | R06 | output-side `-ss` is not paired with `-c copy` | | R07 | H.264/HEVC into an MP4-family container is pinned to 8-bit 4:2:0 | | R08 | scaled dimensions stay even under 4:2:0 chroma (`-2`, not `-1`) | | R09 | only one rate control is asked for (`-crf` versus `-b:v`) | | R10 | VP9 constant quality also sets `-b:v 0` | | R11 | MP4/MOV output carries `-movflags +faststart` | | R12 | the container will actually mux the named codecs | | R13 | `-preset` and `-tune` are values the encoder knows | | R14 | the `subtitles=` path survives ffmpeg's own double escaping | | R15 | a subtitle input has an explicit destination codec | | R16 | every `-map` points at something that exists | | R17 | every labelled filter output is mapped | | R18 | the graph's labels are wired correctly | | R19 | each filter gets the number of pads it takes | | R20 | the shell hands ffmpeg the command you wrote | | R21 | GIF output goes through `palettegen`/`paletteuse` | | R22 | a looped input has something that stops it | | R23 | a two-pass first pass discards its output | | R24 | nothing deprecated or ambiguous (`-vsync`, bare `-b`, bare `-qscale`, `-vcodec`) | | R25 | hardware decoding hands frames to filters that can take them | | R26 | `-re` is only used when feeding something in real time | | R27 | a streamed output names its muxer with `-f` | | R28 | ffmpeg can tell what format to write | | R29 | the concat demuxer is allowed to open its list (`-safe 0`) | | R30 | every option on the line was recognised | | R31 | no option is left decorating nothing | | R32 | every bare word really is an output, not a fragment of a split argument | **Credentials, found first.** An RTMP ingest URL ends in a live stream key; a signed URL carries a signature; `-headers` can carry an Authorization header. Any of these is reported above everything else, masked, with "rotate it" — and the model is instructed to substitute a placeholder rather than repeat the value. **Eight free exports**, no account: the read-out as Markdown, the checks as CSV, the flags as CSV, every token classified as CSV, the filter table as CSV, the parse as JSON, the filter graph as Graphviz `.dot`, and the same command reformatted onto readable lines as a runnable `.sh`. ## The four lanes Every lane takes the same work object — one ffmpeg command line — and every lane returns a runnable command. The run input carries `task` plus `command`, `target`, `context_note` and the `facts` block the browser parsed. - **`read`** — what the command actually does: every option group in order with what it does *here* and whether it is correct, suspect, redundant, ignored or deprecated; where every stream goes from input through filter to output; and what will dominate the wall-clock. - **`graph`** — the `-filter_complex` node by node: what each filter contributes, what each label carries, and the reorderings that make it cheaper or clearer (scale before overlay, `fps` before an expensive filter). If there is no `-filter_complex`, it shows how the `-vf` chain reads as one. - **`subs`** — burn-in versus soft mux, what each player will actually show, the container rules (MP4 takes `mov_text` and nothing else; Matroska takes `srt` and `ass`; WebM takes `webvtt`), and the double escaping that makes a `subtitles=` filter fail to find its file. - **`deliver`** — the command to ship for a chosen target (`web`, `hls`, `social`, `archive`, `audio`, `gif`, `stream` or `general`), with the diff against yours, the trade-offs and runnable checks. Each result offers the next lane as a button, which pre-fills it from the current one. ## How the model is held to the parse 1. The browser parses first and the facts go into the run. The model must return one `reconciliation` entry per critical and high flag **by id**, so a defect it ignored is displayed as unaccounted for rather than quietly dropped. 2. Every input, output, filter node, graph label, rule id and option flag it cites is checked against what the command contains; one that does not exist is shown, marked, rather than trusted. On the graph lane, a filter node the reading does not mention is listed — a node nobody read is a node nobody checked. 3. Every lane returns a runnable command, and that command goes straight back through the same reader before it is shown. A rewrite that still trips R07, or that introduces a check the pasted command passed, says so under the rewrite. ## What it deliberately does not do It executes nothing, probes nothing and fetches nothing. It has never seen your media, so it does not know the source resolution, duration, frame rate, bitrate or the output's file size, and the model is instructed not to invent them — what CRF 23 means is knowledge about encoders and is fair game; "about 40 MB" would be a fabrication. It does not know which encoders your build was compiled with. It validates pad wiring and the arity of filters whose pad count is knowable, not the arguments inside every filter, so a wrong `drawtext` expression passes the browser's checks. It never rewrites a file: every command it produces is text for you to read and run yourself. ## Source skills FFmpeg Desk is a derived work crediting four published agent skills: - `@mindmorass/ffmpeg-patterns` — transcoding, filtering, extraction and batch media patterns. - `@bobmatnyc/media-transcoding` — preset-driven conversions for web, mobile and archive delivery. - `@josiahsiegel/ffmpeg-filter-complex-patterns` — the filter-graph wiring idioms. - `@josiahsiegel/ffmpeg-captions-subtitles` — burning, muxing and styling subtitles and captions. The in-browser tokeniser, the option arity table, the region split, the filter-graph parser, the layout, the rule engine, the flags and all eight exports are an independent implementation written for this app. Not affiliated with those authors or with the FFmpeg project. FFmpeg is a trademark of Fabrice Bellard. ## Costs and accounts The whole browser side is free and needs no account. An AI lane is metered: a worst-case amount is reserved before the run and only what the run actually uses is charged. Three of the four bundled examples replay a saved run at zero cost, which covers every lane.