{"slug":"jianshuo-wjs-reframing-video-59bff4","title":"\u6a2a\u7ad6\u5c4f\u89c6\u9891\u91cd\u6784\u56fe\u88c1\u526a","description":"\u628a\u6a2a\u5c4f\u89c6\u9891\u88c1\u6210\u7ad6\u5c4f\uff08\u6216\u53cd\u5411\uff09\uff0c\u6309\u753b\u9762\u4e2d\u5634\u90e8\u52a8\u4f5c\u5224\u65ad\u5f53\u524d\u8bf4\u8bdd\u4eba\uff0c\u7528\u8fdf\u6ede\u907f\u514d\u955c\u5934\u6765\u56de\u8df3\uff0c\u6309\u8bf4\u8bdd\u4eba\u5206\u6bb5\u56fa\u5b9a\u88c1\u526a\u7a97\u53e3\uff0c\u518d\u7528\u4e00\u6b21 ffmpeg \u6e32\u67d3\u8f93\u51fa\u3002\u9644\u88c1\u526a\u811a\u672c\u4e0e\u672c\u5730\u6a21\u578b\u6587\u4ef6\uff0c\u9002\u5408\u64ad\u5ba2\u3001\u8bbf\u8c08\u3001\u53e3\u64ad\u7c7b\u7d20\u6750\u7684\u4e8c\u6b21\u5206\u53d1\u3002","category":"\u526a\u8f91\u4e0e\u5305\u88c5","tags":["jianshuo","\u5efa\u7855","\u526a\u8f91\u4e0e\u5305\u88c5"],"author":"\u5efa\u7855 \u00b7 jianshuo","source_url":"https://github.com/jianshuo/claude-skills/blob/d36bc423286a34eb32adc604121e5c7714b08740/wjs-reframing-video/SKILL.md","license":"MIT","status":"published","verification":"source_reviewed","verification_notes":"\u5df2\u6838\u5bf9\u539f\u59cb\u4ed3\u5e93\u3001\u56fa\u5b9a\u63d0\u4ea4\u3001\u8bb8\u53ef\u3001\u6280\u80fd\u6b63\u6587\u4e0e\u5305\u7ed3\u6784\uff0c\u5e76\u8865\u5145\u4e2d\u6587\u6267\u884c\u9a8c\u6536\u8bf4\u660e\uff1b\u672a\u8fd0\u884c\u5305\u5185\u811a\u672c\u3002","revision":"4530462302c24b3c80a94226371eb17c","created_at":"2026-11-02T14:47:08+00:00","updated_at":"2026-11-02T14:47:08+00:00","source_repo":"jianshuo/claude-skills","source_path":"wjs-reframing-video/SKILL.md","source_commit":"d36bc423286a34eb32adc604121e5c7714b08740","source_updated_at":"2026-11-02T14:46:04+00:00","stars":0,"proxy_score":null,"score":null,"grade":null,"dimensions":{},"evaluation":{},"installs":0,"platforms":[],"formats":[],"category_slug":"packaging","category_name":"\u526a\u8f91\u4e0e\u5305\u88c5","quality":{"review_type":"text_and_package_review","readiness":"requires_setup","reviewed_at":"2026-11-02T14:46:04+00:00","review_summary":"\u88c1\u526a\u6570\u5b66\u3001\u5206\u6bb5\u4e0e\u515c\u5e95\u7b56\u7565\u5199\u5f97\u5177\u4f53\uff0c\u811a\u672c\u4e0e\u6a21\u578b\u968f\u5305\u63d0\u4f9b\uff0c\u5c5e\u53ef\u7528\u7684\u89c6\u9891\u91cd\u6784\u5de5\u5177\uff1b\u4f46\u9700\u81ea\u5907 ffmpeg \u4e0e Python \u4f9d\u8d56\uff0c\u4e14\u5fc5\u987b\u4eba\u5de5\u6838\u5bf9\u68c0\u6d4b\u65e5\u5fd7\uff0c\u96f6\u68c0\u6d4b\u65f6\u6539\u7528\u56fa\u5b9a\u88c1\u526a\u3002","cost_notes":"\u65e0\u5916\u90e8\u4ed8\u8d39\u670d\u52a1\uff1b\u4ec5\u672c\u5730\u7b97\u529b\u4e0e\u78c1\u76d8\u5360\u7528\u3002\u82e5\u6539\u7528\u4e91\u7aef\u8f6c\u7801\u9700\u53e6\u884c\u786e\u8ba4\u8d39\u7528\uff0c\u672c\u5305\u672a\u6d89\u53ca\u3002","source_sha256":"b1acc670b38919b9d04655f2921133a61ee8d132c468cc22d105ef7e2b74f739","prerequisites":["ffmpeg \u4e0e ffprobe \u5728 PATH","Python \u73af\u5883\u5b89\u88c5 mediapipe\u3001opencv-python\u3001numpy\uff08mediapipe \u5bf9 Python \u7248\u672c\u6709\u4e0a\u9650\uff0c\u9700 3.12 \u4e00\u7c7b\u53ef\u7528\u7248\u672c\uff09","\u9996\u6b21\u8fd0\u884c\u9700\u8054\u7f51\u4e0b\u8f7d face_landmarker.task\uff0c\u6216\u786e\u8ba4\u5305\u5185 models/ \u5df2\u5c31\u4f4d","\u65e0\u9700\u8d26\u53f7\uff1b\u4e0d\u6d89\u53ca\u4ed8\u8d39 API"],"checks":["crop.json \u4e2d crop_window \u4e0e\u6e90\u5c3a\u5bf8\u6ee1\u8db3\u53cd\u5411\u5bbd\u9ad8\u6bd4\u516c\u5f0f\uff08\u5982 1920x1080 \u2192 608x1080\uff09\uff0ctarget_size \u4e0e --output-size \u4e00\u81f4","chunks \u8986\u76d6\u5168\u7247\u65f6\u95f4\u8f74\u4e14\u9996\u5c3e\u76f8\u63a5\u3001\u65e0\u91cd\u53e0\uff0c\u6bcf\u6761\u542b speaker_id \u4e0e\u56fa\u5b9a cx/cy","\u7528 ffprobe \u6838\u5bf9\u8f93\u51fa\u5206\u8fa8\u7387\u3001\u65f6\u957f\u4e0e\u6e90\u4e00\u81f4\uff08\u5141\u8bb8\u7f16\u7801\u8bef\u5dee\uff09\uff0c\u97f3\u9891\u4e3a\u6d41\u62f7\u8d1d\u672a\u91cd\u7f16\u7801","\u62bd\u53d6\u5206\u6bb5\u8fb9\u754c\u524d\u540e\u5404\u4e00\u5e27\uff0c\u786e\u8ba4\u8bf4\u8bdd\u4eba\u4f4d\u4e8e\u88c1\u526a\u7a97\u53e3\u5185\u4e14\u8fb9\u754c\u5904\u4e3a\u786c\u5207\u800c\u975e\u6f02\u79fb","\u65e5\u5fd7\u4e2d\u82e5\u51fa\u73b0 0 face observations \u6216 fallback 100%\uff0c\u786e\u8ba4\u5df2\u6539\u7528\u56fa\u5b9a X \u88c1\u526a\u800c\u975e\u76f4\u63a5\u4ea4\u4ed8"],"limitations":["\u8bf4\u8bdd\u4eba\u5224\u5b9a\u4ec5\u57fa\u4e8e\u5634\u90e8\u52a8\u4f5c\u65b9\u5dee\uff0c\u6253\u54c8\u6b20\u3001\u7b11\u3001\u5403\u4e1c\u897f\u4f1a\u88ab\u8bef\u5224\u4e3a\u8bf4\u8bdd","\u4fa7\u8138\u8d85\u8fc7\u7ea6 60\u00b0 \u6216\u4eba\u8138\u8ddd\u955c\u5934\u8fc7\u8fdc\u65f6\u5173\u952e\u70b9\u5931\u6548\uff0c\u4f1a\u9000\u56de\u6700\u5927\u8138\u7b56\u7565","\u591a\u4eba\u62a2\u8bdd\u573a\u666f\u53ea\u4fdd\u7559\u4e00\u4e2a\u88c1\u526a\u8f68\u9053\uff0c\u65e0\u6cd5\u540c\u65f6\u8ddf\u4e24\u4e2a\u4eba","\u672a\u5728\u672c\u73af\u5883\u8fd0\u884c\u811a\u672c\uff0c\u6027\u80fd\u6570\u5b57\u4e0e\u68c0\u6d4b\u6548\u679c\u5747\u6765\u81ea\u6587\u6863\u63cf\u8ff0\uff0c\u9700\u81ea\u884c\u9a8c\u8bc1"],"issues":["SKILL.md \u4e2d\u56fa\u5b9a\u88c1\u526a\u793a\u4f8b\u5199\u6b7b\u4e86\u672c\u673a imageio_ffmpeg \u7684\u7edd\u5bf9\u8def\u5f84\uff0c\u7528\u6237\u9700\u81ea\u884c\u66ff\u6362\u4e3a\u7cfb\u7edf ffmpeg","\u6587\u6863\u79f0\u9996\u6b21\u8fd0\u884c\u81ea\u52a8\u4e0b\u8f7d\u6a21\u578b\u5230 ~/.claude/skills/... \u8def\u5f84\uff0c\u4e0e\u5305\u5185\u5df2\u9644 models/ \u7684\u52a0\u8f7d\u903b\u8f91\u662f\u5426\u4e00\u81f4\u5f85\u6838\u5b9e","\u4f9d\u8d56\u5b89\u88c5\u8bf4\u660e\u672a\u6807\u6ce8 mediapipe \u652f\u6301\u7684 Python \u7248\u672c\u4e0a\u9650\uff0c\u4ec5\u5728\u53e6\u4e00\u6280\u80fd\u4e2d\u63d0\u53ca 3.12"],"missing_files":[]},"quality_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/SHILIU-QUALITY.md","usage_guide":{"task":"\u5c06\u4e00\u6761\u6a2a\u5c4f\uff08\u6216\u7ad6\u5c4f\uff09\u89c6\u9891\u6309\u76ee\u6807\u5e73\u53f0\u65b9\u5411\u505a\u53cd\u5411\u5bbd\u9ad8\u6bd4\u88c1\u526a\uff0c\u88c1\u526a\u7a97\u53e3\u8ddf\u968f\u6b63\u5728\u8bf4\u8bdd\u7684\u4eba\uff0c\u8f93\u51fa\u65b0\u89c6\u9891\u4e0e\u88c1\u526a\u8ba1\u5212 sidecar\uff0c\u4e0d\u6539\u52a8\u539f\u6587\u4ef6\u3002","inputs":"\u6e90\u89c6\u9891\u6587\u4ef6\u8def\u5f84\uff1b\u76ee\u6807\u65b9\u5411\uff08portrait/landscape\uff0c\u53ef\u81ea\u52a8\u63a8\u65ad\uff09\uff1b\u53ef\u9009\u53c2\u6570\uff1a\u91c7\u6837\u5e27\u7387\u3001MAR \u65b9\u5dee\u9608\u503c\u3001\u6700\u77ed\u5206\u6bb5\u65f6\u957f\u3001\u8f93\u51fa\u5c3a\u5bf8\u3001\u7801\u7387\u3002\u8fd0\u884c\u73af\u5883\u9700 ffmpeg/ffprobe \u5728 PATH\uff0cPython \u73af\u5883\u5b89\u88c5 mediapipe\u3001opencv-python\u3001numpy\uff1b\u9996\u6b21\u8fd0\u884c\u9700\u80fd\u8054\u7f51\u4e0b\u8f7d\u6216\u5df2\u968f\u5305\u63d0\u4f9b face_landmarker.task \u6a21\u578b\u3002","outputs":"<input>_cropped.mp4\uff08\u88c1\u526a\u5e76\u7f29\u653e\u540e\u7684\u6210\u7247\uff09\uff1b<input>.crop.json\uff08\u6e90\u5c3a\u5bf8\u3001\u76ee\u6807\u5c3a\u5bf8\u3001\u88c1\u526a\u7a97\u53e3\u3001\u5206\u6bb5\u5217\u8868\u542b t0/t1/cx/cy/speaker_id\u3001\u53c2\u6570\u4e0e\u68c0\u6d4b\u7edf\u8ba1\uff09\uff1b\u7ec8\u7aef\u65e5\u5fd7\u4e2d\u7684\u6bcf\u5f20\u8138\u5728\u5c4f\u65f6\u957f\u4e0e\u5360\u6bd4\u6458\u8981\u3002","example_input":"\u628a interview.mp4 \u4ece 1920x1080 \u8f6c\u6210 9:16 \u7ad6\u5c4f\uff0c\u8f93\u51fa 1080x1920\uff0c\u7801\u7387 8M\uff0c\u5148\u8dd1\u4e00\u904d\u5e76\u628a crop.json \u91cc\u7684\u5206\u6bb5\u548c\u8bf4\u8bdd\u4eba\u7edf\u8ba1\u8d34\u7ed9\u6211\u3002","example_output":"\u793a\u610f\uff08\u975e\u771f\u5b9e\u8fd0\u884c\u7ed3\u679c\uff09\uff1a\u751f\u6210 interview_cropped.mp4 \u4e0e interview.crop.json\uff1bcrop.json \u542b source_size [1920,1080]\u3001target_size [1080,1920]\u3001crop_window [608,1080]\u3001chunks \u82e5\u5e72\u6761\u5404\u5e26 speaker_id\uff0c\u4ee5\u53ca face_sample_count\u3001track_count \u5b57\u6bb5\uff1b\u65e5\u5fd7\u5217\u51fa face#0\u3001face#1 \u7684\u5728\u5c4f\u65f6\u957f\u4e0e\u767e\u5206\u6bd4\u3002"},"read_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4","skill_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/SKILL.md","download_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/download","install_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/install","files":[{"path":"LICENSE","bytes":1082,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/LICENSE"},{"path":"SHILIU-QUALITY.md","bytes":2514,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/SHILIU-QUALITY.md"},{"path":"SKILL.md","bytes":12257,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/SKILL.md"},{"path":"models/blaze_face_short_range.tflite","bytes":229746,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/models/blaze_face_short_range.tflite"},{"path":"models/face_landmarker.task","bytes":3758596,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/models/face_landmarker.task"},{"path":"scripts/crop.py","bytes":23700,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/scripts/crop.py"},{"path":"shiliu-quality.json","bytes":2255,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/shiliu-quality.json"},{"path":"shiliu-source.json","bytes":271,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/shiliu-source.json"},{"path":"usage.json","bytes":1343,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-reframing-video-59bff4/files/usage.json"}],"content":"---\nname: wjs-reframing-video\ndescription: Use when the user wants to convert a video between horizontal and vertical orientations while preserving the inverted aspect ratio (16:9 \u2194 9:16, 4:3 \u2194 3:4, 21:9 \u2194 9:21). The skill crops a narrow band from the source and tracks the active speaker \u2014 the person whose mouth is moving \u2014 via MediaPipe face landmarks and mouth-aspect-ratio variance, so the talker stays in frame even when other people are visible. Triggers \u2014 \"\u6a2a\u8f6c\u7ad6\", \"\u7ad6\u8f6c\u6a2a\", \"\u505a\u6210\u7ad6\u5c4f\u53d1\u6296\u97f3/\u89c6\u9891\u53f7/\u5c0f\u7ea2\u4e66\", \"16:9 to 9:16\", \"make this vertical for Reels / TikTok / YouTube Shorts\", \"crop to portrait\", \"convert to landscape\".\n---\n\n# wjs-reframing-video\n\nConvert a video's orientation by **cropping** a narrow band from the source \u2014 not by physically rotating it. The crop window follows the **active speaker** (the face whose mouth is *moving*), not just the largest or most-confident face. A `.crop.json` sidecar records the crop plan, the per-segment speaker decisions, and the parameters used. The original input is never modified.\n\n## When to use\n\n- Repurposing a 16:9 podcast / interview / talk for vertical short-video platforms (WeChat Channels \u89c6\u9891\u53f7, Douyin \u6296\u97f3, Xiaohongshu \u5c0f\u7ea2\u4e66, YouTube Shorts, TikTok, Reels).\n- Repurposing a 9:16 phone recording for horizontal players (YouTube long-form, blog embeds).\n- Repurposing 4:3 archive footage for 3:4 mobile, or vice versa.\n\nThe output aspect is the source aspect with width and height swapped \u2014 16:9 \u2192 9:16, not \"letterboxed 16:9 in a 9:16 frame\".\n\n## When NOT to use\n\n- **Multi-person Q&A** where each face needs its own crop \u2014 this skill picks one crop track per video. For per-speaker split renders, use **wjs-editing-multicam** instead.\n- **Animated content / B-roll with no faces** \u2014 falls back to center crop, usually wrong for the intent.\n- **Heavy camera motion in the source** (handheld pan/zoom) \u2014 the face tracker amplifies camera shake. Stabilize first.\n- **Source already at target aspect** \u2014 no work to do.\n\n## What this skill IS \u2014 and IS NOT\n\n| Is | Is not |\n|---|---|\n| **Visual active-speaker detection** via MAR (mouth-aspect-ratio) variance | Audio-visual fusion (audio energy + lip motion cross-correlated) |\n| Stable face tracking across frames by center-distance matching | Re-identification across long gaps / occlusions |\n| Speaker-aligned segments with hysteresis to prevent flicker | Frame-by-frame switching on every flicker |\n| `--face-pick speaker` (default) \u2014 pick whoever's mouth is moving | `--face-pick largest` (opt-in legacy) \u2014 pick largest face |\n| **Hard cuts between segments, fixed crop within each segment** (`--motion cut`, default) | Smooth panning that drifts during a speaker's turn (opt-in `--motion smooth`) |\n| Audio stream-copy (bit-exact) | Audio reprocessing / re-encoding |\n| MediaPipe Tasks `FaceLandmarker` (478-pt mesh) at 5 fps sampled via ffmpeg | Per-frame neural inpainting / out-painting |\n| One `ffmpeg crop + scale` pass | Frame-by-frame Python compositor |\n\nFalls back to \"largest face\" automatically when no one is talking (silence, music-only stretches).\n\n## Dependencies\n\n```bash\npip install mediapipe opencv-python numpy\n```\n\n(MediaPipe lives outside the standard Python distribution; ffmpeg and ffprobe must be on `PATH`.)\n\n**First-run model download**: MediaPipe 0.10+ uses the Tasks API, which needs a `face_landmarker.task` model file (~4 MB). On the first call, `crop.py` downloads it to `~/.claude/skills/wjs-reframing-video/models/` and caches it for subsequent runs. The script fails offline on first run.\n\n**Range limitation**: The bundled landmarker is tuned for faces within ~2 m of the camera (selfie / podcast / interview distance). Wide event shots with small faces may not detect \u2014 sample a frame first to confirm.\n\n## Crop math\n\nSource aspect = `W / H`. Target aspect = `H / W` (inverted). Compute crop window:\n\n| Source orientation | Crop window |\n|---|---|\n| Horizontal (W > H) \u2192 Portrait | `W_crop = H \u00d7 H / W`, `H_crop = H` (narrow vertical band) |\n| Portrait (W < H) \u2192 Horizontal | `W_crop = W`, `H_crop = W \u00d7 W / H` (narrow horizontal band) |\n\nFor 1920\u00d71080 \u2192 portrait, `W_crop = 608`, `H_crop = 1080`. Final scale to 1080\u00d71920 (upscale ~1.78\u00d7).\nFor 1080\u00d71920 \u2192 landscape, `W_crop = 1080`, `H_crop = 608`. Final scale to 1920\u00d71080.\n\nOverride the final size via `--output-size 1080x1920` if you want native crop dimensions instead of upscaling.\n\n## Pipeline\n\n1. **Probe** input dimensions, fps, duration via ffprobe.\n2. **Decide orientation** \u2014 auto from aspect (`--target portrait|landscape` to override).\n3. **Sample frames at `--sample-fps`** (default 5; high enough to catch mouth motion \u2014 Nyquist for speech is ~10 Hz, we need at least 4\u20135 fps).\n4. **Detect face landmarks** per sampled frame with MediaPipe Tasks `FaceLandmarker` (478 landmarks). For each detected face record: center, size proxy, MAR (mouth-aspect-ratio = inner-lip vertical distance / horizontal mouth-corner distance).\n5. **Track faces** across frames by center-distance matching \u2192 each face gets a stable `face_id`.\n6. **Per-sample active speaker**: for each face track, variance of MAR over a sliding window (`--mar-var-window-sec`, default 1 s). The face with the highest variance is \"speaking\". Below `--mar-var-threshold`, no one is speaking \u2192 fall back to largest face.\n7. **Hysteresis**: a candidate switch only commits if the new speaker is stable for `--min-segment-sec` (default 1.5 s). Shorter flickers are squashed \u2014 prevents the crop from ping-ponging on a one-frame mis-detection.\n8. **Speaker-aligned segments** \u2192 for each segment, mean (cx, cy) of that speaker's face over the segment becomes the crop center, *fixed* for the full duration of the segment.\n9. **Build a ffmpeg step-function expression** (`--motion cut`, default) that holds each segment's crop position constant and **jumps instantly at each segment boundary** \u2014 the visual feel of a real cut between camera angles. (`--motion smooth` switches to piecewise-linear pan between segment midpoints; rarely the right call for talking-head content because the camera appears to drift mid-sentence.)\n10. **Render** one ffmpeg pass \u2014 `crop=W:H:x='expr':y='expr', scale=OUT_W:OUT_H`. The crop filter evaluates `x` and `y` per frame natively. Audio stream-copied.\n\n`scripts/crop.py` is the implementation. Output side effects:\n- `<input>.crop.json` \u2014 sidecar with the crop plan\n- `<input>_cropped.mp4` \u2014 final cropped + scaled video\n\n## Sidecar schema (`<input>.crop.json`)\n\n```json\n{\n  \"_about\": \"wjs-reframing-video crop plan for cam_a.MOV. Active-speaker detected via MAR variance.\",\n  \"_help\": {\n    \"source_size\":     \"[width, height] in pixels.\",\n    \"target_size\":     \"[width, height] of the final rendered output.\",\n    \"crop_window\":     \"[width, height] of the moving crop in source coords.\",\n    \"chunks\":          \"Speaker-aligned segments: {t0, t1, cx, cy, speaker_id}.\",\n    \"face_pick_mode\":  \"speaker = MAR-variance active-speaker; largest = old behavior.\",\n    \"speaker_id\":      \"Stable face track id. null means no face / silence fallback.\"\n  },\n  \"schema_version\": 2,\n  \"source\": \"cam_a.MOV\",\n  \"source_size\": [1920, 1080],\n  \"target\": \"portrait\",\n  \"target_size\": [1080, 1920],\n  \"crop_window\": [608, 1080],\n  \"face_pick_mode\": \"speaker\",\n  \"sample_fps\": 5.0,\n  \"mar_var_window_sec\": 1.0,\n  \"mar_var_threshold\": 1.5e-4,\n  \"min_segment_sec\": 1.5,\n  \"chunks\": [\n    {\"t0\":  0.0, \"t1\":  4.2, \"cx\": 808, \"cy\": 540, \"speaker_id\": 0},\n    {\"t0\":  4.2, \"t1\": 11.6, \"cx\": 1182, \"cy\": 540, \"speaker_id\": 1},\n    {\"t0\": 11.6, \"t1\": 14.0, \"cx\": 808, \"cy\": 540, \"speaker_id\": 0}\n  ],\n  \"face_sample_count\": 1234,\n  \"track_count\": 2\n}\n```\n\n## Performance\n\n- **Detection** is the slow step. On Apple Silicon at 2 fps sampling, expect ~10\u201320\u00d7 realtime (a 30-min source detects in ~1\u20132 min). Bumping `--sample-fps` makes detection slower but tracking more responsive.\n- **Render** is fast \u2014 single ffmpeg pass with hardware encode (`hevc_videotoolbox` on macOS). Often <1\u00d7 realtime for a 1080p source.\n- For very long sources (>200 chunks), the ffmpeg expression gets cumbersome; the script auto-downsamples chunk midpoints to keep the expression under ~200 control points.\n\n## Common pitfalls\n\n- **Mouth gestures aren't speech** \u2014 a yawn, laugh, eating, or sucking-in-air all raise MAR variance. The detector can briefly mistake these for talking. For high-stakes content, eyeball the speaker timeline in the sidecar (the script prints a `face#N: Xs on screen (Y%)` summary) and re-run with a different `--mar-var-threshold` if needed.\n- **Side-profile or down-tilted faces** \u2014 when a face is rotated >60\u00b0 from camera, MediaPipe may fail to land mouth landmarks reliably, so MAR variance flatlines. The speaker fallback to \"largest face\" kicks in. If you have a long stretch of profile shots, consider `--face-pick largest`.\n- **Two faces with overlapping speech (interruption / talking over)** \u2014 both faces have MAR variance, only one wins. The losing face is treated as listener. For accurate per-speaker tracking under crosstalk, use **wjs-editing-multicam** with separate cams.\n- **Long stretches of silence (B-roll, music)** \u2014 falls back to largest face. If the largest face is wrong (e.g. a listener stays still while the speaker's mic feeds music), you'll see drift. Pre-segment around music-only sections.\n- **Source has burned-in lower-thirds / subtitles** \u2014 for H\u2192V, the lower band gets cropped out; for V\u2192H, it stays but gets stretched. Strip burn-ins before running.\n- **Wide-angle / fish-eye lenses** \u2014 landmarks miss faces near edges. Pre-correct distortion with `ffmpeg lenscorrection` first.\n- **Upscaling artifacts** \u2014 `608\u00d71080 \u2192 1080\u00d71920` is a 1.78\u00d7 upscale and visible on sharp text. Render at native crop dims (`--output-size 608x1080`) and let the platform upscale, if you have overlays you want to keep sharp.\n- **Output bitrate > platform limit** \u2014 default is `--bitrate 12M`. WeChat Channels (\u89c6\u9891\u53f7) caps at 10 Mbps; pass `--bitrate 8M` for that target.\n\n## Zero-detection fallback: deterministic fixed crop\n\nThe \"may not detect\" range limit isn't just a warning \u2014 when MediaPipe\ndetects **0 faces** (far/static two-person interview, ~2 m+ from a wide\nlens), the crop log reads `0 face observations across N sampled frames`\nand the script **center-crops the frame** \u2014 which on an interview set\nlands the window on the *background between the two people* (a fireplace,\na plant, a logo), not on anyone. The output looks broken and no\n`--mar-var-threshold` tuning helps, because there are no landmarks at all.\n\n**Always read the crop log before trusting the output.** If it says\n`0 face track(s) identified` / `(no face / fallback): \u2026 100%`, abandon\nthe MediaPipe crop and do a **deterministic fixed crop** instead. The\ncamera on these shoots is static and the speakers sit at fixed screen\npositions, so a hand-set X offset is rock-solid:\n\n```bash\n# 1920\u00d71080 \u2192 9:16 \u21d2 crop window 608\u00d71080. X = speaker's screen position:\n#   left speaker  \u2192 x=0      right speaker \u2192 x=1920-608=1312      centred \u2192 x=656\n# Do crop + (HLG\u2192SDR tone-map) + 30fps + dense keyframes in ONE pass so the\n# body clip is final and HyperFrames can seek it (see /wjs-overlaying-video).\nZF=~/Library/Python/3.9/.../imageio_ffmpeg/binaries/ffmpeg-macos-aarch64-v7.1\n\"$ZF\" -i clip.mp4 -vf \\\n  \"crop=608:1080:0:0,zscale=t=linear:npl=203,format=gbrpf32le,\\\ntonemap=tonemap=hable:desat=0,\\\nzscale=w=1080:h=1920:t=bt709:m=bt709:p=bt709:r=tv,format=yuv420p,fps=30\" \\\n  -c:v libx264 -crf 19 -preset medium -g 30 -keyint_min 30 \\\n  -color_primaries bt709 -color_trc bt709 -colorspace bt709 \\\n  -c:a aac -b:a 192k -movflags +faststart clip_vert.mp4\n```\n\nVerify by extracting a frame (`-ss 20 -frames:v 1`) and confirming the\nspeaker is centred before committing. For clips that genuinely need to\nfollow *both* speakers (heavy back-and-forth), hand-label per-speaker\nwindows from the transcript and concatenate fixed crops; for a clip that\nis one person's monologue, a single fixed X is enough. Drop the\ntonemap filters if the source is already SDR (bt709) \u2014 applying the HLG\nrecipe to SDR mis-colors it.\n","usage":"\u8bfb\u53d6 SKILL.md\uff0c\u5e76\u6309\u9700\u8bfb\u53d6\u914d\u5957\u6587\u4ef6\uff1b\u4e0b\u8f7d ZIP \u83b7\u53d6\u5b8c\u6574\u5305\u3002\u6280\u80fd\u662f\u4efb\u52a1\u65b9\u6cd5\uff0c\u4e0d\u6388\u4e88\u65b0\u589e\u8d26\u6237\u3001\u4ed8\u8d39\u6216\u7cfb\u7edf\u6743\u9650\u3002"}
