posteralive gate 조정

This commit is contained in:
jaehwang 2026-09-09 11:32:24 +09:00
parent 3ee1c3bdfc
commit a2543e19d4
2 changed files with 24 additions and 21 deletions

View File

@ -1,9 +1,14 @@
포스터 영상의 서로 다른 시점에서 잘라낸 '제목 영역' 이미지들이다. 포스터 영상의 서로 다른 시점에서 같은 자리를 잘라낸 '제목 영역' 이미지들이다.
질문은 하나다: **각 시점에서 제목 글자가 온전히 읽히는가.** [0]은 생성 전 원본이고 [1]부터가 생성된 프레임이다.
질문은 하나다: **[0]에 보이는 글자가 각 시점에서도 그대로 읽히는가.**
판정 기준: 판정 기준:
- 글자 자체가 온전하면 legible=true. 배경이 변한 것(불꽃·구름·빛이 글자 **뒤로** 지나감)은 - [0]에 없는 글자는 판정 대상이 아니다. 상자가 제목의 일부만 덮고 있을 수 있고,
훼손이 아니다. 잘려 나간 글자를 '없어졌다'고 하면 안 된다.
- 글자가 다른 그림에 **가려짐**, 획이 지워짐·뭉개짐, 다른 글자로 변형, 재작화됐으면 legible=false. - [0]에 있던 글자가 다른 그림에 가려짐, 획이 지워짐·뭉개짐, 다른 글자로 변형,
- 기대 제목의 일부 글자만 훼손돼도 false다. broken에 어떤 글자가 어떻게 됐는지 짧게 쓴다. 재작화됐으면 legible=false.
- 배경만 변한 것은 훼손이 아니다. 불꽃·구름·빛이 글자 뒤로 지나가는 것은 그대로 통과다.
- [0]은 원본이므로 항상 legible=true다.
- index는 이미지에 붙은 번호 그대로 쓴다. 훼손이 없으면 broken은 빈 문자열. - index는 이미지에 붙은 번호 그대로 쓴다. 훼손이 없으면 broken은 빈 문자열.
- broken에는 [0]의 어떤 글자가 어떻게 됐는지 짧게 쓴다.

View File

@ -3,6 +3,9 @@
"파일이 존재하는가" 보던 검증 탓에 구름이 제목을 덮은 클립이 검수대까지 적이 있다. "파일이 존재하는가" 보던 검증 탓에 구름이 제목을 덮은 클립이 검수대까지 적이 있다.
픽셀 지표 하나로는 갈리지 않아서 영역 평균차는 제목 불꽃과 제목 구름을 구분 하고 픽셀 지표 하나로는 갈리지 않아서 영역 평균차는 제목 불꽃과 제목 구름을 구분 하고
글리프 휘도차는 구름이 글자를 덮으면 0 수렴한다 2단을 VLM 가독성 판정으로 둔다. 글리프 휘도차는 구름이 글자를 덮으면 0 수렴한다 2단을 VLM 가독성 판정으로 둔다.
모두 프레임을 기준으로 삼는다. detect가 읽은 제목 문자열과는 대조하지 않는다
상자가 제목의 일부만 덮으면 잘려 나간 글자를 훼손으로 오인한다.
""" """
import numpy as np import numpy as np
from PIL import Image from PIL import Image
@ -76,18 +79,20 @@ def pixel_score(crops: list[Image.Image]) -> float | None:
for crop in crops[1:]) for crop in crops[1:])
async def ask_legibility(crops: list[Image.Image], times: list[float], async def ask_legibility(crops: list[Image.Image], times: list[float]) -> TitleGateAnswer:
title: str) -> TitleGateAnswer: """[0]이 기준이다. 옮겨 적은 제목과 대조하지 않는다 —
images = [(f"[{index}] t={at}s:", to_data_uri(crop, CROP_MAX_SIZE)) detect가 읽은 글자와 detect가 잡은 상자가 어긋나면 차이를 훼손으로 오인한다."""
images = [(f"[{index}] t={at}s{' (원본)' if index == 0 else ''}:",
to_data_uri(crop, CROP_MAX_SIZE))
for index, (crop, at) in enumerate(zip(crops, times, strict=True))] for index, (crop, at) in enumerate(zip(crops, times, strict=True))]
return await title_gate_llm.ask_with_images( return await title_gate_llm.ask_with_images(
TitleGateAnswer, TitleGateAnswer,
f"기대 제목: {title!r}\n시점 {len(crops)}장. 각 이미지를 순서대로 판정하라.", f"시점 {len(crops)}장. [0]을 기준으로 각 이미지를 순서대로 판정하라.",
images, system=TITLE_GATE_PROMPT) images, system=TITLE_GATE_PROMPT)
async def check_title(clip: bytes, regions: Regions) -> TitleGateVerdict: async def check_title(clip: bytes, regions: Regions) -> TitleGateVerdict:
"""판단 근거가 없으면(제목 상자·텍스트 없음) 통과시키고 사유를 남긴다.""" """제목 상자가 없으면 판단 근거가 없으므로 통과시키고 사유를 남긴다."""
clip_seconds = duration(clip) clip_seconds = duration(clip)
times = [round(max(0.0, min(clip_seconds - 0.05, fraction * clip_seconds)), 2) times = [round(max(0.0, min(clip_seconds - 0.05, fraction * clip_seconds)), 2)
for fraction in SAMPLE_FRACTIONS] for fraction in SAMPLE_FRACTIONS]
@ -99,26 +104,19 @@ async def check_title(clip: bytes, regions: Regions) -> TitleGateVerdict:
crops = [frame.crop(box) for frame in frames] crops = [frame.crop(box) for frame in frames]
score = pixel_score(crops) score = pixel_score(crops)
title = (regions.regions["title"].text or "").strip()
verdict = TitleGateVerdict(passed=True, samples=times, verdict = TitleGateVerdict(passed=True, samples=times,
pixel_score=round(score, 2) if score is not None else None) pixel_score=round(score, 2) if score is not None else None)
if score is not None and score > PIXEL_HARD_FAIL: if score is not None and score > PIXEL_HARD_FAIL:
# 정렬 후에도 넘는 값은 대개 배경 통과라 즉시 실패시키지 않고 VLM으로 넘긴다 # 정렬 후에도 넘는 값은 대개 배경 통과라 즉시 실패시키지 않고 VLM으로 넘긴다
verdict.pixel_hard = True verdict.pixel_hard = True
if not title:
verdict.passed = False
verdict.reason = f"획변화 {score:.1f} > {PIXEL_HARD_FAIL}, title.text 없어 VLM 판정 불가"
return verdict
if not title:
verdict.skip = "title.text 없음 — VLM 가독성 판정 불가"
return verdict
answer = await ask_legibility(crops, times, title) answer = await ask_legibility(crops, times)
verdict.frames = [FrameVerdict(index=frame.index, time=times[frame.index], verdict.frames = [FrameVerdict(index=frame.index, time=times[frame.index],
legible=frame.legible, broken=frame.broken) legible=frame.legible, broken=frame.broken)
for frame in answer.frames if frame.index < len(times)] for frame in answer.frames if frame.index < len(times)]
broken = [frame for frame in verdict.frames if not frame.legible] # [0]은 원본이라 판정 대상이 아니다
broken = [frame for frame in verdict.frames if not frame.legible and frame.index > 0]
if broken: if broken:
verdict.passed = False verdict.passed = False
verdict.reason = "제목 가독 훼손: " + "; ".join( verdict.reason = "제목 가독 훼손: " + "; ".join(