Skip to content

Commit 49716ab

Browse files
wjdtjddns98claude
andauthored
fix(video): 자막 환각 억제 재이식 — 재생성이 목소리를 튀게 하고 런까지 죽였다 (#129)
run30 실측: 비트3이 QC text_overlay로 2회 재생성되다 3번째 시도에서 fal 폴링 타임아웃으로 런 자체가 사망(exit 1). 자막 환각은 화면 품질만의 문제가 아니다. 실제 피해 경로는: 대사 한글이 프롬프트에 인용됨 → Veo가 화면 자막으로 그림 → QC text_overlay → 재생성(seed = video_seed + attempt) → **그 비트 목소리가 튄다** PO가 반복 지적한 "목소리가 다 다르다"의 주 경로다. 억제 자체가 보이스 일관성 대책이다. 3중 방어를 복원한다: - ① 대사 **바로 뒤**에 Hangul을 콕 집은 부정(트리거 인접 — 프롬프트 끝 _NEGATIVE보다 가중치가 높다) - ② _NEGATIVE에 "no Hangul/Korean characters, no subtitle bar, no bottom caption" - ③ config.veo_fal_negative_prompt에 hangul·korean subtitles·bottom subtitle·lower third 리버트 가드 추가: 세 겹이 모두 있는지 + 대사 인접 부정이 대사보다 뒤에 오는지 검사한다. PR #121에 있던 억제를 #126 revert가 직립과 함께 되돌리면서 재발했다 — 직립 없이 억제만 이식했다. 한계(ponytail): Veo 자막은 확률적이라 완전 제거는 보장 못 한다. 재생성이 남으면 그 비트 보이스는 여전히 시드 변동으로 튄다(그 경우 재시도 시드 고정이 후속 레버). ruff + pytest 506 passed. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 24ef443 commit 49716ab

3 files changed

Lines changed: 46 additions & 1 deletion

File tree

nutti/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ class Settings(BaseSettings):
107107
default=(
108108
"text, subtitles, captions, words, letters, writing, watermark, "
109109
"on-screen text, caption bar, hardcoded subtitles, korean text overlay, "
110+
# 한글 콕 집은 억제(2026-07-31 재이식) — 자막 환각은 QC 재생성을 부르고,
111+
# 재생성마다 seed가 바뀌어 그 비트 목소리가 튄다(보이스 드리프트의 주 경로).
112+
"hangul, hangul characters, korean characters, korean subtitles, "
113+
"subtitle bar at bottom, bottom subtitle, lower third text, "
110114
# 클립 끝 페이드아웃 억제 — 끝 프레임이 어두워지면 프레임 체이닝이 망가진다.
111115
"fade out, fade to black, fade in, dimming, darkening, vignette, "
112116
# 클립 끝 자세 변화(누움·이탈) 억제 — 비트 경계 점프의 주원인.

nutti/integrations/video.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,9 @@ class VeoPromptBuilder:
673673
"The subject is a real live photorealistic puppy — never a mascot suit, fursuit, "
674674
"costume, person in a costume, or plush toy. Strictly no additional animals, no "
675675
"people. Absolutely no text, subtitles, captions, letters, numbers, words, logos, "
676-
"brand names, watermarks, or UI overlays anywhere in the frame."
676+
"brand names, watermarks, or UI overlays anywhere in the frame — in particular no "
677+
"Hangul or Korean characters, no subtitle bar, and no caption text along the bottom "
678+
"of the screen."
677679
)
678680
# 먹방 연출(2026-07-23 PO): 간식 그릇을 앞에 두고, 클립 "시작"에 한 입만 먹고
679681
# 말한다. 시작에 두는 이유 — 끝 잉여 트림(_generate_and_trim_clip)이 발화 이후
@@ -756,6 +758,17 @@ def build_beat(
756758
prompt = (
757759
f"A photorealistic shot of {self._PERSONA}, {speaking}, "
758760
f"saying (as spoken audio only, no on-screen text): '{dialogue}'. "
761+
# 자막 환각 억제의 핵심 방어: 위 대사가 한글로 프롬프트에 들어가면 Veo가 그
762+
# 글자를 화면 하단 자막으로 그리는 경향이 있다(깨진 한글 = 외계어 자막).
763+
# 대사 '바로 뒤'에 Hangul을 콕 집은 강한 부정을 붙인다 — 프롬프트 끝 _NEGATIVE
764+
# 보다 트리거(대사)에 인접해 가중치가 높다.
765+
# ⚠️ 이건 화면 품질만의 문제가 아니다: 자막 환각 → QC text_overlay 재생성 →
766+
# 재시도 seed 오프셋(video_seed+attempt) → **그 비트 목소리가 튄다**. PO가
767+
# 반복 지적한 "목소리가 다 다르다"의 주 경로라 억제 자체가 보이스 일관성 대책이다
768+
# (2026-07-31 run30 실측: 비트3이 text_overlay로 2회 재생성되다 타임아웃으로 사망).
769+
"These spoken words must never be shown as written text on screen — no captions, "
770+
"no subtitles, no Hangul or Korean letters, no subtitle bar, and no text along "
771+
"the bottom of the frame; the line exists only as the spoken voice. "
759772
f"{scene}{mic}"
760773
f"{self._VOICE} {cta}"
761774
f"{self._LIPSYNC} "

tests/test_video.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1780,6 +1780,34 @@ def test_pick_episode_style_outfit_setting_independent():
17801780
assert mismatched
17811781

17821782

1783+
def test_hangul_subtitle_suppression_is_three_layered():
1784+
"""자막 환각 억제 3중 방어 리버트 가드 — 보이스 일관성 대책이기도 하다.
1785+
1786+
자막 환각은 화면 품질만의 문제가 아니다: 환각 → QC text_overlay 재생성 → 재시도
1787+
seed 오프셋 → **그 비트 목소리가 튄다**. PO가 반복 지적한 "목소리가 다 다르다"의
1788+
주 경로다(2026-07-31 run30 실측: 비트3이 text_overlay로 2회 재생성되다 fal 타임아웃
1789+
으로 런 자체가 사망).
1790+
1791+
① 대사 바로 뒤(트리거 인접·가중치 최상) ② _NEGATIVE(프롬프트 끝) ③ 제출
1792+
negative_prompt(설정). 한 겹이라도 빠지면 재생성이 늘어난다.
1793+
"""
1794+
from nutti.config import Settings
1795+
from nutti.integrations.video import VeoPromptBuilder
1796+
1797+
style = EpisodeStyle("a sporty grey hoodie", "sitting on a sofa", "", "mukbang")
1798+
prompt = VeoPromptBuilder().build_beat("간식 아무거나 주지 마", style=style)
1799+
1800+
# ① 대사 인접 부정 — 대사보다 뒤, _NEGATIVE보다 앞에 있어야 한다.
1801+
assert "no Hangul or Korean letters" in prompt
1802+
assert prompt.index("no Hangul or Korean letters") > prompt.index("간식 아무거나 주지 마")
1803+
# ② 프롬프트 끝 _NEGATIVE.
1804+
assert "no Hangul or Korean characters" in VeoPromptBuilder._NEGATIVE
1805+
# ③ 제출 negative_prompt(설정 기본값).
1806+
neg = Settings(NUTTI_ENV="test").veo_fal_negative_prompt
1807+
for word in ("hangul", "korean subtitles", "bottom subtitle"):
1808+
assert word in neg, f"negative_prompt 억제 어휘 누락: {word}"
1809+
1810+
17831811
def test_build_beat_always_includes_persona_and_fixed_voice():
17841812
"""페르소나·고정 목소리 묘사는 style 유무와 무관하게 모든 비트에 포함된다.
17851813

0 commit comments

Comments
 (0)