가사 숫자 한글화 — Suno 오독 방지

- 프롬프트에 아라비아 숫자 금지 규칙+예시 추가
- 안전망 hangulize_numbers: 한자어 수사(일승·칠십팔승·이차전), 게임·경기는
  고유어(일곱 게임), 스코어 5:3→오 대 삼, 소수 타율 영점삼일이. 제목도 적용

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jwkim 2026-08-26 10:18:09 +09:00
parent 6770543318
commit 579ab768bf

View File

@ -466,6 +466,9 @@ def _lyrics_prompt(team_name: str, context: str, league: str = "kbo") -> str:
"- 경기장에서 수만 관중이 떼창하는 웅장한 스타디움 앤섬\n" "- 경기장에서 수만 관중이 떼창하는 웅장한 스타디움 앤섬\n"
"- 오늘 경기 맥락(어제/최근 결과 설욕·기세, 순위 싸움, 연전 차수, 선발·라인업)을 " "- 오늘 경기 맥락(어제/최근 결과 설욕·기세, 순위 싸움, 연전 차수, 선발·라인업)을 "
"가사에 구체적으로 녹일 것 — 선수 실명 사용 가능\n" "가사에 구체적으로 녹일 것 — 선수 실명 사용 가능\n"
"- 가사에 아라비아 숫자를 절대 쓰지 말 것 — 모두 한글 발음으로 표기 "
"(1승→일승, 1위→일위, 78승→칠십팔승, 2차전→이차전, 1번→일번, "
"7게임→일곱 게임, 15호→십오호, 5:3→오 대 삼)\n"
"- 괄호로 관중 콜앤리스폰스 파트 표기, 섹션 태그에 연주 지시 포함\n" "- 괄호로 관중 콜앤리스폰스 파트 표기, 섹션 태그에 연주 지시 포함\n"
"- 분량은 1분 30초~2분 (Verse 2개 + Chorus 반복 + Bridge + Final Chorus)\n\n" "- 분량은 1분 30초~2분 (Verse 2개 + Chorus 반복 + Bridge + Final Chorus)\n\n"
f"[가사 형식 레퍼런스 — 구조와 톤만 참고, 내용은 오늘 경기에 맞게 새로 쓸 것]\n{_REFERENCE}\n\n" f"[가사 형식 레퍼런스 — 구조와 톤만 참고, 내용은 오늘 경기에 맞게 새로 쓸 것]\n{_REFERENCE}\n\n"
@ -475,6 +478,62 @@ def _lyrics_prompt(team_name: str, context: str, league: str = "kbo") -> str:
) )
# ── 가사 숫자 한글화 (Suno 가 아라비아 숫자를 잘못 읽는 문제 방지) ──
_SINO = "영일이삼사오육칠팔구"
_NATIVE = {
1: "한", 2: "두", 3: "세", 4: "네", 5: "다섯", 6: "여섯", 7: "일곱",
8: "여덟", 9: "아홉", 10: "열", 11: "열한", 12: "열두", 13: "열세",
14: "열네", 15: "열다섯", 16: "열여섯", 17: "열일곱", 18: "열여덟",
19: "열아홉", 20: "스무",
}
def _sino(n: int) -> str:
"""한자어 수사 — 78 → 칠십팔 (0~9999)."""
if n == 0:
return "영"
parts = []
for unit, name in ((1000, "천"), (100, "백"), (10, "십")):
d, n = divmod(n, unit)
if d:
parts.append(("" if d == 1 else _SINO[d]) + name)
if n:
parts.append(_SINO[n])
return "".join(parts)
def _native(n: int) -> str:
"""고유어 수사 — 7 → 일곱 (범위 밖은 한자어 폴백)."""
return _NATIVE.get(n) or _sino(n)
def hangulize_numbers(text: str) -> str:
"""가사 속 아라비아 숫자를 전부 한글 발음으로 치환.
1승→일승, 78승→칠십팔승, 7게임→일곱 게임, 5:3→오 대 삼, 0.312→영점삼일이.
"""
# 소수(타율 등): 0.312 → 영점삼일이 (소수부는 자리별 낭독)
text = re.sub(
r"(\d)\.(\d+)",
lambda m: _sino(int(m.group(1))) + "점" + "".join(_SINO[int(c)] for c in m.group(2)),
text,
)
# 스코어: 5:3 / 5대3 → 오 대 삼
text = re.sub(
r"(\d+)\s*[:대]\s*(\d+)",
lambda m: f"{_sino(int(m.group(1)))} 대 {_sino(int(m.group(2)))}",
text,
)
# 게임·경기 수는 고유어 수사 (7게임 → 일곱 게임)
text = re.sub(
r"(\d+)\s*(게임|경기)",
lambda m: f"{_native(int(m.group(1)))} {m.group(2)}",
text,
)
# 남은 모든 숫자는 한자어 수사 (1위→일위, 2차전→이차전, 15호→십오호)
return re.sub(r"\d+", lambda m: _sino(int(m.group(0))), text)
def _parse_json(text: str) -> dict: def _parse_json(text: str) -> dict:
t = text.strip() t = text.strip()
t = re.sub(r"^```(?:json)?\s*|\s*```$", "", t) t = re.sub(r"^```(?:json)?\s*|\s*```$", "", t)
@ -549,6 +608,8 @@ async def _start_one(db, m: Match, side: str, lineups: dict | None) -> bool:
lyrics = str(piece.get("lyrics") or "").strip() lyrics = str(piece.get("lyrics") or "").strip()
if not lyrics: if not lyrics:
raise RuntimeError("LLM 가사 비어있음") raise RuntimeError("LLM 가사 비어있음")
lyrics = hangulize_numbers(lyrics) # 프롬프트를 뚫고 남은 숫자 안전망
title = hangulize_numbers(title) # 제목도 Suno 가 부르는 경우 대비
task_id = await suno.start_generation(title, style, lyrics) task_id = await suno.start_generation(title, style, lyrics)
upgrade = bool(row and row.status == "complete") upgrade = bool(row and row.status == "complete")