Synced from virality/stages/03-refinement.md in content-extraction skill 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 draft cut-plan.json from 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 --pick argument (e.g. C-01,C-04)

—pick command parsing

Input format: --pick C-01,C-04 (case-insensitive, whitespace-tolerant).

Parser logic:

  1. Split on , and ;. Trim each token.
  2. Normalize each ID to uppercase pattern C-NN (e.g. c-1C-01).
  3. Verify each ID exists in candidates.md (regex match ^## Candidate <ID>). If not found, error and list valid IDs.
  4. 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 --propose does).
  • 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.json v1.1.
  • session_id matches session folder name.
  • source.video_path resolves 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:

  1. Schema validation against assets/cut-plan.schema.json — must pass.
  2. Verbatim re-check on every segment quote against transcript.txt — must pass.
  3. cut-plan.md regenerated from new cut-plan.json (paired markdown for human review).
  4. SHIPPED-CLIPS.md placeholder NOT yet written — that comes after Phase 1 —approve produces final out/clips/.
  5. 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

FailureDetectionMitigation
—pick references missing candidate IDregex match against candidates.md failsError with list of valid IDs
Picked candidate references non-existent beat#beat# not in source-analysis.md inventoryError; suggest re-running S1 (source may have been updated)
Cross-clip beat duplicationshared beat# detected across picked candidatesWarn, proceed; let Phase 1 preview-gate surface to Daniel
Schema validation failsjsonschema.validate raisesSurface error; do NOT write the broken cut-plan.json. Suggest re-running S2 if structurally broken.
Daniel picks 0 candidatesempty argumentError: “—pick requires ≥1 candidate ID”
Daniel picks all candidatesargument lists every ID in candidates.mdAllow, but emit warning: “Picked N/N candidates — Phase 1 preview-gate will render N clips. Did you mean to filter?”