가사 품질 보강 — 한국어 수사 규칙·전체 분량·캐시 TTL
- 서수 고유어(4번째→네 번째), 고유어 조수사(방·개·명·골), 아웃 카운트 영어 수사(2아웃→투아웃) - 가사 축약 방지: 필수 섹션 명시(Verse2·Bridge·Final Chorus), max_tokens 3000→8000, 450자 미만은 재시도 - 프론트 곡 캐시 하루→5분 TTL (당일 재생성 교체 반영) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
cb00945612
commit
746eaced80
@ -468,7 +468,8 @@ def _lyrics_prompt(team_name: str, context: str, league: str = "kbo") -> str:
|
|||||||
"가사에 구체적으로 녹일 것 — 선수 실명 사용 가능\n"
|
"가사에 구체적으로 녹일 것 — 선수 실명 사용 가능\n"
|
||||||
"- 숫자는 자연스럽게 표기 (13:1, 78승, 2차전 등 — 발음 변환은 시스템이 처리)\n"
|
"- 숫자는 자연스럽게 표기 (13:1, 78승, 2차전 등 — 발음 변환은 시스템이 처리)\n"
|
||||||
"- 괄호로 관중 콜앤리스폰스 파트 표기, 섹션 태그에 연주 지시 포함\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"
|
f"[가사 형식 레퍼런스 — 구조와 톤만 참고, 내용은 오늘 경기에 맞게 새로 쓸 것]\n{_REFERENCE}\n\n"
|
||||||
"다음 키를 가진 JSON 객체 하나만 출력하라:\n"
|
"다음 키를 가진 JSON 객체 하나만 출력하라:\n"
|
||||||
' "title": 곡 제목 (한국어, 25자 이내, 오늘 경기 느낌이 나게),\n'
|
' "title": 곡 제목 (한국어, 25자 이내, 오늘 경기 느낌이 나게),\n'
|
||||||
@ -505,6 +506,10 @@ def _native(n: int) -> str:
|
|||||||
return _NATIVE.get(n) or _sino(n)
|
return _NATIVE.get(n) or _sino(n)
|
||||||
|
|
||||||
|
|
||||||
|
# 서수 관형형 — 1~4는 첫/두/세/네, 이후는 고유어 수사 그대로 (다섯 번째)
|
||||||
|
_ORDINAL = {1: "첫", 2: "두", 3: "세", 4: "네"}
|
||||||
|
|
||||||
|
|
||||||
def hangulize_numbers(text: str) -> str:
|
def hangulize_numbers(text: str) -> str:
|
||||||
"""가사 속 아라비아 숫자를 한글 발음으로 치환 (Suno 전송본 전용).
|
"""가사 속 아라비아 숫자를 한글 발음으로 치환 (Suno 전송본 전용).
|
||||||
|
|
||||||
@ -530,9 +535,21 @@ def _hangulize_plain(text: str) -> str:
|
|||||||
lambda m: f"{_sino(int(m.group(1)))} 대 {_sino(int(m.group(2)))}",
|
lambda m: f"{_sino(int(m.group(1)))} 대 {_sino(int(m.group(2)))}",
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
# 게임·경기 수는 고유어 수사 (7게임 → 일곱 게임)
|
# 아웃 카운트는 야구 관례상 영어 수사 (2아웃→투아웃)
|
||||||
text = re.sub(
|
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)}",
|
lambda m: f"{_native(int(m.group(1)))} {m.group(2)}",
|
||||||
text,
|
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)
|
client = AsyncAnthropic(api_key=settings.anthropic_api_key)
|
||||||
msg = await client.messages.create(
|
msg = await client.messages.create(
|
||||||
model=settings.anthropic_model,
|
model=settings.anthropic_model,
|
||||||
max_tokens=3000,
|
max_tokens=8000,
|
||||||
messages=[{"role": "user", "content": prompt}],
|
messages=[{"role": "user", "content": prompt}],
|
||||||
)
|
)
|
||||||
text = "".join(b.text for b in msg.content if getattr(b, "type", "") == "text")
|
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()
|
lyrics = str(piece.get("lyrics") or "").strip()
|
||||||
if not lyrics:
|
if not lyrics:
|
||||||
raise RuntimeError("LLM 가사 비어있음")
|
raise RuntimeError("LLM 가사 비어있음")
|
||||||
|
if len(lyrics) < 450:
|
||||||
|
raise RuntimeError(f"가사 너무 짧음({len(lyrics)}자) — 재시도")
|
||||||
# 표기용(화면 '가사 보기')은 원문 그대로 저장하고,
|
# 표기용(화면 '가사 보기')은 원문 그대로 저장하고,
|
||||||
# Suno 전송본만 숫자를 한글 발음으로 치환 (13:1 → 십삼 대 일)
|
# Suno 전송본만 숫자를 한글 발음으로 치환 (13:1 → 십삼 대 일)
|
||||||
sung_lyrics = hangulize_numbers(lyrics)
|
sung_lyrics = hangulize_numbers(lyrics)
|
||||||
|
|||||||
@ -30,6 +30,9 @@ import { getTodaySongs, type SongOut } from "./api";
|
|||||||
|
|
||||||
let songsPromise: Promise<SongOut[]> | null = null;
|
let songsPromise: Promise<SongOut[]> | null = null;
|
||||||
let songsDay = "";
|
let songsDay = "";
|
||||||
|
let songsAt = 0;
|
||||||
|
// 곡은 라인업 반영 재생성으로 당일 중에도 교체되므로 짧은 TTL 로 재조회
|
||||||
|
const SONGS_TTL_MS = 5 * 60_000;
|
||||||
|
|
||||||
function todayKstKey(): string {
|
function todayKstKey(): string {
|
||||||
return new Date(Date.now() + 9 * 3600_000).toISOString().slice(0, 10);
|
return new Date(Date.now() + 9 * 3600_000).toISOString().slice(0, 10);
|
||||||
@ -37,9 +40,10 @@ function todayKstKey(): string {
|
|||||||
|
|
||||||
function fetchSongsCached(): Promise<SongOut[]> {
|
function fetchSongsCached(): Promise<SongOut[]> {
|
||||||
const day = todayKstKey();
|
const day = todayKstKey();
|
||||||
if (!songsPromise || songsDay !== day) {
|
if (!songsPromise || songsDay !== day || Date.now() - songsAt > SONGS_TTL_MS) {
|
||||||
songsDay = day;
|
songsDay = day;
|
||||||
songsPromise = getTodaySongs().catch(() => []); // 전 리그(kbo+mlb)
|
songsAt = Date.now();
|
||||||
|
songsPromise = getTodaySongs().catch(() => []); // 전 리그
|
||||||
}
|
}
|
||||||
return songsPromise;
|
return songsPromise;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user