Skip to content

Commit 55ebb35

Browse files
committed
fix: batch P3 correctness fixes across backend and installers
- VBS launcher: use WshShell.Environment instead of cmd /c string concatenation to prevent injection from paths containing & or ^ - auth.json: restrict Windows DACL via icacls (was POSIX-only chmod) - CLI: fix bare-filename CWD issue in loudness-match (os.path.abspath) - CLI: use .get() for auto-zoom width/height to prevent KeyError - CLI/InstallerBuilder: fix placeholder github.com/opencut URLs - Export cancel: add proc.wait() after kill before unlinking partial file - preview_frame: check ffmpeg returncode and output file size > 0 - Multiview routes: validate_filepath on video_paths, content_path, reaction_path before forwarding to core - smart_trim: surface TimeoutExpired/SubprocessError instead of swallowing as "no speech detected" - waveform_timeline: use array.array instead of list for PCM samples (~8x less memory for long audio) - Installers: prefer requirements.txt / requirements-lock.txt over loose pip specs in fallback paths
1 parent 7c48d96 commit 55ebb35

10 files changed

Lines changed: 85 additions & 37 deletions

Install.ps1

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,14 @@ Write-Header "Step 4/7: Python Dependencies"
381381

382382
Write-Step "Installing core dependencies..."
383383
& $pythonCmd -m pip install --upgrade pip --quiet 2>$null
384-
& $pythonCmd -m pip install click rich flask flask-cors --quiet 2>&1 | Out-Null
385-
Write-Ok "Core packages installed (click, rich, flask, flask-cors)"
384+
$reqFile = Join-Path $script:InstallDir "requirements.txt"
385+
if (Test-Path $reqFile) {
386+
& $pythonCmd -m pip install -r $reqFile --quiet 2>&1 | Out-Null
387+
Write-Ok "Core packages installed from requirements.txt"
388+
} else {
389+
& $pythonCmd -m pip install click rich flask flask-cors --quiet 2>&1 | Out-Null
390+
Write-Ok "Core packages installed (click, rich, flask, flask-cors)"
391+
}
386392

387393
# Install OpenCut package
388394
Write-Step "Installing OpenCut package..."

InstallerBuilder.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ pause
579579
#define MyAppName "OpenCut"
580580
#define MyAppVersion "0.6.5"
581581
#define MyAppPublisher "OpenCut"
582-
#define MyAppURL "https://github.com/opencut"
582+
#define MyAppURL "https://github.com/SysAdminDoc/OpenCut"
583583
584584
[Setup]
585585
AppId={{8A7B9C0D-1E2F-3A4B-5C6D-7E8F9A0B1C2D}

OpenCut-Server.vbs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,29 @@
11
Set WshShell = CreateObject("WScript.Shell")
22
Set fso = CreateObject("Scripting.FileSystemObject")
3+
Set env = WshShell.Environment("Process")
34

45
strPath = fso.GetParentFolderName(WScript.ScriptFullName)
56

6-
' Build environment
7-
strPython = "python"
8-
strEnv = "set OPENCUT_HOME=" & strPath
7+
' Build environment via WshShell.Environment (safe for paths with & ^ etc.)
8+
env("OPENCUT_HOME") = strPath
99

10+
strPython = "python"
1011
If fso.FileExists(strPath & "\python\python.exe") Then
1112
strPython = """" & strPath & "\python\python.exe"""
12-
strEnv = strEnv & " & set PATH=" & strPath & "\python;" & strPath & "\python\Scripts;%PATH%"
13+
env("PATH") = strPath & "\python;" & strPath & "\python\Scripts;" & env("PATH")
1314
End If
1415

1516
If fso.FolderExists(strPath & "\ffmpeg") Then
16-
strEnv = strEnv & " & set PATH=" & strPath & "\ffmpeg;%PATH%"
17+
env("PATH") = strPath & "\ffmpeg;" & env("PATH")
1718
End If
1819

1920
If fso.FolderExists(strPath & "\models") Then
20-
strEnv = strEnv & " & set OPENCUT_BUNDLED=true"
21-
strEnv = strEnv & " & set WHISPER_MODELS_DIR=" & strPath & "\models\whisper"
22-
strEnv = strEnv & " & set TORCH_HOME=" & strPath & "\models\demucs"
23-
strEnv = strEnv & " & set OPENCUT_FLORENCE_DIR=" & strPath & "\models\florence"
24-
strEnv = strEnv & " & set OPENCUT_LAMA_DIR=" & strPath & "\models\lama"
21+
env("OPENCUT_BUNDLED") = "true"
22+
env("WHISPER_MODELS_DIR") = strPath & "\models\whisper"
23+
env("TORCH_HOME") = strPath & "\models\demucs"
24+
env("OPENCUT_FLORENCE_DIR") = strPath & "\models\florence"
25+
env("OPENCUT_LAMA_DIR") = strPath & "\models\lama"
2526
End If
2627

27-
strCmd = "cmd /c """ & strEnv & " & " & strPython & " -m opencut.server"""
28-
2928
' Run completely hidden (0 = hidden, False = don't wait)
30-
WshShell.Run strCmd, 0, False
29+
WshShell.Run strPython & " -m opencut.server", 0, False

install.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,24 @@ def install_deps():
7373
check=True, timeout=pip_timeout,
7474
)
7575
else:
76-
subprocess.run(
77-
[sys.executable, "-m", "pip", "install",
78-
"click>=8.0", "rich>=13.0", "flask>=3.0", "flask-cors>=4.0",
79-
"python-json-logger>=2.0", "psutil>=5.9",
80-
"faster-whisper>=1.0", "opencv-python-headless>=4.8",
81-
"Pillow>=10.0", "numpy>=1.24",
82-
"--prefer-binary", "--progress-bar", "on"],
83-
check=True, timeout=pip_timeout,
84-
)
76+
lock_file = os.path.join(base_dir, "requirements-lock.txt")
77+
fallback_req = lock_file if os.path.exists(lock_file) else None
78+
if fallback_req:
79+
subprocess.run(
80+
[sys.executable, "-m", "pip", "install", "-r", fallback_req,
81+
"--prefer-binary", "--progress-bar", "on"],
82+
check=True, timeout=pip_timeout,
83+
)
84+
else:
85+
subprocess.run(
86+
[sys.executable, "-m", "pip", "install",
87+
"click>=8.0", "rich>=13.0", "flask>=3.0", "flask-cors>=4.0",
88+
"python-json-logger>=2.0", "psutil>=5.9",
89+
"faster-whisper>=1.0", "opencv-python-headless>=4.8",
90+
"Pillow>=10.0", "numpy>=1.24",
91+
"--prefer-binary", "--progress-bar", "on"],
92+
check=True, timeout=pip_timeout,
93+
)
8594
except subprocess.TimeoutExpired:
8695
print(f" [!!] pip install timed out after {pip_timeout // 60} minutes.")
8796
print(" Check your network connection and try again.")

opencut/auth.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ def as_dict(self) -> dict:
7171
return {"token": self.token, "issued_at": self.issued_at, "label": self.label}
7272

7373

74+
def _restrict_windows_acl(path: Path) -> None:
75+
"""Restrict file to current user only via icacls (best-effort)."""
76+
import subprocess as _sp
77+
try:
78+
target = str(path)
79+
username = os.environ.get("USERNAME", "")
80+
if not username:
81+
return
82+
_sp.run(
83+
["icacls", target, "/inheritance:r", "/grant:r", f"{username}:(R,W)"],
84+
capture_output=True, timeout=10,
85+
)
86+
except Exception as exc:
87+
logger.warning("opencut.auth: could not restrict ACL on %s: %s", path, exc)
88+
89+
7490
def _atomic_write(path: Path, payload: dict) -> None:
7591
path.parent.mkdir(parents=True, exist_ok=True)
7692
fd, tmp_name = tempfile.mkstemp(dir=str(path.parent), prefix="auth_", suffix=".json")
@@ -83,6 +99,8 @@ def _atomic_write(path: Path, payload: dict) -> None:
8399
except OSError as exc: # pragma: no cover - filesystem oddity
84100
logger.warning("opencut.auth: could not chmod %s: %s", tmp_name, exc)
85101
os.replace(tmp_name, path)
102+
if os.name == "nt":
103+
_restrict_windows_acl(path)
86104
except Exception:
87105
try:
88106
os.unlink(tmp_name)

opencut/cli.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545

4646
def print_banner():
4747
console.print(Panel(
48-
BANNER + " Open Source Video Editing Automation\n github.com/opencut",
48+
BANNER + " Open Source Video Editing Automation\n github.com/SysAdminDoc/OpenCut",
4949
style="bold cyan",
5050
box=box.ROUNDED,
5151
padding=(0, 2),
@@ -1046,7 +1046,7 @@ def loudness_match(files, target_lufs, output_dir):
10461046
) as progress:
10471047
task = progress.add_task(f"Normalizing {len(files)} file(s)...", total=None)
10481048
start_time = time.time()
1049-
results = batch_loudness_match(list(files), output_dir=output_dir or os.path.dirname(files[0]), target_lufs=target_lufs)
1049+
results = batch_loudness_match(list(files), output_dir=output_dir or os.path.dirname(os.path.abspath(files[0])), target_lufs=target_lufs)
10501050
elapsed = time.time() - start_time
10511051
progress.update(task, description=f"[green]Normalization complete ({elapsed:.1f}s)")
10521052
progress.stop()
@@ -1110,10 +1110,12 @@ def auto_zoom(file, zoom_amount, easing, output_dir, apply):
11101110
console.print("[bold]Applying zoom to video...[/bold]")
11111111
# Build zoompan filter — simplified: use first keyframe zoom for now
11121112
zoom_val = keyframes[0].get("zoom", zoom_amount) if keyframes else zoom_amount
1113+
w = info.get("width", 1920)
1114+
h = info.get("height", 1080)
11131115
run_ffmpeg([
11141116
get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", "-y",
11151117
"-i", file,
1116-
"-vf", f"zoompan=z={zoom_val}:d=1:s={info['width']}x{info['height']}:fps={fps}",
1118+
"-vf", f"zoompan=z={zoom_val}:d=1:s={w}x{h}:fps={fps}",
11171119
"-c:a", "copy", output_path,
11181120
])
11191121
console.print(f"\n[green bold]Saved:[/green bold] {output_path}\n")

opencut/core/smart_trim.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ def _detect_silence_regions(
186186
"end": total_duration,
187187
"duration": total_duration - start,
188188
})
189+
except _sp.TimeoutExpired:
190+
raise RuntimeError(f"Silence detection timed out for {file_path}")
191+
except _sp.SubprocessError as exc:
192+
raise RuntimeError(f"Silence detection failed for {file_path}: {exc}") from exc
189193
except Exception as exc:
190194
logger.debug("Silence detection failed for %s: %s", file_path, exc)
191195

@@ -244,6 +248,10 @@ def _detect_scene_changes(
244248
# Parse pts_time from showinfo output
245249
times = re.findall(r"pts_time:\s*([\d.]+)", stderr)
246250
timestamps = [float(t) for t in times[:max_scenes]]
251+
except _sp.TimeoutExpired:
252+
raise RuntimeError(f"Scene detection timed out for {file_path}")
253+
except _sp.SubprocessError as exc:
254+
raise RuntimeError(f"Scene detection failed for {file_path}: {exc}") from exc
247255
except Exception as exc:
248256
logger.debug("Scene detection failed for %s: %s", file_path, exc)
249257

opencut/core/waveform_timeline.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
timeline rendering and visualization.
66
"""
77

8+
import array
89
import logging
910
import os
10-
import struct
1111
import subprocess
1212
import tempfile
13-
from typing import Callable, Dict, List, Optional, Tuple
13+
from typing import Callable, Dict, Optional, Tuple
1414

1515
from opencut.helpers import FFmpegCmd, get_ffmpeg_path, run_ffmpeg
1616

@@ -36,10 +36,11 @@ def _extract_pcm(audio_path: str, sample_rate: int = 8000) -> Tuple[bytes, int]:
3636
return result.stdout, sample_rate
3737

3838

39-
def _pcm_to_samples(pcm_data: bytes) -> List[int]:
40-
"""Convert raw PCM bytes (16-bit signed LE) to sample list."""
41-
n_samples = len(pcm_data) // 2
42-
return list(struct.unpack(f"<{n_samples}h", pcm_data[:n_samples * 2]))
39+
def _pcm_to_samples(pcm_data: bytes) -> array.array:
40+
"""Convert raw PCM bytes (16-bit signed LE) to sample array."""
41+
samples = array.array("h")
42+
samples.frombytes(pcm_data[:len(pcm_data) // 2 * 2])
43+
return samples
4344

4445

4546
# ------------------------------------------------------------------

opencut/routes/multiview_repurpose_routes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
safe_bool,
2929
safe_float,
3030
safe_int,
31+
validate_filepath,
3132
validate_output_path,
3233
validate_path,
3334
)
@@ -62,6 +63,7 @@ def split_screen_create(job_id, filepath, data):
6263
video_paths = data.get("video_paths", [])
6364
if not video_paths:
6465
raise ValueError("video_paths is required (list of file paths)")
66+
video_paths = [validate_filepath(p) for p in video_paths]
6567

6668
layout_name = data.get("layout", "side_by_side")
6769
custom_layout = data.get("custom_layout")
@@ -114,6 +116,8 @@ def reaction_create(job_id, filepath, data):
114116
reaction_path = data.get("reaction_path", "")
115117
if not content_path or not reaction_path:
116118
raise ValueError("content_path and reaction_path are required")
119+
content_path = validate_filepath(content_path)
120+
reaction_path = validate_filepath(reaction_path)
117121

118122
preset_name = data.get("preset", "corner_pip")
119123
output_width = safe_int(data.get("width", 1920), 1920, min_val=320, max_val=7680)

opencut/routes/video_core.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -708,9 +708,10 @@ def _seg_len(seg):
708708

709709
if _is_cancelled(job_id):
710710
proc.kill()
711+
proc.wait(timeout=10)
711712
_unregister_job_process(job_id)
712713
_cleanup_filter_script()
713-
# Clean up partial file
714+
# Clean up partial file (wait() above ensures the handle is released)
714715
if os.path.exists(output_path):
715716
try:
716717
os.unlink(output_path)
@@ -1496,8 +1497,8 @@ def preview_frame(job_id, filepath, data):
14961497
"-vframes", "1", "-vf", f"scale={width}:-1",
14971498
"-q:v", "2", "-y", tmp
14981499
]
1499-
_sp.run(cmd, capture_output=True, timeout=30)
1500-
if not os.path.isfile(tmp):
1500+
result = _sp.run(cmd, capture_output=True, timeout=30)
1501+
if result.returncode != 0 or not os.path.isfile(tmp) or os.path.getsize(tmp) == 0:
15011502
raise ValueError("Frame extraction failed")
15021503
with open(tmp, "rb") as f:
15031504
img_data = base64.b64encode(f.read()).decode("utf-8")

0 commit comments

Comments
 (0)