mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-18 21:15:16 +02:00
feat(podcasts): store brief duration in seconds with legacy load
This commit is contained in:
parent
9583e8f250
commit
32e0d21604
1 changed files with 30 additions and 11 deletions
|
|
@ -10,17 +10,21 @@ from __future__ import annotations
|
|||
|
||||
import re
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app.podcasts.duration_limits import (
|
||||
MAX_DURATION_SECONDS,
|
||||
MIN_DURATION_SECONDS,
|
||||
)
|
||||
|
||||
# A speaker count beyond this is almost never a real podcast and explodes the
|
||||
# voice/turn-attribution space, so we reject it at the brief gate.
|
||||
MAX_SPEAKERS = 6
|
||||
|
||||
# Long-form is a goal, but an open-ended upper bound invites runaway TTS bills.
|
||||
# One day of audio is a generous ceiling that still blocks obvious mistakes.
|
||||
MAX_DURATION_MINUTES = 24 * 60
|
||||
|
||||
# BCP-47 primary subtag plus optional region (e.g. ``en``, ``en-US``, ``pt-BR``).
|
||||
# Kept deliberately permissive: the voice catalog, not the brief, decides which
|
||||
# languages can actually be synthesised. Casing is normalised after matching.
|
||||
|
|
@ -91,7 +95,7 @@ class SpeakerSpec(BaseModel):
|
|||
|
||||
|
||||
class DurationTarget(BaseModel):
|
||||
"""The desired finished length as an inclusive minute range.
|
||||
"""The desired finished length as an inclusive second range.
|
||||
|
||||
Drafting aims for the midpoint and treats the bounds as soft guardrails;
|
||||
storing a range (rather than a point) keeps long-form expectations honest
|
||||
|
|
@ -100,19 +104,34 @@ class DurationTarget(BaseModel):
|
|||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
min_minutes: int = Field(..., ge=1, le=MAX_DURATION_MINUTES)
|
||||
max_minutes: int = Field(..., ge=1, le=MAX_DURATION_MINUTES)
|
||||
min_seconds: int = Field(..., ge=MIN_DURATION_SECONDS, le=MAX_DURATION_SECONDS)
|
||||
max_seconds: int = Field(..., ge=MIN_DURATION_SECONDS, le=MAX_DURATION_SECONDS)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _coerce_legacy_minutes(cls, data: Any) -> Any:
|
||||
"""Rows stored before seconds-based briefs still load from JSONB."""
|
||||
if isinstance(data, dict) and "min_seconds" not in data and "min_minutes" in data:
|
||||
migrated = dict(data)
|
||||
migrated["min_seconds"] = int(migrated.pop("min_minutes")) * 60
|
||||
migrated["max_seconds"] = int(migrated.pop("max_minutes")) * 60
|
||||
return migrated
|
||||
return data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_order(self) -> DurationTarget:
|
||||
if self.max_minutes < self.min_minutes:
|
||||
raise ValueError("max_minutes must be >= min_minutes")
|
||||
if self.max_seconds < self.min_seconds:
|
||||
raise ValueError("max_seconds must be >= min_seconds")
|
||||
return self
|
||||
|
||||
@property
|
||||
def midpoint_minutes(self) -> float:
|
||||
def midpoint_seconds(self) -> float:
|
||||
"""The runtime drafting should aim for within the range."""
|
||||
return (self.min_minutes + self.max_minutes) / 2
|
||||
return (self.min_seconds + self.max_seconds) / 2
|
||||
|
||||
@property
|
||||
def midpoint_minutes(self) -> float:
|
||||
return self.midpoint_seconds / 60
|
||||
|
||||
|
||||
class PodcastSpec(BaseModel):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue