mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-02 22:01:05 +02:00
Merge commit '7ce409c580' into dev
This commit is contained in:
commit
0fe650fd8e
27 changed files with 510 additions and 134 deletions
|
|
@ -120,6 +120,9 @@ class FakeStorageBackend:
|
|||
async def open_stream(self, key: str) -> AsyncIterator[bytes]:
|
||||
yield self.objects.get(key, b"audio-bytes")
|
||||
|
||||
async def exists(self, key: str) -> bool:
|
||||
return key in self.objects
|
||||
|
||||
async def delete(self, key: str) -> None:
|
||||
self.deleted.append(key)
|
||||
|
||||
|
|
@ -214,7 +217,7 @@ def build_spec(
|
|||
slot=1, name="Guest", role=SpeakerRole.GUEST, voice_id=voice_ids[1]
|
||||
),
|
||||
],
|
||||
duration=DurationTarget(min_minutes=10, max_minutes=20),
|
||||
duration=DurationTarget(min_seconds=600, max_seconds=1200),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ async def test_quota_denial_fails_the_podcast_without_a_transcript(
|
|||
async def _deny(**_kwargs):
|
||||
raise QuotaInsufficientError(
|
||||
usage_type="podcast_generation",
|
||||
balance_micros=0,
|
||||
balance_micros=5_000_000,
|
||||
remaining_micros=0,
|
||||
)
|
||||
yield # pragma: no cover - unreachable, satisfies the CM protocol
|
||||
|
|
|
|||
|
|
@ -48,6 +48,22 @@ async def test_public_stream_serves_audio_via_storage_key(
|
|||
assert resp.content == b"public-audio"
|
||||
|
||||
|
||||
async def test_public_stream_404_when_object_missing(
|
||||
client, db_session, db_search_space, db_user, fake_storage
|
||||
):
|
||||
await _snapshot(
|
||||
db_session,
|
||||
search_space_id=db_search_space.id,
|
||||
user=db_user,
|
||||
token="tok-gone",
|
||||
podcasts=[{"original_id": 556, "storage_key": "podcasts/gone.mp3"}],
|
||||
)
|
||||
|
||||
resp = await client.get("/api/v1/public/tok-gone/podcasts/556/stream")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
async def test_public_stream_404_when_podcast_absent_from_snapshot(
|
||||
client, db_session, db_search_space, db_user
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
"""Streaming a podcast's rendered audio over HTTP.
|
||||
|
||||
A ready podcast streams its bytes from the storage backend; a podcast with no
|
||||
stored audio returns 404. Storage is an in-memory backend (the object store is a
|
||||
system boundary).
|
||||
A ready podcast streams its bytes; an in-flight one is 409, a stored-but-missing
|
||||
object is 404. Storage is an in-memory backend (the object store is a boundary).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -31,11 +30,23 @@ async def test_stream_serves_stored_audio(
|
|||
assert resp.content == b"the-audio"
|
||||
|
||||
|
||||
async def test_stream_404_when_no_audio(client, db_search_space, make_podcast):
|
||||
async def test_stream_409_while_in_flight(client, db_search_space, make_podcast):
|
||||
podcast = await make_podcast(
|
||||
search_space_id=db_search_space.id, status=PodcastStatus.DRAFTING
|
||||
)
|
||||
|
||||
resp = await client.get(f"{BASE}/{podcast.id}/stream")
|
||||
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
async def test_stream_404_when_object_missing(
|
||||
client, db_search_space, make_podcast, fake_storage
|
||||
):
|
||||
podcast = await make_podcast(
|
||||
search_space_id=db_search_space.id, status=PodcastStatus.READY
|
||||
)
|
||||
|
||||
resp = await client.get(f"{BASE}/{podcast.id}/stream")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ def make_spec():
|
|||
language: str = "en",
|
||||
style: PodcastStyle = PodcastStyle.CONVERSATIONAL,
|
||||
speakers: list[SpeakerSpec] | None = None,
|
||||
min_minutes: int = 10,
|
||||
max_minutes: int = 20,
|
||||
min_seconds: int = 600,
|
||||
max_seconds: int = 1200,
|
||||
focus: str | None = None,
|
||||
) -> PodcastSpec:
|
||||
if speakers is None:
|
||||
|
|
@ -54,7 +54,7 @@ def make_spec():
|
|||
language=language,
|
||||
style=style,
|
||||
speakers=speakers,
|
||||
duration=DurationTarget(min_minutes=min_minutes, max_minutes=max_minutes),
|
||||
duration=DurationTarget(min_seconds=min_seconds, max_seconds=max_seconds),
|
||||
focus=focus,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ def _spec(voice_id: str) -> PodcastSpec:
|
|||
speakers=[
|
||||
SpeakerSpec(slot=0, name="Host", role=SpeakerRole.HOST, voice_id=voice_id)
|
||||
],
|
||||
duration=DurationTarget(min_minutes=5, max_minutes=10),
|
||||
duration=DurationTarget(min_seconds=300, max_seconds=600),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ def test_spec_normalizes_its_language_on_construction():
|
|||
spec = PodcastSpec(
|
||||
language="EN-us",
|
||||
speakers=[_speaker(0)],
|
||||
duration=DurationTarget(min_minutes=5, max_minutes=10),
|
||||
duration=DurationTarget(min_seconds=300, max_seconds=600),
|
||||
)
|
||||
assert spec.language == "en-us"
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ def test_speakers_must_have_unique_slots():
|
|||
PodcastSpec(
|
||||
language="en",
|
||||
speakers=[_speaker(0), _speaker(0, voice_id="kokoro:af_bella")],
|
||||
duration=DurationTarget(min_minutes=5, max_minutes=10),
|
||||
duration=DurationTarget(min_seconds=300, max_seconds=600),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ def test_a_brief_needs_at_least_one_speaker():
|
|||
PodcastSpec(
|
||||
language="en",
|
||||
speakers=[],
|
||||
duration=DurationTarget(min_minutes=5, max_minutes=10),
|
||||
duration=DurationTarget(min_seconds=300, max_seconds=600),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ def test_a_monologue_brief_carries_exactly_one_speaker():
|
|||
language="en",
|
||||
style=PodcastStyle.MONOLOGUE,
|
||||
speakers=[_speaker(0)],
|
||||
duration=DurationTarget(min_minutes=5, max_minutes=10),
|
||||
duration=DurationTarget(min_seconds=300, max_seconds=600),
|
||||
)
|
||||
assert spec.style is PodcastStyle.MONOLOGUE
|
||||
|
||||
|
|
@ -98,18 +98,25 @@ def test_a_monologue_brief_rejects_multiple_speakers():
|
|||
language="en",
|
||||
style=PodcastStyle.MONOLOGUE,
|
||||
speakers=[_speaker(0), _speaker(1, voice_id="kokoro:af_bella")],
|
||||
duration=DurationTarget(min_minutes=5, max_minutes=10),
|
||||
duration=DurationTarget(min_seconds=300, max_seconds=600),
|
||||
)
|
||||
|
||||
|
||||
def test_duration_rejects_an_inverted_range():
|
||||
"""A max below the min is a user error caught at the brief gate."""
|
||||
with pytest.raises(ValidationError):
|
||||
DurationTarget(min_minutes=20, max_minutes=10)
|
||||
DurationTarget(min_seconds=1200, max_seconds=600)
|
||||
|
||||
|
||||
def test_duration_midpoint_is_where_drafting_aims():
|
||||
assert DurationTarget(min_minutes=10, max_minutes=20).midpoint_minutes == 15
|
||||
assert DurationTarget(min_seconds=600, max_seconds=1200).midpoint_seconds == 900
|
||||
assert DurationTarget(min_seconds=600, max_seconds=1200).midpoint_minutes == 15
|
||||
|
||||
|
||||
def test_duration_loads_legacy_minute_fields_from_json():
|
||||
duration = DurationTarget.model_validate({"min_minutes": 10, "max_minutes": 20})
|
||||
assert duration.min_seconds == 600
|
||||
assert duration.max_seconds == 1200
|
||||
|
||||
|
||||
def test_blank_focus_becomes_absent():
|
||||
|
|
@ -117,7 +124,7 @@ def test_blank_focus_becomes_absent():
|
|||
spec = PodcastSpec(
|
||||
language="en",
|
||||
speakers=[_speaker(0)],
|
||||
duration=DurationTarget(min_minutes=5, max_minutes=10),
|
||||
duration=DurationTarget(min_seconds=300, max_seconds=600),
|
||||
focus=" ",
|
||||
)
|
||||
assert spec.focus is None
|
||||
|
|
@ -127,7 +134,7 @@ def test_speaker_for_returns_the_speaker_bound_to_a_slot():
|
|||
spec = PodcastSpec(
|
||||
language="en",
|
||||
speakers=[_speaker(0), _speaker(1, voice_id="kokoro:af_bella")],
|
||||
duration=DurationTarget(min_minutes=5, max_minutes=10),
|
||||
duration=DurationTarget(min_seconds=300, max_seconds=600),
|
||||
)
|
||||
assert spec.speaker_for(1).voice_id == "kokoro:af_bella"
|
||||
|
||||
|
|
@ -136,7 +143,7 @@ def test_speaker_for_raises_when_no_speaker_matches():
|
|||
spec = PodcastSpec(
|
||||
language="en",
|
||||
speakers=[_speaker(0)],
|
||||
duration=DurationTarget(min_minutes=5, max_minutes=10),
|
||||
duration=DurationTarget(min_seconds=300, max_seconds=600),
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
spec.speaker_for(99)
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ async def test_ainvoke_propagates_quota_insufficient_error(monkeypatch):
|
|||
async def _denying_billable_call(**_kwargs):
|
||||
raise QuotaInsufficientError(
|
||||
usage_type="vision_extraction",
|
||||
balance_micros=0,
|
||||
balance_micros=5_000_000,
|
||||
remaining_micros=0,
|
||||
)
|
||||
yield # unreachable but required for asynccontextmanager type
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ async def _denying_billable_call(**kwargs):
|
|||
_CALL_LOG.append(kwargs)
|
||||
raise QuotaInsufficientError(
|
||||
usage_type=kwargs.get("usage_type", "?"),
|
||||
balance_micros=0,
|
||||
balance_micros=5_000_000,
|
||||
remaining_micros=0,
|
||||
)
|
||||
yield SimpleNamespace() # pragma: no cover
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue