Skip to content

Commit bb61b80

Browse files
committed
fix: batch 28 second-pass audit — 35 issues fixed across 12 files
Rate limit acquired flag: 8 more sites converted (audio.py 5, video_editing.py 1, captions.py 1, system.py 2 double-release). Total rate limit sites now consistently use acquired flag pattern. Bare ffmpeg: speed_ramp.py had 4 remaining bare "ffmpeg" strings missed in batch 27 sweep. Replaced with get_ffmpeg_path(). Queue allowlist: 18 async routes added to _ALLOWED_QUEUE_ENDPOINTS (captions, transcript, full, whisperx, emotion-highlights, speed, lut, particles, color, face operations). Info disclosure: safe_error() detail truncated to 200 chars, file_not_found() uses basename only, RuntimeError handler truncated. social_post.py: TikTok upload chunked (was reading entire file), OAuth token access guarded, 8 urlopen wrapped in context managers. animated_captions.py: dict access on Whisper segments uses .get() with defaults. video_ai.py: info["fps"] uses .get() fallback. CSS: added 4 missing rules (cap-hint, badge, btn-xs, result-stats), fixed duplicate .result-area border-radius inconsistency.
1 parent 8d80a5d commit bb61b80

12 files changed

Lines changed: 145 additions & 73 deletions

File tree

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7789,7 +7789,7 @@ textarea:hover,
77897789
.result-area {
77907790
margin-top: 14px;
77917791
padding: 14px 16px;
7792-
border-radius: 18px;
7792+
border-radius: var(--r-md);
77937793
border: 1px solid rgba(236, 230, 218, 0.08);
77947794
background:
77957795
linear-gradient(180deg, rgba(16, 23, 33, 0.96), rgba(12, 17, 25, 0.92));
@@ -8497,6 +8497,31 @@ body.has-clip .media-sidecar-empty {
84978497
}
84988498
}
84998499

8500+
.cap-hint {
8501+
font-size: 11px;
8502+
color: var(--text-dim);
8503+
margin: 4px 0 8px;
8504+
line-height: 1.4;
8505+
}
8506+
.badge {
8507+
display: inline-block;
8508+
padding: 2px 8px;
8509+
font-size: 10px;
8510+
border-radius: 10px;
8511+
background: var(--green);
8512+
color: var(--bg);
8513+
font-weight: 600;
8514+
}
8515+
.btn-xs {
8516+
padding: 4px 8px;
8517+
font-size: 10px;
8518+
}
8519+
.result-stats {
8520+
font-size: 11px;
8521+
color: var(--text-dim);
8522+
margin-top: 8px;
8523+
}
8524+
85008525
@media (max-width: 380px) {
85018526
.sidebar {
85028527
width: 44px;

opencut/core/animated_captions.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ def render_animated_captions(
138138

139139
preset = ANIMATION_PRESETS.get(animation, ANIMATION_PRESETS["pop"])
140140
info = get_video_info(video_path)
141-
w, h = info["width"], info["height"]
141+
w = int(info.get("width", 1920))
142+
h = int(info.get("height", 1080))
142143
fps = float(info.get("fps", 30))
143144

144145
if on_progress:
@@ -206,16 +207,16 @@ def render_animated_captions(
206207
word_widths = []
207208
space_w = draw.textlength(" ", font=font) if hasattr(draw, 'textlength') else font_size * 0.3
208209
for wd in words:
209-
tw = draw.textlength(wd["word"], font=font) if hasattr(draw, 'textlength') else len(wd["word"]) * font_size * 0.6
210+
tw = draw.textlength(wd.get("word", ""), font=font) if hasattr(draw, 'textlength') else len(wd.get("word", "")) * font_size * 0.6
210211
word_widths.append(tw)
211212
total_w = sum(word_widths) + space_w * (len(words) - 1)
212213
x_start = (w - total_w) / 2
213214
y_pos = int(h * position_y)
214215

215216
x = x_start
216217
for i, wd in enumerate(words):
217-
is_active = float(wd["start"]) <= current_time <= float(wd["end"])
218-
is_past = current_time > float(wd["end"])
218+
is_active = float(wd.get("start", 0)) <= current_time <= float(wd.get("end", 0))
219+
is_past = current_time > float(wd.get("end", 0))
219220

220221
if is_active:
221222
color = highlight_color + (int(preset["active_opacity"] * 255),)
@@ -230,7 +231,7 @@ def render_animated_captions(
230231
for dx in range(-stroke_width, stroke_width + 1):
231232
for dy in range(-stroke_width, stroke_width + 1):
232233
if dx * dx + dy * dy <= stroke_width * stroke_width:
233-
draw.text((x + dx, y_pos + dy), wd["word"], font=font, fill=sc)
234+
draw.text((x + dx, y_pos + dy), wd.get("word", ""), font=font, fill=sc)
234235

235236
# Draw highlight box for that preset
236237
if animation == "highlight_box" and is_active:
@@ -240,7 +241,7 @@ def render_animated_captions(
240241
x + word_widths[i] + pad, y_pos + font_size + pad],
241242
fill=bx_color)
242243

243-
draw.text((x, y_pos), wd["word"], font=font, fill=color)
244+
draw.text((x, y_pos), wd.get("word", ""), font=font, fill=color)
244245
x += word_widths[i] + space_w
245246

246247
# Composite

opencut/core/social_post.py

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,8 @@ def _upload_youtube(
266266
},
267267
method="POST",
268268
)
269-
resp = urllib.request.urlopen(req, timeout=30)
270-
upload_url = resp.headers.get("Location")
271-
resp.close()
269+
with urllib.request.urlopen(req, timeout=30) as resp:
270+
upload_url = resp.headers.get("Location")
272271

273272
if not upload_url:
274273
return UploadResult(platform="youtube", success=False, error="Failed to get upload URL")
@@ -296,9 +295,8 @@ def _upload_youtube(
296295
}
297296
req = urllib.request.Request(upload_url, data=chunk, headers=headers, method="PUT")
298297
try:
299-
resp = urllib.request.urlopen(req, timeout=300)
300-
body = resp.read()
301-
resp.close()
298+
with urllib.request.urlopen(req, timeout=300) as resp:
299+
body = resp.read()
302300
except urllib.error.HTTPError as e:
303301
if e.code == 308:
304302
# Resume incomplete — read and discard response
@@ -364,11 +362,14 @@ def _refresh_youtube_token(auth: PlatformAuth) -> Optional[PlatformAuth]:
364362
data=data,
365363
headers={"Content-Type": "application/x-www-form-urlencoded"},
366364
)
367-
resp = urllib.request.urlopen(req, timeout=10)
368-
result = json.loads(resp.read())
369-
resp.close()
365+
with urllib.request.urlopen(req, timeout=10) as resp:
366+
result = json.loads(resp.read())
370367

371-
auth.access_token = result["access_token"]
368+
new_token = result.get("access_token")
369+
if not new_token:
370+
logger.error("YouTube token refresh response missing access_token")
371+
return None
372+
auth.access_token = new_token
372373
auth.expires_at = time.time() + result.get("expires_in", 3600)
373374

374375
creds = _load_credentials()
@@ -439,9 +440,8 @@ def _upload_tiktok(
439440
"Content-Type": "application/json; charset=UTF-8",
440441
},
441442
)
442-
resp = urllib.request.urlopen(req, timeout=30)
443-
init_result = json.loads(resp.read())
444-
resp.close()
443+
with urllib.request.urlopen(req, timeout=30) as resp:
444+
init_result = json.loads(resp.read())
445445

446446
publish_id = init_result.get("data", {}).get("publish_id", "")
447447
upload_url = init_result.get("data", {}).get("upload_url", "")
@@ -452,22 +452,33 @@ def _upload_tiktok(
452452
if on_progress:
453453
on_progress(15, "Uploading to TikTok...")
454454

455-
# Step 2: Upload file
455+
# Step 2: Upload file in 10MB chunks
456+
chunk_size = 10 * 1024 * 1024
457+
uploaded = 0
458+
456459
with open(filepath, "rb") as f:
457-
video_data = f.read()
460+
while uploaded < file_size:
461+
chunk = f.read(chunk_size)
462+
if not chunk:
463+
break
464+
end = min(uploaded + len(chunk), file_size)
465+
req = urllib.request.Request(
466+
upload_url,
467+
data=chunk,
468+
headers={
469+
"Content-Type": "video/mp4",
470+
"Content-Length": str(len(chunk)),
471+
"Content-Range": f"bytes {uploaded}-{end - 1}/{file_size}",
472+
},
473+
method="PUT",
474+
)
475+
with urllib.request.urlopen(req, timeout=300) as resp:
476+
resp.read()
458477

459-
req = urllib.request.Request(
460-
upload_url,
461-
data=video_data,
462-
headers={
463-
"Content-Type": "video/mp4",
464-
"Content-Length": str(len(video_data)),
465-
"Content-Range": f"bytes 0-{len(video_data) - 1}/{len(video_data)}",
466-
},
467-
method="PUT",
468-
)
469-
resp = urllib.request.urlopen(req, timeout=600)
470-
resp.close()
478+
uploaded += len(chunk)
479+
if on_progress:
480+
pct = 15 + int(uploaded / file_size * 70)
481+
on_progress(pct, f"Uploading... {uploaded / (1024*1024):.0f}MB / {file_size / (1024*1024):.0f}MB")
471482

472483
if on_progress:
473484
on_progress(90, "Finalizing...")
@@ -482,9 +493,8 @@ def _upload_tiktok(
482493
"Content-Type": "application/json",
483494
},
484495
)
485-
resp = urllib.request.urlopen(req, timeout=30)
486-
resp.read() # Consume response body
487-
resp.close()
496+
with urllib.request.urlopen(req, timeout=30) as resp:
497+
resp.read() # Consume response body
488498

489499
if on_progress:
490500
on_progress(100, "Upload complete!")
@@ -561,9 +571,8 @@ def _upload_instagram(
561571
"Content-Type": "application/json",
562572
},
563573
)
564-
resp = urllib.request.urlopen(req, timeout=30)
565-
create_result = json.loads(resp.read())
566-
resp.close()
574+
with urllib.request.urlopen(req, timeout=30) as resp:
575+
create_result = json.loads(resp.read())
567576

568577
container_id = create_result.get("id", "")
569578
if not container_id:
@@ -580,9 +589,8 @@ def _upload_instagram(
580589
f"https://graph.facebook.com/v19.0/{container_id}?fields=status_code",
581590
headers={"Authorization": f"Bearer {auth.access_token}"},
582591
)
583-
resp = urllib.request.urlopen(req, timeout=10)
584-
status = json.loads(resp.read())
585-
resp.close()
592+
with urllib.request.urlopen(req, timeout=10) as resp:
593+
status = json.loads(resp.read())
586594

587595
code = status.get("status_code", "")
588596
if code == "FINISHED":
@@ -610,9 +618,8 @@ def _upload_instagram(
610618
"Content-Type": "application/json",
611619
},
612620
)
613-
resp = urllib.request.urlopen(req, timeout=30)
614-
pub_result = json.loads(resp.read())
615-
resp.close()
621+
with urllib.request.urlopen(req, timeout=30) as resp:
622+
pub_result = json.loads(resp.read())
616623

617624
media_id = pub_result.get("id", "")
618625

opencut/core/speed_ramp.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import tempfile
1818
from typing import Callable, Dict, List, Optional
1919

20-
from opencut.helpers import get_video_info, run_ffmpeg
20+
from opencut.helpers import get_ffmpeg_path, get_video_info, run_ffmpeg
2121

2222
logger = logging.getLogger("opencut")
2323

@@ -88,7 +88,7 @@ def change_speed(
8888
# Audio: atempo filter (chained for extreme values)
8989
af_parts = _build_atempo_chain(speed, maintain_pitch)
9090

91-
cmd = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", input_path]
91+
cmd = [get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", "-y", "-i", input_path]
9292
cmd += ["-vf", vf]
9393
if af_parts:
9494
cmd += ["-af", af_parts]
@@ -147,7 +147,7 @@ def reverse_video(
147147
on_progress(10, "Reversing video...")
148148

149149
cmd = [
150-
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
150+
get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", "-y",
151151
"-i", input_path,
152152
"-vf", "reverse",
153153
]
@@ -237,7 +237,7 @@ def speed_ramp(
237237
af = _build_atempo_chain(avg_speed)
238238

239239
cmd = [
240-
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
240+
get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", "-y",
241241
"-ss", str(start_time), "-to", str(end_time),
242242
"-i", input_path,
243243
"-vf", vf,
@@ -265,7 +265,7 @@ def speed_ramp(
265265
f.write(f"file '{seg}'\n")
266266

267267
run_ffmpeg([
268-
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
268+
get_ffmpeg_path(), "-hide_banner", "-loglevel", "error", "-y",
269269
"-f", "concat", "-safe", "0", "-i", list_file,
270270
"-c", "copy",
271271
output_path,

opencut/core/video_ai.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ def frame_interpolate(
614614
output_path = _output_path(input_path, f"interp_{multiplier}x", output_dir)
615615

616616
info = get_video_info(input_path)
617-
target_fps = info["fps"] * multiplier
617+
target_fps = info.get("fps", 30) * multiplier
618618

619619
if on_progress:
620620
on_progress(10, f"Interpolating {info['fps']:.0f}fps -> {target_fps:.0f}fps...")

opencut/errors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"""
1313

1414
import logging
15+
import os
1516

1617
from flask import jsonify
1718

@@ -128,7 +129,7 @@ def safe_error(exc, context=""):
128129
logger.exception("Error [%s]%s: %s", code, log_ctx, exc)
129130

130131
return error_response(code, user_msg, status=status,
131-
suggestion=suggestion, detail=msg)
132+
suggestion=suggestion, detail=msg[:200] if msg else None)
132133

133134

134135
# ---------------------------------------------------------------------------
@@ -148,7 +149,7 @@ def missing_dependency(name: str) -> OpenCutError:
148149
def file_not_found(path: str) -> OpenCutError:
149150
return OpenCutError(
150151
code="FILE_NOT_FOUND",
151-
message=f"File not found: {path}",
152+
message=f"File not found: {os.path.basename(path) if path else 'unknown'}",
152153
suggestion="Check that the file has not been moved or deleted.",
153154
status=404,
154155
)

0 commit comments

Comments
 (0)