A two-hour recording is one asset, and that is the problem: "show me the part about pricing" has no answer when the only unit of retrieval is the whole file. This playbook cuts video into scenes and then describes those scenes — from the soundtrack, from the picture, or both.
What the Platform Gives You
| Node | Produces | Stored as |
|---|---|---|
Scene boundaries from optical-flow analysis |
|
|
Transcript with per-utterance start/end times |
|
|
Contact-sheet preview rendered from video frames |
local |
|
Resolution / blurriness / bitrate metrics |
quality component |
|
|
consistency component |
Scenes and transcript are both timestamped against the same asset, which is what makes them
joinable: a scene is [start, end), an utterance is [start, end), and "what was said in scene 4" is
an overlap query over the two stored components.
|
Important
|
|
Prerequisites
-
A running stack, and assets already in Loom for the video files (nodes resolve the asset by SHA-512 — see Transcribing for the Chat Agent for why that matters).
-
A worker with the OpenCV / video4j native runtime, which
scene-detection,thumbnailandqualityall need. It is present in themetaloom/cortex-serverimage. -
For the audio route, a Whisper runtime and model — same worker setup as the transcription playbook.
Route A — Describe Scenes from the Soundtrack
Cut the video, transcribe the speech, join on time. This is the cheaper route and it works on anything with dialogue: interviews, lectures, podcasts with video, meeting recordings.
{
"nodes": [
{ "id": "source", "type": "filesystem-source", "name": "Video library",
"options": { "path": "/media/video" } },
{ "id": "mime", "type": "filter-mimetype", "name": "Video only",
"options": { "allowedTypes": "video/*" } },
{ "id": "hash", "type": "sha512", "name": "SHA-512 identity" },
{ "id": "consistency", "type": "consistency", "name": "Decodable?" },
{ "id": "scenes", "type": "scene-detection", "name": "Scene boundaries",
"timeoutMs": 900000 },
{ "id": "speech", "type": "whisper", "name": "Transcribe",
"timeoutMs": 3600000 }
],
"edges": [
{ "id": "e1", "source": "source", "target": "mime" },
{ "id": "e2", "source": "mime", "target": "hash", "branch": "PASS" },
{ "id": "e3", "source": "hash", "target": "consistency" },
{ "id": "e4", "source": "consistency", "target": "scenes" },
{ "id": "e5", "source": "consistency", "target": "speech" }
]
}
scenes and speech both depend on consistency and nothing on each other, so Loom dispatches them
independently — potentially to different workers, in parallel. That is the whole reason to model this
as a graph rather than a script: the GPU box transcribes while the CPU box runs optical flow.
The consistency gate is worth the extra node. A truncated download produces a plausible-looking file
that wastes a half-hour of transcription; checking first is cheap.
Reading the result
ASSET=<asset-uuid>
curl -s -H "Authorization: Bearer $TOKEN" $LOOM/api/v1/assets/$ASSET/segments | jq
curl -s -H "Authorization: Bearer $TOKEN" $LOOM/api/v1/assets/$ASSET/transcripts | jq
Joining them is a query-side operation — no node does it for you today:
# For each scene, the utterances that overlap it
jq -n --slurpfile s scenes.json --slurpfile t transcript.json '
$s[0][] as $scene
| { scene: $scene,
said: [ $t[0].segments[]
| select(.start < $scene.end and .end > $scene.start)
| .text ] }'
Feed that per-scene text to an LLM, or simply ask the chat agent — it already reaches the transcript
through search_transcript and can quote the passage and the asset it came from.
|
Note
|
Scene segments are a whole-set replace. Re-running scene-detection on an asset deletes the
previous segments, so anything you stored keyed to old segment identifiers must be regenerated too.
|
Route B — Describe Scenes from the Picture
For content with little or no dialogue — b-roll, surveillance, silent product footage — the soundtrack tells you nothing and you need the frames.
What works today:
{
"nodes": [
{ "id": "source", "type": "filesystem-source", "name": "Video library",
"options": { "path": "/media/video" } },
{ "id": "mime", "type": "filter-mimetype", "name": "Video only",
"options": { "allowedTypes": "video/*" } },
{ "id": "hash", "type": "sha512", "name": "SHA-512 identity" },
{ "id": "scenes", "type": "scene-detection", "name": "Scene boundaries" },
{ "id": "sheet", "type": "thumbnail", "name": "Contact sheet" },
{ "id": "grade", "type": "quality", "name": "Quality metrics" }
],
"edges": [
{ "id": "e1", "source": "source", "target": "mime" },
{ "id": "e2", "source": "mime", "target": "hash", "branch": "PASS" },
{ "id": "e3", "source": "hash", "target": "scenes" },
{ "id": "e4", "source": "hash", "target": "sheet" },
{ "id": "e5", "source": "hash", "target": "grade" }
]
}
That gives you scene boundaries, a contact-sheet preview and quality metrics per video.
|
Warning
|
Per-scene visual captioning of video is not implemented today. The
Two ways forward until such a node exists:
|
A Note on consistency and Node IDs
Some nodes read an upstream result by node id, not by kind. thumbnail looks for
consistency → is_complete and refuses to render a preview for a file reported incomplete (unless
processIncomplete is set on the worker). If you name that node check instead of consistency, the
lookup silently finds nothing and the gate quietly stops working.
The same applies to loom, which reads md5sum → md5 and sha256sum →
sha256. When a playbook names a node a particular way, keep the name.
Doing Both
The two routes are the same graph with both branches attached — that is what a DAG is for:
Costs to keep in mind when you fan out like this:
-
scene-detectiondecodes the whole video for optical flow — CPU-heavy, default task timeout 300 s. Long files need it raised. -
whisperdefaults to concurrency 1 and a 600 s timeout. -
Both read the file from the shared media mount, so a single-spindle NFS export becomes the ceiling long before the CPUs do.
Pin the expensive kinds to the machines that suit them with CORTEX_NODE_WHITELIST — see
Docker → dedicated workers or the GPU pool.
Gotchas
| Symptom | Cause |
|---|---|
Run rejected with |
A kind in the graph is not registered on any online worker — see node availability. |
|
The worker lacks the OpenCV / video4j native runtime, or a mismatched OpenCV version is on the library path. |
Captions never appear for videos |
Expected: |
Segments disappeared after a re-run |
Whole-set replace: a shorter re-run deletes the surplus segments. |
Thumbnails skipped for some files |
The |
Scenes and transcript do not line up |
They are separate components joined on time by the consumer. Nothing in the pipeline aligns them. |
Next Steps
-
Turn scene text into another language — Translating Content.
-
Node reference — Scene Detection, Thumbnail, Quality.
-
Build the frame-sampling node — Cortex Examples.