feat: Added GET /creator/transparency endpoint returning technique page…

- "backend/schemas.py"
- "backend/routers/creator_dashboard.py"

GSD-Task: S05/T01
This commit is contained in:
jlightner 2026-04-04 13:55:13 +00:00
parent 86e31cfa5c
commit 78da2f6585
2 changed files with 207 additions and 0 deletions

View file

@ -10,12 +10,14 @@ from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from auth import get_current_user from auth import get_current_user
from database import get_session from database import get_session
from models import ( from models import (
Creator, Creator,
KeyMoment, KeyMoment,
RelatedTechniqueLink,
SearchLog, SearchLog,
SourceVideo, SourceVideo,
TechniquePage, TechniquePage,
@ -25,6 +27,11 @@ from schemas import (
CreatorDashboardResponse, CreatorDashboardResponse,
CreatorDashboardTechnique, CreatorDashboardTechnique,
CreatorDashboardVideo, CreatorDashboardVideo,
CreatorTransparencyResponse,
TransparencyKeyMoment,
TransparencyRelationship,
TransparencySourceVideo,
TransparencyTechnique,
) )
logger = logging.getLogger("chrysopedia.creator_dashboard") logger = logging.getLogger("chrysopedia.creator_dashboard")
@ -160,3 +167,154 @@ async def get_creator_dashboard(
techniques=techniques, techniques=techniques,
videos=videos, videos=videos,
) )
@router.get("/transparency", response_model=CreatorTransparencyResponse)
async def get_creator_transparency(
current_user: Annotated[User, Depends(get_current_user)],
db: AsyncSession = Depends(get_session),
) -> CreatorTransparencyResponse:
"""Return all entities derived from the authenticated creator's content.
Shows technique pages, key moments, cross-reference relationships,
source videos, and aggregated topic tags everything the AI pipeline
produced from this creator's uploads.
"""
if current_user.creator_id is None:
raise HTTPException(
status_code=404,
detail="No creator profile linked to this account",
)
creator_id = current_user.creator_id
# Verify creator exists
creator = (await db.execute(
select(Creator).where(Creator.id == creator_id)
)).scalar_one_or_none()
if creator is None:
logger.error("User %s has creator_id %s but creator row missing", current_user.id, creator_id)
raise HTTPException(
status_code=404,
detail="Linked creator profile not found",
)
# ── Technique pages with key moment counts ───────────────────────────
technique_pages = (await db.execute(
select(TechniquePage)
.where(TechniquePage.creator_id == creator_id)
.options(
selectinload(TechniquePage.key_moments).selectinload(KeyMoment.source_video),
selectinload(TechniquePage.outgoing_links).selectinload(RelatedTechniqueLink.target_page),
selectinload(TechniquePage.incoming_links).selectinload(RelatedTechniqueLink.source_page),
)
.order_by(TechniquePage.created_at.desc())
)).scalars().all()
techniques = []
all_key_moments: list[TransparencyKeyMoment] = []
all_relationships: list[TransparencyRelationship] = []
all_tags: set[str] = set()
for tp in technique_pages:
techniques.append(TransparencyTechnique(
title=tp.title,
slug=tp.slug,
topic_category=tp.topic_category,
topic_tags=tp.topic_tags or [],
summary=(tp.summary or "")[:200],
created_at=tp.created_at,
key_moment_count=len(tp.key_moments),
))
# Collect tags
if tp.topic_tags:
all_tags.update(tp.topic_tags)
# Key moments from this technique page
for km in tp.key_moments:
all_key_moments.append(TransparencyKeyMoment(
title=km.title,
summary=km.summary,
content_type=km.content_type.value if hasattr(km.content_type, 'value') else str(km.content_type),
start_time=km.start_time,
end_time=km.end_time,
source_video_filename=km.source_video.filename if km.source_video else "",
technique_page_title=tp.title,
))
# Outgoing relationships
for link in tp.outgoing_links:
all_relationships.append(TransparencyRelationship(
relationship_type=link.relationship.value if hasattr(link.relationship, 'value') else str(link.relationship),
source_page_title=tp.title,
source_page_slug=tp.slug,
target_page_title=link.target_page.title if link.target_page else "",
target_page_slug=link.target_page.slug if link.target_page else "",
))
# Incoming relationships
for link in tp.incoming_links:
all_relationships.append(TransparencyRelationship(
relationship_type=link.relationship.value if hasattr(link.relationship, 'value') else str(link.relationship),
source_page_title=link.source_page.title if link.source_page else "",
source_page_slug=link.source_page.slug if link.source_page else "",
target_page_title=tp.title,
target_page_slug=tp.slug,
))
# ── Key moments not linked to a technique page ───────────────────────
# (moments from creator's videos that haven't been assigned to a page)
unlinked_moments = (await db.execute(
select(KeyMoment)
.join(SourceVideo, KeyMoment.source_video_id == SourceVideo.id)
.where(
SourceVideo.creator_id == creator_id,
KeyMoment.technique_page_id.is_(None),
)
.options(selectinload(KeyMoment.source_video))
)).scalars().all()
for km in unlinked_moments:
all_key_moments.append(TransparencyKeyMoment(
title=km.title,
summary=km.summary,
content_type=km.content_type.value if hasattr(km.content_type, 'value') else str(km.content_type),
start_time=km.start_time,
end_time=km.end_time,
source_video_filename=km.source_video.filename if km.source_video else "",
technique_page_title=None,
))
# ── Source videos ────────────────────────────────────────────────────
video_rows = (await db.execute(
select(SourceVideo)
.where(SourceVideo.creator_id == creator_id)
.order_by(SourceVideo.created_at.desc())
)).scalars().all()
source_videos = [
TransparencySourceVideo(
filename=v.filename,
processing_status=v.processing_status.value if hasattr(v.processing_status, 'value') else str(v.processing_status),
created_at=v.created_at,
)
for v in video_rows
]
logger.info(
"Transparency loaded for creator %s: %d techniques, %d moments, %d relationships, %d videos, %d tags",
creator_id, len(techniques), len(all_key_moments),
len(all_relationships), len(source_videos), len(all_tags),
)
return CreatorTransparencyResponse(
techniques=techniques,
key_moments=all_key_moments,
relationships=all_relationships,
source_videos=source_videos,
tags=sorted(all_tags),
)

View file

@ -862,3 +862,52 @@ class NotificationPreferencesUpdate(BaseModel):
"""Partial update for notification preferences.""" """Partial update for notification preferences."""
email_digests: bool | None = None email_digests: bool | None = None
digest_frequency: str | None = None digest_frequency: str | None = None
# ── Creator Transparency ─────────────────────────────────────────────────────
class TransparencyTechnique(BaseModel):
"""Technique page derived from creator's content."""
title: str
slug: str
topic_category: str
topic_tags: list[str] = Field(default_factory=list)
summary: str = ""
created_at: datetime
key_moment_count: int = 0
class TransparencyKeyMoment(BaseModel):
"""Key moment extracted from creator's videos."""
title: str
summary: str
content_type: str
start_time: float
end_time: float
source_video_filename: str = ""
technique_page_title: str | None = None
class TransparencyRelationship(BaseModel):
"""Cross-reference link involving creator's technique pages."""
relationship_type: str
source_page_title: str
source_page_slug: str
target_page_title: str
target_page_slug: str
class TransparencySourceVideo(BaseModel):
"""Source video uploaded by creator."""
filename: str
processing_status: str
created_at: datetime
class CreatorTransparencyResponse(BaseModel):
"""Full transparency payload — all entities derived from a creator's content."""
techniques: list[TransparencyTechnique] = Field(default_factory=list)
key_moments: list[TransparencyKeyMoment] = Field(default_factory=list)
relationships: list[TransparencyRelationship] = Field(default_factory=list)
source_videos: list[TransparencySourceVideo] = Field(default_factory=list)
tags: list[str] = Field(default_factory=list)