mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-18 21:15:16 +02:00
108 lines
3.1 KiB
Python
108 lines
3.1 KiB
Python
"""Request and response shapes for the podcast API.
|
|
|
|
Read models surface the lifecycle state the frontend can't derive from Zero (the
|
|
deserialized brief and transcript); the action requests carry just what each
|
|
guarded transition needs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from app.podcasts.persistence import Podcast, PodcastStatus
|
|
from app.podcasts.schemas import PodcastSpec, Transcript
|
|
from app.podcasts.service import has_stored_episode, read_spec, read_transcript
|
|
|
|
# Defaults applied when a create request omits brief sizing; the brief gate lets
|
|
# the user adjust before any cost is incurred.
|
|
DEFAULT_SPEAKER_COUNT = 2
|
|
DEFAULT_MIN_MINUTES = 10
|
|
DEFAULT_MAX_MINUTES = 20
|
|
|
|
|
|
class CreatePodcastRequest(BaseModel):
|
|
"""Create a podcast and kick off brief proposal."""
|
|
|
|
title: str = Field(..., min_length=1, max_length=500)
|
|
search_space_id: int
|
|
source_content: str = Field(..., min_length=1)
|
|
thread_id: int | None = None
|
|
speaker_count: int = Field(default=DEFAULT_SPEAKER_COUNT, ge=1, le=6)
|
|
min_minutes: int = Field(default=DEFAULT_MIN_MINUTES, ge=1)
|
|
max_minutes: int = Field(default=DEFAULT_MAX_MINUTES, ge=1)
|
|
focus: str | None = Field(default=None, max_length=2000)
|
|
|
|
|
|
class UpdateSpecRequest(BaseModel):
|
|
"""Replace the brief at the gate, guarded by the expected version."""
|
|
|
|
spec: PodcastSpec
|
|
expected_version: int = Field(..., ge=1)
|
|
|
|
|
|
class VoiceOption(BaseModel):
|
|
"""One selectable voice surfaced to the brief editor."""
|
|
|
|
voice_id: str
|
|
display_name: str
|
|
language: str
|
|
gender: str
|
|
|
|
|
|
class LanguageOptions(BaseModel):
|
|
"""The languages the brief editor may offer for the active provider.
|
|
|
|
When ``allows_custom`` is true the list is a curated starting point and
|
|
the editor accepts any BCP-47 tag beyond it.
|
|
"""
|
|
|
|
languages: list[str]
|
|
allows_custom: bool
|
|
|
|
|
|
class PodcastSummary(BaseModel):
|
|
"""Lightweight list item."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
title: str
|
|
status: PodcastStatus
|
|
created_at: datetime
|
|
search_space_id: int
|
|
|
|
|
|
class PodcastDetail(BaseModel):
|
|
"""Full podcast state for the detail view and action responses."""
|
|
|
|
id: int
|
|
title: str
|
|
status: PodcastStatus
|
|
spec_version: int
|
|
spec: PodcastSpec | None
|
|
transcript: Transcript | None
|
|
has_audio: bool
|
|
duration_seconds: int | None
|
|
error: str | None
|
|
created_at: datetime
|
|
search_space_id: int
|
|
thread_id: int | None
|
|
|
|
@classmethod
|
|
def of(cls, podcast: Podcast) -> PodcastDetail:
|
|
return cls(
|
|
id=podcast.id,
|
|
title=podcast.title,
|
|
status=PodcastStatus(podcast.status),
|
|
spec_version=podcast.spec_version,
|
|
spec=read_spec(podcast),
|
|
transcript=read_transcript(podcast),
|
|
has_audio=has_stored_episode(podcast),
|
|
duration_seconds=podcast.duration_seconds,
|
|
error=podcast.error,
|
|
created_at=podcast.created_at,
|
|
search_space_id=podcast.search_space_id,
|
|
thread_id=podcast.thread_id,
|
|
)
|