From 2055524d30cfa227c31f0b0f6bad14dcb63351d3 Mon Sep 17 00:00:00 2001 From: cottongin Date: Tue, 22 Sep 2026 14:26:44 -0400 Subject: [PATCH] docs: add v0.1.2 polish design spec (placeholders, auto-upgrade, analysis cache) Co-authored-by: Cursor --- .../specs/2026-09-22-v0.1.2-polish-design.md | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-22-v0.1.2-polish-design.md diff --git a/docs/superpowers/specs/2026-09-22-v0.1.2-polish-design.md b/docs/superpowers/specs/2026-09-22-v0.1.2-polish-design.md new file mode 100644 index 0000000..f4b6d70 --- /dev/null +++ b/docs/superpowers/specs/2026-09-22-v0.1.2-polish-design.md @@ -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/ + / + 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 +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 +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` | 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` +- `thumbnailSpritesheets: Vec` (with absolute paths resolved from the cache dir) +- `captionFilePath: Option` (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` 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.