NAS Impact Assessment — XIL_PROJECTROOT on a Network Mount
Written 2026-07-11, when XIL_PROJECTROOT was being moved from local disk
(/mnt/c/Users/shaba/xil-projects) to a NAS mount (/mnt/cloudsy/...).
Every workspace read becomes a network round-trip; every stat becomes an SMB
metadata call. This doc ranks where that hurts, what has already been
mitigated in code, and what to watch during and after migration.
The code root (XIL_CODEROOT, the repo itself) stays local — only content
workspace I/O is affected.
Ranked pain points
0. Timeline artifacts baked absolute XIL_PROJECTROOT paths — 403 after the root moved
Status: fixed (root-agnostic relative paths, 2026-07-11)
This was the first thing that actually broke on the NAS. The generated
timeline HTML embedded absolute workspace paths in LAYER_AUDIO and
CLIPS (e.g. /mnt/c/Users/shaba/xil-projects/daw/.../layer_dialogue.wav).
The GUI serves the timeline in an iframe via /gradio_api/file=<abs>, whose
allowed_paths is the current workspace root. Once XIL_PROJECTROOT
moved to /mnt/cloudsy/..., every embedded /mnt/c/... path fell outside
allowed_paths and the browser got HTTP 403 "File not allowed" for all
five layer WAVs and every clip — playback with no sound and (under the old
pre-prefetch code) no visible error. Regenerating on the NAS didn't help
either, because parsed/ had not been synced there yet, so xil daw bailed
with "Parsed JSON not found."
Root cause: an artifact that hard-codes the absolute root is invalidated by
any root move, and re-generating it requires the full source tree
(parsed/, cast configs) present at the new root.
Fix: render_html_timeline now stores every audio URL relative to the
timeline file's own directory (os.path.relpath, POSIX separators, keeping
the ?v={mtime} cache-buster). Layer WAVs sit beside the timeline →
S07E01_layer_dialogue.wav?v=…; stems resolve as
../../../stems/<slug>/<TAG>/<seq>_….mp3?v=…. The browser resolves these
against the iframe's document URL (/gradio_api/file=<abs timeline>), so the
same artifact works under any root — local or NAS — with no
regeneration. A plain file copy to the NAS is now sufficient; the prefetch
fetches the relative path directly (no /gradio_api/file= concatenation).
Guarded by test_paths_are_root_agnostic_relative.
Migration action: timeline HTML files copied from the old local tree still
contain the old absolute paths — re-sync or regenerate them so they carry
the relative form. (Done for the 26 episodes already present on the NAS on
2026-07-11; the413/S07E03 was not yet synced.)
1. Episodes-tab scan — every browser connect walked the whole workspace
Status: mitigated (GUI cache, 2026-07)
xil-gui's Episodes tab calls the XILU019 episode-status engine, which
rglobs + stats every stems/daw file and the full scripts/ tree per
episode. On local disk that is milliseconds; over SMB it is thousands of
serial metadata round-trips — and it fired on every browser connect
(demo.load).
Mitigation: _refresh_episodes results are now cached per workspace root
with a 5-minute TTL (_EPISODES_CACHE in xil_gui.py). New browser tabs get
cached rows instantly; the ⟳ Refresh button forces a rescan.
2. Timeline full-mix playback — 5 WAVs (~1–1.7 GB) streamed blind
Status: mitigated (browser prefetch with progress, 2026-07)
The timeline transport played five layer WAVs streamed straight off
/gradio_api/file=<NAS path>. Over a NAS the browser buffers stalled with
zero feedback — the "hang" that motivated this assessment.
Mitigation: the generated timeline HTML now prefetches all layers to Blobs
with an aggregate Loading audio… N% (X / Y MB) readout, per-layer MB rows,
and a Cancel button (second click on ▶ also cancels). Loaded blobs are cached
by URL + mtime cache-buster, so replays are instant. Single-clip preview and
the sound-profile workflow got the same treatment (Loading… N% in the
player strip). Remember: the timeline HTML is a build artifact — the fix
reaches an episode only after xil daw --episode <TAG> --dry-run
--timeline-html regenerates it (done for all existing artifacts on
2026-07-11).
3. GUI audio tabs — every selection re-read the file from the NAS
Status: mitigated (local read-through cache, 2026-07)
The Audio Preview and Audio Grading tabs handed NAS paths directly to
gr.Audio, and "play all" concatenation decoded every stem from the mount on
every click (and leaked a temp file per click).
Mitigation: _cached_audio_path() copies workspace audio to a bounded
local cache (~/.cache/xil-gui/audio/, 2 GB budget, oldest-mtime eviction)
with a Copying <name>… X / Y MB progress bar; keys are
sha1(path|size|mtime) so they self-invalidate. Concatenated "play all"
products are cached the same way (fixing the temp-file leak). Any OSError
degrades to direct streaming — playback never breaks because the cache did.
4. No I/O timeouts anywhere — a stalled mount is an indefinite hang
Status: open — mount configuration + startup probe recommended
Nothing in the pipeline or GUI sets I/O deadlines. A wedged NAS mount (sleep, network blip, SMB reconnect) turns every stage and every GUI handler into an indefinite hang, indistinguishable from "slow."
Recommendations:
- Mount with timeouts rather than hard-hanging defaults — for CIFS/SMB:
soft, sensibleecho_interval, andactimeoto batch attribute reads; for NFS:soft,timeo=...,retrans=.... - Add a coarse liveness probe at GUI/CLI startup (e.g.
os.statof the workspace root in a thread with a 5 s deadline) and print a clear banner ("workspace on NAS is not responding") instead of hanging silently.
5. Master export reads everything, every run
Status: open — acceptable cost, watch it
XILP011 (master export) full-loads all five layer WAVs each run, and
daw/assembly stages do per-stem pydub/ffmpeg decodes of full files. That is
1–1.7 GB pulled across the network per export, per attempt. Unlike the GUI
there is progress logging, so it is slow-but-visible rather than a hang.
Future option: a read-through staging copy for pipeline stages, reusing the same key scheme as the GUI audio cache.
6. Atomic renames and journal appends over SMB
Status: open — audit during migration
The pipeline leans on os.replace/rename for atomic writes and appends to
JSONL journals (sfx_*_edits.jsonl). On SMB, rename onto a target that
another process holds open (e.g. Gradio streaming a WAV while a re-export
replaces it) can fail with sharing violations, and append atomicity is not
guaranteed across clients.
Recommendation: wrap workspace renames in a small retry helper (3–5 attempts, short backoff) during migration; keep single-writer discipline for journals.
7. sync_to_NAS.sh — now a full-tree mirror (was a partial subset)
Status: fixed 2026-07-11 — rewritten to mirror the whole workspace
The casing concern originally noted here (DAW/ vs daw/) was already
resolved (the script uses lowercase daw/). The real problem, found while
running a sync for the cutover, was coverage: the old script pushed only
configs/ masters/ scripts/ voice_refs/ .regen* + root *.sh, and synced
daw/+stems/ only under an explicit --stems flag. parsed/ was never
synced at all — which is exactly why xil daw --timeline-html could not
regenerate a timeline natively on the NAS ("Parsed JSON not found").
Fix: rewritten to mirror the entire $XIL_PROJECTROOT in one
rsync -av --delete --partial ./ "$NAS"/, excluding only transient
caches/junk (.ruff_cache/, __pycache__/, *.pyc, .DS_Store,
Thumbs.db). This now includes parsed/, all content dirs, and the loose
root reports/logs. --dry-run is preserved.
Direction caveat (important): this is a local → NAS mirror with
--delete — correct only while the local disk is the source of truth
(i.e. during the cutover). Once XIL_PROJECTROOT points at the NAS and you
begin writing there (grades, sfx edits, new renders), re-running it would
erase those NAS-side changes. After cutover, retire the script or reverse
src/dst for a NAS→local backup.
Slowness note: even a --dry-run of the full tree is minutes-long — just
building the file list for daw/+stems/ is thousands of SMB stat
round-trips (a live instance of the metadata cost in #1/#4). Run the real
mirror in the background; the first pass moves ~24 GB, later passes are
incremental (only changed files transfer, but the traversal cost remains).
8. SFX grade scan — 842 serial ID3 reads
Status: mitigated (persistent grade cache, 2026-07)
The Grading tab's _scan_sfx_grades opened every SFX MP3 to read its ID3
grade tag on each scan. Over a NAS: ~842 file opens per refresh.
Mitigation: grades persist in <sfx>/.xil_grade_cache.json keyed by
relative path + size + mtime; a rescan re-reads only new/changed files, and
grading a file patches the cache in place. Corrupt/missing cache degrades to
a clean full rescan.
9. Miscellaneous frictions
Status: open — low individual cost
derive_paths()performs doubleexists()checks per call; in hot loops each becomes an SMB round-trip. Future: memoize layout detection per workspace root per process.- The staleness engine compares mtimes; SMB servers may truncate mtime granularity (commonly to 1–2 s). Same-second regenerations can be judged "not newer." Low risk given multi-second stage runtimes, but if phantom staleness appears post-migration, check timestamp granularity first.
- WSL drvfs already made birthtime/ctime unreliable for produce-time detection (stem-manifest mtime is used instead); verify that signal survives the NAS move too.
Migration checklist
- Fix the
DAW/vsdaw/casing insync_to_NAS.shbefore first sync (#7). - Mount with soft/timeout options, not hang-forever defaults (#4).
- Sync the whole source tree, not just
daw/— in particularparsed/andconfigs/. Withoutparsed/,xil daw --timeline-htmlcannot regenerate a timeline on the NAS ("Parsed JSON not found"). - Re-sync or regenerate every timeline HTML so it carries the new
relative audio paths (#0); a timeline copied before 2026-07-11 still
has absolute
/mnt/c/...paths and will 403 on the NAS. - First GUI session on the NAS: expect the first Episodes scan and SFX grade scan to be slow (cold caches); subsequent ones should be fast. If they are not, the caches are not persisting — investigate before blaming the NAS.
- Spot-check that a re-exported master replaces cleanly while the GUI is open (#6).
- Confirm playback: pick an episode, press ▶, watch for the
"Loading audio… N%" strip and per-layer MB counters, then sound. If the
progress strip completes but no audio plays, see the note below on
post-
awaitautoplay.
Post-await autoplay — verified working
The mix/clip prefetch calls .play() after await fetchAudioBlob(...).
Browsers gate media playback on user activation; a click on ▶ sets sticky
activation for the (same-origin) iframe document, which survives the
await, so play proceeds. Confirmed 2026-07-11 in the browser against the
NAS: S07E01 pressed ▶ → hourglass + "Loading audio…" strip → full-mix
playback started on completion. Recorded here only as the escape hatch if a
future browser/policy change blocks it: start each element muted and unmute
once playing, or fall back to native streaming with a buffered-progress
overlay instead of full prefetch.