Mark trailer title cards as graphics
This commit is contained in:
@@ -167,7 +167,7 @@ wenn sich das zugrundeliegende Match geändert hat.
|
||||
| Source-Clip zeigt richtige Szene, aber falsche Bewegungsphase | `python cli.py rematch --beat N --refine` — schiebt den Inpoint frame-genau aus dem Bildinhalt. |
|
||||
| Score zu niedrig, andere Szene wäre richtig | `python cli.py match --beat N --vision` — vollständiger Re-Match nur für diesen Beat mit Vision-Phasenprüfung. |
|
||||
| Match offensichtlich falsche Szene | `python cli.py rematch --beat N --threshold 0.50` — Schwelle absenken, neuer globaler Scan nur für diesen Beat. |
|
||||
| Beat ist Schwarzbild / Logo / Titel und sollte gar nicht matchen | nichts tun, der Status `MAN.` im `CUTTER_REPORT.md` ist korrekt. |
|
||||
| Beat ist Schwarzbild / Logo / Titel und sollte gar nicht matchen | nichts tun, der Status `GFX` im `CUTTER_REPORT.md` ist korrekt. |
|
||||
|
||||
### Algorithmische Details
|
||||
|
||||
|
||||
@@ -245,6 +245,13 @@ nur wenn die relative Source-Grenze zeitlich zu einem erkannten Trailer-
|
||||
Umschnitt passt. So kann ein Beat aus Frage/Antwort-Shots vollständig erfasst
|
||||
werden, ohne Szenen willkürlich zusammenzukleben.
|
||||
|
||||
## Titel- und Grafikbeats
|
||||
|
||||
Dunkle Trailerkarten mit deutlich isoliertem Text werden im Cutter-Report als
|
||||
`GFX` markiert, wenn es keinen Source-Treffer gibt. Diese Beats sind keine
|
||||
fehlgeschlagenen Matches: Der Cutter soll die Trailer-Grafik beziehungsweise
|
||||
eine NLE-Titelkarte übernehmen und nicht im Spielfilm nach einem Bild suchen.
|
||||
|
||||
## Reranking-Pipeline
|
||||
|
||||
Vor dem teuren Frame-Refine wird der gesamte Kandidatenpool mit einer
|
||||
|
||||
@@ -30,6 +30,8 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageStat
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Frame-rate / timecode helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -388,15 +390,20 @@ class BeatRow:
|
||||
trailer_clip: Path | None = None
|
||||
source_clip: Path | None = None
|
||||
compare_clip: Path | None = None
|
||||
is_graphic: bool = False
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
if self.is_graphic:
|
||||
return "GFX"
|
||||
if not self.matched:
|
||||
return "MAN."
|
||||
return "OK" if self.confirmed else "?"
|
||||
|
||||
@property
|
||||
def status_de(self) -> str:
|
||||
if self.is_graphic:
|
||||
return "Titel/Grafik"
|
||||
if not self.matched:
|
||||
return "Kein Treffer"
|
||||
return "Bestätigt" if self.confirmed else "Vorläufig"
|
||||
@@ -426,6 +433,26 @@ def collect_rows(
|
||||
clips_dir.mkdir(parents=True, exist_ok=True)
|
||||
force_beats = _forced_beats()
|
||||
|
||||
def is_dark_title_card(path: Path | None) -> bool:
|
||||
if path is None or not path.exists():
|
||||
return False
|
||||
try:
|
||||
image = Image.open(path).convert("L").resize((160, 90))
|
||||
except Exception:
|
||||
return False
|
||||
stat = ImageStat.Stat(image)
|
||||
mean = float(stat.mean[0])
|
||||
extrema = image.getextrema()
|
||||
if mean > 55.0 or extrema[1] < 90:
|
||||
return False
|
||||
pixels = list(image.getdata())
|
||||
bright = sum(1 for p in pixels if p >= 92)
|
||||
mid = sum(1 for p in pixels if 30 <= p < 92)
|
||||
total = max(1, len(pixels))
|
||||
bright_ratio = bright / total
|
||||
mid_ratio = mid / total
|
||||
return 0.004 <= bright_ratio <= 0.18 and mid_ratio <= 0.35
|
||||
|
||||
rows: list[BeatRow] = []
|
||||
for beat in beats:
|
||||
bid = beat["beat_id"]
|
||||
@@ -534,6 +561,7 @@ def collect_rows(
|
||||
):
|
||||
compare_clip = cmp4
|
||||
|
||||
is_graphic = (rec is None and is_dark_title_card(trailer_still))
|
||||
rows.append(BeatRow(
|
||||
bid=bid,
|
||||
trailer_in_s=beat["start_s"], trailer_out_s=beat["end_s"],
|
||||
@@ -553,6 +581,7 @@ def collect_rows(
|
||||
trailer_clip=trailer_clip,
|
||||
source_clip=source_clip,
|
||||
compare_clip=compare_clip,
|
||||
is_graphic=is_graphic,
|
||||
))
|
||||
return rows
|
||||
|
||||
@@ -569,6 +598,7 @@ def render_markdown(
|
||||
) -> str:
|
||||
matched = sum(1 for r in rows if r.matched)
|
||||
confirmed = sum(1 for r in rows if r.confirmed)
|
||||
graphic = sum(1 for r in rows if r.is_graphic)
|
||||
|
||||
out: list[str] = []
|
||||
out.append("# Cutter-Report — manuelles Nachschneiden")
|
||||
@@ -587,11 +617,12 @@ def render_markdown(
|
||||
out.append("|--------|-----------|")
|
||||
out.append("| `OK` | Bestätigt durch CV-Analyse — übernehmen |")
|
||||
out.append("| `?` | Vorläufig — korrekte Szene, Phase im NLE prüfen |")
|
||||
out.append("| `GFX` | Titel-/Grafikkarte — nicht aus dem Spielfilm matchen |")
|
||||
out.append("| `MAN.` | Kein automatischer Treffer — manuell setzen |")
|
||||
out.append("")
|
||||
out.append(
|
||||
f"**{len(rows)}** Beats gesamt · **{matched}** automatisch (**{confirmed}** bestätigt)"
|
||||
f" · **{len(rows) - matched}** manuell."
|
||||
f" · **{graphic}** Grafik/Titel · **{len(rows) - matched - graphic}** manuell."
|
||||
)
|
||||
out.append("")
|
||||
|
||||
@@ -643,10 +674,14 @@ def render_markdown(
|
||||
f" @ Trailer-Offset {seg_offset:.2f}s"
|
||||
f" (scene {seg.get('scene_id', '?')})"
|
||||
)
|
||||
else:
|
||||
if r.is_graphic:
|
||||
out.append("- **Source** : — (Titel-/Grafikkarte, nicht aus Source matchen)")
|
||||
else:
|
||||
out.append("- **Source** : — (manuell setzen)")
|
||||
if r.score > 0 and r.score < 0.65:
|
||||
out.append(f"- ⚠ Score {r.score:.3f} unter 0.65 — visuell prüfen")
|
||||
if not r.is_graphic:
|
||||
out.append(f"- **Rematch**: `python cli.py rematch --beat {r.bid}`")
|
||||
if r.phase:
|
||||
out.append(f"- **Phase**: {r.phase}")
|
||||
@@ -666,7 +701,7 @@ def render_markdown(
|
||||
out.append("| Trailer | Source |")
|
||||
out.append("|:---:|:---:|")
|
||||
t_cell = f"" if t_uri else "_(kein Still)_"
|
||||
s_cell = f"" if s_uri else "_(MAN.)_"
|
||||
s_cell = f"" if s_uri else f"_({r.status})_"
|
||||
out.append(f"| {t_cell} | {s_cell} |")
|
||||
out.append("")
|
||||
|
||||
@@ -772,6 +807,7 @@ table.ov tr:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
.badge.ok { background: var(--ok-bg); color: var(--ok); border: 1px solid rgba(74, 222, 128, 0.2); }
|
||||
.badge.q { background: var(--q-bg); color: var(--q); border: 1px solid rgba(251, 191, 36, 0.2); }
|
||||
.badge.man { background: var(--man-bg); color: var(--man); border: 1px solid rgba(248, 113, 113, 0.2); }
|
||||
.badge.gfx { background: rgba(96, 165, 250, 0.12); color: #93c5fd; border: 1px solid rgba(147, 197, 253, 0.24); }
|
||||
|
||||
/* Beat cards */
|
||||
.beats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(600px, 1fr)); gap: 32px; }
|
||||
@@ -884,6 +920,10 @@ def render_html(
|
||||
'<tr><td><span class="badge q">?</span></td>'
|
||||
'<td>Vorläufig — Phase und Aktion im NLE visuell prüfen</td></tr>'
|
||||
)
|
||||
parts.append(
|
||||
'<tr><td><span class="badge gfx">GFX</span></td>'
|
||||
'<td>Titel-/Grafikkarte — als Trailer-Grafik übernehmen, nicht im Spielfilm suchen</td></tr>'
|
||||
)
|
||||
parts.append(
|
||||
'<tr><td><span class="badge man">MAN.</span></td>'
|
||||
'<td>Kein Treffer — manuell suchen oder Schwarzbild einfügen</td></tr>'
|
||||
@@ -911,7 +951,7 @@ def render_html(
|
||||
str(s.get("scene_id", "?")) for s in r.segments
|
||||
))
|
||||
scene = "+".join(all_scenes)
|
||||
bcls = {"OK": "ok", "?": "q", "MAN.": "man"}[r.status]
|
||||
bcls = {"OK": "ok", "?": "q", "GFX": "gfx", "MAN.": "man"}[r.status]
|
||||
parts.append(
|
||||
f'<tr>'
|
||||
f'<td class="num"><a href="#beat-{r.bid:02d}">{r.bid:02d}</a></td>'
|
||||
@@ -932,7 +972,7 @@ def render_html(
|
||||
ti = smpte(r.trailer_in_s, trailer_fps)
|
||||
to = smpte(r.trailer_out_s, trailer_fps)
|
||||
dur = r.trailer_out_s - r.trailer_in_s
|
||||
bcls = {"OK": "ok", "?": "q", "MAN.": "man"}[r.status]
|
||||
bcls = {"OK": "ok", "?": "q", "GFX": "gfx", "MAN.": "man"}[r.status]
|
||||
|
||||
parts.append(f'<div class="beat" id="beat-{r.bid:02d}">')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user