[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6DF2VXK2CTVT56VRDVJPF10":3,"skill-same:01M6DF2VXK2CTVT56VRDVJPF10":97},{"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":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":14,"repositoryHtmlUrl":46,"repositoryHomepageUrl":6,"ownerHtmlUrl":47,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":48,"install":49,"resources":62},"01M6DF2VXK2CTVT56VRDVJPF10","video-resize-aspect",null,"Use when changing a video ASPECT RATIO or RESOLUTION for social delivery (9:16 \u002F 1:1 \u002F 4:5 \u002F 16:9) - horizontal into a vertical Reel\u002FShort, squaring, or scaling to a canvas, with scale + crop and SAR\u002FDAR math. NOT for subject-tracking reframe (use reframe) or codec changes (use video-convert).","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-resize-aspect\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],"Video","video resize aspect","canvas","convert","math","short","tracking","use","social","delivery","scaling","resolution","dar","reframe","2026-11-10 10:12:30",false,"---\nname: video-resize-aspect\ndescription: Use when changing a video ASPECT RATIO or RESOLUTION for social delivery (9:16 \u002F 1:1 \u002F 4:5 \u002F 16:9) - horizontal into a vertical Reel\u002FShort, squaring, or scaling to a canvas, with scale + crop and SAR\u002FDAR math. NOT for subject-tracking reframe (use reframe) or codec changes (use video-convert).\n---\n\n# Video resize \u002F aspect-ratio change (geometry layer)\n\nHow to change a video's frame size or aspect ratio correctly in the media_studio\nsidecar. This is the **pure geometry** layer: dimension math + the ffmpeg\n`-vf` filter chain. Subject tracking, beat zoom, and multi-speaker layout sit on\ntop of it and all funnel through the same `scale=...:flags=lanczos,setsar=1`\nprimitive documented here.\n\n## Source of truth (read these — every command below is copied from them)\n\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Faspect.py` — the canonical aspect registry:\n  `ASPECT_PRESETS`, `output_dimensions()`, `parse_aspect()`, `even()`. **This is\n  the single source of truth for what 9:16 \u002F 1:1 \u002F 4:5 mean.** Both reframe\n  engines and the export catalog import it so they can never drift.\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Fdirector_op_engines.py` (`build_reframe_argv`,\n  ~L555-588) — the in-engine **center-crop → scale** aspect change.\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Freframe_claudeshorts.py` (`build_reframe_argv`,\n  ~L387-420) — the one crop+scale pass with `flags=lanczos,setsar=1` + libx264\u002Faac.\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Freframe_multispeaker.py` (~L559-561) — per-region\n  `crop=...,scale=...:flags=lanczos,setsar=1` chain (same primitive, tiled).\n- `sidecar\u002Fmedia_studio\u002Fffmpeg.py` — `build_convert_argv` (the generic `scale`\n  option), `resolve_binary`, and the progress-streaming `run()` (the stderr-drain\n  + out_time pitfalls live here).\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Fmedia_compat.py` (`build_proxy_argv`, ~L243-274)\n  — the `scale=-2:720` even-width playback-proxy pattern.\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Fzoom.py` — `zoompan` (resize-in-place \u002F Ken Burns).\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Fpanorama_stitch.py` — tall-canvas composite for\n  scrolling reconstruction (OpenCV, not ffmpeg scaling).\n\n---\n\n## 1. The dimension math (from aspect.py — do NOT re-derive)\n\nThe curated social aspects are all **1080-wide** (OpusClip \u002F IG \u002F TikTok delivery):\n\n| aspect | output (w×h) | use |\n|--------|-------------|-----|\n| `9:16` | **1080×1920** | Reels \u002F TikTok \u002F Shorts \u002F Stories (DEFAULT) |\n| `1:1`  | **1080×1080** | Instagram feed (square) |\n| `4:5`  | **1080×1350** | Instagram \u002F Facebook feed (portrait) |\n\n`output_dimensions(aspect)` returns those three from `ASPECT_PRESETS` directly.\nFor **any other** positive ratio it falls back to \"fit the long edge to 1920,\nround the short edge to even\":\n\n```python\n# aspect.py output_dimensions() — generic fallback (e.g. 16:9 -> 1920x1080, 3:4 -> 1440x1920)\nw, h = parse_aspect(norm)\nif h >= w:   # portrait\u002Fsquare: fix HEIGHT\n    return even(round(1920 * (w \u002F h))), 1920\nelse:        # landscape: fix WIDTH\n    return 1920, even(round(1920 * (h \u002F w)))\n```\n\n`even(n)` rounds **up** to the nearest even value. **This is not cosmetic** —\nlibx264 (yuv420p, 4:2:0 chroma subsampling) requires both dimensions even or the\nencode fails. Always size through `output_dimensions`\u002F`even`, never hand-pick odd\nnumbers.\n\n`parse_aspect` accepts `\"W:H\"` **or** `\"WxH\"` (it `.replace(\"x\", \":\")`) and\nfails loud (`ValueError`) on anything that is not two positive integers. The\nexport catalog uses `require_supported_aspect()` to reject parseable-but-uncurated\nratios (e.g. `16:9`) so it can only ever persist a render target the pipeline offers.\n\n---\n\n## 2. The recipe this codebase actually uses: COVER (center-crop → scale)\n\n**Key fact, stated up front for honesty:** the reframe engines **do not\nletterbox\u002Fpad**. To hit a new aspect they carve a centered window of the *target*\nratio out of the source (cover\u002Ffill) and then scale that window to the canvas.\nNothing is padded; edges are cropped away. (`pad=` appears nowhere for video in\nthis repo — only `apad` for audio.)\n\n### 2a. Simple, source-agnostic aspect change (director_op_engines.py)\n\n```python\n# director_op_engines.build_reframe_argv  (L570-572)\naw, ah = _aspect_ratio(aspect)                 # e.g. 9,16\nwidth, height = _reframe.output_dimensions(aspect)   # 1080,1920\nvf = f\"crop=ih*{aw}\u002F{ah}:ih,scale={width}:{height}\"\n```\n\nExact argv (paths are single argv elements — never `shell=True`):\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cin> \\\n  -vf \"crop=ih*9\u002F16:ih,scale=1080:1920\" \\\n  -c:a copy -progress pipe:1 -nostats \u003Cout>\n```\n\nWhy `crop=ih*aw\u002Fah:ih`: it is driven by **input height (`ih`)**, so the crop\nwindow is `(ih·aw\u002Fah) × ih` — the largest centered rectangle of the target ratio\nthat fits any input size. ffmpeg's `crop` defaults to centered when x\u002Fy are\nomitted. Then `scale` maps it to the contract dimensions. The result genuinely\nhas the target aspect — never a no-op.\n\n### 2b. The quality pass (reframe_claudeshorts.py \u002F reframe_multispeaker.py)\n\nWhen the crop window is computed by a tracker (or x drifts over time), the same\nprimitive carries `flags=lanczos` + `setsar=1`:\n\n```python\n# reframe_claudeshorts.build_reframe_argv  (L406-408)\nvf = (\n    f\"crop={crop['w']}:{crop['h']}:'{x_expr}':{crop['y']},\"\n    f\"scale={out_w}:{out_h}:flags=lanczos,setsar=1\"\n)\n```\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cin> \\\n  -vf \"crop=W:H:'\u003Cx_expr>':Y,scale=1080:1920:flags=lanczos,setsar=1\" \\\n  -c:v libx264 -c:a aac -progress pipe:1 -nostats \u003Cout>\n```\n\nTwo non-obvious bits:\n- **`flags=lanczos`** — high-quality downscale. The default (bilinear) softens\n  text\u002Ffaces; for a 1920-tall source → 1080 this is a visible difference. Use it\n  on every social downscale.\n- **`setsar=1`** — see pitfall #2; this is the one that bites.\n- The crop **x expression is single-quoted** (`'...'`) inside the filter string\n  because its `if(...)` form contains commas that would otherwise be parsed as\n  filter-arg separators. Because this is an argv list (no shell), the quotes\n  reach ffmpeg verbatim — do **not** strip them.\n\n### 2c. Generic scale-only (no aspect change) — ffmpeg.py convert option\n\nThe convert feature exposes a free-form `scale`:\n\n```python\n# ffmpeg.build_convert_argv (L157-160)\nvf.append(f\"scale={str(scale).replace('x', ':')}\")   # accepts \"1280:720\" or \"1280x720\"\n```\n\nFor a **pure resize that preserves aspect with an even result**, use `-2`\n(media_compat proxy pattern):\n\n```\n# media_compat.build_proxy_argv (L268) — scale to 720 tall, width auto-even, universal decode\nffmpeg ... -vf \"scale=-2:720\" -pix_fmt yuv420p -c:v libx264 -preset veryfast -crf 23 ...\n```\n\n`scale=-2:H` keeps the source aspect and forces the auto-computed dimension to a\n**multiple of 2** (`-1` would allow odd → encode failure). Use `-2`, not `-1`.\n\n---\n\n## 3. Pitfalls that actually bite (each maps to repo code\u002Fcomments)\n\n1. **Odd dimensions → encode failure.** h264 + yuv420p needs even W and H. Always\n   route sizes through `aspect.even()` \u002F `output_dimensions()`, and use `scale=-2:…`\n   (not `-1`) for auto dimensions. (`aspect.py:even`, `media_compat.py:246`.)\n\n2. **Missing `setsar=1` → stretched output (SAR\u002FDAR trap).** If the *source* has a\n   non-square pixel aspect ratio (SAR ≠ 1, common in DV\u002Fanamorphic\u002Fbroadcast),\n   `scale` changes the storage dimensions but the stream's SAR rides along, so the\n   player applies it again and the picture comes out squished or stretched. Both\n   quality engines append **`setsar=1`** after `scale` to reset pixel aspect to\n   square so DAR == W:H. Add it to any aspect-changing scale.\n   (`reframe_claudeshorts.py:408`, `reframe_multispeaker.py:561`.)\n\n3. **Cover-crop discards edges.** Because the engines crop-to-cover (§2), a\n   horizontal source → 9:16 loses the left\u002Fright thirds. That is *intended* for\n   subject-tracked reframe but surprising for a \"just make it vertical\" request —\n   if the user needs the whole frame visible, that is a **contain\u002Fpad** job, which\n   this repo does not do (see §4).\n\n4. **stderr-pipe deadlock on long encodes (\"stuck clip 1\u002F5\").** `ffmpeg.run()`\n   captures `stderr=PIPE`; ffmpeg chats on stderr even with `-nostats` (banner,\n   stream info, libass\u002Ffont logs). If you `PIPE` stderr and never read it, the\n   ~64 KB pipe buffer fills and the encode **blocks forever**. `run()` drains\n   stderr on a daemon thread (keeping a 40-line tail for error reporting). If you\n   spawn ffmpeg yourself, drain stderr or don't pipe it. (`ffmpeg.py:254-272`.)\n\n5. **`out_time_ms` is microseconds, not milliseconds.** ffmpeg's `-progress`\n   stream mislabels it. `_pct_from_progress` divides `out_time_ms`\u002F`out_time_us`\n   by `1_000_000.0`. Don't treat it as ms. (`ffmpeg.py:202-218`.)\n\n6. **Always argv-list, never `shell=True`.** Every builder returns a list; paths\n   with spaces are one element and just work. `run()` raises `TypeError` if handed\n   a shell string. (`ffmpeg.py:243`, `reframe.py` contract.)\n\n7. **`-progress pipe:1 -nostats` is required for the progress UI.** It routes the\n   key=value progress stream to stdout so `run()` can parse a real percentage;\n   without it you get coarse\u002Fno progress. Every builder includes it.\n\n8. **`-y` overwrites; output-path collision guard.** Builders pass `-y`. The\n   convert feature adds a `.converted` infix when the derived dest equals the\n   source so a re-encode never clobbers the original. (`convert.py:output_path`.)\n\n9. **(verthor\u002FWSL engine only) never pipe the script via stdin.** The WSL verthor\n   adapter passes the script as an argv element (`wsl bash \u003Cscript> …`); piping it\n   `tr | bash` over stdin corrupts mediapipe (it reads stdin). Not a scaling issue\n   but it lives in the same `reframe.py` and will eat hours if hit.\n\n---\n\n## 4. \"Contain \u002F letterbox-pad\" — NOT in this repo (standard ffmpeg, flagged)\n\nThis codebase only does **cover** (§2). If a task genuinely needs the whole source\nframe visible inside the target canvas (black bars), that is the standard ffmpeg\n`scale … force_original_aspect_ratio=decrease` + `pad` idiom. It is **not present\nin media_studio** — use it only as an explicit, separate path and say so:\n\n```\n# STANDARD FFMPEG (not in this repo) — fit-inside + center-pad to 1080x1920:\nffmpeg -i \u003Cin> -vf \\\n \"scale=1080:1920:force_original_aspect_ratio=decrease:flags=lanczos,\\\n  pad=1080:1920:(ow-iw)\u002F2:(oh-ih)\u002F2,setsar=1\" \\\n -c:v libx264 -c:a copy \u003Cout>\n```\n\n`force_original_aspect_ratio=decrease` scales to fit *inside* the box; `pad`\ncenters it and fills the rest. Mirror image of the repo's cover-crop. Keep\n`flags=lanczos` and `setsar=1` for the same reasons as §2\u002F§3.\n\n---\n\n## 5. Resize-in-place \u002F scaling effects (zoom.py)\n\nFor a **subtle scale animation that keeps the frame size constant** (Ken-Burns\nslow push + punch-in on beats), use `zoompan`, not a one-shot scale:\n\n```python\n# zoom.build_zoom_filter — keeps s=WxH, centers the zoom window\nf\"zoompan=z='\u003Cz_expr>':x='iw\u002F2-(iw\u002Fzoom\u002F2)':y='ih\u002F2-(ih\u002Fzoom\u002F2)':d=1:s={W}x{H}:fps={fps}\"\n```\n\nNotes: `d=1` = one output frame per input frame (otherwise zoompan holds frames\nand stutters); `s=WxH` fixes output size; `fps` must match the timebase the\n`on`-based `z` expression assumes (DEFAULT_FPS = 30, the §4 vertical pipeline\nrate). Zoom is video-only — `build_zoom_argv` re-encodes video (`libx264`) and\n**stream-copies audio** (`-c:a copy`). Z is clamped to `MAX_ZOOM = 1.5` so the\ncrop never starves the frame.\n\n---\n\n## 6. Worked example: horizontal 1920×1080 → vertical 9:16 Reel\n\nGoal: turn a landscape clip into a 1080×1920 vertical for Reels, cover-crop\n(repo default), best quality.\n\n1. Resolve dimensions (don't hardcode):\n   ```python\n   from media_studio.features import aspect\n   w, h = aspect.output_dimensions(\"9:16\")   # (1080, 1920)\n   ```\n2. Build the argv (cover-crop → lanczos scale → setsar):\n   ```\n   ffmpeg -hide_banner -nostdin -y -i input_1920x1080.mp4 \\\n     -vf \"crop=ih*9\u002F16:ih,scale=1080:1920:flags=lanczos,setsar=1\" \\\n     -c:v libx264 -c:a copy -progress pipe:1 -nostats output_1080x1920.mp4\n   ```\n   - `crop=ih*9\u002F16:ih` → `crop=607.5≈608:1080` centered (the 9:16 slice of a\n     1080-tall frame). The left\u002Fright of the landscape frame is dropped — expected.\n   - `scale=…:flags=lanczos` → high-quality up to 1080×1920.\n   - `setsar=1` → square pixels so the player shows true 9:16.\n3. Run it through `ffmpeg.run(argv, total_sec=ffprobe_duration(in))` so progress\n   is a real percentage and stderr is drained (no deadlock).\n\nIf instead the whole landscape frame must stay visible (bars top\u002Fbottom), switch\nto the §4 contain\u002Fpad recipe and tell the user the repo's reframe engines don't\ndo that.\n\n---\n\n## Quick checklist before shipping a resize\n\n- [ ] Dimensions from `aspect.output_dimensions()` (even, curated). \n- [ ] `flags=lanczos` on any downscale.\n- [ ] `setsar=1` after any aspect-changing `scale`.\n- [ ] `scale=-2:H` (not `-1`) for aspect-preserving auto-size.\n- [ ] argv list, `-y`, `-progress pipe:1 -nostats`.\n- [ ] stderr drained if you spawn ffmpeg yourself.\n- [ ] cover-crop vs contain-pad decision made explicit (repo = cover only).\n","# Video resize \u002F aspect-ratio change (geometry layer)\n\nHow to change a video's frame size or aspect ratio correctly in the media_studio\nsidecar. This is the **pure geometry** layer: dimension math + the ffmpeg\n`-vf` filter chain. Subject tracking, beat zoom, and multi-speaker layout sit on\ntop of it and all funnel through the same `scale=...:flags=lanczos,setsar=1`\nprimitive documented here.\n\n## Source of truth (read these — every command below is copied from them)\n\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Faspect.py` — the canonical aspect registry:\n  `ASPECT_PRESETS`, `output_dimensions()`, `parse_aspect()`, `even()`. **This is\n  the single source of truth for what 9:16 \u002F 1:1 \u002F 4:5 mean.** Both reframe\n  engines and the export catalog import it so they can never drift.\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Fdirector_op_engines.py` (`build_reframe_argv`,\n  ~L555-588) — the in-engine **center-crop → scale** aspect change.\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Freframe_claudeshorts.py` (`build_reframe_argv`,\n  ~L387-420) — the one crop+scale pass with `flags=lanczos,setsar=1` + libx264\u002Faac.\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Freframe_multispeaker.py` (~L559-561) — per-region\n  `crop=...,scale=...:flags=lanczos,setsar=1` chain (same primitive, tiled).\n- `sidecar\u002Fmedia_studio\u002Fffmpeg.py` — `build_convert_argv` (the generic `scale`\n  option), `resolve_binary`, and the progress-streaming `run()` (the stderr-drain\n  + out_time pitfalls live here).\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Fmedia_compat.py` (`build_proxy_argv`, ~L243-274)\n  — the `scale=-2:720` even-width playback-proxy pattern.\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Fzoom.py` — `zoompan` (resize-in-place \u002F Ken Burns).\n- `sidecar\u002Fmedia_studio\u002Ffeatures\u002Fpanorama_stitch.py` — tall-canvas composite for\n  scrolling reconstruction (OpenCV, not ffmpeg scaling).\n\n---\n\n## 1. The dimension math (from aspect.py — do NOT re-derive)\n\nThe curated social aspects are all **1080-wide** (OpusClip \u002F IG \u002F TikTok delivery):\n\n| aspect | output (w×h) | use |\n|--------|-------------|-----|\n| `9:16` | **1080×1920** | Reels \u002F TikTok \u002F Shorts \u002F Stories (DEFAULT) |\n| `1:1`  | **1080×1080** | Instagram feed (square) |\n| `4:5`  | **1080×1350** | Instagram \u002F Facebook feed (portrait) |\n\n`output_dimensions(aspect)` returns those three from `ASPECT_PRESETS` directly.\nFor **any other** positive ratio it falls back to \"fit the long edge to 1920,\nround the short edge to even\":\n\n```python\n# aspect.py output_dimensions() — generic fallback (e.g. 16:9 -> 1920x1080, 3:4 -> 1440x1920)\nw, h = parse_aspect(norm)\nif h >= w:   # portrait\u002Fsquare: fix HEIGHT\n    return even(round(1920 * (w \u002F h))), 1920\nelse:        # landscape: fix WIDTH\n    return 1920, even(round(1920 * (h \u002F w)))\n```\n\n`even(n)` rounds **up** to the nearest even value. **This is not cosmetic** —\nlibx264 (yuv420p, 4:2:0 chroma subsampling) requires both dimensions even or the\nencode fails. Always size through `output_dimensions`\u002F`even`, never hand-pick odd\nnumbers.\n\n`parse_aspect` accepts `\"W:H\"` **or** `\"WxH\"` (it `.replace(\"x\", \":\")`) and\nfails loud (`ValueError`) on anything that is not two positive integers. The\nexport catalog uses `require_supported_aspect()` to reject parseable-but-uncurated\nratios (e.g. `16:9`) so it can only ever persist a render target the pipeline offers.\n\n---\n\n## 2. The recipe this codebase actually uses: COVER (center-crop → scale)\n\n**Key fact, stated up front for honesty:** the reframe engines **do not\nletterbox\u002Fpad**. To hit a new aspect they carve a centered window of the *target*\nratio out of the source (cover\u002Ffill) and then scale that window to the canvas.\nNothing is padded; edges are cropped away. (`pad=` appears nowhere for video in\nthis repo — only `apad` for audio.)\n\n### 2a. Simple, source-agnostic aspect change (director_op_engines.py)\n\n```python\n# director_op_engines.build_reframe_argv  (L570-572)\naw, ah = _aspect_ratio(aspect)                 # e.g. 9,16\nwidth, height = _reframe.output_dimensions(aspect)   # 1080,1920\nvf = f\"crop=ih*{aw}\u002F{ah}:ih,scale={width}:{height}\"\n```\n\nExact argv (paths are single argv elements — never `shell=True`):\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cin> \\\n  -vf \"crop=ih*9\u002F16:ih,scale=1080:1920\" \\\n  -c:a copy -progress pipe:1 -nostats \u003Cout>\n```\n\nWhy `crop=ih*aw\u002Fah:ih`: it is driven by **input height (`ih`)**, so the crop\nwindow is `(ih·aw\u002Fah) × ih` — the largest centered rectangle of the target ratio\nthat fits any input size. ffmpeg's `crop` defaults to centered when x\u002Fy are\nomitted. Then `scale` maps it to the contract dimensions. The result genuinely\nhas the target aspect — never a no-op.\n\n### 2b. The quality pass (reframe_claudeshorts.py \u002F reframe_multispeaker.py)\n\nWhen the crop window is computed by a tracker (or x drifts over time), the same\nprimitive carries `flags=lanczos` + `setsar=1`:\n\n```python\n# reframe_claudeshorts.build_reframe_argv  (L406-408)\nvf = (\n    f\"crop={crop['w']}:{crop['h']}:'{x_expr}':{crop['y']},\"\n    f\"scale={out_w}:{out_h}:flags=lanczos,setsar=1\"\n)\n```\n\n```\nffmpeg -hide_banner -nostdin -y -i \u003Cin> \\\n  -vf \"crop=W:H:'\u003Cx_expr>':Y,scale=1080:1920:flags=lanczos,setsar=1\" \\\n  -c:v libx264 -c:a aac -progress pipe:1 -nostats \u003Cout>\n```\n\nTwo non-obvious bits:\n- **`flags=lanczos`** — high-quality downscale. The default (bilinear) softens\n  text\u002Ffaces; for a 1920-tall source → 1080 this is a visible difference. Use it\n  on every social downscale.\n- **`setsar=1`** — see pitfall #2; this is the one that bites.\n- The crop **x expression is single-quoted** (`'...'`) inside the filter string\n  because its `if(...)` form contains commas that would otherwise be parsed as\n  filter-arg separators. Because this is an argv list (no shell), the quotes\n  reach ffmpeg verbatim — do **not** strip them.\n\n### 2c. Generic scale-only (no aspect change) — ffmpeg.py convert option\n\nThe convert feature exposes a free-form `scale`:\n\n```python\n# ffmpeg.build_convert_argv (L157-160)\nvf.append(f\"scale={str(scale).replace('x', ':')}\")   # accepts \"1280:720\" or \"1280x720\"\n```\n\nFor a **pure resize that preserves aspect with an even result**, use `-2`\n(media_compat proxy pattern):\n\n```\n# media_compat.build_proxy_argv (L268) — scale to 720 tall, width auto-even, universal decode\nffmpeg ... -vf \"scale=-2:720\" -pix_fmt yuv420p -c:v libx264 -preset veryfast -crf 23 ...\n```\n\n`scale=-2:H` keeps the source aspect and forces the auto-computed dimension to a\n**multiple of 2** (`-1` would allow odd → encode failure). Use `-2`, not `-1`.\n\n---\n\n## 3. Pitfalls that actually bite (each maps to repo code\u002Fcomments)\n\n1. **Odd dimensions → encode failure.** h264 + yuv420p needs even W and H. Always\n   route sizes through `aspect.even()` \u002F `output_dimensions()`, and use `scale=-2:…`\n   (not `-1`) for auto dimensions. (`aspect.py:even`, `media_compat.py:246`.)\n\n2. **Missing `setsar=1` → stretched output (SAR\u002FDAR trap).** If the *source* has a\n   non-square pixel aspect ratio (SAR ≠ 1, common in DV\u002Fanamorphic\u002Fbroadcast),\n   `scale` changes the storage dimensions but the stream's SAR rides along, so the\n   player applies it again and the picture comes out squished or stretched. Both\n   quality engines append **`setsar=1`** after `scale` to reset pixel aspect to\n   square so DAR == W:H. Add it to any aspect-changing scale.\n   (`reframe_claudeshorts.py:408`, `reframe_multispeaker.py:561`.)\n\n3. **Cover-crop discards edges.** Because the engines crop-to-cover (§2), a\n   horizontal source → 9:16 loses the left\u002Fright thirds. That is *intended* for\n   subject-tracked reframe but surprising for a \"just make it vertical\" request —\n   if the user needs the whole frame visible, that is a **contain\u002Fpad** job, which\n   this repo does not do (see §4).\n\n4. **stderr-pipe deadlock on long encodes (\"stuck clip 1\u002F5\").** `ffmpeg.run()`\n   captures `stderr=PIPE`; ffmpeg chats on stderr even with `-nostats` (banner,\n   stream info, libass\u002Ffont logs). If you `PIPE` stderr and never read it, the\n   ~64 KB pipe buffer fills and the encode **blocks forever**. `run()` drains\n   stderr on a daemon thread (keeping a 40-line tail for error reporting). If you\n   spawn ffmpeg yourself, drain stderr or don't pipe it. (`ffmpeg.py:254-272`.)\n\n5. **`out_time_ms` is microseconds, not milliseconds.** ffmpeg's `-progress`\n   stream mislabels it. `_pct_from_progress` divides `out_time_ms`\u002F`out_time_us`\n   by `1_000_000.0`. Don't treat it as ms. (`ffmpeg.py:202-218`.)\n\n6. **Always argv-list, never `shell=True`.** Every builder returns a list; paths\n   with spaces are one element and just work. `run()` raises `TypeError` if handed\n   a shell string. (`ffmpeg.py:243`, `reframe.py` contract.)\n\n7. **`-progress pipe:1 -nostats` is required for the progress UI.** It routes the\n   key=value progress stream to stdout so `run()` can parse a real percentage;\n   without it you get coarse\u002Fno progress. Every builder includes it.\n\n8. **`-y` overwrites; output-path collision guard.** Builders pass `-y`. The\n   convert feature adds a `.converted` infix when the derived dest equals the\n   source so a re-encode never clobbers the original. (`convert.py:output_path`.)\n\n9. **(verthor\u002FWSL engine only) never pipe the script via stdin.** The WSL verthor\n   adapter passes the script as an argv element (`wsl bash \u003Cscript> …`); piping it\n   `tr | bash` over stdin corrupts mediapipe (it reads stdin). Not a scaling issue\n   but it lives in the same `reframe.py` and will eat hours if hit.\n\n---\n\n## 4. \"Contain \u002F letterbox-pad\" — NOT in this repo (standard ffmpeg, flagged)\n\nThis codebase only does **cover** (§2). If a task genuinely needs the whole source\nframe visible inside the target canvas (black bars), that is the standard ffmpeg\n`scale … force_original_aspect_ratio=decrease` + `pad` idiom. It is **not present\nin media_studio** — use it only as an explicit, separate path and say so:\n\n```\n# STANDARD FFMPEG (not in this repo) — fit-inside + center-pad to 1080x1920:\nffmpeg -i \u003Cin> -vf \\\n \"scale=1080:1920:force_original_aspect_ratio=decrease:flags=lanczos,\\\n  pad=1080:1920:(ow-iw)\u002F2:(oh-ih)\u002F2,setsar=1\" \\\n -c:v libx264 -c:a copy \u003Cout>\n```\n\n`force_original_aspect_ratio=decrease` scales to fit *inside* the box; `pad`\ncenters it and fills the rest. Mirror image of the repo's cover-crop. Keep\n`flags=lanczos` and `setsar=1` for the same reasons as §2\u002F§3.\n\n---\n\n## 5. Resize-in-place \u002F scaling effects (zoom.py)\n\nFor a **subtle scale animation that keeps the frame size constant** (Ken-Burns\nslow push + punch-in on beats), use `zoompan`, not a one-shot scale:\n\n```python\n# zoom.build_zoom_filter — keeps s=WxH, centers the zoom window\nf\"zoompan=z='\u003Cz_expr>':x='iw\u002F2-(iw\u002Fzoom\u002F2)':y='ih\u002F2-(ih\u002Fzoom\u002F2)':d=1:s={W}x{H}:fps={fps}\"\n```\n\nNotes: `d=1` = one output frame per input frame (otherwise zoompan holds frames\nand stutters); `s=WxH` fixes output size; `fps` must match the timebase the\n`on`-based `z` expression assumes (DEFAULT_FPS = 30, the §4 vertical pipeline\nrate). Zoom is video-only — `build_zoom_argv` re-encodes video (`libx264`) and\n**stream-copies audio** (`-c:a copy`). Z is clamped to `MAX_ZOOM = 1.5` so the\ncrop never starves the frame.\n\n---\n\n## 6. Worked example: horizontal 1920×1080 → vertical 9:16 Reel\n\nGoal: turn a landscape clip into a 1080×1920 vertical for Reels, cover-crop\n(repo default), best quality.\n\n1. Resolve dimensions (don't hardcode):\n   ```python\n   from media_studio.features import aspect\n   w, h = aspect.output_dimensions(\"9:16\")   # (1080, 1920)\n   ```\n2. Build the argv (cover-crop → lanczos scale → setsar):\n   ```\n   ffmpeg -hide_banner -nostdin -y -i input_1920x1080.mp4 \\\n     -vf \"crop=ih*9\u002F16:ih,scale=1080:1920:flags=lanczos,setsar=1\" \\\n     -c:v libx264 -c:a copy -progress pipe:1 -nostats output_1080x1920.mp4\n   ```\n   - `crop=ih*9\u002F16:ih` → `crop=607.5≈608:1080` centered (the 9:16 slice of a\n     1080-tall frame). The left\u002Fright of the landscape frame is dropped — expected.\n   - `scale=…:flags=lanczos` → high-quality up to 1080×1920.\n   - `setsar=1` → square pixels so the player shows true 9:16.\n3. Run it through `ffmpeg.run(argv, total_sec=ffprobe_duration(in))` so progress\n   is a real percentage and stderr is drained (no deadlock).\n\nIf instead the whole landscape frame must stay visible (bars top\u002Fbottom), switch\nto the §4 contain\u002Fpad recipe and tell the user the repo's reframe engines don't\ndo that.\n\n---\n\n## Quick checklist before shipping a resize\n\n- [ ] Dimensions from `aspect.output_dimensions()` (even, curated). \n- [ ] `flags=lanczos` on any downscale.\n- [ ] `setsar=1` after any aspect-changing `scale`.\n- [ ] `scale=-2:H` (not `-1`) for aspect-preserving auto-size.\n- [ ] argv list, `-y`, `-progress pipe:1 -nostats`.\n- [ ] stderr drained if you spawn ffmpeg yourself.\n- [ ] cover-crop vs contain-pad decision made explicit (repo = cover only).\n",{"name":5,"description":7},"main","a298c929b6cbd662e1fce54302ce520d5ceea1a5","skills\u002Fvideo-resize-aspect","https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain","https:\u002F\u002Fgithub.com\u002FPrekzursil","2026-11-02 14:30:57",{"source":8,"skillName":5,"commands":50,"prompt":60,"zipAvailable":61},[51,54,57],{"tool":52,"command":53},"npx","npx skills add Prekzursil\u002Fagent-skills-toolchain --skill video-resize-aspect",{"tool":55,"command":56},"bunx","bunx skills add Prekzursil\u002Fagent-skills-toolchain --skill video-resize-aspect",{"tool":58,"command":59},"pnpm","pnpm dlx skills add Prekzursil\u002Fagent-skills-toolchain --skill video-resize-aspect","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FPrekzursil\u002Fagent-skills-toolchain\u002Fblob\u002Fmain\u002Fskills\u002Fvideo-resize-aspect\u002FSKILL.md",true,[63,69,73,77,81,85,89,93],{"path":64,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":67,"sourceHtmlUrl":68},"steps\u002Fstep-01-document-discovery.md","REFERENCE","参考文档",5152,"https:\u002F\u002Fgithub.com\u002Fdinesh-style\u002Fportfolio\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fgds-check-implementation-readiness\u002Fsteps\u002Fstep-01-document-discovery.md",{"path":70,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":71,"sourceHtmlUrl":72},"steps\u002Fstep-02-gdd-analysis.md",4753,"https:\u002F\u002Fgithub.com\u002Fdinesh-style\u002Fportfolio\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fgds-check-implementation-readiness\u002Fsteps\u002Fstep-02-gdd-analysis.md",{"path":74,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":75,"sourceHtmlUrl":76},"steps\u002Fstep-03-epic-coverage-validation.md",4216,"https:\u002F\u002Fgithub.com\u002Fdinesh-style\u002Fportfolio\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fgds-check-implementation-readiness\u002Fsteps\u002Fstep-03-epic-coverage-validation.md",{"path":78,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":79,"sourceHtmlUrl":80},"steps\u002Fstep-04-ux-alignment.md",3502,"https:\u002F\u002Fgithub.com\u002Fdinesh-style\u002Fportfolio\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fgds-check-implementation-readiness\u002Fsteps\u002Fstep-04-ux-alignment.md",{"path":82,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":83,"sourceHtmlUrl":84},"steps\u002Fstep-05-epic-quality-review.md",6985,"https:\u002F\u002Fgithub.com\u002Fdinesh-style\u002Fportfolio\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fgds-check-implementation-readiness\u002Fsteps\u002Fstep-05-epic-quality-review.md",{"path":86,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":87,"sourceHtmlUrl":88},"steps\u002Fstep-06-final-assessment.md",3409,"https:\u002F\u002Fgithub.com\u002Fdinesh-style\u002Fportfolio\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fgds-check-implementation-readiness\u002Fsteps\u002Fstep-06-final-assessment.md",{"path":90,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":91,"sourceHtmlUrl":92},"templates\u002Freadiness-report-template.md",95,"https:\u002F\u002Fgithub.com\u002Fdinesh-style\u002Fportfolio\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fgds-check-implementation-readiness\u002Ftemplates\u002Freadiness-report-template.md",{"path":94,"resourceType":65,"resourceTypeDesc":66,"sizeBytes":95,"sourceHtmlUrl":96},"workflow.md",3001,"https:\u002F\u002Fgithub.com\u002Fdinesh-style\u002Fportfolio\u002Fblob\u002Fmain\u002F.agents\u002Fskills\u002Fgds-check-implementation-readiness\u002Fworkflow.md",[98,119,148,175,189,205],{"skillNo":99,"name":100,"nameZh":6,"description":101,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":102,"categories":103,"keywords":104,"publishedAt":38},"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",[],[105,106,107,108,109,110,111,112,113,31,114,115,116,117,118],"clarify requirements","clarify","requirements","risk","options","task","prompts","goal","scanning","context","repo","injection","execution","file",{"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":128,"publishedAt":38},"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",[125],{"categoryNo":126,"categoryName":127,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[129,130,131,132,133,134,135,136,137,138,139,140,141,142,31,143,144,145,146,147],"openmm md","how","decision","pick","linux","analyze","tool","stack","open","NVIDIA","gpu","license","workflow","windows","free","molecular dynamics","local","do","MCP",{"skillNo":149,"name":150,"nameZh":6,"description":151,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":152,"categories":153,"keywords":160,"publishedAt":38},"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",[154,157],{"categoryNo":155,"categoryName":156,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000003","演示文稿",{"categoryNo":158,"categoryName":159,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",[161,162,163,164,165,166,167,168,169,170,130,171,172,173,174],"review","visual","visual review","pages","web","false","iteration","take","check","slides","ui","screenshot","playwright","screenshots",{"skillNo":176,"name":177,"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":38},"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",[],[177,182,183,184,185,186,187,188],"setup","report","project","installation","project setup","issues","state",{"skillNo":190,"name":191,"nameZh":6,"description":192,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":193,"categories":194,"keywords":196,"publishedAt":38},"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",[195],{"categoryNo":126,"categoryName":127,"taxonomyType":21,"taxonomyTypeDesc":22},[191,197,110,198,199,200,31,201,202,203,204],"start issue","issue","ready","work on issue","user","work","work on","development",{"skillNo":206,"name":182,"nameZh":6,"description":207,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":208,"categories":209,"keywords":210,"publishedAt":38},"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",[],[182,184,145,211,186,212],"files","interactive"]