Academy films: locally generated physics videos of the roast and the brew
Test and deploy / test-and-deploy (push) Successful in 1m1s

Two short macro films generated on the local ComfyUI instance (Wan 2.2
14B t2v + lightx2v 4-step LoRAs, 9 clips at 832x480) and stitched with
crossfades and burned-in phase labels: roast-physics.mp4 (charge, drying,
Maillard, first crack, drop) and brew-physics.mp4 (grind, bloom,
immersion, espresso). A "Films — the physics in motion" card on the
Academy menu plays them with posters; opening a lesson hides and pauses
them. The service worker now bypasses /academy-video/ and /academy-audio/
(Range/206 responses the Cache API rejects) and only caches full 200s.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-08-11 18:12:15 -04:00
co-authored by Claude Fable 5
parent 385474ecef
commit cc735aba82
8 changed files with 77 additions and 3 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.
+42
View File
@@ -64,6 +64,48 @@
<main class="page-content">
<div id="academy-menu"></div>
<section class="panel-card" id="academy-films">
<div class="panel-head">
<h2>Films — the physics in motion</h2>
</div>
<div class="panel-body">
<p class="muted" style="margin-top: 0">
Two short macro films of what the lessons teach — every shot
generated on the local roaster's GPU.
</p>
<div class="academy-films-grid">
<figure class="academy-film">
<video
src="/academy-video/roast-physics.mp4?v=__ASSET_VERSION__"
poster="/academy-video/roast-physics-poster.jpg?v=__ASSET_VERSION__"
controls
muted
loop
playsinline
preload="metadata"
></video>
<figcaption>
The roast — charge · drying · Maillard · first crack · drop
</figcaption>
</figure>
<figure class="academy-film">
<video
src="/academy-video/brew-physics.mp4?v=__ASSET_VERSION__"
poster="/academy-video/brew-physics-poster.jpg?v=__ASSET_VERSION__"
controls
muted
loop
playsinline
preload="metadata"
></video>
<figcaption>
The brew — grind · bloom · immersion · espresso
</figcaption>
</figure>
</div>
</div>
</section>
<section class="panel-card hidden" id="academy-player">
<div class="panel-head">
<div>
+22
View File
@@ -3190,6 +3190,28 @@ select.f {
border-color: var(--ember);
}
.academy-films-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}
.academy-film {
margin: 0;
}
.academy-film video {
display: block;
width: 100%;
border-radius: var(--radius-lg);
border: 1px solid var(--line);
background: #1b1614;
}
.academy-film figcaption {
margin-top: 8px;
font-size: 12.5px;
color: var(--ink-2);
text-align: center;
}
/* Scene animations — restarted per slide because the SVG is rebuilt. */
@keyframes ac-draw-kf { from { stroke-dashoffset: 900; } to { stroke-dashoffset: 0; } }
.ac-draw { stroke-dasharray: 900; animation: ac-draw-kf 2.6s ease-out forwards; }
+4
View File
@@ -97,6 +97,9 @@ function renderMenu() {
function openLesson(track, lesson, index) {
current = { track, lesson, index };
$("academy-menu").classList.add("hidden");
$("academy-films")?.classList.add("hidden");
// Pause any playing film so it doesn't run (and download) behind the lesson.
for (const video of document.querySelectorAll("#academy-films video")) video.pause();
$("academy-player").classList.remove("hidden");
renderSlide();
}
@@ -106,6 +109,7 @@ function closeLesson() {
current = null;
$("academy-player").classList.add("hidden");
$("academy-menu").classList.remove("hidden");
$("academy-films")?.classList.remove("hidden");
renderMenu(); // reflect fresh progress
}
+9 -3
View File
@@ -64,7 +64,11 @@ self.addEventListener("fetch", (event) => {
if (
request.method !== "GET" ||
url.origin !== self.location.origin ||
url.pathname.startsWith("/api/")
url.pathname.startsWith("/api/") ||
// Media streams use Range requests (206 responses the Cache API rejects) and are far
// too large to belong in the static cache — let the browser handle them natively.
url.pathname.startsWith("/academy-video/") ||
url.pathname.startsWith("/academy-audio/")
)
return;
@@ -86,10 +90,12 @@ self.addEventListener("fetch", (event) => {
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok)
// Only full 200 responses: cache.put() throws on 206 partials.
if (response.status === 200)
caches
.open(CACHE_NAME)
.then((cache) => cache.put(request, response.clone()));
.then((cache) => cache.put(request, response.clone()))
.catch(() => {});
return response;
})
.catch(() => caches.match(request)),