232 lines
8.0 KiB
Python
232 lines
8.0 KiB
Python
from pathlib import Path
|
|
from typing import List, Optional
|
|
from urllib.parse import unquote, urlparse
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import FileResponse
|
|
from loguru import logger
|
|
from pydantic import BaseModel
|
|
|
|
from api.podcast_service import (
|
|
PodcastGenerationRequest,
|
|
PodcastGenerationResponse,
|
|
PodcastService,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class PodcastEpisodeResponse(BaseModel):
|
|
id: str
|
|
name: str
|
|
episode_profile: dict
|
|
speaker_profile: dict
|
|
briefing: str
|
|
audio_file: Optional[str] = None
|
|
audio_url: Optional[str] = None
|
|
transcript: Optional[dict] = None
|
|
outline: Optional[dict] = None
|
|
created: Optional[str] = None
|
|
job_status: Optional[str] = None
|
|
|
|
|
|
def _resolve_audio_path(audio_file: str) -> Path:
|
|
if audio_file.startswith("file://"):
|
|
parsed = urlparse(audio_file)
|
|
return Path(unquote(parsed.path))
|
|
return Path(audio_file)
|
|
|
|
|
|
@router.post("/podcasts/generate", response_model=PodcastGenerationResponse)
|
|
async def generate_podcast(request: PodcastGenerationRequest):
|
|
"""
|
|
Generate a podcast episode using Episode Profiles.
|
|
Returns immediately with job ID for status tracking.
|
|
"""
|
|
try:
|
|
job_id = await PodcastService.submit_generation_job(
|
|
episode_profile_name=request.episode_profile,
|
|
speaker_profile_name=request.speaker_profile,
|
|
episode_name=request.episode_name,
|
|
notebook_id=request.notebook_id,
|
|
content=request.content,
|
|
briefing_suffix=request.briefing_suffix,
|
|
)
|
|
|
|
return PodcastGenerationResponse(
|
|
job_id=job_id,
|
|
status="submitted",
|
|
message=f"Podcast generation started for episode '{request.episode_name}'",
|
|
episode_profile=request.episode_profile,
|
|
episode_name=request.episode_name,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error generating podcast: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Failed to generate podcast: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/podcasts/jobs/{job_id}")
|
|
async def get_podcast_job_status(job_id: str):
|
|
"""Get the status of a podcast generation job"""
|
|
try:
|
|
status_data = await PodcastService.get_job_status(job_id)
|
|
return status_data
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error fetching podcast job status: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Failed to fetch job status: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/podcasts/episodes", response_model=List[PodcastEpisodeResponse])
|
|
async def list_podcast_episodes():
|
|
"""List all podcast episodes"""
|
|
try:
|
|
episodes = await PodcastService.list_episodes()
|
|
|
|
response_episodes = []
|
|
for episode in episodes:
|
|
# Skip incomplete episodes without command or audio
|
|
if not episode.command and not episode.audio_file:
|
|
continue
|
|
|
|
# Get job status if available
|
|
job_status = None
|
|
if episode.command:
|
|
try:
|
|
job_status = await episode.get_job_status()
|
|
except Exception:
|
|
job_status = "unknown"
|
|
else:
|
|
# No command but has audio file = completed import
|
|
job_status = "completed"
|
|
|
|
audio_url = None
|
|
if episode.audio_file:
|
|
audio_path = _resolve_audio_path(episode.audio_file)
|
|
if audio_path.exists():
|
|
audio_url = f"/api/podcasts/episodes/{episode.id}/audio"
|
|
|
|
response_episodes.append(
|
|
PodcastEpisodeResponse(
|
|
id=str(episode.id),
|
|
name=episode.name,
|
|
episode_profile=episode.episode_profile,
|
|
speaker_profile=episode.speaker_profile,
|
|
briefing=episode.briefing,
|
|
audio_file=episode.audio_file,
|
|
audio_url=audio_url,
|
|
transcript=episode.transcript,
|
|
outline=episode.outline,
|
|
created=str(episode.created) if episode.created else None,
|
|
job_status=job_status,
|
|
)
|
|
)
|
|
|
|
return response_episodes
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error listing podcast episodes: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Failed to list podcast episodes: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/podcasts/episodes/{episode_id}", response_model=PodcastEpisodeResponse)
|
|
async def get_podcast_episode(episode_id: str):
|
|
"""Get a specific podcast episode"""
|
|
try:
|
|
episode = await PodcastService.get_episode(episode_id)
|
|
|
|
# Get job status if available
|
|
job_status = None
|
|
if episode.command:
|
|
try:
|
|
job_status = await episode.get_job_status()
|
|
except Exception:
|
|
job_status = "unknown"
|
|
else:
|
|
# No command but has audio file = completed import
|
|
job_status = "completed" if episode.audio_file else "unknown"
|
|
|
|
audio_url = None
|
|
if episode.audio_file:
|
|
audio_path = _resolve_audio_path(episode.audio_file)
|
|
if audio_path.exists():
|
|
audio_url = f"/api/podcasts/episodes/{episode.id}/audio"
|
|
|
|
return PodcastEpisodeResponse(
|
|
id=str(episode.id),
|
|
name=episode.name,
|
|
episode_profile=episode.episode_profile,
|
|
speaker_profile=episode.speaker_profile,
|
|
briefing=episode.briefing,
|
|
audio_file=episode.audio_file,
|
|
audio_url=audio_url,
|
|
transcript=episode.transcript,
|
|
outline=episode.outline,
|
|
created=str(episode.created) if episode.created else None,
|
|
job_status=job_status,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error fetching podcast episode: {str(e)}")
|
|
raise HTTPException(status_code=404, detail=f"Episode not found: {str(e)}")
|
|
|
|
|
|
@router.get("/podcasts/episodes/{episode_id}/audio")
|
|
async def stream_podcast_episode_audio(episode_id: str):
|
|
"""Stream the audio file associated with a podcast episode"""
|
|
try:
|
|
episode = await PodcastService.get_episode(episode_id)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error fetching podcast episode for audio: {str(e)}")
|
|
raise HTTPException(status_code=404, detail=f"Episode not found: {str(e)}")
|
|
|
|
if not episode.audio_file:
|
|
raise HTTPException(status_code=404, detail="Episode has no audio file")
|
|
|
|
audio_path = _resolve_audio_path(episode.audio_file)
|
|
if not audio_path.exists():
|
|
raise HTTPException(status_code=404, detail="Audio file not found on disk")
|
|
|
|
return FileResponse(
|
|
audio_path,
|
|
media_type="audio/mpeg",
|
|
filename=audio_path.name,
|
|
)
|
|
|
|
|
|
@router.delete("/podcasts/episodes/{episode_id}")
|
|
async def delete_podcast_episode(episode_id: str):
|
|
"""Delete a podcast episode and its associated audio file"""
|
|
try:
|
|
# Get the episode first to check if it exists and get the audio file path
|
|
episode = await PodcastService.get_episode(episode_id)
|
|
|
|
# Delete the physical audio file if it exists
|
|
if episode.audio_file:
|
|
audio_path = _resolve_audio_path(episode.audio_file)
|
|
if audio_path.exists():
|
|
try:
|
|
audio_path.unlink()
|
|
logger.info(f"Deleted audio file: {audio_path}")
|
|
except Exception as e:
|
|
logger.warning(f"Failed to delete audio file {audio_path}: {e}")
|
|
|
|
# Delete the episode from the database
|
|
await episode.delete()
|
|
|
|
logger.info(f"Deleted podcast episode: {episode_id}")
|
|
return {"message": "Episode deleted successfully", "episode_id": episode_id}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error deleting podcast episode: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to delete episode: {str(e)}")
|