doggy
doggy reverse
cowgirl
-- markers table — one row per labeled timestamp CREATE TABLE markers ( id INTEGER PRIMARY KEY, image_id INTEGER REFERENCES images(id) ON DELETE CASCADE, part_idx INTEGER NOT NULL DEFAULT 0, -- multi-part videos seconds REAL NOT NULL, -- 872.3 = 14:32 end_seconds REAL, -- optional, for ranges title TEXT, -- optional free-text primary_tag_id INTEGER REFERENCES tags(id), created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); -- secondary tags (many-to-many) CREATE TABLE marker_tags ( marker_id INTEGER REFERENCES markers(id) ON DELETE CASCADE, tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (marker_id, tag_id) );
-- row in markers table { id: 142, image_id: 4711, -- the cover for SSIS-456 part_idx: 0, seconds: 872.3, -- 14:32 end_seconds: null, title: "close-up · bedroom", primary_tag_id: 38, -- "doggy" created_at: 1735776300000, updated_at: 1735776300000 } -- secondary tags from marker_tags [ 12 "bedroom", 19 "close-up", 22 "POV" ] -- derived assets generated by ffmpeg, keyed by marker.id data/marker-thumbs/142.webp -- 320×180 static, ~40 KB ffmpeg -ss 872.3 -i video.mp4 -frames:v 1 -vf scale=320:-1 ... data/marker-previews/142.webp -- 5s animated, no audio, ~280 KB ffmpeg -ss 872.3 -t 5 -i video.mp4 -vf "scale=320:-1,fps=12" -loop 0 ... -- both are caches. delete them and they regenerate on next request. -- generation is queued, not blocking — same pattern as whisperjav.
<div class="tile"> <div class="tile-media"> <img src="/api/marker-thumb/142" // static, always loaded class="static"> <img src="/api/marker-preview/142" // animated webp, lazy-loaded on hover class="animated" loading="lazy"> <span class="timestamp">14:32</span> </div> <div class="tile-meta"> <span class="primary-tag">doggy</span> <span class="scene">SSIS-456 · part 1</span> </div> </div> // CSS handles the swap purely with hover + opacity transition. // No JS needed for the swap; image fetch is browser-lazy.