Compare commits

..

No commits in common. "2f384fb72a4ab634b451dd7b28973c999ce9a3dd" and "a9d0a3ee7f151ef937e84481f576d6b2b08eeb30" have entirely different histories.

23 changed files with 1304 additions and 2915 deletions

2
.gitignore vendored
View File

@ -28,8 +28,6 @@ build/
*.mp4 *.mp4
media/ media/
*.ipynb_checkpoint*
# Static files # Static files
static/ static/

View File

@ -27,7 +27,6 @@ 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.logger import get_logger from app.utils.logger import get_logger
from app.utils.nvMapScraper import NvMapScraper, GraphQLException from app.utils.nvMapScraper import NvMapScraper, GraphQLException
from app.utils.prompts.prompts import marketing_prompt
from config import MEDIA_ROOT from config import MEDIA_ROOT
# 로거 설정 # 로거 설정
@ -165,49 +164,33 @@ 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
logger.debug(f"[crawling] Step 3-1: 서비스 초기화 완료 ({step3_1_elapsed:.1f}ms)") logger.debug(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()
input_marketing_data = { prompt = chatgpt_service.build_market_analysis_prompt()
"customer_name" : customer_name, step3_2_elapsed = (time.perf_counter() - step3_2_start) * 1000
"region" : region, logger.debug(f"[crawling] Step 3-2: 프롬프트 생성 완료 - {len(prompt)}자 ({step3_2_elapsed:.1f}ms)")
"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
# Step 3-3: GPT API 호출 # Step 3-3: GPT API 호출
step3_3_start = time.perf_counter() step3_3_start = time.perf_counter()
structured_report = await chatgpt_service.generate_structured_output(marketing_prompt, input_marketing_data) raw_response = await chatgpt_service.generate(prompt)
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 호출 완료 - ({step3_3_elapsed:.1f}ms)") logger.info(f"[crawling] Step 3-3: GPT API 호출 완료 - 응답 {len(raw_response)}자 ({step3_3_elapsed:.1f}ms)")
logger.debug(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()
logger.debug(f"[crawling] Step 3-4: 응답 파싱 시작 - facility_info: {scraper.facility_info}") logger.debug(f"[crawling] Step 3-4: 응답 파싱 시작 - facility_info: {scraper.facility_info}")
parsed = await chatgpt_service.parse_marketing_analysis(
# 요약 Deprecated / 20250115 / Selling points를 첫 prompt에서 추출 중 raw_response
# 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로 변수와 데이터구조 변경할 것
) )
# Selling Points 구조 marketing_analysis = MarketingAnalysis(**parsed)
# print(sp['category'])
# print(sp['keywords'])
# print(sp['description'])
step3_4_elapsed = (time.perf_counter() - step3_4_start) * 1000 step3_4_elapsed = (time.perf_counter() - step3_4_start) * 1000
logger.debug(f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)") logger.debug(f"[crawling] Step 3-4: 응답 파싱 완료 ({step3_4_elapsed:.1f}ms)")
@ -240,7 +223,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,
} }

View File

@ -44,12 +44,10 @@ from app.utils.chatgpt_prompt import ChatgptService
from app.utils.logger import get_logger from app.utils.logger import get_logger
from app.utils.pagination import PaginatedResponse, get_paginated from app.utils.pagination import PaginatedResponse, get_paginated
from app.utils.prompts.prompts import lyric_prompt
import traceback as tb
# 로거 설정 # 로거 설정
logger = get_logger("lyric") logger = get_logger("lyric")
router = APIRouter(prefix="/lyric", tags=["lyric"]) router = APIRouter(prefix="/lyric", tags=["Lyric"])
# ============================================================================= # =============================================================================
@ -230,7 +228,6 @@ 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
logger.info(f"[generate_lyric] ========== START ==========") logger.info(f"[generate_lyric] ========== START ==========")
logger.info( logger.info(
f"[generate_lyric] task_id: {task_id}, " f"[generate_lyric] task_id: {task_id}, "
@ -243,45 +240,16 @@ async def generate_lyric(
step1_start = time.perf_counter() step1_start = time.perf_counter()
logger.debug(f"[generate_lyric] Step 1: 서비스 초기화 및 프롬프트 생성...") logger.debug(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" : """
812 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
#logger.debug(f"[generate_lyric] Step 1 완료 - 프롬프트 {len(prompt)}자 ({step1_elapsed:.1f}ms)") logger.debug(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()
@ -305,12 +273,11 @@ async def generate_lyric(
step3_start = time.perf_counter() step3_start = time.perf_counter()
logger.debug(f"[generate_lyric] Step 3: Lyric 저장 (processing)...") logger.debug(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=estimated_prompt, lyric_prompt=prompt,
lyric_result=None, lyric_result=None,
language=request_body.language, language=request_body.language,
) )
@ -328,8 +295,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=lyric_prompt, prompt=prompt,
lyric_input_data=lyric_input_data, language=request_body.language,
) )
step4_elapsed = (time.perf_counter() - step4_start) * 1000 step4_elapsed = (time.perf_counter() - step4_start) * 1000
@ -363,7 +330,7 @@ async def generate_lyric(
task_id=task_id, task_id=task_id,
lyric=None, lyric=None,
language=request_body.language, language=request_body.language,
error_message=''.join(tb.format_exception(None, e, e.__traceback__)), error_message=str(e),
) )

View File

@ -0,0 +1,836 @@
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
from app.utils.logger import get_logger
# 로거 설정
logger = get_logger("lyric")
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:
logger.error(f"Database error in get_store_info: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
)
except Exception as e:
logger.error(f"Unexpected error in get_store_info: {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:
logger.error(f"Database error in get_attribute: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
)
except Exception as e:
logger.error(f"Unexpected error in get_attribute: {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:
logger.error(f"Database error in get_attribute: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
)
except Exception as e:
logger.error(f"Unexpected error in get_attribute: {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:
logger.error(f"Database error in get_sample_song: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
)
except Exception as e:
logger.error(f"Unexpected error in get_sample_song: {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:
logger.error(f"Database error in get_prompt_template: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
)
except Exception as e:
logger.error(f"Unexpected error in get_prompt_template: {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:
logger.error(f"Database error in get_song_result: {e}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
)
except Exception as e:
logger.error(f"Unexpected error in get_song_result: {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)
logger.info(f"{'=' * 60}")
logger.info(f"Store ID: {form_data.store_id}")
logger.info(f"Lyrics IDs: {form_data.lyrics_ids}")
logger.info(f"Prompt IDs: {form_data.prompts}")
logger.info(f"{'=' * 60}")
# 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]
logger.info(f"Store: {store_info.store_name}")
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
# 4. Sample Song 조회 및 결합
combined_sample_song = None
if form_data.lyrics_ids:
logger.info(f"[샘플 가사 조회] - {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)]
)
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else:
logger.info("샘플 가사가 비어있습니다")
else:
logger.info("선택된 lyrics가 없습니다")
# 5. 템플릿 가져오기
if not form_data.prompts:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="프롬프트 ID가 필요합니다",
)
logger.info("템플릿 가져오기")
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]
logger.debug(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}
"""
logger.debug(f"[업데이트된 프롬프트]\n{updated_prompt}")
# 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}"""
logger.debug("=" * 40)
logger.debug(f"[translate:form_data.attributes_str:] {form_data.attributes_str}")
logger.debug(f"[translate:total_chars_with_space:] {total_chars_with_space}")
logger.debug(f"[translate:total_chars_without_space:] {total_chars_without_space}")
logger.debug(f"[translate:final_lyrics:]\n{final_lyrics}")
logger.debug("=" * 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()
logger.info("결과 저장 완료")
logger.info("전체 결과 조회 중...")
# 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()
]
logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
return results_list
except HTTPException:
raise
except SQLAlchemyError as e:
logger.error(f"Database Error: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.",
)
except Exception as e:
logger.error(f"Unexpected Error: {e}", exc_info=True)
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()
]
logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
return results_list
except HTTPException: # HTTPException은 그대로 raise
raise
except SQLAlchemyError as e:
logger.error(f"Database Error: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.",
)
except Exception as e:
logger.error(f"Unexpected Error: {e}", exc_info=True)
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)
logger.info(f"{'=' * 60}")
logger.info(f"Store ID: {form_data.store_id}")
logger.info(f"{'=' * 60}")
# 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]
logger.info(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)
logger.debug(f"[포맷팅된 문자열 속성 정보]\n{formatted_attributes}")
else:
logger.info("속성 데이터가 없습니다")
formatted_attributes = ""
# 4. 템플릿 가져오기
logger.info("템플릿 가져오기 (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],
)
logger.debug(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 "",
)
logger.debug("=" * 80)
logger.debug("업데이트된 프롬프트")
logger.debug("=" * 80)
logger.debug(updated_prompt)
logger.debug("=" * 80)
# 4. Sample Song 조회 및 결합
combined_sample_song = None
if form_data.lyrics_ids:
logger.info(f"[샘플 가사 조회] - {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)]
)
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else:
logger.info("샘플 가사가 비어있습니다")
else:
logger.info("선택된 lyrics가 없습니다")
# 1. song_sample 테이블의 모든 ID 조회
logger.info("[샘플 가사 랜덤 선택]")
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()]
logger.info(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)
logger.info(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)]
)
logger.info(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
else:
logger.info("샘플 가사가 비어있습니다")
else:
logger.info("song_sample 테이블에 데이터가 없습니다")
# 5. 프롬프트에 샘플 가사 추가
if combined_sample_song:
updated_prompt += f"""
다음은 참고해야 하는 샘플 가사 정보입니다.
샘플 가사를 참고하여 작곡을 해주세요.
{combined_sample_song}
"""
logger.info("샘플 가사 정보가 프롬프트에 추가되었습니다")
else:
logger.info("샘플 가사가 없어 기본 프롬프트만 사용합니다")
logger.info(f"[최종 프롬프트 길이: {len(updated_prompt)} 자]")
# 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()
);
"""
logger.debug("[insert_params 선택된 속성 확인]")
logger.debug(f"Categories: {selected_categories}")
logger.debug(f"Values: {selected_values}")
# 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()
logger.info("결과 저장 완료")
logger.info("전체 결과 조회 중...")
# 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()
]
logger.info(f"전체 {len(results_list)}개의 결과 조회 완료")
return results_list
except HTTPException:
raise
except SQLAlchemyError as e:
logger.error(f"Database Error: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="데이터베이스 연결에 문제가 발생했습니다.",
)
except Exception as e:
logger.error(f"Unexpected Error: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="서비스 처리 중 오류가 발생했습니다.",
)

View File

@ -12,7 +12,6 @@ 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
from app.utils.logger import get_logger from app.utils.logger import get_logger
# 로거 설정 # 로거 설정
@ -65,8 +64,8 @@ async def _update_lyric_status(
async def generate_lyric_background( async def generate_lyric_background(
task_id: str, task_id: str,
prompt: Prompt, prompt: str,
lyric_input_data: dict, # 프롬프트 메타데이터에서 정의된 Input language: str,
) -> None: ) -> None:
"""백그라운드에서 ChatGPT를 통해 가사를 생성하고 Lyric 테이블을 업데이트합니다. """백그라운드에서 ChatGPT를 통해 가사를 생성하고 Lyric 테이블을 업데이트합니다.
@ -81,22 +80,20 @@ 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}")
logger.debug(f"[generate_lyric_background] ========== START ==========") logger.debug(f"[generate_lyric_background] ========== START ==========")
logger.debug(f"[generate_lyric_background] task_id: {task_id}") logger.debug(f"[generate_lyric_background] task_id: {task_id}")
logger.debug(f"[generate_lyric_background] language: {lyric_input_data['language']}") logger.debug(f"[generate_lyric_background] language: {language}")
#logger.debug(f"[generate_lyric_background] prompt length: {len(prompt)}자") logger.debug(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()
logger.debug(f"[generate_lyric_background] Step 1: ChatGPT 서비스 초기화...") logger.debug(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
logger.debug(f"[generate_lyric_background] Step 1 완료 ({step1_elapsed:.1f}ms)") logger.debug(f"[generate_lyric_background] Step 1 완료 ({step1_elapsed:.1f}ms)")
@ -106,9 +103,8 @@ 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}")
logger.debug(f"[generate_lyric_background] Step 2: ChatGPT API 호출 시작...") logger.debug(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)")

View File

@ -5,11 +5,222 @@ from openai import AsyncOpenAI
from app.utils.logger import get_logger from app.utils.logger import get_logger
from config import apikey_settings from config import apikey_settings
from app.utils.prompts.prompts import Prompt
# 로거 설정 # 로거 설정
logger = get_logger("chatgpt") logger = get_logger("chatgpt")
# 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 Selection:
<<CRITICAL: Use ONLY the keywords for {language}. DO NOT transliterate or use keywords from other languages.>>
Select keywords based on {language}:
- IF Korean: 인스타 감성, 사진같은 하루, 힐링, 여행, 감성 숙소
- IF English: Instagram vibes, picture-perfect day, healing, travel, getaway
- IF Chinese: 网红打卡, 治愈系, 旅行, 度假, 拍照圣地
- IF Japanese: インスタ映え, 写真のような一日, 癒し, 旅行, 絶景
- IF Thai: กสวย, ลใจ, เทยว, ายร, วสวย
- IF Vietnamese: check-in đẹp, healing, du lịch, nghỉ dưỡng, view đẹp
<<FORBIDDEN: Never transliterate keywords from other languages (e.g., "왕홍다카", "인스타바에", "티팍수아이" are WRONG)>>
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 (한국어)**
2. Facilities
- Based on the business name and region details, identify 5 likely facilities/amenities
- Consider typical facilities for accommodations in the given region
- Examples: 바베큐장, 수영장, 주차장, 와이파이, 주방, 테라스, 정원, etc.
- Return as JSON with key "facilities"
- **MUST be written in Korean (한국어)**
[CRITICAL LANGUAGE REQUIREMENT - ABSOLUTE RULE]
ALL OUTPUT MUST BE WRITTEN IN KOREAN (한국어)
- Analysis sections: Korean only
- Tags: Korean only
- Facilities: 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"],
"facilities": ["부대시설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
@ -19,29 +230,173 @@ class ChatgptService:
GPT 5.0 모델을 사용하여 마케팅 가사 분석을 생성합니다. GPT 5.0 모델을 사용하여 마케팅 가사 분석을 생성합니다.
""" """
def __init__(self): def __init__(
self.client = AsyncOpenAI(api_key=apikey_settings.CHATGPT_API_KEY)
async def _call_structured_output_with_response_gpt_api(self, prompt: str, output_format : dict, model:str) -> dict:
content = [{"type": "input_text", "text": prompt}]
response = await self.client.responses.create(
model=model,
input=[{"role": "user", "content": content}],
text = output_format
)
structured_output = json.loads(response.output_text)
return structured_output or {}
async def generate_structured_output(
self, self,
prompt : Prompt, customer_name: str,
input_data : dict, region: str,
) -> str: detail_region_info: str = "",
prompt_text = prompt.build_prompt(input_data) language: str = "Korean",
):
# 최신 모델: gpt-5-mini
self.model = "gpt-5-mini"
self.client = AsyncOpenAI(api_key=apikey_settings.CHATGPT_API_KEY)
self.customer_name = customer_name
self.region = region
self.detail_region_info = detail_region_info
self.language = language
print(f"[ChatgptService] Generated Prompt (length: {len(prompt_text)})") def build_lyrics_prompt(self) -> str:
logger.info(f"[ChatgptService] Starting GPT request with structured output with model: {prompt.prompt_model}") """LYRICS_PROMPT_TEMPLATE에 고객 정보를 대입하여 완성된 프롬프트 반환"""
return LYRICS_PROMPT_TEMPLATE.format(
customer_name=self.customer_name,
region=self.region,
detail_region_info=self.detail_region_info,
language=self.language,
)
def build_market_analysis_prompt(self) -> str:
"""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,
prompt: str | None = None,
) -> str:
"""GPT에게 프롬프트를 전달하여 결과를 반환합니다.
Args:
prompt: GPT에 전달할 프롬프트 (None이면 기본 가사 프롬프트 사용)
Returns:
GPT 응답 문자열
Raises:
APIError, APIConnectionError, RateLimitError: OpenAI API 오류
"""
if prompt is None:
prompt = self.build_lyrics_prompt()
logger.debug(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_structured_output_with_response_gpt_api(prompt_text, prompt.prompt_output, prompt.prompt_model) response = await self._call_gpt_api(prompt)
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 항목으로 구분: 타겟 고객, 핵심 차별점, 지역 특성, 시즌별 포인트
- 항목은 줄바꿈으로 구분
- 800 이내로 요약
- 내용의 누락이 있어서는 안된다
- 문장이 자연스러워야 한다
- 핵심 정보만 간결하게 포함
- 한국어로 작성
- 특수문자 사용 금지 (괄호, 슬래시, 하이픈, 물결표 제외)
- 쉼표와 마침표만 사용하여 자연스러운 문장으로 작성
[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) -> dict:
"""ChatGPT 마케팅 분석 응답을 파싱하고 요약하여 딕셔너리로 반환
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", [])
facilities = json_data.get("facilities", [])
# 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:
pass
# 리포트 내용을 500자로 요약
if report:
report = await self.summarize_marketing(report)
return {"report": report, "tags": tags, "facilities": facilities}

View File

@ -1,34 +0,0 @@
{
"model": "gpt-5-mini",
"prompt_variables": [
"customer_name",
"region",
"detail_region_info",
"marketing_intelligence_summary",
"language",
"promotional_expression_example",
"timing_rules"
],
"output_format": {
"format": {
"type": "json_schema",
"name": "lyric",
"schema": {
"type": "object",
"properties": {
"lyric": {
"type": "string"
},
"suno_prompt":{
"type" : "string"
}
},
"required": [
"lyric", "suno_prompt"
],
"additionalProperties": false
},
"strict": true
}
}
}

View File

@ -1,76 +0,0 @@
[ROLE]
You are a content marketing expert, brand strategist, and creative songwriter
specializing in Korean pension / accommodation businesses.
You create lyrics strictly based on Brand & Marketing Intelligence analysis
and optimized for viral short-form video content.
[INPUT]
Business Name: {customer_name}
Region: {region}
Region Details: {detail_region_info}
Brand & Marketing Intelligence Report: {marketing_intelligence_summary}
Output Language: {language}
[INTERNAL ANALYSIS DO NOT OUTPUT]
Internally analyze the following to guide all creative decisions:
- Core brand identity and positioning
- Emotional hooks derived from selling points
- Target audience lifestyle, desires, and travel motivation
- Regional atmosphere and symbolic imagery
- How the stay converts into “shareable moments”
- Which selling points must surface implicitly in lyrics
[LYRICS & MUSIC CREATION TASK]
Based on the Brand & Marketing Intelligence Report for [{customer_name} ({region})], generate:
- Original promotional lyrics
- Music attributes for AI music generation (Suno-compatible prompt)
The output must be designed for VIRAL DIGITAL CONTENT
(short-form video, reels, ads).
[LYRICS REQUIREMENTS]
Mandatory Inclusions:
- Business name
- Region name
- Promotion subject
- Promotional expressions including:
{promotional_expression_example}
Content Rules:
- Lyrics must be emotionally driven, not descriptive listings
- Selling points must be IMPLIED, not explained
- Must sound natural when sung
- Must feel like a lifestyle moment, not an advertisement
Tone & Style:
- Warm, emotional, and aspirational
- Trendy, viral-friendly phrasing
- Calm but memorable hooks
- Suitable for travel / stay-related content
[SONG & MUSIC ATTRIBUTES FOR SUNO PROMPT]
After the lyrics, generate a concise music prompt including:
Song mood (emotional keywords)
BPM range
Recommended genres (max 2)
Key musical motifs or instruments
Overall vibe (1 short sentence)
[CRITICAL LANGUAGE REQUIREMENT ABSOLUTE RULE]
ALL OUTPUT MUST BE 100% WRITTEN IN {language}.
no mixed languages
All names, places, and expressions must be in {language}
Any violation invalidates the entire output
[OUTPUT RULES STRICT]
{timing_rules}
No explanations
No headings
No bullet points
No analysis
No extra text
[FAILURE FORMAT]
If generation is impossible:
ERROR: Brief reason in English

View File

@ -1,58 +0,0 @@
{
"model": "gpt-5.2",
"prompt_variables": [
"customer_name",
"region",
"detail_region_info"
],
"output_format": {
"format": {
"type": "json_schema",
"name": "report",
"schema": {
"type": "object",
"properties": {
"report": {
"type": "string"
},
"selling_points": {
"type": "array",
"items": {
"type": "object",
"properties": {
"category": {
"type": "string"
},
"keywords": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": [
"category",
"keywords",
"description"
],
"additionalProperties": false
}
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"report",
"selling_points",
"tags"
],
"additionalProperties": false
},
"strict": true
}
}
}

View File

@ -1,64 +0,0 @@
[Role & Objective]
Act as a content marketing expert with strong domain knowledge in the Korean pension / stay-accommodation industry.
Your goal is to produce a Marketing Intelligence Report that will be shown to accommodation owners BEFORE any content is generated.
The report must clearly explain what makes the property sellable, marketable, and scalable through content.
[INPUT]
- Business Name: {customer_name}
- Region: {region}
- Region Details: {detail_region_info}
[Core Analysis Requirements]
Analyze the property based on:
Location, concept, and nearby environment
Target customer behavior and reservation decision factors
Include:
- Target customer segments & personas
- Unique Selling Propositions (USPs)
- Competitive landscape (direct & indirect competitors)
- Market positioning
[Key Selling Point Structuring UI Optimized]
From the analysis above, extract the main Key Selling Points using the structure below.
Rules:
Focus only on factors that directly influence booking decisions
Each selling point must be concise and visually scannable
Language must be reusable for ads, short-form videos, and listing headlines
Avoid full sentences in descriptions; use short selling phrases
Do not provide in report
Output format:
[Category]
(Tag keyword 5~8 words, noun-based, UI oval-style)
One-line selling phrase (not a full sentence)
Limit:
5 to 8 Key Selling Points only
Do not provide in report
[Content & Automation Readiness Check]
Ensure that:
Each tag keyword can directly map to a content theme
Each selling phrase can be used as:
- Video hook
- Image headline
- Ad copy snippet
[Tag Generation Rules]
- Tags must include **only core keywords that can be directly used for viral video song lyrics**
- Each tag should be selected with **search discovery + emotional resonance + reservation conversion** in mind
- The number of tags must be **exactly 5**
- Tags must be **nouns or short keyword phrases**; full sentences are strictly prohibited
- The following categories must be **balanced and all represented**:
1) **Location / Local context** (region name, neighborhood, travel context)
2) **Accommodation positioning** (emotional stay, private stay, boutique stay, etc.)
3) **Emotion / Experience** (healing, rest, one-day escape, memory, etc.)
4) **SNS / Viral signals** (Instagram vibes, picture-perfect day, aesthetic travel, etc.)
5) **Travel & booking intent** (travel, getaway, stay, relaxation, etc.)
- If a brand name exists, **at least one tag must include the brand name or a brand-specific expression**
- Avoid overly generic keywords (e.g., “hotel”, “travel” alone); **prioritize distinctive, differentiating phrases**
- The final output must strictly follow the JSON format below, with no additional text
"tags": ["Tag1", "Tag2", "Tag3", "Tag4", "Tag5"]

View File

@ -1,62 +0,0 @@
[Role & Objective]
Act as a content marketing expert with strong domain knowledge in the Korean pension / stay-accommodation industry.
Your goal is to produce a Marketing Intelligence Report that will be shown to accommodation owners BEFORE any content is generated.
The report must clearly explain what makes the property sellable, marketable, and scalable through content.
[INPUT]
- Business Name: {customer_name}
- Region: {region}
- Region Details: {detail_region_info}
[Core Analysis Requirements]
Analyze the property based on:
Location, concept, photos, online presence, and nearby environment
Target customer behavior and reservation decision factors
Include:
- Target customer segments & personas
- Unique Selling Propositions (USPs)
- Competitive landscape (direct & indirect competitors)
- Market positioning
[Key Selling Point Structuring UI Optimized]
From the analysis above, extract the main Key Selling Points using the structure below.
Rules:
Focus only on factors that directly influence booking decisions
Each selling point must be concise and visually scannable
Language must be reusable for ads, short-form videos, and listing headlines
Avoid full sentences in descriptions; use short selling phrases
Output format:
[Category]
(Tag keyword 5~8 words, noun-based, UI oval-style)
One-line selling phrase (not a full sentence)
Limit:
5 to 8 Key Selling Points only
[Content & Automation Readiness Check]
Ensure that:
Each tag keyword can directly map to a content theme
Each selling phrase can be used as:
- Video hook
- Image headline
- Ad copy snippet
[Tag Generation Rules]
- Tags must include **only core keywords that can be directly used for viral video song lyrics**
- Each tag should be selected with **search discovery + emotional resonance + reservation conversion** in mind
- The number of tags must be **exactly 5**
- Tags must be **nouns or short keyword phrases**; full sentences are strictly prohibited
- The following categories must be **balanced and all represented**:
1) **Location / Local context** (region name, neighborhood, travel context)
2) **Accommodation positioning** (emotional stay, private stay, boutique stay, etc.)
3) **Emotion / Experience** (healing, rest, one-day escape, memory, etc.)
4) **SNS / Viral signals** (Instagram vibes, picture-perfect day, aesthetic travel, etc.)
5) **Travel & booking intent** (travel, getaway, stay, relaxation, etc.)
- If a brand name exists, **at least one tag must include the brand name or a brand-specific expression**
- Avoid overly generic keywords (e.g., “hotel”, “travel” alone); **prioritize distinctive, differentiating phrases**
- The final output must strictly follow the JSON format below, with no additional text
"tags": ["Tag1", "Tag2", "Tag3", "Tag4", "Tag5"]

View File

@ -1,73 +0,0 @@
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}")
flooding_variables = set(self.prompt_input) - input_data.keys()
if flooding_variables:
raise Exception(f"flooding_variables for prompt {self.prompt_name} : {flooding_variables}")
return True
marketing_prompt = Prompt(
prompt_name=prompt_settings.MARKETING_PROMPT_NAME,
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.MARKETING_PROMPT_NAME)
)
summarize_prompt = Prompt(
prompt_name=prompt_settings.SUMMARIZE_PROMPT_NAME,
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.SUMMARIZE_PROMPT_NAME)
)
lyric_prompt = Prompt(
prompt_name=prompt_settings.LYLIC_PROMPT_NAME,
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.LYLIC_PROMPT_NAME)
)
def reload_all_prompt():
marketing_prompt._reload_prompt()
summarize_prompt._reload_prompt()
lyric_prompt._reload_prompt()

View File

@ -1,33 +0,0 @@
{
"prompt_variables": [
"report",
"selling_points"
],
"output_format": {
"format": {
"type": "json_schema",
"name": "tags",
"schema": {
"type": "object",
"properties": {
"category": {
"type": "string"
},
"tag_keywords": {
"type": "string"
},
"description": {
"type": "string"
}
},
"required": [
"category",
"tag_keywords",
"description"
],
"additionalProperties": false
},
"strict": true
}
}
}

View File

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

View File

@ -145,13 +145,6 @@ 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
class KakaoSettings(BaseSettings): class KakaoSettings(BaseSettings):
"""카카오 OAuth 설정""" """카카오 OAuth 설정"""
@ -382,7 +375,6 @@ 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()
log_settings = LogSettings() log_settings = LogSettings()
kakao_settings = KakaoSettings() kakao_settings = KakaoSettings()
jwt_settings = JWTSettings() jwt_settings = JWTSettings()

View File

@ -1,723 +0,0 @@
{
"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&timestamp=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 커플(2535) — 인생샷·감성카페·주말 데이트 용도\\n2) 휴식형 성인(3050) — ‘힐링’·프라이버시·느긋한 체류 선호\\n3) 콘텐츠 크리에이터·프리랜서(2040) — 사진·영상 소재·원데이 촬영 스팟 수요\\n4) 가족·소규모 그룹(3045) — 주차·편의시설·근거리 식사 옵션 필요\\nUSP(핵심 가치 제안)\\n- 절골길의 조용한 골목 위치로 프라이빗한 휴식 보장\\n- ‘오블로모프’ 감성의 느림·회복 스토리로 차별화\\n- 사진·영상 친화적 인테리어와 야외 테라스(콘텐츠 제작 가치 높음)\\n- 지역 미식·카페 루트와 연계한 플레이스 기반 체류 설계 가능\\n경쟁구도\\n- 직접 경쟁: 군산 내 소규모 펜션·게스트하우스·부티크 스테이\\n- 간접 경쟁: 지역 호텔·에어비앤비·당일치기 여행 코스\\n- 차별화 포인트: 브랜드 스토리(느림)·콘텐츠 친화성·로컬 연계 프로그램\\n시장 포지셔닝 제안\\n- 포지셔닝: ‘군산 절골의 느림 감성 부티크 스테이’ — mid-premium 티어\\n- 가격·프로모션: 주말 프리미엄, 주중 패키지·장기 할인 고려\\n콘텐츠·자동화 준비 체크리스트\\n- 필요 자산: ①외관 황금시간(골목샷) ②테라스/일몰 ③침실·욕실 클로즈업 ④로컬 푸드 컷 ⑤리뷰·게스트 스토리\\n- 콘텐츠 기획: 로컬 루트(카페·식당)·‘하루 힐링’ 숏폼(1530s)·인테리어 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",
"812 lines\n",
"Full verse flow, immersive mood\n",
"\n",
"No explanations\n",
"No headings\n",
"No bullet points\n",
"No analysis\n",
"No extra text\n",
"\n",
"[FAILURE FORMAT]\n",
"If generation is impossible:\n",
"ERROR: Brief reason in English\n",
"\"\"\"\n",
"lyric_prompt_dict = {\n",
" \"prompt_variables\" :\n",
" [\n",
" \"customer_name\",\n",
" \"region\",\n",
" \"detail_region_info\",\n",
" \"marketing_intelligence_summary\",\n",
" \"language\",\n",
" \"promotional_expression_example\",\n",
" \"timing_rules\",\n",
" \n",
" ],\n",
" \"output_format\" : {\n",
" \"format\": {\n",
" \"type\": \"json_schema\",\n",
" \"name\": \"lyric\",\n",
" \"schema\": {\n",
" \"type\":\"object\",\n",
" \"properties\" : {\n",
" \"lyric\" : { \n",
" \"type\" : \"string\"\n",
" }\n",
" },\n",
" \"required\": [\"lyric\"],\n",
" \"additionalProperties\": False,\n",
" },\n",
" \"strict\": True\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "79edd82b-6f4c-43c7-9205-0b970afe06d7",
"metadata": {},
"outputs": [],
"source": [
"\n",
"with open(\"./app/utils/prompts/marketing_prompt.txt\", \"w\") as fp:\n",
" fp.write(marketing_prompt)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "65a5a2a6-06a5-4ee1-a796-406c86aefc20",
"metadata": {},
"outputs": [],
"source": [
"with open(\"prompts/summarize_prompt.json\", \"r\") as fp:\n",
" p = json.load(fp)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "454d920f-e9ed-4fb2-806c-75b8f7033db9",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'prompt_variables': ['report', 'selling_points'],\n",
" 'prompt': '\\n입력 : \\n분석 보고서\\n{report}\\n\\n셀링 포인트\\n{selling_points}\\n\\n위 분석 결과를 바탕으로, 주요 셀링 포인트를 다음 구조로 재정리하라.\\n\\n조건:\\n각 셀링 포인트는 반드시 ‘카테고리 → 태그 키워드 → 한 줄 설명’ 구조를 가질 것\\n태그 키워드는 UI 상에서 타원(oval) 형태의 시각적 태그로 사용될 것을 가정하여\\n- 3 ~ 6단어 이내\\n- 명사 또는 명사형 키워드로 작성\\n- 설명은 문장이 아닌, 짧은 ‘셀링 문구’ 형태로 작성할 것\\n- 광고·숏폼·상세페이지 어디에도 바로 재사용 가능해야 함\\n- 전체 셀링 포인트 개수는 5~7개로 제한\\n\\n출력 형식:\\n[카테고리명]\\n(태그 키워드)\\n- 한 줄 설명 문구\\n\\n예시: \\n[공간 정체성]\\n(100년 적산가옥 · 시간의 결)\\n- 하루를 ‘숙박’이 아닌 ‘체류’로 바꾸는 공간\\n\\n[입지 & 희소성]\\n(말랭이마을 · 로컬 히든플레이스)\\n- 관광지가 아닌, 군산을 아는 사람의 선택\\n\\n[프라이버시]\\n(독채 숙소 · 프라이빗 스테이)\\n- 누구의 방해도 없는 완전한 휴식 구조\\n\\n[비주얼 경쟁력]\\n(감성 인테리어 · 자연광 스폿)\\n- 찍는 순간 콘텐츠가 되는 공간 설계\\n\\n[타깃 최적화]\\n(커플 · 소규모 여행)\\n- 둘에게 가장 이상적인 공간 밀도\\n\\n[체류 경험]\\n(아무것도 안 해도 되는 하루)\\n- 일정 없이도 만족되는 하루 루틴\\n\\n[브랜드 포지션]\\n(호텔도 펜션도 아닌 아지트)\\n- 다시 돌아오고 싶은 개인적 장소\\n ',\n",
" 'output_format': {'format': {'type': 'json_schema',\n",
" 'name': 'tags',\n",
" 'schema': {'type': 'object',\n",
" 'properties': {'category': {'type': 'string'},\n",
" 'tag_keywords': {'type': 'string'},\n",
" 'description': {'type': 'string'}},\n",
" 'required': ['category', 'tag_keywords', 'description'],\n",
" 'additionalProperties': False},\n",
" 'strict': True}}}"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"p"
]
},
{
"cell_type": "code",
"execution_count": 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",
"812 lines\n",
"Full verse flow, immersive mood\n",
"\n",
"No explanations\n",
"No headings\n",
"No bullet points\n",
"No analysis\n",
"No extra text\n",
"\n",
"[FAILURE FORMAT]\n",
"If generation is impossible:\n",
"ERROR: Brief reason in English\n",
"\"\"\"\n",
"with open(\"./app/utils/prompts/lyric_prompt.txt\", \"w\") as fp:\n",
" fp.write(lyric_prompt)"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "5736ca4b-c379-4cae-84a9-534cad9576c7",
"metadata": {},
"outputs": [],
"source": [
"lyric_prompt_dict = {\n",
" \"model\" : \"gpt-5-mini\",\n",
" \"prompt_variables\" :\n",
" [\n",
" \"customer_name\",\n",
" \"region\",\n",
" \"detail_region_info\",\n",
" \"marketing_intelligence_summary\",\n",
" \"language\",\n",
" \"promotional_expression_example\",\n",
" \"timing_rules\",\n",
" \n",
" ],\n",
" \"output_format\" : {\n",
" \"format\": {\n",
" \"type\": \"json_schema\",\n",
" \"name\": \"lyric\",\n",
" \"schema\": {\n",
" \"type\":\"object\",\n",
" \"properties\" : {\n",
" \"lyric\" : { \n",
" \"type\" : \"string\"\n",
" }\n",
" },\n",
" \"required\": [\"lyric\"],\n",
" \"additionalProperties\": False,\n",
" },\n",
" \"strict\": True\n",
" }\n",
" }\n",
"}\n",
"with open(\"./app/utils/prompts/lyric_prompt.json\", \"w\") as fp:\n",
" json.dump(lyric_prompt_dict, fp, ensure_ascii=False)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "430c8914-4e6a-4b53-8903-f454e7ccb8e2",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.8"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -1,77 +0,0 @@
import aiohttp
from pathlib import Path
class BlobUploader():
root_url = "https://ado2mediastoragepublic.blob.core.windows.net/ado2-media-public-access/ado2-media-original/"
image_content_types = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp"
}
# SAS TOKEN 입력으로 저장
def __init__(self, sas_token):
self.sas_token = sas_token
# 음악 파일 업로드 (user_idx/task_idx/music_file_name 경로에 저장), 해당 업로드 링크 출력 / 실패 시 None
async def upload_music_to_azure_blob(self, file_data : bytes, user_idx : str, task_idx : str, music_file_name : str) -> str:
url = f"{self.root_url}/{user_idx}/{task_idx}/{music_file_name}"
access_url = f"{url}?{self.sas_token}"
headers = {
"Content-Type": "audio/mpeg",
"x-ms-blob-type": "BlockBlob"
}
async with aiohttp.ClientSession() as session:
async with session.put(access_url, data=file_data, headers=headers) as resp:
if resp.status in [200, 201]:
return url
else:
text = await resp.text()
print(f"Failed Status Code: {resp.status}")
print(f"Response: {text}")
return None
# 영상 파일 업로드 (user_idx/task_idx/video_file_name 경로에 저장), 해당 업로드 링크 출력 / 실패 시 None
async def upload_video_to_azure_blob(self, file_data : bytes, user_idx : str, task_idx : str, video_file_name : str) -> str:
url = f"{self.root_url}/{user_idx}/{task_idx}/{video_file_name}"
access_url = f"{url}?{self.sas_token}"
headers = {
"Content-Type": "video/mp4",
"x-ms-blob-type": "BlockBlob"
}
async with aiohttp.ClientSession() as session:
async with session.put(access_url, data=file_data, headers=headers) as resp:
if resp.status in [200, 201]:
return url
else:
text = await resp.text()
print(f"Failed Status Code: {resp.status}")
print(f"Response: {text}")
return None
# 이미지 파일 업로드 (user_idx/task_idx/image_file_name 경로에 저장), 해당 업로드 링크 출력 / 실패 시 None
async def upload_image_to_azure_blob(self, file_data : bytes, user_idx : str, task_idx : str, image_file_name : str) -> str:
url = f"{self.root_url}/{user_idx}/{task_idx}/{image_file_name}"
access_url = f"{url}?{self.sas_token}"
extension = Path(image_file_name).suffix.lower()
content_type = self.image_content_types.get(extension, "image/jpeg")
headers = {
"Content-Type": content_type,
"x-ms-blob-type": "BlockBlob"
}
async with aiohttp.ClientSession() as session:
async with session.put(access_url, data=file_data, headers=headers) as resp:
if resp.status in [200, 201]:
return url
else:
text = await resp.text()
print(f"Failed Status Code: {resp.status}")
print(f"Response: {text}")
return None

View File

@ -1,29 +0,0 @@
import asyncio
from nvMapScraper import nvMapScraper
from nvMapPwScraper import nvMapPwScraper
async def main_function():
await nvMapPwScraper.initiate_scraper()
selected = {'title': '<b>스테이</b>,<b>머뭄</b>',
'link': 'https://www.instagram.com/staymeomoom',
'category': '숙박>펜션',
'description': '',
'telephone': '',
'address': '전북특별자치도 군산시 신흥동 63-18',
'roadAddress': '전북특별자치도 군산시 절골길 18',
'mapx': '1267061254',
'mapy': '359864175',
'lng': 126.7061254,
'lat': 35.9864175}
async with nvMapPwScraper() as pw_scraper:
new_url = await pw_scraper.get_place_id_url(selected)
print(new_url)
nv_scraper = nvMapScraper(new_url) # 이후 동일한 플로우
await nv_scraper.scrap()
print(nv_scraper.rawdata)
return
print("running main_funtion..")
asyncio.run(main_function())

View File

@ -1,113 +0,0 @@
import asyncio
from playwright.async_api import async_playwright
from urllib import parse
class nvMapPwScraper():
# cls vars
is_ready = False
_playwright = None
_browser = None
_context = None
_win_width = 1280
_win_height = 720
_max_retry = 30 # place id timeout threshold seconds
# instance var
page = None
@classmethod
def default_context_builder(cls):
context_builder_dict = {}
context_builder_dict['viewport'] = {
'width' : cls._win_width,
'height' : cls._win_height
}
context_builder_dict['screen'] = {
'width' : cls._win_width,
'height' : cls._win_height
}
context_builder_dict['user_agent'] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
context_builder_dict['locale'] = 'ko-KR'
context_builder_dict['timezone_id']='Asia/Seoul'
return context_builder_dict
@classmethod
async def initiate_scraper(cls):
if not cls._playwright:
cls._playwright = await async_playwright().start()
if not cls._browser:
cls._browser = await cls._playwright.chromium.launch(headless=True)
if not cls._context:
cls._context = await cls._browser.new_context(**cls.default_context_builder())
cls.is_ready = True
def __init__(self):
if not self.is_ready:
raise Exception("nvMapScraper is not initiated")
async def __aenter__(self):
await self.create_page()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.page.close()
async def create_page(self):
self.page = await self._context.new_page()
await self.page.add_init_script(
'''const defaultGetter = Object.getOwnPropertyDescriptor(
Navigator.prototype,
"webdriver"
).get;
defaultGetter.apply(navigator);
defaultGetter.toString();
Object.defineProperty(Navigator.prototype, "webdriver", {
set: undefined,
enumerable: true,
configurable: true,
get: new Proxy(defaultGetter, {
apply: (target, thisArg, args) => {
Reflect.apply(target, thisArg, args);
return false;
},
}),
});
const patchedGetter = Object.getOwnPropertyDescriptor(
Navigator.prototype,
"webdriver"
).get;
patchedGetter.apply(navigator);
patchedGetter.toString();''')
await self.page.set_extra_http_headers({
'sec-ch-ua': '\"Not?A_Brand\";v=\"99\", \"Chromium\";v=\"130\"'
})
await self.page.goto("http://google.com")
async def goto_url(self, url, wait_until="domcontentloaded", timeout=20000):
page = self.page
await page.goto(url, wait_until=wait_until, timeout=timeout)
async def get_place_id_url(self, selected):
title = selected['title'].replace("<b>", "").replace("</b>", "")
address = selected.get('roadAddress', selected['address']).replace("<b>", "").replace("</b>", "")
encoded_query = parse.quote(f"{address} {title}")
url = f"https://map.naver.com/p/search/{encoded_query}"
await self.goto_url(url, wait_until="networkidle",timeout = self._max_retry/2*1000)
if "/place/" in self.page.url:
return self.page.url
url = self.page.url.replace("?","?isCorrectAnswer=true&")
await self.goto_url(url, wait_until="networkidle",timeout = self._max_retry/2*1000)
if "/place/" in self.page.url:
return self.page.url
if (count == self._max_retry / 2):
raise Exception("Failed to identify place id. loading timeout")
else:
raise Exception("Failed to identify place id. item is ambiguous")

View File

@ -1,14 +1,13 @@
import re
import aiohttp
import json
import asyncio import asyncio
import bs4 import json
import re
import aiohttp
PLACE_PATTERN = r"/place/(\d+)"
GRAPHQL_URL = "https://pcmap-api.place.naver.com/graphql" GRAPHQL_URL = "https://pcmap-api.place.naver.com/graphql"
NAVER_COOKIES="NAC=mQ7mBownbQf4A; NNB=TQPII6AKDBFGQ; PLACE_LANGUAGE=ko; NACT=1; nid_inf=1431570813; NID_AUT=k2T7FraXOdIMRCHzEZIFtHQup+I7b87M5fd7+p65AXZTdGB/gelRmW8s/Q4oDxm8; tooltipDisplayed=true; SRT30=1762660151; NID_SES=AAAB1Lpy3y3hGzuPbJpJl8vvFx18C+HXXuZEFou/YPgocHe7k2/5MpFlgE48X1JF7c7IPoU2khZKkkuLx+tsvWAzOf0TnG/G8RrBGeawnSluSJcKcTdKKRJ4cygKc/OabVxoc3TNZJWxer3vFtXBoXkDS5querVNS6wvcMhA/p4vkPKOeepwKLR+1IJERlQJWZw4q29IdAysrbBNn3Akf9mDA5eTYvMDLYyRkToRh10TVMW/yhyNQeMXlIdnR8U1ZCNqe/9ErYdos5gQDstswEJQQA0T2cHFGJOtmlYMPlnhWado5w521iZXGJyKcA9ZawizM/i5nK5xNYtPGS3cvImUYl6B5ulIipUJSqpj8v2XstK0TZlOGxHToXaVDrCNmSfCA9vFYbTb6xJHB2JRAT3Jik/z6QgLjJLBWRnsucMDqldxoiEDAUHEhY3pjgZ89quR3c3hwAuTlI9hBn5I3e5VQR0Y/GxoS9mIkMF8pJmcGneqnE0BNIt91RN6Se5rDM69B+JWppBXtSir1JGuXADaRLLMP8VlxJX949iH0UYTKWKsrD4OgNNK5aUx24nAH494WPknBMlx4fCMIeWzy7K3sEZkNUn/+A+eHraqIFfbGpveSCNM+8EqEjMgA+YRgg3eig==; _naver_usersession_=Kkgzim/64JicPJzgkIIvqQ==; page_uid=jesTPsqVWUZssE4qJeossssssD0-011300; SRT5=1762662010; BUC=z5Fu3sAYtFwpbRDrrDFYdn4AgK5hNkOqX-DdaLU7VJM=" NAVER_COOKIES = "NAC=mQ7mBownbQf4A; NNB=TQPII6AKDBFGQ; PLACE_LANGUAGE=ko; NACT=1; nid_inf=1431570813; NID_AUT=k2T7FraXOdIMRCHzEZIFtHQup+I7b87M5fd7+p65AXZTdGB/gelRmW8s/Q4oDxm8; tooltipDisplayed=true; SRT30=1762660151; NID_SES=AAAB1Lpy3y3hGzuPbJpJl8vvFx18C+HXXuZEFou/YPgocHe7k2/5MpFlgE48X1JF7c7IPoU2khZKkkuLx+tsvWAzOf0TnG/G8RrBGeawnSluSJcKcTdKKRJ4cygKc/OabVxoc3TNZJWxer3vFtXBoXkDS5querVNS6wvcMhA/p4vkPKOeepwKLR+1IJERlQJWZw4q29IdAysrbBNn3Akf9mDA5eTYvMDLYyRkToRh10TVMW/yhyNQeMXlIdnR8U1ZCNqe/9ErYdos5gQDstswEJQQA0T2cHFGJOtmlYMPlnhWado5w521iZXGJyKcA9ZawizM/i5nK5xNYtPGS3cvImUYl6B5ulIipUJSqpj8v2XstK0TZlOGxHToXaVDrCNmSfCA9vFYbTb6xJHB2JRAT3Jik/z6QgLjJLBWRnsucMDqldxoiEDAUHEhY3pjgZ89quR3c3hwAuTlI9hBn5I3e5VQR0Y/GxoS9mIkMF8pJmcGneqnE0BNIt91RN6Se5rDM69B+JWppBXtSir1JGuXADaRLLMP8VlxJX949iH0UYTKWKsrD4OgNNK5aUx24nAH494WPknBMlx4fCMIeWzy7K3sEZkNUn/+A+eHraqIFfbGpveSCNM+8EqEjMgA+YRgg3eig==; _naver_usersession_=Kkgzim/64JicPJzgkIIvqQ==; page_uid=jesTPsqVWUZssE4qJeossssssD0-011300; SRT5=1762662010; BUC=z5Fu3sAYtFwpbRDrrDFYdn4AgK5hNkOqX-DdaLU7VJM="
OVERVIEW_QUERY = ''' OVERVIEW_QUERY = """
query getAccommodation($id: String!, $deviceType: String) { query getAccommodation($id: String!, $deviceType: String) {
business: placeDetail(input: {id: $id, isNx: true, deviceType: $deviceType}) { business: placeDetail(input: {id: $id, isNx: true, deviceType: $deviceType}) {
base { base {
@ -26,59 +25,52 @@ query getAccommodation($id: String!, $deviceType: String) {
images { images { origin url } } images { images { origin url } }
cpImages(source: [ugcImage]) { images { origin url } } cpImages(source: [ugcImage]) { images { origin url } }
} }
}''' }"""
REQUEST_HEADERS = { REQUEST_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Referer": "https://map.naver.com/", "Referer": "https://map.naver.com/",
"Origin": "https://map.naver.com", "Origin": "https://map.naver.com",
"Content-Type": "application/json", "Content-Type": "application/json",
"Cookie": NAVER_COOKIES "Cookie": NAVER_COOKIES,
} }
class GraphQLException(Exception): class GraphQLException(Exception):
pass pass
class nvMapScraper():
url : str = None
scrap_type : str = None
rawdata : dict = None
image_link_list : list[str] = None
base_info : dict = None
class nvMapScraper:
url: str = None
scrap_type: str = None
rawdata: dict = None
image_link_list: list[str] = None
base_info: dict = None
def __init__(self, url): def __init__(self, url):
self.url = url self.url = url
async def parse_url(self): def parse_url(self):
if 'place' not in self.url: place_pattern = r"/place/(\d+)"
if 'naver.me' in self.url:
async with aiohttp.ClientSession() as session:
async with session.get(self.url) as response:
self.url = str(response.url)
else:
raise GraphQLException("this shorten url not have place id")
try: try:
place_id = re.search(PLACE_PATTERN, self.url)[1] place_id = re.search(place_pattern, self.url)[1]
except Exception as E: except:
raise GraphQLException("Cannot find place id") raise GraphQLException()
return place_id return place_id
async def scrap(self): async def scrap(self):
try: try:
place_id = await self.parse_url() place_id = self.parse_url()
data = await self.call_get_accomodation(place_id) data = await self.call_get_accomodation(place_id)
self.rawdata = data self.rawdata = data
fac_data = await self.get_facility_string(place_id) self.image_link_list = [
self.rawdata['facilities'] = fac_data nv_image["origin"]
self.image_link_list = [nv_image['origin'] for nv_image in data['data']['business']['images']['images']] for nv_image in data["data"]["business"]["images"]["images"]
self.base_info = data['data']['business']['base'] ]
self.facility_info = fac_data self.base_info = data["data"]["business"]["base"]
self.scrap_type = "GraphQL" self.scrap_type = "GraphQL"
except GraphQLException as G: except GraphQLException:
print (G)
print("fallback") print("fallback")
self.scrap_type = "Playwright" self.scrap_type = "Playwright"
pass # 나중에 pw 이용한 crawling으로 fallback 추가 pass # 나중에 pw 이용한 crawling으로 fallback 추가
@ -87,33 +79,27 @@ class nvMapScraper():
async def call_get_accomodation(self, place_id): async def call_get_accomodation(self, place_id):
payload = { payload = {
"operationName" : "getAccommodation", "operationName": "getAccommodation",
"variables": { "id": place_id, "deviceType": "pc" }, "variables": {"id": place_id, "deviceType": "pc"},
"query": OVERVIEW_QUERY, "query": OVERVIEW_QUERY,
} }
json_payload = json.dumps(payload) json_payload = json.dumps(payload)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(GRAPHQL_URL, data=json_payload, headers=REQUEST_HEADERS) as response: async with session.post(
response.encoding = 'utf-8' GRAPHQL_URL, data=json_payload, headers=REQUEST_HEADERS
) as response:
response.encoding = "utf-8"
if response.status == 200: # 요청 성공 if response.status == 200: # 요청 성공
return await response.json() # await 주의 return await response.json() # await 주의
else: # 요청 실패 else: # 요청 실패
print('실패 상태 코드:', response.status) print("실패 상태 코드:", response.status)
print(response.text)
raise Exception() raise Exception()
async def get_facility_string(self, place_id):
url = f"https://pcmap.place.naver.com/accommodation/{place_id}/home"
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=REQUEST_HEADERS) as response:
soup = bs4.BeautifulSoup(await response.read(), 'html.parser')
c_elem = soup.find('span', 'place_blind', string='편의')
facilities = c_elem.parent.parent.find('div').string
return facilities
# url = "https://naver.me/IgJGCCic" url = "https://map.naver.com/p/search/%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84/place/1133638931?c=14.70,0,0,0,dh&placePath=/photo?businessCategory=pension&fromPanelNum=2&locale=ko&searchText=%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84&svcName=map_pcv5&timestamp=202512191123&fromPanelNum=2&locale=ko&searchText=%EC%8A%A4%ED%85%8C%EC%9D%B4%EB%A8%B8%EB%AD%84&svcName=map_pcv5&timestamp=202512191007&from=map&entry=bmp&filterType=%EC%97%85%EC%B2%B4&businessCategory=pension"
# scraper = nvMapScraper(url) scraper = nvMapScraper(url)
# asyncio.run(scraper.scrap()) asyncio.run(scraper.scrap())
# print(scraper.image_link_list) print(scraper.image_link_list)
# print(len(scraper.image_link_list)) print(len(scraper.image_link_list))
print(scraper.base_info)

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
from openai import OpenAI
from difflib import SequenceMatcher
from dataclasses import dataclass
from typing import List, Tuple
import aiohttp, json
@dataclass
class TimestampedLyric:
text: str
start: float
end: float
SUNO_BASE_URL="https://api.sunoapi.org"
SUNO_TIMESTAMP_ROUTE = "/api/v1/generate/get-timestamped-lyrics"
SUNO_DETAIL_ROUTE = "/api/v1/generate/record-info"
class LyricTimestampMapper:
suno_api_key : str
def __init__(self, suno_api_key):
self.suno_api_key = suno_api_key
async def get_suno_audio_id_from_task_id(self, suno_task_id): # expire if db save audio id
url = f"{SUNO_BASE_URL}{SUNO_DETAIL_ROUTE}"
headers = {"Authorization": f"Bearer {self.suno_api_key}"}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, params={"taskId" : suno_task_id}) as response:
detail = await response.json()
result = detail['data']['response']['sunoData'][0]['id']
return result
async def get_suno_timestamp(self, suno_task_id, suno_audio_id): # expire if db save audio id
url = f"{SUNO_BASE_URL}{SUNO_TIMESTAMP_ROUTE}"
headers = {"Authorization": f"Bearer {self.suno_api_key}"}
payload = {
"task_id" : suno_task_id,
"audio_id" : suno_audio_id
}
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
result = await response.json()
return result

View File

@ -1,19 +0,0 @@
from lyric_timestamp_mapper import LyricTimestampMapper
API_KEY = "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"
AUDIO_PATH = "test_audio.mp3"
GROUND_TRUTH_LYRICS = [
"첫 번째 가사 라인입니다",
"두 번째 가사 라인입니다",
"세 번째 가사 라인입니다",
]
mapper = LyricTimestampMapper(api_key=API_KEY)
result = mapper.map_ground_truth(AUDIO_PATH, GROUND_TRUTH_LYRICS)
for lyric in result:
if lyric.start >= 0:
print(f"[{lyric.start:.2f} - {lyric.end:.2f}] {lyric.text}")
else:
print(f"[매칭 실패] {lyric.text}")