@@ -50,10 +50,10 @@ def test_generate_script_dry_run():
5050 assert script .fact_checked is True
5151
5252
53- def test_generate_script_dry_run_fills_four_beats ():
54- """dry_run 대본은 영상 비트 4개 (훅·핵심· 팁·마무리)로 분할돼 채워진다."""
53+ def test_generate_script_dry_run_fills_beats ():
54+ """dry_run 대본은 영상 비트 3개 (훅·핵심/ 팁·마무리)로 분할돼 채워진다."""
5555 script = _client ().generate_script ("강아지 닭가슴살 간식 적정량" )
56- assert len (script .beats ) == 4
56+ assert len (script .beats ) == 3
5757 assert all (b .strip () for b in script .beats )
5858
5959
@@ -103,13 +103,11 @@ def test_script_system_prompt_pins_strong_hook():
103103 assert "먹으면서 말하는" in SCRIPT_SYSTEM_PROMPT
104104
105105
106- def test_pick_beat_count_deterministic_and_both_variants ():
107- """완료율 A/B: 주제 해시로 3/4비트가 결정적으로 갈리고, 두 변형이 모두 나온다 ."""
106+ def test_pick_beat_count_is_three ():
107+ """비트 수는 3 고정(2026-07-31 PO "3비트로 고정하자, 4비트 너무 김") ."""
108108 from nutti .integrations .ai_text import pick_beat_count
109109
110- assert pick_beat_count ("고구마" ) == pick_beat_count ("고구마" )
111- seen = {pick_beat_count (f"주제-{ i } " ) for i in range (100 )}
112- assert seen == {3 , 4 }
110+ assert {pick_beat_count (f"주제-{ i } " ) for i in range (100 )} == {3 }
113111
114112
115113def test_build_script_system_prompt_three_beats ():
@@ -146,15 +144,10 @@ def test_generate_script_dry_run_beats_follow_ab():
146144
147145
148146def test_split_beats_uses_topic_beat_count ():
149- """REVISE 재분할도 주제 해시 비트 수를 따른다(대본 생성과 단일 소스)."""
150- from nutti .integrations .ai_text import pick_beat_count
151-
147+ """REVISE 재분할도 pick_beat_count를 따른다(대본 생성과 단일 소스)."""
152148 client = _client ()
153149 body = "1문장이야.\n 2문장이야.\n 3문장이야.\n 4문장이야."
154- topic3 = next (f"주제-{ i } " for i in range (100 ) if pick_beat_count (f"주제-{ i } " ) == 3 )
155- topic4 = next (f"주제-{ i } " for i in range (100 ) if pick_beat_count (f"주제-{ i } " ) == 4 )
156- assert len (client .split_beats (body , topic3 )) == 3
157- assert len (client .split_beats (body , topic4 )) == 4
150+ assert len (client .split_beats (body , "주제-0" )) == 3
158151
159152
160153def test_vlog_format_rule_has_fail_twist ():
@@ -405,18 +398,24 @@ def _valid_body() -> str:
405398 return "\n " .join (lines )
406399
407400
401+ def _valid_body3 () -> str :
402+ """운영 기본인 3비트(훅·핵심·CTA) 통과 본문 — _valid_body에서 설명 1줄 제거."""
403+ lines = _valid_body ().splitlines ()
404+ return "\n " .join ([lines [0 ], lines [1 ], lines [3 ]])
405+
406+
408407def test_validate_script_body_passes_clean_script ():
409408 """하드룰 전부 통과하는 대본은 위반 0건."""
410409 from nutti .integrations .ai_text import validate_script_body
411410
412- assert validate_script_body (_valid_body ()) == []
411+ assert validate_script_body (_valid_body3 ()) == []
413412
414413
415414def test_validate_script_body_catches_each_rule ():
416415 """규칙별 검출: 비트 수·글자수·의성어·발음 리스크·브랜드명·마지막 느낌표."""
417416 from nutti .integrations .ai_text import validate_script_body
418417
419- base = _valid_body ().splitlines ()
418+ base = _valid_body3 ().splitlines ()
420419
421420 def swapped (idx : int , line : str ) -> str :
422421 lines = base [:]
@@ -427,11 +426,11 @@ def swapped(idx: int, line: str) -> str:
427426 cases = [
428427 (0 , "강아지가 콜록콜록 기침하면 열에 아홉은 놓치는 위험 신호가 있어요" , "의성어" ),
429428 (1 , "짧은 대사" , "38~44자" ),
430- (2 , "귀진드기 감염은 초기에 잡아야 해요 가려움 신호를 놓치지 마세요 꼭" , "발음" ),
431- (3 , "Nutti 계산기로 우리 아이 맞춤 급여량을 오늘 바로 확인해 보세요" , "브랜드" ),
432- (3 , "프로필 링크의 간식 계산기로 우리 아이 맞춤 급여량을 확인하세요!" , "느낌표" ),
429+ (1 , "귀진드기 감염은 초기에 잡아야 해요 가려움 신호를 놓치지 마세요 꼭" , "발음" ),
430+ (2 , "Nutti 계산기로 우리 아이 맞춤 급여량을 오늘 바로 확인해 보세요" , "브랜드" ),
431+ (2 , "프로필 링크의 간식 계산기로 우리 아이 맞춤 급여량을 확인하세요!" , "느낌표" ),
433432 ]
434- assert any ("4줄 " in v for v in validate_script_body ("\n " .join (base [:3 ])))
433+ assert any ("3줄 " in v for v in validate_script_body ("\n " .join (base [:2 ])))
435434 for idx , line , keyword in cases :
436435 violations = validate_script_body (swapped (idx , line ))
437436 assert any (keyword in v for v in violations ), (keyword , violations )
@@ -441,7 +440,8 @@ def test_generate_script_regenerates_on_hard_rule_violation(monkeypatch):
441440 """하드룰 위반 대본 → 위반 사유를 붙여 자동 재생성, 통과본으로 확정."""
442441 settings = Settings (NUTTI_DRY_RUN = False , ANTHROPIC_API_KEY = "" , NUTTI_ENV = "test" )
443442 client = AITextClient (settings ) # _client=None → 폴백(_llm_text) 경로
444- bodies = iter (["강아지가 콜록콜록 짧은 대사" , _valid_body ()])
443+ good = _valid_body3 ()
444+ bodies = iter (["강아지가 콜록콜록 짧은 대사" , good ])
445445 prompts : list [str ] = []
446446
447447 def fake_llm (full , ** _kw ):
@@ -451,17 +451,11 @@ def fake_llm(full, **_kw):
451451 monkeypatch .setattr (client , "_llm_text" , fake_llm )
452452 # 간식 선정도 _llm_text를 쓰므로 페이크 iterator를 소모하지 않게 고정값으로 대체.
453453 monkeypatch .setattr (client , "suggest_food" , lambda _t : ("고구마 스틱" , "sweet potato" ))
454- # _valid_body()는 4줄이므로 4비트 버킷 주제로 고정(A/B 도입 후 3비트 주제면 반려됨).
455- from nutti .integrations .ai_text import pick_beat_count
456-
457- topic = next (
458- t for t in (f"간식 적정량-{ i } " for i in range (50 )) if pick_beat_count (t ) == 4
459- )
460- script = client .generate_script (topic )
454+ script = client .generate_script ("간식 적정량" )
461455 assert len (prompts ) == 2 # 1회 위반 → 1회 재생성
462456 assert "하드룰 위반" in prompts [1 ] and "의성어" in prompts [1 ] # 위반 사유가 피드백으로
463- assert script .body == _valid_body ()
464- assert len (script .beats ) == 4
457+ assert script .body == good
458+ assert len (script .beats ) == 3
465459
466460
467461def test_generate_script_gives_up_after_max_tries (monkeypatch ):
@@ -493,9 +487,9 @@ def test_fact_check_parse_failure_fails_safe():
493487
494488def test_generate_script_live_populates_beats ():
495489 """라이브 Anthropic 경로도 beats를 채운다(되돌리면 영상이 8초 단일컷으로 퇴화 → 회귀 핀)."""
496- msg = _Msg ([_Block ("text" , text = "훅 문장\n 핵심 문장\n 팁 문장 \ n 마무리 문장" )])
490+ msg = _Msg ([_Block ("text" , text = "훅 문장\n 핵심 문장\n 마무리 문장" )])
497491 script = _live_client (msg ).generate_script ("강아지 간식" )
498- assert len (script .beats ) == 4
492+ assert len (script .beats ) == 3
499493 assert script .beats [0 ] == "훅 문장"
500494 assert script .beats [- 1 ] == "마무리 문장"
501495 # 안전 불변식: 생성 단계는 fact_checked=False여야 한다(오직 fact_check_script만 승격).
0 commit comments