212 lines
9.4 KiB
Markdown
212 lines
9.4 KiB
Markdown
|
|
# 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.
|