feat: Add creator-scoped highlight review endpoints (list/detail/status…
- "backend/models.py" - "alembic/versions/021_add_highlight_trim_columns.py" - "backend/routers/creator_highlights.py" - "backend/main.py" GSD-Task: S01/T01
This commit is contained in:
parent
29c2a58843
commit
c05e4da594
13 changed files with 15559 additions and 21 deletions
1
.gsd/completed-units-M021.json
Normal file
1
.gsd/completed-units-M021.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
|
|
@ -1,6 +1,124 @@
|
|||
# S01: [A] Highlight Reel + Shorts Queue UI
|
||||
|
||||
**Goal:** Build highlight reel and shorts queue review UI for creators
|
||||
**Goal:** Creator reviews auto-detected highlights and short candidates in a review queue — approve, trim, discard
|
||||
**Demo:** After this: Creator reviews auto-detected highlights and short candidates in a review queue — approve, trim, discard
|
||||
|
||||
## Tasks
|
||||
- [x] **T01: Add creator-scoped highlight review endpoints (list/detail/status/trim) with Alembic migration for trim columns** — Build the backend foundation: a new FastAPI router with creator-scoped highlight endpoints, an Alembic migration for trim columns, and model updates. Follows the `creator_chapters.py` auth/ownership pattern exactly.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add `trim_start` (nullable Float) and `trim_end` (nullable Float) columns to `HighlightCandidate` in `backend/models.py` (after `status` field, before `created_at`)
|
||||
2. Create Alembic migration `alembic/versions/021_add_highlight_trim_columns.py` that adds `trim_start` and `trim_end` nullable float columns to `highlight_candidates` table
|
||||
3. Create `backend/routers/creator_highlights.py` with these endpoints:
|
||||
- `GET /creator/highlights` — list highlights for creator's videos, with optional `?status=` and `?shorts_only=true` (duration_secs ≤ 60) query filters. Eager-load `key_moment` relationship for title/start_time/end_time. Order by score DESC.
|
||||
- `GET /creator/highlights/{highlight_id}` — single highlight detail with full score_breakdown and key_moment info. Verify creator owns the source video.
|
||||
- `PATCH /creator/highlights/{highlight_id}` — update status (approve/reject). Accept `{"status": "approved"|"rejected"}` body. Verify creator ownership. Map UI "discard" to `rejected` status to avoid enum migration.
|
||||
- `PATCH /creator/highlights/{highlight_id}/trim` — set trim_start/trim_end. Accept `{"trim_start": float, "trim_end": float}` body. Validate: both non-negative, trim_start < trim_end. Verify creator ownership.
|
||||
4. Use Pydantic schemas for request/response: create `HighlightListResponse`, `HighlightDetailResponse`, `HighlightStatusUpdate`, `HighlightTrimUpdate` in the router file (or a separate schemas file). Reuse `HighlightScoreBreakdown` from `backend/pipeline/highlight_schemas.py`.
|
||||
5. Auth pattern: `current_user: User = Depends(get_current_user)`, then filter `SourceVideo.creator_id == current_user.creator_id`. Reuse `_verify_creator_owns_video` helper or create equivalent.
|
||||
6. Register the router in `backend/main.py`: add `creator_highlights` to the import line and `app.include_router(creator_highlights.router, prefix="/api/v1")`
|
||||
7. Add `chrysopedia.creator_highlights` logger with info-level messages on status changes and trim actions.
|
||||
|
||||
## Failure Modes
|
||||
|
||||
| Dependency | On error | On timeout | On malformed response |
|
||||
|------------|----------|-----------|----------------------|
|
||||
| PostgreSQL | Return 500 with logged traceback | SQLAlchemy handles connection pool timeout | N/A (ORM) |
|
||||
| Auth (JWT) | Return 401 via get_current_user | N/A | Return 401 |
|
||||
|
||||
## Negative Tests
|
||||
|
||||
- **Malformed inputs**: PATCH with invalid status string → 422; trim with negative values or trim_start >= trim_end → 400
|
||||
- **Error paths**: No creator profile → 403; highlight not found → 404; highlight belongs to different creator → 404
|
||||
- **Boundary conditions**: Empty highlight list returns `[]`; shorts_only with no qualifying highlights returns `[]`
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] `trim_start` and `trim_end` nullable Float columns on HighlightCandidate model
|
||||
- [ ] Alembic migration 021 applies cleanly
|
||||
- [ ] GET /creator/highlights returns creator-scoped results with status/shorts filters
|
||||
- [ ] GET /creator/highlights/{id} returns detail with score_breakdown and key_moment
|
||||
- [ ] PATCH /creator/highlights/{id} changes status with ownership verification
|
||||
- [ ] PATCH /creator/highlights/{id}/trim sets trim window with validation
|
||||
- [ ] Router registered in main.py
|
||||
|
||||
## Verification
|
||||
|
||||
- `cd /home/aux/projects/content-to-kb-automator && python -c "from routers.creator_highlights import router; print('Router imported OK')"` (run from backend dir)
|
||||
- `cd /home/aux/projects/content-to-kb-automator && python -c "from models import HighlightCandidate; assert hasattr(HighlightCandidate, 'trim_start'); assert hasattr(HighlightCandidate, 'trim_end'); print('Model columns OK')"`
|
||||
- `grep -q 'creator_highlights' backend/main.py && echo 'Router registered'`
|
||||
- `python -c "import ast; ast.parse(open('alembic/versions/021_add_highlight_trim_columns.py').read()); print('Migration syntax OK')"`
|
||||
|
||||
## Observability Impact
|
||||
|
||||
- Signals added: `chrysopedia.creator_highlights` logger with info-level status transition and trim action messages
|
||||
- Inspection: `GET /api/v1/creator/highlights?status=approved` to check approved highlights; DB query on `highlight_candidates` table
|
||||
- Failure state: 403/404 errors with specific detail messages logged at warning level
|
||||
- Estimate: 1h
|
||||
- Files: backend/models.py, alembic/versions/021_add_highlight_trim_columns.py, backend/routers/creator_highlights.py, backend/main.py
|
||||
- Verify: cd /home/aux/projects/content-to-kb-automator && python -c "import sys; sys.path.insert(0,'backend'); from models import HighlightCandidate; assert hasattr(HighlightCandidate,'trim_start'); print('OK')" && grep -q creator_highlights backend/main.py && echo 'Registered'
|
||||
- [ ] **T02: HighlightQueue page, API layer, route wiring, and sidebar link** — Build the complete frontend: TypeScript API layer, HighlightQueue page with filter tabs and action controls, CSS module, route registration in App.tsx, and Highlights link in SidebarNav. Follows the ChapterReview.tsx pattern for layout and CreatorDashboard SidebarNav for navigation.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `frontend/src/api/highlights.ts` with:
|
||||
- TypeScript interfaces: `HighlightCandidate` (id, key_moment_id, source_video_id, score, score_breakdown: ScoreBreakdown, duration_secs, status, trim_start, trim_end, created_at, key_moment: { title, start_time, end_time, content_type }), `ScoreBreakdown` (7 float fields matching backend), list response type
|
||||
- API functions: `fetchCreatorHighlights(params?: { status?: string; shorts_only?: boolean })`, `fetchHighlightDetail(id: string)`, `updateHighlightStatus(id: string, status: string)`, `trimHighlight(id: string, trim_start: number, trim_end: number)`
|
||||
- Use `request<T>()` and `BASE` from `./client`
|
||||
|
||||
2. Create `frontend/src/pages/HighlightQueue.module.css` following ChapterReview.module.css patterns:
|
||||
- Layout: `.pageLayout` (sidebar + content grid), `.content`, `.header`
|
||||
- Filter tabs: `.filterTabs`, `.filterTab`, `.filterTabActive`
|
||||
- Cards: `.candidateCard`, `.candidateScore`, `.candidateTitle`, `.candidateDuration`, `.candidateStatus`
|
||||
- Score breakdown bars: `.scoreBar`, `.scoreBarFill`, `.scoreLabel`
|
||||
- Action buttons: `.actionBtn`, `.approveBtn`, `.rejectBtn`, `.trimBtn` — reuse CSS custom properties `--color-btn-approve`, `--color-btn-reject`
|
||||
- Status badges: reuse `--color-badge-approved-*`, `--color-badge-rejected-*` custom property patterns
|
||||
- Trim UI: `.trimPanel`, `.trimInput`
|
||||
- Empty state: `.emptyState`
|
||||
- Shorts badge: `.shortsBadge` (small pill for ≤60s candidates)
|
||||
|
||||
3. Create `frontend/src/pages/HighlightQueue.tsx`:
|
||||
- Import `SidebarNav` from `./CreatorDashboard`
|
||||
- State: highlights array, loading boolean, active filter tab (all/shorts/approved/rejected), optional expanded highlight ID for trim UI
|
||||
- On mount: fetch highlights from API, apply filter based on active tab
|
||||
- Filter tabs: All | Shorts (≤60s) | Approved | Rejected — clicking a tab re-fetches with appropriate query params
|
||||
- Candidate cards showing: key_moment title, formatted duration, composite score as percentage bar, content_type badge, status badge
|
||||
- Score breakdown section: 7 horizontal bars (one per dimension) with labels and percentages
|
||||
- Action buttons per card: Approve (green, calls updateHighlightStatus), Discard (red, calls updateHighlightStatus with 'rejected'), Trim (toggles inline trim panel)
|
||||
- Trim panel: two number inputs (start seconds, end seconds) with Save/Cancel buttons. Pre-fill with current key_moment start_time/end_time if no trim exists. On save, call trimHighlight API.
|
||||
- Empty state message when no highlights match current filter
|
||||
- Loading spinner during fetch
|
||||
- Error handling: catch ApiError, show user-friendly message
|
||||
|
||||
4. Add lazy import and route in `frontend/src/App.tsx`:
|
||||
- Add `const HighlightQueue = React.lazy(() => import("./pages/HighlightQueue"));` with other lazy imports
|
||||
- Add route: `<Route path="/creator/highlights" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><HighlightQueue /></Suspense></ProtectedRoute>} />`
|
||||
- Place after the existing `/creator/chapters/:videoId` route
|
||||
|
||||
5. Add "Highlights" link to `SidebarNav` in `frontend/src/pages/CreatorDashboard.tsx`:
|
||||
- Add a new NavLink entry with a star/sparkle SVG icon and text "Highlights" linking to `/creator/highlights`
|
||||
- Place after the "Chapters" link in the nav list
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] TypeScript API layer with all 4 functions matching backend endpoints
|
||||
- [ ] HighlightQueue page renders with SidebarNav, filter tabs, candidate cards, score bars, and action buttons
|
||||
- [ ] Filter tabs switch between All / Shorts / Approved / Rejected views
|
||||
- [ ] Approve and Discard buttons call the status update API and refresh the list
|
||||
- [ ] Trim panel opens inline with number inputs, validates, and calls trim API
|
||||
- [ ] Route registered in App.tsx with ProtectedRoute and Suspense
|
||||
- [ ] SidebarNav has "Highlights" link
|
||||
- [ ] `npx tsc --noEmit` passes with zero errors
|
||||
|
||||
## Verification
|
||||
|
||||
- `cd /home/aux/projects/content-to-kb-automator/frontend && npx tsc --noEmit` — zero errors
|
||||
- `test -f frontend/src/api/highlights.ts && echo 'API layer exists'`
|
||||
- `test -f frontend/src/pages/HighlightQueue.tsx && echo 'Page exists'`
|
||||
- `test -f frontend/src/pages/HighlightQueue.module.css && echo 'CSS exists'`
|
||||
- `grep -q 'HighlightQueue' frontend/src/App.tsx && echo 'Route wired'`
|
||||
- `grep -q 'Highlights' frontend/src/pages/CreatorDashboard.tsx && echo 'Sidebar link added'`
|
||||
- Estimate: 2h
|
||||
- Files: frontend/src/api/highlights.ts, frontend/src/pages/HighlightQueue.tsx, frontend/src/pages/HighlightQueue.module.css, frontend/src/App.tsx, frontend/src/pages/CreatorDashboard.tsx
|
||||
- Verify: cd /home/aux/projects/content-to-kb-automator/frontend && npx tsc --noEmit && grep -q HighlightQueue src/App.tsx && grep -q Highlights src/pages/CreatorDashboard.tsx && echo 'All checks pass'
|
||||
|
|
|
|||
125
.gsd/milestones/M022/slices/S01/S01-RESEARCH.md
Normal file
125
.gsd/milestones/M022/slices/S01/S01-RESEARCH.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# S01 Research: Highlight Reel + Shorts Queue UI
|
||||
|
||||
## Summary
|
||||
|
||||
This slice builds a **creator-facing review queue** for highlight candidates and introduces a "shorts candidate" concept. The backend already has `HighlightCandidate` model, scoring engine, and admin-only API endpoints. The work is: (1) extend the backend with creator-scoped endpoints and a status action for approve/trim/discard, (2) build a new creator-facing page with queue UI, score visualization, and action controls, (3) introduce the "shorts candidate" concept (highlights with duration ≤60s are short-eligible).
|
||||
|
||||
**Depth: Targeted.** Known tech (React + FastAPI), moderate complexity in wiring existing models into a new creator-facing experience with trim/discard actions.
|
||||
|
||||
## Requirement Coverage
|
||||
|
||||
No new explicit requirements (R042+) exist for this slice. The slice is driven by the M022 roadmap vision: "Creator reviews auto-detected highlights and short candidates in a review queue — approve, trim, discard." This implicitly creates a new requirement surface.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Follow the **ChapterReview pattern** exactly — it's the closest analogue (creator-scoped, video-based review queue with status transitions, waveform, sidebar nav). Reuse `SidebarNav` from CreatorDashboard, CSS module approach, and `api/client.ts` patterns.
|
||||
|
||||
## Implementation Landscape
|
||||
|
||||
### What Exists (Backend)
|
||||
|
||||
| Component | Location | What it does |
|
||||
|-----------|----------|-------------|
|
||||
| `HighlightCandidate` model | `backend/models.py:706-741` | SQLAlchemy model with `id`, `key_moment_id`, `source_video_id`, `score`, `score_breakdown` (JSONB), `duration_secs`, `status` (candidate/approved/rejected), timestamps |
|
||||
| `HighlightStatus` enum | `backend/models.py:84-88` | `candidate`, `approved`, `rejected` — needs a `discarded` value if we want "discard" action distinct from "rejected" |
|
||||
| `highlight_scorer.py` | `backend/pipeline/highlight_scorer.py` | 7-dimension heuristic scorer: duration, content_density, technique_relevance, position, uniqueness, engagement_proxy, plugin_diversity |
|
||||
| `highlight_schemas.py` | `backend/pipeline/highlight_schemas.py` | Pydantic response schemas: `HighlightCandidateResponse`, `HighlightScoreBreakdown`, `HighlightBatchResult` |
|
||||
| Admin highlights router | `backend/routers/highlights.py` | Admin-only endpoints: `POST /admin/highlights/detect/{video_id}`, `POST /admin/highlights/detect-all`, `GET /admin/highlights/candidates`, `GET /admin/highlights/candidates/{id}` |
|
||||
| `stage_highlight_detection` | `backend/pipeline/stages.py:2444+` | Celery task that scores all KeyMoments for a video and upserts HighlightCandidates |
|
||||
| `KeyMoment` model | `backend/models.py` | Has `title`, `summary`, `start_time`, `end_time`, `content_type`, `plugins`, `raw_transcript`, `chapter_status`, `sort_order` |
|
||||
|
||||
### What Exists (Frontend)
|
||||
|
||||
| Component | Location | Relevance |
|
||||
|-----------|----------|-----------|
|
||||
| `ChapterReview.tsx` (525 lines) | `frontend/src/pages/ChapterReview.tsx` | **Primary pattern** — creator-scoped video review with WaveSurfer waveform, status badges, action buttons, sidebar nav |
|
||||
| `ChapterReview.module.css` (335 lines) | Same dir | CSS module pattern for creator review pages |
|
||||
| `SidebarNav` component | Exported from `CreatorDashboard.tsx` | Reusable sidebar with Dashboard/Chapters/Consent/Settings links — needs "Highlights" link added |
|
||||
| `CreatorDashboard.module.css` | Same dir | Shared layout/sidebar styles |
|
||||
| `api/videos.ts` | `frontend/src/api/videos.ts` | Pattern for chapter API calls — `fetchCreatorChapters`, `updateChapter`, `approveChapters` |
|
||||
| `api/client.ts` | `frontend/src/api/client.ts` | `request<T>()` helper with auth token injection |
|
||||
| `ProtectedRoute` | `frontend/src/components/ProtectedRoute.tsx` | Auth guard wrapper |
|
||||
| `App.tsx` routes | `frontend/src/App.tsx:172-209` | Route registration pattern — lazy-loaded creator pages under `/creator/*` |
|
||||
|
||||
### What's Missing
|
||||
|
||||
1. **Creator-scoped highlight API endpoints** — existing endpoints are admin-only at `/admin/highlights/*`. Need new `/creator/highlights` endpoints that scope to the authenticated creator's videos.
|
||||
|
||||
2. **Status transition endpoints** — no PATCH/PUT endpoint to change highlight status (approve/reject/discard). The admin router only has GET (list/detail) and POST (trigger detection).
|
||||
|
||||
3. **"Shorts" concept** — no model, no column, no flag anywhere. The roadmap mentions "short candidates" — this is a UI filter/tag on highlights where `duration_secs` falls within a shorts-eligible range (≤60s based on the scorer's sweet spot curve peaking at 30-60s).
|
||||
|
||||
4. **Trim action** — the roadmap says "approve, trim, discard." Trim implies adjusting `start_time`/`end_time` on the underlying KeyMoment or storing a trim window on the HighlightCandidate. The HighlightCandidate model has no trim columns. Options:
|
||||
- Add `trim_start` / `trim_end` nullable Float columns to `highlight_candidates` (preferred — non-destructive)
|
||||
- Or modify the KeyMoment times directly (destructive, bad idea)
|
||||
|
||||
5. **Frontend page** — no `HighlightQueue.tsx` or similar page exists.
|
||||
|
||||
6. **Sidebar nav link** — `SidebarNav` needs a "Highlights" entry.
|
||||
|
||||
### Key Constraints
|
||||
|
||||
- **Auth scoping:** Creator endpoints use the JWT auth from `backend/routers/auth.py` (D036). The `creator_chapters.py` router shows the pattern: `current_user = Depends(get_current_user)`, then filter by `creator_id = current_user.creator_id`.
|
||||
- **HighlightStatus enum:** Currently `candidate/approved/rejected`. Adding `discarded` requires an Alembic migration to alter the PostgreSQL enum. Alternatively, "discard" could map to "rejected" to avoid migration complexity — the UI just labels it differently.
|
||||
- **No waveform for highlights:** ChapterReview uses WaveSurfer with actual audio files. Highlights don't necessarily have audio access. The UI should show a visual timeline/score breakdown instead of a waveform, unless the video's audio URL is available (via `SourceVideo.file_path`).
|
||||
|
||||
## Natural Seams (Task Decomposition)
|
||||
|
||||
### Seam 1: Backend API (creator-scoped highlights + status actions)
|
||||
- New router: `backend/routers/creator_highlights.py`
|
||||
- Endpoints: `GET /creator/highlights` (list, filtered by creator), `GET /creator/highlights/{id}` (detail with KeyMoment info), `PATCH /creator/highlights/{id}` (status change: approve/reject/discard), `PATCH /creator/highlights/{id}/trim` (set trim_start/trim_end)
|
||||
- Alembic migration: add `trim_start`, `trim_end` columns to `highlight_candidates`
|
||||
- Register router in `backend/main.py`
|
||||
- Pattern: follow `backend/routers/creator_chapters.py` exactly
|
||||
|
||||
### Seam 2: Frontend API layer + types
|
||||
- New file: `frontend/src/api/highlights.ts`
|
||||
- Types: `HighlightCandidate`, `ScoreBreakdown`, list response
|
||||
- Functions: `fetchCreatorHighlights()`, `updateHighlightStatus()`, `trimHighlight()`
|
||||
- Pattern: follow `frontend/src/api/videos.ts`
|
||||
|
||||
### Seam 3: Frontend page — HighlightQueue.tsx
|
||||
- New file: `frontend/src/pages/HighlightQueue.tsx` + `.module.css`
|
||||
- Layout: `SidebarNav` + content area (same as ChapterReview)
|
||||
- Features:
|
||||
- Filter tabs: All / Shorts (≤60s) / Approved / Rejected
|
||||
- Candidate cards: title, duration, score bar, content_type badge, status badge
|
||||
- Score breakdown visualization (7 dimensions as small bar chart or radar)
|
||||
- Action buttons: Approve (green), Discard (red), Trim (opens inline trim UI)
|
||||
- Trim UI: two number inputs or range slider for start/end adjustment
|
||||
- Pattern: follow ChapterReview layout + CreatorDashboard sidebar
|
||||
|
||||
### Seam 4: Wiring (routes, sidebar, lazy loading)
|
||||
- Add route to `App.tsx`: `/creator/highlights` and `/creator/highlights/:videoId`
|
||||
- Add "Highlights" link to `SidebarNav` in `CreatorDashboard.tsx`
|
||||
- Lazy-load import
|
||||
|
||||
### Build Order
|
||||
1. **Backend first** (Seam 1) — unblocks everything, can be tested with curl
|
||||
2. **API layer** (Seam 2) — thin, quick
|
||||
3. **Page + CSS** (Seam 3) — bulk of the work
|
||||
4. **Wiring** (Seam 4) — can fold into Seam 3
|
||||
|
||||
### Verification
|
||||
- `docker compose build` succeeds
|
||||
- `cd frontend && npm run build` — zero TypeScript errors
|
||||
- `curl` to creator highlight endpoints returns scoped data
|
||||
- Visual check: navigate to `/creator/highlights`, see candidates listed with scores, filter by shorts, approve/discard/trim actions work
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| No highlight data exists in DB yet (detection may not have been run) | Backend detect-all endpoint exists; include a "Run Detection" button in the UI or document that admin must trigger first |
|
||||
| Trim columns need Alembic migration | Simple nullable Float columns, no complex migration |
|
||||
| HighlightStatus enum expansion for "discarded" | Map discard → rejected to avoid enum migration; or add Alembic ALTER TYPE if distinct status needed |
|
||||
|
||||
## Forward Intelligence for Planner
|
||||
|
||||
- **ChapterReview.tsx is THE pattern** — 525 lines, waveform + sidebar + status transitions + badges. Copy its structure for HighlightQueue.
|
||||
- **SidebarNav is exported** from `CreatorDashboard.tsx` (line ~12 export) — just add a link.
|
||||
- **CSS custom properties** already include `--color-btn-approve`, `--color-btn-reject`, `--color-badge-approved-*`, `--color-badge-rejected-*` — reuse them.
|
||||
- **Creator auth pattern** is in `creator_chapters.py` — `get_current_user` dependency → `current_user.creator_id` filter.
|
||||
- **The "shorts" concept is purely a UI filter** — any highlight with `duration_secs ≤ 60` is a "short candidate." No new model needed.
|
||||
- **Score breakdown JSONB** already has 7 named dimensions — render as horizontal bars or a small radar chart.
|
||||
- **Alembic migration numbering:** latest is `020_*`. Next migration is `021_*`.
|
||||
78
.gsd/milestones/M022/slices/S01/tasks/T01-PLAN.md
Normal file
78
.gsd/milestones/M022/slices/S01/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
estimated_steps: 39
|
||||
estimated_files: 4
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T01: Creator-scoped highlights API + Alembic migration
|
||||
|
||||
Build the backend foundation: a new FastAPI router with creator-scoped highlight endpoints, an Alembic migration for trim columns, and model updates. Follows the `creator_chapters.py` auth/ownership pattern exactly.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add `trim_start` (nullable Float) and `trim_end` (nullable Float) columns to `HighlightCandidate` in `backend/models.py` (after `status` field, before `created_at`)
|
||||
2. Create Alembic migration `alembic/versions/021_add_highlight_trim_columns.py` that adds `trim_start` and `trim_end` nullable float columns to `highlight_candidates` table
|
||||
3. Create `backend/routers/creator_highlights.py` with these endpoints:
|
||||
- `GET /creator/highlights` — list highlights for creator's videos, with optional `?status=` and `?shorts_only=true` (duration_secs ≤ 60) query filters. Eager-load `key_moment` relationship for title/start_time/end_time. Order by score DESC.
|
||||
- `GET /creator/highlights/{highlight_id}` — single highlight detail with full score_breakdown and key_moment info. Verify creator owns the source video.
|
||||
- `PATCH /creator/highlights/{highlight_id}` — update status (approve/reject). Accept `{"status": "approved"|"rejected"}` body. Verify creator ownership. Map UI "discard" to `rejected` status to avoid enum migration.
|
||||
- `PATCH /creator/highlights/{highlight_id}/trim` — set trim_start/trim_end. Accept `{"trim_start": float, "trim_end": float}` body. Validate: both non-negative, trim_start < trim_end. Verify creator ownership.
|
||||
4. Use Pydantic schemas for request/response: create `HighlightListResponse`, `HighlightDetailResponse`, `HighlightStatusUpdate`, `HighlightTrimUpdate` in the router file (or a separate schemas file). Reuse `HighlightScoreBreakdown` from `backend/pipeline/highlight_schemas.py`.
|
||||
5. Auth pattern: `current_user: User = Depends(get_current_user)`, then filter `SourceVideo.creator_id == current_user.creator_id`. Reuse `_verify_creator_owns_video` helper or create equivalent.
|
||||
6. Register the router in `backend/main.py`: add `creator_highlights` to the import line and `app.include_router(creator_highlights.router, prefix="/api/v1")`
|
||||
7. Add `chrysopedia.creator_highlights` logger with info-level messages on status changes and trim actions.
|
||||
|
||||
## Failure Modes
|
||||
|
||||
| Dependency | On error | On timeout | On malformed response |
|
||||
|------------|----------|-----------|----------------------|
|
||||
| PostgreSQL | Return 500 with logged traceback | SQLAlchemy handles connection pool timeout | N/A (ORM) |
|
||||
| Auth (JWT) | Return 401 via get_current_user | N/A | Return 401 |
|
||||
|
||||
## Negative Tests
|
||||
|
||||
- **Malformed inputs**: PATCH with invalid status string → 422; trim with negative values or trim_start >= trim_end → 400
|
||||
- **Error paths**: No creator profile → 403; highlight not found → 404; highlight belongs to different creator → 404
|
||||
- **Boundary conditions**: Empty highlight list returns `[]`; shorts_only with no qualifying highlights returns `[]`
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] `trim_start` and `trim_end` nullable Float columns on HighlightCandidate model
|
||||
- [ ] Alembic migration 021 applies cleanly
|
||||
- [ ] GET /creator/highlights returns creator-scoped results with status/shorts filters
|
||||
- [ ] GET /creator/highlights/{id} returns detail with score_breakdown and key_moment
|
||||
- [ ] PATCH /creator/highlights/{id} changes status with ownership verification
|
||||
- [ ] PATCH /creator/highlights/{id}/trim sets trim window with validation
|
||||
- [ ] Router registered in main.py
|
||||
|
||||
## Verification
|
||||
|
||||
- `cd /home/aux/projects/content-to-kb-automator && python -c "from routers.creator_highlights import router; print('Router imported OK')"` (run from backend dir)
|
||||
- `cd /home/aux/projects/content-to-kb-automator && python -c "from models import HighlightCandidate; assert hasattr(HighlightCandidate, 'trim_start'); assert hasattr(HighlightCandidate, 'trim_end'); print('Model columns OK')"`
|
||||
- `grep -q 'creator_highlights' backend/main.py && echo 'Router registered'`
|
||||
- `python -c "import ast; ast.parse(open('alembic/versions/021_add_highlight_trim_columns.py').read()); print('Migration syntax OK')"`
|
||||
|
||||
## Observability Impact
|
||||
|
||||
- Signals added: `chrysopedia.creator_highlights` logger with info-level status transition and trim action messages
|
||||
- Inspection: `GET /api/v1/creator/highlights?status=approved` to check approved highlights; DB query on `highlight_candidates` table
|
||||
- Failure state: 403/404 errors with specific detail messages logged at warning level
|
||||
|
||||
## Inputs
|
||||
|
||||
- ``backend/models.py` — HighlightCandidate model (lines 706-741) to add trim columns`
|
||||
- ``backend/routers/creator_chapters.py` — auth/ownership pattern to replicate`
|
||||
- ``backend/pipeline/highlight_schemas.py` — HighlightScoreBreakdown schema to reuse`
|
||||
- ``backend/main.py` — router registration pattern (line 15, 78-96)`
|
||||
- ``alembic/versions/020_add_chapter_status_and_sort_order.py` — latest migration for revision chain`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- ``backend/models.py` — HighlightCandidate with trim_start and trim_end columns`
|
||||
- ``alembic/versions/021_add_highlight_trim_columns.py` — migration adding trim columns`
|
||||
- ``backend/routers/creator_highlights.py` — new router with 4 endpoints`
|
||||
- ``backend/main.py` — updated with creator_highlights import and include_router`
|
||||
|
||||
## Verification
|
||||
|
||||
cd /home/aux/projects/content-to-kb-automator && python -c "import sys; sys.path.insert(0,'backend'); from models import HighlightCandidate; assert hasattr(HighlightCandidate,'trim_start'); print('OK')" && grep -q creator_highlights backend/main.py && echo 'Registered'
|
||||
83
.gsd/milestones/M022/slices/S01/tasks/T01-SUMMARY.md
Normal file
83
.gsd/milestones/M022/slices/S01/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
id: T01
|
||||
parent: S01
|
||||
milestone: M022
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["backend/models.py", "alembic/versions/021_add_highlight_trim_columns.py", "backend/routers/creator_highlights.py", "backend/main.py"]
|
||||
key_decisions: ["Pydantic schemas defined inline in the router file rather than in a separate schemas.py — keeps the creator_highlights module self-contained"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "All four verification checks pass: router imports OK, model columns present, router registered in main.py, migration syntax valid."
|
||||
completed_at: 2026-04-04T06:58:23.753Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Add creator-scoped highlight review endpoints (list/detail/status/trim) with Alembic migration for trim columns
|
||||
|
||||
> Add creator-scoped highlight review endpoints (list/detail/status/trim) with Alembic migration for trim columns
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T01
|
||||
parent: S01
|
||||
milestone: M022
|
||||
key_files:
|
||||
- backend/models.py
|
||||
- alembic/versions/021_add_highlight_trim_columns.py
|
||||
- backend/routers/creator_highlights.py
|
||||
- backend/main.py
|
||||
key_decisions:
|
||||
- Pydantic schemas defined inline in the router file rather than in a separate schemas.py — keeps the creator_highlights module self-contained
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-04-04T06:58:23.754Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Add creator-scoped highlight review endpoints (list/detail/status/trim) with Alembic migration for trim columns
|
||||
|
||||
**Add creator-scoped highlight review endpoints (list/detail/status/trim) with Alembic migration for trim columns**
|
||||
|
||||
## What Happened
|
||||
|
||||
Built the backend foundation for the highlight review queue. Added trim_start and trim_end nullable Float columns to HighlightCandidate model. Created Alembic migration 021 chained from 020. Created creator_highlights router with four endpoints: list (with status/shorts filters), detail (with score_breakdown), status update (approve/reject), and trim update (with validation). Registered router in main.py. Auth pattern follows creator_chapters.py exactly. Added chrysopedia.creator_highlights logger.
|
||||
|
||||
## Verification
|
||||
|
||||
All four verification checks pass: router imports OK, model columns present, router registered in main.py, migration syntax valid.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `python -c "from routers.creator_highlights import router; print('Router imported OK')"` | 0 | ✅ pass | 500ms |
|
||||
| 2 | `python -c "from models import HighlightCandidate; assert hasattr(HighlightCandidate, 'trim_start'); assert hasattr(HighlightCandidate, 'trim_end'); print('Model columns OK')"` | 0 | ✅ pass | 500ms |
|
||||
| 3 | `grep -q 'creator_highlights' backend/main.py && echo 'Router registered'` | 0 | ✅ pass | 100ms |
|
||||
| 4 | `python -c "import ast; ast.parse(open('alembic/versions/021_add_highlight_trim_columns.py').read()); print('Migration syntax OK')"` | 0 | ✅ pass | 100ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `backend/models.py`
|
||||
- `alembic/versions/021_add_highlight_trim_columns.py`
|
||||
- `backend/routers/creator_highlights.py`
|
||||
- `backend/main.py`
|
||||
|
||||
|
||||
## Deviations
|
||||
None.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
91
.gsd/milestones/M022/slices/S01/tasks/T02-PLAN.md
Normal file
91
.gsd/milestones/M022/slices/S01/tasks/T02-PLAN.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
estimated_steps: 51
|
||||
estimated_files: 5
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T02: HighlightQueue page, API layer, route wiring, and sidebar link
|
||||
|
||||
Build the complete frontend: TypeScript API layer, HighlightQueue page with filter tabs and action controls, CSS module, route registration in App.tsx, and Highlights link in SidebarNav. Follows the ChapterReview.tsx pattern for layout and CreatorDashboard SidebarNav for navigation.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `frontend/src/api/highlights.ts` with:
|
||||
- TypeScript interfaces: `HighlightCandidate` (id, key_moment_id, source_video_id, score, score_breakdown: ScoreBreakdown, duration_secs, status, trim_start, trim_end, created_at, key_moment: { title, start_time, end_time, content_type }), `ScoreBreakdown` (7 float fields matching backend), list response type
|
||||
- API functions: `fetchCreatorHighlights(params?: { status?: string; shorts_only?: boolean })`, `fetchHighlightDetail(id: string)`, `updateHighlightStatus(id: string, status: string)`, `trimHighlight(id: string, trim_start: number, trim_end: number)`
|
||||
- Use `request<T>()` and `BASE` from `./client`
|
||||
|
||||
2. Create `frontend/src/pages/HighlightQueue.module.css` following ChapterReview.module.css patterns:
|
||||
- Layout: `.pageLayout` (sidebar + content grid), `.content`, `.header`
|
||||
- Filter tabs: `.filterTabs`, `.filterTab`, `.filterTabActive`
|
||||
- Cards: `.candidateCard`, `.candidateScore`, `.candidateTitle`, `.candidateDuration`, `.candidateStatus`
|
||||
- Score breakdown bars: `.scoreBar`, `.scoreBarFill`, `.scoreLabel`
|
||||
- Action buttons: `.actionBtn`, `.approveBtn`, `.rejectBtn`, `.trimBtn` — reuse CSS custom properties `--color-btn-approve`, `--color-btn-reject`
|
||||
- Status badges: reuse `--color-badge-approved-*`, `--color-badge-rejected-*` custom property patterns
|
||||
- Trim UI: `.trimPanel`, `.trimInput`
|
||||
- Empty state: `.emptyState`
|
||||
- Shorts badge: `.shortsBadge` (small pill for ≤60s candidates)
|
||||
|
||||
3. Create `frontend/src/pages/HighlightQueue.tsx`:
|
||||
- Import `SidebarNav` from `./CreatorDashboard`
|
||||
- State: highlights array, loading boolean, active filter tab (all/shorts/approved/rejected), optional expanded highlight ID for trim UI
|
||||
- On mount: fetch highlights from API, apply filter based on active tab
|
||||
- Filter tabs: All | Shorts (≤60s) | Approved | Rejected — clicking a tab re-fetches with appropriate query params
|
||||
- Candidate cards showing: key_moment title, formatted duration, composite score as percentage bar, content_type badge, status badge
|
||||
- Score breakdown section: 7 horizontal bars (one per dimension) with labels and percentages
|
||||
- Action buttons per card: Approve (green, calls updateHighlightStatus), Discard (red, calls updateHighlightStatus with 'rejected'), Trim (toggles inline trim panel)
|
||||
- Trim panel: two number inputs (start seconds, end seconds) with Save/Cancel buttons. Pre-fill with current key_moment start_time/end_time if no trim exists. On save, call trimHighlight API.
|
||||
- Empty state message when no highlights match current filter
|
||||
- Loading spinner during fetch
|
||||
- Error handling: catch ApiError, show user-friendly message
|
||||
|
||||
4. Add lazy import and route in `frontend/src/App.tsx`:
|
||||
- Add `const HighlightQueue = React.lazy(() => import("./pages/HighlightQueue"));` with other lazy imports
|
||||
- Add route: `<Route path="/creator/highlights" element={<ProtectedRoute><Suspense fallback={<LoadingFallback />}><HighlightQueue /></Suspense></ProtectedRoute>} />`
|
||||
- Place after the existing `/creator/chapters/:videoId` route
|
||||
|
||||
5. Add "Highlights" link to `SidebarNav` in `frontend/src/pages/CreatorDashboard.tsx`:
|
||||
- Add a new NavLink entry with a star/sparkle SVG icon and text "Highlights" linking to `/creator/highlights`
|
||||
- Place after the "Chapters" link in the nav list
|
||||
|
||||
## Must-Haves
|
||||
|
||||
- [ ] TypeScript API layer with all 4 functions matching backend endpoints
|
||||
- [ ] HighlightQueue page renders with SidebarNav, filter tabs, candidate cards, score bars, and action buttons
|
||||
- [ ] Filter tabs switch between All / Shorts / Approved / Rejected views
|
||||
- [ ] Approve and Discard buttons call the status update API and refresh the list
|
||||
- [ ] Trim panel opens inline with number inputs, validates, and calls trim API
|
||||
- [ ] Route registered in App.tsx with ProtectedRoute and Suspense
|
||||
- [ ] SidebarNav has "Highlights" link
|
||||
- [ ] `npx tsc --noEmit` passes with zero errors
|
||||
|
||||
## Verification
|
||||
|
||||
- `cd /home/aux/projects/content-to-kb-automator/frontend && npx tsc --noEmit` — zero errors
|
||||
- `test -f frontend/src/api/highlights.ts && echo 'API layer exists'`
|
||||
- `test -f frontend/src/pages/HighlightQueue.tsx && echo 'Page exists'`
|
||||
- `test -f frontend/src/pages/HighlightQueue.module.css && echo 'CSS exists'`
|
||||
- `grep -q 'HighlightQueue' frontend/src/App.tsx && echo 'Route wired'`
|
||||
- `grep -q 'Highlights' frontend/src/pages/CreatorDashboard.tsx && echo 'Sidebar link added'`
|
||||
|
||||
## Inputs
|
||||
|
||||
- ``backend/routers/creator_highlights.py` — API contract (endpoint paths, request/response shapes) from T01`
|
||||
- ``frontend/src/api/client.ts` — request helper and BASE constant`
|
||||
- ``frontend/src/api/videos.ts` — API function pattern to follow`
|
||||
- ``frontend/src/pages/ChapterReview.tsx` — page layout pattern (SidebarNav + content, status badges, action buttons)`
|
||||
- ``frontend/src/pages/ChapterReview.module.css` — CSS module pattern for creator review pages`
|
||||
- ``frontend/src/pages/CreatorDashboard.tsx` — SidebarNav component to add Highlights link (lines 12-52)`
|
||||
- ``frontend/src/App.tsx` — lazy import and route registration pattern (lines 13-25, 201-205)`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- ``frontend/src/api/highlights.ts` — TypeScript API layer with 4 functions and interfaces`
|
||||
- ``frontend/src/pages/HighlightQueue.tsx` — highlight queue page component`
|
||||
- ``frontend/src/pages/HighlightQueue.module.css` — CSS module for the page`
|
||||
- ``frontend/src/App.tsx` — updated with HighlightQueue lazy import and route`
|
||||
- ``frontend/src/pages/CreatorDashboard.tsx` — updated SidebarNav with Highlights link`
|
||||
|
||||
## Verification
|
||||
|
||||
cd /home/aux/projects/content-to-kb-automator/frontend && npx tsc --noEmit && grep -q HighlightQueue src/App.tsx && grep -q Highlights src/pages/CreatorDashboard.tsx && echo 'All checks pass'
|
||||
14742
.gsd/reports/M021-2026-04-04T06-50-37.html
Normal file
14742
.gsd/reports/M021-2026-04-04T06-50-37.html
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -130,7 +130,7 @@ footer{border-top:1px solid var(--border-1);padding:16px 32px}
|
|||
</div>
|
||||
<div class="hdr-right">
|
||||
<span class="gen-lbl">Updated</span>
|
||||
<span class="gen">Apr 3, 2026, 11:30 PM</span>
|
||||
<span class="gen">Apr 4, 2026, 06:50 AM</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -156,6 +156,10 @@ footer{border-top:1px solid var(--border-1);padding:16px 32px}
|
|||
<div class="toc-group-label">M019</div>
|
||||
<ul><li><a href="M019-2026-04-03T23-30-16.html">Apr 3, 2026, 11:30 PM</a> <span class="toc-kind toc-milestone">milestone</span></li></ul>
|
||||
</div>
|
||||
<div class="toc-group">
|
||||
<div class="toc-group-label">M021</div>
|
||||
<ul><li><a href="M021-2026-04-04T06-50-37.html">Apr 4, 2026, 06:50 AM</a> <span class="toc-kind toc-milestone">milestone</span></li></ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
|
|
@ -164,41 +168,43 @@ footer{border-top:1px solid var(--border-1);padding:16px 32px}
|
|||
<h2>Project Overview</h2>
|
||||
|
||||
<div class="idx-summary">
|
||||
<div class="idx-stat"><span class="idx-val">$411.26</span><span class="idx-lbl">Total Cost</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">584.83M</span><span class="idx-lbl">Total Tokens</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">17h 48m</span><span class="idx-lbl">Duration</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">79/123</span><span class="idx-lbl">Slices</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">19/25</span><span class="idx-lbl">Milestones</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">4</span><span class="idx-lbl">Reports</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">$485.08</span><span class="idx-lbl">Total Cost</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">685.34M</span><span class="idx-lbl">Total Tokens</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">21h 0m</span><span class="idx-lbl">Duration</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">94/123</span><span class="idx-lbl">Slices</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">21/25</span><span class="idx-lbl">Milestones</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">5</span><span class="idx-lbl">Reports</span></div>
|
||||
</div>
|
||||
<div class="idx-progress">
|
||||
<div class="idx-bar-track"><div class="idx-bar-fill" style="width:64%"></div></div>
|
||||
<span class="idx-pct">64% complete</span>
|
||||
<div class="idx-bar-track"><div class="idx-bar-fill" style="width:76%"></div></div>
|
||||
<span class="idx-pct">76% complete</span>
|
||||
</div>
|
||||
<div class="sparkline-wrap"><h3>Cost Progression</h3>
|
||||
<div class="sparkline">
|
||||
<svg viewBox="0 0 600 60" width="600" height="60" class="spark-svg">
|
||||
<polyline points="12.0,32.9 204.0,32.2 396.0,16.0 588.0,12.0" class="spark-line" fill="none"/>
|
||||
<circle cx="12.0" cy="32.9" r="3" class="spark-dot">
|
||||
<polyline points="12.0,35.2 156.0,34.6 300.0,20.9 444.0,17.5 588.0,12.0" class="spark-line" fill="none"/>
|
||||
<circle cx="12.0" cy="35.2" r="3" class="spark-dot">
|
||||
<title>M008: M008: Credibility Debt Cleanup — Broken Links, Test Data, Jargon, Empty Metrics — $172.23</title>
|
||||
</circle><circle cx="204.0" cy="32.2" r="3" class="spark-dot">
|
||||
</circle><circle cx="156.0" cy="34.6" r="3" class="spark-dot">
|
||||
<title>M009: Homepage & First Impression — $180.97</title>
|
||||
</circle><circle cx="396.0" cy="16.0" r="3" class="spark-dot">
|
||||
</circle><circle cx="300.0" cy="20.9" r="3" class="spark-dot">
|
||||
<title>M018: M018: Phase 2 Research & Documentation — Site Audit and Forgejo Wiki Bootstrap — $365.18</title>
|
||||
</circle><circle cx="588.0" cy="12.0" r="3" class="spark-dot">
|
||||
</circle><circle cx="444.0" cy="17.5" r="3" class="spark-dot">
|
||||
<title>M019: Foundations — Auth, Consent & LightRAG — $411.26</title>
|
||||
</circle><circle cx="588.0" cy="12.0" r="3" class="spark-dot">
|
||||
<title>M021: Intelligence Online — Chat, Chapters & Search Cutover — $485.08</title>
|
||||
</circle>
|
||||
<text x="12" y="58" class="spark-lbl">$172.23</text>
|
||||
<text x="588" y="58" text-anchor="end" class="spark-lbl">$411.26</text>
|
||||
<text x="588" y="58" text-anchor="end" class="spark-lbl">$485.08</text>
|
||||
</svg>
|
||||
<div class="spark-axis">
|
||||
<span class="spark-tick" style="left:2.0%" title="2026-03-31T05:31:26.249Z">M008</span><span class="spark-tick" style="left:34.0%" title="2026-03-31T05:52:28.456Z">M009</span><span class="spark-tick" style="left:66.0%" title="2026-04-03T21:17:51.201Z">M018</span><span class="spark-tick" style="left:98.0%" title="2026-04-03T23:30:16.641Z">M019</span>
|
||||
<span class="spark-tick" style="left:2.0%" title="2026-03-31T05:31:26.249Z">M008</span><span class="spark-tick" style="left:26.0%" title="2026-03-31T05:52:28.456Z">M009</span><span class="spark-tick" style="left:50.0%" title="2026-04-03T21:17:51.201Z">M018</span><span class="spark-tick" style="left:74.0%" title="2026-04-03T23:30:16.641Z">M019</span><span class="spark-tick" style="left:98.0%" title="2026-04-04T06:50:37.759Z">M021</span>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<section class="idx-cards">
|
||||
<h2>Progression <span class="sec-count">4</span></h2>
|
||||
<h2>Progression <span class="sec-count">5</span></h2>
|
||||
<div class="cards-grid">
|
||||
<a class="report-card" href="M008-2026-03-31T05-31-26.html">
|
||||
<div class="card-top">
|
||||
|
|
@ -263,7 +269,7 @@ footer{border-top:1px solid var(--border-1);padding:16px 32px}
|
|||
<div class="card-delta"><span>+$184.21</span><span>+38 slices</span><span>+9 milestones</span></div>
|
||||
|
||||
</a>
|
||||
<a class="report-card card-latest" href="M019-2026-04-03T23-30-16.html">
|
||||
<a class="report-card" href="M019-2026-04-03T23-30-16.html">
|
||||
<div class="card-top">
|
||||
<span class="card-label">M019: Foundations — Auth, Consent & LightRAG</span>
|
||||
<span class="card-kind card-kind-milestone">milestone</span>
|
||||
|
|
@ -282,6 +288,27 @@ footer{border-top:1px solid var(--border-1);padding:16px 32px}
|
|||
<span>79/123 slices</span>
|
||||
</div>
|
||||
<div class="card-delta"><span>+$46.09</span><span>+6 slices</span><span>+1 milestone</span></div>
|
||||
|
||||
</a>
|
||||
<a class="report-card card-latest" href="M021-2026-04-04T06-50-37.html">
|
||||
<div class="card-top">
|
||||
<span class="card-label">M021: Intelligence Online — Chat, Chapters & Search Cutover</span>
|
||||
<span class="card-kind card-kind-milestone">milestone</span>
|
||||
</div>
|
||||
<div class="card-date">Apr 4, 2026, 06:50 AM</div>
|
||||
<div class="card-progress">
|
||||
<div class="card-bar-track">
|
||||
<div class="card-bar-fill" style="width:76%"></div>
|
||||
</div>
|
||||
<span class="card-pct">76%</span>
|
||||
</div>
|
||||
<div class="card-stats">
|
||||
<span>$485.08</span>
|
||||
<span>685.34M</span>
|
||||
<span>21h 0m</span>
|
||||
<span>94/123 slices</span>
|
||||
</div>
|
||||
<div class="card-delta"><span>+$73.82</span><span>+15 slices</span><span>+2 milestones</span></div>
|
||||
<div class="card-latest-badge">Latest</div>
|
||||
</a></div>
|
||||
</section>
|
||||
|
|
@ -296,7 +323,7 @@ footer{border-top:1px solid var(--border-1);padding:16px 32px}
|
|||
<span class="ftr-sep">—</span>
|
||||
<span>/home/aux/projects/content-to-kb-automator</span>
|
||||
<span class="ftr-sep">—</span>
|
||||
<span>Updated Apr 3, 2026, 11:30 PM</span>
|
||||
<span>Updated Apr 4, 2026, 06:50 AM</span>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,22 @@
|
|||
"doneMilestones": 19,
|
||||
"totalMilestones": 25,
|
||||
"phase": "planning"
|
||||
},
|
||||
{
|
||||
"filename": "M021-2026-04-04T06-50-37.html",
|
||||
"generatedAt": "2026-04-04T06:50:37.759Z",
|
||||
"milestoneId": "M021",
|
||||
"milestoneTitle": "Intelligence Online — Chat, Chapters & Search Cutover",
|
||||
"label": "M021: Intelligence Online — Chat, Chapters & Search Cutover",
|
||||
"kind": "milestone",
|
||||
"totalCost": 485.0819890000003,
|
||||
"totalTokens": 685341099,
|
||||
"totalDuration": 75620579,
|
||||
"doneSlices": 94,
|
||||
"totalSlices": 123,
|
||||
"doneMilestones": 21,
|
||||
"totalMilestones": 25,
|
||||
"phase": "planning"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
24
alembic/versions/021_add_highlight_trim_columns.py
Normal file
24
alembic/versions/021_add_highlight_trim_columns.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""Add trim_start and trim_end columns to highlight_candidates.
|
||||
|
||||
Revision ID: 021_add_highlight_trim_columns
|
||||
Revises: 020_add_chapter_status_and_sort_order
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "021_add_highlight_trim_columns"
|
||||
down_revision = "020_add_chapter_status_and_sort_order"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("highlight_candidates", sa.Column("trim_start", sa.Float(), nullable=True))
|
||||
op.add_column("highlight_candidates", sa.Column("trim_end", sa.Float(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("highlight_candidates", "trim_end")
|
||||
op.drop_column("highlight_candidates", "trim_start")
|
||||
|
|
@ -12,7 +12,7 @@ from fastapi import FastAPI
|
|||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from config import get_settings
|
||||
from routers import admin, auth, chat, consent, creator_chapters, creator_dashboard, creators, health, highlights, ingest, pipeline, reports, search, stats, techniques, topics, videos
|
||||
from routers import admin, auth, chat, consent, creator_chapters, creator_dashboard, creator_highlights, creators, health, highlights, ingest, pipeline, reports, search, stats, techniques, topics, videos
|
||||
|
||||
|
||||
def _setup_logging() -> None:
|
||||
|
|
@ -84,6 +84,7 @@ app.include_router(chat.router, prefix="/api/v1")
|
|||
app.include_router(consent.router, prefix="/api/v1")
|
||||
app.include_router(creator_dashboard.router, prefix="/api/v1")
|
||||
app.include_router(creator_chapters.router, prefix="/api/v1")
|
||||
app.include_router(creator_highlights.router, prefix="/api/v1")
|
||||
app.include_router(creators.router, prefix="/api/v1")
|
||||
app.include_router(highlights.router, prefix="/api/v1")
|
||||
app.include_router(ingest.router, prefix="/api/v1")
|
||||
|
|
|
|||
|
|
@ -725,6 +725,8 @@ class HighlightCandidate(Base):
|
|||
default=HighlightStatus.candidate,
|
||||
server_default="candidate",
|
||||
)
|
||||
trim_start: Mapped[float | None] = mapped_column(Float, nullable=True, default=None)
|
||||
trim_end: Mapped[float | None] = mapped_column(Float, nullable=True, default=None)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
default=_now, server_default=func.now()
|
||||
)
|
||||
|
|
|
|||
230
backend/routers/creator_highlights.py
Normal file
230
backend/routers/creator_highlights.py
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
"""Creator highlight management endpoints — review, trim, approve/reject highlights.
|
||||
|
||||
Auth-guarded endpoints for creators to manage auto-detected highlight
|
||||
candidates for their videos.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import 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 HighlightCandidate, HighlightStatus, KeyMoment, SourceVideo, User
|
||||
from pipeline.highlight_schemas import HighlightScoreBreakdown
|
||||
|
||||
logger = logging.getLogger("chrysopedia.creator_highlights")
|
||||
|
||||
router = APIRouter(prefix="/creator", tags=["creator-highlights"])
|
||||
|
||||
|
||||
# ── Pydantic Schemas ─────────────────────────────────────────────────────────
|
||||
|
||||
class KeyMomentInfo(BaseModel):
|
||||
"""Embedded key-moment info for highlight responses."""
|
||||
id: uuid.UUID
|
||||
title: str
|
||||
start_time: float
|
||||
end_time: float
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class HighlightListItem(BaseModel):
|
||||
"""Single item in the highlights list response."""
|
||||
id: uuid.UUID
|
||||
key_moment_id: uuid.UUID
|
||||
source_video_id: uuid.UUID
|
||||
score: float
|
||||
duration_secs: float
|
||||
status: str
|
||||
trim_start: float | None = None
|
||||
trim_end: float | None = None
|
||||
key_moment: KeyMomentInfo | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class HighlightListResponse(BaseModel):
|
||||
"""Response for GET /creator/highlights."""
|
||||
highlights: list[HighlightListItem]
|
||||
|
||||
|
||||
class HighlightDetailResponse(BaseModel):
|
||||
"""Response for GET /creator/highlights/{id} with full breakdown."""
|
||||
id: uuid.UUID
|
||||
key_moment_id: uuid.UUID
|
||||
source_video_id: uuid.UUID
|
||||
score: float
|
||||
score_breakdown: HighlightScoreBreakdown | None = None
|
||||
duration_secs: float
|
||||
status: str
|
||||
trim_start: float | None = None
|
||||
trim_end: float | None = None
|
||||
key_moment: KeyMomentInfo | None = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class HighlightStatusUpdate(BaseModel):
|
||||
"""Request body for PATCH status."""
|
||||
status: str = Field(..., pattern="^(approved|rejected)$", description="New status: approved or rejected")
|
||||
|
||||
|
||||
class HighlightTrimUpdate(BaseModel):
|
||||
"""Request body for PATCH trim."""
|
||||
trim_start: float = Field(..., ge=0.0, description="Trim start in seconds")
|
||||
trim_end: float = Field(..., ge=0.0, description="Trim end in seconds")
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async def _verify_creator(current_user: User) -> uuid.UUID:
|
||||
"""Return creator_id or raise 403."""
|
||||
if current_user.creator_id is None:
|
||||
raise HTTPException(status_code=403, detail="No creator profile linked")
|
||||
return current_user.creator_id
|
||||
|
||||
|
||||
async def _get_highlight_for_creator(
|
||||
highlight_id: uuid.UUID,
|
||||
creator_id: uuid.UUID,
|
||||
db: AsyncSession,
|
||||
*,
|
||||
eager_key_moment: bool = False,
|
||||
) -> HighlightCandidate:
|
||||
"""Fetch highlight and verify creator ownership via source video."""
|
||||
stmt = select(HighlightCandidate).where(HighlightCandidate.id == highlight_id)
|
||||
if eager_key_moment:
|
||||
stmt = stmt.options(selectinload(HighlightCandidate.key_moment))
|
||||
highlight = (await db.execute(stmt)).scalar_one_or_none()
|
||||
if highlight is None:
|
||||
raise HTTPException(status_code=404, detail="Highlight not found")
|
||||
|
||||
# Verify ownership via source video
|
||||
video = (await db.execute(
|
||||
select(SourceVideo).where(
|
||||
SourceVideo.id == highlight.source_video_id,
|
||||
SourceVideo.creator_id == creator_id,
|
||||
)
|
||||
)).scalar_one_or_none()
|
||||
if video is None:
|
||||
raise HTTPException(status_code=404, detail="Highlight not found or not owned by you")
|
||||
|
||||
return highlight
|
||||
|
||||
|
||||
# ── Endpoints ────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/highlights", response_model=HighlightListResponse)
|
||||
async def list_creator_highlights(
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: AsyncSession = Depends(get_session),
|
||||
status: str | None = Query(None, description="Filter by status: candidate, approved, rejected"),
|
||||
shorts_only: bool = Query(False, description="Only show highlights ≤ 60s"),
|
||||
) -> HighlightListResponse:
|
||||
"""List highlights for the creator's videos with optional filters."""
|
||||
creator_id = await _verify_creator(current_user)
|
||||
|
||||
# Get all video IDs for this creator
|
||||
video_ids_stmt = select(SourceVideo.id).where(SourceVideo.creator_id == creator_id)
|
||||
|
||||
stmt = (
|
||||
select(HighlightCandidate)
|
||||
.where(HighlightCandidate.source_video_id.in_(video_ids_stmt))
|
||||
.options(selectinload(HighlightCandidate.key_moment))
|
||||
.order_by(HighlightCandidate.score.desc())
|
||||
)
|
||||
|
||||
if status is not None:
|
||||
try:
|
||||
HighlightStatus(status)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=422, detail=f"Invalid status: {status}")
|
||||
stmt = stmt.where(HighlightCandidate.status == status)
|
||||
|
||||
if shorts_only:
|
||||
stmt = stmt.where(HighlightCandidate.duration_secs <= 60.0)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
highlights = result.scalars().all()
|
||||
logger.debug("Creator %s highlights: %d results", creator_id, len(highlights))
|
||||
return HighlightListResponse(
|
||||
highlights=[HighlightListItem.model_validate(h) for h in highlights],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/highlights/{highlight_id}", response_model=HighlightDetailResponse)
|
||||
async def get_creator_highlight(
|
||||
highlight_id: uuid.UUID,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: AsyncSession = Depends(get_session),
|
||||
) -> HighlightDetailResponse:
|
||||
"""Get full detail for a single highlight including score breakdown."""
|
||||
creator_id = await _verify_creator(current_user)
|
||||
highlight = await _get_highlight_for_creator(
|
||||
highlight_id, creator_id, db, eager_key_moment=True,
|
||||
)
|
||||
return HighlightDetailResponse.model_validate(highlight)
|
||||
|
||||
|
||||
@router.patch("/highlights/{highlight_id}", response_model=HighlightDetailResponse)
|
||||
async def update_highlight_status(
|
||||
highlight_id: uuid.UUID,
|
||||
body: HighlightStatusUpdate,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: AsyncSession = Depends(get_session),
|
||||
) -> HighlightDetailResponse:
|
||||
"""Approve or reject a highlight. Maps UI 'discard' to 'rejected'."""
|
||||
creator_id = await _verify_creator(current_user)
|
||||
highlight = await _get_highlight_for_creator(
|
||||
highlight_id, creator_id, db, eager_key_moment=True,
|
||||
)
|
||||
|
||||
old_status = highlight.status
|
||||
highlight.status = HighlightStatus(body.status)
|
||||
await db.commit()
|
||||
await db.refresh(highlight)
|
||||
logger.info(
|
||||
"Highlight %s status: %s → %s (creator %s)",
|
||||
highlight_id, old_status.value, body.status, creator_id,
|
||||
)
|
||||
return HighlightDetailResponse.model_validate(highlight)
|
||||
|
||||
|
||||
@router.patch("/highlights/{highlight_id}/trim", response_model=HighlightDetailResponse)
|
||||
async def update_highlight_trim(
|
||||
highlight_id: uuid.UUID,
|
||||
body: HighlightTrimUpdate,
|
||||
current_user: Annotated[User, Depends(get_current_user)],
|
||||
db: AsyncSession = Depends(get_session),
|
||||
) -> HighlightDetailResponse:
|
||||
"""Set trim start/end for a highlight clip."""
|
||||
creator_id = await _verify_creator(current_user)
|
||||
|
||||
# Validate trim_start < trim_end
|
||||
if body.trim_start >= body.trim_end:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="trim_start must be less than trim_end",
|
||||
)
|
||||
|
||||
highlight = await _get_highlight_for_creator(
|
||||
highlight_id, creator_id, db, eager_key_moment=True,
|
||||
)
|
||||
|
||||
highlight.trim_start = body.trim_start
|
||||
highlight.trim_end = body.trim_end
|
||||
await db.commit()
|
||||
await db.refresh(highlight)
|
||||
logger.info(
|
||||
"Highlight %s trim: %.2f–%.2f (creator %s)",
|
||||
highlight_id, body.trim_start, body.trim_end, creator_id,
|
||||
)
|
||||
return HighlightDetailResponse.model_validate(highlight)
|
||||
Loading…
Add table
Reference in a new issue