open-notebook/api/routers/podcasts.py
Luis Novo d7b0fff954
Api podcast migration (#93)
Creates the API layer for Open Notebook
Creates a services API gateway for the Streamlit front-end
Migrates the SurrealDB SDK to the official one
Change all database calls to async
New podcast framework supporting multiple speaker configurations
Implement the surreal-commands library for async processing
Improve docker image and docker-compose configurations
2025-07-17 08:36:11 -03:00

183 lines
6.3 KiB
Python

from typing import List, Optional
from pathlib import Path
from fastapi import APIRouter, HTTPException
from loguru import logger
from pydantic import BaseModel
from api.podcast_service import (
PodcastGenerationRequest,
PodcastGenerationResponse,
PodcastService,
)
from open_notebook.domain.podcast import PodcastEpisode
router = APIRouter()
class PodcastEpisodeResponse(BaseModel):
id: str
name: str
episode_profile: dict
speaker_profile: dict
briefing: str
audio_file: Optional[str] = None
transcript: Optional[dict] = None
outline: Optional[dict] = None
created: Optional[str] = None
job_status: Optional[str] = None
@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:
job_status = "unknown"
else:
# No command but has audio file = completed import
job_status = "completed"
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,
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:
job_status = "unknown"
else:
# No command but has audio file = completed import
job_status = "completed" if episode.audio_file else "unknown"
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,
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.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 = 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)}")