mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-22 21:28:12 +02:00
feat: add public routes for video presentations and audio streaming
This commit is contained in:
parent
d90b6d35ce
commit
eb8cfd296c
3 changed files with 222 additions and 19 deletions
|
|
@ -21,6 +21,7 @@ from app.services.public_chat_service import (
|
|||
get_public_chat,
|
||||
get_snapshot_podcast,
|
||||
get_snapshot_report,
|
||||
get_snapshot_video_presentation,
|
||||
)
|
||||
from app.users import current_active_user
|
||||
|
||||
|
|
@ -117,6 +118,119 @@ async def stream_public_podcast(
|
|||
)
|
||||
|
||||
|
||||
@router.get("/{share_token}/video-presentations/{video_presentation_id}")
|
||||
async def get_public_video_presentation(
|
||||
share_token: str,
|
||||
video_presentation_id: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""
|
||||
Get video presentation details from a public chat snapshot.
|
||||
|
||||
No authentication required - the share_token provides access.
|
||||
Returns slide data (with public audio URLs) and scene codes.
|
||||
"""
|
||||
vp_info = await get_snapshot_video_presentation(
|
||||
session, share_token, video_presentation_id
|
||||
)
|
||||
|
||||
if not vp_info:
|
||||
raise HTTPException(status_code=404, detail="Video presentation not found")
|
||||
|
||||
slides = vp_info.get("slides") or []
|
||||
public_slides = _replace_audio_paths_with_public_urls(
|
||||
share_token, video_presentation_id, slides
|
||||
)
|
||||
|
||||
return {
|
||||
"id": vp_info.get("original_id"),
|
||||
"title": vp_info.get("title"),
|
||||
"status": "ready",
|
||||
"slides": public_slides,
|
||||
"scene_codes": vp_info.get("scene_codes"),
|
||||
"slide_count": len(slides) if slides else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{share_token}/video-presentations/{video_presentation_id}/slides/{slide_number}/audio"
|
||||
)
|
||||
async def stream_public_slide_audio(
|
||||
share_token: str,
|
||||
video_presentation_id: int,
|
||||
slide_number: int,
|
||||
session: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""
|
||||
Stream a slide's audio from a public chat snapshot.
|
||||
|
||||
No authentication required - the share_token provides access.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
vp_info = await get_snapshot_video_presentation(
|
||||
session, share_token, video_presentation_id
|
||||
)
|
||||
|
||||
if not vp_info:
|
||||
raise HTTPException(status_code=404, detail="Video presentation not found")
|
||||
|
||||
slides = vp_info.get("slides") or []
|
||||
slide_data = None
|
||||
for s in slides:
|
||||
if s.get("slide_number") == slide_number:
|
||||
slide_data = s
|
||||
break
|
||||
|
||||
if not slide_data:
|
||||
raise HTTPException(status_code=404, detail=f"Slide {slide_number} not found")
|
||||
|
||||
file_path = slide_data.get("audio_file")
|
||||
if not file_path or not os.path.isfile(file_path):
|
||||
raise HTTPException(status_code=404, detail="Slide audio file not found")
|
||||
|
||||
ext = Path(file_path).suffix.lower()
|
||||
media_type = "audio/wav" if ext == ".wav" else "audio/mpeg"
|
||||
|
||||
def iterfile():
|
||||
with open(file_path, mode="rb") as file_like:
|
||||
yield from file_like
|
||||
|
||||
return StreamingResponse(
|
||||
iterfile(),
|
||||
media_type=media_type,
|
||||
headers={
|
||||
"Accept-Ranges": "bytes",
|
||||
"Content-Disposition": f"inline; filename={Path(file_path).name}",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _replace_audio_paths_with_public_urls(
|
||||
share_token: str,
|
||||
video_presentation_id: int,
|
||||
slides: list[dict],
|
||||
) -> list[dict]:
|
||||
"""Replace server-local audio_file paths with public streaming API URLs."""
|
||||
result = []
|
||||
for slide in slides:
|
||||
slide_copy = dict(slide)
|
||||
slide_number = slide_copy.get("slide_number")
|
||||
audio_file = slide_copy.pop("audio_file", None)
|
||||
|
||||
if audio_file and slide_number is not None:
|
||||
slide_copy["audio_url"] = (
|
||||
f"/api/v1/public/{share_token}"
|
||||
f"/video-presentations/{video_presentation_id}"
|
||||
f"/slides/{slide_number}/audio"
|
||||
)
|
||||
else:
|
||||
slide_copy["audio_url"] = None
|
||||
|
||||
result.append(slide_copy)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{share_token}/reports/{report_id}/content")
|
||||
async def get_public_report_content(
|
||||
share_token: str,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ from app.db import (
|
|||
Report,
|
||||
SearchSpaceMembership,
|
||||
User,
|
||||
VideoPresentation,
|
||||
VideoPresentationStatus,
|
||||
)
|
||||
from app.utils.rbac import check_permission
|
||||
|
||||
|
|
@ -40,6 +42,7 @@ UI_TOOLS = {
|
|||
"link_preview",
|
||||
"generate_podcast",
|
||||
"generate_report",
|
||||
"generate_video_presentation",
|
||||
"scrape_webpage",
|
||||
"multi_link_preview",
|
||||
}
|
||||
|
|
@ -199,6 +202,8 @@ async def create_snapshot(
|
|||
podcast_ids_seen: set[int] = set()
|
||||
reports_data = []
|
||||
report_ids_seen: set[int] = set()
|
||||
video_presentations_data = []
|
||||
video_presentation_ids_seen: set[int] = set()
|
||||
|
||||
for msg in sorted(thread.messages, key=lambda m: m.created_at):
|
||||
author = await get_author_display(session, msg.author_id, user_cache)
|
||||
|
|
@ -225,6 +230,18 @@ async def create_snapshot(
|
|||
# Update status to "ready" so frontend renders PodcastPlayer
|
||||
part["result"] = {**result_data, "status": "ready"}
|
||||
|
||||
elif tool_name == "generate_video_presentation":
|
||||
result_data = part.get("result", {})
|
||||
vp_id = result_data.get("video_presentation_id")
|
||||
if vp_id and vp_id not in video_presentation_ids_seen:
|
||||
vp_info = await _get_video_presentation_for_snapshot(
|
||||
session, vp_id
|
||||
)
|
||||
if vp_info:
|
||||
video_presentations_data.append(vp_info)
|
||||
video_presentation_ids_seen.add(vp_id)
|
||||
part["result"] = {**result_data, "status": "ready"}
|
||||
|
||||
elif tool_name == "generate_report":
|
||||
result_data = part.get("result", {})
|
||||
report_id = result_data.get("report_id")
|
||||
|
|
@ -283,6 +300,7 @@ async def create_snapshot(
|
|||
"messages": messages_data,
|
||||
"podcasts": podcasts_data,
|
||||
"reports": reports_data,
|
||||
"video_presentations": video_presentations_data,
|
||||
}
|
||||
|
||||
# Create new snapshot
|
||||
|
|
@ -326,6 +344,27 @@ async def _get_podcast_for_snapshot(
|
|||
}
|
||||
|
||||
|
||||
async def _get_video_presentation_for_snapshot(
|
||||
session: AsyncSession,
|
||||
video_presentation_id: int,
|
||||
) -> dict | None:
|
||||
"""Get video presentation info for embedding in snapshot_data."""
|
||||
result = await session.execute(
|
||||
select(VideoPresentation).filter(VideoPresentation.id == video_presentation_id)
|
||||
)
|
||||
vp = result.scalars().first()
|
||||
|
||||
if not vp or vp.status != VideoPresentationStatus.READY:
|
||||
return None
|
||||
|
||||
return {
|
||||
"original_id": vp.id,
|
||||
"title": vp.title,
|
||||
"slides": vp.slides,
|
||||
"scene_codes": vp.scene_codes,
|
||||
}
|
||||
|
||||
|
||||
async def _get_report_for_snapshot(
|
||||
session: AsyncSession,
|
||||
report_id: int,
|
||||
|
|
@ -769,6 +808,31 @@ async def get_snapshot_podcast(
|
|||
return None
|
||||
|
||||
|
||||
async def get_snapshot_video_presentation(
|
||||
session: AsyncSession,
|
||||
share_token: str,
|
||||
video_presentation_id: int,
|
||||
) -> dict | None:
|
||||
"""
|
||||
Get video presentation info from a snapshot by original video presentation ID.
|
||||
|
||||
Used for rendering video presentation in public view.
|
||||
Looks up the presentation by its original_id in the snapshot's video_presentations array.
|
||||
"""
|
||||
snapshot = await get_snapshot_by_token(session, share_token)
|
||||
|
||||
if not snapshot:
|
||||
return None
|
||||
|
||||
video_presentations = snapshot.snapshot_data.get("video_presentations", [])
|
||||
|
||||
for vp in video_presentations:
|
||||
if vp.get("original_id") == video_presentation_id:
|
||||
return vp
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def get_snapshot_report(
|
||||
session: AsyncSession,
|
||||
share_token: str,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue