Files
drama-studio/audio-studio-api/app/routers/drama.py
jungwoo choi cc547372c0 feat: Drama Studio 프로젝트 초기 구조 설정
- 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>
2026-01-26 11:39:38 +09:00

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": "프로젝트가 삭제되었습니다"}