/s/stay 시안의 헤더에는 노래 플레이어가 있는데 그건 손으로 채운 목업이라, 새로 발행한 사이트에는 그 자리가 아예 없었다. 이제 발행이 노래를 만든다. ★ 발행이 노래를 기다린다. BUILD 잡이 스냅샷을 뜨기 **전에** 곡을 만든다 — 먼저 굽고 나중에 붙이면 사장님이 [사이트 열기] 로 보는 첫 화면에 그 기능이 빠져 있다. 값은 발행이 30~40초(실측, 상한 5분) 늦어지는 것이고 그건 감수한다. 단 실패는 발행을 막지 않는다 — 기다리는 것과 막는 것은 다르다. 키가 없거나 작곡이 실패하면 노래 없이 발행되고 사유가 빌드 로그와 place_songs.last_error 에 남는다. ★ 가사를 우리가 쓴다. Suno 에 주제만 던지면 가사를 저쪽이 짓고, 거기엔 이 숙소에 없는 것(수영장·조식)이 섞이는데 검증할 방법이 없다 — 다른 모든 문장은 확인된 fact 로만 쓰면서 노래만 지어낸 말을 싣는 꼴이다. 소개문과 **같은 재료**로 Gemini 가 쓰고 Suno 는 곡만 붙인다. 가사에 ground_check 는 걸지 않는다(정서는 fact 로 대응되지 않는다). 대신 프롬프트가 없는 시설·숫자를 말하지 말라고 못 박는다 — 요금을 노래에 넣으면 틀렸을 때 고쳐 부를 수 없다. ★ Suno 주소는 만료된다. 그 주소를 payload 에 실으면 발행 직후엔 재생되고 몇 주 뒤 조용히 죽는다. mp3 를 받아 보관하고 우리 경로(/s/<slug>/<song_id>.mp3)만 내보낸다. ★ 콜백이 아니라 폴링이다. 우리 백엔드는 Suno 가 닿을 수 있는 주소가 아니라, 콜백을 믿으면 "요청은 성공했는데 결과가 영영 안 옴" 이 된다. - services/external/suno.py: 작곡 요청 + record-info 폴링(10초 간격·상한 5분) + 내려받기 - services/external/gemini_text.generate_song · prompts/song.py: 가사·제목·장르 - services/song_service.py: 재료 → 가사 → 작곡 → 파일 보관. ensure_song 을 빌드가 부른다 - build_service: publish 일 때만 ensure_song 을 먼저 부르고 그 뒤 스냅샷(미리보기는 안 만든다 — 유료) - place_songs 표 신설(init.sql + 0010 마이그레이션 + ORM). 검증 상태가 없다 — 수집한 사실이 아니라 창작물이라 "맞는가" 가 아니라 "만들어졌는가" 만 묻는다(SongStatus) - snapshot·site_payload·shared: READY 인 최신 한 곡만 싣는다. audioUrl 은 우리 경로다 - prerender: songs/ 의 파일을 사이트 디렉토리로 복사하고 **지난 발행의 곡은 치운다** (발행마다 새 곡이라 안 치우면 1MB 짜리가 쌓이고 블롭에도 그대로 올라간다) - site/SongPlayer: 헤더의 작은 플레이어. 자동 재생하지 않고, 곡이 없으면 아무것도 안 그린다. 패널은 hidden 으로 여닫는다 — 조건부 렌더면 닫힌 동안 제목·가사가 DOM 에 없어 크롤러가 못 읽는다(오디오 안의 말은 어차피 못 듣는다) - azure_static: .mp3 content-type 과 immutable 캐시. 블롭 업로드는 발행이 사이트째 한다 — 업로더를 하나 더 두면 같은 컨테이너에 경로·캐시·정리 규칙이 두 벌 생긴다 - compose: solution/site/songs 볼륨. .env.example 에 SUNO_API_KEY·SUNO_CALLBACK_URL 검증: 실제 발행(스테이,머뭄 v15) — 가사 154자 $0.0014 → 작곡 40초 → 1.98MB → 스냅샷(노래 1) → 발행 완료. /s/스테이머뭄-99a887f8 200, mp3 200 audio/mpeg, HTML 에 제목·가사·주소 확인, 지난 곡 404. tsc --noEmit · eslint · vitest 55 passed(신규 4) · 백엔드 관련 188 passed (실패 5건은 전부 컨테이너 환경 유입 — 프론트 소스 부재·SITE_PUBLIC_HOST) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
63 lines
3.5 KiB
Python
63 lines
3.5 KiB
Python
"""이 숙소의 노래 — 가사 프롬프트와 응답 스키마.
|
|
|
|
★ 왜 가사를 **우리가** 쓰고 Suno 에는 작곡만 시키나
|
|
Suno 에 "군산 한옥 숙소 노래" 라고만 던지면 가사를 저쪽이 지어낸다. 그 가사에는 이 숙소에
|
|
없는 것(수영장·조식·오션뷰)이 섞이고, 우리는 그걸 검증할 방법이 없다 — 사이트의 다른
|
|
모든 문장은 확인된 fact 로만 쓰는데 노래만 지어낸 말을 싣는 꼴이 된다.
|
|
그래서 가사는 소개문과 **같은 재료**(확인된 fact + 소개문)로 여기서 쓰고, Suno 는 그 가사에
|
|
곡을 붙이기만 한다.
|
|
|
|
★ 그래도 가사는 사실 진술이 아니다
|
|
"밤이 깊어도 불이 켜져 있다" 같은 줄은 fact 가 아니라 분위기다. 그래서 `ground_check` 를
|
|
걸지 않는다 — 대신 프롬프트가 **없는 시설·없는 숫자를 말하지 말라**고 못 박는다.
|
|
요금·전화번호·주소를 가사에 넣지 않는 것도 같은 이유다(틀리면 예약 클레임이고, 노래는
|
|
고쳐 부르기도 어렵다).
|
|
"""
|
|
|
|
# Suno 가 받는 style 문자열. 장르를 모델이 고르게 두되 후보를 좁힌다 —
|
|
# 열어 두면 숙소 사이트에 어울리지 않는 것(하드록·트랩)이 나온다.
|
|
STYLE_CHOICES = [
|
|
"acoustic ballad", "city pop", "folk pop", "lo-fi", "bossa nova", "soft rock", "jazz",
|
|
]
|
|
|
|
RESPONSE_SCHEMA = {
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {"type": "string", "description": "곡 제목. 상호를 그대로 쓰지 말고 한 구절로."},
|
|
"lyrics": {"type": "string", "description": "가사. [Verse]/[Chorus] 구조 태그를 포함한다."},
|
|
"style": {"type": "string", "description": f"장르·분위기. 다음 중 하나에서 시작한다: {', '.join(STYLE_CHOICES)}"},
|
|
},
|
|
"required": ["title", "lyrics", "style"],
|
|
}
|
|
|
|
|
|
def build_prompt(place_name: str, category_label: str, region: str, grounding: list[str], intro: str) -> str:
|
|
"""가사 프롬프트. 재료는 소개문 생성과 같은 것을 받는다."""
|
|
material = "\n".join(f"- {line}" for line in grounding) or "- (확인된 항목 없음)"
|
|
intro_block = f"\n[이 숙소 소개문]\n{intro.strip()}\n" if (intro or "").strip() else ""
|
|
|
|
return f"""너는 작은 가게의 노래를 쓰는 작사가다. 아래 업소의 노래 가사를 쓴다.
|
|
|
|
[업소]
|
|
- 상호: {place_name}
|
|
- 업종: {category_label}
|
|
- 지역: {region or "(모름)"}
|
|
|
|
[확인된 항목 — 이 안에서만 말한다]
|
|
{material}
|
|
{intro_block}
|
|
[쓰는 법]
|
|
- 한국어. 40초 안에 불리는 길이다 — [Verse] 한 덩이 + [Chorus] 한 덩이면 충분하다.
|
|
- 구조 태그([Verse], [Chorus])를 반드시 넣는다. Suno 가 이 태그로 곡을 나눈다.
|
|
- **없는 것을 말하지 않는다.** 위 목록에 없는 시설·풍경·서비스를 지어내지 않는다.
|
|
지역과 계절, 머무는 마음처럼 사실 확인이 필요 없는 정서는 자유롭게 쓴다.
|
|
- **숫자를 넣지 않는다.** 요금·전화번호·주소·객실 수는 가사에 쓰지 않는다.
|
|
틀리면 손님이 손해를 보고, 노래는 고쳐 부르기 어렵다.
|
|
- 광고 문구처럼 쓰지 않는다("최고", "1등", "예약하세요"). 손님이 흥얼거릴 노래다.
|
|
- 상호는 후렴에 한 번쯤 자연스럽게 넣는다.
|
|
|
|
[출력]
|
|
title(곡 제목) · lyrics(가사) · style(장르·분위기) 를 JSON 으로 준다.
|
|
style 은 이 업소의 분위기에 맞는 것을 고른다: {', '.join(STYLE_CHOICES)}.
|
|
"""
|