M021: Chat engine, retrieval cascade, highlights, audio mode, chapters, impersonation write mode docs
parent
be05f5edf2
commit
eec99b6c7d
13 changed files with 533 additions and 7 deletions
|
|
@ -8,7 +8,7 @@
|
||||||
|--------|------|---------------|-------|
|
|--------|------|---------------|-------|
|
||||||
| GET | `/health` | `{status, service, version, database}` | Health check |
|
| GET | `/health` | `{status, service, version, database}` | Health check |
|
||||||
| GET | `/api/v1/stats` | `{technique_count, creator_count}` | Homepage stats |
|
| GET | `/api/v1/stats` | `{technique_count, creator_count}` | Homepage stats |
|
||||||
| GET | `/api/v1/search?q=` | `{items, partial_matches, total, query, fallback_used}` | Semantic + keyword fallback (D009) |
|
| GET | `/api/v1/search?q=&creator=` | `{items, partial_matches, total, query, fallback_used, cascade_tier}` | LightRAG primary + Qdrant fallback, optional creator cascade (D039, D040) |
|
||||||
| GET | `/api/v1/search/suggestions?q=` | `{suggestions: [{text, type}]}` | Typeahead autocomplete |
|
| GET | `/api/v1/search/suggestions?q=` | `{suggestions: [{text, type}]}` | Typeahead autocomplete |
|
||||||
| GET | `/api/v1/search/popular` | `{items: [{query, count}]}` | Popular searches (D025) |
|
| GET | `/api/v1/search/popular` | `{items: [{query, count}]}` | Popular searches (D025) |
|
||||||
| GET | `/api/v1/techniques?limit=&offset=` | `{items, total, offset, limit}` | Paginated technique list |
|
| GET | `/api/v1/techniques?limit=&offset=` | `{items, total, offset, limit}` | Paginated technique list |
|
||||||
|
|
@ -139,3 +139,50 @@ JWT-based authentication added in M019. See [[Authentication]] for full details.
|
||||||
---
|
---
|
||||||
|
|
||||||
*See also: [[Architecture]], [[Data-Model]], [[Frontend]], [[Authentication]]*
|
*See also: [[Architecture]], [[Data-Model]], [[Frontend]], [[Authentication]]*
|
||||||
|
utput` | Delete all pipeline output |
|
||||||
|
| POST | `/admin/pipeline/optimize-prompt` | Trigger prompt optimization |
|
||||||
|
| POST | `/admin/pipeline/reindex-all` | Rebuild Qdrant index |
|
||||||
|
| GET | `/admin/pipeline/worker-status` | Celery worker health |
|
||||||
|
| GET | `/admin/pipeline/recent-activity` | Recent pipeline events |
|
||||||
|
| POST | `/admin/pipeline/creator-profile/{creator_id}` | Update creator profile |
|
||||||
|
| POST | `/admin/pipeline/avatar-fetch/{creator_id}` | Fetch creator avatar |
|
||||||
|
|
||||||
|
## Other Endpoints (2)
|
||||||
|
|
||||||
|
| Method | Path | Notes |
|
||||||
|
|--------|------|-------|
|
||||||
|
| POST | `/api/v1/ingest` | Transcript upload |
|
||||||
|
| GET | `/api/v1/videos` | ⚠️ Bare list (not paginated) |
|
||||||
|
|
||||||
|
## Response Conventions
|
||||||
|
|
||||||
|
**Standard paginated response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [...],
|
||||||
|
"total": 83,
|
||||||
|
"offset": 0,
|
||||||
|
"limit": 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Known inconsistencies:**
|
||||||
|
- `GET /topics` returns bare list instead of paginated dict
|
||||||
|
- `GET /videos` returns bare list instead of paginated dict
|
||||||
|
- Search uses `items` key (not `results`)
|
||||||
|
- `/techniques/random` returns JSON `{slug}` (not HTTP redirect)
|
||||||
|
|
||||||
|
**New endpoints should follow the `{items, total, offset, limit}` paginated pattern.**
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
JWT-based authentication added in M019. See [[Authentication]] for full details.
|
||||||
|
|
||||||
|
- **Public endpoints** (search, browse, techniques) require no auth
|
||||||
|
- **Auth endpoints** (`/auth/register`, `/auth/login`) are open; `/auth/me` requires Bearer JWT
|
||||||
|
- **Consent endpoints** require Bearer JWT with ownership verification (creator must own the video, or be admin)
|
||||||
|
- **Admin endpoints** (`/admin/*`) are accessible to anyone with network access (auth planned for future milestone)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See also: [[Architecture]], [[Data-Model]], [[Frontend]], [[Authentication]]*
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## System Overview
|
## System Overview
|
||||||
|
|
||||||
Chrysopedia is a self-hosted music production knowledge base that synthesizes technique articles from video transcripts using a 6-stage LLM pipeline. It runs as a Docker Compose stack on `ub01` with 11 containers.
|
Chrysopedia is a self-hosted music production knowledge base that synthesizes technique articles from video transcripts using a 7-stage LLM pipeline. It runs as a Docker Compose stack on `ub01` with 11 containers.
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
|
@ -94,3 +94,11 @@ Chrysopedia is a self-hosted music production knowledge base that synthesizes te
|
||||||
---
|
---
|
||||||
|
|
||||||
*See also: [[Deployment]], [[Pipeline]], [[Data-Model]], [[Authentication]]*
|
*See also: [[Deployment]], [[Pipeline]], [[Data-Model]], [[Authentication]]*
|
||||||
|
→ PostgreSQL, embeddings → Qdrant
|
||||||
|
5. **Serving:** React SPA fetches from FastAPI, search queries hit Qdrant then PostgreSQL fallback
|
||||||
|
6. **Auth:** JWT-protected endpoints for creator consent management and admin features (see [[Authentication]])
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See also: [[Deployment]], [[Pipeline]], [[Data-Model]], [[Authentication]]*
|
||||||
|
], [[Authentication]]*
|
||||||
|
|
|
||||||
115
Chat-Engine.md
Normal file
115
Chat-Engine.md
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
# Chat Engine
|
||||||
|
|
||||||
|
Streaming question-answering interface backed by LightRAG retrieval and LLM completion. Added in M021/S03.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
User types question in ChatPage
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POST /api/v1/chat { query: "...", creator?: "..." }
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ChatService.stream(query, creator?)
|
||||||
|
│
|
||||||
|
├─ 1. Retrieve: SearchService.search(query, creator)
|
||||||
|
│ └─ Uses 4-tier cascade if creator provided (see [[Search-Retrieval]])
|
||||||
|
│
|
||||||
|
├─ 2. Prompt: Assemble numbered context block into encyclopedic system prompt
|
||||||
|
│ └─ Sources formatted as [1] Title — Summary for citation mapping
|
||||||
|
│
|
||||||
|
├─ 3. Stream: openai.AsyncOpenAI with stream=True
|
||||||
|
│ └─ Tokens streamed as SSE events in real-time
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
SSE response → ChatPage renders tokens + citation links
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSE Protocol
|
||||||
|
|
||||||
|
The chat endpoint returns a `text/event-stream` response with four event types in strict order:
|
||||||
|
|
||||||
|
| Event | Payload | When |
|
||||||
|
|-------|---------|------|
|
||||||
|
| `sources` | `[{title, slug, creator_name, summary}]` | First — citation metadata for link rendering |
|
||||||
|
| `token` | `string` (text chunk) | Repeated — streamed LLM completion tokens |
|
||||||
|
| `done` | `{cascade_tier: "creator"\|"domain"\|"global"\|"none"\|""}` | Once — signals completion, includes which retrieval tier answered |
|
||||||
|
| `error` | `{message: string}` | On failure — emitted if LLM errors mid-stream |
|
||||||
|
|
||||||
|
The `cascade_tier` in the `done` event reveals which tier of the retrieval cascade served the context (see [[Search-Retrieval]]).
|
||||||
|
|
||||||
|
## Citation Format
|
||||||
|
|
||||||
|
The LLM is instructed to reference sources using numbered citations `[N]` in its response. The frontend parses these into superscript links:
|
||||||
|
|
||||||
|
- `[1]` → links to `/techniques/:slug` for the corresponding source
|
||||||
|
- Multiple citations supported: `[1][3]` or `[1,3]`
|
||||||
|
- Citation regex: `/\[(\d+)\]/g` parsed locally in ChatPage
|
||||||
|
|
||||||
|
## API Endpoint
|
||||||
|
|
||||||
|
### POST /api/v1/chat
|
||||||
|
|
||||||
|
| Field | Type | Required | Validation |
|
||||||
|
|-------|------|----------|------------|
|
||||||
|
| `query` | string | Yes | 1–1000 characters |
|
||||||
|
| `creator` | string | No | Creator UUID or slug for scoped retrieval |
|
||||||
|
|
||||||
|
**Response:** `text/event-stream` (SSE)
|
||||||
|
|
||||||
|
**Error responses:**
|
||||||
|
- `422` — Empty or missing query, query exceeds 1000 chars
|
||||||
|
|
||||||
|
## Backend: ChatService
|
||||||
|
|
||||||
|
Located in `backend/chat_service.py`. The retrieve-prompt-stream pipeline:
|
||||||
|
|
||||||
|
1. **Retrieve** — Calls `SearchService.search()` with the query and optional creator parameter. Gets back ranked technique page results with the cascade_tier.
|
||||||
|
2. **Prompt** — Builds a numbered context block from search results. System prompt instructs the LLM to act as a music production encyclopedia, cite sources with `[N]` notation, and stay grounded in the provided context.
|
||||||
|
3. **Stream** — Opens an async streaming completion via `openai.AsyncOpenAI` (configured to point at DGX Sparks Qwen or local Ollama). Yields SSE events as tokens arrive.
|
||||||
|
|
||||||
|
Error handling: If the LLM fails mid-stream (after some tokens have been sent), an `error` event is emitted so the frontend can display a failure message rather than leaving the response hanging.
|
||||||
|
|
||||||
|
## Frontend: ChatPage
|
||||||
|
|
||||||
|
Route: `/chat` (lazy-loaded, code-split)
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
- **Text input + submit button** — Query entry with Enter-to-submit
|
||||||
|
- **Streaming message display** — Accumulates tokens with blinking cursor animation during streaming
|
||||||
|
- **Citation markers** — `[N]` parsed to superscript links targeting `/techniques/:slug`
|
||||||
|
- **Source list** — Numbered sources with creator attribution displayed below the response
|
||||||
|
- **States:** Loading (streaming indicator), error (message display), empty (placeholder prompt)
|
||||||
|
|
||||||
|
### SSE Client
|
||||||
|
|
||||||
|
Located in `frontend/src/api/chat.ts`. Uses `fetch()` + `ReadableStream` with typed callbacks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
streamChat(query, creator?, {
|
||||||
|
onSources: (sources) => void,
|
||||||
|
onToken: (token) => void,
|
||||||
|
onDone: (data) => void,
|
||||||
|
onError: (error) => void,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- `backend/chat_service.py` — ChatService retrieve-prompt-stream pipeline
|
||||||
|
- `backend/routers/chat.py` — POST /api/v1/chat endpoint
|
||||||
|
- `frontend/src/api/chat.ts` — SSE client utility
|
||||||
|
- `frontend/src/pages/ChatPage.tsx` — Chat UI page component
|
||||||
|
- `frontend/src/pages/ChatPage.module.css` — Chat page styles
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
- **Standalone ASGI test client pattern** — Tests use mocked DB to avoid PostgreSQL dependency, enabling fast CI runs
|
||||||
|
- **Patch `openai.AsyncOpenAI` constructor** rather than instance attribute for reliable test mocking
|
||||||
|
- **Local citation regex** in ChatPage rather than importing from utils — link targets differ from technique page citations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See also: [[Search-Retrieval]], [[API-Surface]], [[Frontend]]*
|
||||||
|
|
@ -71,6 +71,7 @@ Creator (1) ──→ (N) SourceVideo (1) ──→ (N) TranscriptSegment
|
||||||
| topic_tags | ARRAY(String) | |
|
| topic_tags | ARRAY(String) | |
|
||||||
| content_type | Enum | tutorial / tip / exploration / walkthrough |
|
| content_type | Enum | tutorial / tip / exploration / walkthrough |
|
||||||
| review_status | String | pending / approved / rejected |
|
| review_status | String | pending / approved / rejected |
|
||||||
|
| sort_order | Integer | Display ordering within video (M021/S06) |
|
||||||
|
|
||||||
### TechniquePage
|
### TechniquePage
|
||||||
|
|
||||||
|
|
@ -188,6 +189,8 @@ Append-only versioned record of per-field consent changes.
|
||||||
| PipelineRunTrigger | auto, manual, retrigger, clean_retrigger |
|
| PipelineRunTrigger | auto, manual, retrigger, clean_retrigger |
|
||||||
| **UserRole** | admin, creator |
|
| **UserRole** | admin, creator |
|
||||||
| **ConsentField** | kb_inclusion, training_usage, public_display |
|
| **ConsentField** | kb_inclusion, training_usage, public_display |
|
||||||
|
| **HighlightStatus** | candidate, approved, rejected (M021/S04) |
|
||||||
|
| **ChapterStatus** | draft, approved, hidden (M021/S06) |
|
||||||
|
|
||||||
## Schema Notes
|
## Schema Notes
|
||||||
|
|
||||||
|
|
@ -201,4 +204,15 @@ Append-only versioned record of per-field consent changes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
*See also: [[Architecture]], [[API-Surface]], [[Pipeline]], [[Authentication]]*
|
||||||
|
changes currently require manual DDL
|
||||||
|
- **body_sections_format** discriminator enables v1/v2 format coexistence (D024)
|
||||||
|
- **topic_category casing** is inconsistent across records (e.g., "Sound design" vs "Sound Design") — known data quality issue
|
||||||
|
- **Stage 4 classification data** (per-moment topic_tags) stored in Redis with 24h TTL, not DB columns
|
||||||
|
- **Timestamp convention:** `datetime.now(timezone.utc).replace(tzinfo=None)` — asyncpg rejects timezone-aware datetimes for TIMESTAMP WITHOUT TIME ZONE columns (D002)
|
||||||
|
- **User passwords** are stored as bcrypt hashes via `bcrypt.hashpw()`
|
||||||
|
- **Consent audit** uses version numbers assigned in application code (`max(version) + 1` per video_consent_id)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
*See also: [[Architecture]], [[API-Surface]], [[Pipeline]], [[Authentication]]*
|
*See also: [[Architecture]], [[API-Surface]], [[Pipeline]], [[Authentication]]*
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@ Architectural and pattern decisions made during Chrysopedia development. Append-
|
||||||
| D034 | Documentation strategy | Forgejo wiki, KB slice at end of every milestone | Incremental docs stay current; final pass in M025 |
|
| D034 | Documentation strategy | Forgejo wiki, KB slice at end of every milestone | Incremental docs stay current; final pass in M025 |
|
||||||
| D035 | File/object storage | MinIO (S3-compatible) self-hosted | Docker-native, signed URLs, fits existing infrastructure |
|
| D035 | File/object storage | MinIO (S3-compatible) self-hosted | Docker-native, signed URLs, fits existing infrastructure |
|
||||||
|
|
||||||
|
## M021 Decisions
|
||||||
|
|
||||||
|
| # | When | Decision | Choice | Rationale |
|
||||||
|
|---|------|----------|--------|-----------|
|
||||||
|
| D039 | M021/S01 | LightRAG scoring strategy | Position-based (1.0 → 0.5 descending), sequential Qdrant fallback | `/query/data` has no numeric relevance score; retrieval order is the only signal |
|
||||||
|
| D040 | M021/S02 | Creator-scoped retrieval strategy | 4-tier cascade: creator → domain → global → none | Progressive widening ensures results while preferring creator context; `ll_keywords` for soft scoping; 3x oversampling for post-filter survival |
|
||||||
|
|
||||||
## UI/UX Decisions
|
## UI/UX Decisions
|
||||||
|
|
||||||
| # | Decision | Choice |
|
| # | Decision | Choice |
|
||||||
|
|
|
||||||
15
Frontend.md
15
Frontend.md
|
|
@ -53,7 +53,13 @@ Local component state only (`useState`/`useEffect`). No Redux, Zustand, Context
|
||||||
|
|
||||||
## API Client
|
## API Client
|
||||||
|
|
||||||
Single module `public-client.ts` (~600 lines) with typed `request<T>` helper. Relative `/api/v1` base URL (nginx proxies to API container). All response TypeScript interfaces defined in the same file.
|
Two API modules:
|
||||||
|
- `public-client.ts` (~600 lines) — typed `request<T>` helper for REST endpoints
|
||||||
|
- `chat.ts` — SSE streaming client for POST /api/v1/chat using `fetch()` + `ReadableStream`
|
||||||
|
- `videos.ts` — chapter management functions (fetchChapters, fetchCreatorChapters, updateChapter, reorderChapters, approveChapters)
|
||||||
|
- `auth.ts` — authentication + impersonation functions including `fetchImpersonationLog()`
|
||||||
|
|
||||||
|
Relative `/api/v1` base URL (nginx proxies to API container).
|
||||||
|
|
||||||
## CSS Architecture
|
## CSS Architecture
|
||||||
|
|
||||||
|
|
@ -108,3 +114,10 @@ Single module `public-client.ts` (~600 lines) with typed `request<T>` helper. Re
|
||||||
---
|
---
|
||||||
|
|
||||||
*See also: [[Architecture]], [[API-Surface]], [[Development-Guide]]*
|
*See also: [[Architecture]], [[API-Surface]], [[Development-Guide]]*
|
||||||
|
*See also: [[Architecture]], [[API-Surface]], [[Development-Guide]]*
|
||||||
|
ocalhost:8001`
|
||||||
|
- **Production:** nginx serves static `dist/` bundle, proxies `/api` to FastAPI container
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See also: [[Architecture]], [[API-Surface]], [[Development-Guide]]*
|
||||||
|
|
|
||||||
143
Highlights.md
Normal file
143
Highlights.md
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
# Highlight Detection
|
||||||
|
|
||||||
|
Heuristic scoring engine that ranks KeyMoment records into highlight candidates using 7 weighted dimensions. Added in M021/S04.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Highlight detection scores every KeyMoment in a video to identify the most "highlightable" segments — moments that would work well as standalone clips or featured content. The scoring is a pure function (no ML model, no external API) based on 7 dimensions derived from existing KeyMoment metadata.
|
||||||
|
|
||||||
|
## Scoring Dimensions
|
||||||
|
|
||||||
|
Total weight sums to 1.0. Each dimension produces a 0.0–1.0 score.
|
||||||
|
|
||||||
|
| Dimension | Weight | What It Measures |
|
||||||
|
|-----------|--------|-----------------|
|
||||||
|
| `duration_fitness` | 0.25 | Piecewise linear curve peaking at 30–60 seconds (ideal clip length) |
|
||||||
|
| `content_type` | 0.20 | Content type favorability: tutorial > tip > walkthrough > exploration |
|
||||||
|
| `specificity_density` | 0.20 | Regex-based counting of specific units, ratios, and named parameters in summary text |
|
||||||
|
| `plugin_richness` | 0.10 | Number of plugins/VSTs referenced (more = more actionable) |
|
||||||
|
| `transcript_energy` | 0.10 | Teaching-phrase detection in transcript text (e.g., "the trick is", "key thing") |
|
||||||
|
| `source_quality` | 0.10 | Source quality rating: high=1.0, medium=0.6, low=0.3 |
|
||||||
|
| `video_type` | 0.05 | Video type favorability mapping |
|
||||||
|
|
||||||
|
### Duration Fitness Curve
|
||||||
|
|
||||||
|
Uses piecewise linear (not Gaussian) for predictability:
|
||||||
|
- 0–10s → low score (too short)
|
||||||
|
- 10–30s → ramp up
|
||||||
|
- 30–60s → peak score (1.0)
|
||||||
|
- 60–120s → gradual decline
|
||||||
|
- 120s+ → low score (too long for a highlight)
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
### HighlightCandidate
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|-------|------|-------|
|
||||||
|
| id | UUID PK | |
|
||||||
|
| key_moment_id | FK → KeyMoment | Unique constraint (`uq_highlight_candidate_moment`) |
|
||||||
|
| source_video_id | FK → SourceVideo | Indexed |
|
||||||
|
| score | Float | Composite score 0.0–1.0 |
|
||||||
|
| score_breakdown | JSONB | Per-dimension scores (7 fields) |
|
||||||
|
| duration_secs | Float | Cached from KeyMoment for display |
|
||||||
|
| status | Enum(HighlightStatus) | candidate / approved / rejected |
|
||||||
|
| created_at | Timestamp | |
|
||||||
|
| updated_at | Timestamp | |
|
||||||
|
|
||||||
|
### HighlightStatus Enum
|
||||||
|
|
||||||
|
| Value | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| `candidate` | Scored but not reviewed |
|
||||||
|
| `approved` | Admin-approved as a highlight |
|
||||||
|
| `rejected` | Admin-rejected |
|
||||||
|
|
||||||
|
### Database Indexes
|
||||||
|
|
||||||
|
- `source_video_id` — filter by video
|
||||||
|
- `score` DESC — rank ordering
|
||||||
|
- `status` — filter by review state
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
Alembic migration `019_add_highlight_candidates.py` creates the table with all indexes and the named unique constraint.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
All under `/api/v1/admin/highlights/`. Admin access.
|
||||||
|
|
||||||
|
| Method | Path | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| POST | `/admin/highlights/detect/{video_id}` | Score all KeyMoments for a video, upsert candidates |
|
||||||
|
| POST | `/admin/highlights/detect-all` | Score all videos (triggers Celery tasks) |
|
||||||
|
| GET | `/admin/highlights/candidates` | Paginated candidate list, sorted by score DESC |
|
||||||
|
| GET | `/admin/highlights/candidates/{id}` | Single candidate with full `score_breakdown` |
|
||||||
|
|
||||||
|
### Detect Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"video_id": "uuid",
|
||||||
|
"candidates_created": 12,
|
||||||
|
"candidates_updated": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Candidate Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"key_moment_id": "uuid",
|
||||||
|
"source_video_id": "uuid",
|
||||||
|
"score": 0.847,
|
||||||
|
"score_breakdown": {
|
||||||
|
"duration_fitness": 0.95,
|
||||||
|
"content_type_weight": 0.80,
|
||||||
|
"specificity_density": 0.72,
|
||||||
|
"plugin_richness": 0.60,
|
||||||
|
"transcript_energy": 0.85,
|
||||||
|
"source_quality_weight": 1.00,
|
||||||
|
"video_type_weight": 0.50
|
||||||
|
},
|
||||||
|
"duration_secs": 45.0,
|
||||||
|
"status": "candidate",
|
||||||
|
"created_at": "...",
|
||||||
|
"updated_at": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pipeline Integration
|
||||||
|
|
||||||
|
### Celery Task: `stage_highlight_detection`
|
||||||
|
|
||||||
|
- **Binding:** `bind=True, max_retries=3`
|
||||||
|
- **Session:** Uses `_get_sync_session` (sync SQLAlchemy, per D004)
|
||||||
|
- **Flow:** Load KeyMoments for video → score each via `score_moment()` → bulk upsert via `INSERT ON CONFLICT` on named constraint `uq_highlight_candidate_moment`
|
||||||
|
- **Events:** Emits `pipeline_events` rows for start/complete/error with candidate count in payload
|
||||||
|
|
||||||
|
### Scoring Function
|
||||||
|
|
||||||
|
`score_moment()` in `backend/pipeline/highlight_scorer.py` is a **pure function** — no DB access, no side effects. Takes a KeyMoment-like dict, returns `(score, breakdown_dict)`. This separation enables easy unit testing (28 tests, runs in 0.03s).
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
- **Pure function scoring** — No DB or side effects in `score_moment()`, enabling fast unit tests
|
||||||
|
- **Piecewise linear duration** — Predictable behavior vs. Gaussian bell curve
|
||||||
|
- **Named unique constraint** — `uq_highlight_candidate_moment` enables idempotent upserts via `ON CONFLICT`
|
||||||
|
- **Lazy import** — `score_moment` imported inside Celery task to avoid circular imports at module load
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- `backend/pipeline/highlight_scorer.py` — Pure scoring function with 7 dimensions
|
||||||
|
- `backend/pipeline/highlight_schemas.py` — Pydantic schemas (HighlightScoreBreakdown, HighlightCandidateResponse, HighlightBatchResult)
|
||||||
|
- `backend/pipeline/stages.py` — `stage_highlight_detection` Celery task
|
||||||
|
- `backend/routers/highlights.py` — 4 admin API endpoints
|
||||||
|
- `backend/models.py` — HighlightCandidate model, HighlightStatus enum
|
||||||
|
- `alembic/versions/019_add_highlight_candidates.py` — Migration
|
||||||
|
- `backend/pipeline/test_highlight_scorer.py` — 28 unit tests
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See also: [[Pipeline]], [[Data-Model]], [[API-Surface]]*
|
||||||
6
Home.md
6
Home.md
|
|
@ -37,4 +37,10 @@ Producers can search for specific techniques and find timestamped key moments, s
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
*Last updated: 2026-04-04 — M021 chat engine, retrieval cascade, highlights, audio mode, chapters, impersonation write mode*
|
||||||
|
inx reverse proxy on nuc01 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
*Last updated: 2026-04-03 — M018/S02 initial bootstrap*
|
*Last updated: 2026-04-03 — M018/S02 initial bootstrap*
|
||||||
|
” M018/S02 initial bootstrap*
|
||||||
|
|
|
||||||
|
|
@ -86,17 +86,39 @@ The `original_user_id` claim is what `reject_impersonation` checks.
|
||||||
|
|
||||||
### AuthContext Extensions
|
### AuthContext Extensions
|
||||||
|
|
||||||
- `startImpersonation(userId)` — Calls impersonate API, saves current admin token to `sessionStorage`, swaps to impersonation token
|
- `startImpersonation(userId, writeMode?)` — Calls impersonate API (with optional write_mode body), saves current admin token to `sessionStorage`, swaps to impersonation token
|
||||||
- `exitImpersonation()` — Calls stop API, restores admin token from `sessionStorage`
|
- `exitImpersonation()` — Calls stop API, restores admin token from `sessionStorage`
|
||||||
- `user.impersonating` (boolean) — True when viewing as another user
|
- `user.impersonating` (boolean) — True when viewing as another user
|
||||||
|
- `isWriteMode` (boolean) — True when impersonation session has write access (M021/S07)
|
||||||
|
|
||||||
### ImpersonationBanner
|
### ImpersonationBanner
|
||||||
|
|
||||||
Fixed amber bar at page top when impersonating. Shows "Viewing as {name}" with Exit button. Rendered in `AppShell` when `user.impersonating` is true.
|
Fixed bar at page top when impersonating. Two visual states (M021/S07):
|
||||||
|
- **Amber** "👁 Viewing as {name}" — read-only mode
|
||||||
|
- **Red** "✏️ Editing as {name}" — write mode (adds `body.impersonating-write` class)
|
||||||
|
|
||||||
|
Uses `data-write-mode` attribute for CSS color switching.
|
||||||
|
|
||||||
|
### ConfirmModal (M021/S07)
|
||||||
|
|
||||||
|
Reusable confirmation dialog component used for "Edit As" write-mode impersonation:
|
||||||
|
- Backdrop + Escape/backdrop-click dismiss
|
||||||
|
- `variant` prop: `warning` (amber) or `danger` (red confirm button)
|
||||||
|
- Uses `data-variant` attribute for CSS variant styling
|
||||||
|
|
||||||
### AdminUsers Page
|
### AdminUsers Page
|
||||||
|
|
||||||
Route: `/admin/users`. Table of all users with "View As" buttons for creator-role users. Code-split with `React.lazy`.
|
Route: `/admin/users`. Table of all users with two action buttons per creator:
|
||||||
|
- "View As" — starts read-only impersonation (no confirmation modal)
|
||||||
|
- "Edit As" — opens ConfirmModal with danger variant before starting write-mode impersonation
|
||||||
|
|
||||||
|
### AdminAuditLog Page (M021/S07)
|
||||||
|
|
||||||
|
Route: `/admin/audit-log`. Six-column table:
|
||||||
|
- Date/Time, Admin, Target User, Action, Write Mode, IP Address
|
||||||
|
- Badge styling via `data-variant` / `data-action` / `data-write-mode` attributes
|
||||||
|
- Loading/error/empty states, Previous/Next pagination
|
||||||
|
- Linked from AdminDropdown after "Users"
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,14 @@ Stage 6: Embed & Index — generate embeddings, upsert to Qdrant (non-blocking)
|
||||||
- **Non-blocking:** Failures log WARNING but don't fail the pipeline (D005)
|
- **Non-blocking:** Failures log WARNING but don't fail the pipeline (D005)
|
||||||
- Can be re-triggered independently via `/admin/pipeline/reindex-all`
|
- Can be re-triggered independently via `/admin/pipeline/reindex-all`
|
||||||
|
|
||||||
|
### Stage 7: Highlight Detection (M021/S04)
|
||||||
|
- Scores every KeyMoment in a video using 7 weighted heuristic dimensions
|
||||||
|
- Pure function scoring: duration_fitness (0.25), content_type (0.20), specificity_density (0.20), plugin_richness (0.10), transcript_energy (0.10), source_quality (0.10), video_type (0.05)
|
||||||
|
- Celery task `stage_highlight_detection` with `bind=True, max_retries=3`
|
||||||
|
- Bulk upserts via `INSERT ON CONFLICT` on named constraint `uq_highlight_candidate_moment`
|
||||||
|
- Output: HighlightCandidate records in PostgreSQL with composite score and per-dimension breakdown
|
||||||
|
- See [[Highlights]] for full scoring details and API endpoints
|
||||||
|
|
||||||
## LLM Configuration
|
## LLM Configuration
|
||||||
|
|
||||||
| Setting | Value |
|
| Setting | Value |
|
||||||
|
|
|
||||||
31
Player.md
31
Player.md
|
|
@ -74,6 +74,37 @@ Playback rate options: 0.5x, 0.75x, 1x, 1.25x, 1.5x, 2x.
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
|
- `frontend/src/pages/WatchPage.tsx` — Page component
|
||||||
|
- `frontend/src/components/VideoPlayer.tsx` — Video element + HLS setup
|
||||||
|
- `frontend/src/components/PlayerControls.tsx` — Play/pause, speed, volume, seek bar
|
||||||
|
- `frontend/src/components/TranscriptSidebar.tsx` — Synchronized transcript display
|
||||||
|
- `frontend/src/components/AudioWaveform.tsx` — Waveform visualization for audio content (M021/S05)
|
||||||
|
- `frontend/src/components/ChapterMarkers.tsx` — Seek bar chapter overlay (M021/S05)
|
||||||
|
- `frontend/src/hooks/useMediaSync.ts` — Shared playback state hook
|
||||||
|
- `backend/routers/videos.py` — Video detail + transcript API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See also: [[Architecture]], [[API-Surface]], [[Frontend]]*
|
||||||
|
with chapter title
|
||||||
|
- Click seeks playback to chapter start time
|
||||||
|
- Integrated into `PlayerControls` via wrapper container div
|
||||||
|
|
||||||
|
## Audio Waveform (M021/S05)
|
||||||
|
|
||||||
|
`AudioWaveform` component renders when content is audio-only (no video_url):
|
||||||
|
- Hidden `<audio>` element shared between `useMediaSync` and WaveSurfer
|
||||||
|
- wavesurfer.js with MediaElement backend — playback controlled identically to video mode
|
||||||
|
- Dark-themed CSS matching the video player area
|
||||||
|
- RegionsPlugin for labeled chapter regions with drag/resize support
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- `wavesurfer.js` — waveform rendering (~200KB, loaded only in audio mode)
|
||||||
|
- `useMediaSync` hook widened from `HTMLVideoElement` to `HTMLMediaElement` for audio/video polymorphism
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
- `frontend/src/pages/WatchPage.tsx` — Page component
|
- `frontend/src/pages/WatchPage.tsx` — Page component
|
||||||
- `frontend/src/components/VideoPlayer.tsx` — Video element + HLS setup
|
- `frontend/src/components/VideoPlayer.tsx` — Video element + HLS setup
|
||||||
- `frontend/src/components/PlayerControls.tsx` — Play/pause, speed, volume, seek bar
|
- `frontend/src/components/PlayerControls.tsx` — Play/pause, speed, volume, seek bar
|
||||||
|
|
|
||||||
107
Search-Retrieval.md
Normal file
107
Search-Retrieval.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Search & Retrieval
|
||||||
|
|
||||||
|
LightRAG-first search with automatic Qdrant fallback, plus a 4-tier creator-scoped retrieval cascade. Added in M021/S01–S02.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Search went through a major upgrade in M021: LightRAG replaced Qdrant as the primary search engine, with Qdrant retained as an automatic fallback. A 4-tier creator-scoped retrieval cascade was added for context-aware search when querying within a creator's content.
|
||||||
|
|
||||||
|
## LightRAG Integration
|
||||||
|
|
||||||
|
LightRAG is a graph-based RAG engine running as a standalone service on port 9621. It replaced Qdrant as the primary search path for `GET /api/v1/search`.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. **Query** — `SearchService._lightrag_search()` POSTs to LightRAG `/query/data` with `mode: "hybrid"`
|
||||||
|
2. **Parse** — Response contains `chunks` (text passages with file_source metadata) and `entities` (graph nodes)
|
||||||
|
3. **Extract** — Technique slugs parsed from `file_source` paths using format `technique:{slug}:creator:{uuid}`
|
||||||
|
4. **Lookup** — Batch PostgreSQL query maps slugs to full TechniquePage records
|
||||||
|
5. **Score** — Position-based scoring (1.0 → 0.5 descending) since `/query/data` has no numeric relevance score (D039)
|
||||||
|
6. **Supplement** — Entity names matched against technique page titles as supplementary results
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
| Field | Default | Purpose |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| `lightrag_url` | `http://chrysopedia-lightrag:9621` | LightRAG service URL |
|
||||||
|
| `lightrag_search_timeout` | `2.0` (seconds) | Request timeout |
|
||||||
|
| `lightrag_min_query_length` | `3` (characters) | Queries shorter than this skip LightRAG |
|
||||||
|
|
||||||
|
### Fallback Behavior
|
||||||
|
|
||||||
|
LightRAG failures trigger automatic fallback to the existing Qdrant + keyword search path:
|
||||||
|
|
||||||
|
- **Timeout** → fallback + WARNING log with `reason=timeout`
|
||||||
|
- **Connection error** → fallback + WARNING log with `reason=connection_error`
|
||||||
|
- **HTTP error (e.g. 500)** → fallback + WARNING log with `reason=http_error`
|
||||||
|
- **Empty results** → fallback + WARNING log with `reason=empty_results`
|
||||||
|
- **Parse error** → fallback + WARNING log with `reason=parse_error`
|
||||||
|
- **Short query (<3 chars)** → skips LightRAG entirely, uses Qdrant directly
|
||||||
|
|
||||||
|
The `fallback_used` field in the search response indicates which engine served results.
|
||||||
|
|
||||||
|
## 4-Tier Creator-Scoped Cascade
|
||||||
|
|
||||||
|
When a `?creator=` parameter is provided (e.g., from a creator profile page or the chat engine), search runs a progressive cascade that widens scope until results are found. Added in M021/S02 (D040).
|
||||||
|
|
||||||
|
```
|
||||||
|
Tier 1: Creator-scoped
|
||||||
|
└─ LightRAG with ll_keywords=[creator_name], post-filter by creator_id (3× oversampling)
|
||||||
|
│ empty?
|
||||||
|
▼
|
||||||
|
Tier 2: Domain-scoped
|
||||||
|
└─ LightRAG with ll_keywords=[dominant_category] (requires ≥2 pages in category)
|
||||||
|
│ empty?
|
||||||
|
▼
|
||||||
|
Tier 3: Global
|
||||||
|
└─ Standard LightRAG search (no scoping)
|
||||||
|
│ empty?
|
||||||
|
▼
|
||||||
|
Tier 4: None
|
||||||
|
└─ cascade_tier="none" — no results from any tier
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cascade Details
|
||||||
|
|
||||||
|
| Tier | Method | Scoping | Post-Filter |
|
||||||
|
|------|--------|---------|-------------|
|
||||||
|
| Creator | `_creator_scoped_search()` | `ll_keywords: [creator_name]` | Yes — filter by `creator_id`, request 3× `top_k` |
|
||||||
|
| Domain | `_domain_scoped_search()` | `ll_keywords: [domain]` | No — any creator in domain qualifies |
|
||||||
|
| Global | `_lightrag_search()` | None | No |
|
||||||
|
| None | — | — | — (empty result) |
|
||||||
|
|
||||||
|
**Domain detection:** SQL aggregation finds the dominant `topic_category` across a creator's technique pages. Requires ≥2 pages in the category to declare a domain — fewer means insufficient signal.
|
||||||
|
|
||||||
|
**Post-filtering with oversampling:** Creator tier requests 3× the desired result count from LightRAG, then filters locally by `creator_id`. This compensates for LightRAG not supporting native creator filtering.
|
||||||
|
|
||||||
|
### Response Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `cascade_tier` | string | Which tier served: `"creator"`, `"domain"`, `"global"`, `"none"`, or `""` (no cascade) |
|
||||||
|
| `fallback_used` | boolean | `true` if Qdrant fallback was used instead of LightRAG |
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
- `logger.info` per LightRAG search: query, latency_ms, result_count
|
||||||
|
- `logger.info` per cascade tier: query, creator, tier, latency_ms, result_count
|
||||||
|
- `logger.warning` on any failure path with structured `reason=` tag
|
||||||
|
- `cascade_tier` and `fallback_used` in API response for downstream consumers
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
| # | Decision | Choice | Rationale |
|
||||||
|
|---|----------|--------|-----------|
|
||||||
|
| D039 | LightRAG scoring | Position-based (1.0 → 0.5) | `/query/data` has no numeric relevance score; sequential fallback to Qdrant |
|
||||||
|
| D040 | Creator-scoped strategy | 4-tier cascade (creator → domain → global → none) | Progressive widening ensures results while preferring creator context |
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- `backend/search_service.py` — SearchService with LightRAG integration and cascade methods
|
||||||
|
- `backend/config.py` — LightRAG configuration fields
|
||||||
|
- `backend/schemas.py` — `cascade_tier` in SearchResponse
|
||||||
|
- `backend/routers/search.py` — `?creator=` query parameter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See also: [[Chat-Engine]], [[Architecture]], [[API-Surface]], [[Decisions]]*
|
||||||
|
|
@ -10,6 +10,11 @@
|
||||||
- [[Pipeline]]
|
- [[Pipeline]]
|
||||||
- [[Player]]
|
- [[Player]]
|
||||||
|
|
||||||
|
**Features**
|
||||||
|
- [[Chat-Engine]]
|
||||||
|
- [[Search-Retrieval]]
|
||||||
|
- [[Highlights]]
|
||||||
|
|
||||||
**Reference**
|
**Reference**
|
||||||
- [[API-Surface]]
|
- [[API-Surface]]
|
||||||
- [[Frontend]]
|
- [[Frontend]]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue