Merge branch 'main' into insta
commit
f73be9c6d0
|
|
@ -46,3 +46,7 @@ logs/
|
|||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
*.yml
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
|
@ -16,15 +16,18 @@ from app.user.dependencies.auth import get_current_user
|
|||
from app.user.models import User
|
||||
from app.home.schemas.home_schema import (
|
||||
AutoCompleteRequest,
|
||||
AccommodationSearchItem,
|
||||
AccommodationSearchResponse,
|
||||
CrawlingRequest,
|
||||
CrawlingResponse,
|
||||
ErrorResponse,
|
||||
ImageUploadResponse,
|
||||
ImageUploadResultItem,
|
||||
ImageUrlItem,
|
||||
MarketingAnalysis,
|
||||
ProcessedInfo,
|
||||
# MarketingAnalysis,
|
||||
)
|
||||
from app.home.services.naver_search import naver_search_client
|
||||
from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||
from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError
|
||||
from app.utils.common import generate_task_id
|
||||
|
|
@ -70,6 +73,47 @@ KOREAN_CITIES = [
|
|||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/search/accommodation",
|
||||
summary="숙박/펜션 자동완성 검색",
|
||||
description="""
|
||||
네이버 지역 검색 API를 이용한 숙박/펜션 자동완성 검색입니다.
|
||||
|
||||
## 요청 파라미터
|
||||
- **query**: 검색어 (필수)
|
||||
|
||||
## 반환 정보
|
||||
- **query**: 검색어
|
||||
- **count**: 검색 결과 수 (최대 10개)
|
||||
- **items**: 검색 결과 목록
|
||||
- **title**: 숙소명 (HTML 태그 포함 가능)
|
||||
- **address**: 지번 주소
|
||||
- **roadAddress**: 도로명 주소
|
||||
""",
|
||||
response_model=AccommodationSearchResponse,
|
||||
responses={
|
||||
200: {"description": "검색 성공", "model": AccommodationSearchResponse},
|
||||
},
|
||||
tags=["Search"],
|
||||
)
|
||||
async def search_accommodation(
|
||||
query: str,
|
||||
) -> AccommodationSearchResponse:
|
||||
"""숙박/펜션 자동완성 검색"""
|
||||
results = await naver_search_client.search_accommodation(
|
||||
query=query,
|
||||
display=10,
|
||||
)
|
||||
|
||||
items = [AccommodationSearchItem(**item) for item in results]
|
||||
|
||||
return AccommodationSearchResponse(
|
||||
query=query,
|
||||
count=len(items),
|
||||
items=items,
|
||||
)
|
||||
|
||||
|
||||
def _extract_region_from_address(road_address: str | None) -> str:
|
||||
"""roadAddress에서 시 이름 추출"""
|
||||
if not road_address:
|
||||
|
|
@ -259,33 +303,11 @@ async def _crawling_logic(url:str):
|
|||
# marketing_analysis = MarketingAnalysis(**parsed)
|
||||
|
||||
logger.debug(
|
||||
f"[crawling] structured_report 구조 확인:\n"
|
||||
f"{'='*60}\n"
|
||||
f"[report] type: {type(structured_report.get('report'))}\n"
|
||||
f"{'-'*60}\n"
|
||||
f"{structured_report.get('report')}\n"
|
||||
f"{'='*60}\n"
|
||||
f"[tags] type: {type(structured_report.get('tags'))}\n"
|
||||
f"{'-'*60}\n"
|
||||
f"{structured_report.get('tags')}\n"
|
||||
f"{'='*60}\n"
|
||||
f"[selling_points] type: {type(structured_report.get('selling_points'))}\n"
|
||||
f"{'-'*60}\n"
|
||||
f"{structured_report.get('selling_points')}\n"
|
||||
f"{'='*60}"
|
||||
f"structured_report = {structured_report.model_dump()}"
|
||||
)
|
||||
|
||||
marketing_analysis = MarketingAnalysis(
|
||||
report=structured_report["report"],
|
||||
tags=structured_report["tags"],
|
||||
facilities=list(
|
||||
[sp["keywords"] for sp in structured_report["selling_points"]]
|
||||
), # [json.dumps(structured_report["selling_points"])] # 나중에 Selling Points로 변수와 데이터구조 변경할 것
|
||||
)
|
||||
# Selling Points 구조
|
||||
# print(sp['category'])
|
||||
# print(sp['keywords'])
|
||||
# print(sp['description'])
|
||||
marketing_analysis = structured_report
|
||||
|
||||
step3_4_elapsed = (time.perf_counter() - step3_4_start) * 1000
|
||||
logger.debug(
|
||||
f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)"
|
||||
|
|
@ -356,6 +378,17 @@ async def _autocomplete_logic(autocomplete_item:dict):
|
|||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="자동완성 place id 추출 실패",
|
||||
)
|
||||
|
||||
if not new_url:
|
||||
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||
logger.error(
|
||||
f"[crawling] Autocomplete FAILED - URL을 찾을 수 없음 ({step1_elapsed:.1f}ms)"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="해당 장소의 네이버 지도 URL을 찾을 수 없습니다.",
|
||||
)
|
||||
|
||||
return new_url
|
||||
|
||||
def _extract_image_name(url: str, index: int) -> str:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.utils.prompts.schemas import MarketingPromptOutput
|
||||
|
||||
class AttributeInfo(BaseModel):
|
||||
"""음악 속성 정보"""
|
||||
|
|
@ -139,6 +139,45 @@ class AutoCompleteRequest(BaseModel):
|
|||
address: str = Field(..., description="네이버 검색 place API 지번주소")
|
||||
roadAddress: Optional[str] = Field(None, description="네이버 검색 place API 도로명주소")
|
||||
|
||||
|
||||
class AccommodationSearchItem(BaseModel):
|
||||
"""숙박 검색 결과 아이템"""
|
||||
|
||||
title: str = Field(..., description="숙소명 (HTML 태그 포함 가능)")
|
||||
address: str = Field(..., description="지번 주소")
|
||||
roadAddress: str = Field(default="", description="도로명 주소")
|
||||
|
||||
|
||||
class AccommodationSearchResponse(BaseModel):
|
||||
"""숙박 자동완성 검색 응답"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"query": "스테이 머뭄",
|
||||
"count": 2,
|
||||
"items": [
|
||||
{
|
||||
"title": "<b>스테이</b>,<b>머뭄</b>",
|
||||
"address": "전북특별자치도 군산시 신흥동 63-18",
|
||||
"roadAddress": "전북특별자치도 군산시 절골길 18",
|
||||
},
|
||||
{
|
||||
"title": "머뭄<b>스테이</b>",
|
||||
"address": "전북특별자치도 군산시 비응도동 123",
|
||||
"roadAddress": "전북특별자치도 군산시 비응로 456",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
query: str = Field(..., description="검색어")
|
||||
count: int = Field(..., description="검색 결과 수")
|
||||
items: list[AccommodationSearchItem] = Field(
|
||||
default_factory=list, description="검색 결과 목록"
|
||||
)
|
||||
|
||||
class ProcessedInfo(BaseModel):
|
||||
"""가공된 장소 정보 스키마"""
|
||||
|
||||
|
|
@ -147,12 +186,21 @@ class ProcessedInfo(BaseModel):
|
|||
detail_region_info: str = Field(..., description="상세 지역 정보 (roadAddress)")
|
||||
|
||||
|
||||
class MarketingAnalysis(BaseModel):
|
||||
"""마케팅 분석 결과 스키마"""
|
||||
# class MarketingAnalysisDetail(BaseModel):
|
||||
# detail_title : str = Field(..., description="디테일 카테고리 이름")
|
||||
# detail_description : str = Field(..., description="해당 항목 설명")
|
||||
|
||||
report: str = Field(..., description="마케팅 분석 리포트")
|
||||
tags: list[str] = Field(default_factory=list, description="추천 태그 목록")
|
||||
facilities: list[str] = Field(default_factory=list, description="추천 부대시설 목록")
|
||||
# class MarketingAnalysisReport(BaseModel):
|
||||
# """마케팅 분석 리포트 스키마"""
|
||||
# summary : str = Field(..., description="비즈니스 한 줄 요약")
|
||||
# details : list[MarketingAnalysisDetail] = Field(default_factory=list, description="개별 디테일")
|
||||
|
||||
# class MarketingAnalysis(BaseModel):
|
||||
# """마케팅 분석 결과 스키마"""
|
||||
|
||||
# # report: MarketingAnalysisReport = Field(..., description="마케팅 분석 리포트")
|
||||
# tags: list[str] = Field(default_factory=list, description="추천 태그 목록")
|
||||
# selling_points: list[str] = Field(default_factory=list, description="추천 부대시설 목록")
|
||||
|
||||
|
||||
class CrawlingResponse(BaseModel):
|
||||
|
|
@ -170,9 +218,110 @@ class CrawlingResponse(BaseModel):
|
|||
"detail_region_info": "전북특별자치도 군산시 절골길 18"
|
||||
},
|
||||
"marketing_analysis": {
|
||||
"report": "마케팅 분석 리포트...",
|
||||
"tags": ["힐링", "감성숙소"],
|
||||
"facilities": ["조식", "주차"]
|
||||
"brand_identity": {
|
||||
"location_feature_analysis": "전북 군산시 절골길 일대는 도시의 편의성과 근교의 한적함을 동시에 누릴 수 있어 ‘조용한 재충전’ 수요에 적합합니다. 군산의 레트로 감성과 주변 관광 동선 결합이 쉬워 1~2박 체류형 여행지로 매력적입니다.",
|
||||
"concept_scalability": "‘머뭄’이라는 네이밍을 ‘잠시 멈춰 머무는 시간’으로 확장해, 느린 체크인·명상/독서 큐레이션·로컬 티/다과 등 체류 경험형 서비스로 고도화가 가능합니다. 로컬 콘텐츠(군산 빵/커피, 근대문화 투어)와 결합해 패키지화하면 재방문 명분을 만들 수 있습니다."
|
||||
},
|
||||
"market_positioning": {
|
||||
"category_definition": "군산 감성 ‘슬로우 스테이’ 프라이빗 숙소",
|
||||
"core_value": "아무것도 하지 않아도 회복되는 ‘멈춤의 시간’"
|
||||
},
|
||||
"target_persona": [
|
||||
{
|
||||
"persona": "번아웃 회복형 직장인 커플: 주말에 조용히 쉬며 리셋을 원하는 2인 여행자",
|
||||
"age": {
|
||||
"min_age": 27,
|
||||
"max_age": 39
|
||||
},
|
||||
"favor_target": [
|
||||
"조용한 동네 분위기",
|
||||
"미니멀/내추럴 인테리어",
|
||||
"편안한 침구와 숙면 환경",
|
||||
"셀프 체크인 선호",
|
||||
"카페·맛집 연계 동선"
|
||||
],
|
||||
"decision_trigger": "‘조용히 쉬는 데 최적화’된 프라이빗함과 숙면 컨디션(침구/동선/소음 차단) 확신"
|
||||
},
|
||||
{
|
||||
"persona": "감성 기록형 친구 여행: 사진과 무드를 위해 공간을 선택하는 2~3인 여성 그룹",
|
||||
"age": {
|
||||
"min_age": 23,
|
||||
"max_age": 34
|
||||
},
|
||||
"favor_target": [
|
||||
"자연광 좋은 공간",
|
||||
"감성 소품/컬러 톤",
|
||||
"포토존(거울/창가/테이블)",
|
||||
"와인·디저트 페어링",
|
||||
"야간 무드 조명"
|
||||
],
|
||||
"decision_trigger": "사진이 ‘그대로 작품’이 되는 포토 스팟과 야간 무드 연출 요소"
|
||||
},
|
||||
{
|
||||
"persona": "로컬 탐험형 소도시 여행자: 군산의 레트로/로컬 콘텐츠를 깊게 즐기는 커플·솔로",
|
||||
"age": {
|
||||
"min_age": 28,
|
||||
"max_age": 45
|
||||
},
|
||||
"favor_target": [
|
||||
"근대문화/레트로 감성",
|
||||
"로컬 맛집·빵집 투어",
|
||||
"동선 효율(차로 이동 용이)",
|
||||
"체크아웃 후 관광 연계",
|
||||
"조용한 밤"
|
||||
],
|
||||
"decision_trigger": "‘군산 로컬 동선’과 결합하기 좋은 위치 + 숙소 자체의 휴식 완성도"
|
||||
}
|
||||
],
|
||||
"selling_points": [
|
||||
{
|
||||
"category": "LOCATION",
|
||||
"description": "군산 감성 동선",
|
||||
"score": 88
|
||||
},
|
||||
{
|
||||
"category": "HEALING",
|
||||
"description": "멈춤이 되는 쉼",
|
||||
"score": 92
|
||||
},
|
||||
{
|
||||
"category": "PRIVACY",
|
||||
"description": "방해 없는 머뭄",
|
||||
"score": 86
|
||||
},
|
||||
{
|
||||
"category": "NIGHT MOOD",
|
||||
"description": "밤이 예쁜 조명",
|
||||
"score": 84
|
||||
},
|
||||
{
|
||||
"category": "PHOTO SPOT",
|
||||
"description": "자연광 포토존",
|
||||
"score": 83
|
||||
},
|
||||
{
|
||||
"category": "SHORT GETAWAY",
|
||||
"description": "주말 리셋 스테이",
|
||||
"score": 89
|
||||
},
|
||||
{
|
||||
"category": "HOSPITALITY",
|
||||
"description": "세심한 웰컴감",
|
||||
"score": 80
|
||||
}
|
||||
],
|
||||
"target_keywords": [
|
||||
"군산숙소",
|
||||
"군산감성숙소",
|
||||
"전북숙소추천",
|
||||
"군산여행",
|
||||
"커플스테이",
|
||||
"주말여행",
|
||||
"감성스테이",
|
||||
"조용한숙소",
|
||||
"힐링스테이",
|
||||
"스테이머뭄"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -187,8 +336,8 @@ class CrawlingResponse(BaseModel):
|
|||
processed_info: Optional[ProcessedInfo] = Field(
|
||||
None, description="가공된 장소 정보 (customer_name, region, detail_region_info)"
|
||||
)
|
||||
marketing_analysis: Optional[MarketingAnalysis] = Field(
|
||||
None, description="마케팅 분석 결과 (report, tags, facilities). 실패 시 null"
|
||||
marketing_analysis: Optional[MarketingPromptOutput] = Field(
|
||||
None, description="마케팅 분석 결과 . 실패 시 null"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
"""
|
||||
네이버 지역 검색 API 클라이언트
|
||||
|
||||
숙박/펜션 자동완성 검색 기능을 제공합니다.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
import aiohttp
|
||||
|
||||
from config import naver_api_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NaverSearchClient:
|
||||
"""
|
||||
네이버 지역 검색 API 클라이언트
|
||||
|
||||
숙박/펜션 카테고리 검색을 위한 클라이언트입니다.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.client_id = naver_api_settings.NAVER_CLIENT_ID
|
||||
self.client_secret = naver_api_settings.NAVER_CLIENT_SECRET
|
||||
self.api_url = naver_api_settings.NAVER_LOCAL_API_URL
|
||||
|
||||
async def search_accommodation(
|
||||
self,
|
||||
query: str,
|
||||
display: int = 5,
|
||||
) -> List[dict]:
|
||||
"""
|
||||
숙박/펜션 검색
|
||||
|
||||
Args:
|
||||
query: 검색어
|
||||
display: 결과 개수 (기본 5개)
|
||||
|
||||
Returns:
|
||||
검색 결과 리스트 (address, roadAddress, title)
|
||||
"""
|
||||
# 숙박/펜션 카테고리 검색을 위해 쿼리에 키워드 추가
|
||||
search_query = f"{query} 숙박"
|
||||
|
||||
headers = {
|
||||
"X-Naver-Client-Id": self.client_id,
|
||||
"X-Naver-Client-Secret": self.client_secret,
|
||||
}
|
||||
|
||||
params = {
|
||||
"query": search_query,
|
||||
"display": display,
|
||||
"sort": "random", # 정확도순
|
||||
}
|
||||
|
||||
logger.info(f"[NAVER] 지역 검색 요청 - query: {search_query}")
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
self.api_url,
|
||||
headers=headers,
|
||||
params=params,
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
logger.error(
|
||||
f"[NAVER] API 오류 - status: {response.status}, error: {error_text}"
|
||||
)
|
||||
return []
|
||||
|
||||
data = await response.json()
|
||||
items = data.get("items", [])
|
||||
|
||||
# 필요한 필드만 추출
|
||||
results = [
|
||||
{
|
||||
"address": item.get("address", ""),
|
||||
"roadAddress": item.get("roadAddress", ""),
|
||||
"title": item.get("title", ""),
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
logger.info(f"[NAVER] 검색 완료 - 결과 수: {len(results)}")
|
||||
return results
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"[NAVER] 네트워크 오류 - {str(e)}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"[NAVER] 예기치 않은 오류 - {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
# 싱글톤 인스턴스
|
||||
naver_search_client = NaverSearchClient()
|
||||
|
|
@ -108,7 +108,7 @@ async def generate_lyric_background(
|
|||
|
||||
#result = await service.generate(prompt=prompt)
|
||||
result_response = await chatgpt.generate_structured_output(prompt, lyric_input_data)
|
||||
result = result_response['lyric']
|
||||
result = result_response.lyric
|
||||
step2_elapsed = (time.perf_counter() - step2_start) * 1000
|
||||
logger.info(f"[generate_lyric_background] Step 2 완료 - 응답 {len(result)}자 ({step2_elapsed:.1f}ms)")
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select, update
|
||||
|
|
@ -113,7 +113,7 @@ class AuthService:
|
|||
logger.debug(f"[AUTH] 리프레시 토큰 저장 완료 - user_id: {user.id}, user_uuid: {user.user_uuid}")
|
||||
|
||||
# 7. 마지막 로그인 시간 업데이트
|
||||
user.last_login_at = datetime.now(timezone.utc)
|
||||
user.last_login_at = datetime.now()
|
||||
await session.commit()
|
||||
|
||||
redirect_url = f"{prj_settings.PROJECT_DOMAIN}"
|
||||
|
|
@ -168,7 +168,7 @@ class AuthService:
|
|||
if db_token.is_revoked:
|
||||
raise TokenRevokedError()
|
||||
|
||||
if db_token.expires_at < datetime.now(timezone.utc):
|
||||
if db_token.expires_at < datetime.now():
|
||||
raise TokenExpiredError()
|
||||
|
||||
# 4. 사용자 확인
|
||||
|
|
@ -428,7 +428,7 @@ class AuthService:
|
|||
.where(RefreshToken.token_hash == token_hash)
|
||||
.values(
|
||||
is_revoked=True,
|
||||
revoked_at=datetime.now(timezone.utc),
|
||||
revoked_at=datetime.now(),
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
|
@ -453,7 +453,7 @@ class AuthService:
|
|||
)
|
||||
.values(
|
||||
is_revoked=True,
|
||||
revoked_at=datetime.now(timezone.utc),
|
||||
revoked_at=datetime.now(),
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Access Token과 Refresh Token의 생성, 검증, 해시 기능을 제공합니
|
|||
"""
|
||||
|
||||
import hashlib
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from jose import JWTError, jwt
|
||||
|
|
@ -23,7 +23,7 @@ def create_access_token(user_uuid: str) -> str:
|
|||
Returns:
|
||||
JWT 액세스 토큰 문자열
|
||||
"""
|
||||
expire = datetime.now(timezone.utc) + timedelta(
|
||||
expire = datetime.now() + timedelta(
|
||||
minutes=jwt_settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode = {
|
||||
|
|
@ -48,7 +48,7 @@ def create_refresh_token(user_uuid: str) -> str:
|
|||
Returns:
|
||||
JWT 리프레시 토큰 문자열
|
||||
"""
|
||||
expire = datetime.now(timezone.utc) + timedelta(
|
||||
expire = datetime.now() + timedelta(
|
||||
days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
||||
)
|
||||
to_encode = {
|
||||
|
|
@ -104,9 +104,9 @@ def get_refresh_token_expires_at() -> datetime:
|
|||
리프레시 토큰 만료 시간 계산
|
||||
|
||||
Returns:
|
||||
리프레시 토큰 만료 datetime (UTC)
|
||||
리프레시 토큰 만료 datetime (로컬 시간)
|
||||
"""
|
||||
return datetime.now(timezone.utc) + timedelta(
|
||||
return datetime.now() + timedelta(
|
||||
days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import json
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from config import apikey_settings, recovery_settings
|
||||
from app.utils.prompts.prompts import Prompt
|
||||
|
||||
|
||||
# 로거 설정
|
||||
logger = get_logger("chatgpt")
|
||||
|
||||
|
|
@ -32,18 +33,15 @@ class ChatgptService:
|
|||
timeout=self.timeout
|
||||
)
|
||||
|
||||
async def _call_structured_output_with_response_gpt_api(self, prompt: str, output_format: dict, model: str) -> dict:
|
||||
async def _call_pydantic_output(self, prompt : str, output_format : BaseModel, model : str) -> BaseModel: # 입력 output_format의 경우 Pydantic BaseModel Class를 상속한 Class 자체임에 유의할 것
|
||||
content = [{"type": "input_text", "text": prompt}]
|
||||
|
||||
last_error = None
|
||||
for attempt in range(self.max_retries + 1):
|
||||
response = await self.client.responses.create(
|
||||
response = await self.client.responses.parse(
|
||||
model=model,
|
||||
input=[{"role": "user", "content": content}],
|
||||
text=output_format,
|
||||
timeout=self.timeout
|
||||
text_format=output_format
|
||||
)
|
||||
|
||||
# Response 디버그 로깅
|
||||
logger.debug(f"[ChatgptService] Response ID: {response.id}")
|
||||
logger.debug(f"[ChatgptService] Response status: {response.status}")
|
||||
|
|
@ -52,8 +50,8 @@ class ChatgptService:
|
|||
# status 확인: completed, failed, incomplete, cancelled, queued, in_progress
|
||||
if response.status == "completed":
|
||||
logger.debug(f"[ChatgptService] Response output_text: {response.output_text[:200]}..." if len(response.output_text) > 200 else f"[ChatgptService] Response output_text: {response.output_text}")
|
||||
structured_output = json.loads(response.output_text)
|
||||
return structured_output or {}
|
||||
structured_output = response.output_parsed
|
||||
return structured_output #.model_dump() or {}
|
||||
|
||||
# 에러 상태 처리
|
||||
if response.status == "failed":
|
||||
|
|
@ -91,5 +89,6 @@ class ChatgptService:
|
|||
logger.info(f"[ChatgptService] Starting GPT request with structured output with model: {prompt.prompt_model}")
|
||||
|
||||
# GPT API 호출
|
||||
response = await self._call_structured_output_with_response_gpt_api(prompt_text, prompt.prompt_output, prompt.prompt_model)
|
||||
#response = await self._call_structured_output_with_response_gpt_api(prompt_text, prompt.prompt_output, prompt.prompt_model)
|
||||
response = await self._call_pydantic_output(prompt_text, prompt.prompt_output_class, prompt.prompt_model)
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"model": "gpt-5-mini",
|
||||
"prompt_variables": [
|
||||
"customer_name",
|
||||
"region",
|
||||
"detail_region_info",
|
||||
"marketing_intelligence_summary",
|
||||
"language",
|
||||
"promotional_expression_example",
|
||||
"timing_rules"
|
||||
],
|
||||
"output_format": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "lyric",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lyric": {
|
||||
"type": "string"
|
||||
},
|
||||
"suno_prompt":{
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lyric", "suno_prompt"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
{
|
||||
"model": "gpt-5-mini",
|
||||
"prompt_variables": [
|
||||
"customer_name",
|
||||
"region",
|
||||
"detail_region_info"
|
||||
],
|
||||
"output_format": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "report",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"report": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"summary": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"detail_description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"detail_title",
|
||||
"detail_description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"summary",
|
||||
"details"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"selling_points": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"keywords": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"category",
|
||||
"keywords",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"contents_advise": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"report",
|
||||
"selling_points",
|
||||
"tags",
|
||||
"contents_advise"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
|
||||
[Role & Objective]
|
||||
Act as a content marketing expert with strong domain knowledge in the Korean pension / stay-accommodation industry.
|
||||
Your goal is to produce a Marketing Intelligence Report that will be shown to accommodation owners BEFORE any content is generated.
|
||||
The report must clearly explain what makes the property sellable, marketable, and scalable through content.
|
||||
|
||||
[INPUT]
|
||||
- Business Name: {customer_name}
|
||||
- Region: {region}
|
||||
- Region Details: {detail_region_info}
|
||||
|
||||
[Core Analysis Requirements]
|
||||
Analyze the property based on:
|
||||
Location, concept, photos, online presence, and nearby environment
|
||||
Target customer behavior and reservation decision factors
|
||||
Include:
|
||||
- Target customer segments & personas
|
||||
- Unique Selling Propositions (USPs)
|
||||
- Competitive landscape (direct & indirect competitors)
|
||||
- Market positioning
|
||||
|
||||
[Key Selling Point Structuring – UI Optimized]
|
||||
From the analysis above, extract the main Key Selling Points using the structure below.
|
||||
Rules:
|
||||
Focus only on factors that directly influence booking decisions
|
||||
Each selling point must be concise and visually scannable
|
||||
Language must be reusable for ads, short-form videos, and listing headlines
|
||||
Avoid full sentences in descriptions; use short selling phrases
|
||||
|
||||
Output format:
|
||||
[Category]
|
||||
(Tag keyword – 5~8 words, noun-based, UI oval-style)
|
||||
One-line selling phrase (not a full sentence)
|
||||
Limit:
|
||||
5 to 8 Key Selling Points only
|
||||
|
||||
[Content & Automation Readiness Check]
|
||||
Ensure that:
|
||||
Each tag keyword can directly map to a content theme
|
||||
Each selling phrase can be used as:
|
||||
- Video hook
|
||||
- Image headline
|
||||
- Ad copy snippet
|
||||
|
||||
|
||||
[Tag Generation Rules]
|
||||
- Tags must include **only core keywords that can be directly used for viral video song lyrics**
|
||||
- Each tag should be selected with **search discovery + emotional resonance + reservation conversion** in mind
|
||||
- The number of tags must be **exactly 5**
|
||||
- Tags must be **nouns or short keyword phrases**; full sentences are strictly prohibited
|
||||
- The following categories must be **balanced and all represented**:
|
||||
1) **Location / Local context** (region name, neighborhood, travel context)
|
||||
2) **Accommodation positioning** (emotional stay, private stay, boutique stay, etc.)
|
||||
3) **Emotion / Experience** (healing, rest, one-day escape, memory, etc.)
|
||||
4) **SNS / Viral signals** (Instagram vibes, picture-perfect day, aesthetic travel, etc.)
|
||||
5) **Travel & booking intent** (travel, getaway, stay, relaxation, etc.)
|
||||
|
||||
- If a brand name exists, **at least one tag must include the brand name or a brand-specific expression**
|
||||
- Avoid overly generic keywords (e.g., “hotel”, “travel” alone); **prioritize distinctive, differentiating phrases**
|
||||
- The final output must strictly follow the JSON format below, with no additional text
|
||||
|
||||
"tags": ["Tag1", "Tag2", "Tag3", "Tag4", "Tag5"]
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
{
|
||||
"model": "gpt-5.2",
|
||||
"prompt_variables": [
|
||||
"customer_name",
|
||||
"region",
|
||||
"detail_region_info"
|
||||
],
|
||||
"output_format": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "report",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"report": {
|
||||
"type": "string"
|
||||
},
|
||||
"selling_points": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"keywords": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"category",
|
||||
"keywords",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"report",
|
||||
"selling_points",
|
||||
"tags"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import os, json
|
||||
from abc import ABCMeta
|
||||
from config import prompt_settings
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger("prompt")
|
||||
|
||||
class Prompt():
|
||||
prompt_name : str # ex) marketing_prompt
|
||||
prompt_template_path : str #프롬프트 경로
|
||||
prompt_template : str # fstring 포맷
|
||||
prompt_input : list
|
||||
prompt_output : dict
|
||||
prompt_model : str
|
||||
|
||||
def __init__(self, prompt_name, prompt_template_path):
|
||||
self.prompt_name = prompt_name
|
||||
self.prompt_template_path = prompt_template_path
|
||||
self.prompt_template, prompt_dict = self.read_prompt()
|
||||
self.prompt_input = prompt_dict['prompt_variables']
|
||||
self.prompt_output = prompt_dict['output_format']
|
||||
self.prompt_model = prompt_dict.get('model', "gpt-5-mini")
|
||||
|
||||
def _reload_prompt(self):
|
||||
self.prompt_template, prompt_dict = self.read_prompt()
|
||||
self.prompt_input = prompt_dict['prompt_variables']
|
||||
self.prompt_output = prompt_dict['output_format']
|
||||
self.prompt_model = prompt_dict.get('model', "gpt-5-mini")
|
||||
|
||||
def read_prompt(self) -> tuple[str, dict]:
|
||||
template_text_path = self.prompt_template_path + ".txt"
|
||||
prompt_dict_path = self.prompt_template_path + ".json"
|
||||
with open(template_text_path, "r") as fp:
|
||||
prompt_template = fp.read()
|
||||
with open(prompt_dict_path, "r") as fp:
|
||||
prompt_dict = json.load(fp)
|
||||
|
||||
return prompt_template, prompt_dict
|
||||
|
||||
def build_prompt(self, input_data:dict) -> str:
|
||||
self.check_input(input_data)
|
||||
build_template = self.prompt_template
|
||||
logger.debug(f"build_template: {build_template}")
|
||||
logger.debug(f"input_data: {input_data}")
|
||||
build_template = build_template.format(**input_data)
|
||||
return build_template
|
||||
|
||||
def check_input(self, input_data:dict) -> bool:
|
||||
missing_variables = input_data.keys() - set(self.prompt_input)
|
||||
if missing_variables:
|
||||
raise Exception(f"missing_variable for prompt {self.prompt_name} : {missing_variables}")
|
||||
|
||||
flooding_variables = set(self.prompt_input) - input_data.keys()
|
||||
if flooding_variables:
|
||||
raise Exception(f"flooding_variables for prompt {self.prompt_name} : {flooding_variables}")
|
||||
return True
|
||||
|
||||
marketing_prompt = Prompt(
|
||||
prompt_name=prompt_settings.MARKETING_PROMPT_NAME,
|
||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.MARKETING_PROMPT_NAME)
|
||||
)
|
||||
|
||||
summarize_prompt = Prompt(
|
||||
prompt_name=prompt_settings.SUMMARIZE_PROMPT_NAME,
|
||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.SUMMARIZE_PROMPT_NAME)
|
||||
)
|
||||
|
||||
lyric_prompt = Prompt(
|
||||
prompt_name=prompt_settings.LYLIC_PROMPT_NAME,
|
||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.LYLIC_PROMPT_NAME)
|
||||
)
|
||||
|
||||
def reload_all_prompt():
|
||||
marketing_prompt._reload_prompt()
|
||||
summarize_prompt._reload_prompt()
|
||||
lyric_prompt._reload_prompt()
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"prompt_variables": [
|
||||
"report",
|
||||
"selling_points"
|
||||
],
|
||||
"output_format": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "tags",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag_keywords": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"category",
|
||||
"tag_keywords",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
|
||||
입력 :
|
||||
분석 보고서
|
||||
{report}
|
||||
|
||||
셀링 포인트
|
||||
{selling_points}
|
||||
|
||||
위 분석 결과를 바탕으로, 주요 셀링 포인트를 다음 구조로 재정리하라.
|
||||
|
||||
조건:
|
||||
각 셀링 포인트는 반드시 ‘카테고리 → 태그 키워드 → 한 줄 설명’ 구조를 가질 것
|
||||
태그 키워드는 UI 상에서 타원(oval) 형태의 시각적 태그로 사용될 것을 가정하여
|
||||
- 3 ~ 6단어 이내
|
||||
- 명사 또는 명사형 키워드로 작성
|
||||
- 설명은 문장이 아닌, 짧은 ‘셀링 문구’ 형태로 작성할 것
|
||||
- 광고·숏폼·상세페이지 어디에도 바로 재사용 가능해야 함
|
||||
- 전체 셀링 포인트 개수는 5~7개로 제한
|
||||
|
||||
출력 형식:
|
||||
[카테고리명]
|
||||
(태그 키워드)
|
||||
- 한 줄 설명 문구
|
||||
|
||||
예시:
|
||||
[공간 정체성]
|
||||
(100년 적산가옥 · 시간의 결)
|
||||
- 하루를 ‘숙박’이 아닌 ‘체류’로 바꾸는 공간
|
||||
|
||||
[입지 & 희소성]
|
||||
(말랭이마을 · 로컬 히든플레이스)
|
||||
- 관광지가 아닌, 군산을 아는 사람의 선택
|
||||
|
||||
[프라이버시]
|
||||
(독채 숙소 · 프라이빗 스테이)
|
||||
- 누구의 방해도 없는 완전한 휴식 구조
|
||||
|
||||
[비주얼 경쟁력]
|
||||
(감성 인테리어 · 자연광 스폿)
|
||||
- 찍는 순간 콘텐츠가 되는 공간 설계
|
||||
|
||||
[타깃 최적화]
|
||||
(커플 · 소규모 여행)
|
||||
- 둘에게 가장 이상적인 공간 밀도
|
||||
|
||||
[체류 경험]
|
||||
(아무것도 안 해도 되는 하루)
|
||||
- 일정 없이도 만족되는 하루 루틴
|
||||
|
||||
[브랜드 포지션]
|
||||
(호텔도 펜션도 아닌 아지트)
|
||||
- 다시 돌아오고 싶은 개인적 장소
|
||||
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"model": "gpt-5-mini",
|
||||
"prompt_variables": [
|
||||
"customer_name",
|
||||
"region",
|
||||
"detail_region_info",
|
||||
"marketing_intelligence_summary",
|
||||
"language",
|
||||
"promotional_expression_example",
|
||||
"timing_rules"
|
||||
],
|
||||
"output_format": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "lyric",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lyric": {
|
||||
"type": "string"
|
||||
},
|
||||
"suno_prompt":{
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lyric", "suno_prompt"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
|
||||
[ROLE]
|
||||
You are a content marketing expert, brand strategist, and creative songwriter
|
||||
specializing in Korean pension / accommodation businesses.
|
||||
You create lyrics strictly based on Brand & Marketing Intelligence analysis
|
||||
and optimized for viral short-form video content.
|
||||
|
||||
[INPUT]
|
||||
Business Name: {customer_name}
|
||||
Region: {region}
|
||||
Region Details: {detail_region_info}
|
||||
Brand & Marketing Intelligence Report: {marketing_intelligence_summary}
|
||||
Output Language: {language}
|
||||
|
||||
[INTERNAL ANALYSIS – DO NOT OUTPUT]
|
||||
Internally analyze the following to guide all creative decisions:
|
||||
- Core brand identity and positioning
|
||||
- Emotional hooks derived from selling points
|
||||
- Target audience lifestyle, desires, and travel motivation
|
||||
- Regional atmosphere and symbolic imagery
|
||||
- How the stay converts into “shareable moments”
|
||||
- Which selling points must surface implicitly in lyrics
|
||||
|
||||
[LYRICS & MUSIC CREATION TASK]
|
||||
Based on the Brand & Marketing Intelligence Report for [{customer_name} ({region})], generate:
|
||||
- Original promotional lyrics
|
||||
- Music attributes for AI music generation (Suno-compatible prompt)
|
||||
The output must be designed for VIRAL DIGITAL CONTENT
|
||||
(short-form video, reels, ads).
|
||||
|
||||
[LYRICS REQUIREMENTS]
|
||||
Mandatory Inclusions:
|
||||
- Business name
|
||||
- Region name
|
||||
- Promotion subject
|
||||
- Promotional expressions including:
|
||||
{promotional_expression_example}
|
||||
|
||||
Content Rules:
|
||||
- Lyrics must be emotionally driven, not descriptive listings
|
||||
- Selling points must be IMPLIED, not explained
|
||||
- Must sound natural when sung
|
||||
- Must feel like a lifestyle moment, not an advertisement
|
||||
|
||||
Tone & Style:
|
||||
- Warm, emotional, and aspirational
|
||||
- Trendy, viral-friendly phrasing
|
||||
- Calm but memorable hooks
|
||||
- Suitable for travel / stay-related content
|
||||
|
||||
[SONG & MUSIC ATTRIBUTES – FOR SUNO PROMPT]
|
||||
After the lyrics, generate a concise music prompt including:
|
||||
Song mood (emotional keywords)
|
||||
BPM range
|
||||
Recommended genres (max 2)
|
||||
Key musical motifs or instruments
|
||||
Overall vibe (1 short sentence)
|
||||
|
||||
[CRITICAL LANGUAGE REQUIREMENT – ABSOLUTE RULE]
|
||||
ALL OUTPUT MUST BE 100% WRITTEN IN {language}.
|
||||
no mixed languages
|
||||
All names, places, and expressions must be in {language}
|
||||
Any violation invalidates the entire output
|
||||
|
||||
[OUTPUT RULES – STRICT]
|
||||
{timing_rules}
|
||||
|
||||
No explanations
|
||||
No headings
|
||||
No bullet points
|
||||
No analysis
|
||||
No extra text
|
||||
|
||||
[FAILURE FORMAT]
|
||||
If generation is impossible:
|
||||
ERROR: Brief reason in English
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
{
|
||||
"model": "gpt-5.2",
|
||||
"prompt_variables": [
|
||||
"customer_name",
|
||||
"region",
|
||||
"detail_region_info"
|
||||
],
|
||||
"output_format": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "report",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"report": {
|
||||
"type": "string"
|
||||
},
|
||||
"selling_points": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"keywords": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"category",
|
||||
"keywords",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"report",
|
||||
"selling_points",
|
||||
"tags"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
|
||||
[Role & Objective]
|
||||
Act as a content marketing expert with strong domain knowledge in the Korean pension / stay-accommodation industry.
|
||||
Your goal is to produce a Marketing Intelligence Report that will be shown to accommodation owners BEFORE any content is generated.
|
||||
The report must clearly explain what makes the property sellable, marketable, and scalable through content.
|
||||
|
||||
[INPUT]
|
||||
- Business Name: {customer_name}
|
||||
- Region: {region}
|
||||
- Region Details: {detail_region_info}
|
||||
|
||||
[Core Analysis Requirements]
|
||||
Analyze the property based on:
|
||||
Location, concept, and nearby environment
|
||||
Target customer behavior and reservation decision factors
|
||||
Include:
|
||||
- Target customer segments & personas
|
||||
- Unique Selling Propositions (USPs)
|
||||
- Competitive landscape (direct & indirect competitors)
|
||||
- Market positioning
|
||||
|
||||
[Key Selling Point Structuring – UI Optimized]
|
||||
From the analysis above, extract the main Key Selling Points using the structure below.
|
||||
Rules:
|
||||
Focus only on factors that directly influence booking decisions
|
||||
Each selling point must be concise and visually scannable
|
||||
Language must be reusable for ads, short-form videos, and listing headlines
|
||||
Avoid full sentences in descriptions; use short selling phrases
|
||||
Do not provide in report
|
||||
|
||||
Output format:
|
||||
[Category]
|
||||
(Tag keyword – 5~8 words, noun-based, UI oval-style)
|
||||
One-line selling phrase (not a full sentence)
|
||||
Limit:
|
||||
5 to 8 Key Selling Points only
|
||||
Do not provide in report
|
||||
|
||||
[Content & Automation Readiness Check]
|
||||
Ensure that:
|
||||
Each tag keyword can directly map to a content theme
|
||||
Each selling phrase can be used as:
|
||||
- Video hook
|
||||
- Image headline
|
||||
- Ad copy snippet
|
||||
|
||||
|
||||
[Tag Generation Rules]
|
||||
- Tags must include **only core keywords that can be directly used for viral video song lyrics**
|
||||
- Each tag should be selected with **search discovery + emotional resonance + reservation conversion** in mind
|
||||
- The number of tags must be **exactly 5**
|
||||
- Tags must be **nouns or short keyword phrases**; full sentences are strictly prohibited
|
||||
- The following categories must be **balanced and all represented**:
|
||||
1) **Location / Local context** (region name, neighborhood, travel context)
|
||||
2) **Accommodation positioning** (emotional stay, private stay, boutique stay, etc.)
|
||||
3) **Emotion / Experience** (healing, rest, one-day escape, memory, etc.)
|
||||
4) **SNS / Viral signals** (Instagram vibes, picture-perfect day, aesthetic travel, etc.)
|
||||
5) **Travel & booking intent** (travel, getaway, stay, relaxation, etc.)
|
||||
|
||||
- If a brand name exists, **at least one tag must include the brand name or a brand-specific expression**
|
||||
- Avoid overly generic keywords (e.g., “hotel”, “travel” alone); **prioritize distinctive, differentiating phrases**
|
||||
- The final output must strictly follow the JSON format below, with no additional text
|
||||
|
||||
"tags": ["Tag1", "Tag2", "Tag3", "Tag4", "Tag5"]
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
|
||||
[Role & Objective]
|
||||
Act as a content marketing expert with strong domain knowledge in the Korean pension / stay-accommodation industry.
|
||||
Your goal is to produce a Marketing Intelligence Report that will be shown to accommodation owners BEFORE any content is generated.
|
||||
The report must clearly explain what makes the property sellable, marketable, and scalable through content.
|
||||
|
||||
[INPUT]
|
||||
- Business Name: {customer_name}
|
||||
- Region: {region}
|
||||
- Region Details: {detail_region_info}
|
||||
|
||||
[Core Analysis Requirements]
|
||||
Analyze the property based on:
|
||||
Location, concept, photos, online presence, and nearby environment
|
||||
Target customer behavior and reservation decision factors
|
||||
Include:
|
||||
- Target customer segments & personas
|
||||
- Unique Selling Propositions (USPs)
|
||||
- Competitive landscape (direct & indirect competitors)
|
||||
- Market positioning
|
||||
|
||||
[Key Selling Point Structuring – UI Optimized]
|
||||
From the analysis above, extract the main Key Selling Points using the structure below.
|
||||
Rules:
|
||||
Focus only on factors that directly influence booking decisions
|
||||
Each selling point must be concise and visually scannable
|
||||
Language must be reusable for ads, short-form videos, and listing headlines
|
||||
Avoid full sentences in descriptions; use short selling phrases
|
||||
|
||||
Output format:
|
||||
[Category]
|
||||
(Tag keyword – 5~8 words, noun-based, UI oval-style)
|
||||
One-line selling phrase (not a full sentence)
|
||||
Limit:
|
||||
5 to 8 Key Selling Points only
|
||||
|
||||
[Content & Automation Readiness Check]
|
||||
Ensure that:
|
||||
Each tag keyword can directly map to a content theme
|
||||
Each selling phrase can be used as:
|
||||
- Video hook
|
||||
- Image headline
|
||||
- Ad copy snippet
|
||||
|
||||
|
||||
[Tag Generation Rules]
|
||||
- Tags must include **only core keywords that can be directly used for viral video song lyrics**
|
||||
- Each tag should be selected with **search discovery + emotional resonance + reservation conversion** in mind
|
||||
- The number of tags must be **exactly 5**
|
||||
- Tags must be **nouns or short keyword phrases**; full sentences are strictly prohibited
|
||||
- The following categories must be **balanced and all represented**:
|
||||
1) **Location / Local context** (region name, neighborhood, travel context)
|
||||
2) **Accommodation positioning** (emotional stay, private stay, boutique stay, etc.)
|
||||
3) **Emotion / Experience** (healing, rest, one-day escape, memory, etc.)
|
||||
4) **SNS / Viral signals** (Instagram vibes, picture-perfect day, aesthetic travel, etc.)
|
||||
5) **Travel & booking intent** (travel, getaway, stay, relaxation, etc.)
|
||||
|
||||
- If a brand name exists, **at least one tag must include the brand name or a brand-specific expression**
|
||||
- Avoid overly generic keywords (e.g., “hotel”, “travel” alone); **prioritize distinctive, differentiating phrases**
|
||||
- The final output must strictly follow the JSON format below, with no additional text
|
||||
|
||||
"tags": ["Tag1", "Tag2", "Tag3", "Tag4", "Tag5"]
|
||||
|
|
@ -1,76 +1,57 @@
|
|||
import os, json
|
||||
from abc import ABCMeta
|
||||
from pydantic import BaseModel
|
||||
from config import prompt_settings
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.prompts.schemas import *
|
||||
|
||||
logger = get_logger("prompt")
|
||||
|
||||
class Prompt():
|
||||
prompt_name : str # ex) marketing_prompt
|
||||
prompt_template_path : str #프롬프트 경로
|
||||
prompt_template : str # fstring 포맷
|
||||
prompt_input : list
|
||||
prompt_output : dict
|
||||
prompt_model : str
|
||||
|
||||
def __init__(self, prompt_name, prompt_template_path):
|
||||
self.prompt_name = prompt_name
|
||||
prompt_input_class = BaseModel # pydantic class 자체를(instance 아님) 변수로 가짐
|
||||
prompt_output_class = BaseModel
|
||||
|
||||
def __init__(self, prompt_template_path, prompt_input_class, prompt_output_class, prompt_model):
|
||||
self.prompt_template_path = prompt_template_path
|
||||
self.prompt_template, prompt_dict = self.read_prompt()
|
||||
self.prompt_input = prompt_dict['prompt_variables']
|
||||
self.prompt_output = prompt_dict['output_format']
|
||||
self.prompt_model = prompt_dict.get('model', "gpt-5-mini")
|
||||
self.prompt_input_class = prompt_input_class
|
||||
self.prompt_output_class = prompt_output_class
|
||||
self.prompt_template = self.read_prompt()
|
||||
self.prompt_model = prompt_model
|
||||
|
||||
def _reload_prompt(self):
|
||||
self.prompt_template, prompt_dict = self.read_prompt()
|
||||
self.prompt_input = prompt_dict['prompt_variables']
|
||||
self.prompt_output = prompt_dict['output_format']
|
||||
self.prompt_model = prompt_dict.get('model', "gpt-5-mini")
|
||||
self.prompt_template = self.read_prompt()
|
||||
|
||||
def read_prompt(self) -> tuple[str, dict]:
|
||||
template_text_path = self.prompt_template_path + ".txt"
|
||||
prompt_dict_path = self.prompt_template_path + ".json"
|
||||
with open(template_text_path, "r") as fp:
|
||||
with open(self.prompt_template_path, "r") as fp:
|
||||
prompt_template = fp.read()
|
||||
with open(prompt_dict_path, "r") as fp:
|
||||
prompt_dict = json.load(fp)
|
||||
|
||||
return prompt_template, prompt_dict
|
||||
return prompt_template
|
||||
|
||||
def build_prompt(self, input_data:dict) -> str:
|
||||
self.check_input(input_data)
|
||||
verified_input = self.prompt_input_class(**input_data)
|
||||
build_template = self.prompt_template
|
||||
build_template = build_template.format(**verified_input.model_dump())
|
||||
logger.debug(f"build_template: {build_template}")
|
||||
logger.debug(f"input_data: {input_data}")
|
||||
build_template = build_template.format(**input_data)
|
||||
return build_template
|
||||
|
||||
def check_input(self, input_data:dict) -> bool:
|
||||
missing_variables = input_data.keys() - set(self.prompt_input)
|
||||
if missing_variables:
|
||||
raise Exception(f"missing_variable for prompt {self.prompt_name} : {missing_variables}")
|
||||
|
||||
flooding_variables = set(self.prompt_input) - input_data.keys()
|
||||
if flooding_variables:
|
||||
raise Exception(f"flooding_variables for prompt {self.prompt_name} : {flooding_variables}")
|
||||
return True
|
||||
|
||||
marketing_prompt = Prompt(
|
||||
prompt_name=prompt_settings.MARKETING_PROMPT_NAME,
|
||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.MARKETING_PROMPT_NAME)
|
||||
)
|
||||
|
||||
summarize_prompt = Prompt(
|
||||
prompt_name=prompt_settings.SUMMARIZE_PROMPT_NAME,
|
||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.SUMMARIZE_PROMPT_NAME)
|
||||
prompt_template_path = os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.MARKETING_PROMPT_FILE_NAME),
|
||||
prompt_input_class = MarketingPromptInput,
|
||||
prompt_output_class = MarketingPromptOutput,
|
||||
prompt_model = prompt_settings.MARKETING_PROMPT_MODEL
|
||||
)
|
||||
|
||||
lyric_prompt = Prompt(
|
||||
prompt_name=prompt_settings.LYLIC_PROMPT_NAME,
|
||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.LYLIC_PROMPT_NAME)
|
||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.LYRIC_PROMPT_FILE_NAME),
|
||||
prompt_input_class = LyricPromptInput,
|
||||
prompt_output_class = LyricPromptOutput,
|
||||
prompt_model = prompt_settings.LYRIC_PROMPT_MODEL
|
||||
)
|
||||
|
||||
def reload_all_prompt():
|
||||
marketing_prompt._reload_prompt()
|
||||
summarize_prompt._reload_prompt()
|
||||
lyric_prompt._reload_prompt()
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from .lyric import LyricPromptInput, LyricPromptOutput
|
||||
from .marketing import MarketingPromptInput, MarketingPromptOutput
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
# Input 정의
|
||||
class LyricPromptInput(BaseModel):
|
||||
customer_name : str = Field(..., description = "마케팅 대상 사업체 이름")
|
||||
region : str = Field(..., description = "마케팅 대상 지역")
|
||||
detail_region_info : str = Field(..., description = "마케팅 대상 지역 상세")
|
||||
marketing_intelligence_summary : Optional[str] = Field(None, description = "마케팅 분석 정보 보고서")
|
||||
language : str= Field(..., description = "가사 언어")
|
||||
promotional_expression_example : str = Field(..., description = "판촉 가사 표현 예시")
|
||||
timing_rules : str = Field(..., description = "시간 제어문")
|
||||
|
||||
# Output 정의
|
||||
class LyricPromptOutput(BaseModel):
|
||||
lyric: str = Field(..., description="생성된 가사")
|
||||
suno_prompt: str = Field(..., description="Suno AI용 프롬프트")
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
from pydantic import BaseModel, Field
|
||||
from typing import List
|
||||
|
||||
# Input 정의
|
||||
class MarketingPromptInput(BaseModel):
|
||||
customer_name : str = Field(..., description = "마케팅 대상 사업체 이름")
|
||||
region : str = Field(..., description = "마케팅 대상 지역")
|
||||
detail_region_info : str = Field(..., description = "마케팅 대상 지역 상세")
|
||||
|
||||
|
||||
# Output 정의
|
||||
class BrandIdentity(BaseModel):
|
||||
location_feature_analysis: str = Field(..., description="입지 특성 분석 (80자 이상 150자 이하)", min_length = 80, max_length = 150) # min/max constraint는 현재 openai json schema 등에서 작동하지 않는다는 보고가 있음.
|
||||
concept_scalability: str = Field(..., description="컨셉 확장성 (80자 이상 150자 이하)", min_length = 80, max_length = 150)
|
||||
|
||||
|
||||
class MarketPositioning(BaseModel):
|
||||
category_definition: str = Field(..., description="마케팅 카테고리")
|
||||
core_value: str = Field(..., description="마케팅 포지션 핵심 가치")
|
||||
|
||||
class AgeRange(BaseModel):
|
||||
min_age : int = Field(..., ge=0, le=100)
|
||||
max_age : int = Field(..., ge=0, le=100)
|
||||
|
||||
|
||||
class TargetPersona(BaseModel):
|
||||
persona: str = Field(..., description="타겟 페르소나 이름/설명")
|
||||
age: AgeRange = Field(..., description="타겟 페르소나 나이대")
|
||||
favor_target: List[str] = Field(..., description="페르소나의 선호 요소")
|
||||
decision_trigger: str = Field(..., description="구매 결정 트리거")
|
||||
|
||||
|
||||
class SellingPoint(BaseModel):
|
||||
category: str = Field(..., description="셀링포인트 카테고리")
|
||||
description: str = Field(..., description="상세 설명")
|
||||
score: int = Field(..., ge=70, le=99, description="점수 (100점 만점)")
|
||||
|
||||
class MarketingPromptOutput(BaseModel):
|
||||
brand_identity: BrandIdentity = Field(..., description="브랜드 아이덴티티")
|
||||
market_positioning: MarketPositioning = Field(..., description="시장 포지셔닝")
|
||||
target_persona: List[TargetPersona] = Field(..., description="타겟 페르소나")
|
||||
selling_points: List[SellingPoint] = Field(..., description="셀링 포인트")
|
||||
target_keywords: List[str] = Field(..., description="타겟 키워드 리스트")
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"prompt_variables": [
|
||||
"report",
|
||||
"selling_points"
|
||||
],
|
||||
"output_format": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "tags",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag_keywords": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"category",
|
||||
"tag_keywords",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
|
||||
입력 :
|
||||
분석 보고서
|
||||
{report}
|
||||
|
||||
셀링 포인트
|
||||
{selling_points}
|
||||
|
||||
위 분석 결과를 바탕으로, 주요 셀링 포인트를 다음 구조로 재정리하라.
|
||||
|
||||
조건:
|
||||
각 셀링 포인트는 반드시 ‘카테고리 → 태그 키워드 → 한 줄 설명’ 구조를 가질 것
|
||||
태그 키워드는 UI 상에서 타원(oval) 형태의 시각적 태그로 사용될 것을 가정하여
|
||||
- 3 ~ 6단어 이내
|
||||
- 명사 또는 명사형 키워드로 작성
|
||||
- 설명은 문장이 아닌, 짧은 ‘셀링 문구’ 형태로 작성할 것
|
||||
- 광고·숏폼·상세페이지 어디에도 바로 재사용 가능해야 함
|
||||
- 전체 셀링 포인트 개수는 5~7개로 제한
|
||||
|
||||
출력 형식:
|
||||
[카테고리명]
|
||||
(태그 키워드)
|
||||
- 한 줄 설명 문구
|
||||
|
||||
예시:
|
||||
[공간 정체성]
|
||||
(100년 적산가옥 · 시간의 결)
|
||||
- 하루를 ‘숙박’이 아닌 ‘체류’로 바꾸는 공간
|
||||
|
||||
[입지 & 희소성]
|
||||
(말랭이마을 · 로컬 히든플레이스)
|
||||
- 관광지가 아닌, 군산을 아는 사람의 선택
|
||||
|
||||
[프라이버시]
|
||||
(독채 숙소 · 프라이빗 스테이)
|
||||
- 누구의 방해도 없는 완전한 휴식 구조
|
||||
|
||||
[비주얼 경쟁력]
|
||||
(감성 인테리어 · 자연광 스폿)
|
||||
- 찍는 순간 콘텐츠가 되는 공간 설계
|
||||
|
||||
[타깃 최적화]
|
||||
(커플 · 소규모 여행)
|
||||
- 둘에게 가장 이상적인 공간 밀도
|
||||
|
||||
[체류 경험]
|
||||
(아무것도 안 해도 되는 하루)
|
||||
- 일정 없이도 만족되는 하루 루틴
|
||||
|
||||
[브랜드 포지션]
|
||||
(호텔도 펜션도 아닌 아지트)
|
||||
- 다시 돌아오고 싶은 개인적 장소
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Role
|
||||
Act as a Senior Brand Strategist and Marketing Data Analyst. Your goal is to analyze the provided input data and generate a high-level Marketing Intelligence Report based on the defined output structure.
|
||||
|
||||
# Input Data
|
||||
* **Customer Name:** {customer_name}
|
||||
* **Region:** {region}
|
||||
* **Detail Region Info:** {detail_region_info}
|
||||
|
||||
# Output Rules
|
||||
1. **Language:** All descriptive content must be written in **Korean (한국어)**.
|
||||
2. **Terminology:** Use professional marketing terminology suitable for the hospitality and stay industry.
|
||||
3. **Strict Selection for `selling_points.category`:** You must select the value for the `category` field in `selling_points` strictly from the following English allowed list to ensure UI compatibility:
|
||||
* `LOCATION`, `CONCEPT`, `PRIVACY`, `NIGHT MOOD`, `HEALING`, `PHOTO SPOT`, `SHORT GETAWAY`, `HOSPITALITY`, `SWIMMING POOL`, `JACUZZI`, `BBQ PARTY`, `FIRE PIT`, `GARDEN`, `BREAKFAST`, `KIDS FRIENDLY`, `PET FRIENDLY`, `OCEAN VIEW`, `PRIVATE POOL`.
|
||||
|
||||
---
|
||||
|
||||
# Instruction per Output Field (Mapping Logic)
|
||||
|
||||
### 1. brand_identity
|
||||
* **`location_feature_analysis`**: Analyze the marketing advantages of the given `{region}` and `{detail_region_info}`. Explain why this specific location is attractive to travelers. summarize in 1-2 sentences. (e.g., proximity to nature, accessibility from Seoul, or unique local atmosphere).
|
||||
* **`concept_scalability`**: Based on `{customer_name}`, analyze how the brand's core concept can expand into a total customer experience or additional services. summarize in 1-2 sentences.
|
||||
|
||||
### 2. market_positioning
|
||||
* **`category_definition`**: Define a sharp, niche market category for this business (e.g., "Private Forest Cabin" or "Luxury Kids Pool Villa").
|
||||
* **`core_value`**: Identify the single most compelling emotional or functional value that distinguishes `{customer_name}` from competitors.
|
||||
|
||||
### 3. target_persona
|
||||
Generate a list of personas based on the following:
|
||||
* **`persona`**: Provide a descriptive name and profile for the target group.
|
||||
* **`age`**: Set `min_age` and `max_age` (Integer 0-100) that accurately reflects the segment.
|
||||
* **`favor_target`**: List specific elements or vibes this persona prefers (e.g., "Minimalist interior", "Pet-friendly facilities").
|
||||
* **`decision_trigger`**: Identify the specific "Hook" or facility that leads this persona to finalize a booking.
|
||||
|
||||
### 4. selling_points
|
||||
Generate exactly 7 selling points:
|
||||
* **`category`**: Strictly use one keyword from the English allowed list provided in the Output Rules.
|
||||
* **`description`**: A short, punchy marketing phrase in Korean (max 20 characters).
|
||||
* **`score`**: An integer (70-99) representing the strength of this feature based on the brand's potential.
|
||||
|
||||
### 5. target_keywords
|
||||
* **`target_keywords`**: Provide a list of 10 highly relevant marketing keywords or hashtags for search engine optimization and social media targeting.
|
||||
25
config.py
25
config.py
|
|
@ -105,6 +105,19 @@ class CrawlerSettings(BaseSettings):
|
|||
model_config = _base_config
|
||||
|
||||
|
||||
class NaverAPISettings(BaseSettings):
|
||||
"""네이버 API 설정"""
|
||||
|
||||
NAVER_CLIENT_ID: str = Field(default="", description="네이버 API 클라이언트 ID")
|
||||
NAVER_CLIENT_SECRET: str = Field(default="", description="네이버 API 클라이언트 시크릿")
|
||||
NAVER_LOCAL_API_URL: str = Field(
|
||||
default="https://openapi.naver.com/v1/search/local.json",
|
||||
description="네이버 지역 검색 API URL",
|
||||
)
|
||||
|
||||
model_config = _base_config
|
||||
|
||||
|
||||
class AzureBlobSettings(BaseSettings):
|
||||
"""Azure Blob Storage 설정"""
|
||||
|
||||
|
|
@ -146,10 +159,13 @@ class CreatomateSettings(BaseSettings):
|
|||
model_config = _base_config
|
||||
|
||||
class PromptSettings(BaseSettings):
|
||||
PROMPT_FOLDER_ROOT : str = Field(default="./app/utils/prompts")
|
||||
MARKETING_PROMPT_NAME : str = Field(default="marketing_prompt")
|
||||
SUMMARIZE_PROMPT_NAME : str = Field(default="summarize_prompt")
|
||||
LYLIC_PROMPT_NAME : str = Field(default="lyric_prompt")
|
||||
PROMPT_FOLDER_ROOT : str = Field(default="./app/utils/prompts/templates")
|
||||
|
||||
MARKETING_PROMPT_FILE_NAME : str = Field(default="marketing_prompt.txt")
|
||||
MARKETING_PROMPT_MODEL : str = Field(default="gpt-5.2")
|
||||
|
||||
LYRIC_PROMPT_FILE_NAME : str = Field(default="lyric_prompt.txt")
|
||||
LYRIC_PROMPT_MODEL : str = Field(default="gpt-5-mini")
|
||||
|
||||
model_config = _base_config
|
||||
|
||||
|
|
@ -437,6 +453,7 @@ apikey_settings = APIKeySettings()
|
|||
db_settings = DatabaseSettings()
|
||||
cors_settings = CORSSettings()
|
||||
crawler_settings = CrawlerSettings()
|
||||
naver_api_settings = NaverAPISettings()
|
||||
azure_blob_settings = AzureBlobSettings()
|
||||
creatomate_settings = CreatomateSettings()
|
||||
prompt_settings = PromptSettings()
|
||||
|
|
|
|||
28
main.py
28
main.py
|
|
@ -51,6 +51,33 @@ tags_metadata = [
|
|||
# "name": "Home",
|
||||
# "description": "홈 화면 및 프로젝트 관리 API",
|
||||
# },
|
||||
{
|
||||
"name": "Search",
|
||||
"description": """숙박/펜션 검색 API - 네이버 지역 검색 기반 자동완성
|
||||
|
||||
**인증: 불필요** (공개 API)
|
||||
|
||||
## 사용법
|
||||
|
||||
`GET /search/accommodation?query=스테이머뭄`
|
||||
|
||||
## 응답 예시
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "스테이머뭄",
|
||||
"count": 1,
|
||||
"items": [
|
||||
{
|
||||
"title": "<b>스테이</b>,<b>머뭄</b>",
|
||||
"address": "전북특별자치도 군산시 신흥동 63-18",
|
||||
"roadAddress": "전북특별자치도 군산시 절골길 18"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Crawling",
|
||||
"description": """네이버 지도 크롤링 API - 장소 정보 및 이미지 수집
|
||||
|
|
@ -190,6 +217,7 @@ def custom_openapi():
|
|||
"/auth/test/", # 테스트 엔드포인트
|
||||
"/crawling",
|
||||
"/autocomplete",
|
||||
"/search", # 숙박 검색 자동완성
|
||||
]
|
||||
|
||||
# 보안이 필요한 엔드포인트에 security 적용
|
||||
|
|
|
|||
|
|
@ -0,0 +1,694 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "e7af5103-62db-4a32-b431-6395c85d7ac9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from app.home.api.routers.v1.home import crawling\n",
|
||||
"from app.utils.prompts import prompts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "6cf7ae9b-3ffe-4046-9cab-f33bc071b288",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from config import crawler_settings"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "4c4ec4c5-9efb-470f-99cf-a18a5b80352f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from app.home.schemas.home_schema import (\n",
|
||||
" CrawlingRequest,\n",
|
||||
" CrawlingResponse,\n",
|
||||
" ErrorResponse,\n",
|
||||
" ImageUploadResponse,\n",
|
||||
" ImageUploadResultItem,\n",
|
||||
" ImageUrlItem,\n",
|
||||
" MarketingAnalysis,\n",
|
||||
" ProcessedInfo,\n",
|
||||
")\n",
|
||||
"import json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "be5d0e16-8cc6-44d4-ae93-8252caa09940",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"val1 = CrawlingRequest(**{\"url\" : 'https://map.naver.com/p/entry/place/1903455560?placePath=/home?from=map&fromPanelNum=1&additionalHeight=76×tamp=202601131552&locale=ko&svcName=map_pcv5&businessCategory=pension&c=15.00,0,0,0,dh'})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "c13742d7-70f4-4a6d-90c2-8b84f245a08c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from app.utils.prompts.prompts import reload_all_prompt\n",
|
||||
"reload_all_prompt()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "d4db2ec1-b2af-4993-8832-47f380c17015",
|
||||
"metadata": {
|
||||
"scrolled": true
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[2026-01-19 14:13:53] [INFO] [home:crawling:110] [crawling] ========== START ==========\n",
|
||||
"[2026-01-19 14:13:53] [INFO] [home:crawling:111] [crawling] URL: https://map.naver.com/p/entry/place/1903455560?placePath=/home?from=map&fromPane...\n",
|
||||
"[2026-01-19 14:13:53] [INFO] [home:crawling:115] [crawling] Step 1: 네이버 지도 크롤링 시작...\n",
|
||||
"[2026-01-19 14:13:53] [INFO] [scraper:_call_get_accommodation:140] [NvMapScraper] Requesting place_id: 1903455560\n",
|
||||
"[2026-01-19 14:13:53] [INFO] [scraper:_call_get_accommodation:149] [NvMapScraper] SUCCESS - place_id: 1903455560\n",
|
||||
"[2026-01-19 14:13:51] [INFO] [home:crawling:138] [crawling] Step 1 완료 - 이미지 44개 (735.1ms)\n",
|
||||
"[2026-01-19 14:13:51] [INFO] [home:crawling:142] [crawling] Step 2: 정보 가공 시작...\n",
|
||||
"[2026-01-19 14:13:51] [INFO] [home:crawling:159] [crawling] Step 2 완료 - 오블로모프, 군산시 (0.8ms)\n",
|
||||
"[2026-01-19 14:13:51] [INFO] [home:crawling:163] [crawling] Step 3: ChatGPT 마케팅 분석 시작...\n",
|
||||
"[2026-01-19 14:13:51] [DEBUG] [home:crawling:170] [crawling] Step 3-1: 서비스 초기화 완료 (428.6ms)\n",
|
||||
"build_template \n",
|
||||
"[Role & Objective]\n",
|
||||
"Act as a content marketing expert with strong domain knowledge in the Korean pension / stay-accommodation industry.\n",
|
||||
"Your goal is to produce a Marketing Intelligence Report that will be shown to accommodation owners BEFORE any content is generated.\n",
|
||||
"The report must clearly explain what makes the property sellable, marketable, and scalable through content.\n",
|
||||
"\n",
|
||||
"[INPUT]\n",
|
||||
"- Business Name: {customer_name}\n",
|
||||
"- Region: {region}\n",
|
||||
"- Region Details: {detail_region_info}\n",
|
||||
"\n",
|
||||
"[Core Analysis Requirements]\n",
|
||||
"Analyze the property based on:\n",
|
||||
"Location, concept, and nearby environment\n",
|
||||
"Target customer behavior and reservation decision factors\n",
|
||||
"Include:\n",
|
||||
"- Target customer segments & personas\n",
|
||||
"- Unique Selling Propositions (USPs)\n",
|
||||
"- Competitive landscape (direct & indirect competitors)\n",
|
||||
"- Market positioning\n",
|
||||
"\n",
|
||||
"[Key Selling Point Structuring – UI Optimized]\n",
|
||||
"From the analysis above, extract the main Key Selling Points using the structure below.\n",
|
||||
"Rules:\n",
|
||||
"Focus only on factors that directly influence booking decisions\n",
|
||||
"Each selling point must be concise and visually scannable\n",
|
||||
"Language must be reusable for ads, short-form videos, and listing headlines\n",
|
||||
"Avoid full sentences in descriptions; use short selling phrases\n",
|
||||
"Do not provide in report\n",
|
||||
"\n",
|
||||
"Output format:\n",
|
||||
"[Category]\n",
|
||||
"(Tag keyword – 5~8 words, noun-based, UI oval-style)\n",
|
||||
"One-line selling phrase (not a full sentence)\n",
|
||||
"Limit:\n",
|
||||
"5 to 8 Key Selling Points only\n",
|
||||
"Do not provide in report\n",
|
||||
"\n",
|
||||
"[Content & Automation Readiness Check]\n",
|
||||
"Ensure that:\n",
|
||||
"Each tag keyword can directly map to a content theme\n",
|
||||
"Each selling phrase can be used as:\n",
|
||||
"- Video hook\n",
|
||||
"- Image headline\n",
|
||||
"- Ad copy snippet\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"[Tag Generation Rules]\n",
|
||||
"- Tags must include **only core keywords that can be directly used for viral video song lyrics**\n",
|
||||
"- Each tag should be selected with **search discovery + emotional resonance + reservation conversion** in mind\n",
|
||||
"- The number of tags must be **exactly 5**\n",
|
||||
"- Tags must be **nouns or short keyword phrases**; full sentences are strictly prohibited\n",
|
||||
"- The following categories must be **balanced and all represented**:\n",
|
||||
" 1) **Location / Local context** (region name, neighborhood, travel context)\n",
|
||||
" 2) **Accommodation positioning** (emotional stay, private stay, boutique stay, etc.)\n",
|
||||
" 3) **Emotion / Experience** (healing, rest, one-day escape, memory, etc.)\n",
|
||||
" 4) **SNS / Viral signals** (Instagram vibes, picture-perfect day, aesthetic travel, etc.)\n",
|
||||
" 5) **Travel & booking intent** (travel, getaway, stay, relaxation, etc.)\n",
|
||||
"\n",
|
||||
"- If a brand name exists, **at least one tag must include the brand name or a brand-specific expression**\n",
|
||||
"- Avoid overly generic keywords (e.g., “hotel”, “travel” alone); **prioritize distinctive, differentiating phrases**\n",
|
||||
"- The final output must strictly follow the JSON format below, with no additional text\n",
|
||||
"\n",
|
||||
" \"tags\": [\"Tag1\", \"Tag2\", \"Tag3\", \"Tag4\", \"Tag5\"]\n",
|
||||
"\n",
|
||||
"input_data {'customer_name': '오블로모프', 'region': '군산시', 'detail_region_info': '전북 군산시 절골길 16'}\n",
|
||||
"[ChatgptService] Generated Prompt (length: 2791)\n",
|
||||
"[2026-01-19 14:13:51] [INFO] [chatgpt:generate_structured_output:43] [ChatgptService] Starting GPT request with structured output with model: gpt-5-mini\n",
|
||||
"[2026-01-19 14:14:52] [INFO] [home:crawling:187] [crawling] Step 3-3: GPT API 호출 완료 - (63233.5ms)\n",
|
||||
"[2026-01-19 14:14:52] [DEBUG] [home:crawling:188] [crawling] Step 3-3: GPT API 호출 완료 - (63233.5ms)\n",
|
||||
"[2026-01-19 14:14:52] [DEBUG] [home:crawling:193] [crawling] Step 3-4: 응답 파싱 시작 - facility_info: 무선 인터넷, 예약, 주차\n",
|
||||
"[2026-01-19 14:14:52] [DEBUG] [home:crawling:212] [crawling] Step 3-4: 응답 파싱 완료 (2.1ms)\n",
|
||||
"[2026-01-19 14:14:52] [INFO] [home:crawling:215] [crawling] Step 3 완료 - 마케팅 분석 성공 (63670.2ms)\n",
|
||||
"[2026-01-19 14:14:52] [INFO] [home:crawling:229] [crawling] ========== COMPLETE ==========\n",
|
||||
"[2026-01-19 14:14:52] [INFO] [home:crawling:230] [crawling] 총 소요시간: 64412.0ms\n",
|
||||
"[2026-01-19 14:14:52] [INFO] [home:crawling:231] [crawling] - Step 1 (크롤링): 735.1ms\n",
|
||||
"[2026-01-19 14:14:52] [INFO] [home:crawling:233] [crawling] - Step 2 (정보가공): 0.8ms\n",
|
||||
"[2026-01-19 14:14:52] [INFO] [home:crawling:235] [crawling] - Step 3 (GPT 분석): 63670.2ms\n",
|
||||
"[2026-01-19 14:14:52] [INFO] [home:crawling:237] [crawling] - GPT API 호출: 63233.5ms\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"var2 = await crawling(val1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "79f093f0-d7d2-4ed1-ba43-da06e4ee2073",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'image_list': ['https://ldb-phinf.pstatic.net/20230515_163/1684090233619kRU3v_JPEG/20230513_154207.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20250811_213/17548982879808X4MH_PNG/1.png',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_34/1712622373542UY8aC_JPEG/20231007_051403.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_37/1684090234513tT89X_JPEG/20230513_152018.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20241231_272/1735620966755B9XgT_PNG/DSC09054.png',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_100/1712622410472zgP15_JPEG/20230523_153219.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_151/1712623034401FzQbd_JPEG/Screenshot_20240409_093158_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_169/1712622316504ReKji_JPEG/20230728_125946.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230521_279/1684648422643NI2oj_JPEG/20230521_144343.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_52/1712622993632WR1sT_JPEG/Screenshot_20240409_093237_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20250811_151/1754898220223TNtvB_PNG/2.png',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_70/1712622381167p9QOI_JPEG/20230608_175722.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_144/1684090233161cR5mr_JPEG/20230513_180151.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_158/1712621983956CCqdo_JPEG/20240407_121826.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20250811_187/1754893113769iGO5X_JPEG/%B0%C5%BD%C7_01.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_31/17126219901822nnR4_JPEG/20240407_121615.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_94/1712621993863AWMKi_JPEG/20240407_121520.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_165/1684090236297fVhJM_JPEG/20230513_165348.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_102/1684090230350e1v0E_JPEG/20230513_162718.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_26/1684090232743arN2y_JPEG/20230513_174246.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20250811_273/1754893072358V3WcL_JPEG/%B5%F0%C5%D7%C0%CF%C4%C6_02.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_160/1712621974438LLNbD_JPEG/20240407_121848.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_218/1712623006036U39zE_JPEG/Screenshot_20240409_093114_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_210/16840902342654EkeL_JPEG/20230513_152107.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_216/1712623058832HBulg_JPEG/Screenshot_20240409_093309_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_184/1684090223226nO2Az_JPEG/20230514_143325.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_209/1684090697642BHNVR_JPEG/20230514_143528.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_16/1712623029052VNeaz_JPEG/Screenshot_20240409_093141_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_141/1684090233092KwtWy_JPEG/20230513_180105.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_177/1712623066424dcwJ2_JPEG/Screenshot_20240409_093511_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_181/16840902259407iA5Q_JPEG/20230514_144814.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_153/1684090224581Ih4ft_JPEG/20230514_143552.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_205/1684090231467WmulO_JPEG/20230513_180254.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20230515_120/1684090231233PkqCf_JPEG/20230513_152550.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_188/1712623039909sflvy_JPEG/Screenshot_20240409_093209_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_165/1712623049073j0TzM_JPEG/Screenshot_20240409_093254_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_3/17126230950579050V_JPEG/Screenshot_20240409_093412_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_270/1712623091524YX4E6_JPEG/Screenshot_20240409_093355_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_22/1712623083348btwTB_JPEG/Screenshot_20240409_093331_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_242/1712623087423Q7tHk_JPEG/Screenshot_20240409_093339_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_173/1712623098958aFhiB_JPEG/Screenshot_20240409_093422_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_113/1712623103270DOGKI_JPEG/Screenshot_20240409_093435_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_295/17126230704056BTRg_JPEG/Screenshot_20240409_093448_Airbnb.jpg',\n",
|
||||
" 'https://ldb-phinf.pstatic.net/20240409_178/1712623075172JEt43_JPEG/Screenshot_20240409_093457_Airbnb.jpg'],\n",
|
||||
" 'image_count': 44,\n",
|
||||
" 'processed_info': ProcessedInfo(customer_name='오블로모프', region='군산시', detail_region_info='전북 군산시 절골길 16'),\n",
|
||||
" 'marketing_analysis': MarketingAnalysis(report=MarketingAnalysisReport(summary=\"오블로모프는 '느림·쉼·문학적 감성'을 브랜드 콘셉트로 삼아 전북 군산시 절골길 인근의 조용한 주거·근대문화 접근성을 살린 소규모 부티크 스테이입니다. 도심형 접근성과 지역 근대문화·항구 관광지를 결합해 주말 단기체류, 커플·소규모 그룹, 콘텐츠 크리에이터 수요를 공략할 수 있습니다. 핵심은 브랜드 스토리(‘Oblomov’의 느긋함)와 인스타형 비주얼, 지역 연계 체험 상품으로 예약전환을 높이는 것입니다.\", details=[MarketingAnalysisDetail(detail_title='입지·콘셉트·주변 환경', detail_description='절골길 인근의 주택가·언덕형 지형, 조용한 체류 환경. 군산 근대역사문화거리·항구·현지 시장 접근권(차로 10–25분권). 문학적·레트로 감성 콘셉트(오블로모프 → 느림·휴식)으로 도심형 ‘감성 은신처’ 포지셔닝 가능.'), MarketingAnalysisDetail(detail_title='예약 결정 요인(고객 행동)', detail_description='사진·비주얼(첫 인상) → 콘셉트·프라이버시(전용공간 여부) → 접근성(차·대중교통 소요) → 가격 대비 가치·후기 → 체크인 편의성(셀프체크인 여부) → 지역 체험(먹거리·근대문화 투어) 순으로 예약 전환 영향.'), MarketingAnalysisDetail(detail_title='타깃 고객 세그먼트 & 페르소나', detail_description='1) 20–40대 커플: 주말 단기여행, 인생샷·감성 중심. 2) 20–30대 SNS 크리에이터/프리랜서: 콘텐츠·촬영지 탐색. 3) 소규모 가족·친구 그룹: 편안한 휴식·지역먹거리 체험. 4) 도심 직장인(원데이캉스): 근교 드라이브·힐링 목적.'), MarketingAnalysisDetail(detail_title='주요 USP(차별화 포인트)', detail_description='브랜드 스토리(‘Oblomov’ 느림의 미학), 군산 근대문화·항구 접근성, 소규모 부티크·프라이빗 체류감, 감성 포토존·인테리어로 SNS 확산 가능, 지역 먹거리·투어 연계로 체류 체감 가치 상승.'), MarketingAnalysisDetail(detail_title='경쟁 환경(직·간접 경쟁)', detail_description=\"직접: 군산 내 펜션·게스트하우스·한옥스테이(근대문화거리·항구 인근). 간접: 근교 글램핑·리조트·카페형 숙소, 당일투어(시장·박물관)로 체류대체 가능. 경쟁 우위는 '문학적 느림' 콘셉트+인스타블 친화적 비주얼.\"), MarketingAnalysisDetail(detail_title='시장 포지셔닝 제안', detail_description=\"중간 가격대의 부티크 스테이(가성비+감성), '주말 힐링·감성 촬영지' 중심 마케팅. 타깃 채널: 네이버 예약·에어비앤비·인스타그램·유튜브 숏폼. 지역 협업(카페·투어·해산물 체험)으로 패키지화.\")]), tags=['군산오블로모프', '부티크스테이', '힐링타임', '인생샷스팟', '주말여행'], facilities=['군산 근대거리·항구 근접', '문학적 느림·부티크 스테이', '프라이빗 객실·소규모 전용감', '감성 포토존·인테리어', '해산물·시장·근대투어 연계', '주말 단기여행·원데이캉스 수요'])}"
|
||||
]
|
||||
},
|
||||
"execution_count": 7,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"var2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "f3bf1d76-bd2a-43d5-8d39-f0ab2459701a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "KeyError",
|
||||
"evalue": "'selling_points'",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
||||
"\u001b[31mKeyError\u001b[39m Traceback (most recent call last)",
|
||||
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[43mvar2\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mselling_points\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m]\u001b[49m:\n\u001b[32m 2\u001b[39m \u001b[38;5;28mprint\u001b[39m(i[\u001b[33m'\u001b[39m\u001b[33mcategory\u001b[39m\u001b[33m'\u001b[39m])\n\u001b[32m 3\u001b[39m \u001b[38;5;28mprint\u001b[39m(i[\u001b[33m'\u001b[39m\u001b[33mkeywords\u001b[39m\u001b[33m'\u001b[39m])\n",
|
||||
"\u001b[31mKeyError\u001b[39m: 'selling_points'"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"for i in var2[\"selling_points\"]:\n",
|
||||
" print(i['category'])\n",
|
||||
" print(i['keywords'])\n",
|
||||
" print(i['description'])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "c89cf2eb-4f16-4dc5-90c6-df89191b4e39",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"var2[\"selling_points\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "231963d6-e209-41b3-8e78-2ad5d06943fe",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"var2[\"tags\"]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "f8260222-d5a2-4018-b465-a4943c82bd3f",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"lyric_prompt = \"\"\"\n",
|
||||
"[ROLE]\n",
|
||||
"You are a content marketing expert, brand strategist, and creative songwriter\n",
|
||||
"specializing in Korean pension / accommodation businesses.\n",
|
||||
"You create lyrics strictly based on Brand & Marketing Intelligence analysis\n",
|
||||
"and optimized for viral short-form video content.\n",
|
||||
"\n",
|
||||
"[INPUT]\n",
|
||||
"Business Name: {customer_name}\n",
|
||||
"Region: {region}\n",
|
||||
"Region Details: {detail_region_info}\n",
|
||||
"Brand & Marketing Intelligence Report: {marketing_intelligence_summary}\n",
|
||||
"Output Language: {language}\n",
|
||||
"\n",
|
||||
"[INTERNAL ANALYSIS – DO NOT OUTPUT]\n",
|
||||
"Internally analyze the following to guide all creative decisions:\n",
|
||||
"- Core brand identity and positioning\n",
|
||||
"- Emotional hooks derived from selling points\n",
|
||||
"- Target audience lifestyle, desires, and travel motivation\n",
|
||||
"- Regional atmosphere and symbolic imagery\n",
|
||||
"- How the stay converts into “shareable moments”\n",
|
||||
"- Which selling points must surface implicitly in lyrics\n",
|
||||
"\n",
|
||||
"[LYRICS & MUSIC CREATION TASK]\n",
|
||||
"Based on the Brand & Marketing Intelligence Report for [{customer_name} ({region})], generate:\n",
|
||||
"- Original promotional lyrics\n",
|
||||
"- Music attributes for AI music generation (Suno-compatible prompt)\n",
|
||||
"The output must be designed for VIRAL DIGITAL CONTENT\n",
|
||||
"(short-form video, reels, ads).\n",
|
||||
"\n",
|
||||
"[LYRICS REQUIREMENTS]\n",
|
||||
"Mandatory Inclusions:\n",
|
||||
"- Business name\n",
|
||||
"- Region name\n",
|
||||
"- Promotion subject\n",
|
||||
"- Promotional expressions including:\n",
|
||||
"{promotional_expressions[language]}\n",
|
||||
"\n",
|
||||
"Content Rules:\n",
|
||||
"- Lyrics must be emotionally driven, not descriptive listings\n",
|
||||
"- Selling points must be IMPLIED, not explained\n",
|
||||
"- Must sound natural when sung\n",
|
||||
"- Must feel like a lifestyle moment, not an advertisement\n",
|
||||
"\n",
|
||||
"Tone & Style:\n",
|
||||
"- Warm, emotional, and aspirational\n",
|
||||
"- Trendy, viral-friendly phrasing\n",
|
||||
"- Calm but memorable hooks\n",
|
||||
"- Suitable for travel / stay-related content\n",
|
||||
"\n",
|
||||
"[SONG & MUSIC ATTRIBUTES – FOR SUNO PROMPT]\n",
|
||||
"After the lyrics, generate a concise music prompt including:\n",
|
||||
"Song mood (emotional keywords)\n",
|
||||
"BPM range\n",
|
||||
"Recommended genres (max 2)\n",
|
||||
"Key musical motifs or instruments\n",
|
||||
"Overall vibe (1 short sentence)\n",
|
||||
"\n",
|
||||
"[CRITICAL LANGUAGE REQUIREMENT – ABSOLUTE RULE]\n",
|
||||
"ALL OUTPUT MUST BE 100% WRITTEN IN {language}.\n",
|
||||
"no mixed languages\n",
|
||||
"All names, places, and expressions must be in {language} \n",
|
||||
"Any violation invalidates the entire output\n",
|
||||
"\n",
|
||||
"[OUTPUT RULES – STRICT]\n",
|
||||
"{timing_rules}\n",
|
||||
"8–12 lines\n",
|
||||
"Full verse flow, immersive mood\n",
|
||||
"\n",
|
||||
"No explanations\n",
|
||||
"No headings\n",
|
||||
"No bullet points\n",
|
||||
"No analysis\n",
|
||||
"No extra text\n",
|
||||
"\n",
|
||||
"[FAILURE FORMAT]\n",
|
||||
"If generation is impossible:\n",
|
||||
"ERROR: Brief reason in English\n",
|
||||
"\"\"\"\n",
|
||||
"lyric_prompt_dict = {\n",
|
||||
" \"prompt_variables\" :\n",
|
||||
" [\n",
|
||||
" \"customer_name\",\n",
|
||||
" \"region\",\n",
|
||||
" \"detail_region_info\",\n",
|
||||
" \"marketing_intelligence_summary\",\n",
|
||||
" \"language\",\n",
|
||||
" \"promotional_expression_example\",\n",
|
||||
" \"timing_rules\",\n",
|
||||
" \n",
|
||||
" ],\n",
|
||||
" \"output_format\" : {\n",
|
||||
" \"format\": {\n",
|
||||
" \"type\": \"json_schema\",\n",
|
||||
" \"name\": \"lyric\",\n",
|
||||
" \"schema\": {\n",
|
||||
" \"type\":\"object\",\n",
|
||||
" \"properties\" : {\n",
|
||||
" \"lyric\" : { \n",
|
||||
" \"type\" : \"string\"\n",
|
||||
" }\n",
|
||||
" },\n",
|
||||
" \"required\": [\"lyric\"],\n",
|
||||
" \"additionalProperties\": False,\n",
|
||||
" },\n",
|
||||
" \"strict\": True\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "79edd82b-6f4c-43c7-9205-0b970afe06d7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"\n",
|
||||
"with open(\"./app/utils/prompts/marketing_prompt.txt\", \"w\") as fp:\n",
|
||||
" fp.write(marketing_prompt)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 17,
|
||||
"id": "65a5a2a6-06a5-4ee1-a796-406c86aefc20",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"with open(\"prompts/summarize_prompt.json\", \"r\") as fp:\n",
|
||||
" p = json.load(fp)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"id": "454d920f-e9ed-4fb2-806c-75b8f7033db9",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"{'prompt_variables': ['report', 'selling_points'],\n",
|
||||
" 'prompt': '\\n입력 : \\n분석 보고서\\n{report}\\n\\n셀링 포인트\\n{selling_points}\\n\\n위 분석 결과를 바탕으로, 주요 셀링 포인트를 다음 구조로 재정리하라.\\n\\n조건:\\n각 셀링 포인트는 반드시 ‘카테고리 → 태그 키워드 → 한 줄 설명’ 구조를 가질 것\\n태그 키워드는 UI 상에서 타원(oval) 형태의 시각적 태그로 사용될 것을 가정하여\\n- 3 ~ 6단어 이내\\n- 명사 또는 명사형 키워드로 작성\\n- 설명은 문장이 아닌, 짧은 ‘셀링 문구’ 형태로 작성할 것\\n- 광고·숏폼·상세페이지 어디에도 바로 재사용 가능해야 함\\n- 전체 셀링 포인트 개수는 5~7개로 제한\\n\\n출력 형식:\\n[카테고리명]\\n(태그 키워드)\\n- 한 줄 설명 문구\\n\\n예시: \\n[공간 정체성]\\n(100년 적산가옥 · 시간의 결)\\n- 하루를 ‘숙박’이 아닌 ‘체류’로 바꾸는 공간\\n\\n[입지 & 희소성]\\n(말랭이마을 · 로컬 히든플레이스)\\n- 관광지가 아닌, 군산을 아는 사람의 선택\\n\\n[프라이버시]\\n(독채 숙소 · 프라이빗 스테이)\\n- 누구의 방해도 없는 완전한 휴식 구조\\n\\n[비주얼 경쟁력]\\n(감성 인테리어 · 자연광 스폿)\\n- 찍는 순간 콘텐츠가 되는 공간 설계\\n\\n[타깃 최적화]\\n(커플 · 소규모 여행)\\n- 둘에게 가장 이상적인 공간 밀도\\n\\n[체류 경험]\\n(아무것도 안 해도 되는 하루)\\n- 일정 없이도 만족되는 하루 루틴\\n\\n[브랜드 포지션]\\n(호텔도 펜션도 아닌 아지트)\\n- 다시 돌아오고 싶은 개인적 장소\\n ',\n",
|
||||
" 'output_format': {'format': {'type': 'json_schema',\n",
|
||||
" 'name': 'tags',\n",
|
||||
" 'schema': {'type': 'object',\n",
|
||||
" 'properties': {'category': {'type': 'string'},\n",
|
||||
" 'tag_keywords': {'type': 'string'},\n",
|
||||
" 'description': {'type': 'string'}},\n",
|
||||
" 'required': ['category', 'tag_keywords', 'description'],\n",
|
||||
" 'additionalProperties': False},\n",
|
||||
" 'strict': True}}}"
|
||||
]
|
||||
},
|
||||
"execution_count": 18,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"p"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "c46abcda-d6a8-485e-92f1-526fb28c6b53",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"marketing_prompt_dict = {\n",
|
||||
" \"model\" : \"gpt-5-mini\",\n",
|
||||
" \"prompt_variables\" :\n",
|
||||
" [\n",
|
||||
" \"customer_name\",\n",
|
||||
" \"region\",\n",
|
||||
" \"detail_region_info\"\n",
|
||||
" ],\n",
|
||||
" \"output_format\" : {\n",
|
||||
" \"format\": {\n",
|
||||
" \"type\": \"json_schema\",\n",
|
||||
" \"name\": \"report\",\n",
|
||||
" \"schema\": {\n",
|
||||
" \"type\" : \"object\",\n",
|
||||
" \"properties\" : {\n",
|
||||
" \"report\" : {\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\" : {\n",
|
||||
" \"summary\" : {\"type\" : \"string\"},\n",
|
||||
" \"details\" : {\n",
|
||||
" \"type\" : \"array\",\n",
|
||||
" \"items\" : {\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\" : {\n",
|
||||
" \"detail_title\" : {\"type\" : \"string\"},\n",
|
||||
" \"detail_description\" : {\"type\" : \"string\"},\n",
|
||||
" },\n",
|
||||
" \"required\": [\"detail_title\", \"detail_description\"],\n",
|
||||
" \"additionalProperties\": False,\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
" },\n",
|
||||
" \"required\" : [\"summary\", \"details\"],\n",
|
||||
" \"additionalProperties\" : False\n",
|
||||
" },\n",
|
||||
" \"selling_points\" : {\n",
|
||||
" \"type\": \"array\",\n",
|
||||
" \"items\": {\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\" : {\n",
|
||||
" \"category\" : {\"type\" : \"string\"},\n",
|
||||
" \"keywords\" : {\"type\" : \"string\"},\n",
|
||||
" \"description\" : {\"type\" : \"string\"}\n",
|
||||
" },\n",
|
||||
" \"required\": [\"category\", \"keywords\", \"description\"],\n",
|
||||
" \"additionalProperties\": False,\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" \"tags\" : {\n",
|
||||
" \"type\": \"array\",\n",
|
||||
" \"items\": {\n",
|
||||
" \"type\": \"string\"\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
" \"contents_advise\" : {\"type\" : \"string\"}\n",
|
||||
" },\n",
|
||||
" \"required\": [\"report\", \"selling_points\", \"tags\", \"contents_advise\"],\n",
|
||||
" \"additionalProperties\": False,\n",
|
||||
" },\n",
|
||||
" \"strict\": True\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"with open(\"./app/utils/prompts/marketing_prompt.json\", \"w\") as fp:\n",
|
||||
" json.dump(marketing_prompt_dict, fp, ensure_ascii=False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 15,
|
||||
"id": "c3867dab-0c4e-46be-ad12-a9c02b5edb68",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"lyric_prompt = \"\"\"\n",
|
||||
"[ROLE]\n",
|
||||
"You are a content marketing expert, brand strategist, and creative songwriter\n",
|
||||
"specializing in Korean pension / accommodation businesses.\n",
|
||||
"You create lyrics strictly based on Brand & Marketing Intelligence analysis\n",
|
||||
"and optimized for viral short-form video content.\n",
|
||||
"\n",
|
||||
"[INPUT]\n",
|
||||
"Business Name: {customer_name}\n",
|
||||
"Region: {region}\n",
|
||||
"Region Details: {detail_region_info}\n",
|
||||
"Brand & Marketing Intelligence Report: {marketing_intelligence_summary}\n",
|
||||
"Output Language: {language}\n",
|
||||
"\n",
|
||||
"[INTERNAL ANALYSIS – DO NOT OUTPUT]\n",
|
||||
"Internally analyze the following to guide all creative decisions:\n",
|
||||
"- Core brand identity and positioning\n",
|
||||
"- Emotional hooks derived from selling points\n",
|
||||
"- Target audience lifestyle, desires, and travel motivation\n",
|
||||
"- Regional atmosphere and symbolic imagery\n",
|
||||
"- How the stay converts into “shareable moments”\n",
|
||||
"- Which selling points must surface implicitly in lyrics\n",
|
||||
"\n",
|
||||
"[LYRICS & MUSIC CREATION TASK]\n",
|
||||
"Based on the Brand & Marketing Intelligence Report for [{customer_name} ({region})], generate:\n",
|
||||
"- Original promotional lyrics\n",
|
||||
"- Music attributes for AI music generation (Suno-compatible prompt)\n",
|
||||
"The output must be designed for VIRAL DIGITAL CONTENT\n",
|
||||
"(short-form video, reels, ads).\n",
|
||||
"\n",
|
||||
"[LYRICS REQUIREMENTS]\n",
|
||||
"Mandatory Inclusions:\n",
|
||||
"- Business name\n",
|
||||
"- Region name\n",
|
||||
"- Promotion subject\n",
|
||||
"- Promotional expressions including:\n",
|
||||
"{promotional_expressions[language]}\n",
|
||||
"\n",
|
||||
"Content Rules:\n",
|
||||
"- Lyrics must be emotionally driven, not descriptive listings\n",
|
||||
"- Selling points must be IMPLIED, not explained\n",
|
||||
"- Must sound natural when sung\n",
|
||||
"- Must feel like a lifestyle moment, not an advertisement\n",
|
||||
"\n",
|
||||
"Tone & Style:\n",
|
||||
"- Warm, emotional, and aspirational\n",
|
||||
"- Trendy, viral-friendly phrasing\n",
|
||||
"- Calm but memorable hooks\n",
|
||||
"- Suitable for travel / stay-related content\n",
|
||||
"\n",
|
||||
"[SONG & MUSIC ATTRIBUTES – FOR SUNO PROMPT]\n",
|
||||
"After the lyrics, generate a concise music prompt including:\n",
|
||||
"Song mood (emotional keywords)\n",
|
||||
"BPM range\n",
|
||||
"Recommended genres (max 2)\n",
|
||||
"Key musical motifs or instruments\n",
|
||||
"Overall vibe (1 short sentence)\n",
|
||||
"\n",
|
||||
"[CRITICAL LANGUAGE REQUIREMENT – ABSOLUTE RULE]\n",
|
||||
"ALL OUTPUT MUST BE 100% WRITTEN IN {language}.\n",
|
||||
"no mixed languages\n",
|
||||
"All names, places, and expressions must be in {language} \n",
|
||||
"Any violation invalidates the entire output\n",
|
||||
"\n",
|
||||
"[OUTPUT RULES – STRICT]\n",
|
||||
"{timing_rules}\n",
|
||||
"8–12 lines\n",
|
||||
"Full verse flow, immersive mood\n",
|
||||
"\n",
|
||||
"No explanations\n",
|
||||
"No headings\n",
|
||||
"No bullet points\n",
|
||||
"No analysis\n",
|
||||
"No extra text\n",
|
||||
"\n",
|
||||
"[FAILURE FORMAT]\n",
|
||||
"If generation is impossible:\n",
|
||||
"ERROR: Brief reason in English\n",
|
||||
"\"\"\"\n",
|
||||
"with open(\"./app/utils/prompts/lyric_prompt.txt\", \"w\") as fp:\n",
|
||||
" fp.write(lyric_prompt)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 14,
|
||||
"id": "5736ca4b-c379-4cae-84a9-534cad9576c7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"lyric_prompt_dict = {\n",
|
||||
" \"model\" : \"gpt-5-mini\",\n",
|
||||
" \"prompt_variables\" :\n",
|
||||
" [\n",
|
||||
" \"customer_name\",\n",
|
||||
" \"region\",\n",
|
||||
" \"detail_region_info\",\n",
|
||||
" \"marketing_intelligence_summary\",\n",
|
||||
" \"language\",\n",
|
||||
" \"promotional_expression_example\",\n",
|
||||
" \"timing_rules\",\n",
|
||||
" \n",
|
||||
" ],\n",
|
||||
" \"output_format\" : {\n",
|
||||
" \"format\": {\n",
|
||||
" \"type\": \"json_schema\",\n",
|
||||
" \"name\": \"lyric\",\n",
|
||||
" \"schema\": {\n",
|
||||
" \"type\":\"object\",\n",
|
||||
" \"properties\" : {\n",
|
||||
" \"lyric\" : { \n",
|
||||
" \"type\" : \"string\"\n",
|
||||
" }\n",
|
||||
" },\n",
|
||||
" \"required\": [\"lyric\"],\n",
|
||||
" \"additionalProperties\": False,\n",
|
||||
" },\n",
|
||||
" \"strict\": True\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
"with open(\"./app/utils/prompts/lyric_prompt.json\", \"w\") as fp:\n",
|
||||
" json.dump(lyric_prompt_dict, fp, ensure_ascii=False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "430c8914-4e6a-4b53-8903-f454e7ccb8e2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.8"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
Loading…
Reference in New Issue