docs: add v0.1.2 polish design spec (placeholders, auto-upgrade, analysis cache)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
211
docs/superpowers/specs/2026-09-22-v0.1.2-polish-design.md
Normal file
211
docs/superpowers/specs/2026-09-22-v0.1.2-polish-design.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# v0.1.2 Polish: Placeholders, Auto-Upgrade, and Analysis Cache
|
||||
|
||||
**Date**: 2026-09-22
|
||||
**Version**: 0.1.2
|
||||
**Status**: Design
|
||||
|
||||
## Overview
|
||||
|
||||
Three polish features for v0.1.2:
|
||||
|
||||
1. Fix misleading placeholder text shown before any video is loaded
|
||||
2. Automatically use cached HQ video files instead of prompting to upgrade
|
||||
3. Cache video analysis results (waveforms, keyframes, thumbnails, captions) to avoid reprocessing
|
||||
|
||||
## 1. Idle-State Placeholder Text
|
||||
|
||||
### Problem
|
||||
|
||||
When the app first loads with no video, the timeline shows "Generating Waveform..." and "Generating Thumbnails..." in the placeholder lanes. No work is happening — this is misleading.
|
||||
|
||||
### Solution
|
||||
|
||||
Make `drawTimeline` context-aware by accepting a `hasVideo` boolean (derived from `session.status === 'ready'`).
|
||||
|
||||
- **No video loaded** (`hasVideo === false`): Draw empty lane backgrounds with no text.
|
||||
- **Video loaded, data pending** (`hasVideo === true`, data missing): Show "Generating Waveform..." / "Generating Thumbnails..." as today.
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `src/lib/timeline/renderer.ts`: Add `hasVideo` parameter to `drawTimeline()`. Update `drawPlaceholderLane()` to accept a boolean controlling whether to show text. When `hasVideo` is false, render the background rectangle but skip the text label.
|
||||
- `src/lib/components/Timeline.svelte`: Pass `session.status === 'ready'` as the `hasVideo` argument.
|
||||
|
||||
## 2. Auto-Use Cached HQ Video
|
||||
|
||||
### Problem
|
||||
|
||||
When a video's HQ (export) file is already cached on disk from a prior session, the app still downloads a low-res preview, then shows an "Upgrade" toast to switch to HQ. The user should get the HQ version immediately.
|
||||
|
||||
### Solution
|
||||
|
||||
At the start of `beginDownload()`, check the export cache first. If the HQ file exists:
|
||||
|
||||
- Set `activeVideoPath` to the HQ file immediately
|
||||
- Set `exportFilePath`, `exportStatus = 'complete'`, `exportProgress = 1.0`
|
||||
- Mark preview as complete too (skip the preview download entirely)
|
||||
- Run `triggerPostDownloadProcessing()` against the HQ file
|
||||
- No upgrade toast shown
|
||||
|
||||
When no cached HQ exists, the existing two-track download flow (preview first, background HQ) continues unchanged. The upgrade toast still appears when HQ completes mid-session — silently swapping the video source during active playback would be jarring.
|
||||
|
||||
### Behavior Matrix
|
||||
|
||||
| HQ cached? | Preview cached? | Behavior |
|
||||
|---|---|---|
|
||||
| Yes | (irrelevant) | Use HQ directly. No preview download. No toast. |
|
||||
| No | Yes | Use cached preview. Start background HQ download. Toast on HQ completion. |
|
||||
| No | No | Download preview, start background HQ. Toast on HQ completion. |
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `src/lib/stores/videoSession.svelte.ts`: Restructure `beginDownload()` to check export cache before preview cache. Add early-return path that sets HQ as active video.
|
||||
|
||||
## 3. Analysis Cache System
|
||||
|
||||
### Problem
|
||||
|
||||
Every time a video is loaded, waveforms, keyframes, and thumbnails are re-extracted from scratch via ffmpeg. For a 5-minute video this takes 10-30 seconds. Re-opening the same video should be instant.
|
||||
|
||||
### Architecture
|
||||
|
||||
#### Cache Directory
|
||||
|
||||
```
|
||||
~/Library/Application Support/gui-video-clipper/cache/
|
||||
<url-hash>/
|
||||
manifest.json
|
||||
thumbnails/
|
||||
frame_000001.jpg
|
||||
frame_000002.jpg
|
||||
...
|
||||
captions/
|
||||
en.vtt
|
||||
```
|
||||
|
||||
The URL hash is the first 16 hex characters of the SHA-256 of the source URL. This provides sufficient collision resistance while keeping paths short.
|
||||
|
||||
#### Manifest Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"sourceUrl": "https://youtube.com/watch?v=abc123",
|
||||
"title": "Example Video",
|
||||
"duration": 312.5,
|
||||
"fps": 30.0,
|
||||
"createdAt": "2026-09-22T18:00:00Z",
|
||||
"lastAccessedAt": "2026-09-22T18:05:00Z",
|
||||
"waveformTiers": {
|
||||
"tier0": [0.1, 0.5, ...],
|
||||
"tier1": [0.1, 0.3, 0.5, ...],
|
||||
"tier2": [0.05, 0.1, 0.15, ...]
|
||||
},
|
||||
"keyframePositions": [0.0, 2.5, 5.0, 7.5],
|
||||
"thumbnailSpritesheets": [
|
||||
{
|
||||
"filePath": "thumbnails",
|
||||
"startIndex": 0,
|
||||
"count": 100,
|
||||
"thumbWidth": 160,
|
||||
"thumbHeight": 90,
|
||||
"columns": 10,
|
||||
"intervalSeconds": 1.0
|
||||
}
|
||||
],
|
||||
"captionFilePath": "captions/en.vtt"
|
||||
}
|
||||
```
|
||||
|
||||
**Data sizing**: Waveform tier2 is at most 200K floats (~1.6 MB as JSON). Tier0 and tier1 add ~100 KB combined. Keyframes are a few hundred entries. This all fits comfortably inline in JSON. Thumbnails are JPEG files on disk, referenced by relative path.
|
||||
|
||||
All paths in the manifest are relative to the cache entry directory.
|
||||
|
||||
#### New Rust Service: `cache_manager.rs`
|
||||
|
||||
```rust
|
||||
// Public API
|
||||
pub fn cache_dir_for_url(url: &str) -> PathBuf
|
||||
pub fn load_manifest(url: &str) -> Option<CacheManifest>
|
||||
pub fn save_manifest(url: &str, manifest: &CacheManifest) -> Result<(), String>
|
||||
pub fn clear_cache_entry(url: &str) -> Result<(), String>
|
||||
pub fn clear_all_cache() -> Result<(), String>
|
||||
pub fn get_cache_stats() -> Result<CacheStats, String>
|
||||
pub fn thumbnail_cache_dir(url: &str) -> PathBuf
|
||||
pub fn caption_cache_dir(url: &str) -> PathBuf
|
||||
```
|
||||
|
||||
`CacheManifest` struct mirrors the JSON schema above. `CacheStats` contains `entry_count: usize` and `total_bytes: u64`.
|
||||
|
||||
#### New IPC Commands
|
||||
|
||||
Added to `src-tauri/src/commands/` (new file `cache.rs` or added to existing):
|
||||
|
||||
| Command | Signature | Purpose |
|
||||
|---|---|---|
|
||||
| `load_cached_analysis` | `(url: String) -> Option<CachedAnalysis>` | Load all cached data for a URL |
|
||||
| `save_analysis_to_cache` | `(url, waveform_tiers, keyframe_positions, thumbnail_spritesheets, caption_file_path) -> ()` | Persist analysis results |
|
||||
| `clear_video_cache` | `(url: String) -> ()` | Delete one video's cache |
|
||||
| `clear_all_cache` | `() -> ()` | Delete the entire cache dir |
|
||||
| `get_cache_stats` | `() -> CacheStats` | Get entry count and total size |
|
||||
| `get_thumbnail_cache_dir` | `(url: String) -> String` | Get the cache directory path for thumbnail output |
|
||||
| `get_caption_cache_dir` | `(url: String) -> String` | Get the cache directory path for caption output |
|
||||
|
||||
`CachedAnalysis` contains:
|
||||
- `waveformTiers: WaveformTiers`
|
||||
- `keyframePositions: Vec<f64>`
|
||||
- `thumbnailSpritesheets: Vec<ThumbnailSpritesheet>` (with absolute paths resolved from the cache dir)
|
||||
- `captionFilePath: Option<String>` (absolute path)
|
||||
|
||||
`save_analysis_to_cache` accepts the individual analysis fields rather than a wrapper struct, keeping the IPC boundary simple. The Rust side constructs the `CacheManifest` from these fields plus metadata (source URL, title, timestamps) looked up from the manifest or computed fresh.
|
||||
|
||||
#### Frontend Integration
|
||||
|
||||
**`videoSession.svelte.ts` — `triggerPostDownloadProcessing()`**:
|
||||
|
||||
1. Call `load_cached_analysis(session.url)` before running ffmpeg.
|
||||
2. On cache hit: populate session state directly, jump to `processingStep = 'done'`. Skip all ffmpeg extraction.
|
||||
3. On cache miss: run extraction as today. After all steps complete (waveform, keyframes, thumbnails, captions), call `save_analysis_to_cache(...)` to persist.
|
||||
|
||||
**New TypeScript bindings**: `src/lib/bindings/cache.ts` with wrapper functions for the new IPC commands.
|
||||
|
||||
#### Thumbnail Path Changes
|
||||
|
||||
Currently, `extract_thumbnails` (Rust command) generates a UUID-based temp dir for thumbnails. With caching:
|
||||
|
||||
- Add an optional `output_dir: Option<String>` parameter to the `extract_thumbnails` Tauri command. When provided, thumbnails are written there. When `None`, the existing UUID-based temp dir behavior is preserved as a fallback.
|
||||
- The frontend calls `get_thumbnail_cache_dir(session.url)` to get the cache-managed output path, then passes it to `extract_thumbnails`.
|
||||
- Similarly, `get_caption_cache_dir(session.url)` provides the path for caption file output.
|
||||
|
||||
### Cache Invalidation UX
|
||||
|
||||
**Preferences Panel** (`PreferencesPanel.svelte`):
|
||||
- New "Cache" section at the bottom
|
||||
- Display total cache size (e.g., "Cache: 450 MB across 12 videos")
|
||||
- "Clear All Cache" button with confirmation
|
||||
|
||||
**Per-Video Reprocess** (in `StatusBar.svelte` or near the transport controls):
|
||||
- A small "↻" (reprocess) button visible when a video is loaded and processing is complete (`processingStep === 'done'`)
|
||||
- Calls `clear_video_cache(session.url)` then re-runs `triggerPostDownloadProcessing()` on the current video file
|
||||
- This re-extracts analysis data only — it does not re-download the video
|
||||
|
||||
### Future: Project Files
|
||||
|
||||
The cache manifest structure is designed to be referenced by future project files. A project file would store:
|
||||
- The source URL
|
||||
- Clip regions (start/end times, labels, colors)
|
||||
- Caption settings
|
||||
- A reference to the cache entry (by URL hash)
|
||||
|
||||
This design means a project file doesn't duplicate analysis data — it points to the cache. If the cache is cleared, the project can re-derive everything from the source URL.
|
||||
|
||||
No project file save/load UI is implemented in this version. The manifest structure is the forward-looking scaffold.
|
||||
|
||||
### Dead Code Annotations
|
||||
|
||||
Any types or fields scaffolded for future project file support (e.g., fields in `CacheManifest` not yet read by the frontend) will be annotated with `#[allow(dead_code)]` and a `// Future: used by project file save/load` comment. This satisfies the requirement to handle dead code warnings proactively (item 3b).
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Zero new warnings/errors**: All existing warnings and errors were recently resolved. Any new warnings introduced during implementation must be addressed immediately.
|
||||
- **macOS-only paths**: `~/Library/Application Support/` is macOS-specific, which matches the project's macOS-only target.
|
||||
- **Backward compatibility**: Existing temp-dir downloads (preview/export) continue to work as before. The cache is additive — it doesn't replace the download mechanism.
|
||||
Reference in New Issue
Block a user