- FastAPI 백엔드 (audio-studio-api) - Next.js 프론트엔드 (audio-studio-ui) - Qwen3-TTS 엔진 (audio-studio-tts) - MusicGen 서비스 (audio-studio-musicgen) - Docker Compose 개발/운영 환경 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
194 lines
6.5 KiB
Python
194 lines
6.5 KiB
Python
# 드라마 API 라우터
|
|
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
|
from fastapi.responses import FileResponse
|
|
from typing import Optional
|
|
import os
|
|
|
|
from app.models.drama import (
|
|
DramaCreateRequest, DramaGenerateRequest, DramaResponse,
|
|
ParsedScript, Character
|
|
)
|
|
from app.services.script_parser import script_parser
|
|
from app.services.drama_orchestrator import drama_orchestrator
|
|
|
|
router = APIRouter(prefix="/api/v1/drama", tags=["drama"])
|
|
|
|
|
|
@router.post("/parse", response_model=ParsedScript)
|
|
async def parse_script(script: str):
|
|
"""
|
|
스크립트 파싱 (미리보기)
|
|
|
|
마크다운 형식의 스크립트를 구조화된 데이터로 변환합니다.
|
|
실제 프로젝트 생성 없이 파싱 결과만 확인할 수 있습니다.
|
|
"""
|
|
is_valid, errors = script_parser.validate_script(script)
|
|
if not is_valid:
|
|
raise HTTPException(status_code=400, detail={"errors": errors})
|
|
|
|
return script_parser.parse(script)
|
|
|
|
|
|
@router.post("/projects", response_model=DramaResponse)
|
|
async def create_project(request: DramaCreateRequest):
|
|
"""
|
|
새 드라마 프로젝트 생성
|
|
|
|
스크립트를 파싱하고 프로젝트를 생성합니다.
|
|
voice_mapping으로 캐릭터별 보이스를 지정할 수 있습니다.
|
|
"""
|
|
# 스크립트 유효성 검사
|
|
is_valid, errors = script_parser.validate_script(request.script)
|
|
if not is_valid:
|
|
raise HTTPException(status_code=400, detail={"errors": errors})
|
|
|
|
project = await drama_orchestrator.create_project(request)
|
|
|
|
return DramaResponse(
|
|
project_id=project.project_id,
|
|
title=project.title,
|
|
status=project.status,
|
|
characters=project.script_parsed.characters if project.script_parsed else [],
|
|
element_count=len(project.script_parsed.elements) if project.script_parsed else 0,
|
|
estimated_duration=drama_orchestrator.estimate_duration(project.script_parsed) if project.script_parsed else None
|
|
)
|
|
|
|
|
|
@router.get("/projects", response_model=list[DramaResponse])
|
|
async def list_projects(skip: int = 0, limit: int = 20):
|
|
"""프로젝트 목록 조회"""
|
|
projects = await drama_orchestrator.list_projects(skip=skip, limit=limit)
|
|
|
|
return [
|
|
DramaResponse(
|
|
project_id=p.project_id,
|
|
title=p.title,
|
|
status=p.status,
|
|
characters=p.script_parsed.characters if p.script_parsed else [],
|
|
element_count=len(p.script_parsed.elements) if p.script_parsed else 0,
|
|
output_file_id=p.output_file_id,
|
|
error_message=p.error_message
|
|
)
|
|
for p in projects
|
|
]
|
|
|
|
|
|
@router.get("/projects/{project_id}", response_model=DramaResponse)
|
|
async def get_project(project_id: str):
|
|
"""프로젝트 상세 조회"""
|
|
project = await drama_orchestrator.get_project(project_id)
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
|
|
|
|
return DramaResponse(
|
|
project_id=project.project_id,
|
|
title=project.title,
|
|
status=project.status,
|
|
characters=project.script_parsed.characters if project.script_parsed else [],
|
|
element_count=len(project.script_parsed.elements) if project.script_parsed else 0,
|
|
estimated_duration=drama_orchestrator.estimate_duration(project.script_parsed) if project.script_parsed else None,
|
|
output_file_id=project.output_file_id,
|
|
error_message=project.error_message
|
|
)
|
|
|
|
|
|
@router.post("/projects/{project_id}/render")
|
|
async def render_project(
|
|
project_id: str,
|
|
background_tasks: BackgroundTasks,
|
|
output_format: str = "wav"
|
|
):
|
|
"""
|
|
드라마 렌더링 시작
|
|
|
|
백그라운드에서 TTS 생성, 효과음 검색, 믹싱을 수행합니다.
|
|
완료되면 status가 'completed'로 변경됩니다.
|
|
"""
|
|
project = await drama_orchestrator.get_project(project_id)
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
|
|
|
|
if project.status == "processing":
|
|
raise HTTPException(status_code=400, detail="이미 렌더링 중입니다")
|
|
|
|
# 백그라운드 렌더링 시작
|
|
background_tasks.add_task(
|
|
drama_orchestrator.render,
|
|
project_id,
|
|
output_format
|
|
)
|
|
|
|
return {
|
|
"project_id": project_id,
|
|
"status": "processing",
|
|
"message": "렌더링이 시작되었습니다"
|
|
}
|
|
|
|
|
|
@router.get("/projects/{project_id}/download")
|
|
async def download_project(project_id: str):
|
|
"""렌더링된 드라마 다운로드"""
|
|
project = await drama_orchestrator.get_project(project_id)
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
|
|
|
|
if project.status != "completed":
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"렌더링이 완료되지 않았습니다 (현재 상태: {project.status})"
|
|
)
|
|
|
|
if not project.output_file_id or not os.path.exists(project.output_file_id):
|
|
raise HTTPException(status_code=404, detail="출력 파일을 찾을 수 없습니다")
|
|
|
|
return FileResponse(
|
|
project.output_file_id,
|
|
media_type="audio/wav",
|
|
filename=f"{project.title}.wav"
|
|
)
|
|
|
|
|
|
@router.put("/projects/{project_id}/voices")
|
|
async def update_voice_mapping(
|
|
project_id: str,
|
|
voice_mapping: dict[str, str]
|
|
):
|
|
"""캐릭터-보이스 매핑 업데이트"""
|
|
project = await drama_orchestrator.get_project(project_id)
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
|
|
|
|
from app.database import db
|
|
from datetime import datetime
|
|
|
|
await db.dramas.update_one(
|
|
{"project_id": project_id},
|
|
{
|
|
"$set": {
|
|
"voice_mapping": voice_mapping,
|
|
"updated_at": datetime.utcnow()
|
|
}
|
|
}
|
|
)
|
|
|
|
return {"message": "보이스 매핑이 업데이트되었습니다"}
|
|
|
|
|
|
@router.delete("/projects/{project_id}")
|
|
async def delete_project(project_id: str):
|
|
"""프로젝트 삭제"""
|
|
project = await drama_orchestrator.get_project(project_id)
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다")
|
|
|
|
from app.database import db
|
|
|
|
# 출력 파일 삭제
|
|
if project.output_file_id and os.path.exists(project.output_file_id):
|
|
os.remove(project.output_file_id)
|
|
|
|
# DB에서 삭제
|
|
await db.dramas.delete_one({"project_id": project_id})
|
|
|
|
return {"message": "프로젝트가 삭제되었습니다"}
|