mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-08-16 12:23:47 +00:00
feat(artifacts): surface artifact_id on podcast and video detail
Reverse-lookup dual-written Artifacts after READY so chat cards and clients can stream via Artifact without waiting on tool-create payloads.
This commit is contained in:
parent
341844f0bd
commit
238abb5889
7 changed files with 71 additions and 16 deletions
|
|
@ -193,7 +193,7 @@ async def get_podcast(
|
|||
auth: AuthContext = Depends(get_auth_context),
|
||||
):
|
||||
podcast = await _load(session, auth, podcast_id, Permission.PODCASTS_READ)
|
||||
return PodcastDetail.of(podcast)
|
||||
return await PodcastDetail.resolve(session, podcast)
|
||||
|
||||
|
||||
@router.patch("/podcasts/{podcast_id}/spec", response_model=PodcastDetail)
|
||||
|
|
|
|||
|
|
@ -102,9 +102,11 @@ class PodcastDetail(BaseModel):
|
|||
created_at: datetime
|
||||
workspace_id: int
|
||||
thread_id: int | None
|
||||
# Cutover: dual-written Artifact id when present (Phase 3 single-read).
|
||||
artifact_id: int | None = None
|
||||
|
||||
@classmethod
|
||||
def of(cls, podcast: Podcast) -> PodcastDetail:
|
||||
def of(cls, podcast: Podcast, *, artifact_id: int | None = None) -> PodcastDetail:
|
||||
return cls(
|
||||
id=podcast.id,
|
||||
title=podcast.title,
|
||||
|
|
@ -118,4 +120,17 @@ class PodcastDetail(BaseModel):
|
|||
created_at=podcast.created_at,
|
||||
workspace_id=podcast.workspace_id,
|
||||
thread_id=podcast.thread_id,
|
||||
artifact_id=artifact_id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def resolve(cls, session, podcast: Podcast) -> PodcastDetail:
|
||||
from app.artifacts.media.legacy import existing_legacy_artifact
|
||||
|
||||
art = await existing_legacy_artifact(
|
||||
session,
|
||||
workspace_id=podcast.workspace_id,
|
||||
kind="podcast",
|
||||
legacy_id=podcast.id,
|
||||
)
|
||||
return cls.of(podcast, artifact_id=art.id if art else None)
|
||||
|
|
|
|||
|
|
@ -119,7 +119,17 @@ async def read_video_presentation(
|
|||
"You don't have permission to read video presentations in this workspace",
|
||||
)
|
||||
|
||||
return VideoPresentationRead.from_orm_with_slides(video_pres)
|
||||
from app.artifacts.media.legacy import existing_legacy_artifact
|
||||
|
||||
art = await existing_legacy_artifact(
|
||||
session,
|
||||
workspace_id=video_pres.workspace_id,
|
||||
kind="video",
|
||||
legacy_id=video_pres.id,
|
||||
)
|
||||
return VideoPresentationRead.from_orm_with_slides(
|
||||
video_pres, artifact_id=art.id if art else None
|
||||
)
|
||||
except HTTPException as he:
|
||||
raise he
|
||||
except SQLAlchemyError:
|
||||
|
|
|
|||
|
|
@ -45,12 +45,14 @@ class VideoPresentationRead(VideoPresentationBase):
|
|||
created_at: datetime
|
||||
slide_count: int | None = None
|
||||
thread_id: int | None = None
|
||||
# Cutover: dual-written Artifact id when present (Phase 3).
|
||||
artifact_id: int | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@classmethod
|
||||
def from_orm_with_slides(cls, obj):
|
||||
def from_orm_with_slides(cls, obj, *, artifact_id: int | None = None):
|
||||
"""Create VideoPresentationRead with slide_count computed.
|
||||
|
||||
Replaces raw server file paths in `audio_file` with API streaming
|
||||
|
|
@ -70,6 +72,7 @@ class VideoPresentationRead(VideoPresentationBase):
|
|||
"created_at": obj.created_at,
|
||||
"slide_count": len(obj.slides) if obj.slides else None,
|
||||
"thread_id": obj.thread_id,
|
||||
"artifact_id": artifact_id,
|
||||
}
|
||||
return cls(**data)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ test builds a row in one lifecycle shape and asserts the mapping reflects it.
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -85,10 +87,25 @@ def test_a_ready_podcast_reports_available_audio(make_spec, make_transcript):
|
|||
duration_seconds=120,
|
||||
)
|
||||
|
||||
detail = PodcastDetail.of(podcast)
|
||||
detail = PodcastDetail.of(podcast, artifact_id=77)
|
||||
|
||||
assert detail.status == PodcastStatus.READY
|
||||
assert detail.has_audio is True
|
||||
assert detail.duration_seconds == 120
|
||||
assert detail.transcript is not None
|
||||
assert detail.error is None
|
||||
assert detail.artifact_id == 77
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_attaches_dual_written_artifact_id(monkeypatch):
|
||||
import app.artifacts.media.legacy as legacy
|
||||
|
||||
podcast = _podcast(status=PodcastStatus.READY)
|
||||
monkeypatch.setattr(
|
||||
legacy,
|
||||
"existing_legacy_artifact",
|
||||
AsyncMock(return_value=SimpleNamespace(id=55)),
|
||||
)
|
||||
detail = await PodcastDetail.resolve(AsyncMock(), podcast)
|
||||
assert detail.artifact_id == 55
|
||||
|
|
|
|||
|
|
@ -213,18 +213,25 @@ function LivePodcastCard({
|
|||
// a regeneration starts with one).
|
||||
const status = podcast?.status;
|
||||
const [hasEpisode, setHasEpisode] = useState(false);
|
||||
const [artifactId, setArtifactId] = useState<number | undefined>();
|
||||
useEffect(() => {
|
||||
if (!status || !BACK_OUT_STATUSES.has(status)) return;
|
||||
let stale = false;
|
||||
podcastsApiService
|
||||
.getDetail(podcastId)
|
||||
.then((detail) => {
|
||||
if (!stale) setHasEpisode(detail.has_audio);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
stale = true;
|
||||
};
|
||||
if (!status) return;
|
||||
if (status === "ready" || BACK_OUT_STATUSES.has(status)) {
|
||||
let stale = false;
|
||||
podcastsApiService
|
||||
.getDetail(podcastId)
|
||||
.then((detail) => {
|
||||
if (stale) return;
|
||||
if (BACK_OUT_STATUSES.has(status)) setHasEpisode(detail.has_audio);
|
||||
if (status === "ready") {
|
||||
setArtifactId(detail.artifact_id ?? undefined);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
stale = true;
|
||||
};
|
||||
}
|
||||
}, [podcastId, status]);
|
||||
|
||||
if (!podcast) {
|
||||
|
|
@ -296,6 +303,8 @@ function LivePodcastCard({
|
|||
<div>
|
||||
<PodcastPlayer
|
||||
podcastId={podcast.id}
|
||||
artifactId={artifactId}
|
||||
workspaceId={podcast.workspaceId}
|
||||
title={title}
|
||||
durationMs={podcast.durationSeconds ? podcast.durationSeconds * 1000 : undefined}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ export const podcastDetail = z.object({
|
|||
created_at: z.string(),
|
||||
workspace_id: z.number(),
|
||||
thread_id: z.number().nullable(),
|
||||
artifact_id: z.number().int().positive().nullish(),
|
||||
});
|
||||
export type PodcastDetail = z.infer<typeof podcastDetail>;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue