Compare commits

..

11 Commits

26 changed files with 965 additions and 884 deletions

View File

@ -24,8 +24,8 @@ from app.home.schemas.home_schema import (
ImageUploadResponse, ImageUploadResponse,
ImageUploadResultItem, ImageUploadResultItem,
ImageUrlItem, ImageUrlItem,
MarketingAnalysis,
ProcessedInfo, ProcessedInfo,
# MarketingAnalysis,
) )
from app.home.services.naver_search import naver_search_client from app.home.services.naver_search import naver_search_client
from app.utils.upload_blob_as_request import AzureBlobUploader from app.utils.upload_blob_as_request import AzureBlobUploader
@ -303,33 +303,11 @@ async def _crawling_logic(url:str):
# marketing_analysis = MarketingAnalysis(**parsed) # marketing_analysis = MarketingAnalysis(**parsed)
logger.debug( logger.debug(
f"[crawling] structured_report 구조 확인:\n" f"structured_report = {structured_report.model_dump()}"
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}"
) )
marketing_analysis = MarketingAnalysis( marketing_analysis = structured_report
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'])
step3_4_elapsed = (time.perf_counter() - step3_4_start) * 1000 step3_4_elapsed = (time.perf_counter() - step3_4_start) * 1000
logger.debug( logger.debug(
f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)" f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)"

View File

@ -1,7 +1,7 @@
from typing import Literal, Optional from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from app.utils.prompts.schemas import MarketingPromptOutput
class AttributeInfo(BaseModel): class AttributeInfo(BaseModel):
"""음악 속성 정보""" """음악 속성 정보"""
@ -186,12 +186,21 @@ class ProcessedInfo(BaseModel):
detail_region_info: str = Field(..., description="상세 지역 정보 (roadAddress)") 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="마케팅 분석 리포트") # class MarketingAnalysisReport(BaseModel):
tags: list[str] = Field(default_factory=list, description="추천 태그 목록") # """마케팅 분석 리포트 스키마"""
facilities: list[str] = Field(default_factory=list, description="추천 부대시설 목록") # 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): class CrawlingResponse(BaseModel):
@ -209,9 +218,110 @@ class CrawlingResponse(BaseModel):
"detail_region_info": "전북특별자치도 군산시 절골길 18" "detail_region_info": "전북특별자치도 군산시 절골길 18"
}, },
"marketing_analysis": { "marketing_analysis": {
"report": "마케팅 분석 리포트...", "brand_identity": {
"tags": ["힐링", "감성숙소"], "location_feature_analysis": "전북 군산시 절골길 일대는 도시의 편의성과 근교의 한적함을 동시에 누릴 수 있어 ‘조용한 재충전’ 수요에 적합합니다. 군산의 레트로 감성과 주변 관광 동선 결합이 쉬워 1~2박 체류형 여행지로 매력적입니다.",
"facilities": ["조식", "주차"] "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": [
"군산숙소",
"군산감성숙소",
"전북숙소추천",
"군산여행",
"커플스테이",
"주말여행",
"감성스테이",
"조용한숙소",
"힐링스테이",
"스테이머뭄"
]
} }
} }
} }
@ -226,8 +336,8 @@ class CrawlingResponse(BaseModel):
processed_info: Optional[ProcessedInfo] = Field( processed_info: Optional[ProcessedInfo] = Field(
None, description="가공된 장소 정보 (customer_name, region, detail_region_info)" None, description="가공된 장소 정보 (customer_name, region, detail_region_info)"
) )
marketing_analysis: Optional[MarketingAnalysis] = Field( marketing_analysis: Optional[MarketingPromptOutput] = Field(
None, description="마케팅 분석 결과 (report, tags, facilities). 실패 시 null" None, description="마케팅 분석 결과 . 실패 시 null"
) )

View File

@ -1,12 +1,13 @@
import json import json
import re import re
from pydantic import BaseModel
from openai import AsyncOpenAI from openai import AsyncOpenAI
from app.utils.logger import get_logger from app.utils.logger import get_logger
from config import apikey_settings, recovery_settings from config import apikey_settings, recovery_settings
from app.utils.prompts.prompts import Prompt from app.utils.prompts.prompts import Prompt
# 로거 설정 # 로거 설정
logger = get_logger("chatgpt") logger = get_logger("chatgpt")
@ -32,18 +33,15 @@ class ChatgptService:
timeout=self.timeout 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}] content = [{"type": "input_text", "text": prompt}]
last_error = None last_error = None
for attempt in range(self.max_retries + 1): for attempt in range(self.max_retries + 1):
response = await self.client.responses.create( response = await self.client.responses.parse(
model=model, model=model,
input=[{"role": "user", "content": content}], input=[{"role": "user", "content": content}],
text=output_format, text_format=output_format
timeout=self.timeout
) )
# Response 디버그 로깅 # Response 디버그 로깅
logger.debug(f"[ChatgptService] Response ID: {response.id}") logger.debug(f"[ChatgptService] Response ID: {response.id}")
logger.debug(f"[ChatgptService] Response status: {response.status}") logger.debug(f"[ChatgptService] Response status: {response.status}")
@ -52,8 +50,8 @@ class ChatgptService:
# status 확인: completed, failed, incomplete, cancelled, queued, in_progress # status 확인: completed, failed, incomplete, cancelled, queued, in_progress
if response.status == "completed": 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}") 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) structured_output = response.output_parsed
return structured_output or {} return structured_output #.model_dump() or {}
# 에러 상태 처리 # 에러 상태 처리
if response.status == "failed": 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}") logger.info(f"[ChatgptService] Starting GPT request with structured output with model: {prompt.prompt_model}")
# GPT API 호출 # 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 return response

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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"]

View File

@ -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
}
}
}

View File

@ -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()

View File

@ -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
}
}
}

View File

@ -1,53 +0,0 @@
입력 :
분석 보고서
{report}
셀링 포인트
{selling_points}
위 분석 결과를 바탕으로, 주요 셀링 포인트를 다음 구조로 재정리하라.
조건:
각 셀링 포인트는 반드시 ‘카테고리 → 태그 키워드 → 한 줄 설명’ 구조를 가질 것
태그 키워드는 UI 상에서 타원(oval) 형태의 시각적 태그로 사용될 것을 가정하여
- 3 ~ 6단어 이내
- 명사 또는 명사형 키워드로 작성
- 설명은 문장이 아닌, 짧은 ‘셀링 문구’ 형태로 작성할 것
- 광고·숏폼·상세페이지 어디에도 바로 재사용 가능해야 함
- 전체 셀링 포인트 개수는 5~7개로 제한
출력 형식:
[카테고리명]
(태그 키워드)
- 한 줄 설명 문구
예시:
[공간 정체성]
(100년 적산가옥 · 시간의 결)
- 하루를 ‘숙박’이 아닌 ‘체류’로 바꾸는 공간
[입지 & 희소성]
(말랭이마을 · 로컬 히든플레이스)
- 관광지가 아닌, 군산을 아는 사람의 선택
[프라이버시]
(독채 숙소 · 프라이빗 스테이)
- 누구의 방해도 없는 완전한 휴식 구조
[비주얼 경쟁력]
(감성 인테리어 · 자연광 스폿)
- 찍는 순간 콘텐츠가 되는 공간 설계
[타깃 최적화]
(커플 · 소규모 여행)
- 둘에게 가장 이상적인 공간 밀도
[체류 경험]
(아무것도 안 해도 되는 하루)
- 일정 없이도 만족되는 하루 루틴
[브랜드 포지션]
(호텔도 펜션도 아닌 아지트)
- 다시 돌아오고 싶은 개인적 장소

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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"]

View File

@ -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"]

View File

@ -1,76 +1,57 @@
import os, json import os, json
from abc import ABCMeta from pydantic import BaseModel
from config import prompt_settings from config import prompt_settings
from app.utils.logger import get_logger from app.utils.logger import get_logger
from app.utils.prompts.schemas import *
logger = get_logger("prompt") logger = get_logger("prompt")
class Prompt(): class Prompt():
prompt_name : str # ex) marketing_prompt
prompt_template_path : str #프롬프트 경로 prompt_template_path : str #프롬프트 경로
prompt_template : str # fstring 포맷 prompt_template : str # fstring 포맷
prompt_input : list
prompt_output : dict
prompt_model : str prompt_model : str
def __init__(self, prompt_name, prompt_template_path): prompt_input_class = BaseModel # pydantic class 자체를(instance 아님) 변수로 가짐
self.prompt_name = prompt_name 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_path = prompt_template_path
self.prompt_template, prompt_dict = self.read_prompt() self.prompt_input_class = prompt_input_class
self.prompt_input = prompt_dict['prompt_variables'] self.prompt_output_class = prompt_output_class
self.prompt_output = prompt_dict['output_format'] self.prompt_template = self.read_prompt()
self.prompt_model = prompt_dict.get('model', "gpt-5-mini") self.prompt_model = prompt_model
def _reload_prompt(self): def _reload_prompt(self):
self.prompt_template, prompt_dict = self.read_prompt() self.prompt_template = 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]: def read_prompt(self) -> tuple[str, dict]:
template_text_path = self.prompt_template_path + ".txt" with open(self.prompt_template_path, "r") as fp:
prompt_dict_path = self.prompt_template_path + ".json"
with open(template_text_path, "r") as fp:
prompt_template = fp.read() 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: 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 = self.prompt_template
build_template = build_template.format(**verified_input.model_dump())
logger.debug(f"build_template: {build_template}") logger.debug(f"build_template: {build_template}")
logger.debug(f"input_data: {input_data}") logger.debug(f"input_data: {input_data}")
build_template = build_template.format(**input_data)
return build_template 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( 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_FILE_NAME),
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.MARKETING_PROMPT_NAME) prompt_input_class = MarketingPromptInput,
) prompt_output_class = MarketingPromptOutput,
prompt_model = prompt_settings.MARKETING_PROMPT_MODEL
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( lyric_prompt = Prompt(
prompt_name=prompt_settings.LYLIC_PROMPT_NAME, prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.LYRIC_PROMPT_FILE_NAME),
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.LYLIC_PROMPT_NAME) prompt_input_class = LyricPromptInput,
prompt_output_class = LyricPromptOutput,
prompt_model = prompt_settings.LYRIC_PROMPT_MODEL
) )
def reload_all_prompt(): def reload_all_prompt():
marketing_prompt._reload_prompt() marketing_prompt._reload_prompt()
summarize_prompt._reload_prompt()
lyric_prompt._reload_prompt() lyric_prompt._reload_prompt()

View File

@ -0,0 +1,2 @@
from .lyric import LyricPromptInput, LyricPromptOutput
from .marketing import MarketingPromptInput, MarketingPromptOutput

View File

@ -0,0 +1,17 @@
from pydantic import BaseModel, Field
from typing import List
# Input 정의
class LyricPromptInput(BaseModel):
customer_name : str = Field(..., description = "마케팅 대상 사업체 이름")
region : str = Field(..., description = "마케팅 대상 지역")
detail_region_info : str = Field(..., description = "마케팅 대상 지역 상세")
marketing_intelligence_summary : str = Field(..., 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용 프롬프트")

View File

@ -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="타겟 키워드 리스트")

View File

@ -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
}
}
}

View File

@ -1,53 +0,0 @@
입력 :
분석 보고서
{report}
셀링 포인트
{selling_points}
위 분석 결과를 바탕으로, 주요 셀링 포인트를 다음 구조로 재정리하라.
조건:
각 셀링 포인트는 반드시 ‘카테고리 → 태그 키워드 → 한 줄 설명’ 구조를 가질 것
태그 키워드는 UI 상에서 타원(oval) 형태의 시각적 태그로 사용될 것을 가정하여
- 3 ~ 6단어 이내
- 명사 또는 명사형 키워드로 작성
- 설명은 문장이 아닌, 짧은 ‘셀링 문구’ 형태로 작성할 것
- 광고·숏폼·상세페이지 어디에도 바로 재사용 가능해야 함
- 전체 셀링 포인트 개수는 5~7개로 제한
출력 형식:
[카테고리명]
(태그 키워드)
- 한 줄 설명 문구
예시:
[공간 정체성]
(100년 적산가옥 · 시간의 결)
- 하루를 ‘숙박’이 아닌 ‘체류’로 바꾸는 공간
[입지 & 희소성]
(말랭이마을 · 로컬 히든플레이스)
- 관광지가 아닌, 군산을 아는 사람의 선택
[프라이버시]
(독채 숙소 · 프라이빗 스테이)
- 누구의 방해도 없는 완전한 휴식 구조
[비주얼 경쟁력]
(감성 인테리어 · 자연광 스폿)
- 찍는 순간 콘텐츠가 되는 공간 설계
[타깃 최적화]
(커플 · 소규모 여행)
- 둘에게 가장 이상적인 공간 밀도
[체류 경험]
(아무것도 안 해도 되는 하루)
- 일정 없이도 만족되는 하루 루틴
[브랜드 포지션]
(호텔도 펜션도 아닌 아지트)
- 다시 돌아오고 싶은 개인적 장소

View File

@ -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.

View File

@ -159,10 +159,13 @@ class CreatomateSettings(BaseSettings):
model_config = _base_config model_config = _base_config
class PromptSettings(BaseSettings): class PromptSettings(BaseSettings):
PROMPT_FOLDER_ROOT : str = Field(default="./app/utils/prompts") PROMPT_FOLDER_ROOT : str = Field(default="./app/utils/prompts/templates")
MARKETING_PROMPT_NAME : str = Field(default="marketing_prompt")
SUMMARIZE_PROMPT_NAME : str = Field(default="summarize_prompt") MARKETING_PROMPT_FILE_NAME : str = Field(default="marketing_prompt.txt")
LYLIC_PROMPT_NAME : str = Field(default="lyric_prompt") 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 model_config = _base_config

694
main_tester.ipynb Normal file
View File

@ -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&timestamp=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='절골길 인근의 주택가·언덕형 지형, 조용한 체류 환경. 군산 근대역사문화거리·항구·현지 시장 접근권(차로 1025분권). 문학적·레트로 감성 콘셉트(오블로모프 → 느림·휴식)으로 도심형 ‘감성 은신처’ 포지셔닝 가능.'), MarketingAnalysisDetail(detail_title='예약 결정 요인(고객 행동)', detail_description='사진·비주얼(첫 인상) → 콘셉트·프라이버시(전용공간 여부) → 접근성(차·대중교통 소요) → 가격 대비 가치·후기 → 체크인 편의성(셀프체크인 여부) → 지역 체험(먹거리·근대문화 투어) 순으로 예약 전환 영향.'), MarketingAnalysisDetail(detail_title='타깃 고객 세그먼트 & 페르소나', detail_description='1) 2040대 커플: 주말 단기여행, 인생샷·감성 중심. 2) 2030대 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",
"812 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",
"812 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
}