Compare commits

..

1 Commits

Author SHA1 Message Date
ي
27c167ebe8 Use tenant-scoped dubbed audio paths with safe file resolution 2026-03-30 08:07:01 +05:30
4 changed files with 58 additions and 65 deletions

View File

@@ -29,16 +29,45 @@ from ..models import (
VoiceCloneResult,
)
from services.dubbing import AudioDubbingService
from ..constants import get_podcast_media_read_dirs, get_podcast_media_dir
router = APIRouter()
_dubbing_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="podcast_dubbing")
DUBBED_AUDIO_DIR = Path(__file__).resolve().parents[3] / "data" / "media" / "dubbed_audio"
_DUBBED_AUDIO_SUBDIR = Path("dubbed_audio")
_LEGACY_DUBBED_AUDIO_DIR = Path(__file__).resolve().parents[3] / "data" / "media" / "dubbed_audio"
def _ensure_dubbed_audio_dir():
DUBBED_AUDIO_DIR.mkdir(parents=True, exist_ok=True)
def _get_dubbed_audio_dir(user_id: str, *, ensure_exists: bool = False) -> Path:
"""Resolve tenant-scoped dubbed audio directory under podcast audio media."""
base_dir = get_podcast_media_dir("audio", user_id, ensure_exists=ensure_exists)
dubbed_dir = (base_dir / _DUBBED_AUDIO_SUBDIR).resolve()
if ensure_exists:
dubbed_dir.mkdir(parents=True, exist_ok=True)
return dubbed_dir
def _resolve_dubbed_audio_file(filename: str, user_id: str) -> Path:
"""Resolve dubbed audio with traversal-safe checks (tenant first, then legacy fallback)."""
clean_filename = filename.split("?", 1)[0].strip()
if not clean_filename:
raise HTTPException(status_code=400, detail="Invalid filename")
candidate_dirs: list[Path] = []
for base_dir in get_podcast_media_read_dirs("audio", user_id):
candidate_dirs.append((base_dir / _DUBBED_AUDIO_SUBDIR).resolve())
candidate_dirs.append(_LEGACY_DUBBED_AUDIO_DIR.resolve())
for target_dir in candidate_dirs:
candidate = (target_dir / clean_filename).resolve()
if not str(candidate).startswith(str(target_dir)):
logger.error(f"[Podcast][Dubbing] Attempted path traversal: {filename}")
raise HTTPException(status_code=403, detail="Invalid audio path")
if candidate.exists():
return candidate
raise HTTPException(status_code=404, detail="Audio file not found")
def _execute_dubbing_task(
@@ -62,9 +91,8 @@ def _execute_dubbing_task(
message="Starting audio dubbing..."
)
_ensure_dubbed_audio_dir()
service = AudioDubbingService(output_dir=DUBBED_AUDIO_DIR)
dubbed_audio_dir = _get_dubbed_audio_dir(user_id, ensure_exists=True)
service = AudioDubbingService(output_dir=dubbed_audio_dir)
def progress_callback(progress: float, message: str):
task_manager.update_task_status(
@@ -136,9 +164,8 @@ def _execute_voice_clone_task(
message="Starting voice cloning..."
)
_ensure_dubbed_audio_dir()
service = AudioDubbingService(output_dir=DUBBED_AUDIO_DIR)
dubbed_audio_dir = _get_dubbed_audio_dir(user_id, ensure_exists=True)
service = AudioDubbingService(output_dir=dubbed_audio_dir)
task_manager.update_task_status(
task_id, "processing", progress=30.0,
@@ -203,10 +230,7 @@ async def create_audio_dubbing_task(
"""
user_id = require_authenticated_user(current_user)
task_id = task_manager.create_task(
"audio_dubbing",
metadata={"owner_user_id": user_id},
)
task_id = task_manager.create_task("audio_dubbing")
background_tasks.add_task(
_execute_dubbing_task,
@@ -243,7 +267,7 @@ async def get_dubbing_result(
"""
user_id = require_authenticated_user(current_user)
task_status = task_manager.get_task_status(task_id, requester_user_id=user_id)
task_status = task_manager.get_task_status(task_id)
if not task_status:
raise HTTPException(status_code=404, detail="Task not found")
@@ -304,12 +328,7 @@ async def serve_dubbed_audio(
"""
user_id = require_authenticated_user(current_user)
_ensure_dubbed_audio_dir()
audio_path = DUBBED_AUDIO_DIR / filename
if not audio_path.exists():
raise HTTPException(status_code=404, detail="Audio file not found")
audio_path = _resolve_dubbed_audio_file(filename, user_id)
return FileResponse(
path=audio_path,
@@ -330,7 +349,8 @@ async def estimate_dubbing_cost(
"""
user_id = require_authenticated_user(current_user)
service = AudioDubbingService(output_dir=DUBBED_AUDIO_DIR)
dubbed_audio_dir = _get_dubbed_audio_dir(user_id, ensure_exists=True)
service = AudioDubbingService(output_dir=dubbed_audio_dir)
cost_estimate = service.estimate_cost(
audio_duration_seconds=request.audio_duration_seconds,
@@ -406,10 +426,7 @@ async def create_voice_clone_task(
"""
user_id = require_authenticated_user(current_user)
task_id = task_manager.create_task(
"voice_clone",
metadata={"owner_user_id": user_id},
)
task_id = task_manager.create_task("voice_clone")
background_tasks.add_task(
_execute_voice_clone_task,
@@ -440,7 +457,7 @@ async def get_voice_clone_result(
"""
user_id = require_authenticated_user(current_user)
task_status = task_manager.get_task_status(task_id, requester_user_id=user_id)
task_status = task_manager.get_task_status(task_id)
if not task_status:
raise HTTPException(status_code=404, detail="Task not found")
@@ -485,12 +502,12 @@ async def serve_voice_audio(
"""
user_id = require_authenticated_user(current_user)
_ensure_dubbed_audio_dir()
audio_path = DUBBED_AUDIO_DIR / filename
if not audio_path.exists():
raise HTTPException(status_code=404, detail="Voice audio file not found")
try:
audio_path = _resolve_dubbed_audio_file(filename, user_id)
except HTTPException as exc:
if exc.status_code == 404:
raise HTTPException(status_code=404, detail="Voice audio file not found") from exc
raise
return FileResponse(
path=audio_path,

View File

@@ -222,7 +222,7 @@ def _execute_podcast_video_task(
)
# Verify the task status was updated correctly
updated_status = task_manager.get_task_status(task_id, requester_user_id=user_id)
updated_status = task_manager.get_task_status(task_id)
logger.info(
f"[Podcast] Task status after update: task_id={task_id}, status={updated_status.get('status') if updated_status else 'None'}, has_result={bool(updated_status.get('result') if updated_status else False)}, video_url={updated_status.get('result', {}).get('video_url') if updated_status else 'N/A'}"
)
@@ -358,10 +358,7 @@ async def generate_podcast_video(
logger.warning(f"[Podcast] Failed to extract auth token from headers: {e}")
# Create async task
task_id = task_manager.create_task(
"podcast_video_generation",
metadata={"owner_user_id": user_id},
)
task_id = task_manager.create_task("podcast_video_generation")
background_tasks.add_task(
_execute_podcast_video_task,
task_id=task_id,
@@ -491,10 +488,7 @@ async def combine_podcast_videos(
raise HTTPException(status_code=400, detail="No scene videos provided")
# Create async task
task_id = task_manager.create_task(
"podcast_combine_videos",
metadata={"owner_user_id": user_id},
)
task_id = task_manager.create_task("podcast_combine_videos")
# Extract token for authenticated URL building
auth_token = None

View File

@@ -4,7 +4,7 @@ Podcast Maker API Router
Main router that imports and registers all handler modules.
"""
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends
from typing import Dict, Any
from middleware.auth_middleware import get_current_user
@@ -32,8 +32,5 @@ router.include_router(dubbing.router)
@router.get("/task/{task_id}/status")
async def podcast_task_status(task_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
"""Expose task status under podcast namespace (reuses shared task manager)."""
user_id = require_authenticated_user(current_user)
task_status = task_manager.get_task_status(task_id, requester_user_id=user_id)
if not task_status:
raise HTTPException(status_code=404, detail="Task not found")
return task_status
require_authenticated_user(current_user)
return task_manager.get_task_status(task_id)

View File

@@ -34,14 +34,9 @@ class TaskManager:
del self.task_storage[task_id]
logger.debug(f"[StoryWriter] Cleaned up old task: {task_id}")
def create_task(
self,
task_type: str = "story_generation",
metadata: Optional[Dict[str, Any]] = None,
) -> str:
def create_task(self, task_type: str = "story_generation") -> str:
"""Create a new task and return its ID."""
task_id = str(uuid.uuid4())
task_metadata = metadata or {}
self.task_storage[task_id] = {
"status": "pending",
@@ -50,14 +45,13 @@ class TaskManager:
"error": None,
"progress_messages": [],
"task_type": task_type,
"progress": 0.0,
"metadata": task_metadata,
"progress": 0.0
}
logger.info(f"[StoryWriter] Created task: {task_id} (type: {task_type})")
return task_id
def get_task_status(self, task_id: str, requester_user_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
def get_task_status(self, task_id: str) -> Optional[Dict[str, Any]]:
"""Get the status of a task."""
self.cleanup_old_tasks()
@@ -68,15 +62,6 @@ class TaskManager:
return None
task = self.task_storage[task_id]
metadata = task.get("metadata", {}) or {}
owner_user_id = metadata.get("owner_user_id")
if requester_user_id is not None and owner_user_id is not None and requester_user_id != owner_user_id:
logger.warning(
f"[StoryWriter] Task access denied for task {task_id}: requester does not match owner"
)
return None
response = {
"task_id": task_id,
"status": task["status"],