프롬프트 처리 구조 변경 및 마케팅/가사 프롬프트 최신화
parent
ba26284451
commit
4e15e44cbe
|
|
@ -27,6 +27,7 @@ from app.utils.upload_blob_as_request import AzureBlobUploader
|
||||||
from app.utils.chatgpt_prompt import ChatgptService
|
from app.utils.chatgpt_prompt import ChatgptService
|
||||||
from app.utils.common import generate_task_id
|
from app.utils.common import generate_task_id
|
||||||
from app.utils.nvMapScraper import NvMapScraper, GraphQLException
|
from app.utils.nvMapScraper import NvMapScraper, GraphQLException
|
||||||
|
from app.utils.prompts.prompts import marketing_prompt
|
||||||
|
|
||||||
# 로거 설정
|
# 로거 설정
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -172,34 +173,50 @@ async def crawling(request_body: CrawlingRequest):
|
||||||
try:
|
try:
|
||||||
# Step 3-1: ChatGPT 서비스 초기화
|
# Step 3-1: ChatGPT 서비스 초기화
|
||||||
step3_1_start = time.perf_counter()
|
step3_1_start = time.perf_counter()
|
||||||
chatgpt_service = ChatgptService(
|
chatgpt_service = ChatgptService()
|
||||||
customer_name=customer_name,
|
|
||||||
region=region,
|
|
||||||
detail_region_info=road_address or "",
|
|
||||||
)
|
|
||||||
step3_1_elapsed = (time.perf_counter() - step3_1_start) * 1000
|
step3_1_elapsed = (time.perf_counter() - step3_1_start) * 1000
|
||||||
print(f"[crawling] Step 3-1: 서비스 초기화 완료 ({step3_1_elapsed:.1f}ms)")
|
print(f"[crawling] Step 3-1: 서비스 초기화 완료 ({step3_1_elapsed:.1f}ms)")
|
||||||
|
|
||||||
# Step 3-2: 프롬프트 생성
|
# Step 3-2: 프롬프트 생성
|
||||||
step3_2_start = time.perf_counter()
|
# step3_2_start = time.perf_counter()
|
||||||
prompt = chatgpt_service.build_market_analysis_prompt()
|
input_marketing_data = {
|
||||||
step3_2_elapsed = (time.perf_counter() - step3_2_start) * 1000
|
"customer_name" : customer_name,
|
||||||
print(f"[crawling] Step 3-2: 프롬프트 생성 완료 - {len(prompt)}자 ({step3_2_elapsed:.1f}ms)")
|
"region" : region,
|
||||||
|
"detail_region_info" : road_address or ""
|
||||||
|
}
|
||||||
|
# prompt = chatgpt_service.build_market_analysis_prompt()
|
||||||
|
# prompt1 = marketing_prompt.build_prompt(input_marketing_data)
|
||||||
|
# step3_2_elapsed = (time.perf_counter() - step3_2_start) * 1000
|
||||||
|
# print(f"[crawling] Step 3-2: 프롬프트 생성 완료 - ({step3_2_elapsed:.1f}ms)")
|
||||||
|
|
||||||
# Step 3-3: GPT API 호출
|
# Step 3-3: GPT API 호출
|
||||||
step3_3_start = time.perf_counter()
|
step3_3_start = time.perf_counter()
|
||||||
raw_response = await chatgpt_service.generate(prompt)
|
structured_report = await chatgpt_service.generate_structured_output(marketing_prompt, input_marketing_data)
|
||||||
step3_3_elapsed = (time.perf_counter() - step3_3_start) * 1000
|
step3_3_elapsed = (time.perf_counter() - step3_3_start) * 1000
|
||||||
logger.info(f"[crawling] Step 3-3: GPT API 호출 완료 - 응답 {len(raw_response)}자 ({step3_3_elapsed:.1f}ms)")
|
logger.info(f"[crawling] Step 3-3: GPT API 호출 완료 - ({step3_3_elapsed:.1f}ms)")
|
||||||
print(f"[crawling] Step 3-3: GPT API 호출 완료 - 응답 {len(raw_response)}자 ({step3_3_elapsed:.1f}ms)")
|
print(f"[crawling] Step 3-3: GPT API 호출 완료 - ({step3_3_elapsed:.1f}ms)")
|
||||||
|
|
||||||
|
|
||||||
# Step 3-4: 응답 파싱 (크롤링에서 가져온 facility_info 전달)
|
# Step 3-4: 응답 파싱 (크롤링에서 가져온 facility_info 전달)
|
||||||
step3_4_start = time.perf_counter()
|
step3_4_start = time.perf_counter()
|
||||||
print(f"[crawling] Step 3-4: 응답 파싱 시작 - facility_info: {scraper.facility_info}")
|
print(f"[crawling] Step 3-4: 응답 파싱 시작 - facility_info: {scraper.facility_info}")
|
||||||
parsed = await chatgpt_service.parse_marketing_analysis(
|
|
||||||
raw_response, facility_info=scraper.facility_info
|
# 요약 Deprecated / 20250115 / Selling points를 첫 prompt에서 추출 중
|
||||||
|
# parsed = await chatgpt_service.parse_marketing_analysis(
|
||||||
|
# structured_report, facility_info=scraper.facility_info
|
||||||
|
# )
|
||||||
|
|
||||||
|
# marketing_analysis = MarketingAnalysis(**parsed)
|
||||||
|
|
||||||
|
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로 변수와 데이터구조 변경할 것
|
||||||
)
|
)
|
||||||
marketing_analysis = MarketingAnalysis(**parsed)
|
# 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
|
||||||
print(f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)")
|
print(f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)")
|
||||||
|
|
||||||
|
|
@ -236,7 +253,7 @@ async def crawling(request_body: CrawlingRequest):
|
||||||
"image_list": scraper.image_link_list,
|
"image_list": scraper.image_link_list,
|
||||||
"image_count": len(scraper.image_link_list) if scraper.image_link_list else 0,
|
"image_count": len(scraper.image_link_list) if scraper.image_link_list else 0,
|
||||||
"processed_info": processed_info,
|
"processed_info": processed_info,
|
||||||
"marketing_analysis": marketing_analysis,
|
"marketing_analysis": marketing_analysis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ from app.lyric.worker.lyric_task import generate_lyric_background
|
||||||
from app.utils.chatgpt_prompt import ChatgptService
|
from app.utils.chatgpt_prompt import ChatgptService
|
||||||
from app.utils.pagination import PaginatedResponse, get_paginated
|
from app.utils.pagination import PaginatedResponse, get_paginated
|
||||||
|
|
||||||
|
from app.utils.prompts.prompts import lyric_prompt
|
||||||
|
|
||||||
router = APIRouter(prefix="/lyric", tags=["lyric"])
|
router = APIRouter(prefix="/lyric", tags=["lyric"])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -224,6 +226,7 @@ async def generate_lyric(
|
||||||
|
|
||||||
request_start = time.perf_counter()
|
request_start = time.perf_counter()
|
||||||
task_id = request_body.task_id
|
task_id = request_body.task_id
|
||||||
|
|
||||||
|
|
||||||
print(f"[generate_lyric] ========== START ==========")
|
print(f"[generate_lyric] ========== START ==========")
|
||||||
print(
|
print(
|
||||||
|
|
@ -237,16 +240,45 @@ async def generate_lyric(
|
||||||
step1_start = time.perf_counter()
|
step1_start = time.perf_counter()
|
||||||
print(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...")
|
print(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...")
|
||||||
|
|
||||||
service = ChatgptService(
|
# service = ChatgptService(
|
||||||
customer_name=request_body.customer_name,
|
# customer_name=request_body.customer_name,
|
||||||
region=request_body.region,
|
# region=request_body.region,
|
||||||
detail_region_info=request_body.detail_region_info or "",
|
# detail_region_info=request_body.detail_region_info or "",
|
||||||
language=request_body.language,
|
# language=request_body.language,
|
||||||
)
|
# )
|
||||||
prompt = service.build_lyrics_prompt()
|
|
||||||
|
# prompt = service.build_lyrics_prompt()
|
||||||
|
# 원래는 실제 사용할 프롬프트가 들어가야 하나, 로직이 변경되어 이 시점에서 이곳에서 프롬프트를 생성할 이유가 없어서 삭제됨.
|
||||||
|
# 기존 코드와의 호환을 위해 동일한 로직으로 프롬프트 생성
|
||||||
|
|
||||||
|
promotional_expressions = {
|
||||||
|
"Korean" : "인스타 감성, 사진같은 하루, 힐링, 여행, 감성 숙소",
|
||||||
|
"English" : "Instagram vibes, picture-perfect day, healing, travel, getaway",
|
||||||
|
"Chinese" : "网红打卡, 治愈系, 旅行, 度假, 拍照圣地",
|
||||||
|
"Japanese" : "インスタ映え, 写真のような一日, 癒し, 旅行, 絶景",
|
||||||
|
"Thai" : "ที่พักสวย, ฮีลใจ, เที่ยว, ถ่ายรูป, วิวสวย",
|
||||||
|
"Vietnamese" : "check-in đẹp, healing, du lịch, nghỉ dưỡng, view đẹp"
|
||||||
|
}# HARD CODED, 어디에 정리하지? 아직 정리되지 않음
|
||||||
|
|
||||||
|
timing_rules = {
|
||||||
|
"60s" : """
|
||||||
|
8–12 lines
|
||||||
|
Full verse flow, immersive mood
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
lyric_input_data = {
|
||||||
|
"customer_name" : request_body.customer_name,
|
||||||
|
"region" : request_body.region,
|
||||||
|
"detail_region_info" : request_body.detail_region_info or "",
|
||||||
|
"marketing_intelligence_summary" : None, # task_idx 변경 후 marketing intelligence summary DB에 저장하고 사용할 필요가 있음
|
||||||
|
"language" : request_body.language,
|
||||||
|
"promotional_expression_example" : promotional_expressions[request_body.language],
|
||||||
|
"timing_rules" : timing_rules["60s"], # 아직은 선택지 하나
|
||||||
|
}
|
||||||
|
estimated_prompt = lyric_prompt.build_prompt(lyric_input_data)
|
||||||
|
|
||||||
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||||
print(f"[generate_lyric] Step 1 완료 - 프롬프트 {len(prompt)}자 ({step1_elapsed:.1f}ms)")
|
#print(f"[generate_lyric] Step 1 완료 - 프롬프트 {len(prompt)}자 ({step1_elapsed:.1f}ms)")
|
||||||
|
|
||||||
# ========== Step 2: Project 테이블에 데이터 저장 ==========
|
# ========== Step 2: Project 테이블에 데이터 저장 ==========
|
||||||
step2_start = time.perf_counter()
|
step2_start = time.perf_counter()
|
||||||
|
|
@ -270,11 +302,12 @@ async def generate_lyric(
|
||||||
step3_start = time.perf_counter()
|
step3_start = time.perf_counter()
|
||||||
print(f"[generate_lyric] Step 3: Lyric 저장 (processing)...")
|
print(f"[generate_lyric] Step 3: Lyric 저장 (processing)...")
|
||||||
|
|
||||||
|
estimated_prompt = lyric_prompt.build_prompt(lyric_input_data)
|
||||||
lyric = Lyric(
|
lyric = Lyric(
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
status="processing",
|
status="processing",
|
||||||
lyric_prompt=prompt,
|
lyric_prompt=estimated_prompt,
|
||||||
lyric_result=None,
|
lyric_result=None,
|
||||||
language=request_body.language,
|
language=request_body.language,
|
||||||
)
|
)
|
||||||
|
|
@ -292,8 +325,8 @@ async def generate_lyric(
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
generate_lyric_background,
|
generate_lyric_background,
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
prompt=prompt,
|
prompt=lyric_prompt,
|
||||||
language=request_body.language,
|
lyric_input_data=lyric_input_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
step4_elapsed = (time.perf_counter() - step4_start) * 1000
|
step4_elapsed = (time.perf_counter() - step4_start) * 1000
|
||||||
|
|
|
||||||
|
|
@ -1,852 +0,0 @@
|
||||||
import random
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from fastapi import Request, status
|
|
||||||
from fastapi.exceptions import HTTPException
|
|
||||||
from sqlalchemy import Connection, text
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
|
||||||
|
|
||||||
from app.lyric.schemas.lyrics_schema import (
|
|
||||||
AttributeData,
|
|
||||||
PromptTemplateData,
|
|
||||||
SongFormData,
|
|
||||||
SongSampleData,
|
|
||||||
StoreData,
|
|
||||||
)
|
|
||||||
from app.utils.chatgpt_prompt import chatgpt_api
|
|
||||||
|
|
||||||
|
|
||||||
async def get_store_info(conn: Connection) -> List[StoreData]:
|
|
||||||
try:
|
|
||||||
query = """SELECT * FROM store_default_info;"""
|
|
||||||
result = await conn.execute(text(query))
|
|
||||||
|
|
||||||
all_store_info = [
|
|
||||||
StoreData(
|
|
||||||
id=row[0],
|
|
||||||
store_info=row[1],
|
|
||||||
store_name=row[2],
|
|
||||||
store_category=row[3],
|
|
||||||
store_region=row[4],
|
|
||||||
store_address=row[5],
|
|
||||||
store_phone_number=row[6],
|
|
||||||
created_at=row[7],
|
|
||||||
)
|
|
||||||
for row in result
|
|
||||||
]
|
|
||||||
|
|
||||||
result.close()
|
|
||||||
return all_store_info
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_attribute(conn: Connection) -> List[AttributeData]:
|
|
||||||
try:
|
|
||||||
query = """SELECT * FROM attribute;"""
|
|
||||||
result = await conn.execute(text(query))
|
|
||||||
|
|
||||||
all_attribute = [
|
|
||||||
AttributeData(
|
|
||||||
id=row[0],
|
|
||||||
attr_category=row[1],
|
|
||||||
attr_value=row[2],
|
|
||||||
created_at=row[3],
|
|
||||||
)
|
|
||||||
for row in result
|
|
||||||
]
|
|
||||||
|
|
||||||
result.close()
|
|
||||||
return all_attribute
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_attribute(conn: Connection) -> List[AttributeData]:
|
|
||||||
try:
|
|
||||||
query = """SELECT * FROM attribute;"""
|
|
||||||
result = await conn.execute(text(query))
|
|
||||||
|
|
||||||
all_attribute = [
|
|
||||||
AttributeData(
|
|
||||||
id=row[0],
|
|
||||||
attr_category=row[1],
|
|
||||||
attr_value=row[2],
|
|
||||||
created_at=row[3],
|
|
||||||
)
|
|
||||||
for row in result
|
|
||||||
]
|
|
||||||
|
|
||||||
result.close()
|
|
||||||
return all_attribute
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_sample_song(conn: Connection) -> List[SongSampleData]:
|
|
||||||
try:
|
|
||||||
query = """SELECT * FROM song_sample;"""
|
|
||||||
result = await conn.execute(text(query))
|
|
||||||
|
|
||||||
all_sample_song = [
|
|
||||||
SongSampleData(
|
|
||||||
id=row[0],
|
|
||||||
ai=row[1],
|
|
||||||
ai_model=row[2],
|
|
||||||
genre=row[3],
|
|
||||||
sample_song=row[4],
|
|
||||||
)
|
|
||||||
for row in result
|
|
||||||
]
|
|
||||||
|
|
||||||
result.close()
|
|
||||||
return all_sample_song
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_prompt_template(conn: Connection) -> List[PromptTemplateData]:
|
|
||||||
try:
|
|
||||||
query = """SELECT * FROM prompt_template;"""
|
|
||||||
result = await conn.execute(text(query))
|
|
||||||
|
|
||||||
all_prompt_template = [
|
|
||||||
PromptTemplateData(
|
|
||||||
id=row[0],
|
|
||||||
description=row[1],
|
|
||||||
prompt=row[2],
|
|
||||||
)
|
|
||||||
for row in result
|
|
||||||
]
|
|
||||||
|
|
||||||
result.close()
|
|
||||||
return all_prompt_template
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_song_result(conn: Connection) -> List[PromptTemplateData]:
|
|
||||||
try:
|
|
||||||
query = """SELECT * FROM prompt_template;"""
|
|
||||||
result = await conn.execute(text(query))
|
|
||||||
|
|
||||||
all_prompt_template = [
|
|
||||||
PromptTemplateData(
|
|
||||||
id=row[0],
|
|
||||||
description=row[1],
|
|
||||||
prompt=row[2],
|
|
||||||
)
|
|
||||||
for row in result
|
|
||||||
]
|
|
||||||
|
|
||||||
result.close()
|
|
||||||
return all_prompt_template
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def make_song_result(request: Request, conn: Connection):
|
|
||||||
try:
|
|
||||||
# 1. Form 데이터 파싱
|
|
||||||
form_data = await SongFormData.from_form(request)
|
|
||||||
|
|
||||||
print(f"\n{'=' * 60}")
|
|
||||||
print(f"Store ID: {form_data.store_id}")
|
|
||||||
print(f"Lyrics IDs: {form_data.lyrics_ids}")
|
|
||||||
print(f"Prompt IDs: {form_data.prompts}")
|
|
||||||
print(f"{'=' * 60}\n")
|
|
||||||
|
|
||||||
# 2. Store 정보 조회
|
|
||||||
store_query = """
|
|
||||||
SELECT * FROM store_default_info WHERE id=:id;
|
|
||||||
"""
|
|
||||||
store_result = await conn.execute(text(store_query), {"id": form_data.store_id})
|
|
||||||
|
|
||||||
all_store_info = [
|
|
||||||
StoreData(
|
|
||||||
id=row[0],
|
|
||||||
store_info=row[1],
|
|
||||||
store_name=row[2],
|
|
||||||
store_category=row[3],
|
|
||||||
store_region=row[4],
|
|
||||||
store_address=row[5],
|
|
||||||
store_phone_number=row[6],
|
|
||||||
created_at=row[7],
|
|
||||||
)
|
|
||||||
for row in store_result
|
|
||||||
]
|
|
||||||
|
|
||||||
if not all_store_info:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=f"Store not found: {form_data.store_id}",
|
|
||||||
)
|
|
||||||
|
|
||||||
store_info = all_store_info[0]
|
|
||||||
print(f"Store: {store_info.store_name}")
|
|
||||||
|
|
||||||
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
|
||||||
|
|
||||||
# 4. Sample Song 조회 및 결합
|
|
||||||
combined_sample_song = None
|
|
||||||
|
|
||||||
if form_data.lyrics_ids:
|
|
||||||
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
|
||||||
|
|
||||||
lyrics_query = """
|
|
||||||
SELECT sample_song FROM song_sample
|
|
||||||
WHERE id IN :ids
|
|
||||||
ORDER BY created_at;
|
|
||||||
"""
|
|
||||||
lyrics_result = await conn.execute(
|
|
||||||
text(lyrics_query), {"ids": tuple(form_data.lyrics_ids)}
|
|
||||||
)
|
|
||||||
|
|
||||||
sample_songs = [
|
|
||||||
row.sample_song for row in lyrics_result.fetchall() if row.sample_song
|
|
||||||
]
|
|
||||||
|
|
||||||
if sample_songs:
|
|
||||||
combined_sample_song = "\n\n".join(
|
|
||||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
|
||||||
)
|
|
||||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
|
||||||
else:
|
|
||||||
print("샘플 가사가 비어있습니다")
|
|
||||||
else:
|
|
||||||
print("선택된 lyrics가 없습니다")
|
|
||||||
|
|
||||||
# 5. 템플릿 가져오기
|
|
||||||
if not form_data.prompts:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="프롬프트 ID가 필요합니다",
|
|
||||||
)
|
|
||||||
|
|
||||||
print("템플릿 가져오기")
|
|
||||||
|
|
||||||
prompts_query = """
|
|
||||||
SELECT * FROM prompt_template WHERE id=:id;
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ✅ 수정: store_query → prompts_query
|
|
||||||
prompts_result = await conn.execute(
|
|
||||||
text(prompts_query), {"id": form_data.prompts}
|
|
||||||
)
|
|
||||||
|
|
||||||
prompts_info = [
|
|
||||||
PromptTemplateData(
|
|
||||||
id=row[0],
|
|
||||||
description=row[1],
|
|
||||||
prompt=row[2],
|
|
||||||
)
|
|
||||||
for row in prompts_result
|
|
||||||
]
|
|
||||||
|
|
||||||
if not prompts_info:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=f"Prompt not found: {form_data.prompts}",
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt = prompts_info[0]
|
|
||||||
print(f"Prompt Template: {prompt.prompt}")
|
|
||||||
|
|
||||||
# ✅ 6. 프롬프트 조합
|
|
||||||
updated_prompt = prompt.prompt.replace("###", form_data.attributes_str).format(
|
|
||||||
name=store_info.store_name or "",
|
|
||||||
address=store_info.store_address or "",
|
|
||||||
category=store_info.store_category or "",
|
|
||||||
description=store_info.store_info or "",
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_prompt += f"""
|
|
||||||
|
|
||||||
다음은 참고해야 하는 샘플 가사 정보입니다.
|
|
||||||
|
|
||||||
샘플 가사를 참고하여 작곡을 해주세요.
|
|
||||||
|
|
||||||
{combined_sample_song}
|
|
||||||
"""
|
|
||||||
|
|
||||||
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n")
|
|
||||||
|
|
||||||
# 7. 모델에게 요청
|
|
||||||
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
|
|
||||||
|
|
||||||
# 글자 수 계산
|
|
||||||
total_chars_with_space = len(generated_lyrics)
|
|
||||||
total_chars_without_space = len(
|
|
||||||
generated_lyrics.replace(" ", "")
|
|
||||||
.replace("\n", "")
|
|
||||||
.replace("\r", "")
|
|
||||||
.replace("\t", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
# final_lyrics 생성
|
|
||||||
final_lyrics = f"""속성 {form_data.attributes_str}
|
|
||||||
전체 글자 수 (공백 포함): {total_chars_with_space}자
|
|
||||||
전체 글자 수 (공백 제외): {total_chars_without_space}자\r\n\r\n{generated_lyrics}"""
|
|
||||||
|
|
||||||
print("=" * 40)
|
|
||||||
print("[translate:form_data.attributes_str:] ", form_data.attributes_str)
|
|
||||||
print("[translate:total_chars_with_space:] ", total_chars_with_space)
|
|
||||||
print("[translate:total_chars_without_space:] ", total_chars_without_space)
|
|
||||||
print("[translate:final_lyrics:]")
|
|
||||||
print(final_lyrics)
|
|
||||||
print("=" * 40)
|
|
||||||
|
|
||||||
# 8. DB 저장
|
|
||||||
insert_query = """
|
|
||||||
INSERT INTO song_results_all (
|
|
||||||
store_info, store_name, store_category, store_address, store_phone_number,
|
|
||||||
description, prompt, attr_category, attr_value,
|
|
||||||
ai, ai_model, genre,
|
|
||||||
sample_song, result_song, created_at
|
|
||||||
) VALUES (
|
|
||||||
:store_info, :store_name, :store_category, :store_address, :store_phone_number,
|
|
||||||
:description, :prompt, :attr_category, :attr_value,
|
|
||||||
:ai, :ai_model, :genre,
|
|
||||||
:sample_song, :result_song, NOW()
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ✅ attr_category, attr_value 추가
|
|
||||||
insert_params = {
|
|
||||||
"store_info": store_info.store_info or "",
|
|
||||||
"store_name": store_info.store_name,
|
|
||||||
"store_category": store_info.store_category or "",
|
|
||||||
"store_address": store_info.store_address or "",
|
|
||||||
"store_phone_number": store_info.store_phone_number or "",
|
|
||||||
"description": store_info.store_info or "",
|
|
||||||
"prompt": form_data.prompts,
|
|
||||||
"attr_category": ", ".join(form_data.attributes.keys())
|
|
||||||
if form_data.attributes
|
|
||||||
else "",
|
|
||||||
"attr_value": ", ".join(form_data.attributes.values())
|
|
||||||
if form_data.attributes
|
|
||||||
else "",
|
|
||||||
"ai": "ChatGPT",
|
|
||||||
"ai_model": form_data.llm_model,
|
|
||||||
"genre": "후크송",
|
|
||||||
"sample_song": combined_sample_song or "없음",
|
|
||||||
"result_song": final_lyrics,
|
|
||||||
}
|
|
||||||
|
|
||||||
await conn.execute(text(insert_query), insert_params)
|
|
||||||
await conn.commit()
|
|
||||||
|
|
||||||
print("결과 저장 완료")
|
|
||||||
|
|
||||||
print("\n전체 결과 조회 중...")
|
|
||||||
|
|
||||||
# 9. 생성 결과 가져오기 (created_at 역순)
|
|
||||||
select_query = """
|
|
||||||
SELECT * FROM song_results_all
|
|
||||||
ORDER BY created_at DESC;
|
|
||||||
"""
|
|
||||||
|
|
||||||
all_results = await conn.execute(text(select_query))
|
|
||||||
|
|
||||||
results_list = [
|
|
||||||
{
|
|
||||||
"id": row.id,
|
|
||||||
"store_info": row.store_info,
|
|
||||||
"store_name": row.store_name,
|
|
||||||
"store_category": row.store_category,
|
|
||||||
"store_address": row.store_address,
|
|
||||||
"store_phone_number": row.store_phone_number,
|
|
||||||
"description": row.description,
|
|
||||||
"prompt": row.prompt,
|
|
||||||
"attr_category": row.attr_category,
|
|
||||||
"attr_value": row.attr_value,
|
|
||||||
"ai": row.ai,
|
|
||||||
"ai_model": row.ai_model,
|
|
||||||
"genre": row.genre,
|
|
||||||
"sample_song": row.sample_song,
|
|
||||||
"result_song": row.result_song,
|
|
||||||
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
||||||
}
|
|
||||||
for row in all_results.fetchall()
|
|
||||||
]
|
|
||||||
|
|
||||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
|
||||||
|
|
||||||
return results_list
|
|
||||||
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
print(f"Database Error: {e}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Unexpected Error: {e}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_song_result(conn: Connection): # 반환 타입 수정
|
|
||||||
try:
|
|
||||||
select_query = """
|
|
||||||
SELECT * FROM song_results_all
|
|
||||||
ORDER BY created_at DESC;
|
|
||||||
"""
|
|
||||||
|
|
||||||
all_results = await conn.execute(text(select_query))
|
|
||||||
|
|
||||||
results_list = [
|
|
||||||
{
|
|
||||||
"id": row.id,
|
|
||||||
"store_info": row.store_info,
|
|
||||||
"store_name": row.store_name,
|
|
||||||
"store_category": row.store_category,
|
|
||||||
"store_address": row.store_address,
|
|
||||||
"store_phone_number": row.store_phone_number,
|
|
||||||
"description": row.description,
|
|
||||||
"prompt": row.prompt,
|
|
||||||
"attr_category": row.attr_category,
|
|
||||||
"attr_value": row.attr_value,
|
|
||||||
"ai": row.ai,
|
|
||||||
"ai_model": row.ai_model,
|
|
||||||
"season": row.season,
|
|
||||||
"num_of_people": row.num_of_people,
|
|
||||||
"people_category": row.people_category,
|
|
||||||
"genre": row.genre,
|
|
||||||
"sample_song": row.sample_song,
|
|
||||||
"result_song": row.result_song,
|
|
||||||
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
||||||
}
|
|
||||||
for row in all_results.fetchall()
|
|
||||||
]
|
|
||||||
|
|
||||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
|
||||||
|
|
||||||
return results_list
|
|
||||||
except HTTPException: # HTTPException은 그대로 raise
|
|
||||||
raise
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
print(f"Database Error: {e}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Unexpected Error: {e}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def make_automation(request: Request, conn: Connection):
|
|
||||||
try:
|
|
||||||
# 1. Form 데이터 파싱
|
|
||||||
form_data = await SongFormData.from_form(request)
|
|
||||||
|
|
||||||
print(f"\n{'=' * 60}")
|
|
||||||
print(f"Store ID: {form_data.store_id}")
|
|
||||||
print(f"{'=' * 60}\n")
|
|
||||||
|
|
||||||
# 2. Store 정보 조회
|
|
||||||
store_query = """
|
|
||||||
SELECT * FROM store_default_info WHERE id=:id;
|
|
||||||
"""
|
|
||||||
store_result = await conn.execute(text(store_query), {"id": form_data.store_id})
|
|
||||||
|
|
||||||
all_store_info = [
|
|
||||||
StoreData(
|
|
||||||
id=row[0],
|
|
||||||
store_info=row[1],
|
|
||||||
store_name=row[2],
|
|
||||||
store_category=row[3],
|
|
||||||
store_region=row[4],
|
|
||||||
store_address=row[5],
|
|
||||||
store_phone_number=row[6],
|
|
||||||
created_at=row[7],
|
|
||||||
)
|
|
||||||
for row in store_result
|
|
||||||
]
|
|
||||||
|
|
||||||
if not all_store_info:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail=f"Store not found: {form_data.store_id}",
|
|
||||||
)
|
|
||||||
|
|
||||||
store_info = all_store_info[0]
|
|
||||||
print(f"Store: {store_info.store_name}")
|
|
||||||
|
|
||||||
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
|
||||||
attribute_query = """
|
|
||||||
SELECT * FROM attribute;
|
|
||||||
"""
|
|
||||||
|
|
||||||
attribute_results = await conn.execute(text(attribute_query))
|
|
||||||
|
|
||||||
# 결과 가져오기
|
|
||||||
attribute_rows = attribute_results.fetchall()
|
|
||||||
|
|
||||||
formatted_attributes = ""
|
|
||||||
selected_categories = []
|
|
||||||
selected_values = []
|
|
||||||
|
|
||||||
if attribute_rows:
|
|
||||||
attribute_list = [
|
|
||||||
AttributeData(
|
|
||||||
id=row[0],
|
|
||||||
attr_category=row[1],
|
|
||||||
attr_value=row[2],
|
|
||||||
created_at=row[3],
|
|
||||||
)
|
|
||||||
for row in attribute_rows
|
|
||||||
]
|
|
||||||
|
|
||||||
# ✅ 각 category에서 하나의 value만 랜덤 선택
|
|
||||||
formatted_pairs = []
|
|
||||||
for attr in attribute_list:
|
|
||||||
# 쉼표로 분리 및 공백 제거
|
|
||||||
values = [v.strip() for v in attr.attr_value.split(",") if v.strip()]
|
|
||||||
|
|
||||||
if values:
|
|
||||||
# 랜덤하게 하나만 선택
|
|
||||||
selected_value = random.choice(values)
|
|
||||||
formatted_pairs.append(f"{attr.attr_category} : {selected_value}")
|
|
||||||
|
|
||||||
# ✅ 선택된 category와 value 저장
|
|
||||||
selected_categories.append(attr.attr_category)
|
|
||||||
selected_values.append(selected_value)
|
|
||||||
|
|
||||||
# 최종 문자열 생성
|
|
||||||
formatted_attributes = "\n".join(formatted_pairs)
|
|
||||||
|
|
||||||
print(f"\n[포맷팅된 문자열 속성 정보]\n{formatted_attributes}\n")
|
|
||||||
else:
|
|
||||||
print("속성 데이터가 없습니다")
|
|
||||||
formatted_attributes = ""
|
|
||||||
|
|
||||||
# 4. 템플릿 가져오기
|
|
||||||
print("템플릿 가져오기 (ID=1)")
|
|
||||||
|
|
||||||
prompts_query = """
|
|
||||||
SELECT * FROM prompt_template WHERE id=1;
|
|
||||||
"""
|
|
||||||
|
|
||||||
prompts_result = await conn.execute(text(prompts_query))
|
|
||||||
|
|
||||||
row = prompts_result.fetchone()
|
|
||||||
|
|
||||||
if not row:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
detail="Prompt ID 1 not found",
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt = PromptTemplateData(
|
|
||||||
id=row[0],
|
|
||||||
description=row[1],
|
|
||||||
prompt=row[2],
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Prompt Template: {prompt.prompt}")
|
|
||||||
|
|
||||||
# 5. 템플릿 조합
|
|
||||||
|
|
||||||
updated_prompt = prompt.prompt.replace("###", formatted_attributes).format(
|
|
||||||
name=store_info.store_name or "",
|
|
||||||
address=store_info.store_address or "",
|
|
||||||
category=store_info.store_category or "",
|
|
||||||
description=store_info.store_info or "",
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\n" + "=" * 80)
|
|
||||||
print("업데이트된 프롬프트")
|
|
||||||
print("=" * 80)
|
|
||||||
print(updated_prompt)
|
|
||||||
print("=" * 80 + "\n")
|
|
||||||
|
|
||||||
# 4. Sample Song 조회 및 결합
|
|
||||||
combined_sample_song = None
|
|
||||||
|
|
||||||
if form_data.lyrics_ids:
|
|
||||||
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
|
||||||
|
|
||||||
lyrics_query = """
|
|
||||||
SELECT sample_song FROM song_sample
|
|
||||||
WHERE id IN :ids
|
|
||||||
ORDER BY created_at;
|
|
||||||
"""
|
|
||||||
lyrics_result = await conn.execute(
|
|
||||||
text(lyrics_query), {"ids": tuple(form_data.lyrics_ids)}
|
|
||||||
)
|
|
||||||
|
|
||||||
sample_songs = [
|
|
||||||
row.sample_song for row in lyrics_result.fetchall() if row.sample_song
|
|
||||||
]
|
|
||||||
|
|
||||||
if sample_songs:
|
|
||||||
combined_sample_song = "\n\n".join(
|
|
||||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
|
||||||
)
|
|
||||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
|
||||||
else:
|
|
||||||
print("샘플 가사가 비어있습니다")
|
|
||||||
else:
|
|
||||||
print("선택된 lyrics가 없습니다")
|
|
||||||
|
|
||||||
# 1. song_sample 테이블의 모든 ID 조회
|
|
||||||
print("\n[샘플 가사 랜덤 선택]")
|
|
||||||
|
|
||||||
all_ids_query = """
|
|
||||||
SELECT id FROM song_sample;
|
|
||||||
"""
|
|
||||||
ids_result = await conn.execute(text(all_ids_query))
|
|
||||||
all_ids = [row.id for row in ids_result.fetchall()]
|
|
||||||
|
|
||||||
print(f"전체 샘플 가사 개수: {len(all_ids)}개")
|
|
||||||
|
|
||||||
# 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체)
|
|
||||||
combined_sample_song = None
|
|
||||||
|
|
||||||
if all_ids:
|
|
||||||
# 3개 또는 전체 개수 중 작은 값 선택
|
|
||||||
sample_count = min(3, len(all_ids))
|
|
||||||
selected_ids = random.sample(all_ids, sample_count)
|
|
||||||
|
|
||||||
print(f"랜덤 선택된 ID: {selected_ids}")
|
|
||||||
|
|
||||||
# 3. 선택된 ID로 샘플 가사 조회
|
|
||||||
lyrics_query = """
|
|
||||||
SELECT sample_song FROM song_sample
|
|
||||||
WHERE id IN :ids
|
|
||||||
ORDER BY created_at;
|
|
||||||
"""
|
|
||||||
lyrics_result = await conn.execute(
|
|
||||||
text(lyrics_query), {"ids": tuple(selected_ids)}
|
|
||||||
)
|
|
||||||
|
|
||||||
sample_songs = [
|
|
||||||
row.sample_song for row in lyrics_result.fetchall() if row.sample_song
|
|
||||||
]
|
|
||||||
|
|
||||||
# 4. combined_sample_song 생성
|
|
||||||
if sample_songs:
|
|
||||||
combined_sample_song = "\n\n".join(
|
|
||||||
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
|
||||||
)
|
|
||||||
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
|
||||||
else:
|
|
||||||
print("샘플 가사가 비어있습니다")
|
|
||||||
else:
|
|
||||||
print("song_sample 테이블에 데이터가 없습니다")
|
|
||||||
|
|
||||||
# 5. 프롬프트에 샘플 가사 추가
|
|
||||||
if combined_sample_song:
|
|
||||||
updated_prompt += f"""
|
|
||||||
|
|
||||||
다음은 참고해야 하는 샘플 가사 정보입니다.
|
|
||||||
|
|
||||||
샘플 가사를 참고하여 작곡을 해주세요.
|
|
||||||
|
|
||||||
{combined_sample_song}
|
|
||||||
"""
|
|
||||||
print("샘플 가사 정보가 프롬프트에 추가되었습니다")
|
|
||||||
else:
|
|
||||||
print("샘플 가사가 없어 기본 프롬프트만 사용합니다")
|
|
||||||
|
|
||||||
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n")
|
|
||||||
|
|
||||||
# 7. 모델에게 요청
|
|
||||||
generated_lyrics = await chatgpt_api.generate(prompt=updated_prompt)
|
|
||||||
|
|
||||||
# 글자 수 계산
|
|
||||||
total_chars_with_space = len(generated_lyrics)
|
|
||||||
total_chars_without_space = len(
|
|
||||||
generated_lyrics.replace(" ", "")
|
|
||||||
.replace("\n", "")
|
|
||||||
.replace("\r", "")
|
|
||||||
.replace("\t", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
# final_lyrics 생성
|
|
||||||
final_lyrics = f"""속성 {formatted_attributes}
|
|
||||||
전체 글자 수 (공백 포함): {total_chars_with_space}자
|
|
||||||
전체 글자 수 (공백 제외): {total_chars_without_space}자\r\n\r\n{generated_lyrics}"""
|
|
||||||
|
|
||||||
# 8. DB 저장
|
|
||||||
insert_query = """
|
|
||||||
INSERT INTO song_results_all (
|
|
||||||
store_info, store_name, store_category, store_address, store_phone_number,
|
|
||||||
description, prompt, attr_category, attr_value,
|
|
||||||
ai, ai_model, genre,
|
|
||||||
sample_song, result_song, created_at
|
|
||||||
) VALUES (
|
|
||||||
:store_info, :store_name, :store_category, :store_address, :store_phone_number,
|
|
||||||
:description, :prompt, :attr_category, :attr_value,
|
|
||||||
:ai, :ai_model, :genre,
|
|
||||||
:sample_song, :result_song, NOW()
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
print("\n[insert_params 선택된 속성 확인]")
|
|
||||||
print(f"Categories: {selected_categories}")
|
|
||||||
print(f"Values: {selected_values}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# attr_category, attr_value
|
|
||||||
insert_params = {
|
|
||||||
"store_info": store_info.store_info or "",
|
|
||||||
"store_name": store_info.store_name,
|
|
||||||
"store_category": store_info.store_category or "",
|
|
||||||
"store_address": store_info.store_address or "",
|
|
||||||
"store_phone_number": store_info.store_phone_number or "",
|
|
||||||
"description": store_info.store_info or "",
|
|
||||||
"prompt": prompt.id,
|
|
||||||
# 랜덤 선택된 category와 value 사용
|
|
||||||
"attr_category": ", ".join(selected_categories)
|
|
||||||
if selected_categories
|
|
||||||
else "",
|
|
||||||
"attr_value": ", ".join(selected_values) if selected_values else "",
|
|
||||||
"ai": "ChatGPT",
|
|
||||||
"ai_model": "gpt-5-mini",
|
|
||||||
"genre": "후크송",
|
|
||||||
"sample_song": combined_sample_song or "없음",
|
|
||||||
"result_song": final_lyrics,
|
|
||||||
}
|
|
||||||
|
|
||||||
await conn.execute(text(insert_query), insert_params)
|
|
||||||
await conn.commit()
|
|
||||||
|
|
||||||
print("결과 저장 완료")
|
|
||||||
|
|
||||||
print("\n전체 결과 조회 중...")
|
|
||||||
|
|
||||||
# 9. 생성 결과 가져오기 (created_at 역순)
|
|
||||||
select_query = """
|
|
||||||
SELECT * FROM song_results_all
|
|
||||||
ORDER BY created_at DESC;
|
|
||||||
"""
|
|
||||||
|
|
||||||
all_results = await conn.execute(text(select_query))
|
|
||||||
|
|
||||||
results_list = [
|
|
||||||
{
|
|
||||||
"id": row.id,
|
|
||||||
"store_info": row.store_info,
|
|
||||||
"store_name": row.store_name,
|
|
||||||
"store_category": row.store_category,
|
|
||||||
"store_address": row.store_address,
|
|
||||||
"store_phone_number": row.store_phone_number,
|
|
||||||
"description": row.description,
|
|
||||||
"prompt": row.prompt,
|
|
||||||
"attr_category": row.attr_category,
|
|
||||||
"attr_value": row.attr_value,
|
|
||||||
"ai": row.ai,
|
|
||||||
"ai_model": row.ai_model,
|
|
||||||
"genre": row.genre,
|
|
||||||
"sample_song": row.sample_song,
|
|
||||||
"result_song": row.result_song,
|
|
||||||
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
||||||
}
|
|
||||||
for row in all_results.fetchall()
|
|
||||||
]
|
|
||||||
|
|
||||||
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
|
||||||
|
|
||||||
return results_list
|
|
||||||
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
print(f"Database Error: {e}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Unexpected Error: {e}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail="서비스 처리 중 오류가 발생했습니다.",
|
|
||||||
)
|
|
||||||
|
|
@ -13,6 +13,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
||||||
from app.database.session import BackgroundSessionLocal
|
from app.database.session import BackgroundSessionLocal
|
||||||
from app.lyric.models import Lyric
|
from app.lyric.models import Lyric
|
||||||
from app.utils.chatgpt_prompt import ChatgptService
|
from app.utils.chatgpt_prompt import ChatgptService
|
||||||
|
from app.utils.prompts.prompts import Prompt
|
||||||
|
|
||||||
# 로거 설정
|
# 로거 설정
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -68,8 +69,8 @@ async def _update_lyric_status(
|
||||||
|
|
||||||
async def generate_lyric_background(
|
async def generate_lyric_background(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
prompt: str,
|
prompt: Prompt,
|
||||||
language: str,
|
lyric_input_data: dict, # 프롬프트 메타데이터에서 정의된 Input
|
||||||
) -> None:
|
) -> None:
|
||||||
"""백그라운드에서 ChatGPT를 통해 가사를 생성하고 Lyric 테이블을 업데이트합니다.
|
"""백그라운드에서 ChatGPT를 통해 가사를 생성하고 Lyric 테이블을 업데이트합니다.
|
||||||
|
|
||||||
|
|
@ -84,20 +85,22 @@ async def generate_lyric_background(
|
||||||
logger.info(f"[generate_lyric_background] START - task_id: {task_id}")
|
logger.info(f"[generate_lyric_background] START - task_id: {task_id}")
|
||||||
print(f"[generate_lyric_background] ========== START ==========")
|
print(f"[generate_lyric_background] ========== START ==========")
|
||||||
print(f"[generate_lyric_background] task_id: {task_id}")
|
print(f"[generate_lyric_background] task_id: {task_id}")
|
||||||
print(f"[generate_lyric_background] language: {language}")
|
print(f"[generate_lyric_background] language: {lyric_input_data['language']}")
|
||||||
print(f"[generate_lyric_background] prompt length: {len(prompt)}자")
|
#print(f"[generate_lyric_background] prompt length: {len(prompt)}자")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# ========== Step 1: ChatGPT 서비스 초기화 ==========
|
# ========== Step 1: ChatGPT 서비스 초기화 ==========
|
||||||
step1_start = time.perf_counter()
|
step1_start = time.perf_counter()
|
||||||
print(f"[generate_lyric_background] Step 1: ChatGPT 서비스 초기화...")
|
print(f"[generate_lyric_background] Step 1: ChatGPT 서비스 초기화...")
|
||||||
|
|
||||||
service = ChatgptService(
|
# service = ChatgptService(
|
||||||
customer_name="", # 프롬프트가 이미 생성되었으므로 빈 값
|
# customer_name="", # 프롬프트가 이미 생성되었으므로 빈 값
|
||||||
region="",
|
# region="",
|
||||||
detail_region_info="",
|
# detail_region_info="",
|
||||||
language=language,
|
# language=language,
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
chatgpt = ChatgptService()
|
||||||
|
|
||||||
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
step1_elapsed = (time.perf_counter() - step1_start) * 1000
|
||||||
print(f"[generate_lyric_background] Step 1 완료 ({step1_elapsed:.1f}ms)")
|
print(f"[generate_lyric_background] Step 1 완료 ({step1_elapsed:.1f}ms)")
|
||||||
|
|
@ -107,8 +110,9 @@ async def generate_lyric_background(
|
||||||
logger.info(f"[generate_lyric_background] Step 2: ChatGPT API 호출 시작 - task_id: {task_id}")
|
logger.info(f"[generate_lyric_background] Step 2: ChatGPT API 호출 시작 - task_id: {task_id}")
|
||||||
print(f"[generate_lyric_background] Step 2: ChatGPT API 호출 시작...")
|
print(f"[generate_lyric_background] Step 2: ChatGPT API 호출 시작...")
|
||||||
|
|
||||||
result = await service.generate(prompt=prompt)
|
#result = await service.generate(prompt=prompt)
|
||||||
|
result_response = await chatgpt.generate_structured_output(prompt, lyric_input_data)
|
||||||
|
result = result_response['lyric']
|
||||||
step2_elapsed = (time.perf_counter() - step2_start) * 1000
|
step2_elapsed = (time.perf_counter() - step2_start) * 1000
|
||||||
logger.info(f"[generate_lyric_background] Step 2 완료 - 응답 {len(result)}자 ({step2_elapsed:.1f}ms)")
|
logger.info(f"[generate_lyric_background] Step 2 완료 - 응답 {len(result)}자 ({step2_elapsed:.1f}ms)")
|
||||||
print(f"[generate_lyric_background] Step 2 완료 - 응답 {len(result)}자 ({step2_elapsed:.1f}ms)")
|
print(f"[generate_lyric_background] Step 2 완료 - 응답 {len(result)}자 ({step2_elapsed:.1f}ms)")
|
||||||
|
|
|
||||||
|
|
@ -5,207 +5,11 @@ import re
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
|
|
||||||
from config import apikey_settings
|
from config import apikey_settings
|
||||||
|
from app.utils.prompts.prompts import Prompt
|
||||||
|
|
||||||
# 로거 설정
|
# 로거 설정
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# fmt: off
|
|
||||||
LYRICS_PROMPT_TEMPLATE_ORI = """
|
|
||||||
1.Act as a content marketing expert with domain knowledges in [pension/staying services] in Korea, Goal: plan viral content creation that lead online reservations and promotion
|
|
||||||
2.Conduct an in-depth analysis of [업체명:{customer_name}] in [지역명:{region}] by examining their official website or informations, photos on never map and online presence. Create a comprehensive "[지역 상세: {detail_region_info}]_Brand & Marketing Intelligence Report in Korean, that includes:
|
|
||||||
|
|
||||||
**Core Analysis:**
|
|
||||||
- Target customer segments & personas
|
|
||||||
- Unique Selling Propositions (USPs) and competitive differentiators
|
|
||||||
- Comprehensive competitor landscape analysis (direct & indirect competitors)
|
|
||||||
- Market positioning assessment
|
|
||||||
|
|
||||||
**Content Strategy Framework:**
|
|
||||||
- Seasonal content calendar with trend integration
|
|
||||||
- Visual storytelling direction (shot-by-shot creative guidance)
|
|
||||||
- Brand tone & voice guidelines
|
|
||||||
- Content themes aligned with target audience behaviors
|
|
||||||
|
|
||||||
**SEO & AEO Optimization:**
|
|
||||||
- Recommended primary and long-tail keywords
|
|
||||||
- SEO-optimized taglines and meta descriptions
|
|
||||||
- Answer Engine Optimization (AEO) content suggestions
|
|
||||||
- Local search optimization strategies
|
|
||||||
|
|
||||||
**Actionable Recommendations:**
|
|
||||||
- Content distribution strategy across platforms
|
|
||||||
- KPI measurement framework
|
|
||||||
- Budget allocation recommendations by content type
|
|
||||||
|
|
||||||
콘텐츠 기획(Lyrics, Prompt for SUNO)
|
|
||||||
1. Based on the Brand & Marketing Intelligence Report for [업체명 + 지역명 / {customer_name} ({region})], create original lyrics and define music attributes (song mood, BPM, genres, and key musical motifs, Prompt for Suno.com) specifically tailored for viral content.
|
|
||||||
2. The lyrics should include, the name of [ Promotion Subject], [location], [main target],[Famous place, accessible in 10min], promotional words including but not limited to [인스타 감성], [사진같은 하루]
|
|
||||||
|
|
||||||
Deliver outputs optimized for three formats:1 minute. Ensure that each version aligns with the brand's core identity and is suitable for use in digital marketing and social media campaigns, in Korean
|
|
||||||
""".strip()
|
|
||||||
# fmt: on
|
|
||||||
|
|
||||||
LYRICS_PROMPT_TEMPLATE = """
|
|
||||||
[ROLE]
|
|
||||||
Content marketing expert and creative songwriter specializing in pension/accommodation services
|
|
||||||
|
|
||||||
[INPUT]
|
|
||||||
- Business Name: {customer_name}
|
|
||||||
- Region: {region}
|
|
||||||
- Region Details: {detail_region_info}
|
|
||||||
- Output Language: {language}
|
|
||||||
|
|
||||||
[INTERNAL ANALYSIS - DO NOT OUTPUT]
|
|
||||||
Analyze the following internally to inform lyrics creation:
|
|
||||||
- Target customer segments and personas
|
|
||||||
- Unique Selling Propositions (USPs)
|
|
||||||
- Regional characteristics and nearby attractions (within 10 min access)
|
|
||||||
- Seasonal appeal points
|
|
||||||
- Emotional triggers for the target audience
|
|
||||||
|
|
||||||
[LYRICS REQUIREMENTS]
|
|
||||||
1. Must Include Elements:
|
|
||||||
- Business name (TRANSLATED or TRANSLITERATED to {language})
|
|
||||||
- Region name (TRANSLATED or TRANSLITERATED to {language})
|
|
||||||
- Main target audience appeal
|
|
||||||
- Nearby famous places or regional characteristics
|
|
||||||
|
|
||||||
2. Keywords to Incorporate (use language-appropriate trendy expressions):
|
|
||||||
- Korean: 인스타 감성, 사진같은 하루, 힐링, 여행, 감성 숙소
|
|
||||||
- English: Instagram vibes, picture-perfect day, healing, travel, getaway
|
|
||||||
- Chinese: 网红打卡, 治愈系, 旅行, 度假, 拍照圣地
|
|
||||||
- Japanese: インスタ映え, 写真のような一日, 癒し, 旅行, 絶景
|
|
||||||
- Thai: ที่พักสวย, ฮีลใจ, เที่ยว, ถ่ายรูป, วิวสวย
|
|
||||||
- Vietnamese: check-in đẹp, healing, du lịch, nghỉ dưỡng, view đẹp
|
|
||||||
|
|
||||||
3. Structure:
|
|
||||||
- Length: For 1-minute video (approximately 8-12 lines)
|
|
||||||
- Flow: Verse structure suitable for music
|
|
||||||
- Rhythm: Natural speech rhythm in the specified language
|
|
||||||
|
|
||||||
4. Tone:
|
|
||||||
- Emotional and heartfelt
|
|
||||||
- Trendy and viral-friendly
|
|
||||||
- Relatable to target audience
|
|
||||||
|
|
||||||
[CRITICAL LANGUAGE REQUIREMENT - ABSOLUTE RULE]
|
|
||||||
ALL OUTPUT MUST BE 100% WRITTEN IN {language} - NO EXCEPTIONS
|
|
||||||
- ALL lyrics content: {language} ONLY
|
|
||||||
- ALL proper nouns (business names, region names, place names): MUST be translated or transliterated to {language}
|
|
||||||
- Korean input like "군산" must become "Gunsan" in English, "群山" in Chinese, "グンサン" in Japanese, etc.
|
|
||||||
- Korean input like "스테이 머뭄" must become "Stay Meoum" in English, "住留" in Chinese, "ステイモーム" in Japanese, etc.
|
|
||||||
- ZERO Korean characters (한글) allowed when output language is NOT Korean
|
|
||||||
- ZERO mixing of languages - the entire output must be monolingual in {language}
|
|
||||||
- This is a NON-NEGOTIABLE requirement
|
|
||||||
- Any output containing characters from other languages is considered a COMPLETE FAILURE
|
|
||||||
- Violation of this rule invalidates the entire response
|
|
||||||
|
|
||||||
[OUTPUT RULES - STRICTLY ENFORCED]
|
|
||||||
- Output lyrics ONLY
|
|
||||||
- Lyrics MUST be written ENTIRELY in {language} - NO EXCEPTIONS
|
|
||||||
- ALL names and places MUST be in {language} script/alphabet
|
|
||||||
- NO Korean (한글), Chinese (漢字), Japanese (仮名), Thai (ไทย), or Vietnamese (Tiếng Việt) characters unless that is the selected output language
|
|
||||||
- NO titles, descriptions, analysis, or explanations
|
|
||||||
- NO greetings or closing remarks
|
|
||||||
- NO additional commentary before or after lyrics
|
|
||||||
- NO line numbers or labels
|
|
||||||
- Follow the exact format below
|
|
||||||
|
|
||||||
[OUTPUT FORMAT - SUCCESS]
|
|
||||||
---
|
|
||||||
[Lyrics ENTIRELY in {language} here - no other language characters allowed]
|
|
||||||
---
|
|
||||||
|
|
||||||
[OUTPUT FORMAT - FAILURE]
|
|
||||||
If you cannot generate lyrics due to insufficient information, invalid input, or any other reason:
|
|
||||||
---
|
|
||||||
ERROR: [Brief reason for failure in English]
|
|
||||||
---
|
|
||||||
""".strip()
|
|
||||||
# fmt: on
|
|
||||||
|
|
||||||
MARKETING_ANALYSIS_PROMPT_TEMPLATE = """
|
|
||||||
[ROLE]
|
|
||||||
Content marketing expert specializing in pension/accommodation services in Korea
|
|
||||||
|
|
||||||
[INPUT]
|
|
||||||
- Business Name: {customer_name}
|
|
||||||
- Region: {region}
|
|
||||||
- Region Details: {detail_region_info}
|
|
||||||
|
|
||||||
[ANALYSIS REQUIREMENTS]
|
|
||||||
Provide comprehensive marketing analysis including:
|
|
||||||
1. Target Customer Segments
|
|
||||||
- Primary and secondary target personas
|
|
||||||
- Age groups, travel preferences, booking patterns
|
|
||||||
2. Unique Selling Propositions (USPs)
|
|
||||||
- Key differentiators based on location and region details
|
|
||||||
- Competitive advantages
|
|
||||||
3. Regional Characteristics
|
|
||||||
- Nearby attractions and famous places (within 10 min access)
|
|
||||||
- Local food, activities, and experiences
|
|
||||||
- Transportation accessibility
|
|
||||||
4. Seasonal Appeal Points
|
|
||||||
- Best seasons to visit
|
|
||||||
- Seasonal activities and events
|
|
||||||
- Peak/off-peak marketing opportunities
|
|
||||||
5. Marketing Keywords
|
|
||||||
- Recommended hashtags and search keywords
|
|
||||||
- Trending terms relevant to the property
|
|
||||||
|
|
||||||
[ADDITIONAL REQUIREMENTS]
|
|
||||||
1. Recommended Tags
|
|
||||||
- Generate 5 recommended hashtags/tags based on the business characteristics
|
|
||||||
- Tags should be trendy, searchable, and relevant to accommodation marketing
|
|
||||||
- Return as JSON with key "tags"
|
|
||||||
- **MUST be written in Korean (한국어)**
|
|
||||||
|
|
||||||
[CRITICAL LANGUAGE REQUIREMENT - ABSOLUTE RULE]
|
|
||||||
ALL OUTPUT MUST BE WRITTEN IN KOREAN (한국어)
|
|
||||||
- Analysis sections: Korean only
|
|
||||||
- Tags: Korean only
|
|
||||||
- This is a NON-NEGOTIABLE requirement
|
|
||||||
- Any output in English or other languages is considered a FAILURE
|
|
||||||
- Violation of this rule invalidates the entire response
|
|
||||||
|
|
||||||
[OUTPUT RULES - STRICTLY ENFORCED]
|
|
||||||
- Output analysis ONLY
|
|
||||||
- ALL content MUST be written in Korean (한국어) - NO EXCEPTIONS
|
|
||||||
- NO greetings or closing remarks
|
|
||||||
- NO additional commentary before or after analysis
|
|
||||||
- Follow the exact format below
|
|
||||||
|
|
||||||
[OUTPUT FORMAT - SUCCESS]
|
|
||||||
---
|
|
||||||
## 타겟 고객 분석
|
|
||||||
[한국어로 작성된 타겟 고객 분석]
|
|
||||||
|
|
||||||
## 핵심 차별점 (USP)
|
|
||||||
[한국어로 작성된 USP 분석]
|
|
||||||
|
|
||||||
## 지역 특성
|
|
||||||
[한국어로 작성된 지역 특성 분석]
|
|
||||||
|
|
||||||
## 시즌별 매력 포인트
|
|
||||||
[한국어로 작성된 시즌별 분석]
|
|
||||||
|
|
||||||
## 마케팅 키워드
|
|
||||||
[한국어로 작성된 마케팅 키워드]
|
|
||||||
|
|
||||||
## JSON Data
|
|
||||||
```json
|
|
||||||
{{
|
|
||||||
"tags": ["태그1", "태그2", "태그3", "태그4", "태그5"]
|
|
||||||
}}
|
|
||||||
```
|
|
||||||
---
|
|
||||||
|
|
||||||
[OUTPUT FORMAT - FAILURE]
|
|
||||||
If you cannot generate analysis due to insufficient information, invalid input, or any other reason:
|
|
||||||
---
|
|
||||||
ERROR: [Brief reason for failure in English]
|
|
||||||
---
|
|
||||||
""".strip()
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -215,190 +19,30 @@ class ChatgptService:
|
||||||
GPT 5.0 모델을 사용하여 마케팅 가사 및 분석을 생성합니다.
|
GPT 5.0 모델을 사용하여 마케팅 가사 및 분석을 생성합니다.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self):
|
||||||
self,
|
|
||||||
customer_name: str,
|
|
||||||
region: str,
|
|
||||||
detail_region_info: str = "",
|
|
||||||
language: str = "Korean",
|
|
||||||
):
|
|
||||||
# 최신 모델: gpt-5-mini
|
|
||||||
self.model = "gpt-5-mini"
|
|
||||||
self.client = AsyncOpenAI(api_key=apikey_settings.CHATGPT_API_KEY)
|
self.client = AsyncOpenAI(api_key=apikey_settings.CHATGPT_API_KEY)
|
||||||
self.customer_name = customer_name
|
|
||||||
self.region = region
|
async def _call_structured_output_with_response_gpt_api(self, prompt: str, output_format : dict, model:str) -> dict:
|
||||||
self.detail_region_info = detail_region_info
|
content = [{"type": "input_text", "text": prompt}]
|
||||||
self.language = language
|
response = await self.client.responses.create(
|
||||||
|
model=model,
|
||||||
def build_lyrics_prompt(self) -> str:
|
input=[{"role": "user", "content": content}],
|
||||||
"""LYRICS_PROMPT_TEMPLATE에 고객 정보를 대입하여 완성된 프롬프트 반환"""
|
text = output_format
|
||||||
return LYRICS_PROMPT_TEMPLATE.format(
|
|
||||||
customer_name=self.customer_name,
|
|
||||||
region=self.region,
|
|
||||||
detail_region_info=self.detail_region_info,
|
|
||||||
language=self.language,
|
|
||||||
)
|
)
|
||||||
|
structured_output = json.loads(response.output_text)
|
||||||
|
return structured_output or {}
|
||||||
|
|
||||||
def build_market_analysis_prompt(self) -> str:
|
async def generate_structured_output(
|
||||||
"""MARKETING_ANALYSIS_PROMPT_TEMPLATE에 고객 정보를 대입하여 완성된 프롬프트 반환"""
|
|
||||||
return MARKETING_ANALYSIS_PROMPT_TEMPLATE.format(
|
|
||||||
customer_name=self.customer_name,
|
|
||||||
region=self.region,
|
|
||||||
detail_region_info=self.detail_region_info,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _call_gpt_api(self, prompt: str) -> str:
|
|
||||||
"""GPT API를 직접 호출합니다 (내부 메서드).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
prompt: GPT에 전달할 프롬프트
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
GPT 응답 문자열
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
APIError, APIConnectionError, RateLimitError: OpenAI API 오류
|
|
||||||
"""
|
|
||||||
completion = await self.client.chat.completions.create(
|
|
||||||
model=self.model, messages=[{"role": "user", "content": prompt}]
|
|
||||||
)
|
|
||||||
message = completion.choices[0].message.content
|
|
||||||
return message or ""
|
|
||||||
|
|
||||||
async def generate(
|
|
||||||
self,
|
self,
|
||||||
prompt: str | None = None,
|
prompt : Prompt,
|
||||||
|
input_data : dict,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""GPT에게 프롬프트를 전달하여 결과를 반환합니다.
|
prompt_text = prompt.build_prompt(input_data)
|
||||||
|
|
||||||
Args:
|
print(f"[ChatgptService] Generated Prompt (length: {len(prompt_text)})")
|
||||||
prompt: GPT에 전달할 프롬프트 (None이면 기본 가사 프롬프트 사용)
|
logger.info(f"[ChatgptService] Starting GPT request with structured output with model: {prompt.prompt_model}")
|
||||||
|
|
||||||
Returns:
|
|
||||||
GPT 응답 문자열
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
APIError, APIConnectionError, RateLimitError: OpenAI API 오류
|
|
||||||
"""
|
|
||||||
if prompt is None:
|
|
||||||
prompt = self.build_lyrics_prompt()
|
|
||||||
|
|
||||||
print(f"[ChatgptService] Generated Prompt (length: {len(prompt)})")
|
|
||||||
logger.info(f"[ChatgptService] Starting GPT request with model: {self.model}")
|
|
||||||
|
|
||||||
# GPT API 호출
|
# GPT API 호출
|
||||||
response = await self._call_gpt_api(prompt)
|
response = await self._call_structured_output_with_response_gpt_api(prompt_text, prompt.prompt_output, prompt.prompt_model)
|
||||||
|
|
||||||
print(f"[ChatgptService] SUCCESS - Response length: {len(response)}")
|
|
||||||
logger.info(f"[ChatgptService] SUCCESS - Response length: {len(response)}")
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def summarize_marketing(self, text: str) -> str:
|
|
||||||
"""마케팅 텍스트를 항목으로 구분하여 500자로 요약 정리.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: 요약할 마케팅 텍스트
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
요약된 텍스트
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
APIError, APIConnectionError, RateLimitError: OpenAI API 오류
|
|
||||||
"""
|
|
||||||
prompt = f"""[ROLE]
|
|
||||||
마케팅 콘텐츠 요약 전문가
|
|
||||||
|
|
||||||
[INPUT]
|
|
||||||
{text}
|
|
||||||
|
|
||||||
[TASK]
|
|
||||||
위 텍스트를 분석하여 핵심 내용을 항목별로 구분하여 500자 이내로 요약해주세요.
|
|
||||||
|
|
||||||
[OUTPUT REQUIREMENTS]
|
|
||||||
- 5개 항목으로 구분: 타겟 고객, 핵심 차별점, 지역 특성, 시즌별 포인트, 추천 키워드
|
|
||||||
- 각 항목은 줄바꿈으로 구분
|
|
||||||
- 총 500자 이내로 요약
|
|
||||||
- 핵심 정보만 간결하게 포함
|
|
||||||
- 한국어로 작성
|
|
||||||
- 특수문자 사용 금지 (괄호, 슬래시, 하이픈, 물결표 등 제외)
|
|
||||||
- 쉼표와 마침표만 사용하여 자연스러운 문장으로 작성
|
|
||||||
|
|
||||||
[OUTPUT FORMAT - 반드시 아래 형식 준수]
|
|
||||||
---
|
|
||||||
타겟 고객
|
|
||||||
[대상 고객층을 자연스러운 문장으로 설명]
|
|
||||||
|
|
||||||
핵심 차별점
|
|
||||||
[숙소의 차별화 포인트를 자연스러운 문장으로 설명]
|
|
||||||
|
|
||||||
지역 특성
|
|
||||||
[주변 관광지와 지역 특색을 자연스러운 문장으로 설명]
|
|
||||||
|
|
||||||
시즌별 포인트
|
|
||||||
[계절별 매력 포인트를 자연스러운 문장으로 설명]
|
|
||||||
|
|
||||||
추천 키워드
|
|
||||||
[마케팅에 활용할 키워드를 쉼표로 구분하여 나열]
|
|
||||||
---
|
|
||||||
"""
|
|
||||||
|
|
||||||
result = await self.generate(prompt=prompt)
|
|
||||||
|
|
||||||
# --- 구분자 제거
|
|
||||||
if result.startswith("---"):
|
|
||||||
result = result[3:].strip()
|
|
||||||
if result.endswith("---"):
|
|
||||||
result = result[:-3].strip()
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def parse_marketing_analysis(
|
|
||||||
self, raw_response: str, facility_info: str | None = None
|
|
||||||
) -> dict:
|
|
||||||
"""ChatGPT 마케팅 분석 응답을 파싱하고 요약하여 딕셔너리로 반환
|
|
||||||
|
|
||||||
Args:
|
|
||||||
raw_response: ChatGPT 마케팅 분석 응답 원문
|
|
||||||
facility_info: 크롤링에서 가져온 편의시설 정보 문자열
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: {"report": str, "tags": list[str], "facilities": list[str]}
|
|
||||||
"""
|
|
||||||
tags: list[str] = []
|
|
||||||
facilities: list[str] = []
|
|
||||||
report = raw_response
|
|
||||||
|
|
||||||
# JSON 블록 추출 시도
|
|
||||||
json_match = re.search(r"```json\s*(\{.*?\})\s*```", raw_response, re.DOTALL)
|
|
||||||
if json_match:
|
|
||||||
try:
|
|
||||||
json_data = json.loads(json_match.group(1))
|
|
||||||
tags = json_data.get("tags", [])
|
|
||||||
print(f"[parse_marketing_analysis] GPT 응답에서 tags 파싱 완료: {tags}")
|
|
||||||
# JSON 블록을 제외한 리포트 부분 추출
|
|
||||||
report = raw_response[: json_match.start()].strip()
|
|
||||||
# --- 구분자 제거
|
|
||||||
if report.startswith("---"):
|
|
||||||
report = report[3:].strip()
|
|
||||||
if report.endswith("---"):
|
|
||||||
report = report[:-3].strip()
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print("[parse_marketing_analysis] JSON 파싱 실패")
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 크롤링에서 가져온 facility_info로 facilities 설정
|
|
||||||
print(f"[parse_marketing_analysis] 크롤링 facility_info 원본: {facility_info}")
|
|
||||||
if facility_info:
|
|
||||||
# 쉼표로 구분된 편의시설 문자열을 리스트로 변환
|
|
||||||
facilities = [f.strip() for f in facility_info.split(",") if f.strip()]
|
|
||||||
print(f"[parse_marketing_analysis] facility_info 파싱 결과: {facilities}")
|
|
||||||
else:
|
|
||||||
facilities = ["등록된 정보 없음"]
|
|
||||||
print("[parse_marketing_analysis] facility_info 없음 - '등록된 정보 없음' 설정")
|
|
||||||
|
|
||||||
# 리포트 내용을 500자로 요약
|
|
||||||
if report:
|
|
||||||
report = await self.summarize_marketing(report)
|
|
||||||
|
|
||||||
print(f"[parse_marketing_analysis] 최종 facilities: {facilities}")
|
|
||||||
return {"report": report, "tags": tags, "facilities": facilities}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"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"}}, "required": ["lyric"], "additionalProperties": false}, "strict": true}}}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
|
||||||
|
[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_expressions[language]}
|
||||||
|
|
||||||
|
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}
|
||||||
|
8–12 lines
|
||||||
|
Full verse flow, immersive mood
|
||||||
|
|
||||||
|
No explanations
|
||||||
|
No headings
|
||||||
|
No bullet points
|
||||||
|
No analysis
|
||||||
|
No extra text
|
||||||
|
|
||||||
|
[FAILURE FORMAT]
|
||||||
|
If generation is impossible:
|
||||||
|
ERROR: Brief reason in English
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"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": "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}}}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
|
||||||
|
[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"]
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import os, json
|
||||||
|
from abc import ABCMeta
|
||||||
|
from config import prompt_settings
|
||||||
|
|
||||||
|
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
|
||||||
|
print("build_template", build_template)
|
||||||
|
print("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}")
|
||||||
|
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()
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
|
||||||
|
입력 :
|
||||||
|
분석 보고서
|
||||||
|
{report}
|
||||||
|
|
||||||
|
셀링 포인트
|
||||||
|
{selling_points}
|
||||||
|
|
||||||
|
위 분석 결과를 바탕으로, 주요 셀링 포인트를 다음 구조로 재정리하라.
|
||||||
|
|
||||||
|
조건:
|
||||||
|
각 셀링 포인트는 반드시 ‘카테고리 → 태그 키워드 → 한 줄 설명’ 구조를 가질 것
|
||||||
|
태그 키워드는 UI 상에서 타원(oval) 형태의 시각적 태그로 사용될 것을 가정하여
|
||||||
|
- 3 ~ 6단어 이내
|
||||||
|
- 명사 또는 명사형 키워드로 작성
|
||||||
|
- 설명은 문장이 아닌, 짧은 ‘셀링 문구’ 형태로 작성할 것
|
||||||
|
- 광고·숏폼·상세페이지 어디에도 바로 재사용 가능해야 함
|
||||||
|
- 전체 셀링 포인트 개수는 5~7개로 제한
|
||||||
|
|
||||||
|
출력 형식:
|
||||||
|
[카테고리명]
|
||||||
|
(태그 키워드)
|
||||||
|
- 한 줄 설명 문구
|
||||||
|
|
||||||
|
예시:
|
||||||
|
[공간 정체성]
|
||||||
|
(100년 적산가옥 · 시간의 결)
|
||||||
|
- 하루를 ‘숙박’이 아닌 ‘체류’로 바꾸는 공간
|
||||||
|
|
||||||
|
[입지 & 희소성]
|
||||||
|
(말랭이마을 · 로컬 히든플레이스)
|
||||||
|
- 관광지가 아닌, 군산을 아는 사람의 선택
|
||||||
|
|
||||||
|
[프라이버시]
|
||||||
|
(독채 숙소 · 프라이빗 스테이)
|
||||||
|
- 누구의 방해도 없는 완전한 휴식 구조
|
||||||
|
|
||||||
|
[비주얼 경쟁력]
|
||||||
|
(감성 인테리어 · 자연광 스폿)
|
||||||
|
- 찍는 순간 콘텐츠가 되는 공간 설계
|
||||||
|
|
||||||
|
[타깃 최적화]
|
||||||
|
(커플 · 소규모 여행)
|
||||||
|
- 둘에게 가장 이상적인 공간 밀도
|
||||||
|
|
||||||
|
[체류 경험]
|
||||||
|
(아무것도 안 해도 되는 하루)
|
||||||
|
- 일정 없이도 만족되는 하루 루틴
|
||||||
|
|
||||||
|
[브랜드 포지션]
|
||||||
|
(호텔도 펜션도 아닌 아지트)
|
||||||
|
- 다시 돌아오고 싶은 개인적 장소
|
||||||
|
|
||||||
|
|
@ -167,6 +167,13 @@ class CreatomateSettings(BaseSettings):
|
||||||
|
|
||||||
model_config = _base_config
|
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")
|
||||||
|
|
||||||
|
model_config = _base_config
|
||||||
|
|
||||||
prj_settings = ProjectSettings()
|
prj_settings = ProjectSettings()
|
||||||
apikey_settings = APIKeySettings()
|
apikey_settings = APIKeySettings()
|
||||||
|
|
@ -177,3 +184,4 @@ cors_settings = CORSSettings()
|
||||||
crawler_settings = CrawlerSettings()
|
crawler_settings = CrawlerSettings()
|
||||||
azure_blob_settings = AzureBlobSettings()
|
azure_blob_settings = AzureBlobSettings()
|
||||||
creatomate_settings = CreatomateSettings()
|
creatomate_settings = CreatomateSettings()
|
||||||
|
prompt_settings = PromptSettings()
|
||||||
|
|
@ -0,0 +1,723 @@
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"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": 3,
|
||||||
|
"id": "6cf7ae9b-3ffe-4046-9cab-f33bc071b288",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from config import crawler_settings"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 4,
|
||||||
|
"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": 5,
|
||||||
|
"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": 6,
|
||||||
|
"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": 8,
|
||||||
|
"id": "d4db2ec1-b2af-4993-8832-47f380c17015",
|
||||||
|
"metadata": {
|
||||||
|
"scrolled": true
|
||||||
|
},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"[crawling] ========== START ==========\n",
|
||||||
|
"[crawling] URL: https://map.naver.com/p/entry/place/1903455560?placePath=/home?from=map&fromPane...\n",
|
||||||
|
"[crawling] Step 1: 네이버 지도 크롤링 시작...\n",
|
||||||
|
"[NvMapScraper] Requesting place_id: 1903455560\n",
|
||||||
|
"[NvMapScraper] SUCCESS - place_id: 1903455560\n",
|
||||||
|
"[crawling] Step 1 완료 - 이미지 44개 (659.9ms)\n",
|
||||||
|
"[crawling] Step 2: 정보 가공 시작...\n",
|
||||||
|
"[crawling] Step 2 완료 - 오블로모프, 군산시 (0.7ms)\n",
|
||||||
|
"[crawling] Step 3: ChatGPT 마케팅 분석 시작...\n",
|
||||||
|
"[crawling] Step 3-1: 서비스 초기화 완료 (59.1ms)\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, photos, online presence, 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",
|
||||||
|
"\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",
|
||||||
|
"\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: 2766)\n",
|
||||||
|
"[crawling] Step 3-3: GPT API 호출 완료 - (59060.2ms)\n",
|
||||||
|
"[crawling] Step 3-4: 응답 파싱 시작 - facility_info: 무선 인터넷, 예약, 주차\n",
|
||||||
|
"[crawling] Step 3-4: 응답 파싱 완료 (0.1ms)\n",
|
||||||
|
"[crawling] Step 3 완료 - 마케팅 분석 성공 (59119.8ms)\n",
|
||||||
|
"[crawling] ========== COMPLETE ==========\n",
|
||||||
|
"[crawling] 총 소요시간: 59782.3ms\n",
|
||||||
|
"[crawling] - Step 1 (크롤링): 659.9ms\n",
|
||||||
|
"[crawling] - Step 2 (정보가공): 0.7ms\n",
|
||||||
|
"[crawling] - Step 3 (GPT 분석): 59119.8ms\n",
|
||||||
|
"[crawling] - GPT API 호출: 59060.2ms\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"var2 = await crawling(val1)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 9,
|
||||||
|
"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='요약\\n오블로모프(군산 절골길)는 ‘느림의 미학’을 콘셉트로 한 소규모 부티크 스테이로 포지셔닝할 때 강점이 큽니다. 군산의 근대문화·항구·로컬 카페·해산물 레퍼런스가 결합되면 ‘주말 힐링 + 인생샷’ 수요를 끌어올 수 있습니다. 사진·영상 중심의 콘텐츠, 지역 연계 체험, 예약 편의성(주차·즉시예약·정책 명확화)을 우선 강화하면 전환율 개선과 확장성(패키지, 시즌 프로모션)이 용이합니다.\\n위치 분석\\n- 주소: 전북 군산시 절골길 16 — 주거 밀집·골목형 동선으로 ‘조용한 휴식’ 기대 요소\\n- 인근: 근대문화·항구권 관광지·로컬 카페·해산물 식당 밀집(도보·단거리 이동권 장점)\\n- 교통: 자차 접근성·주차 여부가 예약 결정 핵심(대중교통 이용객 대비 자가용 고객 타깃화 필요)\\n콘셉트·포토·온라인 대비\\n- 콘셉트 잠재력: ‘오블로모프=느림·휴식’ 내러티브 활용 가능(브랜드 스토리텔링 유리)\\n- 포토 포인트 제안: 테라스 일출·실내 빈티지 소품·침구 근접 샷·로컬 푸드 플래팅\\n- 온라인: 네이버 예약, 인스타그램, 블로그(지역 키워드) 우선 등재 필요. 리뷰·FAQ·즉시예약 정보 노출 필수\\n주변 환경 영향요인\\n- 식음·체험: 해산물 전문점·카페투어·공방/산책 루트 연계로 1박 체류 가치 강화\\n- 시즌성: 주중 장기 체류보다는 주말·연휴 수요 집중, 계절별 촬영 포인트로 프로모션\\n타깃 고객 행동·예약 결정 요인\\n- 시각 요소 우선: 사진 퀄리티가 예약 전환을 좌우\\n- 프라이버시·편리성: 전용 테라스·주차·와이파이·편의시설(개인화된 체크인)이 중요\\n- 정책: 유연한 취소·즉시예약·가격 패키징(주말/주중/연박)로 예약장벽 완화\\n타깃 세그먼트(페르소나)\\n1) SNS 커플(25–35) — 인생샷·감성카페·주말 데이트 용도\\n2) 휴식형 성인(30–50) — ‘힐링’·프라이버시·느긋한 체류 선호\\n3) 콘텐츠 크리에이터·프리랜서(20–40) — 사진·영상 소재·원데이 촬영 스팟 수요\\n4) 가족·소규모 그룹(30–45) — 주차·편의시설·근거리 식사 옵션 필요\\nUSP(핵심 가치 제안)\\n- 절골길의 조용한 골목 위치로 프라이빗한 휴식 보장\\n- ‘오블로모프’ 감성의 느림·회복 스토리로 차별화\\n- 사진·영상 친화적 인테리어와 야외 테라스(콘텐츠 제작 가치 높음)\\n- 지역 미식·카페 루트와 연계한 플레이스 기반 체류 설계 가능\\n경쟁구도\\n- 직접 경쟁: 군산 내 소규모 펜션·게스트하우스·부티크 스테이\\n- 간접 경쟁: 지역 호텔·에어비앤비·당일치기 여행 코스\\n- 차별화 포인트: 브랜드 스토리(느림)·콘텐츠 친화성·로컬 연계 프로그램\\n시장 포지셔닝 제안\\n- 포지셔닝: ‘군산 절골의 느림 감성 부티크 스테이’ — mid-premium 티어\\n- 가격·프로모션: 주말 프리미엄, 주중 패키지·장기 할인 고려\\n콘텐츠·자동화 준비 체크리스트\\n- 필요 자산: ①외관 황금시간(골목샷) ②테라스/일몰 ③침실·욕실 클로즈업 ④로컬 푸드 컷 ⑤리뷰·게스트 스토리\\n- 콘텐츠 기획: 로컬 루트(카페·식당)·‘하루 힐링’ 숏폼(15–30s)·인테리어 B-roll(10s 반복 가능한 클립)\\n- 예약 전환 포인트 템플릿: 사진 헤로·편의 아이콘(주차·와이파이)·간단 정책·CTA\\n- 태그 매핑: 지역·브랜드·힐링·SNS·여행 의도(ads/shorts/리스트 헤드라인 직접 활용)\\n권장 다음 단계\\n1) 사진 촬영 10컷(상기 항목) 2) 인스타 12포스트 + 숏폼 6개 제작 3) 예약 페이지(네이버·에어비앤비) 표준화 및 FAQ 업데이트\\n', tags=['군산절골', '오블로모프스테이', '힐링스테이', '인생샷스폿', '주말여행'], facilities=['군산 절골길 근대문화 항구 카페거리 해산물', '오블로모프 느림의미학 프라이빗 부티크 스테이', '힐링 휴식 일상탈출 온전한쉼 감성여행', '감성포토 인생샷 테라스 일몰 빈티지인테리어', '주차편의 빠른예약 유연취소 와이파이 원데이스테이', '로컬식당 해산물 카페투어 산책코스 공방체험'])}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 9,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"var2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 9,
|
||||||
|
"id": "f3bf1d76-bd2a-43d5-8d39-f0ab2459701a",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Location\n",
|
||||||
|
"군산 절골 골목 스팟\n",
|
||||||
|
"골목 깊숙한 전원 감성, 조용한 휴식\n",
|
||||||
|
"Concept\n",
|
||||||
|
"프라이빗 전원스테이\n",
|
||||||
|
"소규모 전용 공간, 맞춤형 프라이버시\n",
|
||||||
|
"Visuals\n",
|
||||||
|
"인생샷 포토존\n",
|
||||||
|
"포토제닉 실내·외 컷, 밤 조명 무드\n",
|
||||||
|
"Experience\n",
|
||||||
|
"힐링 리셋 스테이\n",
|
||||||
|
"일상 탈출·짧은 리셋, 주말 최적\n",
|
||||||
|
"Digital\n",
|
||||||
|
"SNS 바이럴 감성\n",
|
||||||
|
"쇼트폼 영상·릴스용 비주얼 중심\n",
|
||||||
|
"Booking\n",
|
||||||
|
"주말 스테이케이션 패키지\n",
|
||||||
|
"주말 1박 패키지, 조식·체험 옵션\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"for i in var2[\"selling_points\"]:\n",
|
||||||
|
" print(i['category'])\n",
|
||||||
|
" print(i['keywords'])\n",
|
||||||
|
" print(i['description'])"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 10,
|
||||||
|
"id": "c89cf2eb-4f16-4dc5-90c6-df89191b4e39",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"[{'category': 'Location',\n",
|
||||||
|
" 'keywords': '군산 절골 골목 스팟',\n",
|
||||||
|
" 'description': '골목 깊숙한 전원 감성, 조용한 휴식'},\n",
|
||||||
|
" {'category': 'Concept',\n",
|
||||||
|
" 'keywords': '프라이빗 전원스테이',\n",
|
||||||
|
" 'description': '소규모 전용 공간, 맞춤형 프라이버시'},\n",
|
||||||
|
" {'category': 'Visuals',\n",
|
||||||
|
" 'keywords': '인생샷 포토존',\n",
|
||||||
|
" 'description': '포토제닉 실내·외 컷, 밤 조명 무드'},\n",
|
||||||
|
" {'category': 'Experience',\n",
|
||||||
|
" 'keywords': '힐링 리셋 스테이',\n",
|
||||||
|
" 'description': '일상 탈출·짧은 리셋, 주말 최적'},\n",
|
||||||
|
" {'category': 'Digital',\n",
|
||||||
|
" 'keywords': 'SNS 바이럴 감성',\n",
|
||||||
|
" 'description': '쇼트폼 영상·릴스용 비주얼 중심'},\n",
|
||||||
|
" {'category': 'Booking',\n",
|
||||||
|
" 'keywords': '주말 스테이케이션 패키지',\n",
|
||||||
|
" 'description': '주말 1박 패키지, 조식·체험 옵션'}]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 10,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"var2[\"selling_points\"]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 11,
|
||||||
|
"id": "231963d6-e209-41b3-8e78-2ad5d06943fe",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"['오블로모프 군산 절골', '전원감성스테이', '힐링리셋', '인생샷 명소', '주말스테이케이션']"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 11,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"var2[\"tags\"]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 13,
|
||||||
|
"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": 15,
|
||||||
|
"id": "c46abcda-d6a8-485e-92f1-526fb28c6b53",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"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\": \"string\"\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",
|
||||||
|
" },\n",
|
||||||
|
" \"required\": [\"report\", \"selling_points\", \"tags\"],\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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue