{"slug":"jianshuo-wjs-syncing-multicam-54608c","title":"\u591a\u673a\u4f4d\u97f3\u9891\u540c\u6b65\u4e0esidecar\u8f93\u51fa","description":"\u7528polysync\u5bf9\u540c\u4e00\u4e8b\u4ef6\u7684\u591a\u4e2a\u89c6\u9891/\u97f3\u9891\u505a\u97f3\u9891\u5305\u7edc\u4e92\u76f8\u5173\u540c\u6b65\uff0c\u4e3a\u6bcf\u4e2a\u8f93\u5165\u751f\u6210.sync.json\u65c1\u8f66\u6587\u4ef6\uff0c\u4e0d\u6539\u52a8\u539f\u6587\u4ef6\uff1b\u542b\u6f02\u79fb\u68c0\u6d4b\u3001\u90e8\u5206\u8986\u76d6\u3001\u9a8c\u8bc1\u4e0effmpeg\u6d88\u8d39\u65b9\u5f0f\u3002\u9700\u81ea\u884c\u5b89\u88c5polysync\u4e0effmpeg\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-syncing-multicam/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":"8e2624c02e8042c3ae4d33aea5a2dc74","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-syncing-multicam/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":"\u65b9\u6cd5\u8bf4\u660e\u5177\u4f53\u3001\u5b57\u6bb5\u4e0e\u5751\u70b9\u6e05\u6670\uff0c\u4f46\u6267\u884c\u5b8c\u5168\u4f9d\u8d56\u5916\u90e8polysync\u5305\u4e0effmpeg\uff0c\u9700\u5148\u5b89\u88c5\u5e76\u81ea\u884c\u9a8c\u8bc1CLI\u884c\u4e3a\uff1b\u6587\u672c\u5ba1\u9605\u672a\u8fd0\u884c\u4efb\u4f55\u547d\u4ee4\u3002","cost_notes":"polysync\u4e3a\u5f00\u6e90pip\u5305\uff0c\u5b89\u88c5\u672c\u8eab\u901a\u5e38\u65e0\u8d39\u7528\uff1b\u82e5\u4f7f\u7528\u4e91\u5b58\u50a8\u6216\u8f6c\u7801\u670d\u52a1\u9700\u53e6\u884c\u786e\u8ba4\uff0c\u672c\u5305\u672a\u62a5\u4ef7\u3002","source_sha256":"f9d116f4c6da520307c88d201a5ccc9a59b88eed2a19a273ffe14e490da2359f","prerequisites":["Python 3 \u4e0e pip\uff0c\u53ef\u8054\u7f51\u5b89\u88c5polysync","ffmpeg/ffprobe \u5728PATH","\u5f85\u540c\u6b65\u7684\u539f\u59cb\u5a92\u4f53\u6587\u4ef6"],"checks":["\u6bcf\u4e2a\u975e\u53c2\u8003\u8f93\u5165\u65c1\u90fd\u5b58\u5728\u5bf9\u5e94.sync.json\uff0c\u4e14source\u5b57\u6bb5\u6307\u5411\u539f\u6587\u4ef6\u800c\u975e\u65b0\u751f\u6210\u6587\u4ef6","\u53c2\u8003\u8f93\u5165\u7684sidecar\u4e2ddelta_seconds\u4e3a0\uff0c\u4e14reference\u5b57\u6bb5\u6307\u5411\u81ea\u8eab","overlap_in_reference\u4e0eoverlap_in_source\u6ee1\u8db3 start_ref - delta = start_src \u7684\u4e00\u81f4\u6027","verification.median_residual_ms\u4e0eresidual_spread_ms\u5df2\u5199\u5165\uff0c\u4e14\u4e0e\u662f\u5426\u542f\u7528\u6f02\u79fb\u6821\u6b63\u7684\u7ed3\u8bba\u4e00\u81f4","\u6309\u6587\u6863\u7528 -itsoffset \u7f6e\u4e8e -i \u4e4b\u524d\u7684\u65b9\u5f0f\u6d88\u8d39sidecar\uff0c\u62bd\u67e5\u5bf9\u9f50\u7ed3\u679c"],"limitations":["\u5305\u5185\u4e0d\u542bpolysync\u6e90\u7801\uff0c\u7b97\u6cd5\u4e0eCLI\u884c\u4e3a\u4f9d\u8d56\u5916\u90e8\u5305\u7248\u672c\uff0c\u672a\u5728\u672c\u5ba1\u9605\u4e2d\u8fd0\u884c\u9a8c\u8bc1","\u540c\u6b65\u8d28\u91cf\u53d6\u51b3\u4e8e\u97f3\u9891\u4fe1\u566a\u6bd4\u4e0e\u91cd\u53e0\u957f\u5ea6\uff0c\u4f4e\u4fe1\u566a\u6bd4\u8fdc\u573a\u9ea6\u514b\u98ce\u53ef\u80fd\u51fa\u73b0\u6b8b\u5dee\u504f\u5927","\u4ec5\u5904\u7406\u65f6\u95f4\u5bf9\u9f50\uff0c\u4e0d\u505a\u526a\u8f91\u3001\u6df7\u97f3\u6216\u753b\u9762\u5408\u6210","\u90e8\u5206\u8986\u76d6\u6a21\u5f0f\u8df3\u8fc7\u53c2\u8003sidecar\uff0c\u9700\u81ea\u884c\u786e\u8ba4\u53c2\u8003\u5df2\u5c5e\u4e8e\u65e2\u6709\u540c\u6b65\u96c6"],"issues":["SKILL.md\u58f0\u660e\u5b9e\u73b0\u4f4d\u4e8e\u5916\u90e8pip\u5305polysync\uff0c\u5305\u5185\u65e0\u811a\u672c\uff0c\u65e0\u6cd5\u79bb\u7ebf\u6838\u9a8cCLI\u53c2\u6570\u4e0e\u8f93\u51fa\u5b57\u6bb5\u662f\u5426\u4e0e\u6587\u6863\u4e00\u81f4","\u6587\u6863\u63d0\u5230\u4e0b\u6e38wjs-editing-multicam\u6d88\u8d39sidecar\uff0c\u4f46\u8be5\u6280\u80fd\u672a\u968f\u672c\u5305\u63d0\u4f9b\uff0c\u8de8\u5305\u4f9d\u8d56\u9700\u53e6\u884c\u786e\u8ba4"],"missing_files":[]},"quality_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/files/SHILIU-QUALITY.md","usage_guide":{"task":"\u628a\u540c\u4e00\u4e8b\u4ef6\u7684\u591a\u673a\u4f4d/\u72ec\u7acb\u5f55\u97f3\u5bf9\u9f50\u5230\u7edf\u4e00\u65f6\u95f4\u7ebf\uff0c\u8f93\u51fa\u53ef\u88ab\u4e0b\u6e38\u5de5\u5177\u8bfb\u53d6\u7684\u540c\u6b65\u5143\u6570\u636e\uff0c\u800c\u975e\u91cd\u65b0\u7f16\u7801\u7684\u89c6\u9891\u6587\u4ef6\u3002","inputs":"2\u4e2a\u53ca\u4ee5\u4e0a\u540c\u4e00\u4e8b\u4ef6\u7684\u89c6\u9891\u6216\u97f3\u9891\u6587\u4ef6\uff1b\u672c\u673a\u53ef\u7528\u7684Python 3\u4e0epip\uff1bffmpeg/ffprobe\u5728PATH\u4e2d\uff1b\u9700\u8054\u7f51\u5b89\u88c5polysync\u5305\uff1b\u6307\u5b9a\u4e00\u4e2a\u53c2\u8003\u673a\u4f4d\u3002","outputs":"\u6bcf\u4e2a\u8f93\u5165\u65c1\u751f\u6210<input>.sync.json\uff0c\u542bdelta_seconds\u3001drift_slope\u3001\u53cc\u5411\u91cd\u53e0\u533a\u95f4\u4e0everification\u6b8b\u5dee\uff1b\u4e00\u4efd\u5404\u673a\u4f4d\u540c\u6b65\u72b6\u6001\u4e0e\u6b8b\u5dee\u6c47\u603b\u8bf4\u660e\u3002","example_input":"\u6211\u6709 cam_a.MOV\u3001cam_b.MOV \u548c\u4e00\u652f\u53ea\u5f55\u4e86\u4e2d\u6bb5\u7684 lav.m4a\uff0c\u8bf7\u4ee5 cam_a.MOV \u4e3a\u53c2\u8003\uff0c\u628a\u53e6\u5916\u4e24\u4e2a\u5bf9\u9f50\uff0c\u5e76\u544a\u8bc9\u6211 lav \u8986\u76d6\u53c2\u8003\u65f6\u95f4\u7ebf\u7684\u54ea\u4e00\u6bb5\u3002","example_output":"\u793a\u610f\uff1acam_a.MOV.sync.json\uff08delta_seconds=0\uff09\u3001cam_b.MOV.sync.json\uff08delta_seconds\u224812.3\uff0c\u542boverlap\u533a\u95f4\u4e0everification\uff09\u3001lav.m4a.sync.json\uff08--partial\u6a21\u5f0f\uff0cdelta\u8f83\u5927\uff09\uff1b\u53e6\u9644\u4e00\u5f20\u8868\u5217\u51fa\u5404\u6587\u4ef6delta\u3001\u91cd\u53e0\u7a97\u53e3\u3001median_residual_ms\u4e0e\u662f\u5426\u9700\u6f02\u79fb\u6821\u6b63\u3002\u6570\u503c\u4ee5\u5b9e\u9645\u8fd0\u884c\u4e3a\u51c6\uff0c\u6b64\u5904\u4ec5\u4e3a\u7ed3\u6784\u793a\u610f\u3002"},"read_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c","skill_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/files/SKILL.md","download_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/download","install_url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/install","files":[{"path":"LICENSE","bytes":1082,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/files/LICENSE"},{"path":"SHILIU-QUALITY.md","bytes":1956,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/files/SHILIU-QUALITY.md"},{"path":"SKILL.md","bytes":11825,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/files/SKILL.md"},{"path":"shiliu-quality.json","bytes":1822,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/files/shiliu-quality.json"},{"path":"shiliu-source.json","bytes":272,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/files/shiliu-source.json"},{"path":"usage.json","bytes":1089,"url":"https://shiliuai.cn/api/v2/skills/jianshuo-wjs-syncing-multicam-54608c/files/usage.json"}],"content":"---\nname: wjs-syncing-multicam\ndescription: Use when the user has 2+ video / audio recordings of the same event captured by different devices (cameras, phones, separate audio recorders) and wants them aligned to a single common timeline. Outputs only a lightweight `.sync.json` sidecar per input \u2014 original files are never re-encoded. Triggers \u2014 \"\u591a\u673a\u4f4d\u540c\u6b65\", \"\u5bf9\u9f50\u8fd9\u51e0\u4e2a\u673a\u4f4d\", \"match camera timelines\", \"sync these angles\", \"audio drift between cameras\", \"separate audio recorder\", \"Riverside / Zoom recording that needs to line up\".\n---\n\n# wjs-syncing-multicam\n\nCompute a single time offset for each multi-source recording of the same event using audio cross-correlation, and emit a `.sync.json` sidecar next to each original. **Originals are never modified, copied, or re-encoded.** Downstream tools use `-itsoffset` to apply the offset at consume time.\n\n## Setup & commands\n\nThe implementation lives in the open-source **`polysync`** pip package (<https://pypi.org/project/polysync/> \u00b7 <https://github.com/jianshuo/polysync>) \u2014 this skill no longer ships its own scripts. Ensure it's installed, then drive it via its CLI:\n\n```bash\npython3 -m pip install -U polysync      # needs ffmpeg/ffprobe on PATH\n\npolysync sync   REFERENCE SOURCE        # align SOURCE to REFERENCE, write sidecars\npolysync sync   REFERENCE SOURCE --partial   # source covers only part of the session\npolysync verify REFERENCE SOURCE SOURCE.sync.json   # independent residual check\n```\n\nRun one `polysync sync` per non-reference angle (reference first, same reference each time). The sections below document the algorithm, the sidecar schema, and the gotchas baked into the package \u2014 read them to interpret output and choose flags.\n\n## Design principle \u2014 sidecar over re-encode\n\nEarlier versions of this skill produced `*_synced.MOV` files by trimming + re-encoding to bake the offset into the file. We removed that:\n\n- **Disk** \u2014 a 75-min 4K shoot from 3 cameras is 60+ GB. Re-encoded synced copies double that for no information gain.\n- **Quality** \u2014 every re-encode is lossy. The originals are the source of truth; sidecars are reversible metadata.\n- **Speed** \u2014 `_synced.MOV` generation took 10+ min per file on Apple Silicon; sidecar emission takes seconds.\n- **Composability** \u2014 any downstream tool (`polysync edit`, NLE import, ffmpeg one-liners) reads the sidecar and applies the offset itself. No tool-specific file format lock-in.\n\n## When NOT to use\n\n- Single-camera footage \u2014 nothing to sync to. For splitting one source into clips, use **video-segmentation**.\n- Sources already aligned in an NLE timeline \u2014 don't fight the editor.\n- For the auto-edit / cut / PiP rendering step that comes AFTER sync, use **wjs-editing-multicam** (consumes these sidecars).\n\n## Why envelope-based, not raw waveform\n\nRaw PCM cross-correlation gives weak peaks and false matches when the two mics have different gain / room response \u2014 i.e., almost always with a secondary cam. The log-energy envelope captures dialogue and music dynamics, which both mics hear regardless of frequency response. **Don't skip the envelope step \u2014 it's the entire reason this skill is robust at low SNR.**\n\n## Algorithm\n\n1. **Extract mono PCM at 8 kHz, 16-bit** from each input. The audio stream is **auto-selected by loudness** (`loudest_audio_stream`): probe each `0:a:N` over a 60 s mid-file window and pick the highest mean volume. Multi-track pro cameras break a naive `0:a:0` \u2014 Sony FX6 MXF clips carry 4 mono PCM tracks and routinely leave a:0 / a:1 **dead (~-90 dB)** with the room mic on a:2 / a:3; correlating the silent track fails to sync. Single-stream inputs (most MP4 cams) short-circuit to a:0.\n2. **Log-energy envelope** at 100 Hz (10 ms hop, 50 ms window). High-pass with a 2nd-order Butterworth, 0.05 Hz cutoff, filtfilt \u2014 removes slow drift and gain offsets.\n3. **FFT cross-correlate envelopes** end-to-end \u2192 coarse offset (~10 ms).\n4. **Refine at sample level** with a 60 s probe from B near the coarse-aligned position in A, \u00b12 s search window, parabolic peak interpolation.\n5. **Multi-probe drift check** \u2014 repeat step 4 every ~3 min. Linear fit `delta(t) = slope\u00b7t + intercept` reveals real clock drift (5\u201350 ppm typical). Use the **midpoint-canonical** offset (`slope \u00b7 midpoint + intercept`) so residual error is symmetric around zero.\n6. **Compute overlap window** in the reference timeline: `overlap = [max(0, delta), min(ref_dur, delta + src_dur)]`.\n7. **Emit `.sync.json` sidecar** next to each non-reference input. No file is copied, trimmed, or re-encoded. The reference input gets a sidecar too (with `delta_seconds: 0`) so downstream code can treat all inputs uniformly.\n\n`polysync sync` is the implementation. It emits **only** the `.sync.json` sidecar \u2014 no `_synced.MOV`, no re-encode.\n\n## Sidecar schema (`<input>.sync.json`)\n\nOne sidecar per original input, written next to it. Pure JSON, no comments in-file \u2014 the field reference below is canonical.\n\n```json\n{\n  \"_about\": \"Sync metadata for cam_b.MOV. Apply via ffmpeg -itsoffset. See wjs-syncing-multicam SKILL.md for full schema.\",\n  \"schema_version\": 1,\n  \"source\": \"cam_b.MOV\",\n  \"reference\": \"cam_a.MOV\",\n  \"delta_seconds\": 12.345,\n  \"drift_slope\": 1.8e-5,\n  \"overlap_in_reference\": [12.345, 4512.180],\n  \"overlap_in_source\":    [0.000,   4499.835],\n  \"verification\": {\n    \"median_residual_ms\": 4.2,\n    \"residual_spread_ms\": 11.8,\n    \"probe_count\": 24\n  }\n}\n```\n\n### Field reference\n\n| Field | Type | Meaning |\n|---|---|---|\n| `_about` | string | Human-readable one-liner. Includes pointer back to this SKILL.md. Always present. |\n| `schema_version` | int | Bumps on any breaking change to this schema. Current: `1`. |\n| `source` | string | Filename of the original this sidecar describes. Relative to the sidecar's directory. **Never points to a re-encoded file.** |\n| `reference` | string | The input whose timeline we're aligned to. Reference's own sidecar lists itself here. |\n| `delta_seconds` | float | The source's `t=0` expressed in the reference's timeline. **If positive, source starts after reference; pass to ffmpeg as `-itsoffset <delta>`.** Can be negative (source starts before reference, e.g. early-rolling camera). |\n| `drift_slope` | float | Linear clock-drift slope (dimensionless, ~10\u207b\u2075). `0.0` means no measurable drift. Downstream applies `atempo = 1 + drift_slope` to the source ONLY for sync-sound / long-form lip-sync \u2014 for camera-cut editing, ignore. |\n| `overlap_in_reference` | `[start, end]` (seconds) | The window during which both source and reference have coverage, expressed in the reference's timeline. Use this to trim outputs to mutually-valid time ranges. |\n| `overlap_in_source` | `[start, end]` (seconds) | Same window expressed in the source's local timeline. `overlap_in_reference[0] - delta_seconds = overlap_in_source[0]`. |\n| `verification` | object | Output of running `polysync verify` \u2014 drives a \"did sync converge?\" gate. `median_residual_ms` should be a few ms; `residual_spread_ms` > 1 frame at delivery fps means drift correction was needed but skipped. |\n\n## How downstream consumes the sidecar\n\n`-itsoffset` is per-input in ffmpeg and applies BEFORE `-i`. Always read the source's `delta_seconds` from the sidecar:\n\n```bash\n# Play cam_b aligned to cam_a's timeline\nffmpeg -itsoffset $(jq -r .delta_seconds cam_b.MOV.sync.json) -i cam_b.MOV \\\n       -i cam_a.MOV \\\n       -filter_complex \"[0:v][1:v]hstack\" out.mp4\n\n# Trim to mutual overlap window (read from cam_b.MOV.sync.json)\nffmpeg -ss <overlap_in_source[0]> -i cam_b.MOV -t <overlap_dur> ...\n```\n\nFor `wjs-editing-multicam`, `polysync edit` ingests every `<input>.sync.json` automatically; you don't compose these flags by hand.\n\n## Partial-coverage clips \u2014 `polysync sync --partial`\n\nCommon case \u2014 main cams cover 75 min, a Riverside / phone / lavalier recorder only covers the middle 30 min. Run `polysync sync REF.MOV NEW.mp4 --partial`:\n\n1. Cross-correlates the new input against the reference (same envelope algorithm as full-overlap mode).\n2. Finds where the new clip's `t=0` sits in the reference timeline (`delta_seconds` may be large, e.g. 1842.5).\n3. Writes ONLY the source sidecar \u2014 **no black padding, no audio padding, no re-encode.** `overlap_in_reference` tells consumers exactly when this input has coverage; outside that window, fall back to the main cams.\n\nThe `--partial` flag changes only the failure philosophy: it degrades gracefully (median delta on few probes, coarse delta if none) instead of failing on <3 good probes, and skips the reference sidecar (the reference is assumed to already belong to an established sync set). Everything else is identical to the default mode.\n\n## When to skip drift correction\n\nFor camera-cut editing (the common case), \u00b125 ms residual across an hour is below human perception \u2014 pass `drift_slope: 0.0` and use only the midpoint `delta_seconds`.\n\nFor sync-sound / lip-sync at long durations (>30 min and `verification.residual_spread_ms > 40`), downstream applies `atempo = 1 + drift_slope` to the source. Source files are still not modified \u2014 the `atempo` filter runs at consume time.\n\n## Verification (always run)\n\n`polysync verify REF.MOV SRC.MOV SRC.sync.json` re-extracts audio from BOTH originals **natively** (loudest stream, no ffmpeg offset) and runs multi-probe correlation again. It applies the sidecar's `delta_seconds` (and, with `--apply-drift`, the drift slope) as **index arithmetic in numpy** \u2014 a probe at reference time `bs` is drawn from the source at local time `bs - delta`, then sought near reference index `bs`; the peak offset is the residual. Writes results back into the sidecar's `verification` field.\n\nPass criteria \u2014 `median_residual_ms < 15` and `residual_spread_ms < 1 frame at delivery fps`. Fail = retry with drift correction enabled.\n\n**A spread-only fail with a near-zero median is usually noise, not desync.** Far-field mics on a wide / B-roll camera (high reverb, low SNR, ncoef ~0.2) produce a few outlier probes that blow up the spread while the median stays at a few ms. For camera-cut editing that is aligned \u2014 the median is the truth; the spread gate is conservative. Only chase it for long-form lip-sync.\n\n## Common pitfalls\n\n- **Raw waveform cross-correlation gives false peaks under low SNR.** Always envelope first \u2014 this is not a tunable, it's the entire premise.\n- **`-itsoffset` semantics differ for audio vs video** \u2014 for sync-correctness it must be the FIRST flag for that input. `ffmpeg -i src -itsoffset X` is wrong; `ffmpeg -itsoffset X -i src` is right.\n- **`-itsoffset` is a NO-OP when muxing to headerless raw PCM (`-f s16le`).** There are no container timestamps to carry the offset, so ffmpeg silently drops it and inserts NO leading silence. Any analysis that extracts to raw PCM (like `polysync verify`) must apply the offset by **index arithmetic in numpy**, never via `-itsoffset` on the extraction. Symptom of getting this wrong: verification residuals scatter by hundreds of ms with `ncoef ~0` even though sync itself was perfect.\n- **Naive `0:a:0` extraction silently syncs against a dead track.** Multi-track cameras (Sony FX6 MXF: a:0/a:1 often -90 dB, mic on a:2) need loudness-based stream selection. `polysync sync` and `polysync verify` both pick the SAME loudest track automatically \u2014 if you ever reimplement, keep that invariant or residuals are meaningless.\n- **Sidecar paths must be relative to the sidecar file's directory**, not the working directory of the consuming process. Resolve `source` / `reference` against `Path(sidecar).parent`.\n- **Don't bake `drift_slope` into the sidecar's `delta_seconds`.** They're separate fields for a reason \u2014 naive consumers can ignore drift, sync-sound consumers can apply it. Mixing them loses information.\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"}
