Educational Videos
Direction noted — NOT decided, NOT built
The shape below was discussed 2026-05-28, after the weighted-saw (Fierastraul) prep and an example 47-minute full-session video (8Rk5wZofAv.mp4) surfaced the question. It is what we lean toward, not a decision — don't ship code against it without re-confirming the architecture.
Revisited 2026-09-22. The "we'll build it when there's a need" framing has expired: the legacy import put 1,049 exercises on production and left 957 videos, 279 hours, with nowhere to go. See What the legacy import settled for what that narrowed and the one fork that has to be picked before code.
What this would be for
A category of platform content that's a single video the patient watches passively, distinct from:
- Exercises (the prescription building block —
reps_based/duration_based/static). See features/exercise-library/rendering-strategy.md. - Sessions (clinician-built playlists of exercises, with the session-run lifecycle, session player overlays, TV companion phone pairing, pose tracking, pain ingest).
The validating example is a 47-minute pre-recorded full session video. Initially proposed modelling that as a new "single-video session" shape — but that would have pulled all the session-engagement machinery (cues, overlays, TV companion, exercise events) into a passive-watching use case and required every future session change to carry a "disable for this kind" branch. The point of educational_videos as a separate type is to avoid exactly that cross-cutting tax.
What it explicitly is NOT
An educational_video deliberately does not touch:
- ❌
sessions/session_runs— no run lifecycle, noended_naturally/ended_explicit/auto_closed. - ❌ Session player overlays — no cues, no rep counter, no set badge, no between-card, no countdowns.
- ❌ TV companion — no phone pairing, no companion-mode messaging. If these need a TV surface, it's a flat-video screen, not a companion-controlled session.
- ❌ Exercises /
exercise_renders/ exercise catalog / exercise prescriptions. - ❌ Pose tracking config, dose dialog, capability snapshot.
- ❌
session_exercise_events,session_pain_events.
If any of those does turn out to be wanted at build time (e.g. lightweight watch-progress for stats), it's a deliberate add — not a feature-flag through the session pipeline.
Sketch (intent only)
- DB: an
educational_videostable —(id, slug, name, description, language, video_id, playback_hls_url, tv_mp4_urls, duration_seconds, thumbnail_url, status, created_*, updated_*). Per-language rows or(slug, language)UNIQUE — pick at build time. - Media: reuses the static-ingest pipeline (Bunny Stream upload + TV Constrained Baseline rungs + Bunny auto-thumb). The existing
POST /v1/exercises/staticendpoint generalizes lightly — accepts(owner_kind, owner_id, language)so the S3 prefix templates toplatform/educational/{slug}/instead ofplatform/exercises/{slug}/. All baking + Bunny logic stays unchanged. - API: its own thin domain — list / get / publish / archive. Standard CRUD; no session-domain coupling.
- Patient UI: a separate "Watch a video" / "Library" surface, distinct from "Start session" and "Do an exercise." Plain HTML
<video>element bound to the Bunny URL. - TV (if needed): parallel "Videos" surface that reads the baseline rungs directly — no companion.
What the legacy import settled (2026-09-22)
The design above predates the legacy catalog landing on production. 1,049 exercises are now live, all of them static — one finished MP4 each — and 957 legacy videos, 279 hours, are still waiting for this content type. That is no longer a hypothetical 47-minute file; it is the majority of the remaining bytes. Sizes, and the ingest ceiling that currently blocks the long ones, are in reference/legacy-media-import.md → What is left.
Three things the import decided, which narrow the sketch:
1. The slug is a source key, so it cannot classify anything. Legacy content is slugged lx-{video_id} — the legacy video id, chosen deliberately because it is already stable, already unique and already the S3 filename. It says nothing about whether the video is an exercise or a lecture, which means one legacy video can honestly be both. RestartiX Neck Academy went in that way on purpose: it interleaves exercise clips with Q&A recordings and was imported whole as static exercises.
2. Nothing collides, but bytes duplicate. An educational_videos table with its own lx- slug collides with nothing — a different table, and a different Storage Zone prefix (platform/educational/{slug}/ vs platform/exercises/{slug}/). The cost is elsewhere: if the media artefact keys on the consumer rather than on the source, a video that is both is encoded twice, stored twice on Bunny Stream, and served as two unrelated videos with separate watch statistics.
3. So the sketch has a fork in it that has to be picked before code. Does an educational video get its own ingest, or may it point at the content_files / exercise_renders row an exercise already produced for the same source? The sketch above assumes the first. The import argues for the second, because all of this content arrives keyed by source video id in the first place, and because a re-encode of an already-encoded master is a quality loss as well as a cost.
What does not need deciding yet. Content already imported stays an exercise — there is no migration implied by any of the above. The new type only has to cover the 957 videos that are not exercises today, and for those the fork never arises: there is no second copy to reconcile. That path is available whichever way the fork eventually goes, and it is the cheap first build.
One data correction to carry: the note that untyped legacy rows are "~12 min webinars" is a mean dragged by a long tail and is not a usable filter. The untyped rows inside the Restart Coloană programmes are a median of 2m36 and all under 8 minutes — they are exercises, and they imported as exercises without trouble. _exercise_type does not predict length or kind; measure per selection.
Open questions when we revisit
These are deliberately not answered now:
- Name —
educational_videos,library_videos,videos,content_videos? Pick at build time. ("Educational" matches how the team described them initially but constrains semantically.) - Categorization / taxonomy — do these have categories the patient browses? Body region tagging? Or just a flat searchable list?
- Assignability — clinician says "patient should watch this," or purely patient-driven discovery? Both?
- Stats — do they count toward anything (watch progress, completion, time-spent)? If yes, a lightweight watch-event ingest path — but not via
session_runs. - Tenancy — platform-curated only, or clinics can upload their own (which raises org-private storage + entitlement questions)?
- Lifecycle status enum — same
draft | published | archivedas exercises, or different? - Static-ingest endpoint generalization — exact wire shape of the
(owner_kind, owner_id, language)parameters when we get there. Or split into per-owner endpoints — implementation detail. - Own ingest, or reuse an exercise's render? — the fork described above. Not an implementation detail: it decides whether the media artefact keys on the consumer or on the source video, and therefore whether a video that is both an exercise and a lecture exists once or twice on Bunny.
- Does the long-video ingest ceiling get fixed first? — 181 of the waiting videos are over 30 minutes, which the current synchronous static-ingest path cannot reliably complete. Whatever fixes that for exercises fixes it here too, so the ordering matters.
Placeholder example
.platform/synced/8Rk5wZofAv.mp4 — 47 minutes, 312 MB, 720p H.264 + AAC. Currently not processed, not uploaded to S3. Stays as a local placeholder until we revisit this design and build for real.
See also
- features/exercise-library/rendering-strategy.md → Render models — the
staticexercise kind wraps a single MP4 as an exercise (prescribable, browsable in the exercise catalog). Full-session pre-recorded videos that need neither the exercise wrapper nor the session-run lifecycle should land here instead. - The architectural lesson is recorded under "Why this is its own type, not a session shape" above — preserve that reasoning when revisiting.