From 746eaced80b79ec20bf14fcaa9bb69402d84f012 Mon Sep 17 00:00:00 2001 From: jwkim Date: Thu, 27 Aug 2026 11:40:45 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=80=EC=82=AC=20=ED=92=88=EC=A7=88=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=20=E2=80=94=20=ED=95=9C=EA=B5=AD=EC=96=B4=20?= =?UTF-8?q?=EC=88=98=EC=82=AC=20=EA=B7=9C=EC=B9=99=C2=B7=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=EB=B6=84=EB=9F=89=C2=B7=EC=BA=90=EC=8B=9C=20TTL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서수 고유어(4번째→네 번째), 고유어 조수사(방·개·명·골), 아웃 카운트 영어 수사(2아웃→투아웃) - 가사 축약 방지: 필수 섹션 명시(Verse2·Bridge·Final Chorus), max_tokens 3000→8000, 450자 미만은 재시도 - 프론트 곡 캐시 하루→5분 TTL (당일 재생성 교체 반영) Co-Authored-By: Claude Fable 5 --- backend/app/services/songs.py | 27 +++++++++++++++++++++++---- frontend/src/lib/playlist.ts | 8 ++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/backend/app/services/songs.py b/backend/app/services/songs.py index 64aaa3b..031ee86 100644 --- a/backend/app/services/songs.py +++ b/backend/app/services/songs.py @@ -468,7 +468,8 @@ def _lyrics_prompt(team_name: str, context: str, league: str = "kbo") -> str: "가사에 구체적으로 녹일 것 — 선수 실명 사용 가능\n" "- 숫자는 자연스럽게 표기 (13:1, 78승, 2차전 등 — 발음 변환은 시스템이 처리)\n" "- 괄호로 관중 콜앤리스폰스 파트 표기, 섹션 태그에 연주 지시 포함\n" - "- 분량은 1분 30초~2분 (Verse 2개 + Chorus 반복 + Bridge + Final Chorus)\n\n" + "- 섹션 구성 필수: [Intro] → [Verse 1] → [Chorus] → [Verse 2] → [Chorus] → " + "[Bridge] → [Final Chorus] → [Outro] 전부 포함 (생략 금지), 가사 본문 700자 이상\n\n" f"[가사 형식 레퍼런스 — 구조와 톤만 참고, 내용은 오늘 경기에 맞게 새로 쓸 것]\n{_REFERENCE}\n\n" "다음 키를 가진 JSON 객체 하나만 출력하라:\n" ' "title": 곡 제목 (한국어, 25자 이내, 오늘 경기 느낌이 나게),\n' @@ -505,6 +506,10 @@ def _native(n: int) -> str: return _NATIVE.get(n) or _sino(n) +# 서수 관형형 — 1~4는 첫/두/세/네, 이후는 고유어 수사 그대로 (다섯 번째) +_ORDINAL = {1: "첫", 2: "두", 3: "세", 4: "네"} + + def hangulize_numbers(text: str) -> str: """가사 속 아라비아 숫자를 한글 발음으로 치환 (Suno 전송본 전용). @@ -530,9 +535,21 @@ def _hangulize_plain(text: str) -> str: lambda m: f"{_sino(int(m.group(1)))} 대 {_sino(int(m.group(2)))}", text, ) - # 게임·경기 수는 고유어 수사 (7게임 → 일곱 게임) + # 아웃 카운트는 야구 관례상 영어 수사 (2아웃→투아웃) text = re.sub( - r"(\d+)\s*(게임|경기)", + r"([123])\s*아웃", + lambda m: {1: "원", 2: "투", 3: "쓰리"}[int(m.group(1))] + "아웃", + text, + ) + # 서수는 관형 고유어 수사 (1번째→첫 번째, 4번째→네 번째, 5번째→다섯 번째) + text = re.sub( + r"(\d+)\s*번째", + lambda m: (_ORDINAL.get(int(m.group(1))) or _native(int(m.group(1)))) + " 번째", + text, + ) + # 고유어 조수사 (7게임→일곱 게임, 2방→두 방, 3개→세 개) + text = re.sub( + r"(\d+)\s*(게임|경기|개|명|방|골|마리|살|바퀴)", lambda m: f"{_native(int(m.group(1)))} {m.group(2)}", text, ) @@ -556,7 +573,7 @@ async def write_lyrics(team_name: str, context: str, league: str = "kbo") -> dic client = AsyncAnthropic(api_key=settings.anthropic_api_key) msg = await client.messages.create( model=settings.anthropic_model, - max_tokens=3000, + max_tokens=8000, messages=[{"role": "user", "content": prompt}], ) text = "".join(b.text for b in msg.content if getattr(b, "type", "") == "text") @@ -614,6 +631,8 @@ async def _start_one(db, m: Match, side: str, lineups: dict | None) -> bool: lyrics = str(piece.get("lyrics") or "").strip() if not lyrics: raise RuntimeError("LLM 가사 비어있음") + if len(lyrics) < 450: + raise RuntimeError(f"가사 너무 짧음({len(lyrics)}자) — 재시도") # 표기용(화면 '가사 보기')은 원문 그대로 저장하고, # Suno 전송본만 숫자를 한글 발음으로 치환 (13:1 → 십삼 대 일) sung_lyrics = hangulize_numbers(lyrics) diff --git a/frontend/src/lib/playlist.ts b/frontend/src/lib/playlist.ts index c01996e..6379d67 100644 --- a/frontend/src/lib/playlist.ts +++ b/frontend/src/lib/playlist.ts @@ -30,6 +30,9 @@ import { getTodaySongs, type SongOut } from "./api"; let songsPromise: Promise | null = null; let songsDay = ""; +let songsAt = 0; +// 곡은 라인업 반영 재생성으로 당일 중에도 교체되므로 짧은 TTL 로 재조회 +const SONGS_TTL_MS = 5 * 60_000; function todayKstKey(): string { return new Date(Date.now() + 9 * 3600_000).toISOString().slice(0, 10); @@ -37,9 +40,10 @@ function todayKstKey(): string { function fetchSongsCached(): Promise { const day = todayKstKey(); - if (!songsPromise || songsDay !== day) { + if (!songsPromise || songsDay !== day || Date.now() - songsAt > SONGS_TTL_MS) { songsDay = day; - songsPromise = getTodaySongs().catch(() => []); // 전 리그(kbo+mlb) + songsAt = Date.now(); + songsPromise = getTodaySongs().catch(() => []); // 전 리그 } return songsPromise; }