davinci-resolve-mcp 2.68.1 → 2.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +213 -0
- package/README.md +46 -3
- package/docs/SKILL.md +181 -1
- package/docs/install.md +27 -1
- package/install.py +26 -1
- package/package.json +1 -1
- package/resolve-advanced/LICENSE +21 -0
- package/resolve-advanced/package.json +2 -1
- package/resolve-advanced/vendor/conform-qc/package.json +1 -0
- package/resolve-advanced/vendor/drp-format/package.json +1 -0
- package/resolve-advanced/vendor/drt-format/package.json +1 -0
- package/scripts/bridge_differential.py +22 -4
- package/scripts/doctor.py +129 -3
- package/scripts/install_resolve_bridge.py +13 -3
- package/src/granular/common.py +1 -1
- package/src/server.py +639 -18
- package/src/utils/beat_detection.py +242 -0
- package/src/utils/bridge_differential.py +27 -1
- package/src/utils/broll_placement.py +201 -0
- package/src/utils/conform_lint.py +437 -0
- package/src/utils/edit_engine.py +633 -1
- package/src/utils/edit_handles.py +206 -0
- package/src/utils/edit_report.py +360 -0
- package/src/utils/first_impression.py +212 -0
- package/src/utils/prebalance.py +471 -0
- package/src/utils/project_journal.py +360 -0
- package/src/utils/reference_match.py +203 -0
- package/src/utils/rhythm_audit.py +252 -0
- package/src/utils/rule_of_six.py +217 -0
- package/src/utils/setup_sheet.py +121 -0
- package/src/utils/shot_assembly.py +259 -0
- package/src/utils/silence_ripple.py +89 -2
- package/src/utils/sound_density.py +253 -0
- package/src/utils/split_edits.py +180 -0
- package/src/utils/take_ranking.py +206 -0
- package/src/utils/transcript_edit.py +101 -5
- package/src/utils/turnover.py +187 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Handle accounting for cut plans — the media beyond an edit point.
|
|
2
|
+
|
|
3
|
+
A "handle" is source media that exists either side of what a timeline actually
|
|
4
|
+
uses. Nobody sees it in the cut, and that is exactly why it gets forgotten and
|
|
5
|
+
why forgetting it is expensive.
|
|
6
|
+
|
|
7
|
+
Every downstream department needs it:
|
|
8
|
+
|
|
9
|
+
- **Color** needs room to slip a shot a few frames, and dissolves consume real
|
|
10
|
+
media on both sides of the join.
|
|
11
|
+
- **VFX** needs overlap to track into and out of — 8-24 frames is the usual ask.
|
|
12
|
+
- **Sound** needs far more, 48-120 frames, because crossfades, room-tone fills
|
|
13
|
+
and dialogue overlaps all reach past the picture cut.
|
|
14
|
+
|
|
15
|
+
An automatic tighten does not know any of this. It optimizes for removing
|
|
16
|
+
silence, and a keep-range that begins on the first frame of a clip is, to that
|
|
17
|
+
objective, a perfect result. It is also a turnover that cannot be dissolved,
|
|
18
|
+
slipped, or crossfaded — and the discovery happens weeks later, in someone
|
|
19
|
+
else's suite, after picture lock, when fixing it is at its most expensive.
|
|
20
|
+
|
|
21
|
+
So this reports rather than blocks. Cutting to the very head or tail of a clip
|
|
22
|
+
is often legitimate and unavoidable — the material simply ends there. What is
|
|
23
|
+
not legitimate is finding out about it downstream. The contract is: no plan
|
|
24
|
+
silently ships zero handles.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
|
30
|
+
|
|
31
|
+
#: Picture handles. The low end of what VFX and color conventionally ask for;
|
|
32
|
+
#: below this a dissolve at the join has nothing to dissolve with.
|
|
33
|
+
DEFAULT_PICTURE_HANDLE_FRAMES = 8
|
|
34
|
+
#: Audio handles are much larger because crossfades, room tone and dialogue
|
|
35
|
+
#: overlap all reach well past the picture cut. 48 frames is 2s at 24fps.
|
|
36
|
+
DEFAULT_AUDIO_HANDLE_FRAMES = 48
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def handle_floor_for(track_type: Optional[str]) -> int:
|
|
40
|
+
"""Frames of source media a keep range should leave spare on each side."""
|
|
41
|
+
if str(track_type or "").strip().lower() == "audio":
|
|
42
|
+
return DEFAULT_AUDIO_HANDLE_FRAMES
|
|
43
|
+
return DEFAULT_PICTURE_HANDLE_FRAMES
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_keep_range_handles(
|
|
47
|
+
keep_ranges: Iterable[Mapping[str, Any]],
|
|
48
|
+
*,
|
|
49
|
+
source_bounds: Mapping[Any, int],
|
|
50
|
+
picture_handle_frames: int = DEFAULT_PICTURE_HANDLE_FRAMES,
|
|
51
|
+
audio_handle_frames: int = DEFAULT_AUDIO_HANDLE_FRAMES,
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
"""Report keep ranges that leave too little source media either side.
|
|
54
|
+
|
|
55
|
+
`source_bounds` maps a clip id to that clip's total length in source frames.
|
|
56
|
+
A clip missing from it is reported as `unknown` rather than assumed fine —
|
|
57
|
+
an unmeasured handle is not a passing handle, and quietly treating it as one
|
|
58
|
+
is how a turnover ships broken.
|
|
59
|
+
"""
|
|
60
|
+
violations: List[Dict[str, Any]] = []
|
|
61
|
+
unknown: List[Dict[str, Any]] = []
|
|
62
|
+
checked = 0
|
|
63
|
+
|
|
64
|
+
for rng in keep_ranges:
|
|
65
|
+
clip_id = rng.get("clip_id")
|
|
66
|
+
track_type = rng.get("track_type")
|
|
67
|
+
floor = audio_handle_frames if str(track_type or "").lower() == "audio" else picture_handle_frames
|
|
68
|
+
try:
|
|
69
|
+
start = int(rng.get("start_frame"))
|
|
70
|
+
end = int(rng.get("end_frame"))
|
|
71
|
+
except (TypeError, ValueError):
|
|
72
|
+
unknown.append({"clip_id": clip_id, "reason": "keep range has no usable frame bounds"})
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
total = source_bounds.get(clip_id)
|
|
76
|
+
if total is None:
|
|
77
|
+
unknown.append({
|
|
78
|
+
"clip_id": clip_id,
|
|
79
|
+
"track_type": track_type,
|
|
80
|
+
"reason": "source length unknown — handles could NOT be verified",
|
|
81
|
+
})
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
checked += 1
|
|
85
|
+
head = start
|
|
86
|
+
tail = int(total) - end
|
|
87
|
+
if head >= floor and tail >= floor:
|
|
88
|
+
continue
|
|
89
|
+
violations.append({
|
|
90
|
+
"clip_id": clip_id,
|
|
91
|
+
"track_type": track_type,
|
|
92
|
+
"start_frame": start,
|
|
93
|
+
"end_frame": end,
|
|
94
|
+
"head_frames": head,
|
|
95
|
+
"tail_frames": tail,
|
|
96
|
+
"required_frames": floor,
|
|
97
|
+
"short_at": (
|
|
98
|
+
"both" if head < floor and tail < floor
|
|
99
|
+
else "head" if head < floor else "tail"
|
|
100
|
+
),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
clean = not violations and not unknown
|
|
104
|
+
return {
|
|
105
|
+
"clean": clean,
|
|
106
|
+
"checked_range_count": checked,
|
|
107
|
+
"picture_handle_frames": picture_handle_frames,
|
|
108
|
+
"audio_handle_frames": audio_handle_frames,
|
|
109
|
+
"violation_count": len(violations),
|
|
110
|
+
"violations": violations,
|
|
111
|
+
"unverified": unknown,
|
|
112
|
+
"note": _summarize(violations, unknown, checked),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_seconds_ranges(
|
|
117
|
+
keep_ranges: Iterable[Mapping[str, Any]],
|
|
118
|
+
*,
|
|
119
|
+
source_duration_seconds: Optional[float],
|
|
120
|
+
fps: float = 24.0,
|
|
121
|
+
picture_handle_frames: int = DEFAULT_PICTURE_HANDLE_FRAMES,
|
|
122
|
+
) -> Dict[str, Any]:
|
|
123
|
+
"""Handle check for seconds-based keep ranges (the word-level cut path).
|
|
124
|
+
|
|
125
|
+
The transcript planner works in seconds against a single clip rather than in
|
|
126
|
+
frames across many, so it needs its own entry point — but the same rule. A
|
|
127
|
+
word-level tighten that begins on the first syllable of a clip leaves a join
|
|
128
|
+
with nothing behind it, exactly like a silence-driven one does, and the fact
|
|
129
|
+
that it was decided from a transcript rather than a waveform does not give
|
|
130
|
+
sound anything to crossfade with.
|
|
131
|
+
"""
|
|
132
|
+
floor_seconds = picture_handle_frames / fps if fps else 0.0
|
|
133
|
+
if source_duration_seconds is None:
|
|
134
|
+
return {
|
|
135
|
+
"clean": False,
|
|
136
|
+
"checked_range_count": 0,
|
|
137
|
+
"violations": [],
|
|
138
|
+
"unverified": [{"reason": "source duration unknown — handles could NOT be verified"}],
|
|
139
|
+
"required_seconds": round(floor_seconds, 3),
|
|
140
|
+
"note": "Source length unknown, so handles were not checked. Unverified is not clean.",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
violations: List[Dict[str, Any]] = []
|
|
144
|
+
checked = 0
|
|
145
|
+
for rng in keep_ranges:
|
|
146
|
+
start = rng.get("start", rng.get("start_seconds"))
|
|
147
|
+
end = rng.get("end", rng.get("end_seconds"))
|
|
148
|
+
if start is None or end is None:
|
|
149
|
+
continue
|
|
150
|
+
checked += 1
|
|
151
|
+
head = float(start)
|
|
152
|
+
tail = float(source_duration_seconds) - float(end)
|
|
153
|
+
if head >= floor_seconds and tail >= floor_seconds:
|
|
154
|
+
continue
|
|
155
|
+
violations.append({
|
|
156
|
+
"start_seconds": round(float(start), 3),
|
|
157
|
+
"end_seconds": round(float(end), 3),
|
|
158
|
+
"head_seconds": round(head, 3),
|
|
159
|
+
"tail_seconds": round(tail, 3),
|
|
160
|
+
"required_seconds": round(floor_seconds, 3),
|
|
161
|
+
"short_at": (
|
|
162
|
+
"both" if head < floor_seconds and tail < floor_seconds
|
|
163
|
+
else "head" if head < floor_seconds else "tail"
|
|
164
|
+
),
|
|
165
|
+
})
|
|
166
|
+
return {
|
|
167
|
+
"clean": not violations,
|
|
168
|
+
"checked_range_count": checked,
|
|
169
|
+
"violation_count": len(violations),
|
|
170
|
+
"violations": violations,
|
|
171
|
+
"unverified": [],
|
|
172
|
+
"required_seconds": round(floor_seconds, 3),
|
|
173
|
+
"note": (
|
|
174
|
+
f"All {checked} kept ranges leave at least {floor_seconds:.2f}s either side."
|
|
175
|
+
if not violations else
|
|
176
|
+
f"{len(violations)} of {checked} kept ranges sit within {floor_seconds:.2f}s of "
|
|
177
|
+
"the clip edge. Those joins cannot be dissolved or crossfaded. Often "
|
|
178
|
+
"unavoidable at the head or tail of a clip — but it must be known before "
|
|
179
|
+
"turnover, not discovered in the mix."
|
|
180
|
+
),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _summarize(violations: List[Dict[str, Any]], unknown: List[Dict[str, Any]], checked: int) -> str:
|
|
185
|
+
if not violations and not unknown:
|
|
186
|
+
return (
|
|
187
|
+
f"All {checked} keep ranges leave usable handles. Dissolves, slips and "
|
|
188
|
+
"audio crossfades have media to work with at every join."
|
|
189
|
+
)
|
|
190
|
+
parts: List[str] = []
|
|
191
|
+
if violations:
|
|
192
|
+
heads = sum(1 for v in violations if v["short_at"] in ("head", "both"))
|
|
193
|
+
tails = sum(1 for v in violations if v["short_at"] in ("tail", "both"))
|
|
194
|
+
parts.append(
|
|
195
|
+
f"{len(violations)} of {checked} keep ranges are short of handles "
|
|
196
|
+
f"({heads} at the head, {tails} at the tail). These joins cannot be "
|
|
197
|
+
"dissolved or slipped downstream, and audio has no room to crossfade. "
|
|
198
|
+
"Often unavoidable when a shot runs to the end of its media — but it "
|
|
199
|
+
"must be known before turnover, not discovered in the mix."
|
|
200
|
+
)
|
|
201
|
+
if unknown:
|
|
202
|
+
parts.append(
|
|
203
|
+
f"{len(unknown)} keep ranges could NOT be checked (source length "
|
|
204
|
+
"unknown). Unverified is not the same as clean."
|
|
205
|
+
)
|
|
206
|
+
return " ".join(parts)
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Human-readable reports for edit plans.
|
|
2
|
+
|
|
3
|
+
Every craft role that touches a cut keeps written artifacts as a primary
|
|
4
|
+
deliverable — the assistant editor's ingest log and known-issues list, the
|
|
5
|
+
assistant colorist's session-prep summary, the online editor's technical handoff
|
|
6
|
+
document, the post supervisor's status report. None of them consider the work
|
|
7
|
+
finished when the change is made; it is finished when someone else can see what
|
|
8
|
+
was decided.
|
|
9
|
+
|
|
10
|
+
This tool changes timelines and, until now, said very little about it. A JSON
|
|
11
|
+
plan is not a report: `keep_ranges: [...]` with 340 entries tells an editor
|
|
12
|
+
nothing, and the rational response to a machine you cannot audit is to re-check
|
|
13
|
+
everything by hand — which is exactly the cost the tool was supposed to remove.
|
|
14
|
+
One reviewer of the first public tutorial put it plainly after watching an
|
|
15
|
+
automated pass: "I feel like at this stage I will be micromanaging Claude's
|
|
16
|
+
edits longer than actually doing everything myself."
|
|
17
|
+
|
|
18
|
+
So the contract is that a plan explains itself:
|
|
19
|
+
|
|
20
|
+
- **What would change**, in seconds and shots, not frame arrays.
|
|
21
|
+
- **Why**, per cut, in the vocabulary an editor already uses.
|
|
22
|
+
- **What was deliberately left alone**, which is invisible in the output and is
|
|
23
|
+
the half people distrust most.
|
|
24
|
+
- **What could not be checked** — never folded into "fine".
|
|
25
|
+
- **What needs a human**, separated from what does not.
|
|
26
|
+
|
|
27
|
+
Rendered as Markdown because it has to survive being pasted into a Slack
|
|
28
|
+
message, an email to a post supervisor, or a review doc, none of which render
|
|
29
|
+
JSON.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import Any, List, Mapping, Optional, Sequence
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _plural(count: int, singular: str, plural: Optional[str] = None) -> str:
|
|
38
|
+
""""1 item" / "2 items". A report that cannot count reads as careless."""
|
|
39
|
+
word = singular if count == 1 else (plural or singular + "s")
|
|
40
|
+
return f"{count} {word}"
|
|
41
|
+
|
|
42
|
+
#: Confidence is reported by several unrelated subsystems — gate calibration,
|
|
43
|
+
#: pause classification, false-start heuristics — each with its own vocabulary.
|
|
44
|
+
#: A reader does not care which produced it; they care whether to look.
|
|
45
|
+
CONFIDENCE_ORDER = {"high": 0, "computed": 0, "medium": 1, "low": 2, "unknown": 3}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def confidence_band(value: Optional[str]) -> str:
|
|
49
|
+
"""Normalize a subsystem's confidence word into one of high/medium/low."""
|
|
50
|
+
raw = str(value or "").strip().lower()
|
|
51
|
+
if raw in ("high", "computed", "certain"):
|
|
52
|
+
return "high"
|
|
53
|
+
if raw in ("medium", "moderate", "marginal"):
|
|
54
|
+
return "medium"
|
|
55
|
+
if raw in ("low", "heuristic", "uncertain"):
|
|
56
|
+
return "low"
|
|
57
|
+
return "unknown"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _fmt_seconds(value: Any) -> str:
|
|
61
|
+
try:
|
|
62
|
+
total = float(value)
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
return "?"
|
|
65
|
+
if total < 60:
|
|
66
|
+
return f"{total:.1f}s"
|
|
67
|
+
minutes, seconds = divmod(total, 60)
|
|
68
|
+
return f"{int(minutes)}m {seconds:04.1f}s"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _fmt_tc(frame: Any, fps: float) -> str:
|
|
72
|
+
"""Frames → HH:MM:SS:FF. Editors navigate by timecode, not frame counts."""
|
|
73
|
+
try:
|
|
74
|
+
f = int(frame)
|
|
75
|
+
except (TypeError, ValueError):
|
|
76
|
+
return "??:??:??:??"
|
|
77
|
+
rate = int(round(fps)) if fps and fps > 0 else 24
|
|
78
|
+
hours, rem = divmod(f, rate * 3600)
|
|
79
|
+
minutes, rem = divmod(rem, rate * 60)
|
|
80
|
+
seconds, frames = divmod(rem, rate)
|
|
81
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}:{frames:02d}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _section(title: str, lines: Sequence[str]) -> List[str]:
|
|
85
|
+
if not lines:
|
|
86
|
+
return []
|
|
87
|
+
return [f"### {title}", "", *lines, ""]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def render_plan_report(plan: Mapping[str, Any], *, max_detail_rows: int = 25) -> str:
|
|
91
|
+
"""Render any edit-engine plan as a Markdown report a human can act on.
|
|
92
|
+
|
|
93
|
+
Unknown plan kinds still produce a useful report rather than an error — a
|
|
94
|
+
report that refuses to render is worse than a generic one, because the
|
|
95
|
+
fallback is reading raw JSON.
|
|
96
|
+
"""
|
|
97
|
+
kind = str(plan.get("kind") or "plan")
|
|
98
|
+
fps = float(plan.get("timeline_fps") or 24.0)
|
|
99
|
+
timeline = plan.get("timeline_name") or "(unnamed timeline)"
|
|
100
|
+
out: List[str] = [f"# {kind.replace('_', ' ').title()} — {timeline}", ""]
|
|
101
|
+
|
|
102
|
+
out.extend(_headline(plan, kind, fps))
|
|
103
|
+
out.extend(_changes_section(plan, fps, max_detail_rows))
|
|
104
|
+
out.extend(_left_alone_section(plan))
|
|
105
|
+
out.extend(_unverified_section(plan))
|
|
106
|
+
out.extend(_needs_a_human_section(plan))
|
|
107
|
+
|
|
108
|
+
note = plan.get("note")
|
|
109
|
+
if note:
|
|
110
|
+
out.extend(["---", "", str(note), ""])
|
|
111
|
+
return "\n".join(out).rstrip() + "\n"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _headline(plan: Mapping[str, Any], kind: str, fps: float) -> List[str]:
|
|
115
|
+
bits: List[str] = []
|
|
116
|
+
if plan.get("marker_count") is not None:
|
|
117
|
+
bits.append(
|
|
118
|
+
f"**{plan['marker_count']} markers proposed**, "
|
|
119
|
+
f"{_fmt_seconds(plan.get('total_dead_seconds'))} of dead space total. "
|
|
120
|
+
"Nothing is cut and no marker is written yet."
|
|
121
|
+
)
|
|
122
|
+
if plan.get("lift_count") is not None:
|
|
123
|
+
bits.append(
|
|
124
|
+
f"**{plan['lift_count']} cuts proposed**, "
|
|
125
|
+
f"{_fmt_seconds(plan.get('estimated_removed_seconds'))} removed."
|
|
126
|
+
)
|
|
127
|
+
if plan.get("tightness"):
|
|
128
|
+
bits.append(
|
|
129
|
+
f"Tightness **{plan['tightness']}**"
|
|
130
|
+
+ (" (the default — a first assembly is meant to run long)."
|
|
131
|
+
if plan["tightness"] == "generous" else ".")
|
|
132
|
+
)
|
|
133
|
+
return [*bits, ""] if bits else []
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _cut_rows(plan: Mapping[str, Any], fps: float, limit: int) -> List[str]:
|
|
137
|
+
rows: List[str] = []
|
|
138
|
+
entries: List[Mapping[str, Any]] = list(plan.get("lifts") or []) or list(plan.get("markers") or [])
|
|
139
|
+
if not entries:
|
|
140
|
+
return rows
|
|
141
|
+
rows.append("| At | Length | Confidence | Why |")
|
|
142
|
+
rows.append("|---|---|---|---|")
|
|
143
|
+
ordered = sorted(entries, key=lambda e: e.get("timeline_start_frame") or 0)
|
|
144
|
+
for entry in ordered[:limit]:
|
|
145
|
+
start = entry.get("timeline_start_frame")
|
|
146
|
+
if entry.get("duration_frames") is not None:
|
|
147
|
+
length = float(entry["duration_frames"]) / (fps or 24.0)
|
|
148
|
+
else:
|
|
149
|
+
length = (
|
|
150
|
+
(entry.get("timeline_end_frame") or 0) - (start or 0)
|
|
151
|
+
) / (fps or 24.0)
|
|
152
|
+
reason = (
|
|
153
|
+
entry.get("rationale")
|
|
154
|
+
or entry.get("note")
|
|
155
|
+
or entry.get("classification_reason")
|
|
156
|
+
or "—"
|
|
157
|
+
)
|
|
158
|
+
band = confidence_band(
|
|
159
|
+
entry.get("confidence")
|
|
160
|
+
or (entry.get("evidence") or {}).get("confidence")
|
|
161
|
+
or "high"
|
|
162
|
+
)
|
|
163
|
+
rows.append(
|
|
164
|
+
f"| `{_fmt_tc(start, fps)}` | {_fmt_seconds(length)} | {band} | "
|
|
165
|
+
f"{str(reason).replace('|', '/').strip()} |"
|
|
166
|
+
)
|
|
167
|
+
if len(ordered) > limit:
|
|
168
|
+
rows.append(f"| … | | | _{len(ordered) - limit} more not shown_ |")
|
|
169
|
+
return rows
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _changes_section(plan: Mapping[str, Any], fps: float, limit: int) -> List[str]:
|
|
173
|
+
return _section("What would change", _cut_rows(plan, fps, limit))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _left_alone_section(plan: Mapping[str, Any]) -> List[str]:
|
|
177
|
+
"""What was deliberately preserved.
|
|
178
|
+
|
|
179
|
+
This is the half people distrust, because it is invisible in the output: a
|
|
180
|
+
beat that was correctly kept looks exactly like a beat nobody considered.
|
|
181
|
+
"""
|
|
182
|
+
lines: List[str] = []
|
|
183
|
+
preserved = plan.get("preserved") or []
|
|
184
|
+
for item in preserved:
|
|
185
|
+
lines.append(f"- {item}")
|
|
186
|
+
if plan.get("tightness") == "generous":
|
|
187
|
+
lines.append(
|
|
188
|
+
"- Short gaps were left in by design at this tightness. Re-run with "
|
|
189
|
+
"`tightness='balanced'` or `'tight'` to remove more."
|
|
190
|
+
)
|
|
191
|
+
if plan.get("breathing_room_preserved"):
|
|
192
|
+
lines.append(
|
|
193
|
+
f"- {plan['breathing_room_preserved']} pauses after a sentence were kept "
|
|
194
|
+
"as breathing room — the beat that lets a line land."
|
|
195
|
+
)
|
|
196
|
+
return _section("Deliberately left alone", lines)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _unverified_section(plan: Mapping[str, Any]) -> List[str]:
|
|
200
|
+
"""Never fold "could not check" into "checked and fine"."""
|
|
201
|
+
lines: List[str] = []
|
|
202
|
+
for skip in plan.get("skipped") or []:
|
|
203
|
+
name = skip.get("item") or skip.get("clip_id") or "(unnamed)"
|
|
204
|
+
lines.append(f"- **{name}** — {skip.get('reason') or 'not analyzed'}")
|
|
205
|
+
handles = plan.get("handle_report") or {}
|
|
206
|
+
for unver in handles.get("unverified") or []:
|
|
207
|
+
lines.append(
|
|
208
|
+
f"- **{unver.get('clip_id')}** — {unver.get('reason')}"
|
|
209
|
+
)
|
|
210
|
+
if lines:
|
|
211
|
+
lines.append("")
|
|
212
|
+
lines.append(
|
|
213
|
+
"_These were **not** analyzed. That is not the same as clean._"
|
|
214
|
+
)
|
|
215
|
+
return _section("Not verified", lines)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _needs_a_human_section(plan: Mapping[str, Any]) -> List[str]:
|
|
219
|
+
lines: List[str] = []
|
|
220
|
+
handles = plan.get("handle_report") or {}
|
|
221
|
+
if handles.get("violation_count"):
|
|
222
|
+
lines.append(
|
|
223
|
+
f"- **Handles:** {handles['violation_count']} keep ranges are short of "
|
|
224
|
+
f"media at the join. {handles.get('note', '').strip()}"
|
|
225
|
+
)
|
|
226
|
+
entries = list(plan.get("lifts") or []) + list(plan.get("markers") or [])
|
|
227
|
+
low = [e for e in entries if confidence_band(e.get("confidence")) == "low"]
|
|
228
|
+
if low:
|
|
229
|
+
lines.append(
|
|
230
|
+
f"- **{_plural(len(low), 'low-confidence call')}** — review before accepting."
|
|
231
|
+
)
|
|
232
|
+
marginal = [
|
|
233
|
+
c for c in (plan.get("calibrations") or [])
|
|
234
|
+
if isinstance(c, dict) and c.get("usable") and not c.get("quiet_is_digital_silence")
|
|
235
|
+
and isinstance(c.get("separation_db"), (int, float)) and c["separation_db"] < 18
|
|
236
|
+
]
|
|
237
|
+
if marginal:
|
|
238
|
+
lines.append(
|
|
239
|
+
f"- **{_plural(len(marginal), 'item')} had speech and room close together**, "
|
|
240
|
+
"so the silence gate is usable but not confident there."
|
|
241
|
+
)
|
|
242
|
+
if plan.get("warning"):
|
|
243
|
+
lines.append(f"- **Warning:** {plan['warning']}")
|
|
244
|
+
return _section("Needs a human", lines)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
#: Audit payloads that are not edit plans — they carry criteria and/or graded
|
|
248
|
+
#: findings rather than cuts. Rendered by `render_audit_report`.
|
|
249
|
+
AUDIT_KINDS = ("rule_of_six_audit", "conform_lint", "sound_density_audit", "setup_sheet")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def render_audit_report(audit: Mapping[str, Any]) -> str:
|
|
253
|
+
"""Render an audit as Markdown — criteria, findings, and what was not checked.
|
|
254
|
+
|
|
255
|
+
Audits differ from plans in that they propose no change, so the sections
|
|
256
|
+
differ too: there is no "what would change", and the load-bearing part is
|
|
257
|
+
what the audit *could not* look at. That section is mandatory here for the
|
|
258
|
+
same reason it is everywhere else in this codebase — a reader who cannot see
|
|
259
|
+
the gaps will read a short report as a clean one.
|
|
260
|
+
"""
|
|
261
|
+
kind = str(audit.get("kind") or "audit")
|
|
262
|
+
title = kind.replace("_", " ").title()
|
|
263
|
+
where = audit.get("timeline_name") or audit.get("media_path") or ""
|
|
264
|
+
out: List[str] = [f"# {title}" + (f" — {where}" if where else ""), ""]
|
|
265
|
+
|
|
266
|
+
coverage = audit.get("coverage") or {}
|
|
267
|
+
if coverage.get("statement"):
|
|
268
|
+
out += [f"**{coverage['statement']}**", ""]
|
|
269
|
+
|
|
270
|
+
criteria = audit.get("criteria") or []
|
|
271
|
+
if criteria:
|
|
272
|
+
out += ["### Criteria", "", "| Weight | State | Criterion | |", "|---|---|---|---|"]
|
|
273
|
+
for row in criteria:
|
|
274
|
+
out.append(
|
|
275
|
+
f"| {float(row.get('weight', 0)) * 100:.0f}% | `{row.get('state')}` | "
|
|
276
|
+
f"{str(row.get('label') or row.get('criterion')).split(' — ')[0]} | "
|
|
277
|
+
f"{str(row.get('detail') or '').replace('|', '/')} |"
|
|
278
|
+
)
|
|
279
|
+
out.append("")
|
|
280
|
+
|
|
281
|
+
findings = audit.get("findings") or []
|
|
282
|
+
if findings:
|
|
283
|
+
out += ["### Findings", ""]
|
|
284
|
+
for finding in findings:
|
|
285
|
+
head = finding.get("summary") or finding.get("code") or "finding"
|
|
286
|
+
tag = finding.get("severity") or finding.get("criterion") or ""
|
|
287
|
+
out.append(f"- **{head}**" + (f" _({tag})_" if tag else ""))
|
|
288
|
+
if finding.get("detail"):
|
|
289
|
+
out.append(f" {finding['detail']}")
|
|
290
|
+
out.append("")
|
|
291
|
+
|
|
292
|
+
out += _audit_not_checked(audit)
|
|
293
|
+
|
|
294
|
+
for key, heading in (("threshold_provenance", "Threshold"),
|
|
295
|
+
("method", "Method"),
|
|
296
|
+
("grouping_basis", "Grouping"),
|
|
297
|
+
("source_caveat", "Source"),
|
|
298
|
+
("ordering", "Ordering")):
|
|
299
|
+
if audit.get(key):
|
|
300
|
+
out += [f"_{heading}: {audit[key]}_", ""]
|
|
301
|
+
|
|
302
|
+
if audit.get("note"):
|
|
303
|
+
out += ["---", "", str(audit["note"]), ""]
|
|
304
|
+
return "\n".join(out).rstrip() + "\n"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _audit_not_checked(audit: Mapping[str, Any]) -> List[str]:
|
|
308
|
+
"""Everything the audit did not or could not look at. Never optional."""
|
|
309
|
+
lines: List[str] = []
|
|
310
|
+
for entry in audit.get("not_checked") or []:
|
|
311
|
+
lines.append(f"- {entry}")
|
|
312
|
+
for entry in audit.get("unanalyzed") or audit.get("unusable") or []:
|
|
313
|
+
if isinstance(entry, Mapping):
|
|
314
|
+
lines.append(f"- **{entry.get('clip') or entry.get('item')}** — {entry.get('reason')}")
|
|
315
|
+
else:
|
|
316
|
+
lines.append(f"- {entry}")
|
|
317
|
+
not_implemented = [
|
|
318
|
+
r for r in (audit.get("criteria") or []) if r.get("state") == "NOT_IMPLEMENTED"
|
|
319
|
+
]
|
|
320
|
+
if not_implemented:
|
|
321
|
+
lines.append(
|
|
322
|
+
"- Criteria not computed by this build: "
|
|
323
|
+
+ ", ".join(str(r["criterion"]) for r in not_implemented)
|
|
324
|
+
)
|
|
325
|
+
if not lines:
|
|
326
|
+
return []
|
|
327
|
+
lines.append("")
|
|
328
|
+
lines.append("_Not looked at is not the same as clean._")
|
|
329
|
+
return _section("Not checked", lines)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def render_any(payload: Mapping[str, Any], *, max_detail_rows: int = 25) -> str:
|
|
333
|
+
"""Render a plan or an audit, whichever this is."""
|
|
334
|
+
if str(payload.get("kind") or "") in AUDIT_KINDS or payload.get("criteria"):
|
|
335
|
+
return render_audit_report(payload)
|
|
336
|
+
return render_plan_report(payload, max_detail_rows=max_detail_rows)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def summarize_for_chat(plan: Mapping[str, Any]) -> str:
|
|
340
|
+
"""One-line summary for a chat reply, where a full report is too much."""
|
|
341
|
+
kind = str(plan.get("kind") or "plan").replace("_", " ")
|
|
342
|
+
if plan.get("marker_count") is not None:
|
|
343
|
+
head = (
|
|
344
|
+
f"{plan['marker_count']} dead-space markers proposed "
|
|
345
|
+
f"({_fmt_seconds(plan.get('total_dead_seconds'))} total)"
|
|
346
|
+
)
|
|
347
|
+
elif plan.get("lift_count") is not None:
|
|
348
|
+
head = (
|
|
349
|
+
f"{plan['lift_count']} cuts proposed "
|
|
350
|
+
f"({_fmt_seconds(plan.get('estimated_removed_seconds'))} removed)"
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
head = kind
|
|
354
|
+
caveats: List[str] = []
|
|
355
|
+
if plan.get("skipped"):
|
|
356
|
+
caveats.append(f"{_plural(len(plan['skipped']), 'item')} not analyzed")
|
|
357
|
+
handles = plan.get("handle_report") or {}
|
|
358
|
+
if handles.get("violation_count"):
|
|
359
|
+
caveats.append(f"{handles['violation_count']} short of handles")
|
|
360
|
+
return head + (f" — {', '.join(caveats)}" if caveats else "")
|