Adding fixes to the grading

This commit is contained in:
2026-07-23 15:55:22 +02:00
parent b1136a9f7d
commit de329a8679
2 changed files with 102 additions and 45 deletions
+50 -28
View File
@@ -81,7 +81,7 @@ Examples:
gnommo -p video1 grade --set screen_gain=200 Preview with a gnommokey override gnommo -p video1 grade --set screen_gain=200 Preview with a gnommokey override
gnommo -p video1 grade --stage key Auto-tune the matte key → candidate + manifest (then --pick key_1) gnommo -p video1 grade --stage key Auto-tune the matte key → candidate + manifest (then --pick key_1)
gnommo -p video1 grade --stage despill Sweep spill_suppress 0.71.5 → stills to pick from gnommo -p video1 grade --stage despill Sweep spill_suppress 0.71.5 → stills to pick from
gnommo -p video1 grade --stage grade Sweep paleness → color-grade stills to pick from gnommo -p video1 grade --stage grade Sweep centered grade (5=camera vibrance, <5 paler, >5 more saturated)
gnommo -p video1 grade --pick despill_5 Apply a chosen candidate to project.json gnommo -p video1 grade --pick despill_5 Apply a chosen candidate to project.json
gnommo -p video1 description Generate YouTube description file gnommo -p video1 description Generate YouTube description file
gnommo -p video1 archive Copy project to connected external drive gnommo -p video1 archive Copy project to connected external drive
@@ -1465,9 +1465,16 @@ def _import_narration_segments(narration_dir: Path, config, verbose: bool) -> No
# so the project can be rendered from raw before the preprocess stage runs. # so the project can be rendered from raw before the preprocess stage runs.
migrated_count = 0 migrated_count = 0
for seg_id, entry in existing_narration.items(): for seg_id, entry in existing_narration.items():
changed = False
# Fold the legacy "output_file" key into "processed_file".
if "output_file" in entry:
entry.setdefault("processed_file", entry.pop("output_file"))
changed = True
# Older runs stored the processed output in source_file itself.
src = entry.get("source_file", "") src = entry.get("source_file", "")
if not (src.startswith("processed/") or "_processed." in src): if src.startswith("processed/") or "_processed." in src:
continue
# Preserve the processed path under processed_file (don't clobber an # Preserve the processed path under processed_file (don't clobber an
# explicit one the user already set). # explicit one the user already set).
entry.setdefault("processed_file", src) entry.setdefault("processed_file", src)
@@ -1484,9 +1491,12 @@ def _import_narration_segments(narration_dir: Path, config, verbose: bool) -> No
) )
if raw_match: if raw_match:
entry["source_file"] = f"raw_mov/{raw_match.name}" entry["source_file"] = f"raw_mov/{raw_match.name}"
changed = True
# else: no raw available — leave source_file as the processed file so
# the segment still renders; it just can't be re-preprocessed from raw.
if changed:
migrated_count += 1 migrated_count += 1
# else: no raw available — leave source_file as the processed file so the
# segment still renders; it simply can't be re-preprocessed from raw.
default_filters = config.default_filters if config else {} default_filters = config.default_filters if config else {}
added_count = 0 added_count = 0
@@ -4992,27 +5002,35 @@ def _stage_despill(project_path, filters, ref, out_dir, source_name, ss) -> int:
def _stage_grade(project_path, filters, ref, out_dir, source_name, ss) -> int: def _stage_grade(project_path, filters, ref, out_dir, source_name, ss) -> int:
"""Deterministic look × yellow-tint grid → punchy color_grade candidates. """Deterministic centered-grade × yellow-tint grid → color_grade candidates.
Two axes: 'look' ramps the auto-levels/vividness punch (flat → strong), and Two axes. 'grade' is a centered 19 dial: 5 keeps the camera's native
'yellow_tint' hue-selectively pulls the yellow costume back from the orange vibrance as-shot (no saturation/contrast/punch change), below 5 is paler
that punch introduces (negative = greener/preserve, 0 = as-is). Skin (reds) than default (desaturated, flatter), above 5 is more saturated with a
is untouched by the tint, so you can crank vibrance and keep the suit yellow.""" growing auto-levels punch. 'yellow_tint' hue-selectively pulls the yellow
costume back from the orange that punch introduces (negative = greener/
preserve, 0 = as-is). Skin (reds) is untouched by the tint, so you can crank
vibrance and keep the suit yellow."""
key = _grade_step(filters, "gnommokey") key = _grade_step(filters, "gnommokey")
looks = [0.4, 0.7, 1.0] grades = [1, 3, 5, 7, 9] # centered on 5 = camera vibrance
tints = [0.0, -0.4, -0.8] # 0 = as-is, negative = pull yellows back from orange tints = [0.0, -0.4, -0.8] # 0 = as-is, negative = pull yellows back from orange
print(" Grid: look {0.4, 0.7, 1.0} × yellow_tint {0.0, -0.4, -0.8}") print(" Grid: grade {1, 3, 5, 7, 9} (5 = camera vibrance) × yellow_tint {0.0, -0.4, -0.8}")
candidates = [] candidates = []
recommended = None
i = 0 i = 0
for p in looks: for g in grades:
# d ∈ [-1, +1] around the centered midpoint (grade 5 → d = 0).
d = (g - 5) / 4.0
for yt in tints: for yt in tints:
i += 1 i += 1
cg = { cg = {
"type": "color_grade", "type": "color_grade",
"auto_levels": round(p, 3), # Punch only ramps ABOVE center; at/below 5 there is none.
"saturation": round(1.0 + 0.5 * p, 3), "auto_levels": round(0.4 * max(0.0, d), 3),
"contrast": round(1.0 + 0.15 * p, 3), # Symmetric around 1.0: <5 paler, 5 as-shot, >5 more saturated.
"brightness": round(0.02 * p, 3), "saturation": round(1.0 + 0.25 * d, 3),
"contrast": round(1.0 + 0.08 * d, 3),
"brightness": round(0.02 * max(0.0, d), 3),
"yellow_tint": yt, "yellow_tint": yt,
} }
chain = _mask_and(filters, key, cg) chain = _mask_and(filters, key, cg)
@@ -5020,28 +5038,32 @@ def _stage_grade(project_path, filters, ref, out_dir, source_name, ss) -> int:
png = out_dir / f"grade_{i}.png" png = out_dir / f"grade_{i}.png"
_render_preview(ref, chain, png) _render_preview(ref, chain, png)
params = {k: cg[k] for k in ("auto_levels", "saturation", "contrast", "brightness", "yellow_tint")} params = {k: cg[k] for k in ("auto_levels", "saturation", "contrast", "brightness", "yellow_tint")}
params["look"] = p params["grade"] = g
candidates.append({ candidates.append({
"id": f"grade_{i}", "file": png.name, "params": params, "id": f"grade_{i}", "file": png.name, "params": params,
"hint": {"suit_rgb": list(suit), "orange": suit[0] - suit[1]}, "hint": {"suit_rgb": list(suit), "orange": suit[0] - suit[1]},
}) })
# Default suggestion: the centered, true-to-camera look (grade 5, no tint).
if g == 5 and yt == 0.0:
recommended = f"grade_{i}"
manifest = { manifest = {
"stage": "grade", "target_step": "color_grade", "stage": "grade", "target_step": "color_grade",
"source": source_name, "ss": round(ss, 2), "swept": ["look", "yellow_tint"], "source": source_name, "ss": round(ss, 2), "swept": ["grade", "yellow_tint"],
"recommended": "grade_8", "candidates": candidates, # look 1.0, tint -0.4 "recommended": recommended, "candidates": candidates, # grade 5 = camera vibrance
} }
mpath = _grade_write_manifest(out_dir, manifest) mpath = _grade_write_manifest(out_dir, manifest)
rec = manifest["recommended"] rec = manifest["recommended"]
print(f" {'id':>10} {'look':>5} {'tint':>5} {'suit RGB':>17} {'orange':>6} png") print(f" {'id':>10} {'grade':>5} {'tint':>5} {'suit RGB':>17} {'orange':>6} png")
print(f" {'-'*10} {'-'*5} {'-'*5} {'-'*17} {'-'*6} {'-'*3}") print(f" {'-'*10} {'-'*5} {'-'*5} {'-'*17} {'-'*6} {'-'*3}")
for c in candidates: for c in candidates:
star = " ◀ suggested" if c["id"] == rec else "" star = " ◀ suggested (camera vibrance)" if c["id"] == rec else ""
print(f" {c['id']:>10} {c['params']['look']:>5} {c['params']['yellow_tint']:>5} " print(f" {c['id']:>10} {c['params']['grade']:>5} {c['params']['yellow_tint']:>5} "
f"{str(tuple(c['hint']['suit_rgb'])):>17} {c['hint']['orange']:>6} {c['file']}{star}") f"{str(tuple(c['hint']['suit_rgb'])):>17} {c['hint']['orange']:>6} {c['file']}{star}")
print("\n orange = suit RG: lower is more yellow, higher is more orange") print("\n grade: 5 = camera vibrance, <5 paler than default, >5 more saturated")
print(" orange = suit RG: lower is more yellow, higher is more orange")
print(f" Manifest: {mpath}") print(f" Manifest: {mpath}")
print(" Pick punch (look) + a tint that keeps the suit yellow:") print(" Pick a grade (paler <5 / punchier >5) + a tint that keeps the suit yellow:")
print(f" gnommo -p {project_path.name} grade --pick grade_8") print(f" gnommo -p {project_path.name} grade --pick {rec}")
return 0 return 0
@@ -5068,9 +5090,9 @@ def _grade_pick(project_path, stage_hint, pick) -> int:
if cand is None: if cand is None:
print(f" ERROR: candidate '{cand_id}' not in {mpath.name}.") print(f" ERROR: candidate '{cand_id}' not in {mpath.name}.")
return 1 return 1
# 'look'/'paleness'/'aggressiveness' are UI-only dials, not real fields. # 'grade'/'look'/'paleness'/'aggressiveness' are UI-only dials, not real fields.
params = {k: v for k, v in cand["params"].items() params = {k: v for k, v in cand["params"].items()
if k not in ("look", "paleness", "aggressiveness")} if k not in ("grade", "look", "paleness", "aggressiveness")}
_apply_candidate_to_project(project_path, manifest["target_step"], params) _apply_candidate_to_project(project_path, manifest["target_step"], params)
print(f" Applied {cand_id} → project.json ({manifest['target_step']}): " print(f" Applied {cand_id} → project.json ({manifest['target_step']}): "
+ ", ".join(f"{k}={v}" for k, v in params.items())) + ", ".join(f"{k}={v}" for k, v in params.items()))
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
./gnommo.sh -p video0 grade --stage key
./gnommo.sh -p video1 grade --stage key
./gnommo.sh -p video2 grade --stage key
./gnommo.sh -p video3 grade --stage key
./gnommo.sh -p video4 grade --stage key
./gnommo.sh -p video5 grade --stage key
./gnommo.sh -p video6 grade --stage key
./gnommo.sh -p video0 grade --pick key_5
./gnommo.sh -p video1 grade --pick key_5
./gnommo.sh -p video2 grade --pick key_5
./gnommo.sh -p video3 grade --pick key_5
./gnommo.sh -p video4 grade --pick key_5
./gnommo.sh -p video5 grade --pick key_5
./gnommo.sh -p video6 grade --pick key_5
./gnommo.sh -p video0 grade --stage grade
./gnommo.sh -p video1 grade --stage grade
./gnommo.sh -p video2 grade --stage grade
./gnommo.sh -p video3 grade --stage grade
./gnommo.sh -p video4 grade --stage grade
./gnommo.sh -p video5 grade --stage grade
./gnommo.sh -p video6 grade --stage grade
./gnommo.sh -p video0 grade --pick grade_5
./gnommo.sh -p video1 grade --pick grade_5
./gnommo.sh -p video2 grade --pick grade_5
./gnommo.sh -p video3 grade --pick grade_5
./gnommo.sh -p video4 grade --pick grade_5
./gnommo.sh -p video5 grade --pick grade_5
./gnommo.sh -p video6 grade --pick grade_5