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>
This commit is contained in:
193
audio-studio-api/app/routers/drama.py
Normal file
193
audio-studio-api/app/routers/drama.py
Normal file
@ -0,0 +1,193 @@
|
||||
# 드라마 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": "프로젝트가 삭제되었습니다"}
|
||||
Reference in New Issue
Block a user