**jms:** *(ran /clear)* ok lets take a look at 330.0020 TDPyMovieplayer. this is a mostly empty touchdesigner project that was generated by 330.0010. 330.0020 TDPyMovieplayer is intended as a test project for working on TD files in python. orient yourself to the project and give me a project outline for developing the following. A TD TOE that contains a moviefile player that loads a folder of video files, gets the length of each video, plays them in a random order, starting in a random spot, and at a random speed, and jumps to the next video after a fixed duration of time (later we will convert this to midi control). **Claude:** I'll orient myself to the project first. I've read the project, the framework it came from, and the phase plan on 160.0010. Here's where things stand and what I'd build. ## Orientation [0020 - TDPyMovieplayer/](300-Code/330 - TouchDesigner/0020 - TDPyMovieplayer/) holds `testTDPyProject/` — a generator-fresh repo at commit `e90f8b3`, still carrying the Phase 0 placeholder network (noise → level → null) plus one uncommitted character change (`period` 0.175 → 1.175). The startup log shows the loop working end to end: build at launch, four successful rebuilds via `reload()`. So the scaffolding is proven and completely empty of content. Two things to note before design: - The inner repo is still named `testTDPyProject` — `.toe`, README, launcher and all. Regenerating with `new-project.bat TDPyMovieplayer` is cheaper than renaming by hand, and there's nothing here worth keeping. - 160.0010's Phase 3 says the build layer waits "for a real project to say which helpers it needs." **This is that project.** The outline below is as much a spec for Phase 3 as for the player. ## The one genuinely hard problem Everything in your description is easy except *"gets the length of each video."* A Movie File In TOP doesn't know a file's duration until it has loaded and parsed the header, which happens over some number of frames after you set the `file` parameter. There is no synchronous `get_duration(path)`. A naive Python loop over the playlist returns zeros or stale values for every file. That forces the project's central architectural choice, and it's worth deciding before writing anything: **A. Probe inside TouchDesigner.** A dedicated Movie File In TOP walks the playlist, one file every N frames, reading duration off an Info CHOP and writing it into a table. No dependencies, but it's an asynchronous state machine — the framework has never expressed one, so this is real work and the interesting kind. **B. Probe outside.** `ffprobe` (or a Python lib) reads durations at build time, synchronously, in about ten lines. Fast and testable on a normal machine, but adds an external binary the framework has so far avoided entirely. **My recommendation: A, with a JSON cache.** Probe in TD once, cache `{path, size, mtime → duration, fps}` to a gitignored sidecar, and skip the probe on every later launch when the cache is warm. You get the dependency-free version, the startup cost is paid once, and the async-probe machinery is exactly the kind of thing the framework needs to learn to build in code. One thing to check first, because it could shrink the problem: verify whether the Movie File In TOP's **Cue Point Unit** offers a fractional/normalized option in 2025.32050. If it does, "start at a random spot" needs no duration at all — you'd still want lengths for the speed/dwell math below, but the critical path gets much shorter. ## Architecture **The advance trigger is a plain Python function.** This is the decision that makes the MIDI conversion trivial later: ```python player.advance() # pick next clip, cue it, start it ``` The dwell timer's callback is a two-line shim that calls it. A MIDI note callback will be a different two-line shim calling the same function. Nothing else needs to change — the same pattern `controls.py` already uses, where the Panel Execute DAT's text is written from disk and holds no logic. **Timing** via a Timer CHOP with a callbacks DAT, not a frame counter. It gives a real cycle callback, and the dwell length becomes a parameter rather than arithmetic. **Playback** starts as one Movie File In TOP and moves to A/B later — switching `file` on a single TOP gives you a black frame or a hitch at every cut. Two TOPs, preload the next during the current clip's dwell, cross with a Switch TOP. Don't build this until you've seen the single-player hitch and decided it matters. **Speed, start point and dwell interact.** With speed `s` and dwell `T`, a clip consumes `s·T` seconds of media. Either constrain the random start to `[0, duration − s·T]` so a clip never runs out early, or let it run off the end and accept it. Worth an explicit toggle rather than an accident — this is the main thing durations buy you beyond the random start. **State lives in the network, not in module globals.** A playlist Table DAT, plus custom parameters for seed, dwell, and speed range. Note the tension with clear-and-rebuild: `build()` destroys its own container, so anything that must survive a rebuild sits beside it at `/project1`, the way the rebuild button already does. Seeding the RNG makes a sequence reproducible, which matters a lot when debugging something this stochastic. **Media path is machine-local** — the videos don't go in the repo. A gitignored config file or env var with a sane fallback, decided in Phase 1. ## Phases **Phase 1 — playlist.** Regenerate as `TDPyMovieplayer`. Scan a configured folder for video extensions, write a Table DAT playlist with path/name/size. No playback yet. Pure Python, unit-testable outside TouchDesigner. Settles the media-path config question. **Phase 2 — one clip, playing.** A single Movie File In TOP plus a null out. Load one file, play it, get it on screen. Establishes the parameter names — `file`, `play`, `cue`, `cuepoint`, `speed`, and their real internal spellings, which are regularly not what the UI labels say (`mono`, not `monochrome`). Record them; guessing costs a warning per the `set_par` design, but the point is to stop guessing. **Phase 3 — durations.** The async prober and its cache, per the decision above. This is the phase that can go long, and the only one that can. Ends with every playlist row carrying a duration and an fps. **Phase 4 — the cycle.** Timer CHOP, callbacks DAT, `advance()`. Random order (shuffled-deck, not independent draws, so you don't get immediate repeats), random start, random speed. Fixed dwell. The player is done at the end of this phase. **Phase 5 — controls.** A panel beside the build container: dwell, speed range, seed, a skip button, a reload-playlist button. Note the known open issue — `activeViewer` doesn't stick when set from code, so the first click of a session lands on an inert panel until the flag is switched by hand. This phase is where that either gets fixed (the untested `delayFrames=1` hypothesis) or gets loud. **Phase 6 — seamless cuts.** A/B players and preloading, if Phase 4 showed a visible hitch. **Phase 7 — MIDI.** A MIDI In callback shim calling `advance()`, and mapping CCs onto the parameters from Phase 5. Small by construction if the earlier phases hold the line. Phases 1–4 are the actual project. 5–7 are extensions, and 6 is conditional. ## What I'd like decided before I start 1. **Duration strategy** — in-TD prober with cache (my recommendation), or ffprobe? 2. **Regenerate as `TDPyMovieplayer`,** discarding `testTDPyProject`? Nothing is lost but the one-character `period` edit. 3. **Where the videos live** — do you have a folder in mind, or should Phase 1 invent a config convention? 4. **Where this outline gets recorded.** 160.0010's plan lives on its catalog page, but 330.0020 deliberately has no catalog page yet, pending the `160` vs `110`/`140` question. I can park the plan in the Working Notes of [0020 - TDPyMovieplayer.md](300-Code/330 - TouchDesigner/0020 - TDPyMovieplayer.md) for now and move it when the catalog page exists — or you can settle the catalog page's home and I'll write it there properly. **jms:** 1. Prober in TD if it works 2. i deleted the folder. regenerate as TDPyMovieplayer 3. in a folder in the repo, contents in gitignore. 4. create the page in 160. 5. i have created a blank repo at https://github.com/wtkns/TDPyMovieplayer.git set this as the remote for the repository **Claude:** Both siblings are repo roots, so I'll flatten: `0020 - TDPyMovieplayer/` will *be* the repo (last time it was nested only because the generated name didn't match). Generating with the clean name, then renaming the folder to carry the vault number — that keeps `TDPyMovieplayer.toe` and `TDPyMovieplayer_vEnv` free of numbering artifacts. Now the remote and the commit. Nothing was written. Falling back to the Write tool for this one. Now the index entries and the code-side page. All five points are done. **Repository** — [0020 - TDPyMovieplayer/](300-Code/330 - TouchDesigner/0020 - TDPyMovieplayer/) regenerated with `new-project.bat TDPyMovieplayer`, remote set to `https://github.com/wtkns/TDPyMovieplayer.git`, and both commits pushed to `main`: - `d78201a` — generated project - `8df4df8` — `media/` folder and a README opening that describes the player rather than the framework Two judgment calls worth flagging: - **Flattened the nesting.** The numbered folder *is* the repo now, matching `330.0010` and `330.0060`. Last time's `testTDPyProject/` sublevel existed only because the generated name didn't match the folder. - **Generated as `TDPyMovieplayer`, then renamed the folder** to carry the vault number, rather than passing `0020 - TDPyMovieplayer` as the project name. The generator derives the `.toe` filename, the vEnv name and the README title from the name given, so the numbered form would have leaked vault numbering into a repo published as `TDPyMovieplayer` — which is what happened to `330.0010`, whose committed `0010 - TouchDesigner-Python_vEnv/` still carries it. I recorded this as a folder-wide convention in [00010 - TouchDesigner Notes.md](300-Code/330 - TouchDesigner/00010 - TouchDesigner Notes.md). `media/` uses the same tracked-but-empty pattern as the vEnv folders — its own `.gitignore` with `*` and `!.gitignore` — so the folder survives a clone while nothing in it is ever committed. Documented in the root `.gitignore` and the README. **Vault pages** — [0020 - TDPyMovieplayer.md](100-Projects/160 - Software/0020 - TDPyMovieplayer.md) created in `160 - Software` with the full seven-phase plan, and [330.0020](300-Code/330 - TouchDesigner/0020 - TDPyMovieplayer.md) rewritten with the real repository, setup and layout notes. Both indexes updated. No new tags needed — `#video` and `#live-visuals` were already registered. One thing I want to name before Phase 1: the plan commits to the in-TD prober, but Phase 2 should check whether **Cue Point Unit** offers a fractional option in 2025.32050. If it does, the random-start requirement needs no durations at all, and Phase 3 — the one phase whose length is genuinely unknown — comes off the critical path for everything except the speed/dwell overrun math. Cheap to check, and it's the only thing that could meaningfully reshape the schedule. The vault changes aren't committed — say "update and push" when you want the session wrapped up. **jms:** update and push **Claude:** Now the session log entry. Now Site Notes' Current Work. Now the transcript.