[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6DF2MWHZPY22M1E4QX4HXG9":3,"skill-same:01M6DF2MWHZPY22M1E4QX4HXG9":69},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":14,"repositoryHtmlUrl":52,"repositoryHomepageUrl":6,"ownerHtmlUrl":53,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":54,"install":55,"resources":68},"01M6DF2MWHZPY22M1E4QX4HXG9","video-convert",null,"Use when converting a video\u002Faudio file's FORMAT, CODEC, or CONTAINER in media-studio (mp4\u002Fmkv\u002Fmov\u002Fwebm\u002Fgif, h264\u002Fh265\u002Fvp9\u002Fav1, or audio-only extraction to mp3\u002Fm4a\u002Fwav\u002Fflac\u002Fopus) — single file or batch, or normalizing a non-Chromium-playable source for playback (remux vs h264-720p proxy). Covers the real ffmpeg argv builder, codec\u002FCRF\u002Fbitrate\u002Fpix_fmt choices, faststart, container\u003C->codec legality, and the progress\u002Fseek\u002Ftimebase pitfalls that actually bite. DO NOT use this skill for: cutting\u002Ftrimming\u002Fsplitting clips (use reframe-video-cutting), reframing 16:9->9:16 (use reframe), burning subtitles\u002Fcaptions, watermark\u002Flogo removal (use video-watermark-removal), or pure thumbnail\u002Fframe extraction. Also NOT a general 'run any ffmpeg command' helper — it documents media-studio's specific, frozen convert.* \u002F media.* contracts.","Prekzursil\u002Fagent-skills-toolchain","Prekzursil","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F54636077?v=4","01M6DACZ1PCQHD99SCR1YXG28B","Andrei Mihai Visalon","01M6DACZ670WZF535V3WYYSS4Z",0,"MIT","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Fskills\u002Fvideo-convert\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"video convert","Video","convert","use","reframe","flac","mp3","do","general","builder","container","only","documents","mp4","logo","Captions","Audio","Media","proxy","subtitles","2026-11-10 10:12:30",false,"---\nname: video-convert\ndescription: \"Use when converting a video\u002Faudio file's FORMAT, CODEC, or CONTAINER in media-studio (mp4\u002Fmkv\u002Fmov\u002Fwebm\u002Fgif, h264\u002Fh265\u002Fvp9\u002Fav1, or audio-only extraction to mp3\u002Fm4a\u002Fwav\u002Fflac\u002Fopus) — single file or batch, or normalizing a non-Chromium-playable source for playback (remux vs h264-720p proxy). Covers the real ffmpeg argv builder, codec\u002FCRF\u002Fbitrate\u002Fpix_fmt choices, faststart, container\u003C->codec legality, and the progress\u002Fseek\u002Ftimebase pitfalls that actually bite. DO NOT use this skill for: cutting\u002Ftrimming\u002Fsplitting clips (use reframe-video-cutting), reframing 16:9->9:16 (use reframe), burning subtitles\u002Fcaptions, watermark\u002Flogo removal (use video-watermark-removal), or pure thumbnail\u002Fframe extraction. Also NOT a general 'run any ffmpeg command' helper — it documents media-studio's specific, frozen convert.* \u002F media.* contracts.\"\n---\n\n# Video Convert (format \u002F codec \u002F container) — media-studio\n\nConvert format\u002Fcodec\u002Fcontainer, extract audio, batch-convert, and produce a Chromium-playable derivative (remux or h264-720p proxy). This skill documents the **real** media-studio implementation — every command below is what the code actually emits. No invented flags.\n\n## Source modules (read these for ground truth)\n- `C:\u002FUsers\u002FPrekzursil\u002FDocuments\u002FGitHub\u002FReframe\u002Fsidecar\u002Fmedia_studio\u002Fffmpeg.py` — **the argv builder** (`build_convert_argv`), `build_probe_argv`, `ffprobe_duration`, and the progress-streaming `run()`. This is where the convert command is actually assembled.\n- `C:\u002FUsers\u002FPrekzursil\u002FDocuments\u002FGitHub\u002FReframe\u002Fsidecar\u002Fmedia_studio\u002Ffeatures\u002Fconvert.py` — `convert.start` \u002F `convert.batch` feature layer: pure output-path derivation (`output_path`), audio-ext map, `convert_one` \u002F `convert_batch`, job handlers.\n- `C:\u002FUsers\u002FPrekzursil\u002FDocuments\u002FGitHub\u002FReframe\u002Fsidecar\u002Fmedia_studio\u002Ffeatures\u002Fmedia_compat.py` — `media.playable` \u002F `media.proxy.start`: codec-driven playability `classify`, `build_remux_argv`, `build_proxy_argv`, proxy cache.\n- `C:\u002FUsers\u002FPrekzursil\u002FDocuments\u002FGitHub\u002FReframe\u002Fsidecar\u002Fmedia_studio\u002Ffeatures\u002Fexport_presets.py` — `exportPresets.*` catalog (platform export targets; aspect + 20-60s window, NOT codec).\n\n## When to use which path (pick fast)\n1. **User wants a different format\u002Fcodec\u002Fcontainer, or audio extraction** -> `convert.start` \u002F `convert.batch` (this is \"convert\"). Builder = `ffmpeg.build_convert_argv` (convert.py:182).\n2. **User just wants a file to PLAY in the app and it won't** (HEVC, WMV, MPEG-2, or h264-in-MKV) -> `media.playable` then `media.proxy.start`. The verdict is **codec-driven, not extension-driven** (media_compat.py:162 `classify`):\n   - all streams Chromium-playable AND container playable -> `playable` (play original, build nothing).\n   - all streams playable but container isn't (e.g. h264-in-MKV) -> `remux` (`-c copy` into mp4, cheap).\n   - ANY stream unplayable (HEVC even inside MKV) -> `proxy` (h264 720p transcode).\n   Playable video codecs: `h264, vp8, vp9, av1`; audio: `aac, mp3, opus, vorbis, flac, pcm_*` (media_compat.py:59-61). Anything else => proxy.\n\n## The convert option contract (frozen — CONTRACTS.md §2)\n`convert.start({videoId|path, options, out?})` \u002F `convert.batch({items:[...]})`. `options` keys (convert.py:23-25, consumed in ffmpeg.py:122-169):\n\n| key | effect in argv | notes |\n|-----|----------------|-------|\n| `container` | output **extension** only (mp4\u002Fmkv\u002Fmov\u002Fwebm\u002Fgif\u002F...) | ffmpeg picks the muxer from the out file extension. NOT validated against codec. |\n| `vcodec` | `-c:v \u003Cvcodec>` | e.g. `libx264`, `libx265`, `libvpx-vp9`, `libaom-av1`. Omit => ffmpeg default for the container. |\n| `acodec` | `-c:a \u003Cacodec>` | e.g. `aac`, `libmp3lame`, `libopus`. |\n| `crf` | `-crf \u003Ccrf>` | video path only. NOT clamped\u002Fvalidated. Encoder-specific scale. |\n| `scale` | `-vf scale=W:H` | accepts `\"1280:720\"` OR `\"1280x720\"` (`x` -> `:`, ffmpeg.py:159). |\n| `fps` | `-r \u003Cfps>` | output frame rate. |\n| `audioOnly` | adds `-vn`, drops all video flags | extract audio. Only `acodec` applies. |\n| `audioFormat` | drives the output **extension** (not a codec flag) | mapped to ext in convert.py:61-84. |\n\nThere is **no bitrate, no preset, no pix_fmt, no -map, no faststart** key. If you need those, the convert path does not provide them — see Pitfalls 1, 2, 5.\n\n## The real convert command\n`ffmpeg.build_convert_argv(in, out, options, settings)` produces exactly (ffmpeg.py:137-169):\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cin>\n   [ -vn ]                              # audioOnly\n   [ -c:v \u003Cvcodec> ]                    # video path only\n   [ -c:a \u003Cacodec> ]\n   [ -crf \u003Ccrf> ]                       # video path only\n   [ -vf scale=\u003CW>:\u003CH> ]                # video path only; \"WxH\" normalized to \"W:H\"\n   [ -r \u003Cfps> ]                         # video path only\n   -progress pipe:1 -nostats \u003Cout>\n```\n\n- Always argv **list**, never `shell=True` (paths with spaces are one element — safe). The binary is resolved settings.ffmpegPath -> `MEDIA_STUDIO_FFMPEG` env -> bundled `resources\u002Fbin\u002F` -> PATH (ffmpeg.py:48-92).\n- `-progress pipe:1 -nostats` makes stdout a `key=value` stream; `run()` (ffmpeg.py:224) turns `out_time*` into a 0-100 pct against the ffprobe duration.\n\n### Output path derivation (convert.py:94-125 `output_path`)\n- explicit `out` -> used verbatim (caller owns the name).\n- else `\u003Cin stem>.\u003Cext>` in the **source's own directory**, where `ext` = `audioFormat` ext when `audioOnly`, else `container` ext.\n- if the derived dest equals the source (same dir+stem+ext, e.g. re-encoding `a.mp4` -> mp4 in place) it becomes `\u003Cstem>.converted.\u003Cext>` so it never clobbers the original.\n- audioFormat->ext map (convert.py:61-76): `mp3\u002Flibmp3lame`->mp3, `aac\u002Fm4a\u002Falac`->m4a, `wav\u002Fpcm`->wav, `flac`->flac, `opus\u002Flibopus`->opus, `vorbis\u002Flibvorbis\u002Fogg`->ogg, `ac3`->ac3. Unknown value used **verbatim** as the extension (default video `mp4`, default audio `m4a`).\n\n## Worked examples\n\n**1. Re-encode an MKV to a web-friendly mp4 (h264 + aac, CRF 20, 720p):**\n```jsonc\n\u002F\u002F convert.start params\n{ \"path\": \"C:\u002Fclips\u002Fraw recording.mkv\",\n  \"options\": { \"container\": \"mp4\", \"vcodec\": \"libx264\", \"acodec\": \"aac\",\n               \"crf\": 20, \"scale\": \"1280x720\" } }\n```\nEmitted argv (note the space in the path is one safe element):\n```\nffmpeg -hide_banner -nostdin -y -i \"C:\u002Fclips\u002Fraw recording.mkv\" \\\n   -c:v libx264 -c:a aac -crf 20 -vf scale=1280:720 \\\n   -progress pipe:1 -nostats \"C:\u002Fclips\u002Fraw recording.mp4\"\n```\n> Add `pix_fmt yuv420p` + faststart yourself if this must stream\u002Fplay everywhere — the convert builder does NOT (Pitfalls 1 & 2). If you can, prefer routing such \"make it play\" cases through `media.proxy.start`, which DOES set both.\n\n**2. Extract audio to mp3:**\n```jsonc\n{ \"path\": \"C:\u002Fclips\u002Ftalk.mp4\", \"options\": { \"audioOnly\": true, \"audioFormat\": \"mp3\", \"acodec\": \"libmp3lame\" } }\n```\n-> `ffmpeg -hide_banner -nostdin -y -i talk.mp4 -vn -c:a libmp3lame -progress pipe:1 -nostats talk.mp3`\n\n**3. Batch convert a folder to mp4:** `convert.batch({items:[{path,options},...]})`. Progress is spread evenly: item *i* of *n* reports `(i*100 + per_item_pct)\u002Fn` (convert.py:219-225), so a 4-item batch climbs 0->25->50->... Cancellation is honored **between** items and within an item.\n\n**4. Make a non-playable source playable (the proxy path):** call `media.playable({videoId})`; if `{playable:false, reason:\"video codec not Chromium-playable: hevc\"}`, call `media.proxy.start({videoId})`. It runs the **proxy** transcode (media_compat.py:243-281):\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cin> -map 0:v:0 -map 0:a:0? \\\n   -c:v libx264 -preset veryfast -crf 23 -vf scale=-2:720 -pix_fmt yuv420p \\\n   -c:a aac -b:a 128k -movflags +faststart -progress pipe:1 -nostats \u003Cout>\n```\nFor h264-in-MKV it instead **remuxes** (media_compat.py:210-240): `-map 0 -map -0:s -map -0:d -map -0:t -c copy -movflags +faststart` (drops subtitle\u002Fdata\u002Fattachment streams that aren't mp4-legal under stream copy). Result is cached at `%APPDATA%\u002Fmedia-studio\u002Fproxies\u002F\u003CvideoId>-\u003Cmtime_ns>.mp4`; the mtime IS the cache invalidation (re-edit the source -> new path; stale siblings evicted). The original is always kept for convert\u002Fexport — the proxy is playback-only.\n\n## Pitfalls (the ones that actually bite)\n\n1. **No `pix_fmt` in convert -> may not play in Chromium\u002FQuickTime.** `build_convert_argv` never sets `-pix_fmt`. x264 will happily emit `yuv444p` or 10-bit for some inputs\u002Foptions, which the Electron renderer (and Safari\u002FQuickTime) refuse to decode. The proxy path forces `yuv420p` for exactly this reason (media_compat.py:269). If a converted mp4 \"won't play\", suspect chroma\u002Fbit-depth first. Workaround: pass it through `media.proxy.start`, or add `-pix_fmt yuv420p` if you build the argv directly.\n\n2. **No `-movflags +faststart` in convert -> mp4 not progressively streamable.** convert output keeps the `moov` atom at the END, so a `\u003Cvideo>` tag can't begin until the whole file is fetched. Remux and proxy add `+faststart` (media_compat.py:235, 275); plain convert does not. Fine for local files; bad for serve-over-HTTP.\n\n3. **Container\u002Fcodec legality is NOT validated.** `container` only sets the output extension; the muxer is chosen from it. `container:\"webm\"` + `vcodec:\"libx264\"` makes ffmpeg fail (\"Only VP8\u002FVP9\u002FAV1... supported for WebM\"); `container:\"mp4\"` + `vcodec:\"libvpx-vp9\"` is also illegal. The failure surfaces as a non-zero exit -> `RuntimeError` (convert.py:189). Match codec to container: mp4\u002Fmov -> h264\u002Fh265\u002Fav1 + aac; webm -> vp8\u002Fvp9\u002Fav1 + opus\u002Fvorbis; mkv -> almost anything.\n\n4. **GIF needs special handling the convert builder doesn't give you.** `container:\"gif\"` just changes the extension; without a palette filter ffmpeg uses a generic 256-color quantization -> banding\u002Fdither artifacts, and audio flags are meaningless. For a quality GIF you need a two-pass `palettegen`\u002F`paletteuse` filtergraph, which this contract can't express. Treat GIF export as out-of-scope for plain convert.\n\n5. **`-progress pipe:1` puts the progress stream on STDOUT — never `capture_output` without draining stderr.** `run()` (ffmpeg.py:246-272) captures stderr=PIPE and **drains it on a daemon thread**. ffmpeg chats continuously on stderr even with `-nostats` (banner, stream info, font logs); if you ever rebuild this and forget to drain, the ~64KB stderr pipe fills and the encode **blocks forever** — this was the real \"stuck at clip 1\u002F5 for 29 min\" bug (ffmpeg.py:254-258). On non-zero exit the last ~8 stderr lines are logged for diagnosis.\n\n6. **`out_time_ms` is actually MICROSECONDS.** ffmpeg's `-progress` field is historically mislabeled; the code divides by `1_000_000`, not `1000` (ffmpeg.py:202-218). If you parse progress yourself, don't trust the field name.\n\n7. **A failed\u002Fzero duration probe degrades progress to coarse, it does NOT fail the job.** `convert_one` swallows a probe exception and sets `total_sec=0` (convert.py:176-180); with no total, `run()` can't compute a percentage and only emits the final `100.0 \u002F \"done\"` on `progress=end`. Long convert with a \"frozen at 0%\" bar usually means ffprobe couldn't read a duration (streaming source, no container duration) — the encode is still progressing.\n\n8. **`media.playable` is codec-driven; extension lies.** A `.mp4` containing HEVC is `proxy`, and a `.mkv` containing h264 is `remux`, not `playable`. Also: ffprobe reports BOTH `.webm` and `.mkv` as `matroska,webm` (one demuxer), so classify disambiguates by file extension (media_compat.py:148-159) — a real `.webm` whose streams pass is playable, `.mkv` goes to remux. Never decide playability from the extension.\n\n9. **Re-encoding in place is auto-renamed, not blocked.** `a.mp4` -> mp4 in the same dir becomes `a.converted.mp4` (convert.py:122-124). If you expected to overwrite, pass an explicit `out`. Conversely an explicit `out` is verbatim with `-y` -> it WILL overwrite whatever is there.\n\n10. **No `-map` on convert -> default-stream selection.** convert relies on ffmpeg's implicit stream mapping (best video + best audio of the chosen codecs). Multi-audio \u002F multi-subtitle sources lose the non-default tracks. The remux path is the only one that explicitly maps (`-map 0` minus subs\u002Fdata\u002Fattachments). If track preservation matters, convert is the wrong tool.\n\n11. **`exportPresets` are platform\u002Faspect targets, NOT codec settings.** `export_presets.py` stores `{aspect, minSec, maxSec, count, captionStyle, reframeEngine}` with the window hard-clamped to 20-60s (export_presets.py:54-55, 137-138) and aspect guarded to the curated social set (9:16\u002F1:1\u002F4:5). They feed the short-maker\u002Freframe pipeline, not `convert.*`. Don't conflate \"export preset\" with \"convert options\" — they share no fields.\n\n## GPU \u002F HF-model notes\n- **No GPU, no HF model is involved.** Convert, remux, and proxy are pure CPU ffmpeg (`libx264 -preset veryfast` for the proxy). There is no NVENC\u002FQSV\u002FVAAPI hardware-encode path in the contract; adding `-c:v h264_nvenc` would require routing it through `vcodec` and a matching ffmpeg build, and it is unvalidated. For batch throughput, parallelism is the lever, not hardware encode — but note `convert_batch` runs items **sequentially** within one job (convert.py:215-238); concurrency would be at the job-registry level.\n\n## Confidence\nHIGH — every command, option key, codec table, cache path, and pitfall is read directly from the four cited source files (ffmpeg.py, convert.py, media_compat.py, export_presets.py). The convert builder, proxy\u002Fremux argvs, ext maps, window clamp, and the stderr-drain\u002Fmicrosecond\u002Ffaststart\u002Fpix_fmt gaps are verbatim from the code. Behavior of ffmpeg muxers (container\u002Fcodec legality, GIF palette) is standard ffmpeg, noted as such where the media-studio contract does not enforce it.\n","# Video Convert (format \u002F codec \u002F container) — media-studio\n\nConvert format\u002Fcodec\u002Fcontainer, extract audio, batch-convert, and produce a Chromium-playable derivative (remux or h264-720p proxy). This skill documents the **real** media-studio implementation — every command below is what the code actually emits. No invented flags.\n\n## Source modules (read these for ground truth)\n- `C:\u002FUsers\u002FPrekzursil\u002FDocuments\u002FGitHub\u002FReframe\u002Fsidecar\u002Fmedia_studio\u002Fffmpeg.py` — **the argv builder** (`build_convert_argv`), `build_probe_argv`, `ffprobe_duration`, and the progress-streaming `run()`. This is where the convert command is actually assembled.\n- `C:\u002FUsers\u002FPrekzursil\u002FDocuments\u002FGitHub\u002FReframe\u002Fsidecar\u002Fmedia_studio\u002Ffeatures\u002Fconvert.py` — `convert.start` \u002F `convert.batch` feature layer: pure output-path derivation (`output_path`), audio-ext map, `convert_one` \u002F `convert_batch`, job handlers.\n- `C:\u002FUsers\u002FPrekzursil\u002FDocuments\u002FGitHub\u002FReframe\u002Fsidecar\u002Fmedia_studio\u002Ffeatures\u002Fmedia_compat.py` — `media.playable` \u002F `media.proxy.start`: codec-driven playability `classify`, `build_remux_argv`, `build_proxy_argv`, proxy cache.\n- `C:\u002FUsers\u002FPrekzursil\u002FDocuments\u002FGitHub\u002FReframe\u002Fsidecar\u002Fmedia_studio\u002Ffeatures\u002Fexport_presets.py` — `exportPresets.*` catalog (platform export targets; aspect + 20-60s window, NOT codec).\n\n## When to use which path (pick fast)\n1. **User wants a different format\u002Fcodec\u002Fcontainer, or audio extraction** -> `convert.start` \u002F `convert.batch` (this is \"convert\"). Builder = `ffmpeg.build_convert_argv` (convert.py:182).\n2. **User just wants a file to PLAY in the app and it won't** (HEVC, WMV, MPEG-2, or h264-in-MKV) -> `media.playable` then `media.proxy.start`. The verdict is **codec-driven, not extension-driven** (media_compat.py:162 `classify`):\n   - all streams Chromium-playable AND container playable -> `playable` (play original, build nothing).\n   - all streams playable but container isn't (e.g. h264-in-MKV) -> `remux` (`-c copy` into mp4, cheap).\n   - ANY stream unplayable (HEVC even inside MKV) -> `proxy` (h264 720p transcode).\n   Playable video codecs: `h264, vp8, vp9, av1`; audio: `aac, mp3, opus, vorbis, flac, pcm_*` (media_compat.py:59-61). Anything else => proxy.\n\n## The convert option contract (frozen — CONTRACTS.md §2)\n`convert.start({videoId|path, options, out?})` \u002F `convert.batch({items:[...]})`. `options` keys (convert.py:23-25, consumed in ffmpeg.py:122-169):\n\n| key | effect in argv | notes |\n|-----|----------------|-------|\n| `container` | output **extension** only (mp4\u002Fmkv\u002Fmov\u002Fwebm\u002Fgif\u002F...) | ffmpeg picks the muxer from the out file extension. NOT validated against codec. |\n| `vcodec` | `-c:v \u003Cvcodec>` | e.g. `libx264`, `libx265`, `libvpx-vp9`, `libaom-av1`. Omit => ffmpeg default for the container. |\n| `acodec` | `-c:a \u003Cacodec>` | e.g. `aac`, `libmp3lame`, `libopus`. |\n| `crf` | `-crf \u003Ccrf>` | video path only. NOT clamped\u002Fvalidated. Encoder-specific scale. |\n| `scale` | `-vf scale=W:H` | accepts `\"1280:720\"` OR `\"1280x720\"` (`x` -> `:`, ffmpeg.py:159). |\n| `fps` | `-r \u003Cfps>` | output frame rate. |\n| `audioOnly` | adds `-vn`, drops all video flags | extract audio. Only `acodec` applies. |\n| `audioFormat` | drives the output **extension** (not a codec flag) | mapped to ext in convert.py:61-84. |\n\nThere is **no bitrate, no preset, no pix_fmt, no -map, no faststart** key. If you need those, the convert path does not provide them — see Pitfalls 1, 2, 5.\n\n## The real convert command\n`ffmpeg.build_convert_argv(in, out, options, settings)` produces exactly (ffmpeg.py:137-169):\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cin>\n   [ -vn ]                              # audioOnly\n   [ -c:v \u003Cvcodec> ]                    # video path only\n   [ -c:a \u003Cacodec> ]\n   [ -crf \u003Ccrf> ]                       # video path only\n   [ -vf scale=\u003CW>:\u003CH> ]                # video path only; \"WxH\" normalized to \"W:H\"\n   [ -r \u003Cfps> ]                         # video path only\n   -progress pipe:1 -nostats \u003Cout>\n```\n\n- Always argv **list**, never `shell=True` (paths with spaces are one element — safe). The binary is resolved settings.ffmpegPath -> `MEDIA_STUDIO_FFMPEG` env -> bundled `resources\u002Fbin\u002F` -> PATH (ffmpeg.py:48-92).\n- `-progress pipe:1 -nostats` makes stdout a `key=value` stream; `run()` (ffmpeg.py:224) turns `out_time*` into a 0-100 pct against the ffprobe duration.\n\n### Output path derivation (convert.py:94-125 `output_path`)\n- explicit `out` -> used verbatim (caller owns the name).\n- else `\u003Cin stem>.\u003Cext>` in the **source's own directory**, where `ext` = `audioFormat` ext when `audioOnly`, else `container` ext.\n- if the derived dest equals the source (same dir+stem+ext, e.g. re-encoding `a.mp4` -> mp4 in place) it becomes `\u003Cstem>.converted.\u003Cext>` so it never clobbers the original.\n- audioFormat->ext map (convert.py:61-76): `mp3\u002Flibmp3lame`->mp3, `aac\u002Fm4a\u002Falac`->m4a, `wav\u002Fpcm`->wav, `flac`->flac, `opus\u002Flibopus`->opus, `vorbis\u002Flibvorbis\u002Fogg`->ogg, `ac3`->ac3. Unknown value used **verbatim** as the extension (default video `mp4`, default audio `m4a`).\n\n## Worked examples\n\n**1. Re-encode an MKV to a web-friendly mp4 (h264 + aac, CRF 20, 720p):**\n```jsonc\n\u002F\u002F convert.start params\n{ \"path\": \"C:\u002Fclips\u002Fraw recording.mkv\",\n  \"options\": { \"container\": \"mp4\", \"vcodec\": \"libx264\", \"acodec\": \"aac\",\n               \"crf\": 20, \"scale\": \"1280x720\" } }\n```\nEmitted argv (note the space in the path is one safe element):\n```\nffmpeg -hide_banner -nostdin -y -i \"C:\u002Fclips\u002Fraw recording.mkv\" \\\n   -c:v libx264 -c:a aac -crf 20 -vf scale=1280:720 \\\n   -progress pipe:1 -nostats \"C:\u002Fclips\u002Fraw recording.mp4\"\n```\n> Add `pix_fmt yuv420p` + faststart yourself if this must stream\u002Fplay everywhere — the convert builder does NOT (Pitfalls 1 & 2). If you can, prefer routing such \"make it play\" cases through `media.proxy.start`, which DOES set both.\n\n**2. Extract audio to mp3:**\n```jsonc\n{ \"path\": \"C:\u002Fclips\u002Ftalk.mp4\", \"options\": { \"audioOnly\": true, \"audioFormat\": \"mp3\", \"acodec\": \"libmp3lame\" } }\n```\n-> `ffmpeg -hide_banner -nostdin -y -i talk.mp4 -vn -c:a libmp3lame -progress pipe:1 -nostats talk.mp3`\n\n**3. Batch convert a folder to mp4:** `convert.batch({items:[{path,options},...]})`. Progress is spread evenly: item *i* of *n* reports `(i*100 + per_item_pct)\u002Fn` (convert.py:219-225), so a 4-item batch climbs 0->25->50->... Cancellation is honored **between** items and within an item.\n\n**4. Make a non-playable source playable (the proxy path):** call `media.playable({videoId})`; if `{playable:false, reason:\"video codec not Chromium-playable: hevc\"}`, call `media.proxy.start({videoId})`. It runs the **proxy** transcode (media_compat.py:243-281):\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cin> -map 0:v:0 -map 0:a:0? \\\n   -c:v libx264 -preset veryfast -crf 23 -vf scale=-2:720 -pix_fmt yuv420p \\\n   -c:a aac -b:a 128k -movflags +faststart -progress pipe:1 -nostats \u003Cout>\n```\nFor h264-in-MKV it instead **remuxes** (media_compat.py:210-240): `-map 0 -map -0:s -map -0:d -map -0:t -c copy -movflags +faststart` (drops subtitle\u002Fdata\u002Fattachment streams that aren't mp4-legal under stream copy). Result is cached at `%APPDATA%\u002Fmedia-studio\u002Fproxies\u002F\u003CvideoId>-\u003Cmtime_ns>.mp4`; the mtime IS the cache invalidation (re-edit the source -> new path; stale siblings evicted). The original is always kept for convert\u002Fexport — the proxy is playback-only.\n\n## Pitfalls (the ones that actually bite)\n\n1. **No `pix_fmt` in convert -> may not play in Chromium\u002FQuickTime.** `build_convert_argv` never sets `-pix_fmt`. x264 will happily emit `yuv444p` or 10-bit for some inputs\u002Foptions, which the Electron renderer (and Safari\u002FQuickTime) refuse to decode. The proxy path forces `yuv420p` for exactly this reason (media_compat.py:269). If a converted mp4 \"won't play\", suspect chroma\u002Fbit-depth first. Workaround: pass it through `media.proxy.start`, or add `-pix_fmt yuv420p` if you build the argv directly.\n\n2. **No `-movflags +faststart` in convert -> mp4 not progressively streamable.** convert output keeps the `moov` atom at the END, so a `\u003Cvideo>` tag can't begin until the whole file is fetched. Remux and proxy add `+faststart` (media_compat.py:235, 275); plain convert does not. Fine for local files; bad for serve-over-HTTP.\n\n3. **Container\u002Fcodec legality is NOT validated.** `container` only sets the output extension; the muxer is chosen from it. `container:\"webm\"` + `vcodec:\"libx264\"` makes ffmpeg fail (\"Only VP8\u002FVP9\u002FAV1... supported for WebM\"); `container:\"mp4\"` + `vcodec:\"libvpx-vp9\"` is also illegal. The failure surfaces as a non-zero exit -> `RuntimeError` (convert.py:189). Match codec to container: mp4\u002Fmov -> h264\u002Fh265\u002Fav1 + aac; webm -> vp8\u002Fvp9\u002Fav1 + opus\u002Fvorbis; mkv -> almost anything.\n\n4. **GIF needs special handling the convert builder doesn't give you.** `container:\"gif\"` just changes the extension; without a palette filter ffmpeg uses a generic 256-color quantization -> banding\u002Fdither artifacts, and audio flags are meaningless. For a quality GIF you need a two-pass `palettegen`\u002F`paletteuse` filtergraph, which this contract can't express. Treat GIF export as out-of-scope for plain convert.\n\n5. **`-progress pipe:1` puts the progress stream on STDOUT — never `capture_output` without draining stderr.** `run()` (ffmpeg.py:246-272) captures stderr=PIPE and **drains it on a daemon thread**. ffmpeg chats continuously on stderr even with `-nostats` (banner, stream info, font logs); if you ever rebuild this and forget to drain, the ~64KB stderr pipe fills and the encode **blocks forever** — this was the real \"stuck at clip 1\u002F5 for 29 min\" bug (ffmpeg.py:254-258). On non-zero exit the last ~8 stderr lines are logged for diagnosis.\n\n6. **`out_time_ms` is actually MICROSECONDS.** ffmpeg's `-progress` field is historically mislabeled; the code divides by `1_000_000`, not `1000` (ffmpeg.py:202-218). If you parse progress yourself, don't trust the field name.\n\n7. **A failed\u002Fzero duration probe degrades progress to coarse, it does NOT fail the job.** `convert_one` swallows a probe exception and sets `total_sec=0` (convert.py:176-180); with no total, `run()` can't compute a percentage and only emits the final `100.0 \u002F \"done\"` on `progress=end`. Long convert with a \"frozen at 0%\" bar usually means ffprobe couldn't read a duration (streaming source, no container duration) — the encode is still progressing.\n\n8. **`media.playable` is codec-driven; extension lies.** A `.mp4` containing HEVC is `proxy`, and a `.mkv` containing h264 is `remux`, not `playable`. Also: ffprobe reports BOTH `.webm` and `.mkv` as `matroska,webm` (one demuxer), so classify disambiguates by file extension (media_compat.py:148-159) — a real `.webm` whose streams pass is playable, `.mkv` goes to remux. Never decide playability from the extension.\n\n9. **Re-encoding in place is auto-renamed, not blocked.** `a.mp4` -> mp4 in the same dir becomes `a.converted.mp4` (convert.py:122-124). If you expected to overwrite, pass an explicit `out`. Conversely an explicit `out` is verbatim with `-y` -> it WILL overwrite whatever is there.\n\n10. **No `-map` on convert -> default-stream selection.** convert relies on ffmpeg's implicit stream mapping (best video + best audio of the chosen codecs). Multi-audio \u002F multi-subtitle sources lose the non-default tracks. The remux path is the only one that explicitly maps (`-map 0` minus subs\u002Fdata\u002Fattachments). If track preservation matters, convert is the wrong tool.\n\n11. **`exportPresets` are platform\u002Faspect targets, NOT codec settings.** `export_presets.py` stores `{aspect, minSec, maxSec, count, captionStyle, reframeEngine}` with the window hard-clamped to 20-60s (export_presets.py:54-55, 137-138) and aspect guarded to the curated social set (9:16\u002F1:1\u002F4:5). They feed the short-maker\u002Freframe pipeline, not `convert.*`. Don't conflate \"export preset\" with \"convert options\" — they share no fields.\n\n## GPU \u002F HF-model notes\n- **No GPU, no HF model is involved.** Convert, remux, and proxy are pure CPU ffmpeg (`libx264 -preset veryfast` for the proxy). There is no NVENC\u002FQSV\u002FVAAPI hardware-encode path in the contract; adding `-c:v h264_nvenc` would require routing it through `vcodec` and a matching ffmpeg build, and it is unvalidated. For batch throughput, parallelism is the lever, not hardware encode — but note `convert_batch` runs items **sequentially** within one job (convert.py:215-238); concurrency would be at the job-registry level.\n\n## Confidence\nHIGH — every command, option key, codec table, cache path, and pitfall is read directly from the four cited source files (ffmpeg.py, convert.py, media_compat.py, export_presets.py). The convert builder, proxy\u002Fremux argvs, ext maps, window clamp, and the stderr-drain\u002Fmicrosecond\u002Ffaststart\u002Fpix_fmt gaps are verbatim from the code. Behavior of ffmpeg muxers (container\u002Fcodec legality, GIF palette) is standard ffmpeg, noted as such where the media-studio contract does not enforce it.\n",{"name":5,"description":7},"main","a298c929b6cbd662e1fce54302ce520d5ceea1a5","skills\u002Fvideo-convert","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain","https:\u002F\u002Fgithub.com\u002FPrekzursil","2026-11-02 14:30:57",{"source":8,"skillName":5,"commands":56,"prompt":66,"zipAvailable":67},[57,60,63],{"tool":58,"command":59},"npx","npx skills add Prekzursil\u002Fagent-skills-toolchain --skill video-convert",{"tool":61,"command":62},"bunx","bunx skills add Prekzursil\u002Fagent-skills-toolchain --skill video-convert",{"tool":64,"command":65},"pnpm","pnpm dlx skills add Prekzursil\u002Fagent-skills-toolchain --skill video-convert","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Fskills\u002Fvideo-convert\u002FSKILL.md",true,[],[70,91,119,146,160,176],{"skillNo":71,"name":72,"nameZh":6,"description":73,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":74,"categories":75,"keywords":76,"publishedAt":44},"01M6DF7D7478R55P46REYH4QAC","clarify-requirements","Resolve genuine pre-execution ambiguity by scanning repo context, generating 1-6 grounded options, and asking 1-3 focused questions via AskUserQuestion. NOT for mid-task use (injection risk); NOT when the goal is already unambiguous; use to disambiguate vague prompts before any file edits.","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Fworkspace-hooks\u002Fskills\u002Fclarify-requirements\u002FSKILL.md",[],[77,78,79,80,81,82,83,84,85,27,86,87,88,89,90],"clarify requirements","clarify","requirements","risk","options","task","prompts","goal","scanning","context","repo","injection","execution","file",{"skillNo":92,"name":93,"nameZh":6,"description":94,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":95,"categories":96,"keywords":100,"publishedAt":44},"01M6DF79CA4BHJS66J8EZDBPWJ","openmm-md","Run FREE, open-source, Windows-native GPU molecular dynamics via the openmm-md MCP (OpenMM + CUDA on the local NVIDIA GPU). Use for protein \u002F protein-ligand MD when you want a free stack or are on Windows without a Schrodinger license. Covers the full workflow (prepare, solvate, minimize, equilibrate, production, analyze, mmgbsa) AND an honest decision guide vs Schrodinger Desmond, including how to run Desmond legitimately on Linux or a cloud GPU with a real license. This is a deliberately LESSER free alternative and does NOT replace the schrodinger MCP tools. Do NOT use it to pick a free tool when a validated FEP+, WaterMap, IFD-MD, or Prime MM-GBSA result is actually required.","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Fmcp\u002Fopenmm-md-mcp\u002Fskill\u002Fopenmm-md\u002FSKILL.md",[97],{"categoryNo":98,"categoryName":99,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[101,102,103,104,105,106,107,108,109,110,111,112,113,114,27,115,116,117,31,118],"openmm md","how","decision","pick","linux","analyze","tool","stack","open","NVIDIA","gpu","license","workflow","windows","free","molecular dynamics","local","MCP",{"skillNo":120,"name":121,"nameZh":6,"description":122,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":123,"categories":124,"keywords":131,"publishedAt":44},"01M6DF76C1MG85VEWP8VZCBNMN","visual-review","Take screenshots of web pages and UI using Playwright for visual review and iteration","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Feditor\u002Fmetaswarm\u002Fskills\u002Fvisual-review\u002FSKILL.md",[125,128],{"categoryNo":126,"categoryName":127,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000003","演示文稿",{"categoryNo":129,"categoryName":130,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",[132,133,134,135,136,137,138,139,140,141,102,142,143,144,145],"review","visual","visual review","pages","web","false","iteration","take","check","slides","ui","screenshot","playwright","screenshots",{"skillNo":147,"name":148,"nameZh":6,"description":149,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":150,"categories":151,"keywords":152,"publishedAt":44},"01M6DF73AQVCBZKEN0YXEX9FXP","status","Diagnostic status report — shows metaswarm installation state, project setup, and potential issues","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Feditor\u002Fmetaswarm\u002Fskills\u002Fstatus\u002FSKILL.md",[],[148,153,154,155,156,157,158,159],"setup","report","project","installation","project setup","issues","state",{"skillNo":161,"name":162,"nameZh":6,"description":163,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":164,"categories":165,"keywords":167,"publishedAt":44},"01M6DF6X6C2XB0P6GX76P7TTYV","start","Use when starting work on any task, when the user mentions metaswarm, or when the user wants to begin tracked development work","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Feditor\u002Fmetaswarm\u002Fskills\u002Fstart\u002FSKILL.md",[166],{"categoryNo":98,"categoryName":99,"taxonomyType":21,"taxonomyTypeDesc":22},[162,168,82,169,170,171,27,172,173,174,175],"start issue","issue","ready","work on issue","user","work","work on","development",{"skillNo":177,"name":153,"nameZh":6,"description":178,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":179,"categories":180,"keywords":181,"publishedAt":44},"01M6DF6QMJ9AJKFKHHKRT005PQ","Interactive project setup — detects your project, configures metaswarm, writes project-local files","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Ftools\u002Feditor\u002Fmetaswarm\u002Fskills\u002Fsetup\u002FSKILL.md",[],[153,155,117,182,157,183],"files","interactive"]