home, lyric, song, video에 대한 app 추가, 각 app별 모델 추가, admin 추가, debug 모드 추가, lifespan에 debug시 동작하는 것으로 업데이트

insta
bluebamus 2025-12-20 23:16:22 +09:00
parent 6180c23151
commit 5f1b9da77e
74 changed files with 3437 additions and 416 deletions

View File

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

View File

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

View File

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

View File

@ -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": "생성일시",
}

View File

@ -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}')>"

View File

@ -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": "생성일시",
}

View File

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

131
app/lyric/models.py Normal file
View File

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

View File

@ -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="서비스 처리 중 오류가 발생했습니다.",
)

View File

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

View File

@ -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
app/song/api/__init__.py Normal file
View File

View File

View File

View File

View File

@ -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": "생성일시",
}

8
app/song/dependencies.py Normal file
View File

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

144
app/song/models.py Normal file
View File

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

View File

View File

@ -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", ""),
)

View File

24
app/song/services/base.py Normal file
View File

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

View File

View File

View File

View File

View File

0
app/video/__init__.py Normal file
View File

View File

View File

View File

View File

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

View File

View File

@ -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": "생성일시",
}

View File

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

136
app/video/models.py Normal file
View File

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

View File

View File

@ -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", ""),
)

View File

View File

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

852
app/video/services/video.py Normal file
View File

@ -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="서비스 처리 중 오류가 발생했습니다.",
)

View File

View File

View File

View File

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@

View File

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

View File

@ -0,0 +1 @@
app