From de329a86791445cc66aa69ba2d15033b83094cd4 Mon Sep 17 00:00:00 2001 From: jenstandstad Date: Thu, 23 Jul 2026 15:55:22 +0200 Subject: [PATCH] Adding fixes to the grading --- gnommo/cli.py | 112 ++++++++++++++++++++++++++++++-------------------- grade_all.sh | 35 ++++++++++++++++ 2 files changed, 102 insertions(+), 45 deletions(-) create mode 100755 grade_all.sh diff --git a/gnommo/cli.py b/gnommo/cli.py index 2a32b69..bdebd22 100644 --- a/gnommo/cli.py +++ b/gnommo/cli.py @@ -81,7 +81,7 @@ Examples: 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 despill Sweep spill_suppress 0.7–1.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 description Generate YouTube description file gnommo -p video1 archive Copy project to connected external drive @@ -1465,28 +1465,38 @@ def _import_narration_segments(narration_dir: Path, config, verbose: bool) -> No # so the project can be rendered from raw before the preprocess stage runs. migrated_count = 0 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", "") - if not (src.startswith("processed/") or "_processed." in src): - continue - # Preserve the processed path under processed_file (don't clobber an - # explicit one the user already set). - entry.setdefault("processed_file", src) - # Repoint source_file at the raw recording when we can find it. - raw_match = next( - ( - f - for f in (raw_dir.iterdir() if raw_dir.exists() else []) - if f.is_file() - and f.suffix.lower() in _raw_video_exts_set - and f.stem.lower() == seg_id.lower() - ), - None, - ) - if raw_match: - entry["source_file"] = f"raw_mov/{raw_match.name}" + if src.startswith("processed/") or "_processed." in src: + # Preserve the processed path under processed_file (don't clobber an + # explicit one the user already set). + entry.setdefault("processed_file", src) + # Repoint source_file at the raw recording when we can find it. + raw_match = next( + ( + f + for f in (raw_dir.iterdir() if raw_dir.exists() else []) + if f.is_file() + and f.suffix.lower() in _raw_video_exts_set + and f.stem.lower() == seg_id.lower() + ), + None, + ) + if raw_match: + 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 - # 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 {} 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: - """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 - '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.""" + Two axes. 'grade' is a centered 1–9 dial: 5 keeps the camera's native + vibrance as-shot (no saturation/contrast/punch change), below 5 is paler + than default (desaturated, flatter), above 5 is more saturated with a + 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") - looks = [0.4, 0.7, 1.0] - 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}") + 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 + print(" Grid: grade {1, 3, 5, 7, 9} (5 = camera vibrance) × yellow_tint {0.0, -0.4, -0.8}") candidates = [] + recommended = None 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: i += 1 cg = { "type": "color_grade", - "auto_levels": round(p, 3), - "saturation": round(1.0 + 0.5 * p, 3), - "contrast": round(1.0 + 0.15 * p, 3), - "brightness": round(0.02 * p, 3), + # Punch only ramps ABOVE center; at/below 5 there is none. + "auto_levels": round(0.4 * max(0.0, d), 3), + # Symmetric around 1.0: <5 paler, 5 as-shot, >5 more saturated. + "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, } 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" _render_preview(ref, chain, png) params = {k: cg[k] for k in ("auto_levels", "saturation", "contrast", "brightness", "yellow_tint")} - params["look"] = p + params["grade"] = g candidates.append({ "id": f"grade_{i}", "file": png.name, "params": params, "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 = { "stage": "grade", "target_step": "color_grade", - "source": source_name, "ss": round(ss, 2), "swept": ["look", "yellow_tint"], - "recommended": "grade_8", "candidates": candidates, # look 1.0, tint -0.4 + "source": source_name, "ss": round(ss, 2), "swept": ["grade", "yellow_tint"], + "recommended": recommended, "candidates": candidates, # grade 5 = camera vibrance } mpath = _grade_write_manifest(out_dir, manifest) 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}") for c in candidates: - star = " ◀ suggested" if c["id"] == rec else "" - print(f" {c['id']:>10} {c['params']['look']:>5} {c['params']['yellow_tint']:>5} " + star = " ◀ suggested (camera vibrance)" if c["id"] == rec else "" + 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}") - print("\n orange = suit R−G: lower is more yellow, higher is more orange") + print("\n grade: 5 = camera vibrance, <5 paler than default, >5 more saturated") + print(" orange = suit R−G: lower is more yellow, higher is more orange") print(f" Manifest: {mpath}") - print(" Pick punch (look) + a tint that keeps the suit yellow:") - print(f" gnommo -p {project_path.name} grade --pick grade_8") + print(" Pick a grade (paler <5 / punchier >5) + a tint that keeps the suit yellow:") + print(f" gnommo -p {project_path.name} grade --pick {rec}") return 0 @@ -5068,9 +5090,9 @@ def _grade_pick(project_path, stage_hint, pick) -> int: if cand is None: print(f" ERROR: candidate '{cand_id}' not in {mpath.name}.") 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() - 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) print(f" Applied {cand_id} → project.json ({manifest['target_step']}): " + ", ".join(f"{k}={v}" for k, v in params.items())) diff --git a/grade_all.sh b/grade_all.sh new file mode 100755 index 0000000..fdd55dd --- /dev/null +++ b/grade_all.sh @@ -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