docs: add Thumbnail QA Analysis for M5Max48 implementation
This commit is contained in:
340
docs_v1.0/DESIGN/Thumbnail_QA_Analysis.md
Normal file
340
docs_v1.0/DESIGN/Thumbnail_QA_Analysis.md
Normal file
@@ -0,0 +1,340 @@
|
||||
---
|
||||
title: Thumbnail Endpoint Quality Assurance Analysis
|
||||
version: 1.0.0
|
||||
date: 2026-05-27
|
||||
author: M5Max128
|
||||
status: research_complete
|
||||
---
|
||||
|
||||
# Thumbnail Endpoint Quality Assurance Analysis
|
||||
|
||||
## Scope
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| Research | Complete |
|
||||
| Implementation | Pending (M5Max48) |
|
||||
| Affected Endpoints | 2 |
|
||||
|
||||
## Overview
|
||||
|
||||
Thumbnail endpoints currently lack quality validation, resulting in potential anomalies:
|
||||
- **Empty images** - ffmpeg produces 0 bytes output
|
||||
- **Black frames** - extracted frame is all black
|
||||
- **Corrupted JPEG** - incomplete ffmpeg output
|
||||
|
||||
## Affected Endpoints
|
||||
|
||||
| Endpoint | File | Line |
|
||||
|----------|------|------|
|
||||
| `/api/v1/file/:file_uuid/thumbnail` | `src/api/media_api.rs` | 700-764 |
|
||||
| `/api/v1/file/:file_uuid/trace/:trace_id/thumbnail` | `src/api/trace_agent_api.rs` | 514-556 |
|
||||
|
||||
---
|
||||
|
||||
## Anomaly Classification
|
||||
|
||||
### Type 1: Empty Image (No Frame)
|
||||
|
||||
**Symptom**: Returns 0 bytes or very small JPEG
|
||||
|
||||
**Root Causes**:
|
||||
1. `frame_number > total_frames` - requested frame exceeds video length
|
||||
2. Video file missing or corrupted
|
||||
3. Codec does not support frame-level seek
|
||||
4. ffmpeg `-vf select` filter finds no matching frame
|
||||
|
||||
**Code Locations**:
|
||||
- `media_api.rs:710-716` - `query_auto_representative_frame()` may return invalid frame
|
||||
- `media_api.rs:720-728` - `file_path` query may return non-existent file
|
||||
- `media_api.rs:754-756` - only checks `output.status.success()`, not output content
|
||||
|
||||
### Type 2: Black Frame
|
||||
|
||||
**Symptom**: Returns valid JPEG but all black or very dark
|
||||
|
||||
**Root Causes**:
|
||||
1. `crop` parameters exceed video dimensions (`x+w > width` or `y+h > height`)
|
||||
2. Extracted frame is from fade-in/fade-out transition
|
||||
3. Video has black opening/closing credits
|
||||
4. Low-light scene
|
||||
|
||||
**Code Locations**:
|
||||
- `media_api.rs:731-735` - crop validation missing
|
||||
- `trace_agent_api.rs:530` - crop may exceed dimensions
|
||||
|
||||
### Type 3: Corrupted JPEG
|
||||
|
||||
**Symptom**: Returns incomplete JPEG (browser shows broken image)
|
||||
|
||||
**Root Causes**:
|
||||
1. ffmpeg stdout pipe interrupted before completion
|
||||
2. ffmpeg process killed mid-output
|
||||
3. JPEG encoder failure
|
||||
4. Incomplete write to stdout buffer
|
||||
|
||||
**Code Locations**:
|
||||
- `media_api.rs:751` - pipe output may be truncated
|
||||
- `media_api.rs:758-763` - no JPEG validation before serving
|
||||
|
||||
---
|
||||
|
||||
## Current Quality Mechanisms
|
||||
|
||||
### Endpoint 1: `face_thumbnail`
|
||||
|
||||
| Mechanism | Status | Location |
|
||||
|-----------|--------|----------|
|
||||
| Representative frame selection | Present | `tkg::query_auto_representative_frame()` |
|
||||
| ffmpeg success check | Present | `output.status.success()` |
|
||||
| JPEG validation | Missing | - |
|
||||
| Size validation | Missing | - |
|
||||
| Black frame detection | Missing | - |
|
||||
| Retry mechanism | Missing | - |
|
||||
|
||||
### Endpoint 2: `get_trace_thumbnail`
|
||||
|
||||
| Mechanism | Status | Location |
|
||||
|-----------|--------|----------|
|
||||
| Blur detection (candidate selection) | Present | `select_rep_face()` lines 463-480 |
|
||||
| Confidence filter (>0.7) | Present | `select_rep_face()` line 429 |
|
||||
| QC metadata filter | Present | `select_rep_face()` line 430 |
|
||||
| ffmpeg success check | Present | `status.status.success()` |
|
||||
| JPEG validation | Missing | - |
|
||||
| Black frame detection (extraction) | Missing | - |
|
||||
| Retry mechanism | Missing | - |
|
||||
|
||||
**Note**: `select_rep_face()` has sophisticated quality control for SELECTING the representative face, but the actual EXTRACTION step lacks validation.
|
||||
|
||||
---
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### A. Input Data Problems
|
||||
|
||||
| Problem | Impact | Condition |
|
||||
|---------|--------|-----------|
|
||||
| `frame_number > total_frames` | Empty image | TKG returns wrong frame, user passes invalid value |
|
||||
| `crop exceeds dimensions` | Black frame / error | face bbox incorrect, video resolution changed |
|
||||
| Video file missing | 500 error | File deleted/moved |
|
||||
| Codec不支持seek | Empty/corrupted | Some codecs only support sequential read |
|
||||
|
||||
### B. ffmpeg Execution Problems
|
||||
|
||||
| Problem | Impact | Cause |
|
||||
|---------|--------|-------|
|
||||
| `select` no output | Empty JPEG | frame超出範圍 → ffmpeg skips all frames |
|
||||
| Pipe interrupted | Corrupted JPEG | stdout buffer full, ffmpeg terminated early |
|
||||
| `-ss` imprecise | Wrong frame | input seeking approximate, error ±5 frames |
|
||||
| crop failure | Black frame / 500 | `x+w > width` or `y+h > height` |
|
||||
|
||||
### C. Quality Control Gaps
|
||||
|
||||
| Gap | Impact | Current |
|
||||
|-----|--------|---------|
|
||||
| No JPEG validation | Corrupted image served | Only checks exit code |
|
||||
| No size check | 0 bytes returned | No output length check |
|
||||
| No black detection | Black frame served | blurdetect only in candidate selection |
|
||||
| No retry | Single failure = error | No retry mechanism |
|
||||
|
||||
---
|
||||
|
||||
## Concrete Failure Cases
|
||||
|
||||
### Case 1: Frame Exceeds Range
|
||||
|
||||
```
|
||||
Video: total_frames=1000 (DB record)
|
||||
Actual: video has only 950 frames (file truncated)
|
||||
Request: frame=980
|
||||
ffmpeg: select=eq(n\,980) → no match
|
||||
Output: 0 bytes JPEG
|
||||
Frontend: blank image
|
||||
```
|
||||
|
||||
### Case 2: Crop Exceeds Dimensions
|
||||
|
||||
```
|
||||
Video: 1920x1080
|
||||
face_bbox: x=1850, y=1050, w=100, h=100
|
||||
ffmpeg: crop=100:100:1850:1050
|
||||
Result: x+100=1950 > 1920 → ffmpeg error or black border
|
||||
```
|
||||
|
||||
### Case 3: Seek Imprecise
|
||||
|
||||
```
|
||||
Video: 25fps
|
||||
Request: frame=1000 (40 seconds)
|
||||
ffmpeg -ss 40.0 -i video
|
||||
Actual: seeks to frame 995~1005 range
|
||||
Result: extracts different face than select_rep_face chose
|
||||
```
|
||||
|
||||
### Case 4: Pipe Interrupted
|
||||
|
||||
```
|
||||
ffmpeg -i large_video -vf select=eq(n\,50000) -f image2pipe -
|
||||
Video large, select needs scan to frame 50000
|
||||
Pipe buffer full → ffmpeg may be killed or terminate early
|
||||
Output: incomplete JPEG (missing FFD9 footer)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended Fixes
|
||||
|
||||
### Phase P0: Critical (Must Implement)
|
||||
|
||||
| Fix | Description | LOC | Location |
|
||||
|-----|-------------|-----|----------|
|
||||
| **Frame validation** | `frame <= total_frames` | ~20 | `media_api.rs:707-718` |
|
||||
| **Crop validation** | `x+w <= width, y+h <= height` | ~15 | `media_api.rs:731-735` |
|
||||
| **JPEG header check** | `data[0..3] == [0xFF,0xD8,0xFF]` | ~10 | Helper function |
|
||||
| **JPEG footer check** | `data[-2..] == [0xFF,0xD9]` | ~10 | Helper function |
|
||||
| **Minimum size check** | `data.len() > 100` | ~5 | Helper function |
|
||||
|
||||
### Phase P1: Important (Should Implement)
|
||||
|
||||
| Fix | Description | LOC | Location |
|
||||
|-----|-------------|-----|----------|
|
||||
| **Black frame detection** | ffmpeg `-vf blackdetect` filter | ~30 | After extraction |
|
||||
| **Output seeking** | Move `-ss` after `-i` for precision | ~5 | `trace_agent_api.rs:527` |
|
||||
|
||||
### Phase P2: Enhancement (Nice to Have)
|
||||
|
||||
| Fix | Description | LOC | Location |
|
||||
|-----|-------------|-----|----------|
|
||||
| **Retry mechanism** | Max 3 attempts, offset +30 frames each | ~50 | Both endpoints |
|
||||
| **Fallback frame** | Extract middle frame if all fail | ~30 | Both endpoints |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Create Validation Module
|
||||
|
||||
Create `src/core/thumbnail/validator.rs`:
|
||||
|
||||
```rust
|
||||
pub fn validate_jpeg(data: &[u8]) -> Result<()> {
|
||||
// P0-1: Minimum size
|
||||
if data.len() < 100 {
|
||||
bail!("JPEG too small: {} bytes", data.len());
|
||||
}
|
||||
|
||||
// P0-2: JPEG header (SOI marker)
|
||||
if data[0..3] != [0xFF, 0xD8, 0xFF] {
|
||||
bail!("Invalid JPEG header");
|
||||
}
|
||||
|
||||
// P0-3: JPEG footer (EOI marker)
|
||||
if data[data.len()-2..] != [0xFF, 0xD9] {
|
||||
bail!("Incomplete JPEG");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Add Frame/Crop Validation
|
||||
|
||||
In `media_api.rs`:
|
||||
|
||||
```rust
|
||||
// P0-4: Validate frame number
|
||||
let total_frames: i64 = sqlx::query_scalar(...)
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
if frame > total_frames {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// P0-5: Validate crop dimensions
|
||||
if let (Some(x), Some(y), Some(w), Some(h)) = (q.x, q.y, q.w, q.h) {
|
||||
let (width, height): (i32, i32) = sqlx::query_as(...)
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
if x + w > width || y + h > height {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Integrate Validation
|
||||
|
||||
In both endpoints, after ffmpeg extraction:
|
||||
|
||||
```rust
|
||||
// Apply validation
|
||||
validate_jpeg(&output.stdout)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Cases
|
||||
|
||||
| Test | Input | Expected |
|
||||
|------|-------|----------|
|
||||
| Valid frame | `frame=500` (valid) | JPEG returned |
|
||||
| Frame exceeds | `frame=999999` | 400 BAD_REQUEST |
|
||||
| Valid crop | `x=100,y=100,w=200,h=200` | JPEG returned |
|
||||
| Crop exceeds | `x=1800,y=1000,w=200,h=200` | 400 BAD_REQUEST |
|
||||
| Empty video | corrupted video file | 500 INTERNAL_ERROR |
|
||||
| Black frame | fade-out frame | Retry or fallback |
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/core/thumbnail/mod.rs` | Add validator module |
|
||||
| `src/core/thumbnail/validator.rs` | New file (validation helpers) |
|
||||
| `src/api/media_api.rs` | Add validation in `face_thumbnail()` |
|
||||
| `src/api/trace_agent_api.rs` | Add validation in `get_trace_thumbnail()` |
|
||||
|
||||
---
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
| Phase | LOC | Time |
|
||||
|-------|-----|------|
|
||||
| P0 (Critical) | ~60 | 1-2 days |
|
||||
| P1 (Important) | ~35 | 1 day |
|
||||
| P2 (Enhancement) | ~80 | 2-3 days |
|
||||
| **Total** | ~175 | 4-6 days |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0.0 | 2026-05-27 | M5Max128 | Initial analysis complete |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps for M5Max48
|
||||
|
||||
1. Read this document
|
||||
2. Implement P0 fixes first
|
||||
3. Test with edge cases
|
||||
4. Add P1/P2 as needed
|
||||
5. Update `AGENTS.md` if adding new validation commands
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- `docs_v1.0/DESIGN/Processor_Refactoring_Assessment.md` - Processor refactoring priorities
|
||||
- `src/api/media_api.rs:700-764` - face_thumbnail implementation
|
||||
- `src/api/trace_agent_api.rs:394-556` - select_rep_face and get_trace_thumbnail
|
||||
- `ffmpeg -vf blackdetect` documentation
|
||||
Reference in New Issue
Block a user