video-resize-aspect
Use when changing a video ASPECT RATIO or RESOLUTION for social delivery (9:16 / 1:1 / 4:5 / 16:9) - horizontal into a vertical Reel/Short, squaring, or scaling to a canvas, with scale + crop and SAR/DAR math. NOT for subject-tracking reframe (use reframe) or codec changes (use video-convert).
- License 许可
- MIT
- 最近更新
- 2026-11-02
- Stars
- ★ 0
- Forks
- 0
01 前言 Front Matter
- name
- video-resize-aspect
- description
- Use when changing a video ASPECT RATIO or RESOLUTION for social delivery (9:16 / 1:1 / 4:5 / 16:9) - horizontal into a vertical Reel/Short, squaring, or scaling to a canvas, with scale + crop and SAR/DAR math. NOT for subject-tracking reframe (use reframe) or codec changes (use video-convert).
02 资源文件 8
- 参考文档steps/step-01-document-discovery.md5.0 KB
- 参考文档steps/step-02-gdd-analysis.md4.6 KB
- 参考文档steps/step-03-epic-coverage-validation.md4.1 KB
- 参考文档steps/step-04-ux-alignment.md3.4 KB
- 参考文档steps/step-05-epic-quality-review.md6.8 KB
- 参考文档steps/step-06-final-assessment.md3.3 KB
- 参考文档templates/readiness-report-template.md95 B
- 参考文档workflow.md2.9 KB
03 SKILL.md 正文
Video resize / aspect-ratio change (geometry layer)
How to change a video's frame size or aspect ratio correctly in the media_studio
sidecar. This is the pure geometry layer: dimension math + the ffmpeg
-vf filter chain. Subject tracking, beat zoom, and multi-speaker layout sit on
top of it and all funnel through the same scale=...:flags=lanczos,setsar=1
primitive documented here.
Source of truth (read these — every command below is copied from them)
sidecar/media_studio/features/aspect.py— the canonical aspect registry:ASPECT_PRESETS,output_dimensions(),parse_aspect(),even(). This is the single source of truth for what 9:16 / 1:1 / 4:5 mean. Both reframe engines and the export catalog import it so they can never drift.sidecar/media_studio/features/director_op_engines.py(build_reframe_argv, ~L555-588) — the in-engine center-crop → scale aspect change.sidecar/media_studio/features/reframe_claudeshorts.py(build_reframe_argv, ~L387-420) — the one crop+scale pass withflags=lanczos,setsar=1+ libx264/aac.sidecar/media_studio/features/reframe_multispeaker.py(~L559-561) — per-regioncrop=...,scale=...:flags=lanczos,setsar=1chain (same primitive, tiled).sidecar/media_studio/ffmpeg.py—build_convert_argv(the genericscaleoption),resolve_binary, and the progress-streamingrun()(the stderr-drain- out_time pitfalls live here).
sidecar/media_studio/features/media_compat.py(build_proxy_argv, ~L243-274) — thescale=-2:720even-width playback-proxy pattern.sidecar/media_studio/features/zoom.py—zoompan(resize-in-place / Ken Burns).sidecar/media_studio/features/panorama_stitch.py— tall-canvas composite for scrolling reconstruction (OpenCV, not ffmpeg scaling).
1. The dimension math (from aspect.py — do NOT re-derive)
The curated social aspects are all 1080-wide (OpusClip / IG / TikTok delivery):
| aspect | output (w×h) | use |
|---|---|---|
9:16 |
1080×1920 | Reels / TikTok / Shorts / Stories (DEFAULT) |
1:1 |
1080×1080 | Instagram feed (square) |
4:5 |
1080×1350 | Instagram / Facebook feed (portrait) |
output_dimensions(aspect) returns those three from ASPECT_PRESETS directly.
For any other positive ratio it falls back to "fit the long edge to 1920,
round the short edge to even":
# aspect.py output_dimensions() — generic fallback (e.g. 16:9 -> 1920x1080, 3:4 -> 1440x1920)
w, h = parse_aspect(norm)
if h >= w: # portrait/square: fix HEIGHT
return even(round(1920 * (w / h))), 1920
else: # landscape: fix WIDTH
return 1920, even(round(1920 * (h / w)))
even(n) rounds up to the nearest even value. This is not cosmetic —
libx264 (yuv420p, 4:2:0 chroma subsampling) requires both dimensions even or the
encode fails. Always size through output_dimensions/even, never hand-pick odd
numbers.
parse_aspect accepts "W:H" or "WxH" (it .replace("x", ":")) and
fails loud (ValueError) on anything that is not two positive integers. The
export catalog uses require_supported_aspect() to reject parseable-but-uncurated
ratios (e.g. 16:9) so it can only ever persist a render target the pipeline offers.
2. The recipe this codebase actually uses: COVER (center-crop → scale)
Key fact, stated up front for honesty: the reframe engines do not
letterbox/pad. To hit a new aspect they carve a centered window of the target
ratio out of the source (cover/fill) and then scale that window to the canvas.
Nothing is padded; edges are cropped away. (pad= appears nowhere for video in
this repo — only apad for audio.)
2a. Simple, source-agnostic aspect change (director_op_engines.py)
# director_op_engines.build_reframe_argv (L570-572)
aw, ah = _aspect_ratio(aspect) # e.g. 9,16
width, height = _reframe.output_dimensions(aspect) # 1080,1920
vf = f"crop=ih*{aw}/{ah}:ih,scale={width}:{height}"
Exact argv (paths are single argv elements — never shell=True):
ffmpeg -hide_banner -nostdin -y -i <in> \
-vf "crop=ih*9/16:ih,scale=1080:1920" \
-c:a copy -progress pipe:1 -nostats <out>
Why crop=ih*aw/ah:ih: it is driven by input height (ih), so the crop
window is (ih·aw/ah) × ih — the largest centered rectangle of the target ratio
that fits any input size. ffmpeg's crop defaults to centered when x/y are
omitted. Then scale maps it to the contract dimensions. The result genuinely
has the target aspect — never a no-op.
2b. The quality pass (reframe_claudeshorts.py / reframe_multispeaker.py)
When the crop window is computed by a tracker (or x drifts over time), the same
primitive carries flags=lanczos + setsar=1:
# reframe_claudeshorts.build_reframe_argv (L406-408)
vf = (
f"crop={crop['w']}:{crop['h']}:'{x_expr}':{crop['y']},"
f"scale={out_w}:{out_h}:flags=lanczos,setsar=1"
)
ffmpeg -hide_banner -nostdin -y -i <in> \
-vf "crop=W:H:'<x_expr>':Y,scale=1080:1920:flags=lanczos,setsar=1" \
-c:v libx264 -c:a aac -progress pipe:1 -nostats <out>
Two non-obvious bits:
flags=lanczos— high-quality downscale. The default (bilinear) softens text/faces; for a 1920-tall source → 1080 this is a visible difference. Use it on every social downscale.setsar=1— see pitfall #2; this is the one that bites.- The crop x expression is single-quoted (
'...') inside the filter string because itsif(...)form contains commas that would otherwise be parsed as filter-arg separators. Because this is an argv list (no shell), the quotes reach ffmpeg verbatim — do not strip them.
2c. Generic scale-only (no aspect change) — ffmpeg.py convert option
The convert feature exposes a free-form scale:
# ffmpeg.build_convert_argv (L157-160)
vf.append(f"scale={str(scale).replace('x', ':')}") # accepts "1280:720" or "1280x720"
For a pure resize that preserves aspect with an even result, use -2
(media_compat proxy pattern):
# media_compat.build_proxy_argv (L268) — scale to 720 tall, width auto-even, universal decode
ffmpeg ... -vf "scale=-2:720" -pix_fmt yuv420p -c:v libx264 -preset veryfast -crf 23 ...
scale=-2:H keeps the source aspect and forces the auto-computed dimension to a
multiple of 2 (-1 would allow odd → encode failure). Use -2, not -1.
3. Pitfalls that actually bite (each maps to repo code/comments)
-
Odd dimensions → encode failure. h264 + yuv420p needs even W and H. Always route sizes through
aspect.even()/output_dimensions(), and usescale=-2:…(not-1) for auto dimensions. (aspect.py:even,media_compat.py:246.) -
Missing
setsar=1→ stretched output (SAR/DAR trap). If the source has a non-square pixel aspect ratio (SAR ≠ 1, common in DV/anamorphic/broadcast),scalechanges the storage dimensions but the stream's SAR rides along, so the player applies it again and the picture comes out squished or stretched. Both quality engines appendsetsar=1afterscaleto reset pixel aspect to square so DAR == W:H. Add it to any aspect-changing scale. (reframe_claudeshorts.py:408,reframe_multispeaker.py:561.) -
Cover-crop discards edges. Because the engines crop-to-cover (§2), a horizontal source → 9:16 loses the left/right thirds. That is intended for subject-tracked reframe but surprising for a "just make it vertical" request — if the user needs the whole frame visible, that is a contain/pad job, which this repo does not do (see §4).
-
stderr-pipe deadlock on long encodes ("stuck clip 1/5").
ffmpeg.run()capturesstderr=PIPE; ffmpeg chats on stderr even with-nostats(banner, stream info, libass/font logs). If youPIPEstderr and never read it, the ~64 KB pipe buffer fills and the encode blocks forever.run()drains stderr on a daemon thread (keeping a 40-line tail for error reporting). If you spawn ffmpeg yourself, drain stderr or don't pipe it. (ffmpeg.py:254-272.) -
out_time_msis microseconds, not milliseconds. ffmpeg's-progressstream mislabels it._pct_from_progressdividesout_time_ms/out_time_usby1_000_000.0. Don't treat it as ms. (ffmpeg.py:202-218.) -
Always argv-list, never
shell=True. Every builder returns a list; paths with spaces are one element and just work.run()raisesTypeErrorif handed a shell string. (ffmpeg.py:243,reframe.pycontract.) -
-progress pipe:1 -nostatsis required for the progress UI. It routes the key=value progress stream to stdout sorun()can parse a real percentage; without it you get coarse/no progress. Every builder includes it. -
-yoverwrites; output-path collision guard. Builders pass-y. The convert feature adds a.convertedinfix when the derived dest equals the source so a re-encode never clobbers the original. (convert.py:output_path.) -
(verthor/WSL engine only) never pipe the script via stdin. The WSL verthor adapter passes the script as an argv element (
wsl bash <script> …); piping ittr | bashover stdin corrupts mediapipe (it reads stdin). Not a scaling issue but it lives in the samereframe.pyand will eat hours if hit.
4. "Contain / letterbox-pad" — NOT in this repo (standard ffmpeg, flagged)
This codebase only does cover (§2). If a task genuinely needs the whole source
frame visible inside the target canvas (black bars), that is the standard ffmpeg
scale … force_original_aspect_ratio=decrease + pad idiom. It is not present
in media_studio — use it only as an explicit, separate path and say so:
# STANDARD FFMPEG (not in this repo) — fit-inside + center-pad to 1080x1920:
ffmpeg -i <in> -vf \
"scale=1080:1920:force_original_aspect_ratio=decrease:flags=lanczos,\
pad=1080:1920:(ow-iw)/2:(oh-ih)/2,setsar=1" \
-c:v libx264 -c:a copy <out>
force_original_aspect_ratio=decrease scales to fit inside the box; pad
centers it and fills the rest. Mirror image of the repo's cover-crop. Keep
flags=lanczos and setsar=1 for the same reasons as §2/§3.
5. Resize-in-place / scaling effects (zoom.py)
For a subtle scale animation that keeps the frame size constant (Ken-Burns
slow push + punch-in on beats), use zoompan, not a one-shot scale:
# zoom.build_zoom_filter — keeps s=WxH, centers the zoom window
f"zoompan=z='<z_expr>':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=1:s={W}x{H}:fps={fps}"
Notes: d=1 = one output frame per input frame (otherwise zoompan holds frames
and stutters); s=WxH fixes output size; fps must match the timebase the
on-based z expression assumes (DEFAULT_FPS = 30, the §4 vertical pipeline
rate). Zoom is video-only — build_zoom_argv re-encodes video (libx264) and
stream-copies audio (-c:a copy). Z is clamped to MAX_ZOOM = 1.5 so the
crop never starves the frame.
6. Worked example: horizontal 1920×1080 → vertical 9:16 Reel
Goal: turn a landscape clip into a 1080×1920 vertical for Reels, cover-crop (repo default), best quality.
- Resolve dimensions (don't hardcode):
from media_studio.features import aspect w, h = aspect.output_dimensions("9:16") # (1080, 1920) - Build the argv (cover-crop → lanczos scale → setsar):
ffmpeg -hide_banner -nostdin -y -i input_1920x1080.mp4 \ -vf "crop=ih*9/16:ih,scale=1080:1920:flags=lanczos,setsar=1" \ -c:v libx264 -c:a copy -progress pipe:1 -nostats output_1080x1920.mp4crop=ih*9/16:ih→crop=607.5≈608:1080centered (the 9:16 slice of a 1080-tall frame). The left/right of the landscape frame is dropped — expected.scale=…:flags=lanczos→ high-quality up to 1080×1920.setsar=1→ square pixels so the player shows true 9:16.
- Run it through
ffmpeg.run(argv, total_sec=ffprobe_duration(in))so progress is a real percentage and stderr is drained (no deadlock).
If instead the whole landscape frame must stay visible (bars top/bottom), switch to the §4 contain/pad recipe and tell the user the repo's reframe engines don't do that.
Quick checklist before shipping a resize
- [ ] Dimensions from
aspect.output_dimensions()(even, curated). - [ ]
flags=lanczoson any downscale. - [ ]
setsar=1after any aspect-changingscale. - [ ]
scale=-2:H(not-1) for aspect-preserving auto-size. - [ ] argv list,
-y,-progress pipe:1 -nostats. - [ ] stderr drained if you spawn ffmpeg yourself.
- [ ] cover-crop vs contain-pad decision made explicit (repo = cover only).
04 同仓库更多 Skills
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.
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 / 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.
visual-review
Take screenshots of web pages and UI using Playwright for visual review and iteration
status
Diagnostic status report — shows metaswarm installation state, project setup, and potential issues
start
Use when starting work on any task, when the user mentions metaswarm, or when the user wants to begin tracked development work
setup
Interactive project setup — detects your project, configures metaswarm, writes project-local files