Improve render.py robustness/performance, add editing helpers#63
Improve render.py robustness/performance, add editing helpers#63eaikarensantos-gif wants to merge 4 commits into
Conversation
- Surface ffmpeg stderr on failure instead of swallowing it - Match output fps to the source instead of forcing 24fps - Probe each source once (HDR + orientation + fps) with a cache - Encode segments in parallel (up to 4 ffmpeg processes) - Detect libass support by probing -filters instead of hardcoding paths - Refuse lossless concat when segment resolutions mismatch - Escape subtitle paths safely for the filtergraph (brackets, quotes) - Use -map 0:a? so audioless sources don't fail the composite Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Reuse one Anthropic client instead of per-request instantiation - Cache TechCrunch headlines for 5 minutes - Update model to claude-opus-4-8 - Disable Flask debug mode by default (opt in via FLASK_DEBUG=1) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
18 issues found across 12 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".gitignore">
<violation number="1" location=".gitignore:75">
P2: Root-only .gitignore patterns are not anchored with a leading '/', causing them to match directories at any depth despite the explicit "repo root" comment.</violation>
</file>
<file name="helpers/export_formats.py">
<violation number="1" location="helpers/export_formats.py:48">
P2: `get_video_info` does not handle ffprobe failures or malformed output, leading to unhandled `JSONDecodeError`, `KeyError`, or `StopIteration` on invalid/non-video inputs.</violation>
<violation number="2" location="helpers/export_formats.py:70">
P2: ffmpeg global options `-y` and `-loglevel` are placed after the output path, so they may not take effect and non-interactive runs can hang on overwrite prompts</violation>
</file>
<file name="helpers/burn_subs_pil.py">
<violation number="1" location="helpers/burn_subs_pil.py:181">
P1: Silent fallback from yuva420p to yuv420p removes alpha channel, breaking transparent subtitle overlay</violation>
</file>
<file name="helpers/correct_transcript.py">
<violation number="1" location="helpers/correct_transcript.py:94">
P1: Invalid `ensure_ascii=False` passed to built-in `open()`, causing a runtime TypeError before the corrected JSON is written.</violation>
</file>
<file name="helpers/batch.py">
<violation number="1" location="helpers/batch.py:89">
P2: Using only `Path(video_path).stem` for generated artifact paths causes collisions when inputs share the same base name with different extensions (e.g., `clip.mp4` and `clip.mov`). The second input reuses or overwrites the first's transcript, EDL, and final output, silently corrupting results.</violation>
</file>
<file name="helpers/thumbnail.py">
<violation number="1" location="helpers/thumbnail.py:35">
P2: Uncaught `ValueError` when ffprobe returns non-numeric duration (e.g., `N/A`) crashes the script instead of exiting gracefully.</violation>
<violation number="2" location="helpers/thumbnail.py:51">
P1: Missing validation for non-positive `--interval` causes infinite loop in frame extraction</violation>
<violation number="3" location="helpers/thumbnail.py:193">
P1: `saved.append` runs unconditionally even when `cv2.imwrite` was skipped, causing `make_contact_sheet` to crash on `FileNotFoundError` when it tries to open missing thumbnail files.</violation>
</file>
<file name="helpers/detect_breaths.py">
<violation number="1" location="helpers/detect_breaths.py:47">
P2: Audio extraction reads WAV data with a hardcoded 44-byte header offset, which can mis-parse valid WAV files that contain extra chunks (e.g., LIST, fact, metadata) before the data chunk. Non-audio bytes would then be interpreted as samples, corrupting RMS calculations and breath detection results.</violation>
<violation number="2" location="helpers/detect_breaths.py:57">
P1: Missing edge-case guards can crash on invalid `--window-ms` or very short audio: `window` may be 0 causing `ZeroDivisionError`, and empty RMS causes `np.percentile` to crash on empty array.</violation>
<violation number="3" location="helpers/detect_breaths.py:77">
P2: Minimum breath duration filtering uses `int()` (floor) for `min_windows`, allowing regions shorter than the user-configured `--min-ms`.</violation>
</file>
<file name="helpers/denoise.py">
<violation number="1" location="helpers/denoise.py:57">
P2: curl model download lacks `--fail`, allowing HTTP error pages to be silently saved as the model file, breaking `arnndn` denoise mode persistently.</violation>
</file>
<file name="news_server.py">
<violation number="1" location="news_server.py:102">
P2: request.json can raise BadRequest/UnsupportedMediaType before the route's try/except, causing the API to return framework error responses instead of its own JSON error contract.</violation>
</file>
<file name="helpers/reframe.py">
<violation number="1" location="helpers/reframe.py:128">
P2: Missing guard for empty sampled positions can crash `reframe.py` with `ValueError` when processing a corrupt or zero-frame video that opens but yields no frames.</violation>
<violation number="2" location="helpers/reframe.py:187">
P2: ffmpeg global options `-y` and `-loglevel` placed after output file may be ignored</violation>
</file>
<file name="helpers/check_gaps.py">
<violation number="1" location="helpers/check_gaps.py:27">
P2: Using `Path.glob()` with raw user/data-derived `source_name` can produce incorrect matches if the source name contains glob metacharacters (`*`, `?`, `[]`). Furthermore, selecting `candidates[0]` without sorting makes the file choice nondeterministic when multiple files match.</violation>
<violation number="2" location="helpers/check_gaps.py:28">
P2: Missing transcript files are silently ignored, leading to false "no gaps found" reports when segments were never actually checked.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| result = subprocess.run(cmd, capture_output=True) | ||
| if result.returncode != 0: | ||
| # Fallback: yuv420p se yuva420p não suportado | ||
| cmd[cmd.index("yuva420p")] = "yuv420p" |
There was a problem hiding this comment.
P1: Silent fallback from yuva420p to yuv420p removes alpha channel, breaking transparent subtitle overlay
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/burn_subs_pil.py, line 181:
<comment>Silent fallback from yuva420p to yuv420p removes alpha channel, breaking transparent subtitle overlay</comment>
<file context>
@@ -0,0 +1,195 @@
+ result = subprocess.run(cmd, capture_output=True)
+ if result.returncode != 0:
+ # Fallback: yuv420p se yuva420p não suportado
+ cmd[cmd.index("yuva420p")] = "yuv420p"
+ result2 = subprocess.run(cmd, capture_output=True)
+ if result2.returncode != 0:
</file context>
| data["words"] = correct_words(data["words"], api_key=args.api_key) | ||
|
|
||
| out_path = args.output or str(Path(args.transcript).with_suffix(".corrected.json")) | ||
| with open(out_path, "w", ensure_ascii=False) as f: |
There was a problem hiding this comment.
P1: Invalid ensure_ascii=False passed to built-in open(), causing a runtime TypeError before the corrected JSON is written.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/correct_transcript.py, line 94:
<comment>Invalid `ensure_ascii=False` passed to built-in `open()`, causing a runtime TypeError before the corrected JSON is written.</comment>
<file context>
@@ -0,0 +1,101 @@
+ data["words"] = correct_words(data["words"], api_key=args.api_key)
+
+ out_path = args.output or str(Path(args.transcript).with_suffix(".corrected.json"))
+ with open(out_path, "w", ensure_ascii=False) as f:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+
</file context>
| saved.append((ts, dst, sc, box)) | ||
| print(f" #{rank} t={ts:.1f}s score={sc:.1f} → {dst}") | ||
|
|
There was a problem hiding this comment.
P1: saved.append runs unconditionally even when cv2.imwrite was skipped, causing make_contact_sheet to crash on FileNotFoundError when it tries to open missing thumbnail files.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/thumbnail.py, line 193:
<comment>`saved.append` runs unconditionally even when `cv2.imwrite` was skipped, causing `make_contact_sheet` to crash on `FileNotFoundError` when it tries to open missing thumbnail files.</comment>
<file context>
@@ -0,0 +1,206 @@
+ img = cv2.imread(img_path)
+ if img is not None:
+ cv2.imwrite(dst, img, [cv2.IMWRITE_JPEG_QUALITY, 95])
+ saved.append((ts, dst, sc, box))
+ print(f" #{rank} t={ts:.1f}s score={sc:.1f} → {dst}")
+
</file context>
| saved.append((ts, dst, sc, box)) | |
| print(f" #{rank} t={ts:.1f}s score={sc:.1f} → {dst}") | |
| if img is not None: | |
| cv2.imwrite(dst, img, [cv2.IMWRITE_JPEG_QUALITY, 95]) | |
| saved.append((ts, dst, sc, box)) | |
| print(f" #{rank} t={ts:.1f}s score={sc:.1f} → {dst}") |
| ], check=False) | ||
| if os.path.exists(out_path): | ||
| frames.append((t, out_path)) | ||
| t = round(t + interval_s, 3) |
There was a problem hiding this comment.
P1: Missing validation for non-positive --interval causes infinite loop in frame extraction
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/thumbnail.py, line 51:
<comment>Missing validation for non-positive `--interval` causes infinite loop in frame extraction</comment>
<file context>
@@ -0,0 +1,206 @@
+ ], check=False)
+ if os.path.exists(out_path):
+ frames.append((t, out_path))
+ t = round(t + interval_s, 3)
+ return frames
+
</file context>
|
|
||
| def compute_rms(samples, sr, window_ms=50): | ||
| """Calcula RMS por janelas de window_ms milissegundos.""" | ||
| window = int(sr * window_ms / 1000) |
There was a problem hiding this comment.
P1: Missing edge-case guards can crash on invalid --window-ms or very short audio: window may be 0 causing ZeroDivisionError, and empty RMS causes np.percentile to crash on empty array.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/detect_breaths.py, line 57:
<comment>Missing edge-case guards can crash on invalid `--window-ms` or very short audio: `window` may be 0 causing `ZeroDivisionError`, and empty RMS causes `np.percentile` to crash on empty array.</comment>
<file context>
@@ -0,0 +1,227 @@
+
+def compute_rms(samples, sr, window_ms=50):
+ """Calcula RMS por janelas de window_ms milissegundos."""
+ window = int(sr * window_ms / 1000)
+ n_windows = len(samples) // window
+ rms = np.array([
</file context>
|
|
||
| @app.route("/api/generate", methods=["POST"]) | ||
| def generate(): | ||
| data = request.json or {} |
There was a problem hiding this comment.
P2: request.json can raise BadRequest/UnsupportedMediaType before the route's try/except, causing the API to return framework error responses instead of its own JSON error contract.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At news_server.py, line 102:
<comment>request.json can raise BadRequest/UnsupportedMediaType before the route's try/except, causing the API to return framework error responses instead of its own JSON error contract.</comment>
<file context>
@@ -0,0 +1,133 @@
+
+@app.route("/api/generate", methods=["POST"])
+def generate():
+ data = request.json or {}
+ headline = data.get("headline", "").strip()
+ summary = data.get("summary", "").strip()
</file context>
| crop_h = int(src_w * target_h / target_w) | ||
|
|
||
| # posição X do centro de interesse (mediana suavizada) | ||
| median_cx = float(np.median([cx for _, cx in smooth_pos])) |
There was a problem hiding this comment.
P2: Missing guard for empty sampled positions can crash reframe.py with ValueError when processing a corrupt or zero-frame video that opens but yields no frames.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/reframe.py, line 128:
<comment>Missing guard for empty sampled positions can crash `reframe.py` with `ValueError` when processing a corrupt or zero-frame video that opens but yields no frames.</comment>
<file context>
@@ -0,0 +1,198 @@
+ crop_h = int(src_w * target_h / target_w)
+
+ # posição X do centro de interesse (mediana suavizada)
+ median_cx = float(np.median([cx for _, cx in smooth_pos]))
+ center_x = int(median_cx * src_w)
+
</file context>
| out_path, "-y", "-loglevel", "error" | ||
| ] | ||
| result = subprocess.run(cmd) | ||
| if result.returncode == 0: | ||
| size_mb = os.path.getsize(out_path) / 1024 / 1024 | ||
| print(f"\ndone: {out_path} ({size_mb:.1f} MB)") | ||
| else: | ||
| sys.exit("Erro no reframe.") | ||
|
|
There was a problem hiding this comment.
P2: ffmpeg global options -y and -loglevel placed after output file may be ignored
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/reframe.py, line 187:
<comment>ffmpeg global options `-y` and `-loglevel` placed after output file may be ignored</comment>
<file context>
@@ -0,0 +1,198 @@
+ "-vf", vf,
+ "-c:v", "libx264", "-crf", "20", "-preset", "fast",
+ "-c:a", "copy",
+ out_path, "-y", "-loglevel", "error"
+ ]
+ result = subprocess.run(cmd)
</file context>
| out_path, "-y", "-loglevel", "error" | |
| ] | |
| result = subprocess.run(cmd) | |
| if result.returncode == 0: | |
| size_mb = os.path.getsize(out_path) / 1024 / 1024 | |
| print(f"\ndone: {out_path} ({size_mb:.1f} MB)") | |
| else: | |
| sys.exit("Erro no reframe.") | |
| cmd = [ | |
| "ffmpeg", "-y", "-loglevel", "error", | |
| "-i", video_path, | |
| "-vf", vf, | |
| "-c:v", "libx264", "-crf", "20", "-preset", "fast", | |
| "-c:a", "copy", | |
| out_path, | |
| ] |
| # index transcripts by source name | ||
| transcripts = {} | ||
| for source_name in edl["sources"]: | ||
| candidates = list(Path(transcripts_dir).glob(f"{source_name}.json")) |
There was a problem hiding this comment.
P2: Using Path.glob() with raw user/data-derived source_name can produce incorrect matches if the source name contains glob metacharacters (*, ?, []). Furthermore, selecting candidates[0] without sorting makes the file choice nondeterministic when multiple files match.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/check_gaps.py, line 27:
<comment>Using `Path.glob()` with raw user/data-derived `source_name` can produce incorrect matches if the source name contains glob metacharacters (`*`, `?`, `[]`). Furthermore, selecting `candidates[0]` without sorting makes the file choice nondeterministic when multiple files match.</comment>
<file context>
@@ -0,0 +1,87 @@
+ # index transcripts by source name
+ transcripts = {}
+ for source_name in edl["sources"]:
+ candidates = list(Path(transcripts_dir).glob(f"{source_name}.json"))
+ if candidates:
+ transcripts[source_name] = load_words(str(candidates[0]))
</file context>
| transcripts = {} | ||
| for source_name in edl["sources"]: | ||
| candidates = list(Path(transcripts_dir).glob(f"{source_name}.json")) | ||
| if candidates: |
There was a problem hiding this comment.
P2: Missing transcript files are silently ignored, leading to false "no gaps found" reports when segments were never actually checked.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At helpers/check_gaps.py, line 28:
<comment>Missing transcript files are silently ignored, leading to false "no gaps found" reports when segments were never actually checked.</comment>
<file context>
@@ -0,0 +1,87 @@
+ transcripts = {}
+ for source_name in edl["sources"]:
+ candidates = list(Path(transcripts_dir).glob(f"{source_name}.json"))
+ if candidates:
+ transcripts[source_name] = load_words(str(candidates[0]))
+
</file context>
Summary
-filtersinstead of hardcoding Homebrew paths; refuse lossless concat when segment resolutions mismatch (previously produced a corrupt file); escape subtitle paths safely in the filtergraph;-map 0:a?so audioless sources don't fail the composite.claude-opus-4-8, Flask debug off by default.Test plan
python3 -m py_compileon all edited files🤖 Generated with Claude Code
Summary by cubic
Improve
helpers/render.pyreliability and speed by matching source FPS, parallelizing segment encodes, surfacingffmpegerrors, and safer subtitle/libass handling. Add a compact toolkit of editing helpers plus a smallnews_server.py, and ignore large media in.gitignore.New Features
batch.py,denoise.py,reframe.py,detect_breaths.py,burn_subs_pil.py,correct_transcript.py,check_gaps.py,export_formats.py,thumbnail.py.news_server.py: shared Anthropic client, 5‑min RSS cache, modelclaude-opus-4-8, debug off by default..gitignore: ignore common media binaries and working folders at repo root.Bug Fixes
-map 0:a?to handle audioless sources; auto-pick a libass-capableffmpegwhen burning subs.ffprobeper source, encode segments in parallel (up to 4), and surfaceffmpegstderr on failure.Written for commit f44ac89. Summary will update on new commits.