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