Skip to content

Commit f4d2192

Browse files
committed
fix: v1.9.18 comprehensive 6-phase audit — 26 bugs fixed across 22 files
P0: loudness_match completely broken (run_ffmpeg return type mismatch), UXP CSRF header read wrong name, UXP chat/sequence-info hit 404 endpoints. P1: WorkerPool shutdown TypeError on poison pill comparison, styled captions progress crash (missing msg default), 4x bare "ffmpeg" in core modules breaking bundled installs, GPU memory leaks in MusicGen/SAM2/ Florence-2, UXP JobPoller.start() called with wrong signature (7 handlers). P2: path traversal in search/auto-index, sync route in queue allowlist, bare float() on user input, unbounded safe_float params, unhandled ValueError in validate_path, caption_style unsanitized, WorkerPool shutdown_pool race condition. 453 tests pass, ruff lint clean.
1 parent d57c314 commit f4d2192

36 files changed

Lines changed: 266 additions & 172 deletions

CLAUDE.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,44 @@ enhance = ["resemble-enhance>=0.0.1"]
922922
- **10 duplicate class attributes in HTML** — 10 elements had two `class=` attributes; HTML parser silently ignores the second, losing spacing utilities (mt-xs, mt-sm, mb-sm, mt-md). All merged into single attributes.
923923
- **pip install permission denied**`safe_pip_install()` failed on Windows when both normal and `--user` installs hit Errno 13 (Microsoft Store Python, OneDrive-synced user dirs, restrictive ACLs). Added `--target ~/.opencut/packages` as third fallback strategy. server.py adds `~/.opencut/packages` to `sys.path` at startup.
924924

925+
## v1.9.18 Comprehensive 6-Phase Audit (April 2026)
926+
927+
### P0 Fixes
928+
- **loudness_match COMPLETELY BROKEN**`_run_ffmpeg()` wrapper returned consolidated helper's `str` but callers accessed `.returncode`/`.stderr` (CompletedProcess attributes). `measure_loudness()`, `normalize_to_lufs()`, `batch_loudness_match()` all crashed with `AttributeError`. Renamed to `_run_ffmpeg_raw()`, made it always return `str` and raise `RuntimeError` on failure. Removed all `.returncode`/`.stderr` access from callers.
929+
- **UXP CSRF token refresh reads wrong header**`resp.headers.get("X-CSRF-Token")` instead of `"X-OpenCut-Token"`. After backend restarts, all UXP POST requests used stale token → 403 failures.
930+
- **UXP chat endpoint 404** — Posted to `/chat/message` but backend route is `/chat`. Chat completely non-functional in UXP.
931+
- **UXP sequence-info 404** — Fell back to non-existent `/sequence-info` endpoint. Removed dead backend fallback; PProBridge is the only source.
932+
933+
### P1 Fixes
934+
- **WorkerPool shutdown TypeError**`shutdown()` put `None` poison pills into `PriorityQueue`, but `None` is not comparable with `(int, int, tuple)` work items during heap insertion. Server shutdown crashed with `TypeError` if any jobs were queued. Fixed: poison pill is now `(-1, -1, None)`, worker loop checks `item[2] is None`.
935+
- **Styled captions progress crash**`_on_render_progress(pct, msg)` in captions.py missing `msg=""` default. `render_styled_caption_video()` calls `on_progress(int)` with 1 arg → `TypeError` every time.
936+
- **scene_detect bare "ffmpeg"**`detect_scenes_ml()` used bare `"ffmpeg"` in `extract_cmd` instead of `get_ffmpeg_path()`. TransNetV2 ML scene detection failed on bundled installs.
937+
- **shorts_pipeline 3x bare "ffmpeg"**`_trim_clip()` and fallback crop in `generate_shorts()` all used bare `"ffmpeg"`. Added `get_ffmpeg_path` import and replaced all 3.
938+
- **silence.py spurious FFmpeg check**`_extract_audio_wav()` checked `shutil.which("ffmpeg")` which returns `None` on bundled installs even though `get_ffmpeg_path()` resolves it. Changed to check `get_ffmpeg_path()` return value.
939+
- **audio_enhance bare "ffmpeg"**`_extract_audio()` assigned `get_ffmpeg_path()` to `ffmpeg_path` but then used bare `"ffmpeg"` in the command list. Fixed to use `ffmpeg_path`.
940+
- **MusicGen GPU memory leak**`generate_music()`, `generate_music_with_melody()`, and `continue_audio()` never freed 1.2-13GB MusicGen models after generation. Added `try/finally` with `del model` + `torch.cuda.empty_cache()` to all 3 functions.
941+
- **SAM2 GPU memory leak**`generate_masks_sam2()` loaded SAM2 predictor (1-4GB VRAM) but never cleaned up. Added `del predictor, state` + `torch.cuda.empty_cache()` to existing `finally` block.
942+
- **Florence-2 GPU memory leak**`detect_watermark_region()` loaded Florence-2 (~450MB) but never freed it. Added `try/finally` with cleanup around inference block.
943+
944+
### P2 Fixes
945+
- **timeline.py bare float()** — OTIO segment export used `float(s.get("start", 0))` on user input; 500 on non-numeric. Changed to `safe_float()`.
946+
- **search/auto-index path traversal** — File paths validated only with `os.path.isfile()`, no null byte/symlink/traversal check. Added `validate_filepath()` on each path.
947+
- **search/auto-index in queue allowlist** — Sync route was in `_ALLOWED_QUEUE_ENDPOINTS`, causing queue entries to silently fail (no job_id returned). Removed from allowlist.
948+
- **video_core.py unbounded safe_float**`threshold` and `min_scene_length` in scene detection had no `min_val`/`max_val` bounds. Added clamps.
949+
- **video_editing.py validate_path crash**`multicam_xml_export()` called `validate_path()` without catching `ValueError`. Added try/except returning 400.
950+
- **video_specialty.py caption_style** — Unsanitized user string passed into shorts pipeline config. Added allowlist validation.
951+
- **WorkerPool shutdown_pool race**`_pool = None` set without `_pool_lock`, allowing another thread to create a zombie pool. Now acquires lock before clearing.
952+
953+
### Frontend Fixes (Phase 4)
954+
- **UXP JobPoller.start() wrong signature (7 handlers)**`runDepthEffect`, `runEmotionHighlights`, `runBrollAnalysis`, `runUpscaleUxp`, `runSceneDetectUxp`, `runStyleTransferUxp`, `runShortsPipelineUxp` all passed `(jobId, callback)` to `JobPoller.start()` which expects `(endpoint, body, onProgress, onComplete, onError)`. All 7 handlers now use new `JobPoller.poll(jobId)` method (added to IIFE return).
955+
- **UXP .toFixed() crash** — Cut result display used raw `.toFixed()` on potentially string values. Added `Number()` coercion.
956+
- **CEP reframe "face" value** — HTML `<option value="face">` but backend expects `"auto"` since batch 22. Changed to `value="auto"`.
957+
958+
### Test Updates
959+
- **test_core_modules_batch2**`test_extract_audio_calls_ffmpeg` updated to accept resolved FFmpeg path (not bare `"ffmpeg"` string).
960+
- **test_new_features**`test_extract_audio_wav_checks_ffmpeg` updated to mock `opencut.core.silence.get_ffmpeg_path` instead of `shutil.which`.
961+
- **test_new_modules** — 4 loudness_match tests updated: mock target changed from `_run_ffmpeg` to `_run_ffmpeg_raw`, mock return values changed from `MagicMock(returncode=0, stderr=...)` to plain `str`.
962+
925963
## v1.9.17 Full Audit & Repair (April 2026)
926964
- **test_clip_notes_plugin.py rewrite** — Old test fixture accessed `mod._thread_local.conn` after flaky `importlib` module load, causing `AttributeError` on Windows. The `temp_db` autouse fixture was a dead no-op (created module without executing it). Rewrote entire test file to use JSON storage backend (matching `test_plugin_clip_notes.py` pattern), eliminating `_thread_local` dependency. All 10 legacy route tests pass cleanly.
927965
- **LUT_SIZE parsing crash**`_parse_cube()` in `lut_library.py` called `int(line.split()[-1])` without checking `split()` returned ≥2 parts. Malformed `.cube` files with bare `LUT_SIZE` keyword (no value) crashed with `ValueError`. Added `len(parts) >= 2` guard + `try/except ValueError`.

Install.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ Write-Host " \___/| .__/ \___|_| |_|\____\__,_|\__|" -ForegroundColor Cyan
155155
Write-Host " |_| " -ForegroundColor Cyan
156156
Write-Host ""
157157
Write-Host " Open Source Video Editing Automation" -ForegroundColor DarkGray
158-
Write-Host " Installer v1.9.16" -ForegroundColor DarkGray
158+
Write-Host " Installer v1.9.18" -ForegroundColor DarkGray
159159

160160
$isAdmin = Test-IsAdmin
161161
if ($isAdmin) {

OpenCut.iss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
; Fully self-contained installer — bundles server exe, ffmpeg, and CEP extension
33

44
#define MyAppName "OpenCut"
5-
#define MyAppVersion "1.9.16"
5+
#define MyAppVersion "1.9.18"
66
#define MyAppPublisher "SysAdminDoc"
77
#define MyAppURL "https://github.com/SysAdminDoc/OpenCut"
88

extension/com.opencut.panel/CSXS/manifest.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
<ExtensionManifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
33
Version="7.0"
44
ExtensionBundleId="com.opencut.panel"
5-
ExtensionBundleVersion="1.9.16"
5+
ExtensionBundleVersion="1.9.18"
66
ExtensionBundleName="OpenCut">
77

88
<ExtensionList>
9-
<Extension Id="com.opencut.panel.main" Version="1.9.16" />
9+
<Extension Id="com.opencut.panel.main" Version="1.9.18" />
1010
</ExtensionList>
1111

1212
<ExecutionEnvironment>

extension/com.opencut.panel/client/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2399,7 +2399,7 @@ <h3 class="media-sidecar-title">No media in the workspace yet.</h3>
23992399
<option value="bottom">Bottom</option>
24002400
<option value="left">Left</option>
24012401
<option value="right">Right</option>
2402-
<option value="face">Face Tracking (auto)</option>
2402+
<option value="auto">Face Tracking (auto)</option>
24032403
</select>
24042404
<span class="hint-inline">Where to anchor the crop</span>
24052405
</div>
@@ -3388,7 +3388,7 @@ <h3 class="media-sidecar-title">No media in the workspace yet.</h3>
33883388
<div class="card-header"><div class="card-title" data-i18n="settings.about">About OpenCut</div></div>
33893389
<div class="settings-row">
33903390
<span class="settings-label">Version</span>
3391-
<span class="settings-value">1.9.16</span>
3391+
<span class="settings-value">1.9.18</span>
33923392
</div>
33933393
<div class="about-links">
33943394
<a href="https://github.com/SysAdminDoc/opencut" class="about-link" target="_blank">GitHub</a>

extension/com.opencut.panel/client/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* ============================================================
2-
OpenCut CEP Panel - Main Controller v1.9.16
2+
OpenCut CEP Panel - Main Controller v1.9.18
33
6-Tab Professional Toolkit
44
============================================================ */
55
(function () {

extension/com.opencut.panel/client/style.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* ============================================================
2-
OpenCut CEP Panel v1.9.16 - ULTRA PREMIUM EDITION
2+
OpenCut CEP Panel v1.9.18 - ULTRA PREMIUM EDITION
33
Next-Generation AI Editing Suite for Adobe Premiere Pro
44
============================================================ */
55

extension/com.opencut.panel/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencut-panel",
3-
"version": "1.9.16",
3+
"version": "1.9.18",
44
"private": true,
55
"description": "OpenCut CEP Panel for Adobe Premiere Pro",
66
"scripts": {

extension/com.opencut.uxp/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<path d="M4 2.5a3 3 0 00-1.76 5.43L7.33 11l-5.09 3.07A3 3 0 104.8 19.5a3 3 0 001.76-5.43L8.93 12.6 16.5 17V5L8.93 9.4 6.56 7.93A3 3 0 004 2.5z" fill="var(--accent)"/>
1717
</svg>
1818
<span class="oc-logo">OpenCut</span>
19-
<span class="oc-version">v1.9.16</span>
19+
<span class="oc-version">v1.9.18</span>
2020
</div>
2121
<div class="oc-header-right">
2222
<div class="oc-connection" id="connectionStatus" title="Backend connection status">

extension/com.opencut.uxp/main.js

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const HEALTH_CHECK_MS = 8000;
2626
const HEALTH_MAX_MS = 60000;
2727
const MEDIA_SCAN_MS = 30000;
2828
const SSE_AVAILABLE = typeof EventSource !== "undefined";
29-
const VERSION = "1.9.16";
29+
const VERSION = "1.9.18";
3030

3131
async function detectBackend() {
3232
// Try ports 5679-5689 like CEP panel does
@@ -349,7 +349,7 @@ const BackendClient = (() => {
349349
const resp = await fetch(url, opts);
350350

351351
// Refresh CSRF token if provided in response headers
352-
const newToken = resp.headers.get("X-CSRF-Token");
352+
const newToken = resp.headers.get("X-OpenCut-Token");
353353
if (newToken) csrfToken = newToken;
354354

355355
let data;
@@ -553,7 +553,25 @@ const JobPoller = (() => {
553553
_fireCompletionHooks();
554554
}
555555

556-
return { start, cancel, onJobFinished };
556+
/**
557+
* Poll an already-started job by ID until completion.
558+
* Returns the final result object or throws.
559+
*/
560+
function poll(jobId) {
561+
return new Promise((resolve, reject) => {
562+
activeJobId = jobId;
563+
const onProgress = () => {};
564+
const onComplete = (result) => resolve(result);
565+
const onError = (msg) => reject(new Error(msg));
566+
if (SSE_AVAILABLE) {
567+
trackJobSSE(jobId, onProgress, onComplete, onError);
568+
} else {
569+
pollJob(jobId, onProgress, onComplete, onError);
570+
}
571+
});
572+
}
573+
574+
return { start, poll, cancel, onJobFinished };
557575
})();
558576

559577
// ─────────────────────────────────────────────────────────────
@@ -890,7 +908,7 @@ function showCutResult(result) {
890908
if (!area || !body) return;
891909
area.classList.remove("hidden");
892910
const lines = (result.cuts ?? []).map((c, i) =>
893-
`Cut ${i + 1}: ${c.start.toFixed(3)}s → ${c.end.toFixed(3)}s (${((c.end - c.start) * 1000).toFixed(0)} ms)`
911+
`Cut ${i + 1}: ${Number(c.start).toFixed(3)}s → ${Number(c.end).toFixed(3)}s (${((Number(c.end) - Number(c.start)) * 1000).toFixed(0)} ms)`
894912
);
895913
body.textContent = lines.join("\n") || "No cuts.";
896914
}
@@ -1569,10 +1587,9 @@ async function loadSequenceInfo() {
15691587
info = await PProBridge.getSequenceInfo();
15701588
}
15711589

1572-
// Fall back to backend
1590+
// No backend equivalent for sequence-info; PProBridge is the only source
15731591
if (!info) {
1574-
const r = await BackendClient.get("/sequence-info");
1575-
if (r.ok) info = r.data;
1592+
info = null;
15761593
}
15771594

15781595
UIController.setButtonLoading("loadSeqInfoBtn", false);
@@ -2162,17 +2179,15 @@ async function runDepthEffect() {
21622179
UIController.setButtonLoading("runDepthBtnUxp", true);
21632180
const r = await BackendClient.post(endpoint, payload);
21642181
if (r.ok && r.data?.job_id) {
2165-
activeJobId = r.data.job_id;
21662182
UIController.showProcessing("Running depth effect...");
2167-
JobPoller.start(r.data.job_id, (job) => {
2168-
UIController.hideProcessing();
2169-
UIController.setButtonLoading("runDepthBtnUxp", false);
2170-
if (job.status === "complete") {
2171-
UIController.showToast(`Depth effect complete: ${job.result?.output_path?.split(/[/\\]/).pop() ?? "done"}`, "success");
2172-
} else {
2173-
UIController.showToast(`Depth effect failed: ${job.error || "unknown"}`, "error");
2174-
}
2175-
});
2183+
try {
2184+
const result = await JobPoller.poll(r.data.job_id);
2185+
UIController.showToast(`Depth effect complete: ${result?.output_path?.split(/[/\\]/).pop() ?? "done"}`, "success");
2186+
} catch (e) {
2187+
UIController.showToast(`Depth effect failed: ${e.message || "unknown"}`, "error");
2188+
}
2189+
UIController.hideProcessing();
2190+
UIController.setButtonLoading("runDepthBtnUxp", false);
21762191
} else {
21772192
UIController.setButtonLoading("runDepthBtnUxp", false);
21782193
UIController.showToast(`Error: ${r.error || "Failed to start depth effect"}`, "error");
@@ -2189,18 +2204,16 @@ async function runEmotionHighlights() {
21892204
UIController.setButtonLoading("runEmotionBtnUxp", true);
21902205
const r = await BackendClient.post("/video/emotion-highlights", { filepath: clipPath });
21912206
if (r.ok && r.data?.job_id) {
2192-
activeJobId = r.data.job_id;
21932207
UIController.showProcessing("Analyzing emotions...");
2194-
JobPoller.start(r.data.job_id, (job) => {
2195-
UIController.hideProcessing();
2196-
UIController.setButtonLoading("runEmotionBtnUxp", false);
2197-
if (job.status === "complete") {
2198-
const peaks = job.result?.peaks?.length ?? 0;
2199-
UIController.showToast(`Emotion analysis complete: ${peaks} emotional peaks found.`, "success");
2200-
} else {
2201-
UIController.showToast(`Emotion analysis failed: ${job.error || "unknown"}`, "error");
2202-
}
2203-
});
2208+
try {
2209+
const result = await JobPoller.poll(r.data.job_id);
2210+
const peaks = result?.peaks?.length ?? 0;
2211+
UIController.showToast(`Emotion analysis complete: ${peaks} emotional peaks found.`, "success");
2212+
} catch (e) {
2213+
UIController.showToast(`Emotion analysis failed: ${e.message || "unknown"}`, "error");
2214+
}
2215+
UIController.hideProcessing();
2216+
UIController.setButtonLoading("runEmotionBtnUxp", false);
22042217
} else {
22052218
UIController.setButtonLoading("runEmotionBtnUxp", false);
22062219
UIController.showToast(`Error: ${r.error || "Failed to start emotion analysis"}`, "error");
@@ -2217,18 +2230,16 @@ async function runBrollAnalysis() {
22172230
UIController.setButtonLoading("runBrollPlanBtnUxp", true);
22182231
const r = await BackendClient.post("/video/broll-plan", { filepath: clipPath });
22192232
if (r.ok && r.data?.job_id) {
2220-
activeJobId = r.data.job_id;
22212233
UIController.showProcessing("Analyzing B-roll points...");
2222-
JobPoller.start(r.data.job_id, (job) => {
2223-
UIController.hideProcessing();
2224-
UIController.setButtonLoading("runBrollPlanBtnUxp", false);
2225-
if (job.status === "complete") {
2226-
const windows = job.result?.windows?.length ?? 0;
2227-
UIController.showToast(`B-roll analysis complete: ${windows} insertion points found.`, "success");
2228-
} else {
2229-
UIController.showToast(`B-roll analysis failed: ${job.error || "unknown"}`, "error");
2230-
}
2231-
});
2234+
try {
2235+
const result = await JobPoller.poll(r.data.job_id);
2236+
const windows = result?.windows?.length ?? 0;
2237+
UIController.showToast(`B-roll analysis complete: ${windows} insertion points found.`, "success");
2238+
} catch (e) {
2239+
UIController.showToast(`B-roll analysis failed: ${e.message || "unknown"}`, "error");
2240+
}
2241+
UIController.hideProcessing();
2242+
UIController.setButtonLoading("runBrollPlanBtnUxp", false);
22322243
} else {
22332244
UIController.setButtonLoading("runBrollPlanBtnUxp", false);
22342245
UIController.showToast(`Error: ${r.error || "Failed to start B-roll analysis"}`, "error");
@@ -2260,7 +2271,7 @@ async function sendChatMessage() {
22602271

22612272
if (!_chatSessionId) _chatSessionId = `uxp-${Date.now()}`;
22622273

2263-
const r = await BackendClient.post("/chat/message", {
2274+
const r = await BackendClient.post("/chat", {
22642275
session_id: _chatSessionId,
22652276
message: message,
22662277
filepath: clipPath,
@@ -2474,9 +2485,12 @@ async function runUpscaleUxp() {
24742485
UIController.setButtonLoading("runUpscaleBtnUxp", false);
24752486

24762487
if (r.ok && r.data?.job_id) {
2477-
JobPoller.start(r.data.job_id, (result) => {
2488+
try {
2489+
const result = await JobPoller.poll(r.data.job_id);
24782490
UIController.showToast(`Upscaled: ${result?.output_path || "done"}`, "success");
2479-
});
2491+
} catch (e) {
2492+
UIController.showToast(`Upscale failed: ${e.message}`, "error");
2493+
}
24802494
} else {
24812495
UIController.showToast(r.data?.error || "Upscale failed.", "error");
24822496
}
@@ -2496,10 +2510,13 @@ async function runSceneDetectUxp() {
24962510
UIController.setButtonLoading("runSceneDetectBtnUxp", false);
24972511

24982512
if (r.ok && r.data?.job_id) {
2499-
JobPoller.start(r.data.job_id, (result) => {
2513+
try {
2514+
const result = await JobPoller.poll(r.data.job_id);
25002515
const count = result?.scenes?.length || result?.total_scenes || 0;
25012516
UIController.showToast(`Found ${count} scene boundaries.`, "success");
2502-
});
2517+
} catch (e) {
2518+
UIController.showToast(`Scene detection failed: ${e.message}`, "error");
2519+
}
25032520
} else {
25042521
UIController.showToast(r.data?.error || "Scene detection failed.", "error");
25052522
}
@@ -2519,9 +2536,12 @@ async function runStyleTransferUxp() {
25192536
UIController.setButtonLoading("runStyleTransferBtnUxp", false);
25202537

25212538
if (r.ok && r.data?.job_id) {
2522-
JobPoller.start(r.data.job_id, (result) => {
2539+
try {
2540+
const result = await JobPoller.poll(r.data.job_id);
25232541
UIController.showToast(`Style applied: ${result?.output_path || "done"}`, "success");
2524-
});
2542+
} catch (e) {
2543+
UIController.showToast(`Style transfer failed: ${e.message}`, "error");
2544+
}
25252545
} else {
25262546
UIController.showToast(r.data?.error || "Style transfer failed.", "error");
25272547
}
@@ -2547,10 +2567,13 @@ async function runShortsPipelineUxp() {
25472567
UIController.setButtonLoading("runShortsPipelineBtnUxp", false);
25482568

25492569
if (r.ok && r.data?.job_id) {
2550-
JobPoller.start(r.data.job_id, (result) => {
2570+
try {
2571+
const result = await JobPoller.poll(r.data.job_id);
25512572
const count = result?.total_clips || result?.clips?.length || 0;
25522573
UIController.showToast(`Generated ${count} short-form clips.`, "success");
2553-
});
2574+
} catch (e) {
2575+
UIController.showToast(`Shorts pipeline failed: ${e.message}`, "error");
2576+
}
25542577
} else {
25552578
UIController.showToast(r.data?.error || "Shorts pipeline failed.", "error");
25562579
}

0 commit comments

Comments
 (0)