Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .kiro/hooks/reset-database.kiro.hook
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
},
"then": {
"type": "runCommand",
"command": "cd dev && ./stop && docker volume rm dev_postgres_data dev_valkey_data && ./start"
}
}



"command": "cd dev && ./stop && docker volume rm dev_postgres_data && ./start"
},
"workspaceFolderName": "eioku-kiro-hackathon",
"shortName": "reset-database"
}
81 changes: 64 additions & 17 deletions .kiro/specs/artifact-thumbnails-gallery/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ class ArtifactSearchResult(BaseModel):
preview: dict
video_filename: str
file_created_at: str
artifact_count: int | None = None # Only present when group_by_video=true

class ArtifactSearchResponse(BaseModel):
results: list[ArtifactSearchResult]
Expand All @@ -267,12 +268,14 @@ async def search_artifacts(
min_confidence: float | None = Query(None, ge=0, le=1),
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
group_by_video: bool = Query(False, description="Collapse results to one per video"),
session: Session = Depends(get_db),
) -> ArtifactSearchResponse:
"""
Search artifacts across all videos with pagination.

Returns results ordered by global timeline with thumbnail URLs.
When group_by_video=true, returns one result per video with artifact_count.
"""
# Map kind to artifact_type
type_map = {
Expand All @@ -285,20 +288,42 @@ async def search_artifacts(
}
artifact_type = type_map.get(kind)

# Build query
base_query = """
SELECT
a.artifact_id,
a.video_id,
a.artifact_type,
COALESCE((a.payload->>'start_ms')::int, 0) as start_ms,
a.payload as preview,
v.filename as video_filename,
v.file_created_at
FROM artifacts a
JOIN videos v ON v.video_id = a.video_id
WHERE a.artifact_type = :artifact_type
"""
# Build base query - different for grouped vs ungrouped
if group_by_video:
# Grouped: Use window function to get first artifact per video + count
base_query = """
WITH ranked AS (
SELECT
a.artifact_id,
a.video_id,
a.artifact_type,
COALESCE((a.payload->>'start_ms')::int, 0) as start_ms,
a.payload as preview,
v.filename as video_filename,
v.file_created_at,
ROW_NUMBER() OVER (
PARTITION BY a.video_id
ORDER BY COALESCE((a.payload->>'start_ms')::int, 0) ASC
) as rn,
COUNT(*) OVER (PARTITION BY a.video_id) as artifact_count
FROM artifacts a
JOIN videos v ON v.video_id = a.video_id
WHERE a.artifact_type = :artifact_type
"""
else:
base_query = """
SELECT
a.artifact_id,
a.video_id,
a.artifact_type,
COALESCE((a.payload->>'start_ms')::int, 0) as start_ms,
a.payload as preview,
v.filename as video_filename,
v.file_created_at
FROM artifacts a
JOIN videos v ON v.video_id = a.video_id
WHERE a.artifact_type = :artifact_type
"""

params = {"artifact_type": artifact_type}

Expand All @@ -319,8 +344,29 @@ async def search_artifacts(
base_query += " AND (a.payload->>'confidence')::float >= :min_confidence"
params["min_confidence"] = min_confidence

# Handle grouped query completion
if group_by_video:
base_query += """
)
SELECT artifact_id, video_id, artifact_type, start_ms, preview,
video_filename, file_created_at, artifact_count
FROM ranked WHERE rn = 1
"""

# Count total
count_query = f"SELECT COUNT(*) FROM ({base_query}) sub"
if group_by_video:
count_query = f"SELECT COUNT(DISTINCT a.video_id) FROM artifacts a JOIN videos v ON v.video_id = a.video_id WHERE a.artifact_type = :artifact_type"
# Re-add filters for count
if label:
count_query += " AND a.payload->>'label' = :label"
if query:
count_query += " AND a.payload->>'text' ILIKE '%' || :query || '%'"
if filename:
count_query += " AND v.filename ILIKE '%' || :filename || '%'"
if min_confidence:
count_query += " AND (a.payload->>'confidence')::float >= :min_confidence"
else:
count_query = f"SELECT COUNT(*) FROM ({base_query}) sub"
total = session.execute(text(count_query), params).scalar()

# Add ordering and pagination
Expand All @@ -339,10 +385,11 @@ async def search_artifacts(
artifact_id=row.artifact_id,
artifact_type=row.artifact_type,
start_ms=row.start_ms,
thumbnail_url=f"/api/v1/thumbnails/{row.video_id}/{row.start_ms}",
thumbnail_url=f"/v1/thumbnails/{row.video_id}/{row.start_ms}",
preview=row.preview,
video_filename=row.video_filename,
file_created_at=row.file_created_at.isoformat() if row.file_created_at else None,
artifact_count=getattr(row, 'artifact_count', None),
)
for row in rows
]
Expand Down Expand Up @@ -482,7 +529,7 @@ sequenceDiagram
**Validates: Requirements 2.1**

**Property 3: Thumbnail URL Correctness**
*For any* artifact search result, the `thumbnail_url` SHALL point to `/api/v1/thumbnails/{video_id}/{start_ms}` where `start_ms` matches the artifact's timestamp.
*For any* artifact search result, the `thumbnail_url` SHALL point to `/v1/thumbnails/{video_id}/{start_ms}` where `start_ms` matches the artifact's timestamp.
**Validates: Requirements 4.5**

**Property 4: Search Result Ordering**
Expand Down
8 changes: 6 additions & 2 deletions .kiro/specs/artifact-thumbnails-gallery/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ This document specifies the requirements for artifact thumbnail generation and a

#### Acceptance Criteria

1. THE backend SHALL provide a `GET /api/v1/thumbnails/{video_id}/{timestamp_ms}` endpoint
1. THE backend SHALL provide a `GET /v1/thumbnails/{video_id}/{timestamp_ms}` endpoint
2. WHEN the thumbnail exists, THE endpoint SHALL return the WebP file with appropriate content type
3. WHEN the thumbnail does not exist, THE endpoint SHALL return 404
4. THE endpoint SHALL set appropriate cache headers for browser caching (e.g., 1 week)
Expand All @@ -54,13 +54,15 @@ This document specifies the requirements for artifact thumbnail generation and a
#### Acceptance Criteria

1. THE backend SHALL provide a `GET /api/v1/artifacts/search` endpoint for gallery-style artifact search
2. THE endpoint SHALL accept parameters: `kind`, `label`, `query`, `min_confidence`, `filename`, `limit`, `offset`
2. THE endpoint SHALL accept parameters: `kind`, `label`, `query`, `min_confidence`, `filename`, `limit`, `offset`, `group_by_video`
3. THE endpoint SHALL return results ordered by global timeline (file_created_at, video_id, start_ms)
4. EACH result SHALL include: `video_id`, `artifact_id`, `start_ms`, `thumbnail_url`, `preview`, `video_filename`
5. THE `thumbnail_url` SHALL point to the thumbnail serving endpoint for that artifact's timestamp
6. THE endpoint SHALL support pagination via `limit` and `offset` parameters
7. THE endpoint SHALL return total count of matching artifacts for pagination UI
8. WHEN `filename` parameter is provided, THE endpoint SHALL filter results to videos whose filename contains the search string (case-insensitive)
9. WHEN `group_by_video` is true, THE endpoint SHALL return only the first matching artifact per video (collapsed view)
10. WHEN `group_by_video` is true, EACH result SHALL include `artifact_count` indicating total matches in that video

### Requirement 5: Artifact Gallery UI Component

Expand Down Expand Up @@ -88,6 +90,8 @@ This document specifies the requirements for artifact thumbnail generation and a
4. THE search form SHALL include a filename filter input to search within specific videos
5. WHEN the user submits the search form, THE gallery SHALL update to show matching results
6. THE search form state SHALL be preserved in the URL for shareable links
7. THE search form SHALL include a "Group by video" toggle to collapse results by video
8. WHEN "Group by video" is enabled, THE gallery SHALL show one thumbnail per video with artifact count badge

### Requirement 7: Thumbnail Fallback

Expand Down
Loading