home, lyric, song, video에 대한 app 추가, 각 app별 모델 추가, admin 추가, debug 모드 추가, lifespan에 debug시 동작하는 것으로 업데이트
parent
6180c23151
commit
5f1b9da77e
|
|
@ -2,7 +2,7 @@ from fastapi import FastAPI
|
||||||
from sqladmin import Admin
|
from sqladmin import Admin
|
||||||
|
|
||||||
from app.database.session import engine
|
from app.database.session import engine
|
||||||
from app.lyrics.api.lyrics_admin import (
|
from app.lyric.api.lyrics_admin import (
|
||||||
LyricsAttributeAdmin,
|
LyricsAttributeAdmin,
|
||||||
LyricsPromptTemplateAdmin,
|
LyricsPromptTemplateAdmin,
|
||||||
LyricsSongResultsAllAdmin,
|
LyricsSongResultsAllAdmin,
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,14 @@ async def lifespan(app: FastAPI):
|
||||||
print("Starting up...")
|
print("Starting up...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pass
|
from config import prj_settings
|
||||||
# # 데이터베이스 테이블 생성
|
|
||||||
# from app.database.session import create_db_tables
|
|
||||||
|
|
||||||
# await create_db_tables()
|
# DEBUG 모드일 때만 데이터베이스 테이블 자동 생성
|
||||||
# print("Database tables ready")
|
if prj_settings.DEBUG:
|
||||||
|
from app.database.session import create_db_tables
|
||||||
|
|
||||||
|
await create_db_tables()
|
||||||
|
print("Database tables created (DEBUG mode)")
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
print("Database initialization timed out")
|
print("Database initialization timed out")
|
||||||
# 타임아웃 시 앱 시작 중단하려면 raise, 계속하려면 pass
|
# 타임아웃 시 앱 시작 중단하려면 raise, 계속하려면 pass
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ AsyncSessionLocal = async_sessionmaker(
|
||||||
|
|
||||||
async def create_db_tables():
|
async def create_db_tables():
|
||||||
async with engine.begin() as connection:
|
async with engine.begin() as connection:
|
||||||
from app.lyrics.models import ( # noqa: F401
|
from app.lyric.models import ( # noqa: F401
|
||||||
Attribute,
|
Attribute,
|
||||||
PromptTemplate,
|
PromptTemplate,
|
||||||
SongResultsAll,
|
SongResultsAll,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
from sqladmin import ModelView
|
||||||
|
|
||||||
|
from app.home.models import Image, Project
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectAdmin(ModelView, model=Project):
|
||||||
|
name = "프로젝트"
|
||||||
|
name_plural = "프로젝트 목록"
|
||||||
|
icon = "fa-solid fa-folder"
|
||||||
|
category = "프로젝트 관리"
|
||||||
|
page_size = 20
|
||||||
|
|
||||||
|
column_list = [
|
||||||
|
"id",
|
||||||
|
"store_name",
|
||||||
|
"region",
|
||||||
|
"task_id",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
column_details_list = [
|
||||||
|
"id",
|
||||||
|
"store_name",
|
||||||
|
"region",
|
||||||
|
"task_id",
|
||||||
|
"detail_region_info",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 폼(생성/수정)에서 제외
|
||||||
|
form_excluded_columns = ["created_at", "lyrics", "songs", "videos"]
|
||||||
|
|
||||||
|
column_searchable_list = [
|
||||||
|
Project.store_name,
|
||||||
|
Project.region,
|
||||||
|
Project.task_id,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_default_sort = (Project.created_at, True) # True: DESC (최신순)
|
||||||
|
|
||||||
|
column_sortable_list = [
|
||||||
|
Project.id,
|
||||||
|
Project.store_name,
|
||||||
|
Project.region,
|
||||||
|
Project.created_at,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_labels = {
|
||||||
|
"id": "ID",
|
||||||
|
"store_name": "가게명",
|
||||||
|
"region": "지역",
|
||||||
|
"task_id": "작업 ID",
|
||||||
|
"detail_region_info": "상세 지역 정보",
|
||||||
|
"created_at": "생성일시",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAdmin(ModelView, model=Image):
|
||||||
|
name = "이미지"
|
||||||
|
name_plural = "이미지 목록"
|
||||||
|
icon = "fa-solid fa-image"
|
||||||
|
category = "프로젝트 관리"
|
||||||
|
page_size = 20
|
||||||
|
|
||||||
|
column_list = [
|
||||||
|
"id",
|
||||||
|
"task_id",
|
||||||
|
"img_name",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
column_details_list = [
|
||||||
|
"id",
|
||||||
|
"task_id",
|
||||||
|
"img_name",
|
||||||
|
"img_url",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 폼(생성/수정)에서 제외
|
||||||
|
form_excluded_columns = ["created_at"]
|
||||||
|
|
||||||
|
column_searchable_list = [
|
||||||
|
Image.task_id,
|
||||||
|
Image.img_name,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_default_sort = (Image.created_at, True) # True: DESC (최신순)
|
||||||
|
|
||||||
|
column_sortable_list = [
|
||||||
|
Image.id,
|
||||||
|
Image.img_name,
|
||||||
|
Image.created_at,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_labels = {
|
||||||
|
"id": "ID",
|
||||||
|
"task_id": "작업 ID",
|
||||||
|
"img_name": "이미지명",
|
||||||
|
"img_url": "이미지 URL",
|
||||||
|
"created_at": "생성일시",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
"""
|
||||||
|
Home 모듈 SQLAlchemy 모델 정의
|
||||||
|
|
||||||
|
이 모듈은 영상 제작 파이프라인의 핵심 데이터 모델을 정의합니다.
|
||||||
|
- Project: 프로젝트(사용자 입력 이력) 관리
|
||||||
|
- Image: 업로드된 이미지 URL 관리
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
DateTime,
|
||||||
|
Index,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
func,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.database.session import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.lyric.models import Lyric
|
||||||
|
from app.song.models import Song
|
||||||
|
from app.video.models import Video
|
||||||
|
|
||||||
|
|
||||||
|
class Project(Base):
|
||||||
|
"""
|
||||||
|
프로젝트 테이블 (사용자 입력 이력)
|
||||||
|
|
||||||
|
영상 제작 요청의 시작점으로, 고객 정보와 지역 정보를 저장합니다.
|
||||||
|
하위 테이블(Lyric, Song, Video)의 부모 테이블 역할을 합니다.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: 고유 식별자 (자동 증가)
|
||||||
|
store_name: 고객명 (필수)
|
||||||
|
region: 지역명 (필수, 예: 서울, 부산, 대구 등)
|
||||||
|
task_id: 작업 고유 식별자 (UUID 형식, 36자)
|
||||||
|
detail_region_info: 상세 지역 정보 (선택, JSON 또는 텍스트 형식)
|
||||||
|
created_at: 생성 일시 (자동 설정)
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
lyrics: 생성된 가사 목록
|
||||||
|
songs: 생성된 노래 목록
|
||||||
|
videos: 최종 영상 결과 목록
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "project"
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_project_task_id", "task_id"),
|
||||||
|
Index("idx_project_store_name", "store_name"),
|
||||||
|
Index("idx_project_region", "region"),
|
||||||
|
{"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
comment="고유 식별자",
|
||||||
|
)
|
||||||
|
|
||||||
|
store_name: Mapped[str] = mapped_column(
|
||||||
|
String(255),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
comment="가게명",
|
||||||
|
)
|
||||||
|
|
||||||
|
region: Mapped[str] = mapped_column(
|
||||||
|
String(100),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
comment="지역명 (예: 군산)",
|
||||||
|
)
|
||||||
|
|
||||||
|
task_id: Mapped[str] = mapped_column(
|
||||||
|
String(36),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
comment="프로젝트 작업 고유 식별자 (UUID)",
|
||||||
|
)
|
||||||
|
|
||||||
|
detail_region_info: Mapped[Optional[str]] = mapped_column(
|
||||||
|
Text,
|
||||||
|
nullable=True,
|
||||||
|
comment="상세 지역 정보",
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime,
|
||||||
|
nullable=False,
|
||||||
|
server_default=func.now(),
|
||||||
|
comment="생성 일시",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
lyrics: Mapped[List["Lyric"]] = relationship(
|
||||||
|
"Lyric",
|
||||||
|
back_populates="project",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
songs: Mapped[List["Song"]] = relationship(
|
||||||
|
"Song",
|
||||||
|
back_populates="project",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
videos: Mapped[List["Video"]] = relationship(
|
||||||
|
"Video",
|
||||||
|
back_populates="project",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
def truncate(value: str | None, max_len: int = 10) -> str:
|
||||||
|
if value is None:
|
||||||
|
return "None"
|
||||||
|
return (value[:max_len] + "...") if len(value) > max_len else value
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"<Project("
|
||||||
|
f"id={self.id}, "
|
||||||
|
f"store_name='{self.store_name}', "
|
||||||
|
f"task_id='{truncate(self.task_id)}'"
|
||||||
|
f")>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Image(Base):
|
||||||
|
"""
|
||||||
|
업로드 이미지 테이블
|
||||||
|
|
||||||
|
사용자가 업로드한 이미지의 URL을 저장합니다.
|
||||||
|
독립적으로 관리되며 Project와 직접적인 관계가 없습니다.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: 고유 식별자 (자동 증가)
|
||||||
|
task_id: 이미지 업로드 작업 고유 식별자 (UUID)
|
||||||
|
img_name: 이미지명
|
||||||
|
img_url: 이미지 URL (S3, CDN 등의 경로)
|
||||||
|
created_at: 생성 일시 (자동 설정)
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "image"
|
||||||
|
__table_args__ = (
|
||||||
|
{"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
comment="고유 식별자",
|
||||||
|
)
|
||||||
|
|
||||||
|
task_id: Mapped[str] = mapped_column(
|
||||||
|
String(36),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
comment="이미지 업로드 작업 고유 식별자 (UUID)",
|
||||||
|
)
|
||||||
|
|
||||||
|
img_name: Mapped[str] = mapped_column(
|
||||||
|
String(255),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
comment="이미지명",
|
||||||
|
)
|
||||||
|
|
||||||
|
img_url: Mapped[str] = mapped_column(
|
||||||
|
String(2048),
|
||||||
|
nullable=False,
|
||||||
|
comment="이미지 URL (blob, CDN 경로)",
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime,
|
||||||
|
nullable=False,
|
||||||
|
server_default=func.now(),
|
||||||
|
comment="생성 일시",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
task_id_str = (self.task_id[:10] + "...") if len(self.task_id) > 10 else self.task_id
|
||||||
|
img_name_str = (self.img_name[:10] + "...") if len(self.img_name) > 10 else self.img_name
|
||||||
|
|
||||||
|
return f"<Image(id={self.id}, task_id='{task_id_str}', img_name='{img_name_str}')>"
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
from sqladmin import ModelView
|
||||||
|
|
||||||
|
from app.lyric.models import Lyric
|
||||||
|
|
||||||
|
|
||||||
|
class LyricAdmin(ModelView, model=Lyric):
|
||||||
|
name = "가사"
|
||||||
|
name_plural = "가사 목록"
|
||||||
|
icon = "fa-solid fa-music"
|
||||||
|
category = "가사 관리"
|
||||||
|
page_size = 20
|
||||||
|
|
||||||
|
column_list = [
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"task_id",
|
||||||
|
"status",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
column_details_list = [
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"task_id",
|
||||||
|
"status",
|
||||||
|
"lyric_prompt",
|
||||||
|
"lyric_result",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 폼(생성/수정)에서 제외
|
||||||
|
form_excluded_columns = ["created_at", "songs", "videos"]
|
||||||
|
|
||||||
|
column_searchable_list = [
|
||||||
|
Lyric.task_id,
|
||||||
|
Lyric.status,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_default_sort = (Lyric.created_at, True) # True: DESC (최신순)
|
||||||
|
|
||||||
|
column_sortable_list = [
|
||||||
|
Lyric.id,
|
||||||
|
Lyric.project_id,
|
||||||
|
Lyric.status,
|
||||||
|
Lyric.created_at,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_labels = {
|
||||||
|
"id": "ID",
|
||||||
|
"project_id": "프로젝트 ID",
|
||||||
|
"task_id": "작업 ID",
|
||||||
|
"status": "상태",
|
||||||
|
"lyric_prompt": "프롬프트",
|
||||||
|
"lyric_result": "생성 결과",
|
||||||
|
"created_at": "생성일시",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Request # , UploadFile, File, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.database.session import get_session
|
||||||
|
from app.lyric.services import lyrics
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/lyrics", tags=["lyrics"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/store")
|
||||||
|
async def home(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
# store_info_list: List[StoreData] = await lyrics_svc.get_store_info(conn)
|
||||||
|
result: Any = await lyrics.get_store_info(conn)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="store.html",
|
||||||
|
# context={"store_info_list": result},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/attributes")
|
||||||
|
async def attribute(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("attributes")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.get_attribute(conn)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="attribute.html",
|
||||||
|
# context={
|
||||||
|
# "attribute_group_dict": result,
|
||||||
|
# "before_dict": await request.form(),
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/fewshot")
|
||||||
|
async def sample_song(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("fewshot")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.get_sample_song(conn)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="fewshot.html",
|
||||||
|
# context={"fewshot_list": result, "before_dict": await request.form()},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/prompt")
|
||||||
|
async def prompt_template(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("prompt_template")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.get_prompt_template(conn)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
print("prompt_template after")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="prompt.html",
|
||||||
|
# context={"prompt_list": result, "before_dict": await request.form()},
|
||||||
|
# )
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/result")
|
||||||
|
async def song_result(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("song_result")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.make_song_result(request, conn)
|
||||||
|
print("result : ", result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="result.html",
|
||||||
|
# context={"result_dict": result},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/result")
|
||||||
|
async def get_song_result(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("get_song_result")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.get_song_result(conn)
|
||||||
|
print("result : ", result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="result.html",
|
||||||
|
# context={"result_dict": result},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/automation")
|
||||||
|
async def automation(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("automation")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.make_automation(request, conn)
|
||||||
|
print("result : ", result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="result.html",
|
||||||
|
# context={"result_dict": result},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
DateTime,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
func,
|
||||||
|
)
|
||||||
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.database.session import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.home.models import Project
|
||||||
|
from app.song.models import Song
|
||||||
|
from app.video.models import Video
|
||||||
|
|
||||||
|
|
||||||
|
class Lyric(Base):
|
||||||
|
"""
|
||||||
|
가사 테이블
|
||||||
|
|
||||||
|
AI를 통해 생성된 가사 정보를 저장합니다.
|
||||||
|
프롬프트와 생성 결과, 처리 상태를 관리합니다.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: 고유 식별자 (자동 증가)
|
||||||
|
project_id: 연결된 Project의 id (외래키)
|
||||||
|
task_id: 가사 생성 작업의 고유 식별자 (UUID 형식)
|
||||||
|
status: 처리 상태 (pending, processing, completed, failed 등)
|
||||||
|
lyric_prompt: 가사 생성에 사용된 프롬프트
|
||||||
|
lyric_result: 생성된 가사 결과 (LONGTEXT로 긴 가사 지원)
|
||||||
|
created_at: 생성 일시 (자동 설정)
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
project: 연결된 Project
|
||||||
|
songs: 이 가사를 사용한 노래 목록
|
||||||
|
videos: 이 가사를 사용한 영상 목록
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "lyric"
|
||||||
|
__table_args__ = (
|
||||||
|
{"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
comment="고유 식별자",
|
||||||
|
)
|
||||||
|
|
||||||
|
project_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("project.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
comment="연결된 Project의 id",
|
||||||
|
)
|
||||||
|
|
||||||
|
task_id: Mapped[str] = mapped_column(
|
||||||
|
String(36),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
comment="가사 생성 작업 고유 식별자 (UUID)",
|
||||||
|
)
|
||||||
|
|
||||||
|
status: Mapped[str] = mapped_column(
|
||||||
|
String(50),
|
||||||
|
nullable=False,
|
||||||
|
comment="처리 상태 (processing, completed, failed)",
|
||||||
|
)
|
||||||
|
|
||||||
|
lyric_prompt: Mapped[str] = mapped_column(
|
||||||
|
Text,
|
||||||
|
nullable=False,
|
||||||
|
comment="가사 생성에 사용된 프롬프트",
|
||||||
|
)
|
||||||
|
|
||||||
|
lyric_result: Mapped[str] = mapped_column(
|
||||||
|
LONGTEXT,
|
||||||
|
nullable=True,
|
||||||
|
comment="생성된 가사 결과",
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime,
|
||||||
|
nullable=True,
|
||||||
|
server_default=func.now(),
|
||||||
|
comment="생성 일시",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
project: Mapped["Project"] = relationship(
|
||||||
|
"Project",
|
||||||
|
back_populates="lyrics",
|
||||||
|
)
|
||||||
|
|
||||||
|
songs: Mapped[List["Song"]] = relationship(
|
||||||
|
"Song",
|
||||||
|
back_populates="lyric",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
videos: Mapped[List["Video"]] = relationship(
|
||||||
|
"Video",
|
||||||
|
back_populates="lyric",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
def truncate(value: str | None, max_len: int = 10) -> str:
|
||||||
|
if value is None:
|
||||||
|
return "None"
|
||||||
|
return (value[:max_len] + "...") if len(value) > max_len else value
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"<Lyric("
|
||||||
|
f"id={self.id}, "
|
||||||
|
f"task_id='{truncate(self.task_id)}', "
|
||||||
|
f"status='{self.status}'"
|
||||||
|
f")>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,852 @@
|
||||||
|
import random
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi import Request, status
|
||||||
|
from fastapi.exceptions import HTTPException
|
||||||
|
from sqlalchemy import Connection, text
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from app.lyric.schemas.lyrics_schema import (
|
||||||
|
AttributeData,
|
||||||
|
PromptTemplateData,
|
||||||
|
SongFormData,
|
||||||
|
SongSampleData,
|
||||||
|
StoreData,
|
||||||
|
)
|
||||||
|
from app.utils.chatgpt_prompt import chatgpt_api
|
||||||
|
|
||||||
|
|
||||||
|
async def get_store_info(conn: Connection) -> List[StoreData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM store_default_info;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_store_info = [
|
||||||
|
StoreData(
|
||||||
|
id=row[0],
|
||||||
|
store_info=row[1],
|
||||||
|
store_name=row[2],
|
||||||
|
store_category=row[3],
|
||||||
|
store_region=row[4],
|
||||||
|
store_address=row[5],
|
||||||
|
store_phone_number=row[6],
|
||||||
|
created_at=row[7],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_store_info
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_attribute(conn: Connection) -> List[AttributeData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM attribute;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_attribute = [
|
||||||
|
AttributeData(
|
||||||
|
id=row[0],
|
||||||
|
attr_category=row[1],
|
||||||
|
attr_value=row[2],
|
||||||
|
created_at=row[3],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_attribute
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_attribute(conn: Connection) -> List[AttributeData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM attribute;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_attribute = [
|
||||||
|
AttributeData(
|
||||||
|
id=row[0],
|
||||||
|
attr_category=row[1],
|
||||||
|
attr_value=row[2],
|
||||||
|
created_at=row[3],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_attribute
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_sample_song(conn: Connection) -> List[SongSampleData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM song_sample;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_sample_song = [
|
||||||
|
SongSampleData(
|
||||||
|
id=row[0],
|
||||||
|
ai=row[1],
|
||||||
|
ai_model=row[2],
|
||||||
|
genre=row[3],
|
||||||
|
sample_song=row[4],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_sample_song
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_prompt_template(conn: Connection) -> List[PromptTemplateData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM prompt_template;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_prompt_template = [
|
||||||
|
PromptTemplateData(
|
||||||
|
id=row[0],
|
||||||
|
description=row[1],
|
||||||
|
prompt=row[2],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_prompt_template
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_song_result(conn: Connection) -> List[PromptTemplateData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM prompt_template;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_prompt_template = [
|
||||||
|
PromptTemplateData(
|
||||||
|
id=row[0],
|
||||||
|
description=row[1],
|
||||||
|
prompt=row[2],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_prompt_template
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def make_song_result(request: Request, conn: Connection):
|
||||||
|
try:
|
||||||
|
# 1. Form 데이터 파싱
|
||||||
|
form_data = await SongFormData.from_form(request)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"Store ID: {form_data.store_id}")
|
||||||
|
print(f"Lyrics IDs: {form_data.lyrics_ids}")
|
||||||
|
print(f"Prompt IDs: {form_data.prompts}")
|
||||||
|
print(f"{'=' * 60}\n")
|
||||||
|
|
||||||
|
# 2. Store 정보 조회
|
||||||
|
store_query = """
|
||||||
|
SELECT * FROM store_default_info WHERE id=:id;
|
||||||
|
"""
|
||||||
|
store_result = await conn.execute(text(store_query), {"id": form_data.store_id})
|
||||||
|
|
||||||
|
all_store_info = [
|
||||||
|
StoreData(
|
||||||
|
id=row[0],
|
||||||
|
store_info=row[1],
|
||||||
|
store_name=row[2],
|
||||||
|
store_category=row[3],
|
||||||
|
store_region=row[4],
|
||||||
|
store_address=row[5],
|
||||||
|
store_phone_number=row[6],
|
||||||
|
created_at=row[7],
|
||||||
|
)
|
||||||
|
for row in store_result
|
||||||
|
]
|
||||||
|
|
||||||
|
if not all_store_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Store not found: {form_data.store_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
store_info = all_store_info[0]
|
||||||
|
print(f"Store: {store_info.store_name}")
|
||||||
|
|
||||||
|
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||||
|
|
||||||
|
# 4. Sample Song 조회 및 결합
|
||||||
|
combined_sample_song = None
|
||||||
|
|
||||||
|
if form_data.lyrics_ids:
|
||||||
|
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||||
|
|
||||||
|
lyrics_query = """
|
||||||
|
SELECT sample_song FROM song_sample
|
||||||
|
WHERE id IN :ids
|
||||||
|
ORDER BY created_at;
|
||||||
|
"""
|
||||||
|
lyrics_result = await conn.execute(
|
||||||
|
text(lyrics_query), {"ids": tuple(form_data.lyrics_ids)}
|
||||||
|
)
|
||||||
|
|
||||||
|
sample_songs = [
|
||||||
|
row.sample_song for row in lyrics_result.fetchall() if row.sample_song
|
||||||
|
]
|
||||||
|
|
||||||
|
if sample_songs:
|
||||||
|
combined_sample_song = "\n\n".join(
|
||||||
|
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||||
|
)
|
||||||
|
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||||
|
else:
|
||||||
|
print("샘플 가사가 비어있습니다")
|
||||||
|
else:
|
||||||
|
print("선택된 lyrics가 없습니다")
|
||||||
|
|
||||||
|
# 5. 템플릿 가져오기
|
||||||
|
if not form_data.prompts:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="프롬프트 ID가 필요합니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("템플릿 가져오기")
|
||||||
|
|
||||||
|
prompts_query = """
|
||||||
|
SELECT * FROM prompt_template WHERE id=:id;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ✅ 수정: store_query → prompts_query
|
||||||
|
prompts_result = await conn.execute(
|
||||||
|
text(prompts_query), {"id": form_data.prompts}
|
||||||
|
)
|
||||||
|
|
||||||
|
prompts_info = [
|
||||||
|
PromptTemplateData(
|
||||||
|
id=row[0],
|
||||||
|
description=row[1],
|
||||||
|
prompt=row[2],
|
||||||
|
)
|
||||||
|
for row in prompts_result
|
||||||
|
]
|
||||||
|
|
||||||
|
if not prompts_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Prompt not found: {form_data.prompts}",
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = prompts_info[0]
|
||||||
|
print(f"Prompt Template: {prompt.prompt}")
|
||||||
|
|
||||||
|
# ✅ 6. 프롬프트 조합
|
||||||
|
updated_prompt = prompt.prompt.replace("###", form_data.attributes_str).format(
|
||||||
|
name=store_info.store_name or "",
|
||||||
|
address=store_info.store_address or "",
|
||||||
|
category=store_info.store_category or "",
|
||||||
|
description=store_info.store_info or "",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_prompt += f"""
|
||||||
|
|
||||||
|
다음은 참고해야 하는 샘플 가사 정보입니다.
|
||||||
|
|
||||||
|
샘플 가사를 참고하여 작곡을 해주세요.
|
||||||
|
|
||||||
|
{combined_sample_song}
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n")
|
||||||
|
|
||||||
|
# 7. 모델에게 요청
|
||||||
|
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt)
|
||||||
|
|
||||||
|
# 글자 수 계산
|
||||||
|
total_chars_with_space = len(generated_lyrics)
|
||||||
|
total_chars_without_space = len(
|
||||||
|
generated_lyrics.replace(" ", "")
|
||||||
|
.replace("\n", "")
|
||||||
|
.replace("\r", "")
|
||||||
|
.replace("\t", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
# final_lyrics 생성
|
||||||
|
final_lyrics = f"""속성 {form_data.attributes_str}
|
||||||
|
전체 글자 수 (공백 포함): {total_chars_with_space}자
|
||||||
|
전체 글자 수 (공백 제외): {total_chars_without_space}자\r\n\r\n{generated_lyrics}"""
|
||||||
|
|
||||||
|
print("=" * 40)
|
||||||
|
print("[translate:form_data.attributes_str:] ", form_data.attributes_str)
|
||||||
|
print("[translate:total_chars_with_space:] ", total_chars_with_space)
|
||||||
|
print("[translate:total_chars_without_space:] ", total_chars_without_space)
|
||||||
|
print("[translate:final_lyrics:]")
|
||||||
|
print(final_lyrics)
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
# 8. DB 저장
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO song_results_all (
|
||||||
|
store_info, store_name, store_category, store_address, store_phone_number,
|
||||||
|
description, prompt, attr_category, attr_value,
|
||||||
|
ai, ai_model, genre,
|
||||||
|
sample_song, result_song, created_at
|
||||||
|
) VALUES (
|
||||||
|
:store_info, :store_name, :store_category, :store_address, :store_phone_number,
|
||||||
|
:description, :prompt, :attr_category, :attr_value,
|
||||||
|
:ai, :ai_model, :genre,
|
||||||
|
:sample_song, :result_song, NOW()
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ✅ attr_category, attr_value 추가
|
||||||
|
insert_params = {
|
||||||
|
"store_info": store_info.store_info or "",
|
||||||
|
"store_name": store_info.store_name,
|
||||||
|
"store_category": store_info.store_category or "",
|
||||||
|
"store_address": store_info.store_address or "",
|
||||||
|
"store_phone_number": store_info.store_phone_number or "",
|
||||||
|
"description": store_info.store_info or "",
|
||||||
|
"prompt": form_data.prompts,
|
||||||
|
"attr_category": ", ".join(form_data.attributes.keys())
|
||||||
|
if form_data.attributes
|
||||||
|
else "",
|
||||||
|
"attr_value": ", ".join(form_data.attributes.values())
|
||||||
|
if form_data.attributes
|
||||||
|
else "",
|
||||||
|
"ai": "ChatGPT",
|
||||||
|
"ai_model": form_data.llm_model,
|
||||||
|
"genre": "후크송",
|
||||||
|
"sample_song": combined_sample_song or "없음",
|
||||||
|
"result_song": final_lyrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.execute(text(insert_query), insert_params)
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
|
print("결과 저장 완료")
|
||||||
|
|
||||||
|
print("\n전체 결과 조회 중...")
|
||||||
|
|
||||||
|
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||||
|
select_query = """
|
||||||
|
SELECT * FROM song_results_all
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
all_results = await conn.execute(text(select_query))
|
||||||
|
|
||||||
|
results_list = [
|
||||||
|
{
|
||||||
|
"id": row.id,
|
||||||
|
"store_info": row.store_info,
|
||||||
|
"store_name": row.store_name,
|
||||||
|
"store_category": row.store_category,
|
||||||
|
"store_address": row.store_address,
|
||||||
|
"store_phone_number": row.store_phone_number,
|
||||||
|
"description": row.description,
|
||||||
|
"prompt": row.prompt,
|
||||||
|
"attr_category": row.attr_category,
|
||||||
|
"attr_value": row.attr_value,
|
||||||
|
"ai": row.ai,
|
||||||
|
"ai_model": row.ai_model,
|
||||||
|
"genre": row.genre,
|
||||||
|
"sample_song": row.sample_song,
|
||||||
|
"result_song": row.result_song,
|
||||||
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||||
|
}
|
||||||
|
for row in all_results.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||||
|
|
||||||
|
return results_list
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Database Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_song_result(conn: Connection): # 반환 타입 수정
|
||||||
|
try:
|
||||||
|
select_query = """
|
||||||
|
SELECT * FROM song_results_all
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
all_results = await conn.execute(text(select_query))
|
||||||
|
|
||||||
|
results_list = [
|
||||||
|
{
|
||||||
|
"id": row.id,
|
||||||
|
"store_info": row.store_info,
|
||||||
|
"store_name": row.store_name,
|
||||||
|
"store_category": row.store_category,
|
||||||
|
"store_address": row.store_address,
|
||||||
|
"store_phone_number": row.store_phone_number,
|
||||||
|
"description": row.description,
|
||||||
|
"prompt": row.prompt,
|
||||||
|
"attr_category": row.attr_category,
|
||||||
|
"attr_value": row.attr_value,
|
||||||
|
"ai": row.ai,
|
||||||
|
"ai_model": row.ai_model,
|
||||||
|
"season": row.season,
|
||||||
|
"num_of_people": row.num_of_people,
|
||||||
|
"people_category": row.people_category,
|
||||||
|
"genre": row.genre,
|
||||||
|
"sample_song": row.sample_song,
|
||||||
|
"result_song": row.result_song,
|
||||||
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||||
|
}
|
||||||
|
for row in all_results.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||||
|
|
||||||
|
return results_list
|
||||||
|
except HTTPException: # HTTPException은 그대로 raise
|
||||||
|
raise
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Database Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def make_automation(request: Request, conn: Connection):
|
||||||
|
try:
|
||||||
|
# 1. Form 데이터 파싱
|
||||||
|
form_data = await SongFormData.from_form(request)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"Store ID: {form_data.store_id}")
|
||||||
|
print(f"{'=' * 60}\n")
|
||||||
|
|
||||||
|
# 2. Store 정보 조회
|
||||||
|
store_query = """
|
||||||
|
SELECT * FROM store_default_info WHERE id=:id;
|
||||||
|
"""
|
||||||
|
store_result = await conn.execute(text(store_query), {"id": form_data.store_id})
|
||||||
|
|
||||||
|
all_store_info = [
|
||||||
|
StoreData(
|
||||||
|
id=row[0],
|
||||||
|
store_info=row[1],
|
||||||
|
store_name=row[2],
|
||||||
|
store_category=row[3],
|
||||||
|
store_region=row[4],
|
||||||
|
store_address=row[5],
|
||||||
|
store_phone_number=row[6],
|
||||||
|
created_at=row[7],
|
||||||
|
)
|
||||||
|
for row in store_result
|
||||||
|
]
|
||||||
|
|
||||||
|
if not all_store_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Store not found: {form_data.store_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
store_info = all_store_info[0]
|
||||||
|
print(f"Store: {store_info.store_name}")
|
||||||
|
|
||||||
|
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||||
|
attribute_query = """
|
||||||
|
SELECT * FROM attribute;
|
||||||
|
"""
|
||||||
|
|
||||||
|
attribute_results = await conn.execute(text(attribute_query))
|
||||||
|
|
||||||
|
# 결과 가져오기
|
||||||
|
attribute_rows = attribute_results.fetchall()
|
||||||
|
|
||||||
|
formatted_attributes = ""
|
||||||
|
selected_categories = []
|
||||||
|
selected_values = []
|
||||||
|
|
||||||
|
if attribute_rows:
|
||||||
|
attribute_list = [
|
||||||
|
AttributeData(
|
||||||
|
id=row[0],
|
||||||
|
attr_category=row[1],
|
||||||
|
attr_value=row[2],
|
||||||
|
created_at=row[3],
|
||||||
|
)
|
||||||
|
for row in attribute_rows
|
||||||
|
]
|
||||||
|
|
||||||
|
# ✅ 각 category에서 하나의 value만 랜덤 선택
|
||||||
|
formatted_pairs = []
|
||||||
|
for attr in attribute_list:
|
||||||
|
# 쉼표로 분리 및 공백 제거
|
||||||
|
values = [v.strip() for v in attr.attr_value.split(",") if v.strip()]
|
||||||
|
|
||||||
|
if values:
|
||||||
|
# 랜덤하게 하나만 선택
|
||||||
|
selected_value = random.choice(values)
|
||||||
|
formatted_pairs.append(f"{attr.attr_category} : {selected_value}")
|
||||||
|
|
||||||
|
# ✅ 선택된 category와 value 저장
|
||||||
|
selected_categories.append(attr.attr_category)
|
||||||
|
selected_values.append(selected_value)
|
||||||
|
|
||||||
|
# 최종 문자열 생성
|
||||||
|
formatted_attributes = "\n".join(formatted_pairs)
|
||||||
|
|
||||||
|
print(f"\n[포맷팅된 문자열 속성 정보]\n{formatted_attributes}\n")
|
||||||
|
else:
|
||||||
|
print("속성 데이터가 없습니다")
|
||||||
|
formatted_attributes = ""
|
||||||
|
|
||||||
|
# 4. 템플릿 가져오기
|
||||||
|
print("템플릿 가져오기 (ID=1)")
|
||||||
|
|
||||||
|
prompts_query = """
|
||||||
|
SELECT * FROM prompt_template WHERE id=1;
|
||||||
|
"""
|
||||||
|
|
||||||
|
prompts_result = await conn.execute(text(prompts_query))
|
||||||
|
|
||||||
|
row = prompts_result.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Prompt ID 1 not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = PromptTemplateData(
|
||||||
|
id=row[0],
|
||||||
|
description=row[1],
|
||||||
|
prompt=row[2],
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Prompt Template: {prompt.prompt}")
|
||||||
|
|
||||||
|
# 5. 템플릿 조합
|
||||||
|
|
||||||
|
updated_prompt = prompt.prompt.replace("###", formatted_attributes).format(
|
||||||
|
name=store_info.store_name or "",
|
||||||
|
address=store_info.store_address or "",
|
||||||
|
category=store_info.store_category or "",
|
||||||
|
description=store_info.store_info or "",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("업데이트된 프롬프트")
|
||||||
|
print("=" * 80)
|
||||||
|
print(updated_prompt)
|
||||||
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
# 4. Sample Song 조회 및 결합
|
||||||
|
combined_sample_song = None
|
||||||
|
|
||||||
|
if form_data.lyrics_ids:
|
||||||
|
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||||
|
|
||||||
|
lyrics_query = """
|
||||||
|
SELECT sample_song FROM song_sample
|
||||||
|
WHERE id IN :ids
|
||||||
|
ORDER BY created_at;
|
||||||
|
"""
|
||||||
|
lyrics_result = await conn.execute(
|
||||||
|
text(lyrics_query), {"ids": tuple(form_data.lyrics_ids)}
|
||||||
|
)
|
||||||
|
|
||||||
|
sample_songs = [
|
||||||
|
row.sample_song for row in lyrics_result.fetchall() if row.sample_song
|
||||||
|
]
|
||||||
|
|
||||||
|
if sample_songs:
|
||||||
|
combined_sample_song = "\n\n".join(
|
||||||
|
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||||
|
)
|
||||||
|
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||||
|
else:
|
||||||
|
print("샘플 가사가 비어있습니다")
|
||||||
|
else:
|
||||||
|
print("선택된 lyrics가 없습니다")
|
||||||
|
|
||||||
|
# 1. song_sample 테이블의 모든 ID 조회
|
||||||
|
print("\n[샘플 가사 랜덤 선택]")
|
||||||
|
|
||||||
|
all_ids_query = """
|
||||||
|
SELECT id FROM song_sample;
|
||||||
|
"""
|
||||||
|
ids_result = await conn.execute(text(all_ids_query))
|
||||||
|
all_ids = [row.id for row in ids_result.fetchall()]
|
||||||
|
|
||||||
|
print(f"전체 샘플 가사 개수: {len(all_ids)}개")
|
||||||
|
|
||||||
|
# 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체)
|
||||||
|
combined_sample_song = None
|
||||||
|
|
||||||
|
if all_ids:
|
||||||
|
# 3개 또는 전체 개수 중 작은 값 선택
|
||||||
|
sample_count = min(3, len(all_ids))
|
||||||
|
selected_ids = random.sample(all_ids, sample_count)
|
||||||
|
|
||||||
|
print(f"랜덤 선택된 ID: {selected_ids}")
|
||||||
|
|
||||||
|
# 3. 선택된 ID로 샘플 가사 조회
|
||||||
|
lyrics_query = """
|
||||||
|
SELECT sample_song FROM song_sample
|
||||||
|
WHERE id IN :ids
|
||||||
|
ORDER BY created_at;
|
||||||
|
"""
|
||||||
|
lyrics_result = await conn.execute(
|
||||||
|
text(lyrics_query), {"ids": tuple(selected_ids)}
|
||||||
|
)
|
||||||
|
|
||||||
|
sample_songs = [
|
||||||
|
row.sample_song for row in lyrics_result.fetchall() if row.sample_song
|
||||||
|
]
|
||||||
|
|
||||||
|
# 4. combined_sample_song 생성
|
||||||
|
if sample_songs:
|
||||||
|
combined_sample_song = "\n\n".join(
|
||||||
|
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||||
|
)
|
||||||
|
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||||
|
else:
|
||||||
|
print("샘플 가사가 비어있습니다")
|
||||||
|
else:
|
||||||
|
print("song_sample 테이블에 데이터가 없습니다")
|
||||||
|
|
||||||
|
# 5. 프롬프트에 샘플 가사 추가
|
||||||
|
if combined_sample_song:
|
||||||
|
updated_prompt += f"""
|
||||||
|
|
||||||
|
다음은 참고해야 하는 샘플 가사 정보입니다.
|
||||||
|
|
||||||
|
샘플 가사를 참고하여 작곡을 해주세요.
|
||||||
|
|
||||||
|
{combined_sample_song}
|
||||||
|
"""
|
||||||
|
print("샘플 가사 정보가 프롬프트에 추가되었습니다")
|
||||||
|
else:
|
||||||
|
print("샘플 가사가 없어 기본 프롬프트만 사용합니다")
|
||||||
|
|
||||||
|
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n")
|
||||||
|
|
||||||
|
# 7. 모델에게 요청
|
||||||
|
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt)
|
||||||
|
|
||||||
|
# 글자 수 계산
|
||||||
|
total_chars_with_space = len(generated_lyrics)
|
||||||
|
total_chars_without_space = len(
|
||||||
|
generated_lyrics.replace(" ", "")
|
||||||
|
.replace("\n", "")
|
||||||
|
.replace("\r", "")
|
||||||
|
.replace("\t", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
# final_lyrics 생성
|
||||||
|
final_lyrics = f"""속성 {formatted_attributes}
|
||||||
|
전체 글자 수 (공백 포함): {total_chars_with_space}자
|
||||||
|
전체 글자 수 (공백 제외): {total_chars_without_space}자\r\n\r\n{generated_lyrics}"""
|
||||||
|
|
||||||
|
# 8. DB 저장
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO song_results_all (
|
||||||
|
store_info, store_name, store_category, store_address, store_phone_number,
|
||||||
|
description, prompt, attr_category, attr_value,
|
||||||
|
ai, ai_model, genre,
|
||||||
|
sample_song, result_song, created_at
|
||||||
|
) VALUES (
|
||||||
|
:store_info, :store_name, :store_category, :store_address, :store_phone_number,
|
||||||
|
:description, :prompt, :attr_category, :attr_value,
|
||||||
|
:ai, :ai_model, :genre,
|
||||||
|
:sample_song, :result_song, NOW()
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
print("\n[insert_params 선택된 속성 확인]")
|
||||||
|
print(f"Categories: {selected_categories}")
|
||||||
|
print(f"Values: {selected_values}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# attr_category, attr_value
|
||||||
|
insert_params = {
|
||||||
|
"store_info": store_info.store_info or "",
|
||||||
|
"store_name": store_info.store_name,
|
||||||
|
"store_category": store_info.store_category or "",
|
||||||
|
"store_address": store_info.store_address or "",
|
||||||
|
"store_phone_number": store_info.store_phone_number or "",
|
||||||
|
"description": store_info.store_info or "",
|
||||||
|
"prompt": prompt.id,
|
||||||
|
# 랜덤 선택된 category와 value 사용
|
||||||
|
"attr_category": ", ".join(selected_categories)
|
||||||
|
if selected_categories
|
||||||
|
else "",
|
||||||
|
"attr_value": ", ".join(selected_values) if selected_values else "",
|
||||||
|
"ai": "ChatGPT",
|
||||||
|
"ai_model": "gpt-4o",
|
||||||
|
"genre": "후크송",
|
||||||
|
"sample_song": combined_sample_song or "없음",
|
||||||
|
"result_song": final_lyrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.execute(text(insert_query), insert_params)
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
|
print("결과 저장 완료")
|
||||||
|
|
||||||
|
print("\n전체 결과 조회 중...")
|
||||||
|
|
||||||
|
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||||
|
select_query = """
|
||||||
|
SELECT * FROM song_results_all
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
all_results = await conn.execute(text(select_query))
|
||||||
|
|
||||||
|
results_list = [
|
||||||
|
{
|
||||||
|
"id": row.id,
|
||||||
|
"store_info": row.store_info,
|
||||||
|
"store_name": row.store_name,
|
||||||
|
"store_category": row.store_category,
|
||||||
|
"store_address": row.store_address,
|
||||||
|
"store_phone_number": row.store_phone_number,
|
||||||
|
"description": row.description,
|
||||||
|
"prompt": row.prompt,
|
||||||
|
"attr_category": row.attr_category,
|
||||||
|
"attr_value": row.attr_value,
|
||||||
|
"ai": row.ai,
|
||||||
|
"ai_model": row.ai_model,
|
||||||
|
"genre": row.genre,
|
||||||
|
"sample_song": row.sample_song,
|
||||||
|
"result_song": row.result_song,
|
||||||
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||||
|
}
|
||||||
|
for row in all_results.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||||
|
|
||||||
|
return results_list
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Database Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||||
|
)
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
from sqladmin import ModelView
|
|
||||||
|
|
||||||
from app.lyrics.models import PromptTemplate # noqa: F401
|
|
||||||
from app.lyrics.models import Attribute, SongResultsAll, SongSample, StoreDefaultInfo
|
|
||||||
|
|
||||||
|
|
||||||
class LyricsStoreDefaultInfoAdmin(ModelView, model=StoreDefaultInfo):
|
|
||||||
name = "상가 기본 정보"
|
|
||||||
name_plural = "상가 정보 목록"
|
|
||||||
icon = "fa-solid fa-store"
|
|
||||||
category = "상가 정보 관리"
|
|
||||||
page_size = 20
|
|
||||||
|
|
||||||
column_list = ["id", "store_name"]
|
|
||||||
|
|
||||||
# 폼(생성/수정)에서 제외
|
|
||||||
form_excluded_columns = ["created_at"]
|
|
||||||
|
|
||||||
column_searchable_list = [
|
|
||||||
StoreDefaultInfo.store_name,
|
|
||||||
StoreDefaultInfo.store_phone_number,
|
|
||||||
]
|
|
||||||
|
|
||||||
column_default_sort = (StoreDefaultInfo.store_name, False) # False: ASC, True: DESC
|
|
||||||
|
|
||||||
column_sortable_list = [
|
|
||||||
StoreDefaultInfo.created_at,
|
|
||||||
StoreDefaultInfo.store_phone_number,
|
|
||||||
]
|
|
||||||
|
|
||||||
# 폼 컬럼 (기존 유지, user_posts가 관계라면 모델에 정의 필요)
|
|
||||||
# form_columns = [
|
|
||||||
# Post.user_id,
|
|
||||||
# Post.title,
|
|
||||||
# Post.content,
|
|
||||||
# Post.is_published,
|
|
||||||
# Post.user_posts, # Many-to-Many 또는 One-to-Many 관계 가정
|
|
||||||
# ]
|
|
||||||
|
|
||||||
|
|
||||||
class LyricsAttributeAdmin(ModelView, model=Attribute):
|
|
||||||
name = "속성"
|
|
||||||
name_plural = "속성 목록"
|
|
||||||
icon = "fa-solid fa-tags"
|
|
||||||
category = "속성 관리"
|
|
||||||
page_size = 20
|
|
||||||
|
|
||||||
column_list = ["id", "attr_category"]
|
|
||||||
|
|
||||||
# 폼(생성/수정)에서 제외
|
|
||||||
form_excluded_columns = ["created_at"]
|
|
||||||
|
|
||||||
column_searchable_list = [
|
|
||||||
Attribute.attr_category,
|
|
||||||
Attribute.attr_value,
|
|
||||||
]
|
|
||||||
|
|
||||||
column_default_sort = (Attribute.created_at, False) # False: ASC, True: DESC
|
|
||||||
|
|
||||||
column_sortable_list = [
|
|
||||||
Attribute.created_at,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class LyricsSongSampleAdmin(ModelView, model=SongSample):
|
|
||||||
name = "가사 샘플"
|
|
||||||
name_plural = "가사 샘플 목록"
|
|
||||||
icon = "fa-solid fa-flask"
|
|
||||||
category = "가사 샘플 관리"
|
|
||||||
page_size = 20
|
|
||||||
|
|
||||||
column_list = [
|
|
||||||
"id",
|
|
||||||
"ai_model",
|
|
||||||
"season",
|
|
||||||
"num_of_people",
|
|
||||||
"people_category",
|
|
||||||
"genre",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 폼(생성/수정)에서 제외
|
|
||||||
form_excluded_columns = ["created_at"]
|
|
||||||
|
|
||||||
column_default_sort = (SongSample.created_at, False) # False: ASC, True: DESC
|
|
||||||
|
|
||||||
|
|
||||||
class LyricsPromptTemplateAdmin(ModelView, model=PromptTemplate):
|
|
||||||
name = "프롬프트 템플릿"
|
|
||||||
name_plural = "프롬프트 템플릿 목록"
|
|
||||||
icon = "fa-solid fa-file-alt"
|
|
||||||
category = "프롬프트 템플릿 관리"
|
|
||||||
page_size = 20
|
|
||||||
|
|
||||||
column_list = [
|
|
||||||
"id",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 폼(생성/수정)에서 제외
|
|
||||||
form_excluded_columns = ["created_at"]
|
|
||||||
|
|
||||||
column_default_sort = (PromptTemplate.created_at, False) # False: ASC, True: DESC
|
|
||||||
|
|
||||||
|
|
||||||
class LyricsSongResultsAllAdmin(ModelView, model=SongResultsAll):
|
|
||||||
name = "가사 결과"
|
|
||||||
name_plural = "가사 결과 목록"
|
|
||||||
icon = "fa-solid fa-music"
|
|
||||||
category = "가사 결과 관리"
|
|
||||||
page_size = 20
|
|
||||||
|
|
||||||
column_list = [
|
|
||||||
"id",
|
|
||||||
"store_name",
|
|
||||||
]
|
|
||||||
|
|
||||||
# 폼(생성/수정)에서 제외
|
|
||||||
form_excluded_columns = ["created_at"]
|
|
||||||
|
|
||||||
column_searchable_list = [
|
|
||||||
SongResultsAll.ai,
|
|
||||||
SongResultsAll.ai_model,
|
|
||||||
]
|
|
||||||
|
|
||||||
column_default_sort = (SongResultsAll.created_at, False) # False: ASC, True: DESC
|
|
||||||
|
|
||||||
column_sortable_list = [
|
|
||||||
SongResultsAll.store_name,
|
|
||||||
SongResultsAll.store_category,
|
|
||||||
SongResultsAll.ai,
|
|
||||||
SongResultsAll.ai_model,
|
|
||||||
SongResultsAll.num_of_people,
|
|
||||||
SongResultsAll.genre,
|
|
||||||
SongResultsAll.created_at,
|
|
||||||
]
|
|
||||||
|
|
@ -1,273 +0,0 @@
|
||||||
from sqlalchemy import DateTime, Integer, String, Text, func
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
from app.database.session import Base
|
|
||||||
|
|
||||||
# from sqlalchemy import (
|
|
||||||
# Column,
|
|
||||||
# Integer,
|
|
||||||
# String,
|
|
||||||
# Boolean,
|
|
||||||
# DateTime,
|
|
||||||
# ForeignKey,
|
|
||||||
# Numeric,
|
|
||||||
# Table,
|
|
||||||
# Index,
|
|
||||||
# UniqueConstraint,
|
|
||||||
# CheckConstraint,
|
|
||||||
# text,
|
|
||||||
# func,
|
|
||||||
# PrimaryKeyConstraint,
|
|
||||||
# Enum,
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
class StoreDefaultInfo(Base):
|
|
||||||
__tablename__ = "store_default_info"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(
|
|
||||||
Integer, primary_key=True, nullable=False, autoincrement=True
|
|
||||||
)
|
|
||||||
|
|
||||||
store_info: Mapped[str] = mapped_column(
|
|
||||||
Text,
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
store_name: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
store_category: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
store_region: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
store_address: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
store_phone_number: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=True,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"id={self.id}, store_name={self.store_name}"
|
|
||||||
|
|
||||||
|
|
||||||
class PromptTemplate(Base):
|
|
||||||
__tablename__ = "prompt_template"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(
|
|
||||||
Integer, primary_key=True, nullable=False, autoincrement=True
|
|
||||||
)
|
|
||||||
|
|
||||||
description: Mapped[str] = mapped_column(
|
|
||||||
Text,
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt: Mapped[str] = mapped_column(
|
|
||||||
Text,
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"id={self.id}, description={self.description}"
|
|
||||||
|
|
||||||
|
|
||||||
class Attribute(Base):
|
|
||||||
__tablename__ = "attribute"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(
|
|
||||||
Integer, primary_key=True, nullable=False, autoincrement=True
|
|
||||||
)
|
|
||||||
|
|
||||||
attr_category: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=True,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
attr_value: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=True,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"id={self.id}, attr_category={self.attr_category}"
|
|
||||||
|
|
||||||
|
|
||||||
class SongSample(Base):
|
|
||||||
__tablename__ = "song_sample"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(
|
|
||||||
Integer, primary_key=True, nullable=False, autoincrement=True
|
|
||||||
)
|
|
||||||
|
|
||||||
ai: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
ai_model: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
genre: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
sample_song: Mapped[str] = mapped_column(
|
|
||||||
Text,
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"id={self.id}, sample_song={self.sample_song}"
|
|
||||||
|
|
||||||
|
|
||||||
class SongResultsAll(Base):
|
|
||||||
__tablename__ = "song_results_all"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(
|
|
||||||
Integer, primary_key=True, nullable=False, autoincrement=True
|
|
||||||
)
|
|
||||||
|
|
||||||
store_info: Mapped[str] = mapped_column(
|
|
||||||
Text,
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
store_name: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
store_category: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
store_address: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
store_phone_number: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
description: Mapped[str] = mapped_column(
|
|
||||||
Text,
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
prompt: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
attr_category: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
attr_value: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
ai: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
ai_model: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
season: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
num_of_people: Mapped[int] = mapped_column(
|
|
||||||
Integer,
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
people_category: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
genre: Mapped[str] = mapped_column(
|
|
||||||
String(255),
|
|
||||||
unique=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
sample_song: Mapped[str] = mapped_column(
|
|
||||||
Text,
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
result_song: Mapped[str] = mapped_column(
|
|
||||||
Text,
|
|
||||||
unique=False,
|
|
||||||
nullable=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"id={self.id}, result_song={self.result_song}"
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
from sqladmin import ModelView
|
||||||
|
|
||||||
|
from app.song.models import Song
|
||||||
|
|
||||||
|
|
||||||
|
class SongAdmin(ModelView, model=Song):
|
||||||
|
name = "노래"
|
||||||
|
name_plural = "노래 목록"
|
||||||
|
icon = "fa-solid fa-headphones"
|
||||||
|
category = "노래 관리"
|
||||||
|
page_size = 20
|
||||||
|
|
||||||
|
column_list = [
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"lyric_id",
|
||||||
|
"task_id",
|
||||||
|
"status",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
column_details_list = [
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"lyric_id",
|
||||||
|
"task_id",
|
||||||
|
"status",
|
||||||
|
"song_prompt",
|
||||||
|
"song_result_url_1",
|
||||||
|
"song_result_url_2",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 폼(생성/수정)에서 제외
|
||||||
|
form_excluded_columns = ["created_at", "videos"]
|
||||||
|
|
||||||
|
column_searchable_list = [
|
||||||
|
Song.task_id,
|
||||||
|
Song.status,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_default_sort = (Song.created_at, True) # True: DESC (최신순)
|
||||||
|
|
||||||
|
column_sortable_list = [
|
||||||
|
Song.id,
|
||||||
|
Song.project_id,
|
||||||
|
Song.lyric_id,
|
||||||
|
Song.status,
|
||||||
|
Song.created_at,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_labels = {
|
||||||
|
"id": "ID",
|
||||||
|
"project_id": "프로젝트 ID",
|
||||||
|
"lyric_id": "가사 ID",
|
||||||
|
"task_id": "작업 ID",
|
||||||
|
"status": "상태",
|
||||||
|
"song_prompt": "프롬프트",
|
||||||
|
"song_result_url_1": "결과 URL 1",
|
||||||
|
"song_result_url_2": "결과 URL 2",
|
||||||
|
"created_at": "생성일시",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.database.session import get_session
|
||||||
|
|
||||||
|
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
DateTime,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
Text,
|
||||||
|
func,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.database.session import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.home.models import Project
|
||||||
|
from app.lyric.models import Lyric
|
||||||
|
from app.video.models import Video
|
||||||
|
|
||||||
|
|
||||||
|
class Song(Base):
|
||||||
|
"""
|
||||||
|
노래 테이블
|
||||||
|
|
||||||
|
AI를 통해 생성된 노래 정보를 저장합니다.
|
||||||
|
가사를 기반으로 생성되며, 두 개의 결과 URL을 저장할 수 있습니다.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: 고유 식별자 (자동 증가)
|
||||||
|
project_id: 연결된 Project의 id (외래키)
|
||||||
|
lyric_id: 연결된 Lyric의 id (외래키)
|
||||||
|
task_id: 노래 생성 작업의 고유 식별자 (UUID 형식)
|
||||||
|
status: 처리 상태 (pending, processing, completed, failed 등)
|
||||||
|
song_prompt: 노래 생성에 사용된 프롬프트
|
||||||
|
song_result_url_1: 첫 번째 생성 결과 URL (선택)
|
||||||
|
song_result_url_2: 두 번째 생성 결과 URL (선택)
|
||||||
|
created_at: 생성 일시 (자동 설정)
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
project: 연결된 Project
|
||||||
|
lyric: 연결된 Lyric
|
||||||
|
videos: 이 노래를 사용한 영상 결과 목록
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "song"
|
||||||
|
__table_args__ = (
|
||||||
|
{"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
comment="고유 식별자",
|
||||||
|
)
|
||||||
|
|
||||||
|
project_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("project.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
comment="연결된 Project의 id",
|
||||||
|
)
|
||||||
|
|
||||||
|
lyric_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("lyric.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
comment="연결된 Lyric의 id",
|
||||||
|
)
|
||||||
|
|
||||||
|
task_id: Mapped[str] = mapped_column(
|
||||||
|
String(36),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
comment="노래 생성 작업 고유 식별자 (UUID)",
|
||||||
|
)
|
||||||
|
|
||||||
|
status: Mapped[str] = mapped_column(
|
||||||
|
String(50),
|
||||||
|
nullable=False,
|
||||||
|
comment="처리 상태 (processing, completed, failed)",
|
||||||
|
)
|
||||||
|
|
||||||
|
song_prompt: Mapped[str] = mapped_column(
|
||||||
|
Text,
|
||||||
|
nullable=False,
|
||||||
|
comment="노래 생성에 사용된 프롬프트",
|
||||||
|
)
|
||||||
|
|
||||||
|
song_result_url_1: Mapped[Optional[str]] = mapped_column(
|
||||||
|
String(2048),
|
||||||
|
nullable=True,
|
||||||
|
comment="첫 번째 노래 결과 URL",
|
||||||
|
)
|
||||||
|
|
||||||
|
song_result_url_2: Mapped[Optional[str]] = mapped_column(
|
||||||
|
String(2048),
|
||||||
|
nullable=True,
|
||||||
|
comment="두 번째 노래 결과 URL",
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime,
|
||||||
|
nullable=False,
|
||||||
|
server_default=func.now(),
|
||||||
|
comment="생성 일시",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
project: Mapped["Project"] = relationship(
|
||||||
|
"Project",
|
||||||
|
back_populates="songs",
|
||||||
|
)
|
||||||
|
|
||||||
|
lyric: Mapped["Lyric"] = relationship(
|
||||||
|
"Lyric",
|
||||||
|
back_populates="songs",
|
||||||
|
)
|
||||||
|
|
||||||
|
videos: Mapped[List["Video"]] = relationship(
|
||||||
|
"Video",
|
||||||
|
back_populates="song",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="selectin",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
def truncate(value: str | None, max_len: int = 10) -> str:
|
||||||
|
if value is None:
|
||||||
|
return "None"
|
||||||
|
return (value[:max_len] + "...") if len(value) > max_len else value
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"<Song("
|
||||||
|
f"id={self.id}, "
|
||||||
|
f"task_id='{truncate(self.task_id)}', "
|
||||||
|
f"status='{self.status}'"
|
||||||
|
f")>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StoreData:
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
store_name: str
|
||||||
|
store_category: str | None = None
|
||||||
|
store_region: str | None = None
|
||||||
|
store_address: str | None = None
|
||||||
|
store_phone_number: str | None = None
|
||||||
|
store_info: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AttributeData:
|
||||||
|
id: int
|
||||||
|
attr_category: str
|
||||||
|
attr_value: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SongSampleData:
|
||||||
|
id: int
|
||||||
|
ai: str
|
||||||
|
ai_model: str
|
||||||
|
sample_song: str
|
||||||
|
season: str | None = None
|
||||||
|
num_of_people: int | None = None
|
||||||
|
people_category: str | None = None
|
||||||
|
genre: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PromptTemplateData:
|
||||||
|
id: int
|
||||||
|
prompt: str
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SongFormData:
|
||||||
|
store_name: str
|
||||||
|
store_id: str
|
||||||
|
prompts: str
|
||||||
|
attributes: Dict[str, str] = field(default_factory=dict)
|
||||||
|
attributes_str: str = ""
|
||||||
|
lyrics_ids: List[int] = field(default_factory=list)
|
||||||
|
llm_model: str = "gpt-4o"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_form(cls, request: Request):
|
||||||
|
"""Request의 form 데이터로부터 dataclass 인스턴스 생성"""
|
||||||
|
form_data = await request.form()
|
||||||
|
|
||||||
|
# 고정 필드명들
|
||||||
|
fixed_keys = {"store_info_name", "store_id", "llm_model", "prompts"}
|
||||||
|
|
||||||
|
# lyrics-{id} 형태의 모든 키를 찾아서 ID 추출
|
||||||
|
lyrics_ids = []
|
||||||
|
attributes = {}
|
||||||
|
|
||||||
|
for key, value in form_data.items():
|
||||||
|
if key.startswith("lyrics-"):
|
||||||
|
lyrics_id = key.split("-")[1]
|
||||||
|
lyrics_ids.append(int(lyrics_id))
|
||||||
|
elif key not in fixed_keys:
|
||||||
|
attributes[key] = value
|
||||||
|
|
||||||
|
# attributes를 문자열로 변환
|
||||||
|
attributes_str = (
|
||||||
|
"\r\n\r\n".join([f"{key} : {value}" for key, value in attributes.items()])
|
||||||
|
if attributes
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
store_name=form_data.get("store_info_name", ""),
|
||||||
|
store_id=form_data.get("store_id", ""),
|
||||||
|
attributes=attributes,
|
||||||
|
attributes_str=attributes_str,
|
||||||
|
lyrics_ids=lyrics_ids,
|
||||||
|
llm_model=form_data.get("llm_model", "gpt-4o"),
|
||||||
|
prompts=form_data.get("prompts", ""),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from uuid import UUID
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class BaseService:
|
||||||
|
def __init__(self, model, session: AsyncSession):
|
||||||
|
self.model = model
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def _get(self, id: UUID):
|
||||||
|
return await self.session.get(self.model, id)
|
||||||
|
|
||||||
|
async def _add(self, entity):
|
||||||
|
self.session.add(entity)
|
||||||
|
await self.session.commit()
|
||||||
|
await self.session.refresh(entity)
|
||||||
|
return entity
|
||||||
|
|
||||||
|
async def _update(self, entity):
|
||||||
|
return await self._add(entity)
|
||||||
|
|
||||||
|
async def _delete(self, entity):
|
||||||
|
await self.session.delete(entity)
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Request # , UploadFile, File, status
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.database.session import get_session
|
||||||
|
from app.lyrics.services import lyrics
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/lyrics", tags=["lyrics"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/store")
|
||||||
|
async def home(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
# store_info_list: List[StoreData] = await lyrics_svc.get_store_info(conn)
|
||||||
|
result: Any = await lyrics.get_store_info(conn)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="store.html",
|
||||||
|
# context={"store_info_list": result},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/attributes")
|
||||||
|
async def attribute(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("attributes")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.get_attribute(conn)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="attribute.html",
|
||||||
|
# context={
|
||||||
|
# "attribute_group_dict": result,
|
||||||
|
# "before_dict": await request.form(),
|
||||||
|
# },
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/fewshot")
|
||||||
|
async def sample_song(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("fewshot")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.get_sample_song(conn)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="fewshot.html",
|
||||||
|
# context={"fewshot_list": result, "before_dict": await request.form()},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/prompt")
|
||||||
|
async def prompt_template(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("prompt_template")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.get_prompt_template(conn)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
print("prompt_template after")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="prompt.html",
|
||||||
|
# context={"prompt_list": result, "before_dict": await request.form()},
|
||||||
|
# )
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/result")
|
||||||
|
async def song_result(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("song_result")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.make_song_result(request, conn)
|
||||||
|
print("result : ", result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="result.html",
|
||||||
|
# context={"result_dict": result},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/result")
|
||||||
|
async def get_song_result(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("get_song_result")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.get_song_result(conn)
|
||||||
|
print("result : ", result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="result.html",
|
||||||
|
# context={"result_dict": result},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/automation")
|
||||||
|
async def automation(
|
||||||
|
request: Request,
|
||||||
|
conn: AsyncSession = Depends(get_session),
|
||||||
|
):
|
||||||
|
print("automation")
|
||||||
|
print(await request.form())
|
||||||
|
|
||||||
|
result: Any = await lyrics.make_automation(request, conn)
|
||||||
|
print("result : ", result)
|
||||||
|
|
||||||
|
# return templates.TemplateResponse(
|
||||||
|
# request=request,
|
||||||
|
# name="result.html",
|
||||||
|
# context={"result_dict": result},
|
||||||
|
# )
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
from sqladmin import ModelView
|
||||||
|
|
||||||
|
from app.video.models import Video
|
||||||
|
|
||||||
|
|
||||||
|
class VideoAdmin(ModelView, model=Video):
|
||||||
|
name = "영상"
|
||||||
|
name_plural = "영상 목록"
|
||||||
|
icon = "fa-solid fa-video"
|
||||||
|
category = "영상 관리"
|
||||||
|
page_size = 20
|
||||||
|
|
||||||
|
column_list = [
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"lyric_id",
|
||||||
|
"song_id",
|
||||||
|
"task_id",
|
||||||
|
"status",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
column_details_list = [
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"lyric_id",
|
||||||
|
"song_id",
|
||||||
|
"task_id",
|
||||||
|
"status",
|
||||||
|
"result_movie_url",
|
||||||
|
"created_at",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 폼(생성/수정)에서 제외
|
||||||
|
form_excluded_columns = ["created_at"]
|
||||||
|
|
||||||
|
column_searchable_list = [
|
||||||
|
Video.task_id,
|
||||||
|
Video.status,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_default_sort = (Video.created_at, True) # True: DESC (최신순)
|
||||||
|
|
||||||
|
column_sortable_list = [
|
||||||
|
Video.id,
|
||||||
|
Video.project_id,
|
||||||
|
Video.lyric_id,
|
||||||
|
Video.song_id,
|
||||||
|
Video.status,
|
||||||
|
Video.created_at,
|
||||||
|
]
|
||||||
|
|
||||||
|
column_labels = {
|
||||||
|
"id": "ID",
|
||||||
|
"project_id": "프로젝트 ID",
|
||||||
|
"lyric_id": "가사 ID",
|
||||||
|
"song_id": "노래 ID",
|
||||||
|
"task_id": "작업 ID",
|
||||||
|
"status": "상태",
|
||||||
|
"result_movie_url": "영상 URL",
|
||||||
|
"created_at": "생성일시",
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.database.session import get_session
|
||||||
|
|
||||||
|
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
DateTime,
|
||||||
|
ForeignKey,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
func,
|
||||||
|
)
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.database.session import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.home.models import Project
|
||||||
|
from app.lyric.models import Lyric
|
||||||
|
from app.song.models import Song
|
||||||
|
|
||||||
|
|
||||||
|
class Video(Base):
|
||||||
|
"""
|
||||||
|
영상 결과 테이블
|
||||||
|
|
||||||
|
최종 생성된 영상의 결과 URL을 저장합니다.
|
||||||
|
Creatomate 서비스를 통해 이미지와 노래를 결합한 영상 결과입니다.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: 고유 식별자 (자동 증가)
|
||||||
|
project_id: 연결된 Project의 id (외래키)
|
||||||
|
lyric_id: 연결된 Lyric의 id (외래키)
|
||||||
|
song_id: 연결된 Song의 id (외래키)
|
||||||
|
task_id: 영상 생성 작업의 고유 식별자 (UUID 형식)
|
||||||
|
status: 처리 상태 (pending, processing, completed, failed 등)
|
||||||
|
result_movie_url: 생성된 영상 URL (S3, CDN 경로)
|
||||||
|
created_at: 생성 일시 (자동 설정)
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
project: 연결된 Project
|
||||||
|
lyric: 연결된 Lyric
|
||||||
|
song: 연결된 Song
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "video"
|
||||||
|
__table_args__ = (
|
||||||
|
{"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
primary_key=True,
|
||||||
|
nullable=False,
|
||||||
|
autoincrement=True,
|
||||||
|
comment="고유 식별자",
|
||||||
|
)
|
||||||
|
|
||||||
|
project_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("project.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
comment="연결된 Project의 id",
|
||||||
|
)
|
||||||
|
|
||||||
|
lyric_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("lyric.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
comment="연결된 Lyric의 id",
|
||||||
|
)
|
||||||
|
|
||||||
|
song_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("song.id", ondelete="CASCADE"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
comment="연결된 Song의 id",
|
||||||
|
)
|
||||||
|
|
||||||
|
task_id: Mapped[str] = mapped_column(
|
||||||
|
String(36),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
index=True,
|
||||||
|
comment="영상 생성 작업 고유 식별자 (UUID)",
|
||||||
|
)
|
||||||
|
|
||||||
|
status: Mapped[str] = mapped_column(
|
||||||
|
String(50),
|
||||||
|
nullable=False,
|
||||||
|
comment="처리 상태 (processing, completed, failed)",
|
||||||
|
)
|
||||||
|
|
||||||
|
result_movie_url: Mapped[Optional[str]] = mapped_column(
|
||||||
|
String(2048),
|
||||||
|
nullable=True,
|
||||||
|
comment="생성된 영상 URL",
|
||||||
|
)
|
||||||
|
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime,
|
||||||
|
nullable=False,
|
||||||
|
server_default=func.now(),
|
||||||
|
comment="생성 일시",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
project: Mapped["Project"] = relationship(
|
||||||
|
"Project",
|
||||||
|
back_populates="videos",
|
||||||
|
)
|
||||||
|
|
||||||
|
lyric: Mapped["Lyric"] = relationship(
|
||||||
|
"Lyric",
|
||||||
|
back_populates="videos",
|
||||||
|
)
|
||||||
|
|
||||||
|
song: Mapped["Song"] = relationship(
|
||||||
|
"Song",
|
||||||
|
back_populates="videos",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
def truncate(value: str | None, max_len: int = 10) -> str:
|
||||||
|
if value is None:
|
||||||
|
return "None"
|
||||||
|
return (value[:max_len] + "...") if len(value) > max_len else value
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"<Video("
|
||||||
|
f"id={self.id}, "
|
||||||
|
f"task_id='{truncate(self.task_id)}', "
|
||||||
|
f"status='{self.status}'"
|
||||||
|
f")>"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StoreData:
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
store_name: str
|
||||||
|
store_category: str | None = None
|
||||||
|
store_region: str | None = None
|
||||||
|
store_address: str | None = None
|
||||||
|
store_phone_number: str | None = None
|
||||||
|
store_info: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AttributeData:
|
||||||
|
id: int
|
||||||
|
attr_category: str
|
||||||
|
attr_value: str
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SongSampleData:
|
||||||
|
id: int
|
||||||
|
ai: str
|
||||||
|
ai_model: str
|
||||||
|
sample_song: str
|
||||||
|
season: str | None = None
|
||||||
|
num_of_people: int | None = None
|
||||||
|
people_category: str | None = None
|
||||||
|
genre: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PromptTemplateData:
|
||||||
|
id: int
|
||||||
|
prompt: str
|
||||||
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SongFormData:
|
||||||
|
store_name: str
|
||||||
|
store_id: str
|
||||||
|
prompts: str
|
||||||
|
attributes: Dict[str, str] = field(default_factory=dict)
|
||||||
|
attributes_str: str = ""
|
||||||
|
lyrics_ids: List[int] = field(default_factory=list)
|
||||||
|
llm_model: str = "gpt-4o"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_form(cls, request: Request):
|
||||||
|
"""Request의 form 데이터로부터 dataclass 인스턴스 생성"""
|
||||||
|
form_data = await request.form()
|
||||||
|
|
||||||
|
# 고정 필드명들
|
||||||
|
fixed_keys = {"store_info_name", "store_id", "llm_model", "prompts"}
|
||||||
|
|
||||||
|
# lyrics-{id} 형태의 모든 키를 찾아서 ID 추출
|
||||||
|
lyrics_ids = []
|
||||||
|
attributes = {}
|
||||||
|
|
||||||
|
for key, value in form_data.items():
|
||||||
|
if key.startswith("lyrics-"):
|
||||||
|
lyrics_id = key.split("-")[1]
|
||||||
|
lyrics_ids.append(int(lyrics_id))
|
||||||
|
elif key not in fixed_keys:
|
||||||
|
attributes[key] = value
|
||||||
|
|
||||||
|
# attributes를 문자열로 변환
|
||||||
|
attributes_str = (
|
||||||
|
"\r\n\r\n".join([f"{key} : {value}" for key, value in attributes.items()])
|
||||||
|
if attributes
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
store_name=form_data.get("store_info_name", ""),
|
||||||
|
store_id=form_data.get("store_id", ""),
|
||||||
|
attributes=attributes,
|
||||||
|
attributes_str=attributes_str,
|
||||||
|
lyrics_ids=lyrics_ids,
|
||||||
|
llm_model=form_data.get("llm_model", "gpt-4o"),
|
||||||
|
prompts=form_data.get("prompts", ""),
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from uuid import UUID
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class BaseService:
|
||||||
|
def __init__(self, model, session: AsyncSession):
|
||||||
|
self.model = model
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def _get(self, id: UUID):
|
||||||
|
return await self.session.get(self.model, id)
|
||||||
|
|
||||||
|
async def _add(self, entity):
|
||||||
|
self.session.add(entity)
|
||||||
|
await self.session.commit()
|
||||||
|
await self.session.refresh(entity)
|
||||||
|
return entity
|
||||||
|
|
||||||
|
async def _update(self, entity):
|
||||||
|
return await self._add(entity)
|
||||||
|
|
||||||
|
async def _delete(self, entity):
|
||||||
|
await self.session.delete(entity)
|
||||||
|
|
@ -0,0 +1,852 @@
|
||||||
|
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.lyrics.schemas.lyrics_schema import (
|
||||||
|
AttributeData,
|
||||||
|
PromptTemplateData,
|
||||||
|
SongFormData,
|
||||||
|
SongSampleData,
|
||||||
|
StoreData,
|
||||||
|
)
|
||||||
|
from app.utils.chatgpt_prompt import chatgpt_api
|
||||||
|
|
||||||
|
|
||||||
|
async def get_store_info(conn: Connection) -> List[StoreData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM store_default_info;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_store_info = [
|
||||||
|
StoreData(
|
||||||
|
id=row[0],
|
||||||
|
store_info=row[1],
|
||||||
|
store_name=row[2],
|
||||||
|
store_category=row[3],
|
||||||
|
store_region=row[4],
|
||||||
|
store_address=row[5],
|
||||||
|
store_phone_number=row[6],
|
||||||
|
created_at=row[7],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_store_info
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_attribute(conn: Connection) -> List[AttributeData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM attribute;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_attribute = [
|
||||||
|
AttributeData(
|
||||||
|
id=row[0],
|
||||||
|
attr_category=row[1],
|
||||||
|
attr_value=row[2],
|
||||||
|
created_at=row[3],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_attribute
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_attribute(conn: Connection) -> List[AttributeData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM attribute;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_attribute = [
|
||||||
|
AttributeData(
|
||||||
|
id=row[0],
|
||||||
|
attr_category=row[1],
|
||||||
|
attr_value=row[2],
|
||||||
|
created_at=row[3],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_attribute
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_sample_song(conn: Connection) -> List[SongSampleData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM song_sample;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_sample_song = [
|
||||||
|
SongSampleData(
|
||||||
|
id=row[0],
|
||||||
|
ai=row[1],
|
||||||
|
ai_model=row[2],
|
||||||
|
genre=row[3],
|
||||||
|
sample_song=row[4],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_sample_song
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_prompt_template(conn: Connection) -> List[PromptTemplateData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM prompt_template;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_prompt_template = [
|
||||||
|
PromptTemplateData(
|
||||||
|
id=row[0],
|
||||||
|
description=row[1],
|
||||||
|
prompt=row[2],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_prompt_template
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_song_result(conn: Connection) -> List[PromptTemplateData]:
|
||||||
|
try:
|
||||||
|
query = """SELECT * FROM prompt_template;"""
|
||||||
|
result = await conn.execute(text(query))
|
||||||
|
|
||||||
|
all_prompt_template = [
|
||||||
|
PromptTemplateData(
|
||||||
|
id=row[0],
|
||||||
|
description=row[1],
|
||||||
|
prompt=row[2],
|
||||||
|
)
|
||||||
|
for row in result
|
||||||
|
]
|
||||||
|
|
||||||
|
result.close()
|
||||||
|
return all_prompt_template
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="알수없는 이유로 서비스 오류가 발생하였습니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def make_song_result(request: Request, conn: Connection):
|
||||||
|
try:
|
||||||
|
# 1. Form 데이터 파싱
|
||||||
|
form_data = await SongFormData.from_form(request)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"Store ID: {form_data.store_id}")
|
||||||
|
print(f"Lyrics IDs: {form_data.lyrics_ids}")
|
||||||
|
print(f"Prompt IDs: {form_data.prompts}")
|
||||||
|
print(f"{'=' * 60}\n")
|
||||||
|
|
||||||
|
# 2. Store 정보 조회
|
||||||
|
store_query = """
|
||||||
|
SELECT * FROM store_default_info WHERE id=:id;
|
||||||
|
"""
|
||||||
|
store_result = await conn.execute(text(store_query), {"id": form_data.store_id})
|
||||||
|
|
||||||
|
all_store_info = [
|
||||||
|
StoreData(
|
||||||
|
id=row[0],
|
||||||
|
store_info=row[1],
|
||||||
|
store_name=row[2],
|
||||||
|
store_category=row[3],
|
||||||
|
store_region=row[4],
|
||||||
|
store_address=row[5],
|
||||||
|
store_phone_number=row[6],
|
||||||
|
created_at=row[7],
|
||||||
|
)
|
||||||
|
for row in store_result
|
||||||
|
]
|
||||||
|
|
||||||
|
if not all_store_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Store not found: {form_data.store_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
store_info = all_store_info[0]
|
||||||
|
print(f"Store: {store_info.store_name}")
|
||||||
|
|
||||||
|
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||||
|
|
||||||
|
# 4. Sample Song 조회 및 결합
|
||||||
|
combined_sample_song = None
|
||||||
|
|
||||||
|
if form_data.lyrics_ids:
|
||||||
|
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||||
|
|
||||||
|
lyrics_query = """
|
||||||
|
SELECT sample_song FROM song_sample
|
||||||
|
WHERE id IN :ids
|
||||||
|
ORDER BY created_at;
|
||||||
|
"""
|
||||||
|
lyrics_result = await conn.execute(
|
||||||
|
text(lyrics_query), {"ids": tuple(form_data.lyrics_ids)}
|
||||||
|
)
|
||||||
|
|
||||||
|
sample_songs = [
|
||||||
|
row.sample_song for row in lyrics_result.fetchall() if row.sample_song
|
||||||
|
]
|
||||||
|
|
||||||
|
if sample_songs:
|
||||||
|
combined_sample_song = "\n\n".join(
|
||||||
|
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||||
|
)
|
||||||
|
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||||
|
else:
|
||||||
|
print("샘플 가사가 비어있습니다")
|
||||||
|
else:
|
||||||
|
print("선택된 lyrics가 없습니다")
|
||||||
|
|
||||||
|
# 5. 템플릿 가져오기
|
||||||
|
if not form_data.prompts:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="프롬프트 ID가 필요합니다",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("템플릿 가져오기")
|
||||||
|
|
||||||
|
prompts_query = """
|
||||||
|
SELECT * FROM prompt_template WHERE id=:id;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ✅ 수정: store_query → prompts_query
|
||||||
|
prompts_result = await conn.execute(
|
||||||
|
text(prompts_query), {"id": form_data.prompts}
|
||||||
|
)
|
||||||
|
|
||||||
|
prompts_info = [
|
||||||
|
PromptTemplateData(
|
||||||
|
id=row[0],
|
||||||
|
description=row[1],
|
||||||
|
prompt=row[2],
|
||||||
|
)
|
||||||
|
for row in prompts_result
|
||||||
|
]
|
||||||
|
|
||||||
|
if not prompts_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Prompt not found: {form_data.prompts}",
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = prompts_info[0]
|
||||||
|
print(f"Prompt Template: {prompt.prompt}")
|
||||||
|
|
||||||
|
# ✅ 6. 프롬프트 조합
|
||||||
|
updated_prompt = prompt.prompt.replace("###", form_data.attributes_str).format(
|
||||||
|
name=store_info.store_name or "",
|
||||||
|
address=store_info.store_address or "",
|
||||||
|
category=store_info.store_category or "",
|
||||||
|
description=store_info.store_info or "",
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_prompt += f"""
|
||||||
|
|
||||||
|
다음은 참고해야 하는 샘플 가사 정보입니다.
|
||||||
|
|
||||||
|
샘플 가사를 참고하여 작곡을 해주세요.
|
||||||
|
|
||||||
|
{combined_sample_song}
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(f"\n[업데이트된 프롬프트]\n{updated_prompt}\n")
|
||||||
|
|
||||||
|
# 7. 모델에게 요청
|
||||||
|
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt)
|
||||||
|
|
||||||
|
# 글자 수 계산
|
||||||
|
total_chars_with_space = len(generated_lyrics)
|
||||||
|
total_chars_without_space = len(
|
||||||
|
generated_lyrics.replace(" ", "")
|
||||||
|
.replace("\n", "")
|
||||||
|
.replace("\r", "")
|
||||||
|
.replace("\t", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
# final_lyrics 생성
|
||||||
|
final_lyrics = f"""속성 {form_data.attributes_str}
|
||||||
|
전체 글자 수 (공백 포함): {total_chars_with_space}자
|
||||||
|
전체 글자 수 (공백 제외): {total_chars_without_space}자\r\n\r\n{generated_lyrics}"""
|
||||||
|
|
||||||
|
print("=" * 40)
|
||||||
|
print("[translate:form_data.attributes_str:] ", form_data.attributes_str)
|
||||||
|
print("[translate:total_chars_with_space:] ", total_chars_with_space)
|
||||||
|
print("[translate:total_chars_without_space:] ", total_chars_without_space)
|
||||||
|
print("[translate:final_lyrics:]")
|
||||||
|
print(final_lyrics)
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
# 8. DB 저장
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO song_results_all (
|
||||||
|
store_info, store_name, store_category, store_address, store_phone_number,
|
||||||
|
description, prompt, attr_category, attr_value,
|
||||||
|
ai, ai_model, genre,
|
||||||
|
sample_song, result_song, created_at
|
||||||
|
) VALUES (
|
||||||
|
:store_info, :store_name, :store_category, :store_address, :store_phone_number,
|
||||||
|
:description, :prompt, :attr_category, :attr_value,
|
||||||
|
:ai, :ai_model, :genre,
|
||||||
|
:sample_song, :result_song, NOW()
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ✅ attr_category, attr_value 추가
|
||||||
|
insert_params = {
|
||||||
|
"store_info": store_info.store_info or "",
|
||||||
|
"store_name": store_info.store_name,
|
||||||
|
"store_category": store_info.store_category or "",
|
||||||
|
"store_address": store_info.store_address or "",
|
||||||
|
"store_phone_number": store_info.store_phone_number or "",
|
||||||
|
"description": store_info.store_info or "",
|
||||||
|
"prompt": form_data.prompts,
|
||||||
|
"attr_category": ", ".join(form_data.attributes.keys())
|
||||||
|
if form_data.attributes
|
||||||
|
else "",
|
||||||
|
"attr_value": ", ".join(form_data.attributes.values())
|
||||||
|
if form_data.attributes
|
||||||
|
else "",
|
||||||
|
"ai": "ChatGPT",
|
||||||
|
"ai_model": form_data.llm_model,
|
||||||
|
"genre": "후크송",
|
||||||
|
"sample_song": combined_sample_song or "없음",
|
||||||
|
"result_song": final_lyrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.execute(text(insert_query), insert_params)
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
|
print("결과 저장 완료")
|
||||||
|
|
||||||
|
print("\n전체 결과 조회 중...")
|
||||||
|
|
||||||
|
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||||
|
select_query = """
|
||||||
|
SELECT * FROM song_results_all
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
all_results = await conn.execute(text(select_query))
|
||||||
|
|
||||||
|
results_list = [
|
||||||
|
{
|
||||||
|
"id": row.id,
|
||||||
|
"store_info": row.store_info,
|
||||||
|
"store_name": row.store_name,
|
||||||
|
"store_category": row.store_category,
|
||||||
|
"store_address": row.store_address,
|
||||||
|
"store_phone_number": row.store_phone_number,
|
||||||
|
"description": row.description,
|
||||||
|
"prompt": row.prompt,
|
||||||
|
"attr_category": row.attr_category,
|
||||||
|
"attr_value": row.attr_value,
|
||||||
|
"ai": row.ai,
|
||||||
|
"ai_model": row.ai_model,
|
||||||
|
"genre": row.genre,
|
||||||
|
"sample_song": row.sample_song,
|
||||||
|
"result_song": row.result_song,
|
||||||
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||||
|
}
|
||||||
|
for row in all_results.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||||
|
|
||||||
|
return results_list
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Database Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_song_result(conn: Connection): # 반환 타입 수정
|
||||||
|
try:
|
||||||
|
select_query = """
|
||||||
|
SELECT * FROM song_results_all
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
all_results = await conn.execute(text(select_query))
|
||||||
|
|
||||||
|
results_list = [
|
||||||
|
{
|
||||||
|
"id": row.id,
|
||||||
|
"store_info": row.store_info,
|
||||||
|
"store_name": row.store_name,
|
||||||
|
"store_category": row.store_category,
|
||||||
|
"store_address": row.store_address,
|
||||||
|
"store_phone_number": row.store_phone_number,
|
||||||
|
"description": row.description,
|
||||||
|
"prompt": row.prompt,
|
||||||
|
"attr_category": row.attr_category,
|
||||||
|
"attr_value": row.attr_value,
|
||||||
|
"ai": row.ai,
|
||||||
|
"ai_model": row.ai_model,
|
||||||
|
"season": row.season,
|
||||||
|
"num_of_people": row.num_of_people,
|
||||||
|
"people_category": row.people_category,
|
||||||
|
"genre": row.genre,
|
||||||
|
"sample_song": row.sample_song,
|
||||||
|
"result_song": row.result_song,
|
||||||
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||||
|
}
|
||||||
|
for row in all_results.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||||
|
|
||||||
|
return results_list
|
||||||
|
except HTTPException: # HTTPException은 그대로 raise
|
||||||
|
raise
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Database Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def make_automation(request: Request, conn: Connection):
|
||||||
|
try:
|
||||||
|
# 1. Form 데이터 파싱
|
||||||
|
form_data = await SongFormData.from_form(request)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 60}")
|
||||||
|
print(f"Store ID: {form_data.store_id}")
|
||||||
|
print(f"{'=' * 60}\n")
|
||||||
|
|
||||||
|
# 2. Store 정보 조회
|
||||||
|
store_query = """
|
||||||
|
SELECT * FROM store_default_info WHERE id=:id;
|
||||||
|
"""
|
||||||
|
store_result = await conn.execute(text(store_query), {"id": form_data.store_id})
|
||||||
|
|
||||||
|
all_store_info = [
|
||||||
|
StoreData(
|
||||||
|
id=row[0],
|
||||||
|
store_info=row[1],
|
||||||
|
store_name=row[2],
|
||||||
|
store_category=row[3],
|
||||||
|
store_region=row[4],
|
||||||
|
store_address=row[5],
|
||||||
|
store_phone_number=row[6],
|
||||||
|
created_at=row[7],
|
||||||
|
)
|
||||||
|
for row in store_result
|
||||||
|
]
|
||||||
|
|
||||||
|
if not all_store_info:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Store not found: {form_data.store_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
store_info = all_store_info[0]
|
||||||
|
print(f"Store: {store_info.store_name}")
|
||||||
|
|
||||||
|
# 3. 속성 조회 -- 단계별 선택 프로세서시 구현 필요 없음
|
||||||
|
attribute_query = """
|
||||||
|
SELECT * FROM attribute;
|
||||||
|
"""
|
||||||
|
|
||||||
|
attribute_results = await conn.execute(text(attribute_query))
|
||||||
|
|
||||||
|
# 결과 가져오기
|
||||||
|
attribute_rows = attribute_results.fetchall()
|
||||||
|
|
||||||
|
formatted_attributes = ""
|
||||||
|
selected_categories = []
|
||||||
|
selected_values = []
|
||||||
|
|
||||||
|
if attribute_rows:
|
||||||
|
attribute_list = [
|
||||||
|
AttributeData(
|
||||||
|
id=row[0],
|
||||||
|
attr_category=row[1],
|
||||||
|
attr_value=row[2],
|
||||||
|
created_at=row[3],
|
||||||
|
)
|
||||||
|
for row in attribute_rows
|
||||||
|
]
|
||||||
|
|
||||||
|
# ✅ 각 category에서 하나의 value만 랜덤 선택
|
||||||
|
formatted_pairs = []
|
||||||
|
for attr in attribute_list:
|
||||||
|
# 쉼표로 분리 및 공백 제거
|
||||||
|
values = [v.strip() for v in attr.attr_value.split(",") if v.strip()]
|
||||||
|
|
||||||
|
if values:
|
||||||
|
# 랜덤하게 하나만 선택
|
||||||
|
selected_value = random.choice(values)
|
||||||
|
formatted_pairs.append(f"{attr.attr_category} : {selected_value}")
|
||||||
|
|
||||||
|
# ✅ 선택된 category와 value 저장
|
||||||
|
selected_categories.append(attr.attr_category)
|
||||||
|
selected_values.append(selected_value)
|
||||||
|
|
||||||
|
# 최종 문자열 생성
|
||||||
|
formatted_attributes = "\n".join(formatted_pairs)
|
||||||
|
|
||||||
|
print(f"\n[포맷팅된 문자열 속성 정보]\n{formatted_attributes}\n")
|
||||||
|
else:
|
||||||
|
print("속성 데이터가 없습니다")
|
||||||
|
formatted_attributes = ""
|
||||||
|
|
||||||
|
# 4. 템플릿 가져오기
|
||||||
|
print("템플릿 가져오기 (ID=1)")
|
||||||
|
|
||||||
|
prompts_query = """
|
||||||
|
SELECT * FROM prompt_template WHERE id=1;
|
||||||
|
"""
|
||||||
|
|
||||||
|
prompts_result = await conn.execute(text(prompts_query))
|
||||||
|
|
||||||
|
row = prompts_result.fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Prompt ID 1 not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = PromptTemplateData(
|
||||||
|
id=row[0],
|
||||||
|
description=row[1],
|
||||||
|
prompt=row[2],
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Prompt Template: {prompt.prompt}")
|
||||||
|
|
||||||
|
# 5. 템플릿 조합
|
||||||
|
|
||||||
|
updated_prompt = prompt.prompt.replace("###", formatted_attributes).format(
|
||||||
|
name=store_info.store_name or "",
|
||||||
|
address=store_info.store_address or "",
|
||||||
|
category=store_info.store_category or "",
|
||||||
|
description=store_info.store_info or "",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
print("업데이트된 프롬프트")
|
||||||
|
print("=" * 80)
|
||||||
|
print(updated_prompt)
|
||||||
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
|
# 4. Sample Song 조회 및 결합
|
||||||
|
combined_sample_song = None
|
||||||
|
|
||||||
|
if form_data.lyrics_ids:
|
||||||
|
print(f"\n[샘플 가사 조회] - {len(form_data.lyrics_ids)}개")
|
||||||
|
|
||||||
|
lyrics_query = """
|
||||||
|
SELECT sample_song FROM song_sample
|
||||||
|
WHERE id IN :ids
|
||||||
|
ORDER BY created_at;
|
||||||
|
"""
|
||||||
|
lyrics_result = await conn.execute(
|
||||||
|
text(lyrics_query), {"ids": tuple(form_data.lyrics_ids)}
|
||||||
|
)
|
||||||
|
|
||||||
|
sample_songs = [
|
||||||
|
row.sample_song for row in lyrics_result.fetchall() if row.sample_song
|
||||||
|
]
|
||||||
|
|
||||||
|
if sample_songs:
|
||||||
|
combined_sample_song = "\n\n".join(
|
||||||
|
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||||
|
)
|
||||||
|
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||||
|
else:
|
||||||
|
print("샘플 가사가 비어있습니다")
|
||||||
|
else:
|
||||||
|
print("선택된 lyrics가 없습니다")
|
||||||
|
|
||||||
|
# 1. song_sample 테이블의 모든 ID 조회
|
||||||
|
print("\n[샘플 가사 랜덤 선택]")
|
||||||
|
|
||||||
|
all_ids_query = """
|
||||||
|
SELECT id FROM song_sample;
|
||||||
|
"""
|
||||||
|
ids_result = await conn.execute(text(all_ids_query))
|
||||||
|
all_ids = [row.id for row in ids_result.fetchall()]
|
||||||
|
|
||||||
|
print(f"전체 샘플 가사 개수: {len(all_ids)}개")
|
||||||
|
|
||||||
|
# 2. 랜덤하게 3개 선택 (또는 전체 개수가 3개 미만이면 전체)
|
||||||
|
combined_sample_song = None
|
||||||
|
|
||||||
|
if all_ids:
|
||||||
|
# 3개 또는 전체 개수 중 작은 값 선택
|
||||||
|
sample_count = min(3, len(all_ids))
|
||||||
|
selected_ids = random.sample(all_ids, sample_count)
|
||||||
|
|
||||||
|
print(f"랜덤 선택된 ID: {selected_ids}")
|
||||||
|
|
||||||
|
# 3. 선택된 ID로 샘플 가사 조회
|
||||||
|
lyrics_query = """
|
||||||
|
SELECT sample_song FROM song_sample
|
||||||
|
WHERE id IN :ids
|
||||||
|
ORDER BY created_at;
|
||||||
|
"""
|
||||||
|
lyrics_result = await conn.execute(
|
||||||
|
text(lyrics_query), {"ids": tuple(selected_ids)}
|
||||||
|
)
|
||||||
|
|
||||||
|
sample_songs = [
|
||||||
|
row.sample_song for row in lyrics_result.fetchall() if row.sample_song
|
||||||
|
]
|
||||||
|
|
||||||
|
# 4. combined_sample_song 생성
|
||||||
|
if sample_songs:
|
||||||
|
combined_sample_song = "\n\n".join(
|
||||||
|
[f"[샘플 {i + 1}]\n{song}" for i, song in enumerate(sample_songs)]
|
||||||
|
)
|
||||||
|
print(f"{len(sample_songs)}개의 샘플 가사 조회 완료")
|
||||||
|
else:
|
||||||
|
print("샘플 가사가 비어있습니다")
|
||||||
|
else:
|
||||||
|
print("song_sample 테이블에 데이터가 없습니다")
|
||||||
|
|
||||||
|
# 5. 프롬프트에 샘플 가사 추가
|
||||||
|
if combined_sample_song:
|
||||||
|
updated_prompt += f"""
|
||||||
|
|
||||||
|
다음은 참고해야 하는 샘플 가사 정보입니다.
|
||||||
|
|
||||||
|
샘플 가사를 참고하여 작곡을 해주세요.
|
||||||
|
|
||||||
|
{combined_sample_song}
|
||||||
|
"""
|
||||||
|
print("샘플 가사 정보가 프롬프트에 추가되었습니다")
|
||||||
|
else:
|
||||||
|
print("샘플 가사가 없어 기본 프롬프트만 사용합니다")
|
||||||
|
|
||||||
|
print(f"\n[최종 프롬프트 길이: {len(updated_prompt)} 자]\n")
|
||||||
|
|
||||||
|
# 7. 모델에게 요청
|
||||||
|
generated_lyrics = await chatgpt_api.generate_lyrics(prompt=updated_prompt)
|
||||||
|
|
||||||
|
# 글자 수 계산
|
||||||
|
total_chars_with_space = len(generated_lyrics)
|
||||||
|
total_chars_without_space = len(
|
||||||
|
generated_lyrics.replace(" ", "")
|
||||||
|
.replace("\n", "")
|
||||||
|
.replace("\r", "")
|
||||||
|
.replace("\t", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
# final_lyrics 생성
|
||||||
|
final_lyrics = f"""속성 {formatted_attributes}
|
||||||
|
전체 글자 수 (공백 포함): {total_chars_with_space}자
|
||||||
|
전체 글자 수 (공백 제외): {total_chars_without_space}자\r\n\r\n{generated_lyrics}"""
|
||||||
|
|
||||||
|
# 8. DB 저장
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO song_results_all (
|
||||||
|
store_info, store_name, store_category, store_address, store_phone_number,
|
||||||
|
description, prompt, attr_category, attr_value,
|
||||||
|
ai, ai_model, genre,
|
||||||
|
sample_song, result_song, created_at
|
||||||
|
) VALUES (
|
||||||
|
:store_info, :store_name, :store_category, :store_address, :store_phone_number,
|
||||||
|
:description, :prompt, :attr_category, :attr_value,
|
||||||
|
:ai, :ai_model, :genre,
|
||||||
|
:sample_song, :result_song, NOW()
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
print("\n[insert_params 선택된 속성 확인]")
|
||||||
|
print(f"Categories: {selected_categories}")
|
||||||
|
print(f"Values: {selected_values}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# attr_category, attr_value
|
||||||
|
insert_params = {
|
||||||
|
"store_info": store_info.store_info or "",
|
||||||
|
"store_name": store_info.store_name,
|
||||||
|
"store_category": store_info.store_category or "",
|
||||||
|
"store_address": store_info.store_address or "",
|
||||||
|
"store_phone_number": store_info.store_phone_number or "",
|
||||||
|
"description": store_info.store_info or "",
|
||||||
|
"prompt": prompt.id,
|
||||||
|
# 랜덤 선택된 category와 value 사용
|
||||||
|
"attr_category": ", ".join(selected_categories)
|
||||||
|
if selected_categories
|
||||||
|
else "",
|
||||||
|
"attr_value": ", ".join(selected_values) if selected_values else "",
|
||||||
|
"ai": "ChatGPT",
|
||||||
|
"ai_model": "gpt-4o",
|
||||||
|
"genre": "후크송",
|
||||||
|
"sample_song": combined_sample_song or "없음",
|
||||||
|
"result_song": final_lyrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.execute(text(insert_query), insert_params)
|
||||||
|
await conn.commit()
|
||||||
|
|
||||||
|
print("결과 저장 완료")
|
||||||
|
|
||||||
|
print("\n전체 결과 조회 중...")
|
||||||
|
|
||||||
|
# 9. 생성 결과 가져오기 (created_at 역순)
|
||||||
|
select_query = """
|
||||||
|
SELECT * FROM song_results_all
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
all_results = await conn.execute(text(select_query))
|
||||||
|
|
||||||
|
results_list = [
|
||||||
|
{
|
||||||
|
"id": row.id,
|
||||||
|
"store_info": row.store_info,
|
||||||
|
"store_name": row.store_name,
|
||||||
|
"store_category": row.store_category,
|
||||||
|
"store_address": row.store_address,
|
||||||
|
"store_phone_number": row.store_phone_number,
|
||||||
|
"description": row.description,
|
||||||
|
"prompt": row.prompt,
|
||||||
|
"attr_category": row.attr_category,
|
||||||
|
"attr_value": row.attr_value,
|
||||||
|
"ai": row.ai,
|
||||||
|
"ai_model": row.ai_model,
|
||||||
|
"genre": row.genre,
|
||||||
|
"sample_song": row.sample_song,
|
||||||
|
"result_song": row.result_song,
|
||||||
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
||||||
|
}
|
||||||
|
for row in all_results.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"전체 {len(results_list)}개의 결과 조회 완료\n")
|
||||||
|
|
||||||
|
return results_list
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
print(f"Database Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="데이터베이스 연결에 문제가 발생했습니다.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected Error: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="서비스 처리 중 오류가 발생했습니다.",
|
||||||
|
)
|
||||||
|
|
@ -18,6 +18,7 @@ class ProjectSettings(BaseSettings):
|
||||||
VERSION: str = Field(default="0.1.0")
|
VERSION: str = Field(default="0.1.0")
|
||||||
DESCRIPTION: str = Field(default="FastAPI 기반 CastAD 프로젝트")
|
DESCRIPTION: str = Field(default="FastAPI 기반 CastAD 프로젝트")
|
||||||
ADMIN_BASE_URL: str = Field(default="/admin")
|
ADMIN_BASE_URL: str = Field(default="/admin")
|
||||||
|
DEBUG: bool = Field(default=True)
|
||||||
|
|
||||||
model_config = _base_config
|
model_config = _base_config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
-- input_history 테이블
|
||||||
|
CREATE TABLE input_history (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
customer_name VARCHAR(255) NOT NULL,
|
||||||
|
region VARCHAR(100) NOT NULL,
|
||||||
|
task_id CHAR(36) NOT NULL,
|
||||||
|
detail_region_info TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- upload_img_url 테이블
|
||||||
|
CREATE TABLE upload_img_url (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
task_id CHAR(36) NOT NULL,
|
||||||
|
img_uid INT NOT NULL,
|
||||||
|
img_url VARCHAR(2048) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (task_id) REFERENCES input_history(task_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- lyrics 테이블
|
||||||
|
CREATE TABLE lyrics (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
input_history_id INT NOT NULL,
|
||||||
|
task_id CHAR(36) NOT NULL,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
lyrics_prompt TEXT NOT NULL,
|
||||||
|
lyrics_result LONGTEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (input_history_id) REFERENCES input_history(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- song 테이블
|
||||||
|
CREATE TABLE song (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
input_history_id INT NOT NULL,
|
||||||
|
lyrics_id INT NOT NULL,
|
||||||
|
task_id CHAR(36) NOT NULL,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
song_prompt TEXT NOT NULL,
|
||||||
|
song_result_url_1 VARCHAR(2048),
|
||||||
|
song_result_url_2 VARCHAR(2048),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (input_history_id) REFERENCES input_history(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (lyrics_id) REFERENCES lyrics(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- creatomate_result_url 테이블
|
||||||
|
CREATE TABLE creatomate_result_url (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
input_history_id INT NOT NULL,
|
||||||
|
song_id INT NOT NULL,
|
||||||
|
task_id CHAR(36) NOT NULL,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
result_movie_url VARCHAR(2048),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (input_history_id) REFERENCES input_history(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (song_id) REFERENCES song(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ===== 인덱스 추가 (쿼리 성능 최적화) =====
|
||||||
|
|
||||||
|
-- input_history
|
||||||
|
CREATE INDEX idx_input_history_task_id ON input_history(task_id);
|
||||||
|
|
||||||
|
-- upload_img_url (task_id 인덱스 + 복합 인덱스)
|
||||||
|
CREATE INDEX idx_upload_img_url_task_id ON upload_img_url(task_id);
|
||||||
|
CREATE INDEX idx_upload_img_url_task_id_img_uid ON upload_img_url(task_id, img_uid);
|
||||||
|
|
||||||
|
-- lyrics (input_history_id + task_id 인덱스)
|
||||||
|
CREATE INDEX idx_lyrics_input_history_id ON lyrics(input_history_id);
|
||||||
|
CREATE INDEX idx_lyrics_task_id ON lyrics(task_id);
|
||||||
|
|
||||||
|
-- song (input_history_id + lyrics_id + task_id 인덱스)
|
||||||
|
CREATE INDEX idx_song_input_history_id ON song(input_history_id);
|
||||||
|
CREATE INDEX idx_song_lyrics_id ON song(lyrics_id);
|
||||||
|
CREATE INDEX idx_song_task_id ON song(task_id);
|
||||||
|
|
||||||
|
-- creatomate_result_url (input_history_id + song_id + task_id 인덱스)
|
||||||
|
CREATE INDEX idx_creatomate_input_history_id ON creatomate_result_url(input_history_id);
|
||||||
|
CREATE INDEX idx_creatomate_song_id ON creatomate_result_url(song_id);
|
||||||
|
CREATE INDEX idx_creatomate_task_id ON creatomate_result_url(task_id);
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
-- input_history 테이블
|
||||||
|
CREATE TABLE input_history (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
customer_name VARCHAR(255) NOT NULL,
|
||||||
|
region VARCHAR(100) NOT NULL,
|
||||||
|
task_id CHAR(36) NOT NULL UNIQUE, -- 유니크 UUID
|
||||||
|
detail_region_info TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- upload_img_url 테이블
|
||||||
|
CREATE TABLE upload_img_url (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
task_id CHAR(36) NOT NULL, -- input_history와 연결
|
||||||
|
img_uid INT NOT NULL,
|
||||||
|
img_url VARCHAR(2048) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY unique_img_task_image (task_id, img_uid),
|
||||||
|
FOREIGN KEY (task_id) REFERENCES input_history(task_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- lyrics 테이블
|
||||||
|
CREATE TABLE lyrics (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
input_history_id INT NOT NULL,
|
||||||
|
task_id CHAR(36) NOT NULL UNIQUE,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
lyrics_prompt TEXT NOT NULL,
|
||||||
|
lyrics_result LONGTEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (input_history_id) REFERENCES input_history(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- song 테이블
|
||||||
|
CREATE TABLE song (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
input_history_id INT NOT NULL,
|
||||||
|
lyrics_id INT NOT NULL,
|
||||||
|
task_id CHAR(36) NOT NULL UNIQUE,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
song_prompt TEXT NOT NULL,
|
||||||
|
song_result_url_1 VARCHAR(2048),
|
||||||
|
song_result_url_2 VARCHAR(2048),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (input_history_id) REFERENCES input_history(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (lyrics_id) REFERENCES lyrics(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- creatomate_result_url 테이블
|
||||||
|
CREATE TABLE creatomate_result_url (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
input_history_id INT NOT NULL,
|
||||||
|
song_id INT NOT NULL,
|
||||||
|
task_id CHAR(36) NOT NULL UNIQUE,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
result_movie_url VARCHAR(2048),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (input_history_id) REFERENCES input_history(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (song_id) REFERENCES song(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ===== 인덱스 추가 (쿼리 성능 최적화) =====
|
||||||
|
|
||||||
|
-- input_history
|
||||||
|
CREATE INDEX idx_input_history_task_id ON input_history(task_id);
|
||||||
|
|
||||||
|
-- upload_img_url (task_id 인덱스 + 복합 인덱스)
|
||||||
|
CREATE INDEX idx_upload_img_url_task_id ON upload_img_url(task_id);
|
||||||
|
CREATE INDEX idx_upload_img_url_task_id_img_uid ON upload_img_url(task_id, img_uid);
|
||||||
|
|
||||||
|
-- lyrics (input_history_id + task_id 인덱스)
|
||||||
|
CREATE INDEX idx_lyrics_input_history_id ON lyrics(input_history_id);
|
||||||
|
CREATE INDEX idx_lyrics_task_id ON lyrics(task_id);
|
||||||
|
|
||||||
|
-- song (input_history_id + lyrics_id + task_id 인덱스)
|
||||||
|
CREATE INDEX idx_song_input_history_id ON song(input_history_id);
|
||||||
|
CREATE INDEX idx_song_lyrics_id ON song(lyrics_id);
|
||||||
|
CREATE INDEX idx_song_task_id ON song(task_id);
|
||||||
|
|
||||||
|
-- creatomate_result_url (input_history_id + song_id + task_id 인덱스)
|
||||||
|
CREATE INDEX idx_creatomate_input_history_id ON creatomate_result_url(input_history_id);
|
||||||
|
CREATE INDEX idx_creatomate_song_id ON creatomate_result_url(song_id);
|
||||||
|
CREATE INDEX idx_creatomate_task_id ON creatomate_result_url(task_id);
|
||||||
2
main.py
2
main.py
|
|
@ -7,7 +7,7 @@ from app.admin_manager import init_admin
|
||||||
from app.core.common import lifespan
|
from app.core.common import lifespan
|
||||||
from app.database.session import engine
|
from app.database.session import engine
|
||||||
from app.home.api.routers.v1.home import router as home_router
|
from app.home.api.routers.v1.home import router as home_router
|
||||||
from app.lyrics.api.routers.v1.router import router as lyrics_router
|
from app.lyric.api.routers.v1.router import router as lyrics_router
|
||||||
from app.utils.cors import CustomCORSMiddleware
|
from app.utils.cors import CustomCORSMiddleware
|
||||||
from config import prj_settings
|
from config import prj_settings
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: o2o-castad-backend
|
||||||
|
Version: 0.1.0
|
||||||
|
Summary: Add your description here
|
||||||
|
Requires-Python: >=3.13
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
Requires-Dist: aiomysql>=0.3.2
|
||||||
|
Requires-Dist: asyncmy>=0.2.10
|
||||||
|
Requires-Dist: fastapi-cli>=0.0.16
|
||||||
|
Requires-Dist: fastapi[standard]>=0.125.0
|
||||||
|
Requires-Dist: openai>=2.13.0
|
||||||
|
Requires-Dist: pydantic-settings>=2.12.0
|
||||||
|
Requires-Dist: redis>=7.1.0
|
||||||
|
Requires-Dist: ruff>=0.14.9
|
||||||
|
Requires-Dist: scalar-fastapi>=1.5.0
|
||||||
|
Requires-Dist: sqladmin[full]>=0.22.0
|
||||||
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.45
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
README.md
|
||||||
|
pyproject.toml
|
||||||
|
app/__init__.py
|
||||||
|
app/admin_manager.py
|
||||||
|
app/core/__init__.py
|
||||||
|
app/core/common.py
|
||||||
|
app/core/exceptions.py
|
||||||
|
app/core/logging.py
|
||||||
|
app/database/__init__.py
|
||||||
|
app/database/redis.py
|
||||||
|
app/database/session-prod.py
|
||||||
|
app/database/session.py
|
||||||
|
app/dependencies/__init__.py
|
||||||
|
app/dependencies/auth.py
|
||||||
|
app/dependencies/common.py
|
||||||
|
app/dependencies/database.py
|
||||||
|
app/dependencies/pagination.py
|
||||||
|
app/dependencies/permissions.py
|
||||||
|
app/dependencies/rate_limit.py
|
||||||
|
app/home/__init__.py
|
||||||
|
app/home/dependency.py
|
||||||
|
app/home/models.py
|
||||||
|
app/home/api/__init__.py
|
||||||
|
app/home/api/home_admin.py
|
||||||
|
app/home/api/routers/__init__.py
|
||||||
|
app/home/api/routers/v1/__init__.py
|
||||||
|
app/home/api/routers/v1/home.py
|
||||||
|
app/home/schemas/__init__.py
|
||||||
|
app/home/services/__init__.py
|
||||||
|
app/home/services/base.py
|
||||||
|
app/home/tests/__init__.py
|
||||||
|
app/home/tests/conftest.py
|
||||||
|
app/home/tests/test_db.py
|
||||||
|
app/home/tests/home/__init__.py
|
||||||
|
app/home/tests/home/conftest.py
|
||||||
|
app/home/tests/home/test_db.py
|
||||||
|
app/home/worker/__init__.py
|
||||||
|
app/lyric/__init__.py
|
||||||
|
app/lyric/dependencies.py
|
||||||
|
app/lyric/models.py
|
||||||
|
app/lyric/api/__init__.py
|
||||||
|
app/lyric/api/lyrics_admin.py
|
||||||
|
app/lyric/api/routers/__init__.py
|
||||||
|
app/lyric/api/routers/v1/__init__.py
|
||||||
|
app/lyric/api/routers/v1/router.py
|
||||||
|
app/lyric/schemas/__init__.py
|
||||||
|
app/lyric/schemas/lyrics_schema.py
|
||||||
|
app/lyric/services/__init__.py
|
||||||
|
app/lyric/services/base.py
|
||||||
|
app/lyric/services/lyrics.py
|
||||||
|
app/lyric/tests/__init__.py
|
||||||
|
app/lyric/tests/conftest.py
|
||||||
|
app/lyric/tests/lyrics/__init__.py
|
||||||
|
app/lyric/tests/lyrics/test_module.py
|
||||||
|
app/lyric/worker/__init__.py
|
||||||
|
app/song/__init__.py
|
||||||
|
app/song/dependencies.py
|
||||||
|
app/song/models.py
|
||||||
|
app/song/api/__init__.py
|
||||||
|
app/song/api/song_admin.py
|
||||||
|
app/song/api/routers/__init__.py
|
||||||
|
app/song/api/routers/v1/__init__.py
|
||||||
|
app/song/api/routers/v1/router.py
|
||||||
|
app/song/schemas/__init__.py
|
||||||
|
app/song/schemas/song_schema.py
|
||||||
|
app/song/services/__init__.py
|
||||||
|
app/song/services/base.py
|
||||||
|
app/song/services/song.py
|
||||||
|
app/song/tests/__init__.py
|
||||||
|
app/song/tests/conftest.py
|
||||||
|
app/song/tests/lyrics/__init__.py
|
||||||
|
app/song/tests/lyrics/test_module.py
|
||||||
|
app/song/worker/__init__.py
|
||||||
|
app/utils/__init__.py
|
||||||
|
app/utils/chatgpt_prompt.py
|
||||||
|
app/utils/cors.py
|
||||||
|
app/video/__init__.py
|
||||||
|
app/video/dependencies.py
|
||||||
|
app/video/models.py
|
||||||
|
app/video/api/__init__.py
|
||||||
|
app/video/api/video_admin.py
|
||||||
|
app/video/api/routers/__init__.py
|
||||||
|
app/video/api/routers/v1/__init__.py
|
||||||
|
app/video/api/routers/v1/router.py
|
||||||
|
app/video/schemas/__init__.py
|
||||||
|
app/video/schemas/video_schema.py
|
||||||
|
app/video/services/__init__.py
|
||||||
|
app/video/services/base.py
|
||||||
|
app/video/services/video.py
|
||||||
|
app/video/tests/__init__.py
|
||||||
|
app/video/tests/conftest.py
|
||||||
|
app/video/tests/lyrics/__init__.py
|
||||||
|
app/video/tests/lyrics/test_module.py
|
||||||
|
app/video/worker/__init__.py
|
||||||
|
o2o_castad_backend.egg-info/PKG-INFO
|
||||||
|
o2o_castad_backend.egg-info/SOURCES.txt
|
||||||
|
o2o_castad_backend.egg-info/dependency_links.txt
|
||||||
|
o2o_castad_backend.egg-info/requires.txt
|
||||||
|
o2o_castad_backend.egg-info/top_level.txt
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
aiomysql>=0.3.2
|
||||||
|
asyncmy>=0.2.10
|
||||||
|
fastapi-cli>=0.0.16
|
||||||
|
fastapi[standard]>=0.125.0
|
||||||
|
openai>=2.13.0
|
||||||
|
pydantic-settings>=2.12.0
|
||||||
|
redis>=7.1.0
|
||||||
|
ruff>=0.14.9
|
||||||
|
scalar-fastapi>=1.5.0
|
||||||
|
sqladmin[full]>=0.22.0
|
||||||
|
sqlalchemy[asyncio]>=2.0.45
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
app
|
||||||
Loading…
Reference in New Issue