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:
parent
86e31cfa5c
commit
78da2f6585
2 changed files with 207 additions and 0 deletions
|
|
@ -10,12 +10,14 @@ from typing import Annotated
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from auth import get_current_user
|
||||
from database import get_session
|
||||
from models import (
|
||||
Creator,
|
||||
KeyMoment,
|
||||
RelatedTechniqueLink,
|
||||
SearchLog,
|
||||
SourceVideo,
|
||||
TechniquePage,
|
||||
|
|
@ -25,6 +27,11 @@ from schemas import (
|
|||
CreatorDashboardResponse,
|
||||
CreatorDashboardTechnique,
|
||||
CreatorDashboardVideo,
|
||||
CreatorTransparencyResponse,
|
||||
TransparencyKeyMoment,
|
||||
TransparencyRelationship,
|
||||
TransparencySourceVideo,
|
||||
TransparencyTechnique,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("chrysopedia.creator_dashboard")
|
||||
|
|
@ -160,3 +167,154 @@ async def get_creator_dashboard(
|
|||
techniques=techniques,
|
||||
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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -862,3 +862,52 @@ class NotificationPreferencesUpdate(BaseModel):
|
|||
"""Partial update for notification preferences."""
|
||||
email_digests: bool | 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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue