Studio 2.0 - #99
Draft
senigami wants to merge 119 commits into
Draft
Conversation
* stubbs * more detailed connections * robust scaffolding * add missing functions and cleanup * fixing issues before they happen * fix some other issues * added dependency contracts * added missing scaffolding * pr fix and better UX navigation * add manifest version * phase 1 complete * final final commit v2
* phase 2 * Update test_domain_contracts.py * Update phase_2_domain_contracts.md * Update __init__.py * Fix for build * Update phase_2_domain_contracts.md
…93) * engine contract, registry, registry, validator * fixing bugs * update plan * next part * VoxtralVoiceEngine * xtts * first-pass bridge-backed synthesis contract * voice_build and voice_test now opt into the new bridge-backed synthesis path behind USE_V2_ENGINE_BRIDGE * Path.replace() -> shutil.move() * enhance importance of websocket communication
* normalized websocket event contract * Preserve job status in normalized websocket updates * Implement progress service throttling and ETA safeguards * Implement revision-safe artifact reconciliation * Allow explicit progress resets in ProgressService * Harden progress timing and stale websocket guards * Preserve live job metadata across hydration and websocket updates * Align progress ETA controls with V2 update semantics * `authoritativeFloor` is only honored in some paths right now. The main ` * Simplify predictive progress handoff and staged test inputs * Fix predictive bar ETA anchoring and floor handling * Stabilize predictive progress floor handling * Extract predictive progress bar helpers * Refactor predictive progress bar into engine and debug helpers * Reorganize agent rules into task-focused routers
#101) * Expose raw smoothing budget in progress bar debug * Expand phase plans for TTS server and settings UX * Add TTS Server feature flag and bridge routing * Update test_tts_client.py * Add explicit Studio boot wiring and orchestrator flag * Complete Phase 5 orchestrator foundation and TTS server routing * Implement core Studio 2.0 orchestration task bodies * Update registry.py
…avigation realignment (#103) * Add named feature flag constants and helper tests * Document Phase 6 handoff and frontend cutover assumptions * Clarify Phase 6 queue and header cutover plan * Introduce live-jobs store for frontend queue state Implement hydration coordinator for queue snapshots Wire queue and header badge to shared sync boundary Fix duplicate hydration pull on initial websocket connect Normalize overlay pruning timestamps to prevent stale state regressions * Mark Phase 6 patch 1 checks complete * Fix backend ETA projection for live progress events * Harden startup ETA and speed metric training * Checkpoint progress ETA history storage * Move render performance history to database * Add collaboration rules for Codex Add .agents/rules/collaboration-with-codex.md: a new agent rule document defining expectations for working with Codex. It instructs implementers to follow prompts exactly, avoid unrelated edits, run `git status --short` and inspect diffs before handoff, clean up transient/scratch files, and never delete tracked files unless asked. It also documents state handling (Durable Studio 2.0 in SQLite; `state.json` as a migration shim), UI component responsibilities, job `status` as authoritative, ETA training restrictions (only successful persisted `done` jobs), test requirements when changing behavior (update product-rule tests and do not weaken tests), and a final handoff checklist (changed files, tests run and results, remaining risks, and artifacts). * Fix progress hook test expectations * Update .gitignore * Stabilize shell hydration state in the app shell * Wire project subnavigation into route shell * Harden project route transitions * Harden queue hydration source tracking * Cut over queue route boundary * Mark Phase 6 checklist complete * Polish Phase 6 cleanup seams * Refine project chapter breadcrumb navigation * Remove duplicate chapter dropdown option * Remove tracked frontend scratch artifacts * Move performance retention to startup * Improve worker fallback observability * Remove debug logs and name layout z-index tiers
* Begin Phase 7 settings route foundation * Phase 7 production blocks and export actions * Add batch generation actions to production editor * Add render batch recovery actions * Add Script view and export polish * Add Script range selection and split assignment * Place it in the ScriptView playback polish slice, not the source-text or * Highlight active playback batch in Script view * Tighten Script playback highlight spacing * Fix pre-push lint fallout * Add TTS Engines settings shell * Clarify TTS Engines install action status * Restore API settings tab * Mark API settings tab complete * Add About tab diagnostics * Fix settings deep links and Voxtral controls * Fix settings deep links and Voxtral controls * Fix Voxtral settings metadata and reload caching * Expose plugin-owned engine schema metadata * Remove legacy settings tray * Finalize plugin enablement contract * Fix voice engine filter labels * Fix cloud voice control fallbacks * Harden voice plugin hook contract * Bump python-multipart to patched version * Tighten voice gating and disabled voice tabs * Mark phase 7 ready for handoff * Fix settings test expectations
* Move queue into companion drawer * Add project snapshot and export manifest foundation * Fix plugin template whitespace * Add dated project backup bundle contract * Add downloadable project backup archive * Align backup archive format with issue 19 * Add stored project backup history * Expose project assembly and backup tabs * Finalize project tabs and backup management * Unify inline editing across project tabs * Unify chapter assignment feedback and cleanup flow * Restore default selection and paragraph click targets * Expand book paragraph hit targets * Fix chapter audio export and voice readiness handling * Tighten chapter header wrap and clarify stability mode * Fix voice storage paths and rebuild status * Clarify voice rebuild status and sample playback * Fix stale voice and settings test expectations * Add portable voice bundle export and import * Make voice export/import more discoverable * Update phase_8_cleanup_and_legacy_removal.md * Harden chapter asset resolution
Contributor
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
* Fix voice profile resolution for narrator defaults and renames * Harden filesystem path joins across chapter and project APIs * fix push * Update projects.py * Harden backend path handling for CodeQL * fix tests * Harden path containment in project and chapter helpers * The lesson is to keep the safety boundary explicit and local: - use `for * Update chapters.py * Harden path containment and sanitize error responses * fix tests * That rule is not currently in `.agent/rules.md`, so it should be added: * Refactor settings route into focused panels * Refactor ChapterEditor into focused hooks and components * Split chapter subcomponent tests into focused files * Split voice router into focused modules * Refactor chapter compatibility helpers into smaller modules * Split chapter and job modules into focused files * Split chapter and voice tests into focused files * Split Voices tab into focused modules * Split engine registry into smaller modules * Refactor engines into modular utility packages * Split speaker refinement tests into focused modules * Refactor large test suites into modular files * Fix manual memoization lint error in useVoicesData * Fix fixture shadowing lint errors and pre-existing react-hooks/refs errors * Fix lint issues in voices and XTTS utils * Fix pytest test package imports * Restore legacy state and xtts facade exports * Fix state file patching across compatibility modules * trial 1 * trial 2 * trial 3 * Update migration.py * Update test_storage_normalization.py * Update migration.py * Update migration.py * Harden project file paths for CodeQL * fixed for push * Update config.py * Update config.py * fix tests * Delete codeql-annotations.json * Update speakers.py * trial 4 * Harden project migration path containment * Update migration.py * Refactor speaker path containment with realpath checks * trial 5 * trial 6 * Update bundles.py * Harden chapter path validation and update security tests * trial 7 * Harden path containment and update chapter tests * Update chapters_cleanup.py * Harden voice storage version path checks * Harden voice bundle root resolution and update path lessons * trial 8 * trial 9 * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Information exposure through an exception' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Information exposure through an exception' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Potential fix for pull request finding 'CodeQL / Uncontrolled data used in path expression' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Delete codeql-annotations.json * Update speaker.py * Update voices_bundles.py * Create codeql.yml --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
* Add plugin security docs and guided install PoC * Add public Studio TTS API and gateway controls * Document Studio TTS gateway integration * Add plugin isolation tests and mark phase 9 complete * Promote TTS Server to default runtime * Promote Studio 2.0 defaults and runtime status * Harden Phase 10 retry fallback handling * Add engine test endpoint and tighten voice profile scanning * Handle engine API outages and voice migration * It’s fixed enough to proceed with the plan of attack. The last patch is, * Harden v2 voice profile discovery and startup readiness * Persist engine test audio and expose replay metadata * Clarify XTTS settings and make toast actions optional * Fix XTTS docs and orchestration imports * Update tts_api.py * Use a short delayed startup overlay reveal and keep threading startup it * Refine voice assignment dropdown rendering * Add character reassignment to script view * Relax startup gate and surface engine diagnostics * Improve Voxtral settings and diagnostics handling * Load Voxtral schema from plugin settings * Consolidate Voxtral enablement state and remove stale notes * Move autosave notice to General settings * Stop sentence dropdown clicks from triggering paint assignment * Document Phase 11 v2-only runtime cleanup * Bump Studio to 2.0.0 and expand phase cleanup plans * Mark phase 10 cleanup checklist complete * Refine runtime status text and privacy notice metadata * Update voice tests for motion and engine props * Update phase 10 cleanup tracking notes * Fix CI blockers in Phase 10 cleanup * Stabilize backend tests for default TTS runtime * Stabilize backend CI for v2-default runtime
* Document Phase 10 handoff and Phase 11 audit start * Remove silent TTS Server fallback * Trim wasteful legacy-path tests * Remove legacy bridge quarantine tests * Consolidate built-in engine metadata * Finish Phase 11 audit cleanup * Remove legacy voice routing flags * Remove v2 bridge feature flag * Remove unused legacy bridge shim * Route render handlers through voice bridge * Fix bridge handler error logging * Trim bridge helper whitespace * Add plugin behavior metadata foundation * Migrate voice action policy to plugin behavior metadata * Route orchestration through engine behavior * Fix behavior cutover lint issues * Move legacy engine implementations behind adapters * Generalize legacy dashboard defaults * Realign Phase 11 cleanup plan * Record Phase 11 hard cutover direction * Document declared plugin hook contract * Generalize plugin hook contract and cleanup docs * Genericize voice UI and job behavior metadata * Finish state and metrics hard cutover * Fix checkpoint blockers in metrics slice * Fix analysis chunk limit response * Remove legacy local bridge and cleanup tests * Record deferred tts namespace rename * Finalize frontend engine label cleanup * Purge final dead legacy dashboard tests * Record engine bundle test colocation * Keep plugin engines self-contained * Remove residual legacy config and dashboard * Fix worker bridge output tests * Fix worker bridge output tests * Record final engine bundle verification * Polish voice contracts and job engine typing * Generalize engine boundary comments and contracts * Move v1 engine cleanup into migration and generic defaults * Remove legacy runtime logic and finalize v2 cleanup * Enforce v2 speaker resolution and add profile tests * Route assembly and voice jobs through orchestrator * Remove legacy app.jobs worker runtime * Fix voice verification and queue visibility regressions * Add dynamic dev port selection for backend and frontend * Track render start separately from preparing * Add task cancellation and restore predictive progress * Skip ETA projection for post-synthesis progress updates * Harden chapter audio routing and legacy path compatibility * Update test_studio_task_progress.py * Remove remaining V1 runtime fallback paths * Update test_voice_security.py * fix tests * Remove V1 fallbacks and move migration to boot * phase 11 plan organize * Disambiguate audiobook assembly cache paths * Add manifest-driven engine test samples * Remove per-project migration from project listing * Fix blank engine enablement message in queue validation * Fix duplicate React keys in chapter and settings views * Honor plugin test_text for engine verification * Improve TTS startup readiness and runtime cleanup * Fix grouped render progress and chapter audio routing * Serialize bridge-backed synthesis jobs * Record render stats from orchestrator completions * Refresh queue and segment state on live updates * Restore pending render cues in ScriptView * Add dev load timing probes * Instrument project load timing * Remove project migration from reads * Optimize chapter segment loading * Add Playwright settings navigation coverage * Smooth chapter render text progress from predictive bar * Propagate render block progress to script text * Add model-aware render speed calibration * Display plugin computer speed as CPS * Add reset for computed engine speed * Add hard reset for engine speed baseline * Fix enqueue single API test * Exclude Playwright specs from Vitest * Update AboutSettingsPanel.tsx * Refactor TTS plugins to canonical mini-repo layout * refactor: migrate legacy frontend components to the new pages directory structure and implement comprehensive unit test coverage. * docs: add version 2.0.0 and 1.8.5 release highlights to changelog * refactor: reorganize project and settings components into dedicated feature directories for improved project structure * Remove obsolete cleanup code * Update bridge.py * Keep engine test artifacts inside plugin folders * Make Voices toolbar responsive * Enhance agent rules with review and verification guidance * Normalize engine test timestamps and Voxtral reference lookup * Codify project structure rules * Remove frontend XTTS fallback assumptions * Remove last jobs shim compatibility stub * Remove backend jobs queue shim cleanup * Remove obsolete enqueue-single API route * Harden backend behavior helpers with manifest progress parsing * Switch chunking to engine-specific text limits * plugin-declared test samples for voice assets * Harden public project and voice asset routes * Decommission stale V1 storage utility * Audit uploads legacy cover compatibility * Close behavior and text utility cleanup * Relocate baseline CPS fallback to behavior helpers * Move XTTS requirements into plugin * Move XTTS launcher conflict checks into plugin * Remove hardcoded XTTS-v2 copy from frontend * Close phase 11 and plan phase 12 polish * remove accidental files
* Expand Phase 12 backlog and verification gates * Remove app persistence imports from Voxtral plugin core * Localize plugin core dependencies for portability * Refine chapter VCR playback controls * Add press-and-hold skim controls to chapter playback * Add Project Library sorting, list view, and created dates * Retire legacy jobs API and complete master agnostic conversion * Refine memory and handoff workflow * Fix storage and registry lint regressions * Move jobs hydration to WebSockets * Document standalone CLI usage for XTTS and Voxtral * Add developer harness links to plugin READMEs * Add plugin developer mode and engine logos * Document multilingual voice languages and disable dev mode * Tighten plugin dev mode scenario safety * Validate TTS plugin manifest contracts * Surface developer plugin diagnostics safely * Improve engine dev panel diagnostics * Validate plugin dev scenarios * Harden plugin dependency install refresh * Finish plugin lifecycle and compatibility hardening * Support per-voice plugin settings save flow * Harden Chapter Editor playback controls * Remove obsolete Chapter Editor tabs * Remove remaining Chapter Editor production tab * Prune production block infrastructure * Fix API test syntax error * Fix ChapterEditor queue test drift * Refine Chapter Editor toolbar order * Update Phase 12 cleanup boards * Show queue output metadata * Sync Phase 12 audit boards * Mark TTS plugin zip flows complete * Retire legacy jobs API surface * Allow synthesis during pending verification * Fix chapter audio finalization fallback * Surface queue failure reasons * Sync Memory for queue failure reasons * attempted fix * Reduce websocket chatter for job status updates * Trace websocket sources and tighten queue scope * Add websocket classification for queue routing * Move websocket debug logging into listeners * Restore xtts studio handler logger * checkpoint before full "finalizing" removal * not perfect but better queue * startup working * checkpoint but display still not as good as previous commit * Trim redundant job_updated websocket broadcasts * Skip redundant job_updated broadcasts during synthesis * Trim redundant startup websocket broadcasts * just before creating a central consumer * Prevent inferred voice engines from persisting to disk * Enforce explicit voice engine resolution across backend * Remove last voice-engine fallback defaults * Document the live event stream contract * Add live event stream contract and schema module * Flatten job update payloads in live event normalization * Wire live output to the live event audit store * Wire TTS diagnostics to live audit logs * Route job events through live topic subscriptions * Refactor queue handling and update tests * Document live output subscriber ownership fix * Refactor live output filtering to use consumer registry * Document the studio event broadcaster contract * Implement phase 1 studio event broadcaster helpers * Add websocket facade for canonical studio events * Refine phase handoff and memory workflow * Route live events through new topic handlers * Emit canonical queue events for websocket broadcasts * Normalize voice test progress broadcast envelope * Emit canonical studio events for TTS logs * Migrate project updates to canonical lifecycle events * Migrate segment progress to canonical studio events * Remove frontend websocket compatibility shims * Audit and correct studio event progress and log contracts * Restrict queue sync to authoritative queue items * Fix completed group rendering in Chapter Editor * Fix live progress regression handling in queue and chapter jobs * Stabilize queue ETA and retention debug visibility * Refine queue retention and ETA diagnostics * Fix ChapterEditor segment progress source and reset state * Update test_websocket_broadcast.py * Stabilize segment progress handoff at segment boundaries * Refactor queue cleanup and update tests * Remove synthetic 99 percent segment completion event * Add confidence weighting to chapter editor progress bars * Add confidence to live output and stabilize chapter progress * Fix segment start seeding and preparing guard * Unify live queue merge logic and block stale progress * Promote segment start to processing for ChapterEditor bar * Standardize runtime on Python 3.11 * Add job lifecycle live events * feat: implement live event subscription routing for chapter and voice test states with improved queue snapshot persistence * Fix progress rollback and segment source selection * Disable startup verification synthesis and require verified synth * Checkpoint ETA calibration rebuild * Checkpoint ETA checklist corrections * Checkpoint synthesis metrics path audit * Fix ChapterEditor batch progress mapping * Fix XTTS render timing handoff * Fix render timing and ETA calibration * Add ETA timestamps and stabilize queue countdown source selection * Smooth predictive ETA during lane migrations * `confidence` is missing from that debug projection, not necessarily from * Normalize canonical event payloads and confidence fields * Restore computed CPS display in engine settings * Expose calibration summary on engine cards * Add structured timing contracts for synthesis results * Expose calibration confidence in engine registry * Drop legacy make_mp3 from render performance samples * Fix XTTS sample counts from render chunks * Refine phase handoff and memory tracking * Move voice-test queue schema into docs * Align voice sample start timing with real synthesis markers * Fix terminal parameter retention in done transition * Fix pre-push whitespace warnings * Update sample progress test contract
…t reserve (#203) reserve_task_resources and release_task_resources each read ENGINE_CLASS_ADMISSION independently. If the toggle moved while a task was in flight, release took the other branch and the engine-class, engine-id and global cap slots were never returned. release() is idempotent, so nothing errored and nothing logged; the slots just went missing until a restart. Reserve now records the mode it used in the claims dict and release branches on that, so the two paths cannot disagree. Where the claims dict never went through reserve (the recovery path rebuilds one), release covers both paths, which is safe because releasing a slot a task does not hold is a no-op. Closes #199
…A+B) (#206) * test(xtts): add strict app-import gate covering function bodies (#200 A1) The existing S4 cleanliness test walks only module-level statements, so all 31 app.* imports inside function bodies pass it today. Add a second test that walks the full AST across the five extraction-target modules and asserts zero. Marked xfail(strict=True): the gate lands before Stage B removes the imports, so the fix cannot silently regress and an unexpected pass is itself a failure. Refs #200 * feat(sdk): add get_lexicon context accessor and move apply_lexicon into the SDK (#200 A2) Plugins need a book's pronunciation overrides and the substitution pass, and today they reach into app.db.lexicon and app.utils.text.lexicon for both. ctx.get_lexicon(project_id) mirrors the shipped ctx.get_chapter_segments: a scoped read returning plain dicts, not a DB handle. apply_lexicon imports only re, so it moves into studio_plugin_sdk/text.py as a pure utility (the audio.py and proc.py shape) rather than becoming a wrapper that calls back into app.*. app/utils/text/lexicon.py is now a re-export shim and app/orchestration/tasks/synthesis.py imports from the SDK, so there is one implementation reached by two paths, not two implementations. Refs #200 * fix(sdk): ctx.split_long_sentences returns str, matching the function it wraps (#200 A3) safe_split_long_sentences returns a re-joined string. The SDK wrapper declared list[str] and wrapped the result as [result], so any plugin that swapped its local alias for the ctx method would have fed a list into a synthesis script. Owner decision: the SDK was the wrong side. The only caller was the SDK's own unit test, which asserted isinstance(result, list) and is updated. No production caller depended on the list shape. New tests run the real function and assert literal expected strings traced by hand, so the expectation does not follow the implementation. Refs #200 * feat(sdk): bump SDK to 1.1 and check manifest sdk_version against the package (#200 A4) The manifest's sdk_version was validated against a hardcoded {"1.0"} allow-list in the loader and nothing compared it to studio_plugin_sdk.SDK_VERSION. Both happened to read 1.0, so the check could not fail and the field was decorative: bump the package and every manifest still declaring the old value loads clean. sdk_version now comes out of _SUPPORTED_VERSION_FIELDS entirely and is checked against the installed package (same major, at or below its minor). One rule, derived from the package, so the two cannot drift. Mismatch raises PluginLoadError naming both versions. scripts/validate_plugin_manifests.py applies the same rule so CI and the runtime loader cannot disagree. app/studio_plugin_sdk re-exports SDK_VERSION rather than declaring a second literal. Shipped manifests stay at 1.0 and remain valid. plugin-contract.md goes to spec_version 1.9.0 with a changelog row covering the new capabilities, the corrected return type, the version check, and the reversal of v1.3.0's tolerance of function-body app.* imports. Refs #200 * refactor(xtts): route bake/segments/standard_handler host calls through the SDK (#200 B1) The eighteen app.* imports in these three files were function-body imports inside module-level alias functions. The alias names stay exactly as they were (tests patch them as module attributes), only what they resolve to changes: the SDK context instead of app.*. Two SDK gaps this exposed, both fixed rather than worked around at the call site: ctx.wav_to_mp3 dropped on_output and cancel_check and returned None, while the function it wraps takes both and returns the ffmpeg exit code. A caller swapping a direct app import for it lost cancellation in a render path and could no longer tell a failed transcode from a good one. It now forwards both and returns the code. ctx.get_chapter_dir took only chapter_id and re-derived project_id from the DB. It now accepts an optional project_id, so a caller that already holds one keeps its own authoritative and skips the read. Retargeting the tests that patched app.db.get_chapter_segments and friends turned up two mocks that were already inert before this change (they patched a re-export the code never read), so those tests were exercising the real DB without noticing. They now patch the module the function actually lives in. * refactor(xtts): route adapter and app_adapter host calls through the SDK (#200 B2) Removes the last twelve app.* imports Stage B is scoped for. adapter.py's TYPE_CHECKING import of the Job model goes with them: the parameter is now typed Any, matching how the rest of the plugin already treats it. Three shape mismatches between the context and these call sites, adapted here rather than pushed into the render path, since the import boundary and the data shapes are separate questions: ctx.get_speaker_wavs returns the split list, but the code downstream of the bridge still parses the legacy comma-joined string, so adapter._get_speaker_wavs and app_adapter.select_voice re-join before returning. select_voice's declared contract is str | None either way. ctx.get_chapter_dir, get_voices_dir and get_voice_profile_dir return str where these call sites do path arithmetic, so each wraps in Path. Two test fixtures needed correcting. app.db.state.update_job and app.db.state_jobs.update_job are the same object reached by two names, and the context reads the second, so the patches move. Separately, a fixture was returning a list from get_profile_wavs, whose contract is Optional[str]. That only went unnoticed while the adapter passed the value straight through without parsing it. * test(xtts): make the app-import gate live, and sync the plugin contract (#200 B3) The xfail(strict) marker comes off test_studio_zero_app_imports_at_any_scope. It went from 31 hits to zero and now fails on any new app.* import at any scope in the five extraction targets. One exemption remains: app_adapter.py's guarded import of the app-side BaseVoiceEngine and engine models. That is Stage C, and needs a plugin-facing registration contract in the SDK rather than a re-export. The exemption is keyed on (file, module) so it cannot quietly cover a different app.* import in the same file, and a companion test fails if it ever outlives the import it names. Spec goes to 1.10.0 with the migration itself and the two SDK signature changes, plus the three shape traps that cost real time here, written down for whoever migrates tts_voxtral the same way. Doc 02's signature block had three entries stale against the code, two of them since Stage A.
The workflow only triggered on main, so every PR into studio-2.0 has merged without CI running on it. studio-2.0 is the development line, so that is where the gate is needed. CodeQL already got this treatment in 8ad04b5, before it moved to GitHub's default setup. ci.yml was never given the same fix. This will surface one known-red job immediately: the hardcoded styles check fails on a pre-existing violation at ArchetypeQuickPick.tsx:242, tracked as item A10 in #192.
… C) (#208) (#211) * feat(sdk): move the engine bridge error tree into the SDK (#200 C1) The six engine bridge exceptions were stdlib-only with no imports at all, so they move verbatim into studio_plugin_sdk/engine_errors.py and app/engines/errors.py becomes a re-export shim. Same class objects, so every existing importer and every `except EngineBridgeError` clause keeps working. Re-parenting any of these would fail silently: an `except` clause that stops matching raises nothing of its own. Verified by mutation: with EngineExecutionError re-parented under StudioException, the whole engine and plugin suite (768 tests) still passed. The new tests/engines/test_engine_error_hierarchy.py pins every direct parent by name against hand-written literals and asserts every class is caught by both `except EngineBridgeError` and `except RuntimeError`; it goes red on that mutation in 3 places. * feat(sdk): move the engine discovery and health models into the SDK (#200 C2) ResourceProfile, EngineManifestModel and EngineHealthModel are frozen dataclasses over dataclasses, datetime and typing, with no app reference at runtime, so they move verbatim into studio_plugin_sdk/engine_models.py. app/engines/models.py re-exports them (identical object identity) and keeps EngineRegistrationModel, which pairs a manifest with a resolved engine object and flattens both for /api/engines: host orchestration glue, not plugin contract. test_sdk_module_identity.py gains TestEngineModelIdentity, asserting the three moved types are one object across app.engines.models, studio_plugin_sdk.engine_models and the package namespace, and that EngineRegistrationModel is not exported by the SDK. * feat(sdk): publish the app-adapter contract as a Protocol (#200 C3) VoiceEngineAdapter is a runtime_checkable Protocol over the nine methods BaseVoiceEngine declares, so a plugin's app adapter satisfies the contract structurally and inherits nothing host-side. That matches what the host already does at the one place it consumes an engine object: registry.py wraps every engine in _TtsServerEngineProxy, which has never subclassed anything. BaseVoiceEngine is not moved into the SDK and stays app-side. Publishing it would ship nine NotImplementedError stubs and four concrete method bodies into the contract, and put a second engine base class next to StudioTTSEngine for plugin authors to confuse it with. A Protocol says the same thing without shipping behavior. The four PL-3 request helpers are now module-level SDK functions. They already took everything they needed as arguments and never used cls, so BaseVoiceEngine binds these exact function objects with staticmethod rather than keeping a second copy: the `self.normalize_output_format(...)` call shape in both plugin adapters is unchanged, and the two names cannot drift apart. * refactor(xtts): drop the last app import from the plugin adapter (#200 C4) XttsVoiceEngine now satisfies studio_plugin_sdk.VoiceEngineAdapter structurally and subclasses nothing, so app_adapter.py's guarded `from app.studio_plugin_sdk import BaseVoiceEngine, ...` is gone. The four data and error types come from the SDK, and the four request helpers are called as SDK functions rather than inherited methods. Three methods the class used to inherit (validate_environment, current_settings, build_voice_asset) are now declared here, with the same behavior the base gave them: empty settings, and NotImplementedError on the two the app side has never driven. Nothing in the host or the suite called them on this class. The Stage A gate loses _STAGE_C_EXEMPT and its companion test, since the exemption's reason is gone. tts_xtts shipped code is now at zero app.* imports at any scope by AST walk. Four remain in the plugin's own tests/conftest.py, which the gate does not cover; flagged, not touched. * docs(spec): bump SDK to 1.2 and record Stage C in the plugin contract (#200 C5) plugin-contract.md 1.10.0 to 1.11.0. The app-adapter section is rewritten: it had been telling plugin authors to import BaseVoiceEngine from studio_plugin_sdk since 1.5.0, which was never true (the real package has never exported it, only app.studio_plugin_sdk did). It now documents the VoiceEngineAdapter Protocol, the nine required methods, the four helper functions, and why BaseVoiceEngine is deliberately not in the contract. engines-and-plugins.md 1.2.0 to 1.3.0: the SDK module list was missing three modules and still said SDK_VERSION = "1.0", and the import-boundary rule still described tts_xtts's plugin/studio as module-level-only. Both corrected, with the older rule kept for tts_voxtral, which has not been migrated. SDK 1.1 to 1.2. Minor, not patch: the surface grew by a Protocol, four functions, nine classes and three modules. Not major: nothing was removed or reshaped, every relocation sits behind a re-export with identical object identity, and the derived sdk_version rule from 1.9.0 means shipped manifests at "1.0" stay valid.
None of these are regressions. Enabling CI on studio-2.0 made long-standing failures visible for the first time. 1. Backend (Quick) listed ten test files by path, none of which have existed since the tests/ reorganisation into subdirectories. Replaced with directory targets, which cannot rot the same way. Verified: 1929 passed, 1 skipped. 2. TakeManager.test.tsx:119 asserted synchronously with getByText right after a fireEvent that triggers a React state update, so it raced the flush. It won locally and lost on the runner. Now findByText. 3. ArchetypeQuickPick used 'var(--danger, #d33)'. There is no --danger token, so the #d33 fallback was always what rendered and the error text has never used the design system's colour. Now var(--error-text), which fixes a real visual bug rather than only the style check.
…fix the Blob shim behind the Node divergence (#215) * test(xtts): skip the torch/numpy tests when the XTTS env is absent CI installs only root requirements.txt, which deliberately excludes torch and numpy (they live in the separate ~/xtts-env). pytest.ini collects tts_engines/, so five tests in these two files failed in CI while passing on any developer machine that has the XTTS env provisioned. Module-scope pytest.importorskip with an explicit reason, so CI skips them and a developer with the env still runs them. Verified by building a venv from root requirements.txt alone: before, 5 failed / 2770 passed; after, 2769 passed / 8 skipped. That same run also confirms no other file under tts_engines/ has the latent problem. * fix(tests): read jsdom Blob bytes without undici, fixing the Node 20/22 CI failures The `Blob.prototype.arrayBuffer` shim in the vitest setup routed through undici's `Response`, which does not recognise a jsdom Blob as a Blob. That behaves differently per Node version: Node 22: throws TypeError "object.stream is not a function" Node 24: silently coerces the blob to the string "[object Blob]" and returns those 13 bytes So the shim was broken everywhere. It just failed loudly on the Node 20 CI runner and quietly on the Node 24 dev machines. RecordControls' captured-state effect awaits `blob.arrayBuffer()`, caught the throw, and set the "Could not analyze this recording" verdict, which left Keep disabled and took out the 10 downstream assertions in RecordControls.test.tsx and TakeManager.test.tsx. Read jsdom's own byte storage instead. Not FileReader: it returns the right bytes but never settles under vitest fake timers, so a test reading a take under fake timers would hang rather than fail. FileReader stays as the fallback if a future jsdom stops exposing its storage, and the new test pins both the bytes and the fake-timer behaviour so losing the fast path fails loudly. R1: with the old shim restored, the new test is 5 failed on Node 22 (TypeError) and 5 failed on Node 24 ("expected [ Array(13) ] to deeply equal [ 97, 98, 99 ]"). Expected byte values are literals, never recomputed from the blob. Full suite: 285 files / 2527 tests green on both Node 22 and Node 24. * chore: declare the supported Node range instead of leaving it implied Nothing in the repo stated which Node versions are supported, so the gap between the Node 20 CI pin and the Node 24 dev machines was invisible until CI started running on studio-2.0 (#214). The range is the toolchain's own requirement, not a guess: vite 7.3.5 and @vitejs/plugin-react-swc 4.2.2 both declare "^20.19.0 || >=22.12.0", which is the tightest constraint in the tree (eslint, vitest and typescript are all looser). setup-node's "20" resolves to the latest 20.x, so the existing CI pin satisfies it and no workflow change is needed. npm only enforces this with engine-strict, so treat it as documentation. * test(frontend): serialise test files so local and CI agree This suite has order-dependent tests: they pass alone and fail when run alongside others, and the failing set MOVES between runs. Three runs of the same code gave three different answers: Node 22, one worker 285 files pass Node 22, parallel 1 fail (RecordControls announce) Node 20, parallel (CI) 3 fail (App, BookLayout, ChapterWorkspaceHeader) Chasing whichever test loses the race on a given day is endless, and a green local run means nothing while CI runs a different way. fileParallelism false makes both identical. Verified: three consecutive full runs, 285 files each, with no extra flags. This does not fix the isolation defects, it stops them producing noise. They are tracked separately. * test(frontend): make WaveformTape's rAF loop and peaks decode deterministic WaveformTape runs an unconditional 60Hz requestAnimationFrame polling loop for as long as it's mounted. jsdom's real rAF is backed by a real ~16ms setTimeout, so that loop kept a real timer armed through every `await waitFor(...)` in WaveformTape.test.tsx — under CI/Docker load, stray extra frames fired mid-test and raced assertions (R4: no real-timer-based test timing). Confirmed via Docker (node:20-bookworm, matching CI's pinned Node/OS): the pre-fix suite failed at the sub-grid rAF test. installControlledRaf() (new tests/helpers/rafControl.ts) replaces rAF/cAF with a manual, non-self-firing queue; position now only advances when a test explicitly calls raf.flush(). The two tests that previously awaited real animation frames now flush deterministically instead. While chasing this down, the same fixed-code Docker run surfaced a second, narrower race: usePeaks' fetch+decode is a real async Promise chain, and if it settles between two snapshot captures in the fixed-grid shape-stability tests (rather than before both), peakArray/barCount changes out from under the comparison. Both tests now wait for the decode mock to have been called before taking their first snapshot. Verified clean: 10/10 isolated Docker runs of WaveformTape.test.tsx, plus zero WaveformTape failures across 8 full-suite Docker runs (2 of them --cpus=1 constrained). A separate, pre-existing flake in the ChapterWorkspace/Book test family (confirmed present before this change, confirmed unrelated — no WaveformTape/PlayerBar render, no timers) remains and is called out separately rather than folded into this fix. * test(frontend): stop racing ChapterWorkspace's async chapter load Next/Previous chapter, Jump-to-next-unrendered, and Bookmark-this-chapter all render before useBookData's fetchChapters resolves (chapters starts as []). The affected tests only waited for the control to appear, not for it to reflect settled data, so a click landed in the pre-load window: the nav/jump buttons are silently disabled then (no-op click, stuck on the same route), and the bookmark button has no disabled state to gate on so it silently records the raw chapterId as the label instead of the title. Reproduced both failures deterministically by forcing an artificial delay into the fetchChapters mock, confirmed the fix holds under that same forced delay, then removed the delay. Fixes wait on the real settled condition (button enabled, or the resolved chapter title) per R4 rather than a longer timeout. Fixes #217.
…ath leaks (#220) * test(xtts): skip the torch/numpy tests when the XTTS env is absent CI installs only root requirements.txt, which deliberately excludes torch and numpy (they live in the separate ~/xtts-env). pytest.ini collects tts_engines/, so five tests in these two files failed in CI while passing on any developer machine that has the XTTS env provisioned. Module-scope pytest.importorskip with an explicit reason, so CI skips them and a developer with the env still runs them. Verified by building a venv from root requirements.txt alone: before, 5 failed / 2770 passed; after, 2769 passed / 8 skipped. That same run also confirms no other file under tts_engines/ has the latent problem. * fix(tests): read jsdom Blob bytes without undici, fixing the Node 20/22 CI failures The `Blob.prototype.arrayBuffer` shim in the vitest setup routed through undici's `Response`, which does not recognise a jsdom Blob as a Blob. That behaves differently per Node version: Node 22: throws TypeError "object.stream is not a function" Node 24: silently coerces the blob to the string "[object Blob]" and returns those 13 bytes So the shim was broken everywhere. It just failed loudly on the Node 20 CI runner and quietly on the Node 24 dev machines. RecordControls' captured-state effect awaits `blob.arrayBuffer()`, caught the throw, and set the "Could not analyze this recording" verdict, which left Keep disabled and took out the 10 downstream assertions in RecordControls.test.tsx and TakeManager.test.tsx. Read jsdom's own byte storage instead. Not FileReader: it returns the right bytes but never settles under vitest fake timers, so a test reading a take under fake timers would hang rather than fail. FileReader stays as the fallback if a future jsdom stops exposing its storage, and the new test pins both the bytes and the fake-timer behaviour so losing the fast path fails loudly. R1: with the old shim restored, the new test is 5 failed on Node 22 (TypeError) and 5 failed on Node 24 ("expected [ Array(13) ] to deeply equal [ 97, 98, 99 ]"). Expected byte values are literals, never recomputed from the blob. Full suite: 285 files / 2527 tests green on both Node 22 and Node 24. * chore: declare the supported Node range instead of leaving it implied Nothing in the repo stated which Node versions are supported, so the gap between the Node 20 CI pin and the Node 24 dev machines was invisible until CI started running on studio-2.0 (#214). The range is the toolchain's own requirement, not a guess: vite 7.3.5 and @vitejs/plugin-react-swc 4.2.2 both declare "^20.19.0 || >=22.12.0", which is the tightest constraint in the tree (eslint, vitest and typescript are all looser). setup-node's "20" resolves to the latest 20.x, so the existing CI pin satisfies it and no workflow change is needed. npm only enforces this with engine-strict, so treat it as documentation. * test(frontend): serialise test files so local and CI agree This suite has order-dependent tests: they pass alone and fail when run alongside others, and the failing set MOVES between runs. Three runs of the same code gave three different answers: Node 22, one worker 285 files pass Node 22, parallel 1 fail (RecordControls announce) Node 20, parallel (CI) 3 fail (App, BookLayout, ChapterWorkspaceHeader) Chasing whichever test loses the race on a given day is endless, and a green local run means nothing while CI runs a different way. fileParallelism false makes both identical. Verified: three consecutive full runs, 285 files each, with no extra flags. This does not fix the isolation defects, it stops them producing noise. They are tracked separately. * test(frontend): make WaveformTape's rAF loop and peaks decode deterministic WaveformTape runs an unconditional 60Hz requestAnimationFrame polling loop for as long as it's mounted. jsdom's real rAF is backed by a real ~16ms setTimeout, so that loop kept a real timer armed through every `await waitFor(...)` in WaveformTape.test.tsx — under CI/Docker load, stray extra frames fired mid-test and raced assertions (R4: no real-timer-based test timing). Confirmed via Docker (node:20-bookworm, matching CI's pinned Node/OS): the pre-fix suite failed at the sub-grid rAF test. installControlledRaf() (new tests/helpers/rafControl.ts) replaces rAF/cAF with a manual, non-self-firing queue; position now only advances when a test explicitly calls raf.flush(). The two tests that previously awaited real animation frames now flush deterministically instead. While chasing this down, the same fixed-code Docker run surfaced a second, narrower race: usePeaks' fetch+decode is a real async Promise chain, and if it settles between two snapshot captures in the fixed-grid shape-stability tests (rather than before both), peakArray/barCount changes out from under the comparison. Both tests now wait for the decode mock to have been called before taking their first snapshot. Verified clean: 10/10 isolated Docker runs of WaveformTape.test.tsx, plus zero WaveformTape failures across 8 full-suite Docker runs (2 of them --cpus=1 constrained). A separate, pre-existing flake in the ChapterWorkspace/Book test family (confirmed present before this change, confirmed unrelated — no WaveformTape/PlayerBar render, no timers) remains and is called out separately rather than folded into this fix. * test(frontend): stop racing ChapterWorkspace's async chapter load Next/Previous chapter, Jump-to-next-unrendered, and Bookmark-this-chapter all render before useBookData's fetchChapters resolves (chapters starts as []). The affected tests only waited for the control to appear, not for it to reflect settled data, so a click landed in the pre-load window: the nav/jump buttons are silently disabled then (no-op click, stuck on the same route), and the bookmark button has no disabled state to gate on so it silently records the raw chapterId as the label instead of the title. Reproduced both failures deterministically by forcing an artificial delay into the fetchChapters mock, confirmed the fix holds under that same forced delay, then removed the delay. Fixes wait on the real settled condition (button enabled, or the resolved chapter title) per R4 rather than a longer timeout. Fixes #217. * fix(security): close assembly path escape, plugin zip-bomb ceilings, and error-body path leaks Three fixes, all TDD'd with red-then-green: Issue #218: a project's display name flowed unsanitized into the assembly output filename and from there into ffmpeg's read directory (input_folder = output_path.parent), so a traversal-shaped or absolute project name could steer both outside the project's own m4b_dir. Fixed with a new safe_path_stem helper feeding secure_join_flat for the on-disk filename only (the display title still flows verbatim into job/queue metadata), plus a containment check in AssemblyTask.run() before ffmpeg runs, regardless of who built the path. Issue #219a: /plugins/import and /plugins/preview read an upload fully into memory and extracted it with no size ceiling anywhere. Added three named ceilings (200 MB upload, 2 GB total declared uncompressed size, 10,000 members), enforced before extraction, rejecting with 413. Issue #219b: an OSError's str() embeds the full filesystem path it failed on; the icon-save handler forwarded that straight into the HTTP response. Now logs the real detail server-side and returns a fixed generic message. Same shape fixed in two related sites that are currently latent (no path in today's message, but one change away from leaking one). design-docs/specs/security.md bumped 1.3.0 -> 1.4.0 (S16/S17/S18) with the new invariants this establishes. * fix(security): cap the plugin upload the LAN actually reaches, and make the ceiling tests real reds Review follow-up to ffffe4d. The 200 MB upload ceiling landed on the TTS server's own /plugins/import and /plugins/preview, which only ever see traffic the Studio process has already forwarded. The routes a browser or a LAN client actually posts to are /api/engines/import and /api/engines/preview, and those still did an unbounded await file.read() into the Studio process before handing the bytes to the bridge. So the ceiling existed everywhere except the surface an attacker reaches. Capped there too, answering 413 without touching the bridge. The constant is duplicated rather than imported, because app/api must not import the TTS server's in-process runtime. A new test pins the two values together so raising one and forgetting the other fails rather than half-applying the ceiling. The five existing ceiling tests were not valid reds. They monkeypatched the new constants down to small values, so against pre-fix source they died with AttributeError before the request was ever made, which means they never demonstrated the defect they were written for. Adding raising=False makes the patch a no-op on pre-fix code instead: the request goes through, the oversized upload is accepted and extracted, and the test fails with 200 != 413. Verified in a throwaway worktree at ffffe4d^. security.md 1.4.0 to 1.4.1. Phase 0 also now records the two limits it does not provide, since the previous wording claimed more than the code does: Starlette's multipart parser spools the whole request body to a temp file before the handler runs, so the ceiling bounds handler memory rather than bytes on disk, and the 2 GB uncompressed ceiling is a sum, so one member declaring just under it is still read whole into memory. * fix(security): close the one genuinely unbounded zip read, and correct security.md's overstated claim about the others The prior fix's own security.md claimed Python's zipfile "enforces file_size as the stop condition when reading/extracting a member" as a blanket property of the module. Measured directly rather than trusted: it isn't. That truncation-at-declared-size behavior belongs to the streaming reader (zf.open(member), which extractall() also uses internally) -- not to ZipFile.read(name), the single-shot convenience call this module used for manifest.json/settings_schema.json/requirements.txt. read(name) decompresses a member's entire DEFLATE stream in one call regardless of declared size and only compares against it, via CRC, at the very end. Confirmed empirically: a member declaring 100 bytes while its real content was 800 MB cost +839 MB RSS through zf.read() before raising; the identical forgery read through zf.open() in a bounded loop cost none. So extractall() was never actually exploitable this way -- only the three pre-extraction convenience reads were, and manifest.json is read before any size check has a chance to matter. Fixed by routing all four reads through _safe_read_member/_safe_extractall, which stream via zf.open() and count real bytes rather than relying on zipfile's internal bookkeeping (extraction gets this too, for defense in depth and consistent error handling, not because it was proven exploitable). Because a forged declared size and an honest CRC can never both hold -- truncating an honest CRC's real content always mismatches -- the actual gap this closes is not a size-ceiling bypass. It's that a corrupted or tampered member propagated as an unhandled zipfile.BadZipFile, landing as a bare 500 (extraction) or a misleading 400 "Invalid manifest.json" (mis-caught by the JSON-parse handler, which hid that the archive was tampered with rather than badly formed). Now a clean, correctly-labeled 400. security.md 1.4.1 to 1.4.2, with the correction recorded rather than the overstated claim quietly edited away.
Addresses #222: 14 rows in design-docs/specs/README.md had gone stale — each spec file's own spec_version had bumped (with a matching changelog entry) but the central index table was never refreshed alongside it. Also refreshed install-distribution.md's description, the one row where the version bump (GitHub install/trust flow: URL validation, preview/ confirm staging, registry-membership trust model) added scope the one-line summary didn't mention at all. Issue needs manual closing after merge — GitHub's closing keywords don't fire on a merge into a non-default branch.
The spec's own front-matter had already bumped to spec_version 1.3.0 (commit 1709b44, #200 C5) but the changelog table's last row still stopped at 1.2.0, so the spec was drifting from its own version header. Backfilled from that commit's diff and message: SDK module list correction, SDK_VERSION 1.0 -> 1.2, and the tightened zero-app-imports rule for tts_xtts (tts_voxtral kept the older rule). Addresses #225. This PR targets studio-2.0 (non-default), so GitHub's closing keywords won't auto-fire on merge -- close #225 by hand after.
ChapterSynthesisTask never carried a resource_claim, so the orchestrator admitted every queued chapter unconditionally (an empty engine_class skips every gate, including the global backstop) regardless of tts_parallel_cap. Chapters showed status="running" with a live ETA the instant they were submitted, while their segments quietly queued behind the real per-segment cap underneath - producing wildly wrong ETAs (a reported 4-hour prediction that finished in 45 minutes). Gives chapters a dedicated admission pool (ResourceClaim.chapter_admission) so tts_parallel_cap now genuinely bounds concurrent chapters, exempted from the shared global-concurrency backstop since a chapter task consumes no resources itself - only its children do, and they're gated normally. The first version of this fix let a chapter's claim and its own children's claims compete for the same global pool, which deadlocked whenever the cap was raised to meet the global default; caught by adversarial review with an actual reproduction, not a read-through, and fixed with a narrow exemption. A parked chapter now stays "queued" instead of flashing "preparing" before admission is even attempted.
#230) The "Concurrent Renders" override on each engine card was hidden whenever a manifest declared segment_orchestration, meant to exclude delegation-only orchestrators (Mixed, which fans work to sub-engines with no real concurrency of its own). XTTS also declares that flag, for an unrelated reason (Studio's chunk-group chapter fan-out with resumable recovery), so the control was invisible for the one engine an operator would most want to tune. Only Voxtral showed it, and its manifest caps it at 1 anyway. Added a correctly-scoped delegation_only flag to tts_mixed's manifest and gated the UI on that instead. Also fixed the control's own description, which claimed a restart was needed to take effect; that stopped being true back in July when live cap resolution shipped. The existing test's mock XTTS object never carried a features array at all, so it never exercised the real bug. Updated it to match the real manifest and added cases proving XTTS still shows the control and Mixed still hides it.
SegmentRenderMonitor's "N of M segments" counter used the raw count of chapter_segments database rows (one per sentence) as both the label and the implied unit of rendering work. The real unit is a render batch: several consecutive same-speaker sentences merged into one synthesis call. For a real chapter this session verified, 402 sentence rows collapsed into 58 actual batches - the display was overstating the work by 7x, and produced exactly the "50 of 400 segments, but the bar says 60%" mismatch reported. The backend already computes and broadcasts the real batch numbers (render_group_count, completed_render_groups) on every progress update. This was a pure frontend wiring gap: the component just never used data that already existed and already reached the browser. This is also a regression against an existing spec rule (text-processing.md 1.1.0, June): UI segment counts must derive from the real batch computation, not row counts. The percentage bar itself stays character-weighted, deliberately - that math is a legitimate estimate from real character counts and a calibrated per-character rate, not fabrication. Only the discrete count was wrong. Also fixed: a failed segment was getting partial progress credit in that same calculation; it now contributes zero. Two issues an adversarial review caught in the first pass of this fix, both corrected here: the batch-derived label still said "segments" for a number that actually meant something else, reproducing the exact naming collision a new glossary (added in this same change) was written to name; and a missing completed_render_groups value could have silently shown "0 done" over real progress, closed by requiring both real values before the batch display activates at all. New: design-docs/specs/glossary.md, canonical term definitions. Records the owner's ruling: "segment" means the render batch; the per-sentence row is a "span" (sentence-granularity today, word-level by design). Existing code still uses the old names - that rename is real, cross-cutting work, tracked but not yet scoped.
Consolidated write-up of the segment/span terminology ruling and the adversarial review that found it isn't safe to build as scoped. Kept in sync with issue #232's comment thread per OD-0033, this file is the durable record.
…weep) ad2_cap_resolution_hardening touches reserve/release_task_resources, which #228/#229 modified today (new chapter_admission exemption). parallel-segment-rendering's render-monitor doc predates the segment/span terminology ruling. Neither doc is false, both get a dated note so a future executor isn't surprised.
…renders ChapterSynthesisTask._publish_progress (the W-PAR parallel chapter-fanout path) already computed the real render-batch total/completed count but never passed them to ProgressService.publish, so the frontend's real-batch display (#231) silently fell back to the raw per-sentence segment count for every chapter dispatched through this path — reproducing the exact "39 of 306 segments" bug the owner reported live. Regression test added and R1 revert-checked (red pre-fix, green after).
EngineClassSemaphore.try_acquire had no concept of arrival order: every
waiting task polled it independently about once a second, and whichever
poll landed right after a slot freed won admission — regardless of which
task had been queued (and polling) longest. With several chapters sharing
one CHAPTER_ADMISSION_ENGINE_CLASS pool, this let a later-queued chapter
("Part 19") jump ahead of earlier-queued ones ("Part 9") still waiting,
matching the owner's live report exactly.
Fixed with FIFO waiter fairness: a free slot now goes only to the
earliest still-waiting task_id (self-pruned after 10s of no polling, so a
crashed/cancelled waiter can't block admission forever).
Also discovered, recorded but not fixed here: choose_next_task/
_task_sort_key (queue-jobs.md §6) are dead code, never called from the
live admission path, so the documented priority-bucket ordering
(studio_first/api_first) is currently aspirational for real dispatch.
Spec updated (queue-jobs.md 1.13.1 -> 1.13.2) with both the fix and the
drift note; README's version pointer, already stale, corrected too.
Regression tests added and R1 revert-checked (red pre-fix, green after);
test_live_cap_admission.py's shrink test updated to release its abandoned
waiter, matching the new fairness contract.
No test exercised the actual click-to-expand interaction on the >60-segment summary bar's "Segment detail (N)" disclosure, or whether it survives a live-progress re-render (new segments prop) without losing its open state. Both new tests pass against the current code — the click toggle and its persistence across prop updates work correctly at the component level.
…ibleTable Owner confirmed clicking the "Segment detail (N)" summary does nothing in the real running app, even after a full cache/reload. Live-browser testing of the isolated component shows the native <details> toggle working correctly (open attribute flips, table appears), so the failure is coming from something in the real page's context rather than the <summary> element itself — not reproducible in isolation, and not chased further without a live multi-tick render job. Either way, a bigger explicit button with a controlled open state is the more reliable target regardless of what's intercepting the native toggle.
…e bug) _publish_progress's live WS broadcast got render_group_count/completed_render_groups in 44649b6, but the durable update_job() refresh right below it never included them, so a restarted app's job row stayed null until the next WS tick landed on an already-open tab. REST-hydrated reads (page load, reconnect, recovery-after- restart) kept showing the raw per-sentence count. Confirmed live: state.json showed render_group_count/completed_render_groups both null on a job with real active_segments_map progress since restart.
… counts At 466 segments, SegmentBlockRow's 6px-per-block floor sums to ~2800px, and the peek strip's flex chain (root div, expand button, block-row span) left the default flex-item min-width:auto in place, so each ancestor inflated to that intrinsic width instead of letting the existing overflow:hidden clip it — the row bled into the neighboring panel. min-width: 0 on the three containers fixes it (live-repro-verified in a real browser: rowRight went from 2814px to 307.5px against a 348px card).
Owner ruling: render batch is the finest granularity this UI may ever
show. useSegmentInventory now aggregates the per-span merge into one
row per render batch, using the script-view API's existing
render_batches field (no backend change needed — same grouping as the
real synthesis-time build_chunk_groups). SegmentRenderMonitor,
SegmentBlockRow, and SegmentPeekStrip needed no logic changes, since
they were already generic over whatever segments array they're
handed — only their copy changed ("segment" -> "batch"). Retry now
resolves a clicked row back to every span in its batch and re-generates
them together through the existing multi-id generate endpoint, instead
of retrying only the batch's leader span.
Bumps progress-presentation.md to 1.12.0 (new invariant M6).
_dispatch_segment's stats recorder reads chars from payload["script_text"]; _SyntheticSegmentTask.describe() never included that key, so every W-PAR batch child returned early with chars <= 0, starving engine speed calibration on the whole parallel fan-out path.
…ds (#239) SynthesisTask.from_task_context() read payload["engine_id"], but a recovered payload is a raw processing_queue row whose column is "engine" (there's no "engine_id" key in a raw DB row) - every recovered plain synthesis job has always failed validate() with "engine_id is required" on restart, not just the reported one. Falls back to payload.get("engine") now. The DB row's cancelled status at crash time isn't a separate filtering bug: reconcile_queue_status() intentionally cancels snapshotted rows before recover() resubmits them (queue-jobs.md 5.3), and the crash just prevented that reactivation from ever happening. Also hardened the recovery thread itself: an uncaught exception inside a real thread's target never reaches the parent's try/except, so a resubmission failure silently dropped the job with nothing but a stderr traceback. It now logs and publishes a proper failed status. Regression tests in test_recovery_db_integration.py, R1 revert-checked. Spec bumped to 1.13.4 with a changelog row. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Boot silently swallowed migration failure and had no mechanism to run a destructive schema change safely -- no ordering, no record of what ran, no backup, no abort path. #232's chapter_segments redesign needs a real migration and can't be scoped without this existing first. app/db/migrations/ adds a runner: migrations apply in version order, each in its own transaction with explicit rollback on failure, backed up to a timestamped file before the first write, with a dry-run mode that validates without persisting. boot_studio() now runs it before the legacy one-shot data migrations and lets a failure abort boot instead of catching it -- refusing to serve beats booting on a half-migrated schema. registry.py is empty for now; #232 adds the first entry.
* Fix WAL-unsound migration backup (#246) backup_database() used shutil.copy2, which never touches the <db>-wal sidecar. Every connection runs PRAGMA journal_mode=WAL, so a recently committed write can live only in the WAL until a checkpoint -- the raw copy could silently produce a backup that opens fine but has quietly rewound past real work, with no visible sign anything was wrong. This backup is the entire rollback plan for #232's destructive migration, so it has to actually be correct. Switched to sqlite3.Connection.backup() (reads through the live WAL, writes one consolidated file) and to a write-.partial-then-rename pattern so a crash mid-backup can't leave a truncated file mistaken for a complete one. * Harden the migration backup's .partial scratch path Review follow-up on the #246 fix. The Connection.backup() switch itself holds up (the WAL-sidecar regression test reproduces the original data loss), but the .partial mechanism it introduced had two problems of its own. The scratch path was derived only from the backup's final name, which has one-second resolution, so two overlapping migration runs opened the same scratch file and all but one died with "attempt to write a readonly database". boot_studio()'s _booted guard is an unsynchronized flag, so overlapping runs are possible. The scratch path is now unique per call. It was also never cleaned up when a backup failed, which is more than untidiness: sqlite3 opens a junk file happily and only fails on the copy, so an orphaned .partial makes any later backup landing on that path fail with "file is not a database". It is now removed on failure. Both are covered by tests that were revert-checked red against the shared-scratch, no-cleanup version. Spec amends the in-flight 1.13.1 row rather than adding a version, since #256 hasn't merged.
* Fix migration/recovery startup race (#247) startup_event() ran run_startup_recovery() synchronously while boot_studio() (which runs the versioned schema-migration runner) was dispatched to a background daemon thread afterward, so recovery could resubmit interrupted jobs — and the web server could start serving chapter/segment routes — before a schema migration finished. That's unsafe for #232's upcoming segment-shape migration. Extracted run_schema_migrations() out of boot_studio() and call it synchronously from startup_event() right after init_db(), before the recovery-context snapshot and run_startup_recovery(). boot_studio() still calls it too (idempotent no-op once web.py already ran it) so direct callers like tests and CLI tools are unaffected. * Fix spec drift found in review of #247 Two documentation problems on this branch, both in the specs the change itself touched: The router index in specs/README.md still pointed at data-model.md 1.13.1 while the spec had been bumped to 1.13.2 during the rebase past #246. The index is where CLAUDE.md tells you to start reading, so a stale pointer there sends you to the wrong version of the contract. The Migration section's closing line still claimed boot_studio() triggers both migration mechanisms. That is no longer true (and was already only half true): startup_event() now runs the schema runner plus migrate_voice_profiles() and migrate_legacy_project_covers(), while boot_studio() runs the schema runner and migrate_state_json_to_db(). Spelled out the split per entry point, and kept the guarantee that actually matters (the schema runner precedes any legacy data migration on every path). No behavior change.
…rts (#242) react-router-dom ^7.17.0 -> ^7.18.2 resolves react-router to 7.18.3, closing all 12 react-router advisories (XSS, open redirect, CSRF, DoS). npm audit fix then closed the remaining transitive high-severity set: js-yaml (quadratic CPU via merge-key/omap), postcss (source-map path traversal), and brace-expansion (ReDoS/OOM), with no dependency-tree changes beyond patch bumps. vite and vitest were already on patched versions (7.3.5 / 3.2.6) per existing package.json ranges. The two high-severity transformers/RCE alerts (requirements-xtts.txt) are NOT closed here: both patched versions (5.3.0, 5.5.0) are a transformers major-version bump, and installing 5.16.1 into ~/xtts-env breaks coqui-tts's actual XTTS import path (ImportError: cannot import name 'isin_mps_friendly' from transformers.pytorch_utils, removed upstream in transformers 5.x) - confirmed by direct import test, not assumed. Left open pending either an upstream coqui-tts release compatible with transformers 5.x or a manual compatibility patch; documented back on #216. Verified: backend `pytest -q` 2809 passed/3 skipped; frontend `vitest --run` 285 files/2541 tests passed; `npm run build` and `npm run lint` clean; `npm audit` reports 0 vulnerabilities.
…240) * Add engine-id allowlist to tts_engine_caps settings endpoint (#235) POST /api/settings accepted any string key in tts_engine_caps with no check against real engines, so a client could set an override for synthetic internal keys (e.g. chapter_admission) or plain typos (an existing test used "tts_voxtral", the plugin dir name, instead of the real engine_id "voxtral" — silently accepted and silently inert). Validate against the live plugin registry and reject unknown keys with 400. * Fail closed when the engine registry is unreachable (#235 review) save_settings() swallowed registry errors from the new allowlist check inside its existing broad except-Exception handler, so an unavailable TTS server made the endpoint silently return 200 and drop every field in the request, including tts_api_key/huggingface_token. Also reject the empty-registry case explicitly instead of failing every engine_id.
* Carry project_id/chapter_id through crash recovery
load_recoverable_task_contexts() read chapter_id only for its per-chapter
dedup and never passed it (or project_id) to TaskContext, so every recovered
context arrived with both as None. The orchestrator gates on these as fields
rather than through payload, so a crashed chapter render could not resume: the
resume-progress lookup never fired, and reconstruction fell through to a bare
SynthesisTask whose validate() rejects the empty script_text, killing the job
instead of resuming it.
The existing recovery tests asserted only task_id, which is why this survived.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fix two recovery tests exposed by the chapter_id/project_id fix
test_startup_recovery_resubmits_interrupted_task asserted on a background
recovery-<task_id> daemon thread's side effect with no synchronization. It
only ever passed because a recovered context with chapter_id=None took a
fast/no-op progress-publish path; with real identifiers now threaded through,
the real (slower) path exposed the race. Fixed by joining the thread via the
existing join_recovery_threads() helper before asserting, inside the patch
window.
test_recovered_job_from_real_db_row_reconstructs_with_engine_id's own
docstring documented that chapter_id wasn't threaded through as a known,
separate defect and asserted the exact resulting error message
("script_text must not be empty") as expected behavior -- pinning the bug
it described. Now that chapter_id survives recovery, validate() correctly
skips that check for a chapter-driven job and reaches the next real gap
(a recovered row has no output_path either); updated the assertion and
comments accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.