Skip to content

Commit 1aac64a

Browse files
committed
fix: batch 18 — 12 bugs across core, routes, and frontend
Core: - video_fx: apply_lut() uses -filter_complex for partial intensity (was -vf, crashed FFmpeg) - music_gen: escape single quotes in concat demuxer file paths Routes: - system: /whisper/reinstall backend allowlist (was arbitrary pip install — SECURITY) - video: add "auto" to reframe _VALID_POSITIONS (auto-crop was dead code) - video: title overlay text cap (500 chars, matches render route) - video: merge/join file count cap (MAX_BATCH_FILES) Frontend: - scanForServer: extract CSRF token + reset healthBackoff on port-scan reconnect - updateClipPreview: null guards on child elements - command palette: null guards on input/results across 4 functions - clearWhisperCache: guard data.cleared before .length - recent clips: document click handler to close dropdown - refreshOutputs: Array.isArray guard on API response
1 parent 30ea648 commit 1aac64a

6 files changed

Lines changed: 57 additions & 18 deletions

File tree

CLAUDE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,20 @@
439439
- **runBatch file selection** — uses `_batchFiles` array when populated instead of ignoring user's batch picker selection
440440
- **Dead code + play() promise** — removed unused `_translations` variable; `showAudioPreview()` catches play() rejection
441441

442+
## v1.3.1 Batch 18 Bug Fixes
443+
- **apply_lut() FFmpeg crash** — partial-intensity LUT uses filter_complex graph syntax (named pads/semicolons) but was passed via `-vf`; now correctly switches to `-filter_complex` when intensity < 1.0 (video_fx.py)
444+
- **concat path quoting**`concatenate_audio()` escapes single quotes in file paths for FFmpeg concat demuxer list file (music_gen.py)
445+
- **whisper/reinstall SECURITY**`/whisper/reinstall` backend param was unvalidated, allowing arbitrary pip install; added allowlist matching `/install-whisper` route (system.py)
446+
- **reframe "auto" dead code**`"auto"` position was missing from `_VALID_POSITIONS` set, making auto-crop detection unreachable; added to allowlist (video.py)
447+
- **title overlay text cap**`/video/title/overlay` now caps text at 500 chars, matching `/video/title/render` (video.py)
448+
- **merge/join file cap**`/video/merge` and `/video/transitions/join` now enforce `MAX_BATCH_FILES` limit (video.py)
449+
- **scanForServer CSRF** — port-scan reconnection now extracts `csrf_token` from health response and resets `healthBackoff` + restarts health interval; prevents stale CSRF and 60s backoff after backend restart (main.js)
450+
- **updateClipPreview null guards** — null-safe access on `clipThumb`, `clipMetaRes`, `clipMetaDur`, `clipMetaSize` elements (main.js)
451+
- **command palette null guards**`openCommandPalette`, `renderPaletteResults`, `initCommandPalette`, `paletteNavigate` all guard `commandPaletteInput` and `commandPaletteResults` (main.js)
452+
- **clearWhisperCache crash**`data.cleared.length` guarded with `(data.cleared ? data.cleared.length : 0)` (main.js)
453+
- **recent clips click-outside** — document click handler closes recent clips dropdown when clicking outside (main.js)
454+
- **refreshOutputs array check**`Array.isArray(data)` guard prevents "undefined" display when API returns non-array (main.js)
455+
442456
## v1.3.0 New Optional Dependencies
443457
```toml
444458
auto-edit = ["auto-editor>=24.0"]

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,10 +1112,14 @@
11121112
found = true;
11131113
BACKEND = testUrl;
11141114
connected = true;
1115+
if (data.csrf_token) csrfToken = data.csrf_token;
11151116
el.connDot.className = "conn-dot on";
11161117
el.connLabel.textContent = "Connected" + (port !== BACKEND_BASE_PORT ? " (:" + port + ")" : "");
11171118
el.backendPort.textContent = "Port " + port;
11181119
if (data.capabilities) capabilities = data.capabilities;
1120+
healthBackoff = HEALTH_MS;
1121+
clearInterval(healthTimer);
1122+
healthTimer = setInterval(checkHealth, HEALTH_MS);
11191123
updateButtons();
11201124
loadCapabilities();
11211125
portScanPending = false;
@@ -3332,7 +3336,7 @@
33323336
api("POST", "/whisper/clear-cache", {}, function (err, data) {
33333337
if (!err && data) {
33343338
if (data.success) {
3335-
showAlert("Cache cleared! Cleared " + data.cleared.length + " location(s). Models will re-download on next use.");
3339+
showAlert("Cache cleared! Cleared " + (data.cleared ? data.cleared.length : 0) + " location(s). Models will re-download on next use.");
33363340
} else {
33373341
showAlert("Cache clear had errors: " + (data.errors ? data.errors.join(", ") : "unknown"));
33383342
}
@@ -4608,7 +4612,7 @@
46084612

46094613
function refreshOutputs() {
46104614
api("GET", "/outputs/recent", null, function (err, data) {
4611-
if (err || !data) return;
4615+
if (err || !data || !Array.isArray(data)) return;
46124616
if (el.outputBrowserToggle) {
46134617
el.outputBrowserToggle.textContent = "Outputs (" + data.length + ")";
46144618
}
@@ -5004,25 +5008,25 @@
50045008
return;
50055009
}
50065010
el.clipPreviewRow.classList.remove("hidden");
5007-
el.clipThumb.innerHTML = '<div class="clip-thumb-loading"></div>';
5008-
el.clipMetaRes.textContent = "";
5009-
el.clipMetaDur.textContent = "";
5010-
el.clipMetaSize.textContent = "";
5011+
if (el.clipThumb) el.clipThumb.innerHTML = '<div class="clip-thumb-loading"></div>';
5012+
if (el.clipMetaRes) el.clipMetaRes.textContent = "";
5013+
if (el.clipMetaDur) el.clipMetaDur.textContent = "";
5014+
if (el.clipMetaSize) el.clipMetaSize.textContent = "";
50115015
// Fetch thumbnail
50125016
api("POST", "/video/preview-frame", { file: selectedPath, timestamp: "00:00:01", width: 160 }, function(err, data) {
50135017
if (err || !data || !data.image) {
5014-
el.clipThumb.innerHTML = '<div class="clip-thumb-none">No Preview</div>';
5018+
if (el.clipThumb) el.clipThumb.innerHTML = '<div class="clip-thumb-none">No Preview</div>';
50155019
return;
50165020
}
5017-
el.clipThumb.innerHTML = '<img src="data:image/jpeg;base64,' + data.image + '" alt="preview">';
5021+
if (el.clipThumb) el.clipThumb.innerHTML = '<img src="data:image/jpeg;base64,' + data.image + '" alt="preview">';
50185022
});
50195023
// Fetch metadata via probe
50205024
api("POST", "/audio/waveform", { file: selectedPath, samples: 1 }, function(err, data) {
50215025
if (!err && data && data.duration) {
50225026
var dur = Math.round(data.duration);
50235027
var m = Math.floor(dur / 60);
50245028
var s = dur % 60;
5025-
el.clipMetaDur.textContent = m + ":" + (s < 10 ? "0" : "") + s;
5029+
if (el.clipMetaDur) el.clipMetaDur.textContent = m + ":" + (s < 10 ? "0" : "") + s;
50265030
}
50275031
});
50285032
}
@@ -5106,11 +5110,11 @@
51065110
var _paletteResults = [];
51075111

51085112
function openCommandPalette() {
5109-
if (!el.commandPaletteOverlay) return;
5113+
if (!el.commandPaletteOverlay || !el.commandPaletteInput || !el.commandPaletteResults) return;
51105114
el.commandPaletteOverlay.classList.remove("hidden");
51115115
el.commandPaletteInput.value = "";
51125116
renderPaletteResults("");
5113-
setTimeout(function() { el.commandPaletteInput.focus(); }, 50);
5117+
setTimeout(function() { if (el.commandPaletteInput) el.commandPaletteInput.focus(); }, 50);
51145118
}
51155119

51165120
function closeCommandPalette() {
@@ -5134,7 +5138,7 @@
51345138
if (_paletteResults.length === 0) {
51355139
html = '<div class="command-palette-empty">No matching operations</div>';
51365140
}
5137-
el.commandPaletteResults.innerHTML = html;
5141+
if (el.commandPaletteResults) el.commandPaletteResults.innerHTML = html;
51385142
}
51395143

51405144
function executePaletteItem(tab, sub) {
@@ -5143,7 +5147,7 @@
51435147
}
51445148

51455149
function paletteNavigate(dir) {
5146-
if (!_paletteResults.length) return;
5150+
if (!_paletteResults.length || !el.commandPaletteResults) return;
51475151
var items = el.commandPaletteResults.querySelectorAll(".command-palette-item");
51485152
if (items[_paletteSelectedIdx]) items[_paletteSelectedIdx].classList.remove("selected");
51495153
_paletteSelectedIdx += dir;
@@ -5163,7 +5167,7 @@
51635167
}
51645168

51655169
function initCommandPalette() {
5166-
if (!el.commandPaletteOverlay) return;
5170+
if (!el.commandPaletteOverlay || !el.commandPaletteInput || !el.commandPaletteResults) return;
51675171

51685172
el.commandPaletteInput.addEventListener("input", function() {
51695173
renderPaletteResults(this.value);
@@ -5727,6 +5731,13 @@
57275731

57285732
// v1.3.0 - Recent Clips
57295733
if (el.recentClipsBtn) el.recentClipsBtn.addEventListener("click", showRecentClips);
5734+
// Close recent clips dropdown on outside click
5735+
document.addEventListener("click", function(e) {
5736+
if (el.recentClipsDropdown && !el.recentClipsDropdown.classList.contains("hidden") &&
5737+
!e.target.closest("#recentClipsBtn") && !e.target.closest("#recentClipsDropdown")) {
5738+
el.recentClipsDropdown.classList.add("hidden");
5739+
}
5740+
});
57305741
if (el.recentClipsDropdown) el.recentClipsDropdown.addEventListener("click", function(e) {
57315742
var item = e.target.closest(".recent-clip-item");
57325743
if (item) {

opencut/core/music_gen.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,8 @@ def concatenate_audio(
299299
list_file = tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False)
300300
try:
301301
for p in input_paths:
302-
list_file.write(f"file '{p}'\n")
302+
escaped = p.replace("'", "'\\''")
303+
list_file.write(f"file '{escaped}'\n")
303304
list_file.close()
304305

305306
cmd = [

opencut/core/video_fx.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,16 @@ def apply_lut(
211211
lut_safe = lut_path.replace("\\", "/").replace("'", "\\'")
212212
if intensity >= 1.0:
213213
vf = f"lut3d='{lut_safe}'"
214+
vf_flag = "-vf"
214215
else:
215-
# Blend between original and LUT-graded
216+
# Blend between original and LUT-graded (requires filter_complex for named pads)
216217
vf = f"split[a][b];[a]lut3d='{lut_safe}'[graded];[b][graded]blend=all_expr='A*{1-intensity}+B*{intensity}'"
218+
vf_flag = "-filter_complex"
217219

218220
cmd = [
219221
"ffmpeg", "-hide_banner", "-loglevel", "error",
220222
"-y", "-i", input_path,
221-
"-vf", vf,
223+
vf_flag, vf,
222224
"-c:a", "copy",
223225
output_path,
224226
]

opencut/routes/system.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,11 @@ def whisper_reinstall():
761761
"""Complete Whisper reinstall: uninstall, clear cache, reinstall fresh."""
762762
data = request.get_json(force=True) if request.data else {}
763763
backend = data.get("backend", "faster-whisper")
764+
765+
allowed_backends = {"faster-whisper", "openai-whisper", "whisperx"}
766+
if backend not in allowed_backends:
767+
return jsonify({"error": f"Unknown backend: {backend}"}), 400
768+
764769
cpu_mode = data.get("cpu_mode", False)
765770

766771
job_id = _new_job("reinstall-whisper", backend)

opencut/routes/video.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2209,6 +2209,8 @@ def title_overlay():
22092209

22102210
if not text:
22112211
return jsonify({"error": "No text"}), 400
2212+
if len(text) > 500:
2213+
return jsonify({"error": "Title text too long (max 500 chars)"}), 400
22122214
job_id = _new_job("title-overlay", fp)
22132215

22142216
def _process():
@@ -2312,6 +2314,8 @@ def transitions_join():
23122314
clips = data.get("clips", [])
23132315
if len(clips) < 2:
23142316
return jsonify({"error": "Need 2+ clips"}), 400
2317+
if len(clips) > MAX_BATCH_FILES:
2318+
return jsonify({"error": f"Too many clips (max {MAX_BATCH_FILES})"}), 400
23152319

23162320
# Validate all clip paths
23172321
validated_clips = []
@@ -2734,7 +2738,7 @@ def video_reframe():
27342738
mode = data.get("mode", "crop")
27352739
if mode not in _VALID_REFRAME_MODES:
27362740
mode = "crop"
2737-
_VALID_POSITIONS = {"center", "top", "bottom", "left", "right", "face"}
2741+
_VALID_POSITIONS = {"center", "top", "bottom", "left", "right", "face", "auto"}
27382742
position = data.get("position", "center")
27392743
if position not in _VALID_POSITIONS:
27402744
import re as _re2
@@ -2910,6 +2914,8 @@ def video_merge():
29102914
files = data.get("files", [])
29112915
if not files or len(files) < 2:
29122916
return jsonify({"error": "At least 2 files required"}), 400
2917+
if len(files) > MAX_BATCH_FILES:
2918+
return jsonify({"error": f"Too many files (max {MAX_BATCH_FILES})"}), 400
29132919

29142920
# Validate all file paths
29152921
validated_files = []

0 commit comments

Comments
 (0)