Synced from
virality/stages/03-refinement.mdincontent-extractionskill on 2026-05-18. Edit upstream in the skill; this file is overwritten on next sync.
Stage 3 — Refinement Loop
Daniel reads
candidates.md, picks one or more via--pick C-XX,C-YY. S3 composes draftcut-plan.jsonfrom the picked candidates’ beat# refs. Phase 1 infrastructure (silence-tighten + preview-gate + —expand-pre/post recovery) handles the cut + iteration loop downstream.
Output: <session>/cut-plan.json (draft) + <session>/cut-plan.md (paired)
Triggered by: Daniel running --pick <comma-separated-candidate-ids>
Required inputs
S3 refuses to run without:
<session>/source-analysis.md— S1 inventory<session>/candidates.md— S2 output containing the picked IDs<session>/transcript.txt— verbatim contract source- Comma-separated candidate IDs from
--pickargument (e.g.C-01,C-04)
—pick command parsing
Input format: --pick C-01,C-04 (case-insensitive, whitespace-tolerant).
Parser logic:
- Split on
,and;. Trim each token. - Normalize each ID to uppercase pattern
C-NN(e.g.c-1→C-01). - Verify each ID exists in
candidates.md(regex match^## Candidate <ID>). If not found, error and list valid IDs. - Resolve each picked candidate’s body-beat list via candidates.md fields:
- Opener beat#
- Body beats array
- Close beat#
Beat# → segments mapping
For each picked candidate, compose ONE clip in cut-plan.json:
clip = {
"clip_id": f"{NN}-<slug-from-candidate>",
"title": "<from candidate>",
"pillar": "<from candidate>",
"format_fit": [<from candidate>],
"posting_target": [<from candidate>],
"hook_strength": 4, # default; sub-agent doesn't pre-rank in S2 equal-tier
"retention_risk_notes": "<from candidate's Risks section>",
"target_duration_seconds": <from candidate Duration target>,
"segments": [
# Opener beat first
{"start_seconds": opener.start, "end_seconds": opener.end, "quote": opener.quote, "language": "en", "silence_origin": "llm"},
# Body beats in order
{"start_seconds": body[0].start, "end_seconds": body[0].end, "quote": body[0].quote, "language": "en", "silence_origin": "llm"},
...
# Close beat last
{"start_seconds": close.start, "end_seconds": close.end, "quote": close.quote, "language": "en", "silence_origin": "llm"},
],
"caption_seed": opener.quote,
"broll_cues": [],
"reorder_rationale": f"Composed from virality candidate {ID} via --pick. Opener: beat {NN} (tail-as-opener); body beats {body_list}; close: beat {NN}."
}Pull each beat’s start/end/quote directly from S1 inventory (verbatim — no synthesis). Quotes match transcript.txt by construction.
silence_origin: "llm" — sub-agent composed; will become "silence-detect" after Step 5 (silence_to_cutplan.py —mode tighten) runs.
Multi-clip composition
If --pick C-01,C-04: write TWO clips in cut-plan.json. Each gets sequential clip_id (01-…, 02-…). Order in cut-plan.json matches order in —pick argument.
session_id, version, source.* fields populated from session context. version defaults to "cut-plan.v1.1" (Phase 1.3 schema).
Cross-clip de-duplication
If two picked candidates share a beat# (e.g. both use beat 17 as close), surface a warning:
[S3-DUPLICATION-WARNING] Picked C-01 and C-04 both use beat 17 in close position.
- Same verbatim "<quote>" plays at end of both clips.
- Recommendation: stagger posting ≥3 days OR drop beat 17 from one clip.
- Proceeding with cut-plan.json containing both — Daniel decides at preview-gate.
Don’t auto-resolve — surface, write the cut-plan as picked, let Daniel decide via Phase 1 preview-gate.
Pre-roll / post-roll padding
S3 does NOT add pre-roll padding to beat boundaries. Beat boundaries from S1 are word-start to word-end (raw). Phase 1 silence_to_cutplan.py --mode tighten adds breath via word-attack guard (Rule M, 0.30s pre-roll on word-start).
Rationale: keep S3 deterministic (input → output). All padding decisions live in Phase 1’s silence pipeline.
Refinement iteration (post-S3)
After S3 writes draft cut-plan.json, Daniel runs Phase 1 standard pipeline:
# Apply silence-tightening (Rule M holds preserved; word-attack guard applies)
~/.venvs/afe-whisper/bin/python scripts/silence_to_cutplan.py <session> --mode tighten
# Preview-gate (default behavior since Phase 1.4)
~/.venvs/afe-whisper/bin/python scripts/cut_with_ffmpeg.py <session>
# → proposal/proposal.md + proposal/previews/<clip>.mp4
# Daniel watches previews. If a clip needs adjustment:
scripts/cut_with_ffmpeg.py <session> --clip 01 --expand-pre 0.30 --expand-post 0.50 --propose
# Iterate ~5s round-trip until happy.
# When happy:
scripts/cut_with_ffmpeg.py <session> --approve
# → out/clips/<clip>.mp4 (production)S3 does NOT run silence-tighten or preview-gate itself. It composes the draft cut-plan and surfaces the next-step command for Daniel.
—pick dialect alternatives (deferred)
Daniel may want richer pick syntax in future:
--pick C-01 --opener beat-14— override opener beat--pick C-01 --add-beat 09— add a beat to candidate’s body--pick C-01 --drop-beat 11— drop a beat from candidate’s body
These are NOT in the current —pick spec. Phase 2 v2 ships with comma-separated candidate IDs only. If Daniel hits the limitation in dogfood, add in Phase 2.5.
What S3 deliberately does NOT do
- Does NOT run silence-detect or silence-tighten (Phase 1 does).
- Does NOT render preview MP4s (Phase 1
cut_with_ffmpeg.py --proposedoes). - Does NOT add breath/pre-roll padding (Rule M / silence_to_cutplan.py does).
- Does NOT re-compose candidates (S2’s job; if Daniel wants different candidates, re-run S2 not —pick).
- Does NOT auto-merge picked candidates without Daniel’s explicit —pick. No defaults; —pick is mandatory to trigger S3.
Hard gates for S3 output
- Every segment’s quote is verbatim substring of transcript.txt (whitespace-tolerant). Inherited from S1 inventory by construction.
- cut-plan.json schema-valid against
assets/cut-plan.schema.jsonv1.1. session_idmatches session folder name.source.video_pathresolves to source.mp4 in session folder.- Every
start_seconds < end_seconds. - Every clip has ≥1 segment.
- Banned vocab check on title / caption_seed / reorder_rationale (no revolutionize, seamless, AI-powered, game-changer, leverage, exclamation points).
Verification (S3 output sanity)
After S3 writes cut-plan.json:
- Schema validation against assets/cut-plan.schema.json — must pass.
- Verbatim re-check on every segment quote against transcript.txt — must pass.
- cut-plan.md regenerated from new cut-plan.json (paired markdown for human review).
- SHIPPED-CLIPS.md placeholder NOT yet written — that comes after Phase 1 —approve produces final out/clips/.
- Surface to Daniel: “draft cut-plan written from picks {IDs}. Next: silence_to_cutplan.py —mode tighten, then cut_with_ffmpeg.py —propose.”
Failure modes
| Failure | Detection | Mitigation |
|---|---|---|
| —pick references missing candidate ID | regex match against candidates.md fails | Error with list of valid IDs |
| Picked candidate references non-existent beat# | beat# not in source-analysis.md inventory | Error; suggest re-running S1 (source may have been updated) |
| Cross-clip beat duplication | shared beat# detected across picked candidates | Warn, proceed; let Phase 1 preview-gate surface to Daniel |
| Schema validation fails | jsonschema.validate raises | Surface error; do NOT write the broken cut-plan.json. Suggest re-running S2 if structurally broken. |
| Daniel picks 0 candidates | empty argument | Error: “—pick requires ≥1 candidate ID” |
| Daniel picks all candidates | argument lists every ID in candidates.md | Allow, but emit warning: “Picked N/N candidates — Phase 1 preview-gate will render N clips. Did you mean to filter?” |