Save local changes (GSC/Bing integrations) before merging PR #354
This commit is contained in:
@@ -4,10 +4,10 @@ Podcast Audio Handlers
|
||||
Audio generation, combining, and serving endpoints.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
||||
from fastapi.responses import FileResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Optional
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
import tempfile
|
||||
@@ -31,6 +31,83 @@ from ..models import (
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/audio/upload")
|
||||
async def upload_podcast_audio(
|
||||
file: UploadFile = File(...),
|
||||
project_id: Optional[str] = Form(None),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Upload an audio file (voice sample) for a podcast project.
|
||||
Returns the audio URL for use in video generation.
|
||||
"""
|
||||
user_id = require_authenticated_user(current_user)
|
||||
|
||||
# Validate file type
|
||||
if not file.content_type or not file.content_type.startswith('audio/'):
|
||||
# Allow octet-stream if extension is audio
|
||||
allowed_exts = ['.mp3', '.wav', '.m4a', '.aac']
|
||||
file_ext = Path(file.filename).suffix.lower()
|
||||
if file_ext not in allowed_exts and file.content_type != 'application/octet-stream':
|
||||
raise HTTPException(status_code=400, detail="File must be an audio file")
|
||||
|
||||
# Validate file size (max 20MB)
|
||||
file_content = await file.read()
|
||||
if len(file_content) > 20 * 1024 * 1024:
|
||||
raise HTTPException(status_code=400, detail="Audio file size must be less than 20MB")
|
||||
|
||||
try:
|
||||
# Generate filename
|
||||
file_ext = Path(file.filename).suffix or '.mp3'
|
||||
unique_id = str(uuid.uuid4())[:8]
|
||||
audio_filename = f"audio_{project_id or 'temp'}_{unique_id}{file_ext}"
|
||||
audio_path = PODCAST_AUDIO_DIR / audio_filename
|
||||
|
||||
# Save file
|
||||
with open(audio_path, "wb") as f:
|
||||
f.write(file_content)
|
||||
|
||||
logger.info(f"[Podcast] Audio uploaded: {audio_path}")
|
||||
|
||||
# Create audio URL
|
||||
audio_url = f"/api/podcast/audio/{audio_filename}"
|
||||
|
||||
# Save to asset library if project_id provided
|
||||
if project_id:
|
||||
try:
|
||||
save_asset_to_library(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
asset_type="audio",
|
||||
source_module="podcast_maker",
|
||||
filename=audio_filename,
|
||||
file_url=audio_url,
|
||||
file_path=str(audio_path),
|
||||
file_size=len(file_content),
|
||||
mime_type=file.content_type,
|
||||
title=f"Uploaded Audio - {project_id}",
|
||||
description="Uploaded podcast audio/voice sample",
|
||||
tags=["podcast", "audio", "upload", project_id],
|
||||
asset_metadata={
|
||||
"project_id": project_id,
|
||||
"type": "uploaded_audio",
|
||||
"status": "completed",
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Podcast] Failed to save audio asset: {e}")
|
||||
|
||||
return {
|
||||
"audio_url": audio_url,
|
||||
"audio_filename": audio_filename,
|
||||
"message": "Audio uploaded successfully"
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.error(f"[Podcast] Audio upload failed: {exc}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"Audio upload failed: {str(exc)}")
|
||||
|
||||
|
||||
@router.post("/audio", response_model=PodcastAudioResponse)
|
||||
async def generate_podcast_audio(
|
||||
request: PodcastAudioRequest,
|
||||
|
||||
@@ -10,6 +10,7 @@ from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from .constants import PODCAST_AUDIO_DIR, PODCAST_IMAGES_DIR
|
||||
from utils.media_utils import load_media_bytes
|
||||
|
||||
|
||||
def load_podcast_audio_bytes(audio_url: str) -> bytes:
|
||||
@@ -54,49 +55,23 @@ def load_podcast_audio_bytes(audio_url: str) -> bytes:
|
||||
|
||||
|
||||
def load_podcast_image_bytes(image_url: str) -> bytes:
|
||||
"""Load podcast image bytes from URL. Only handles /api/podcast/images/ URLs."""
|
||||
"""Load podcast image bytes from URL. Uses centralized media loader."""
|
||||
if not image_url:
|
||||
raise HTTPException(status_code=400, detail="Image URL is required")
|
||||
|
||||
logger.info(f"[Podcast] Loading image from URL: {image_url}")
|
||||
|
||||
try:
|
||||
parsed = urlparse(image_url)
|
||||
path = parsed.path if parsed.scheme else image_url
|
||||
# REUSE: Use centralized media loader which handles cross-module lookups
|
||||
image_bytes = load_media_bytes(image_url)
|
||||
|
||||
# Only handle /api/podcast/images/ URLs
|
||||
prefix = "/api/podcast/images/"
|
||||
if prefix not in path:
|
||||
logger.error(f"[Podcast] Unsupported image URL format: {image_url}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unsupported image URL format: {image_url}. Only /api/podcast/images/ URLs are supported."
|
||||
)
|
||||
|
||||
filename = path.split(prefix, 1)[1].split("?", 1)[0].strip()
|
||||
if not filename:
|
||||
logger.error(f"[Podcast] Could not extract filename from URL: {image_url}")
|
||||
raise HTTPException(status_code=400, detail=f"Could not extract filename from URL: {image_url}")
|
||||
|
||||
logger.info(f"[Podcast] Extracted filename: {filename}")
|
||||
logger.info(f"[Podcast] PODCAST_IMAGES_DIR: {PODCAST_IMAGES_DIR}")
|
||||
|
||||
# Podcast images are stored in podcast_images directory
|
||||
image_path = (PODCAST_IMAGES_DIR / filename).resolve()
|
||||
logger.info(f"[Podcast] Resolved image path: {image_path}")
|
||||
|
||||
# Security check: ensure path is within PODCAST_IMAGES_DIR
|
||||
if not str(image_path).startswith(str(PODCAST_IMAGES_DIR)):
|
||||
logger.error(f"[Podcast] Attempted path traversal when resolving image: {image_url} -> {image_path}")
|
||||
raise HTTPException(status_code=403, detail="Invalid image path")
|
||||
|
||||
if not image_path.exists():
|
||||
logger.error(f"[Podcast] Image file not found: {image_path}")
|
||||
raise HTTPException(status_code=404, detail=f"Image file not found: {filename}")
|
||||
|
||||
image_bytes = image_path.read_bytes()
|
||||
logger.info(f"[Podcast] ✅ Successfully loaded image: {len(image_bytes)} bytes from {image_path}")
|
||||
if not image_bytes:
|
||||
logger.error(f"[Podcast] Image file not found for URL: {image_url}")
|
||||
raise HTTPException(status_code=404, detail=f"Image file not found: {image_url}")
|
||||
|
||||
logger.info(f"[Podcast] ✅ Successfully loaded image: {len(image_bytes)} bytes")
|
||||
return image_bytes
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
|
||||
Reference in New Issue
Block a user