유튜브 SEO 설명 추가 PoC
parent
f1dd675ecb
commit
b3354d4ad1
|
|
@ -292,7 +292,6 @@ async def generate_lyric(
|
|||
"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
|
||||
#logger.debug(f"[generate_lyric] Step 1 완료 - 프롬프트 {len(prompt)}자 ({step1_elapsed:.1f}ms)")
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
소셜 미디어 영상 업로드 관련 엔드포인트를 제공합니다.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import logging, json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Query
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
|
@ -22,12 +23,17 @@ from app.social.schemas import (
|
|||
SocialUploadRequest,
|
||||
SocialUploadResponse,
|
||||
SocialUploadStatusResponse,
|
||||
YoutubeDescriptionRequest,
|
||||
YoutubeDescriptionResponse,
|
||||
)
|
||||
from app.social.services import social_account_service
|
||||
from app.social.worker.upload_task import process_social_upload
|
||||
from app.user.dependencies import get_current_user
|
||||
from app.user.models import User
|
||||
from app.home.models import Project, MarketingIntel
|
||||
from app.video.models import Video
|
||||
from app.utils.prompts.prompts import yt_upload_prompt
|
||||
from app.utils.chatgpt_prompt import ChatgptService, ChatGPTResponseError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -403,16 +409,12 @@ async def cancel_upload(
|
|||
upload = result.scalar_one_or_none()
|
||||
|
||||
if not upload:
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="업로드 정보를 찾을 수 없습니다.",
|
||||
)
|
||||
|
||||
if upload.status != UploadStatus.PENDING.value:
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="대기 중인 업로드만 취소할 수 있습니다.",
|
||||
|
|
@ -425,3 +427,62 @@ async def cancel_upload(
|
|||
success=True,
|
||||
message="업로드가 취소되었습니다.",
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/autodescription",
|
||||
response_model=YoutubeDescriptionResponse,
|
||||
summary="유튜브 SEO descrption 생성",
|
||||
description="유튜브 업로드 시 사용할 descrption을 SEO 적용하여 생성",
|
||||
)
|
||||
async def youtube_seo_description(
|
||||
request_body: YoutubeDescriptionRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> YoutubeDescriptionResponse:
|
||||
|
||||
logger.info(
|
||||
f"[youtube_seo_description] START - user: {current_user.user_uuid} "
|
||||
)
|
||||
try:
|
||||
task_id = request_body.task_id
|
||||
project_query = await session.execute(
|
||||
select(Project)
|
||||
.where(
|
||||
Project.task_id == task_id,
|
||||
Project.user_uuid == current_user.user_uuid)
|
||||
.order_by(Project.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
project = project_query.scalar_one_or_none()
|
||||
marketing_query = await session.execute(
|
||||
select(MarketingIntel)
|
||||
.where(MarketingIntel.id == project.marketing_inteligence)
|
||||
)
|
||||
marketing_intelligence = marketing_query.scalar_one_or_none()
|
||||
|
||||
hashtags = marketing_intelligence.intel_result["target_keywords"]
|
||||
|
||||
yt_seo_input_data = {
|
||||
"customer_name" : project.store_name,
|
||||
"detail_region_info" : project.detail_region_info,
|
||||
"marketing_intelligence_summary" : json.dumps(marketing_intelligence.intel_result, ensure_ascii=False),
|
||||
"language" : project.language,
|
||||
"target_keywords" : hashtags
|
||||
}
|
||||
chatgpt = ChatgptService()
|
||||
yt_seo_output = await chatgpt.generate_structured_output(yt_upload_prompt, yt_seo_input_data)
|
||||
response = YoutubeDescriptionResponse(
|
||||
title=yt_seo_output.title,
|
||||
description=yt_seo_output.description,
|
||||
keywords = hashtags
|
||||
)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[youtube_seo_description] EXCEPTION - error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"유튜브 SEO 생성에 실패했습니다. : {str(e)}",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -276,6 +276,32 @@ class SocialUploadHistoryResponse(BaseModel):
|
|||
}
|
||||
)
|
||||
|
||||
class YoutubeDescriptionRequest(BaseModel):
|
||||
"""유튜브 SEO Description 제안 (자동완성) Request 모델"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"task_id" : "019c739f-65fc-7d15-8c88-b31be00e588e"
|
||||
}
|
||||
}
|
||||
)
|
||||
task_id: str = Field(..., description="작업 고유 식별자")
|
||||
|
||||
class YoutubeDescriptionResponse(BaseModel):
|
||||
"""유튜브 SEO Description 제안 (자동완성) Response 모델"""
|
||||
title:str = Field(..., description="유튜브 영상 제목 - SEO/AEO 최적화")
|
||||
description : str = Field(..., description="제안된 유튜브 SEO Description")
|
||||
keywords : list[str] = Field(..., description="해시태그 리스트")
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"title" : "여기에 더미 타이틀",
|
||||
"description": "여기에 더미 텍스트",
|
||||
"keywords": ["여기에", "더미", "해시태그"]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# 공통 응답 스키마
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class ChatgptService:
|
|||
self,
|
||||
prompt : Prompt,
|
||||
input_data : dict,
|
||||
) -> str:
|
||||
) -> BaseModel:
|
||||
prompt_text = prompt.build_prompt(input_data)
|
||||
|
||||
logger.debug(f"[ChatgptService] Generated Prompt (length: {len(prompt_text)})")
|
||||
|
|
|
|||
|
|
@ -52,6 +52,14 @@ lyric_prompt = Prompt(
|
|||
prompt_model = prompt_settings.LYRIC_PROMPT_MODEL
|
||||
)
|
||||
|
||||
yt_upload_prompt = Prompt(
|
||||
prompt_template_path=os.path.join(prompt_settings.PROMPT_FOLDER_ROOT, prompt_settings.YOUTUBE_PROMPT_FILE_NAME),
|
||||
prompt_input_class = YTUploadPromptInput,
|
||||
prompt_output_class = YTUploadPromptOutput,
|
||||
prompt_model = prompt_settings.YOUTUBE_PROMPT_MODEL
|
||||
)
|
||||
|
||||
def reload_all_prompt():
|
||||
marketing_prompt._reload_prompt()
|
||||
lyric_prompt._reload_prompt()
|
||||
yt_upload_prompt._reload_prompt()
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
from .lyric import LyricPromptInput, LyricPromptOutput
|
||||
from .marketing import MarketingPromptInput, MarketingPromptOutput
|
||||
from .youtube import YTUploadPromptInput, YTUploadPromptOutput
|
||||
|
|
@ -184,6 +184,8 @@ class PromptSettings(BaseSettings):
|
|||
LYRIC_PROMPT_FILE_NAME : str = Field(default="lyric_prompt.txt")
|
||||
LYRIC_PROMPT_MODEL : str = Field(default="gpt-5-mini")
|
||||
|
||||
YOUTUBE_PROMPT_FILE_NAME : str = Field(default="yt_upload_prompt.txt")
|
||||
YOUTUBE_PROMPT_MODEL : str = Field(default="gpt-5-mini")
|
||||
model_config = _base_config
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue