o2o-site-AEO/solution/backend/services/prompts/catchphrase.py
Mina Choi ef0c44c5f2 [feat] solution/backend: 수집 뒤 콘텐츠 채울 때 캐치프레이즈·순환 문구 생성 — 숙박만
가게별 첫 화면 문구를 만드는 곳이 없어 모든 숙박이 공통 문구 10개를 돌려 썼다.

- COPY 잡에 catchphrase 단계(prepare→generate→save→catchphrase→faq_fill), 숙박이 아니면 건너뜀
- prompts/catchphrase.py·gemini_text.generate_catchphrases: 캐치프레이즈 1 + 일반 20·계절 3×4·월 12·날씨 2×4.
  문구마다 길이·상호명·ground_check(근거 없는 시설·수치) 검사
- fact tagline·catchphrases(lodging 스키마, allow_llm). 순환 문구는 사장님이 줄 단위로 고치게 글로 저장
  (catchphrase_text: `봄 | ` · `9월 | ` · `비 | ` 머리)
- site_payload: narrative.tagline·catchphrases 로 넘기고 이용정보 표(facts)에서는 뺀다
- 빌더 생성 화면 단계 문구, DATA_MODEL·GENERATION_FLOW

테스트 6건 추가. 관련 20개 파일 386 passed · 실패 102건은 main 과 목록이 같다(기존 실패)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:33:17 +09:00

85 lines
3.5 KiB
Python

"""숙소 첫 화면 문구 — 고정 캐치프레이즈 하나와 그 아래 줄에서 도는 문구 모음."""
from typing import Optional, Sequence
from services.prompts.copy import FactLike, _fact_lines
SEASONS = ("봄", "여름", "가을", "겨울")
WEATHERS = ("맑음", "흐림", "비", "눈")
COUNT_GENERAL = 20
COUNT_PER_SEASON = 3
COUNT_PER_WEATHER = 2
MIN_LEN = 8
MAX_LEN = 30
RESPONSE_SCHEMA = {
"type": "object",
"properties": {
"tagline": {"type": "string"},
"general": {"type": "array", "items": {"type": "string"}},
"season": {
"type": "array",
"items": {
"type": "object",
"properties": {"season": {"type": "string", "enum": list(SEASONS)}, "text": {"type": "string"}},
"required": ["season", "text"],
},
},
"month": {
"type": "array",
"items": {
"type": "object",
"properties": {"month": {"type": "integer"}, "text": {"type": "string"}},
"required": ["month", "text"],
},
},
"weather": {
"type": "array",
"items": {
"type": "object",
"properties": {"weather": {"type": "string", "enum": list(WEATHERS)}, "text": {"type": "string"}},
"required": ["weather", "text"],
},
},
},
"required": ["tagline", "general", "season", "month", "weather"],
}
def build_prompt(
place_name: str,
facts: Sequence[FactLike],
records: Optional[Sequence[str]] = None,
) -> str:
sections = [
f"숙소 '{place_name}' 홈페이지 첫 화면 문구를 쓴다.",
"첫 화면은 숙소 이름 아래 **고정 캐치프레이즈 한 줄**, 그 아래 줄에서 짧은 문구가 몇 초마다 바뀐다.",
"",
"확인된 사실:",
_fact_lines(facts) if facts else "- 없음",
]
if records:
sections.extend(["", "출처가 있는 기록:", *(f"- {line}" for line in records)])
sections.extend([
"",
"출력:",
f"- tagline: 이 숙소만의 캐치프레이즈 1개, {MIN_LEN}~{MAX_LEN}자. 이 집을 한 줄로 알아보게 하는 문장",
f"- general: 계절·날씨와 상관없는 문구 {COUNT_GENERAL}개",
f"- season: 봄·여름·가을·겨울 각 {COUNT_PER_SEASON}개",
"- month: 1월~12월 각 1개",
f"- weather: 맑음·흐림·비·눈 각 {COUNT_PER_WEATHER}개. 그 날씨에 이 집에서 보이고 들리는 것",
f"- 모든 문구는 공백 포함 {MIN_LEN}~{MAX_LEN}자, 한 문장",
"",
"좋은 예(다른 숙소): '백 년 된 집에서 하룻밤' · '담 너머는 히로쓰 가옥입니다' ·"
" '마당 평상에 앉으면 항구 소리가 들립니다' · '이불에서 볕 냄새가 납니다'",
"",
"규칙:",
"- 위 사실과 기록에 있는 것만 쓴다. 없는 시설·경관·숫자·거리·역사를 지어내지 않는다.",
"- 사실이 적으면 머무는 시간·쉼처럼 확인이 필요 없는 말로 쓴다.",
"- 담백하게 쓴다. '최고', '완벽한', '특별한', '힐링' 같은 홍보 수식어와 느낌표를 쓰지 않는다.",
"- 문구끼리 표현이 겹치지 않게 한다. 상호명은 tagline 에도 넣지 않는다.",
"- false·불가·없음 값을 있는 것처럼 쓰지 않는다.",
"- 존댓말 평서문 또는 명사형으로 끝낸다.",
])
return "\n".join(sections)