Skip to content

feat: config profiles, integrations system, and DaVinci Resolve export#1

Merged
nmbrthirteen merged 5 commits into
mainfrom
feat/config-profiles-and-integrations
Jun 2, 2026
Merged

feat: config profiles, integrations system, and DaVinci Resolve export#1
nmbrthirteen merged 5 commits into
mainfrom
feat/config-profiles-and-integrations

Conversation

@nmbrthirteen

@nmbrthirteen nmbrthirteen commented May 22, 2026

Copy link
Copy Markdown
Owner

Summary

Three connected additions plus the paths cleanup they ride on top of:

  • Path centralization — replaces hand-rolled os.path.join(__file__, "..", "..", ".podcli", ...) across the Python backend with a single config.paths module. TypeScript mirrors the same env-var precedence (PODCLI_HOME, PODCLI_DATA) and propagates both vars to the Python subprocess. Also ignores the .podcli-home profile marker so absolute paths can't leak into commits.
  • Config profilespodcli config <export|import|use|status|migrate> packages the config home (knowledge, presets, assets, settings) into a portable zip and restores it elsewhere. Asset paths in registries/presets are rewritten to point at the imported copies. Imports back up the existing home before clobbering. MCP tool manage_config and /api/config/* HTTP endpoints expose the same actions. Idempotent migration moves legacy project/.podcli/cache/ content into data/cache/ on first run. Zip extraction rejects symlinks, absolute paths, and parent-traversal members.
  • Output integrations systemexperimentalbackend/services/integrations/ introduces a registry/manager for editor exporters. DaVinci Resolve is the first integration (opt-in via manage_integrations action=enable name=davinci_resolve): emits an FCPXML 1.10 project with one compound clip per short on the master timeline, V1 source + optional V2 alpha caption overlay, editable in free or Studio Resolve 20.x. Configure via /integrations.html. Real-clip flow tested end-to-end against 19 rendered shorts.
  • ProRes 4444 alpha caption overlayremotion/render.mjs --keep-overlay keeps a transparent caption overlay alongside the baked output instead of deleting it. clip_generator.generate_clip returns caption_overlay_path + cropped_source_path for editor handoff and auto-enables the overlay when the DaVinci integration is on, so enabling the integration is the only toggle users need.
  • CLI: resume cached AI clip suggestions[3/4] Selecting moments... now persists results to <home>/sessions/clips-<hash>.json. Subsequent runs of podcli process on the same video instant-resume instead of re-running Claude/Codex (saves several minutes + tokens per retry on long episodes). --no-resume opts out.

Also fixes a pre-existing paths variable shadow in cmd_process that the new module-level from config.paths import paths import would otherwise unmask (local paths = _thumb_gen(...) in the thumbnail loop turned the dict access into an UnboundLocalError).

Test plan

  • pytest tests/ — 298 passing (incl. new test_config_bundle.py covering round-trip, backup-on-failure, zip-slip rejection, legacy cache + presets migration)
  • npx tsc --noEmit clean
  • End-to-end: 19 real podcli clips → export_to_davinci_resolve → 17 KB FCPXML written → import path documented in backend/services/integrations/davinci_resolve/README.md
  • Manual: import the emitted .fcpxml into free DaVinci Resolve 20.x and confirm the 19 compound clips render identically to the baked outputs
  • Manual: enable davinci_resolve integration → run create_clip → verify _captions.mov ProRes 4444 alpha overlay lands next to the MP4
  • Manual: podcli config export ~/test.zippodcli config import ~/test.zip --home ~/.podcli-alt --activate → verify knowledge/presets/assets paths resolve correctly under the new home

Notes on scope

DaVinci Resolve integration is marked experimental in code (default_enabled = False) and in docs. The FCPXML emitter is intentionally constrained to free-tier Resolve structures — no Studio-only effects, no adjust-blend, no transform keyframes (Resolve mis-translates those). Future editor integrations (Premiere, FCP, CapCut) can subclass IntegrationBase and reuse the _shared FCPXML/IR helpers.

Summary by CodeRabbit

  • New Features

    • Config profiles UI and CLI: export/import/migrate/manage profiles and integrations
    • DaVinci Resolve export (FCPXML) and caption-overlay preservation option
    • Integrations gallery with enable/disable and per-tool execution
  • Documentation

    • Full CONTRIBUTING guide; expanded README; DaVinci Resolve integration docs
  • Bug Fixes

    • Unified transcript cache and improved caching/resume behavior
  • Refactor

    • Centralized path/config resolution (home/data), safe config import/export
  • Tests

    • New tests for config bundle, transcript cache, and render overlay handling

Replace ad-hoc `os.path.join(__file__, "..", "..", ".podcli", ...)` patterns
across the Python backend with a single `config.paths` module that resolves
home/data/cache locations from env vars (PODCLI_HOME, PODCLI_DATA) and a
`.podcli-home` marker file. The TypeScript side mirrors the same precedence
in `src/config/paths.ts`, and `python-executor.ts` propagates both env vars
to the Python subprocess so the two layers always agree.

Also: ignore the `.podcli-home` profile marker to prevent absolute paths
from leaking into commits.
Three independent but related additions, plus the ProRes 4444 alpha caption
overlay that the editor exporter needs.

Config profiles
- `podcli config <export|import|use|status|migrate>` packages a config home
  (knowledge, presets, assets, settings) into a portable zip and restores
  it elsewhere. Asset paths in registries/presets are rewritten to point at
  the imported copies. Imports back up the existing home before clobbering.
- Idempotent legacy-cache migration runs once on startup to move stale
  `project/.podcli/cache/` content into `data/cache/`.
- Equivalent MCP tool `manage_config` and HTTP endpoints under `/api/config`.
- Bundle extraction rejects symlinks, absolute paths, and parent-traversal
  members to prevent zip-slip and out-of-tree writes.

Output integrations
- New `backend/services/integrations/` package with a registry/manager and a
  shared FCPXML/IR helper module for editor exporters.
- DaVinci Resolve integration (experimental, opt-in via `manage_integrations
  action=enable name=davinci_resolve`) emits an FCPXML 1.10 project: one
  compound clip per short on the master timeline, V1 source plus optional V2
  alpha caption overlay so cuts and captions remain editable in free or
  Studio Resolve 20.x.
- MCP tools: `manage_integrations`, `export_to_davinci_resolve`. UI page at
  `/integrations.html` toggles per-integration state.

ProRes alpha caption overlay
- `remotion/render.mjs --keep-overlay` writes a ProRes 4444 alpha overlay
  alongside the baked output instead of deleting it.
- `clip_generator.generate_clip` exposes `keep_caption_overlay` and returns
  `caption_overlay_path` + `cropped_source_path` for editor handoff. It also
  auto-enables the overlay when the davinci_resolve integration is on, so
  enabling the editor integration is the only knob users have to flip.

Also fixes a pre-existing bug exposed by the new `from config.paths import
paths` import in cli.py: a local `paths = _thumb_gen(...)` in the thumbnail
loop shadowed the module-level dict, so `cmd_process` crashed with
UnboundLocalError on any clip that triggered earlier `paths[...]` lookups.
Renamed the local to `thumb_paths`.
After Claude/Codex picks moments in `[3/4] Selecting moments...`, persist the
result to `<home>/sessions/clips-<cache_hash>.json` keyed by the same video
hash used for the transcript cache. A subsequent `podcli process` on the
same video reloads the saved suggestions instantly instead of re-running
the AI step (which can take several minutes on long episodes and bills
tokens each time).

`--no-resume` forces regeneration. Session files are only loaded when
`top_n` matches; mismatches fall through to a fresh AI call.
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nmbrthirteen, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 44 minutes. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 830cf240-ccdb-43fe-aabf-b08900e50997

📥 Commits

Reviewing files that changed from the base of the PR and between 0333512 and 7b14887.

📒 Files selected for processing (13)
  • backend/cli.py
  • backend/config/paths.py
  • backend/config_bundle.py
  • backend/main.py
  • backend/services/encoder.py
  • backend/services/integrations/_shared/media_probe.py
  • backend/services/integrations/base.py
  • backend/utils/prompt_files.py
  • src/config/paths.ts
  • src/handlers/integrations.handler.ts
  • src/server.ts
  • src/ui/public/integrations.html
  • src/ui/web-server.ts
📝 Walkthrough

Walkthrough

This PR centralizes filesystem paths, adds an integrations framework and DaVinci Resolve exporter, implements portable config bundle export/import with safe extraction and legacy migration, adds Remotion caption-overlay export and wiring, updates MCP/Web UI routes and tools, and includes tests and docs.

Changes

Path Resolution and Configuration Infrastructure

Layer / File(s) Summary
Path resolution modules (Python and TypeScript)
backend/config/paths.py, src/config/paths.ts, src/config/paths.test.ts
Introduces centralized path resolution checking PODCLI_HOME, then .podcli-home marker, then project-root default; exports paths with cache, knowledge, assets, integrations, and other directories.
Path usage in backend services
backend/presets.py, backend/services/asset_store.py, backend/services/claude_suggest.py, backend/services/clip_generator.py, backend/services/content_generator.py, backend/services/corrections.py, backend/services/encoder.py, backend/services/thumbnail_*.py, backend/utils/prompt_files.py
Backend modules now import and use config.paths.paths instead of constructing relative .podcli/ paths.
Web server and executor path integration
src/services/python-executor.ts, src/ui/web-server.ts
Python executor sets PODCLI_HOME and PODCLI_DATA; web-server refactors path usages (corrections, knowledge, uploads) to use centralized paths.

Integration Framework and DaVinci Resolve

Layer / File(s) Summary
Integration base and registry
backend/services/integrations/base.py, backend/services/integrations/__init__.py
Adds ToolSpec, IntegrationBase, and IntegrationRegistry for registering and describing integrations and tools.
DaVinci Resolve exporter and shared utilities
backend/services/integrations/_shared/fcpxml.py, backend/services/integrations/_shared/media_probe.py, backend/services/integrations/_shared/timeline_ir.py, backend/services/integrations/davinci_resolve/*
Implements media probing, timeline IR dataclasses, FCPXML emitter, CLI spike, and integration wiring to emit Resolve-compatible FCPXML.
Integrations state manager
backend/services/integrations/manager.py
Persists per-integration enabled state to integrations.json and exposes enable/list APIs.

Caption Overlay Export for DaVinci Resolve

Layer / File(s) Summary
Remotion rendering with overlay support
remotion/render.mjs, remotion/test-render.mjs
Adds --keep-overlay flag, preserves overlay as <output>_captions.mov when requested, and uses PODCLI_CACHE_DIR for bundle cache.
Clip generator changes
backend/services/clip_generator.py
generate_clip() accepts keep_caption_overlay, _render_with_remotion returns (success, overlay_path), and ClipResult can include caption_overlay_path and cropped_source_path.
MCP/Web inputs and models
src/handlers/create-clip.handler.ts, src/handlers/batch-clips.handler.ts, src/models/index.ts
Adds keep_caption_overlay option to create/batch tools and caption_overlay_path to result models.
Overlay tests
tests/test_clip_generator.py
Adds naming contract test and updates remotion-failure tests to expect (False, None) tuple returns.

Config Bundle Export, Import, and Legacy Migration

Layer / File(s) Summary
Config bundle implementation
backend/config_bundle.py
Exports/imports portable home bundles with manifest.json and path_map, safe ZIP extraction rejecting zip-slip/symlinks, backup/restore on import failures, and legacy cache/presets migration with marker writing.
CLI profile commands and interactive UI
backend/cli.py
Adds podcli config subcommands (status/migrate/export/import/use), auto-migration gating, and an interactive _interactive_config() flow.
Backend task handler for config
backend/main.py
Adds handle_manage_config and _maybe_auto_migrate_backend to run migrations before dispatching config tasks.
Config tests
tests/test_config_bundle.py
Comprehensive tests for export/import round-trips, failure safety, legacy migration, zip-slip security, and transcript cache round-trip.

Transcript Cache Migration and Suggestion Resume

Layer / File(s) Summary
Transcript cache canonical layout
backend/services/transcript_packer.py
Moves from legacy per-file MD5 cache to canonical hash-based cache/transcripts/, adds helpers for legacy filename mapping, load/save with migration-on-read, and migration routine.
Suggestion session persistence
backend/cli.py
Adds per-video suggestions-session caching keyed by transcript cache hash and selection signature; --no-resume disables reuse.
Knowledge consolidation
backend/services/claude_suggest.py, backend/services/content_generator.py
Knowledge base resolution centralized to paths["knowledge"].

Web API Routes and Web UI

Layer / File(s) Summary
Config and integrations Express routes
src/handlers/integrations.routes.ts
Adds endpoints for config status/migrate/export/import (multer ZIP import) and integrations list/enable/disable.
MCP handlers and server wiring
src/handlers/integrations.handler.ts, src/server.ts, src/ui/web-server.ts
Registers MCP tools for integration/config management, refactors many Zod schemas and responses, wires config routes into Express, and runs startup legacy migration checks.
Web UI pages
src/ui/public/config.html, src/ui/public/integrations.html, src/ui/public/index.html, src/ui/public/integration.html
Adds Config profiles and Output Integrations pages and navigation links.

Documentation, tests, and misc

Layer / File(s) Summary
Docs and CONTRIBUTING
CONTRIBUTING.md, README.md
Adds contribution guide and updates README with two-halves architecture, project structure, and config profile/migration docs.
Gitignore and logger test
.gitignore, src/utils/logger.test.ts
Ignores .podcli-home marker; formats logger test assertion.
Other tests
tests/*
Adds/updates unit tests for config bundle and clip generator behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through paths both new and old,
Saved overlays in files of gold,
Bundles zipped to travel far,
Integrations shine — a guiding star,
Happy devs, a carrot bar!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/config-profiles-and-integrations

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/services/clip_generator.py (1)

900-913: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist the overlay artifacts outside work_dir.

caption_overlay_path and cropped_source_path point into work_dir, but the finally block deletes that directory whenever output_dir is set. In the normal CLI flow the caller gets paths to files that no longer exist, so the Resolve exporter can't consume them.

💡 Proposed fix
-        out = {
+        persisted_overlay_path = None
+        persisted_cropped_path = None
+        if keep_caption_overlay and caption_overlay_path and os.path.exists(caption_overlay_path):
+            base, _ = os.path.splitext(final_path)
+            persisted_overlay_path = f"{base}_captions.mov"
+            persisted_cropped_path = f"{base}_cropped.mp4"
+            shutil.copy2(caption_overlay_path, persisted_overlay_path)
+            shutil.copy2(cropped_path, persisted_cropped_path)
+
+        out = {
             "output_path": final_path,
             "duration": round(duration, 2),
             "file_size_mb": file_size_mb,
             "title": title,
             "start_second": start_second,
             "end_second": end_second,
             "caption_style": caption_style,
             "crop_strategy": crop_strategy,
         }
-        if keep_caption_overlay and caption_overlay_path and os.path.exists(caption_overlay_path):
-            out["caption_overlay_path"] = caption_overlay_path
-            out["cropped_source_path"] = cropped_path
+        if persisted_overlay_path and persisted_cropped_path:
+            out["caption_overlay_path"] = persisted_overlay_path
+            out["cropped_source_path"] = persisted_cropped_path
         return out

Also applies to: 915-918

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/clip_generator.py` around lines 900 - 913, The caption
overlay and cropped source artifacts (caption_overlay_path and cropped_path) are
currently inside work_dir which is removed in the finally block, so persist them
to a safe location before work_dir is deleted: if output_dir (or final_path's
parent) is set, copy/move caption_overlay_path and cropped_path to a durable
location (e.g., the same directory as final_path or a subfolder under
output_dir), update the out dict keys ("caption_overlay_path" and
"cropped_source_path") to point to the new persisted paths, and only include the
persisted paths in the returned out; ensure this logic is applied where out is
constructed (the block that builds out and the conditional that adds
caption_overlay_path/cropped_source_path) so callers receive valid,
non-temporary file paths.
src/server.ts (1)

771-824: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the resolved batch inputs in the direct fallback.

You compute resolvedClips, resolvedVideoPath, and resolvedTranscriptWords for export_selected / clip_numbers, but the non-Web-UI fallback still calls handleBatchClips(params). If the UI server is down, this path can export the wrong clips or fail with missing inputs even though resolution already succeeded.

Suggested fix
-          finalResult = await handleBatchClips(params);
+          finalResult = await handleBatchClips({
+            ...params,
+            video_path: resolvedVideoPath,
+            clips: resolvedClips,
+            transcript_words: resolvedTranscriptWords,
+          });

Also applies to: 895-900

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server.ts` around lines 771 - 824, The resolved batch inputs
(resolvedClips, resolvedVideoPath, resolvedTranscriptWords) computed for
export_selected/clip_numbers are not used in the non‑UI fallback; instead
handleBatchClips(params) is called which can use stale/missing params. Update
the fallback(s) that call handleBatchClips (and the similar block around the
895-900 region) to call handleBatchClips with the resolved values or to replace
params' fields before calling it — i.e., pass an object where clips =
resolvedClips, video_path = resolvedVideoPath, and transcript_words =
resolvedTranscriptWords (falling back to original params only if those resolved
variables are still undefined) so the exported batch uses the resolved inputs.
src/ui/web-server.ts (1)

1737-1747: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

/api/mcp/export is stripping clip fields the new export flow depends on.

This mapper only preserves a small subset of clip properties and only translates segments -> keep_segments. That drops already-normalized keep_segments from src/server.ts and also drops keep_caption_overlay, so MCP-driven multi-cut exports collapse to single ranges and overlay exports silently lose their sidecar assets.

Suggested fix
   const styledClips = clips.map((c: any) => ({
+    ...c,
     start_second: c.start_second,
     end_second: c.end_second,
     title: (c.title || "clip").slice(0, 40),
     caption_style: c.caption_style || captionStyle,
     crop_strategy: c.crop_strategy || cropStrategy,
     allow_ass_fallback: c.allow_ass_fallback === true || allowAssFallback,
-    // Preserve multi-cut segments from suggestions
-    ...(Array.isArray(c.segments) &&
-      c.segments.length > 0 && { keep_segments: c.segments }),
+    ...(Array.isArray(c.keep_segments) &&
+      c.keep_segments.length > 0 && { keep_segments: c.keep_segments }),
+    ...(Array.isArray(c.segments) &&
+      c.segments.length > 0 && { keep_segments: c.segments }),
+    keep_caption_overlay: c.keep_caption_overlay === true,
   }));
@@
       {
         video_path: videoPath,
         clips: styledClips,
         transcript_words: transcriptWords,
         output_dir: paths.output,
         logo_path: logoPath,
         outro_path: outroPath,
+        keep_caption_overlay: req.body.keep_caption_overlay === true,
         face_map: uiState.transcript?.face_map,
       },

Also applies to: 1771-1778

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ui/web-server.ts` around lines 1737 - 1747, The mapper in styledClips is
dropping clip properties the export flow needs (it converts segments ->
keep_segments but overwrites or omits already-normalized keep_segments and omits
keep_caption_overlay and other passthrough fields), so update the clips.map
transformation (styledClips and the similar block around the other occurrence)
to: 1) preserve an existing c.keep_segments if present and only derive
keep_segments from c.segments when c.keep_segments is absent, 2) include
c.keep_caption_overlay (or default) in the output, and 3) pass-through any other
important sidecar/overlay fields rather than stripping them—use the existing c.*
values (e.g. keep_segments, keep_caption_overlay, allow_ass_fallback) when
available instead of always replacing them.
🧹 Nitpick comments (6)
backend/services/integrations/davinci_resolve/__init__.py (1)

8-8: 💤 Low value

Consider alphabetically sorting __all__.

Ruff suggests sorting __all__ entries alphabetically. While the current order matches import order, alphabetical sorting improves consistency and maintainability.

♻️ Proposed fix
-__all__ = ["integration", "emitter"]
+__all__ = ["emitter", "integration"]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/__init__.py` at line 8, The
__all__ export list is unsorted; update the __all__ variable in the module to
list its names in alphabetical order (e.g., put "emitter" before "integration")
so the __all__ = [...] tuple/ list is alphabetically ordered for consistency and
to satisfy linters like Ruff.
backend/services/integrations/_shared/media_probe.py (1)

13-50: ⚡ Quick win

Consider combining the two ffprobe calls.

Running ffprobe twice (once for video, once for audio) is less efficient than running once with both stream selections. While this works correctly, combining them could reduce overhead, especially when probing many files.

♻️ Example combined approach
cmd = [
    "ffprobe", "-v", "error",
    "-select_streams", "v:0,a:0",
    "-show_entries", "stream=width,height,r_frame_rate,nb_frames,duration,channels:format=duration",
    "-of", "json",
    p,
]
data = json.loads(subprocess.check_output(cmd, text=True, timeout=30))
streams = data.get("streams", [])
v_stream = next((s for s in streams if s.get("width")), {})
a_stream = next((s for s in streams if s.get("channels")), {})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/_shared/media_probe.py` around lines 13 - 50,
The two separate ffprobe invocations in media_probe.py should be merged into a
single ffprobe call: call ffprobe once with "-select_streams", "v:0,a:0" and
"-show_entries",
"stream=width,height,r_frame_rate,nb_frames,duration,channels:format=duration",
parse the single JSON result (replace v_data/a_data usage with a single data
variable), then locate v_stream as the stream with width/height/r_frame_rate and
a_stream as the stream with channels (e.g., using next over data.get("streams",
[])); keep the existing logic that computes width, height, fps, fmt_duration and
duration_frames but read those values from v_stream/a_stream in the consolidated
data and include a timeout on subprocess.check_output as appropriate.
backend/services/integrations/davinci_resolve/cli.py (2)

70-72: 💤 Low value

Default output directory may not exist.

The default output path assumes data/export/davinci_resolve/ exists. If it doesn't, emitter.emit() will fail when writing the file. Consider ensuring the directory exists or documenting that users should create it.

🛡️ Proposed fix
     out = args.out or (
         REPO_ROOT / "data" / "export" / "davinci_resolve" / f"{args.title.replace(' ', '_')}.fcpxml"
     )
+    out.parent.mkdir(parents=True, exist_ok=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/cli.py` around lines 70 - 72,
The default output path assigned to out (out = args.out or (REPO_ROOT / "data" /
"export" / "davinci_resolve" / f"{args.title.replace(' ', '_')}.fcpxml"))
assumes the parent directory exists and will cause emitter.emit() to fail;
before calling emitter.emit() ensure the output file's parent directory exists
by creating out.parent with parents=True and exist_ok=True (or document that
users must create data/export/davinci_resolve), referencing the variables out,
REPO_ROOT, args.title, and the emitter.emit call so the directory creation
happens immediately prior to writing the file.

16-18: ⚡ Quick win

Fragile path calculation using fixed parent depth.

The REPO_ROOT calculation assumes this file is exactly 4 directories deep. If the file is moved or the structure changes, this will break silently.

♻️ More robust alternative
-REPO_ROOT = Path(__file__).resolve().parents[4]
+# Find repo root by looking for a marker file
+def _find_repo_root():
+    p = Path(__file__).resolve()
+    for parent in [p] + list(p.parents):
+        if (parent / "backend").is_dir() and (parent / "package.json").is_file():
+            return parent
+    raise RuntimeError("Could not locate repository root")
+
+REPO_ROOT = _find_repo_root()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/cli.py` around lines 16 - 18,
The REPO_ROOT calculation is fragile because parents[4] assumes fixed depth;
change it to walk up the directory tree from Path(__file__).resolve() until you
find a repository marker (e.g., a ".git" directory or the "backend" folder) and
set REPO_ROOT to that parent instead of using parents[4]; then use that computed
REPO_ROOT when checking/inserting into sys.path (symbols to update: REPO_ROOT,
Path(__file__).resolve(), and the sys.path.insert call) so the import path logic
continues to work if files are moved.
backend/services/integrations/davinci_resolve/emitter.py (2)

30-31: 💤 Low value

Document the rationale for ID starting values.

The asset IDs start at 2 and media IDs at 1000 without explanation. A brief comment would help future maintainers understand whether these specific values are significant or arbitrary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/emitter.py` around lines 30 -
31, Add a short explanatory comment above the next_asset and next_media
initializations in emitter.py that documents why asset IDs start at 2 and media
IDs at 1000 (e.g., reserved ID 1 for root/placeholder, gap for legacy IDs, or
arbitrary chosen to avoid collision), or mark them as intentionally arbitrary if
no special meaning; reference the variables next_asset and next_media and update
their nearby comment or module docstring to capture this rationale for future
maintainers.

23-33: ⚡ Quick win

Consider validating that project.shorts is non-empty.

The function proceeds directly to building the library even if project.shorts is empty, which would produce valid FCPXML with no clips. Adding an early check would provide clearer feedback.

🛡️ Proposed validation
 def emit(project: Project, out_path: Path) -> Path:
+    if not project.shorts:
+        raise ValueError("Project must contain at least one short")
     fmt_id = "r1"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/davinci_resolve/emitter.py` around lines 23 -
33, The emit function should validate that project.shorts is non-empty before
building the FCPXML; add an early check at the top of emit (before building
fmt_id/resources/compounds) that tests if not project.shorts and then raise a
clear exception (e.g., ValueError with a descriptive message) or return an
explicit error so callers know there are no clips to emit; update any callers or
tests that expect this behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/cli.py`:
- Around line 405-410: The session key currently only uses
compute_cache_hash(video_path) so passing a custom transcript (args.transcript)
can reuse stale suggestions; update the resume logic in the code that sets
cache_hash (uses services.transcript_packer.compute_cache_hash) to either (a)
disable resume when args.transcript is present by leaving cache_hash empty/None,
or (b) include the transcript and relevant config in the session key (e.g.,
compute a combined hash of video_path plus the transcript file contents/mtime
and any config flags) so functions that rely on cache_hash will never reuse
suggestions from a different transcript; apply the same change to the other
resume spots noted (the blocks around where cache_hash is computed at the other
occurrences).

In `@backend/config_bundle.py`:
- Around line 496-497: Activating a new home writes the marker but never
refreshes the module-level in-memory paths cache, so functions like
get_active_home() and later home-relative operations still return the old home;
after writing the marker in set_active_home() and any code path that calls
_marker_path().write_text(...) (including import_config(..., activate=True)),
invalidate or reload the cached paths (e.g., update the module-level paths
variable or call the existing path-loading routine) so
config.paths/get_active_home() reflect the new marker immediately in-process;
alternatively remove the import-time caching and make get_active_home() read the
marker dynamically. Ensure you update all activation sites referenced (the
functions that call _marker_path().write_text) to perform this refresh.

In `@backend/main.py`:
- Around line 476-483: The auto-migration call in _maybe_auto_migrate_backend
currently runs inline and will propagate exceptions to request handling; wrap
the import and call to auto_migrate_legacy_if_pending(quiet=True) in a
try/except that catches Exception, logs the exception (contextual message
referencing auto-migrate) and continues without re-raising so migration failures
are best-effort and non-blocking; apply the same try/except pattern to the other
identical call referenced at the other location (the occurrence around line 498)
and keep the existing early-return logic for "manage_config" tasks unchanged.
- Around line 453-454: The current except block returns the full traceback in
the API response; instead, log the full traceback on the server and emit a
sanitized error string to the client. In the except block for emit_result use
logging.exception(...) or a server logger to record traceback.format_exc() (or
call logger.exception(e)) and then call emit_result(task_id, "error",
error=f"{type(e).__name__}: {e}") or a generic message like "Internal server
error" so no stack/paths are sent to clients; update the block around
emit_result and the exception handler to perform server-side logging and a
sanitized emit_result call.

In `@backend/services/encoder.py`:
- Line 173: The docstring comment still hardcodes "data/cache/encoder.json" but
the cache path is now resolved dynamically via paths["cache"]; update the
docstring in backend/services/encoder.py (the comment at the Cached at ... line)
to reference the dynamic cache location (e.g. paths["cache"] joined with the
encoder filename) and note it is keyed by the ffmpeg binary fingerprint so the
documentation matches the actual behavior in this module.

In `@backend/services/integrations/_shared/media_probe.py`:
- Around line 13-21: The subprocess.check_output calls that run ffprobe
(building v_cmd and invoking subprocess.check_output to produce v_data) need a
timeout to avoid hanging; update the call to include a timeout parameter (e.g.,
timeout=30) and handle subprocess.TimeoutExpired if desired, and apply the same
change to the audio probe call that builds a_cmd / produces a_data so both
ffprobe invocations use a timeout.

In `@backend/services/integrations/base.py`:
- Around line 46-47: The register method on IntegrationBase currently overwrites
any existing entry in cls._instances; change register (def register(cls,
integration: IntegrationBase)) to first check if integration.name already exists
in cls._instances and if so raise a clear exception (e.g., ValueError or
RuntimeError) preventing silent overwrite; otherwise proceed to assign
cls._instances[integration.name] = integration so duplicate registrations fail
fast and surface the conflict.

In `@backend/services/integrations/davinci_resolve/integration.py`:
- Around line 64-66: The code currently allows an empty params["shorts"] to
proceed and emit a project with no clips; before calling probe_media and
building shorts (the block that starts with shorts: list[Short] = [] and the
later similar block around the probe_media loop), validate that params["shorts"]
is present and not empty (or that the resulting shorts list after filtering is
non-empty) and raise a clear validation error (e.g., ValueError or a
ValidationError) instead of continuing; apply this check both where
params["shorts"] is consumed and in the second similar loop to fail fast and
return an error response rather than success metadata.

In `@backend/utils/prompt_files.py`:
- Line 25: The module docstring and any function comments referring to
“.podcli/tmp/” are out of date—update them to reflect that temporary files are
now written to os.path.join(paths["home"], "tmp") (the variable base in this
file), so change wording to indicate the temp directory is under the configured
paths["home"]/tmp location (and adjust any troubleshooting/manual cleanup
instructions accordingly).

In `@src/config/paths.ts`:
- Around line 11-22: The resolveHome() function currently calls
readFileSync(homeMarker) without error handling which can crash the app; wrap
the readFileSync(homeMarker, "utf-8") call in a try-catch inside the
existsSync(homeMarker) block (keeping the isAbsolute(marker) check) and on any
exception treat it as a missing/invalid marker by falling back to return
resolve(projectRoot, ".podcli"); reference resolveHome(), homeMarker,
readFileSync, existsSync, and projectRoot when making the change.

In `@src/handlers/integrations.handler.ts`:
- Around line 127-133: The handler currently calls handleManageIntegrations({
action, name }) even when action is "enable" or "disable" and name is empty; add
an upfront guard in the async handler (the anonymous async ({ action, name }) =>
{ ... } block) that checks if action === "enable" || action === "disable" and if
name is falsy, then return a clear MCP error via mcpText (e.g., a descriptive
message like "Missing required 'name' for enable/disable action") instead of
calling handleManageIntegrations; this will fail fast and avoid sending an empty
name to Python.

In `@src/handlers/integrations.routes.ts`:
- Around line 44-57: The export route that calls
executor.execute("manage_config", ...) and then res.download(file, ...) must
delete the temporary bundle file after the response completes—use the
res.download callback or res.on('finish'/'close') to unlink the bundlePath/file
so the ZIPs don't accumulate; likewise, for the import route that reads
req.file.path, ensure you remove the uploaded file in a finally block after
processing (regardless of success/failure) to guarantee cleanup; reference the
export handler around executor.execute and res.download and the import handler
using req.file.path when adding the unlink logic.

In `@src/server.ts`:
- Around line 509-515: The export path in src/server.ts is not forwarding the
keep_caption_overlay flag to the /api/mcp/export endpoint, so Web-UI exports
ignore overlay-enabled renders; update the code that constructs the export
request payload for the Web-UI export path so it includes the
keep_caption_overlay property (preserve its boolean value/default), propagate it
through any intermediate structures or typed interfaces used when calling
/api/mcp/export, and ensure the server handler reading /api/mcp/export consumes
that field; specifically add keep_caption_overlay to the JSON/body sent to
/api/mcp/export wherever export requests are assembled.

In `@src/ui/public/integrations.html`:
- Around line 169-170: The current integration toggle is a non-focusable div
("toggle" element with role="switch" and data-name="${escapeHtml(int.name)}")
and must be replaced or augmented with a keyboard-accessible control: change the
element to a semantic, focusable control (e.g., a <button> or an <input
type="checkbox">) that maintains role="switch" or native semantics, ensure
aria-checked reflects int.enabled, include keyboard event handling (Space/Enter)
to toggle the state, keep the data-name attribute for identification, and ensure
visual classes ("on") are applied when toggled; update any click-only listeners
attached to the old ".toggle" to handle keyboard activation as well.

In `@src/ui/web-server.ts`:
- Around line 1643-1645: The field transcriptWordCount is returning the words
array itself due to misplaced parentheses around (uiState.transcript?.words ??
[].length); change it to return a numeric length instead by using
uiState.transcript?.words.length (or (uiState.transcript?.words ?? []).length)
so transcriptWordCount is always a number; update the expression that sets
transcriptWordCount in web-server.ts to reference
uiState.transcript?.words.length and fall back to 0 when missing.
- Around line 909-919: The current isUploadedFile check uses startsWith on
path.resolve(join(paths.working, "uploads")), which mistakenly matches sibling
directories like "uploads-backup"; change the guard in the web-server logic that
computes resolvedPath and isUploadedFile so you compare against the uploads
directory with a directory boundary (e.g., compute uploadsDir =
path.resolve(join(paths.working, "uploads")) and ensure resolvedPath ===
uploadsDir or resolvedPath.startsWith(uploadsDir + path.sep)) or use
path.relative to verify the file is inside uploads (relative does not begin with
'..'); update the isUploadedFile boolean accordingly where resolvedPath,
isSessionVideo, and paths.working are used.

---

Outside diff comments:
In `@backend/services/clip_generator.py`:
- Around line 900-913: The caption overlay and cropped source artifacts
(caption_overlay_path and cropped_path) are currently inside work_dir which is
removed in the finally block, so persist them to a safe location before work_dir
is deleted: if output_dir (or final_path's parent) is set, copy/move
caption_overlay_path and cropped_path to a durable location (e.g., the same
directory as final_path or a subfolder under output_dir), update the out dict
keys ("caption_overlay_path" and "cropped_source_path") to point to the new
persisted paths, and only include the persisted paths in the returned out;
ensure this logic is applied where out is constructed (the block that builds out
and the conditional that adds caption_overlay_path/cropped_source_path) so
callers receive valid, non-temporary file paths.

In `@src/server.ts`:
- Around line 771-824: The resolved batch inputs (resolvedClips,
resolvedVideoPath, resolvedTranscriptWords) computed for
export_selected/clip_numbers are not used in the non‑UI fallback; instead
handleBatchClips(params) is called which can use stale/missing params. Update
the fallback(s) that call handleBatchClips (and the similar block around the
895-900 region) to call handleBatchClips with the resolved values or to replace
params' fields before calling it — i.e., pass an object where clips =
resolvedClips, video_path = resolvedVideoPath, and transcript_words =
resolvedTranscriptWords (falling back to original params only if those resolved
variables are still undefined) so the exported batch uses the resolved inputs.

In `@src/ui/web-server.ts`:
- Around line 1737-1747: The mapper in styledClips is dropping clip properties
the export flow needs (it converts segments -> keep_segments but overwrites or
omits already-normalized keep_segments and omits keep_caption_overlay and other
passthrough fields), so update the clips.map transformation (styledClips and the
similar block around the other occurrence) to: 1) preserve an existing
c.keep_segments if present and only derive keep_segments from c.segments when
c.keep_segments is absent, 2) include c.keep_caption_overlay (or default) in the
output, and 3) pass-through any other important sidecar/overlay fields rather
than stripping them—use the existing c.* values (e.g. keep_segments,
keep_caption_overlay, allow_ass_fallback) when available instead of always
replacing them.

---

Nitpick comments:
In `@backend/services/integrations/_shared/media_probe.py`:
- Around line 13-50: The two separate ffprobe invocations in media_probe.py
should be merged into a single ffprobe call: call ffprobe once with
"-select_streams", "v:0,a:0" and "-show_entries",
"stream=width,height,r_frame_rate,nb_frames,duration,channels:format=duration",
parse the single JSON result (replace v_data/a_data usage with a single data
variable), then locate v_stream as the stream with width/height/r_frame_rate and
a_stream as the stream with channels (e.g., using next over data.get("streams",
[])); keep the existing logic that computes width, height, fps, fmt_duration and
duration_frames but read those values from v_stream/a_stream in the consolidated
data and include a timeout on subprocess.check_output as appropriate.

In `@backend/services/integrations/davinci_resolve/__init__.py`:
- Line 8: The __all__ export list is unsorted; update the __all__ variable in
the module to list its names in alphabetical order (e.g., put "emitter" before
"integration") so the __all__ = [...] tuple/ list is alphabetically ordered for
consistency and to satisfy linters like Ruff.

In `@backend/services/integrations/davinci_resolve/cli.py`:
- Around line 70-72: The default output path assigned to out (out = args.out or
(REPO_ROOT / "data" / "export" / "davinci_resolve" / f"{args.title.replace(' ',
'_')}.fcpxml")) assumes the parent directory exists and will cause
emitter.emit() to fail; before calling emitter.emit() ensure the output file's
parent directory exists by creating out.parent with parents=True and
exist_ok=True (or document that users must create data/export/davinci_resolve),
referencing the variables out, REPO_ROOT, args.title, and the emitter.emit call
so the directory creation happens immediately prior to writing the file.
- Around line 16-18: The REPO_ROOT calculation is fragile because parents[4]
assumes fixed depth; change it to walk up the directory tree from
Path(__file__).resolve() until you find a repository marker (e.g., a ".git"
directory or the "backend" folder) and set REPO_ROOT to that parent instead of
using parents[4]; then use that computed REPO_ROOT when checking/inserting into
sys.path (symbols to update: REPO_ROOT, Path(__file__).resolve(), and the
sys.path.insert call) so the import path logic continues to work if files are
moved.

In `@backend/services/integrations/davinci_resolve/emitter.py`:
- Around line 30-31: Add a short explanatory comment above the next_asset and
next_media initializations in emitter.py that documents why asset IDs start at 2
and media IDs at 1000 (e.g., reserved ID 1 for root/placeholder, gap for legacy
IDs, or arbitrary chosen to avoid collision), or mark them as intentionally
arbitrary if no special meaning; reference the variables next_asset and
next_media and update their nearby comment or module docstring to capture this
rationale for future maintainers.
- Around line 23-33: The emit function should validate that project.shorts is
non-empty before building the FCPXML; add an early check at the top of emit
(before building fmt_id/resources/compounds) that tests if not project.shorts
and then raise a clear exception (e.g., ValueError with a descriptive message)
or return an explicit error so callers know there are no clips to emit; update
any callers or tests that expect this behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f56e140a-d39d-4b4f-a887-30b5a0c3746f

📥 Commits

Reviewing files that changed from the base of the PR and between 61e1be1 and d4a5162.

📒 Files selected for processing (50)
  • .gitignore
  • CONTRIBUTING.md
  • README.md
  • backend/cli.py
  • backend/config/paths.py
  • backend/config_bundle.py
  • backend/main.py
  • backend/presets.py
  • backend/services/asset_store.py
  • backend/services/claude_suggest.py
  • backend/services/clip_generator.py
  • backend/services/content_generator.py
  • backend/services/corrections.py
  • backend/services/encoder.py
  • backend/services/integrations/__init__.py
  • backend/services/integrations/_shared/__init__.py
  • backend/services/integrations/_shared/fcpxml.py
  • backend/services/integrations/_shared/media_probe.py
  • backend/services/integrations/_shared/timeline_ir.py
  • backend/services/integrations/base.py
  • backend/services/integrations/davinci_resolve/README.md
  • backend/services/integrations/davinci_resolve/__init__.py
  • backend/services/integrations/davinci_resolve/cli.py
  • backend/services/integrations/davinci_resolve/emitter.py
  • backend/services/integrations/davinci_resolve/integration.py
  • backend/services/integrations/manager.py
  • backend/services/thumbnail_ai.py
  • backend/services/thumbnail_generator.py
  • backend/services/thumbnail_html.py
  • backend/services/transcript_packer.py
  • backend/utils/prompt_files.py
  • remotion/render.mjs
  • remotion/test-render.mjs
  • src/config/paths.test.ts
  • src/config/paths.ts
  • src/handlers/batch-clips.handler.ts
  • src/handlers/create-clip.handler.ts
  • src/handlers/integrations.handler.ts
  • src/handlers/integrations.routes.ts
  • src/models/index.ts
  • src/server.ts
  • src/services/python-executor.ts
  • src/ui/public/config.html
  • src/ui/public/index.html
  • src/ui/public/integration.html
  • src/ui/public/integrations.html
  • src/ui/web-server.ts
  • src/utils/logger.test.ts
  • tests/test_clip_generator.py
  • tests/test_config_bundle.py

Comment thread backend/cli.py Outdated
Comment thread backend/config_bundle.py
Comment thread backend/main.py Outdated
Comment thread backend/main.py Outdated
Comment thread backend/services/encoder.py Outdated
Comment thread src/handlers/integrations.routes.ts Outdated
Comment thread src/server.ts
Comment thread src/ui/public/integrations.html Outdated
Comment thread src/ui/web-server.ts
Comment thread src/ui/web-server.ts
- cli: key resume sessions on selection signature (ai_select, duration
  bounds); make --no-cache bypass resume; clear stale session on regenerate
- fcpxml: emit canonical NTSC frame durations and derive DF/NDF timecode
  instead of limit_denominator(1000) and hardcoded NDF
- media_probe: wrap ffprobe with actionable errors; prefer duration*fps
  over unreliable nb_frames
- config_bundle: drop imported manifest.json (leaks exporter abs paths),
  clean it on rollback; validate zip directory entries; fix asset_count
- davinci: reject empty shorts export
- routes: delete temp export/import zips

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/config_bundle.py (1)

509-515: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't return the raw imported manifest to callers.

manifest still contains source_home and the absolute-path path_map from the exporting machine. Returning it here leaks the same host-specific data you just removed from disk, and this result is surfaced through the config action entrypoints.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/config_bundle.py` around lines 509 - 515, The returned dict exposes
host-specific data because the raw manifest (variable manifest) still contains
source_home and absolute paths in path_map; sanitize before returning by
creating a deep copy of manifest and removing or redacting source_home and
converting or removing absolute entries in path_map (or replace them with
relative keys/URIs) so callers do not receive host-specific paths; then return
the cleaned copy in the "manifest" key instead of the original manifest.
backend/services/integrations/_shared/fcpxml.py (1)

47-48: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use Path.as_uri() for file:// URIs in file_uri()

The current file_uri() manually builds URIs and produces incorrect Windows-style output (file://C%3A%5C...) instead of a valid local file URI. Use p.resolve().as_uri() (it formats correctly on both POSIX and Windows).

def file_uri(p: Path) -> str:
    return p.resolve().as_uri()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/_shared/fcpxml.py` around lines 47 - 48, The
file_uri function currently builds a file URI by concatenating "file://" with
urllib.parse.quote of p.resolve(), which produces incorrect Windows-style URIs;
update the implementation of file_uri to return a proper file URI using
Path.as_uri on the resolved path (i.e., call p.resolve().as_uri()) so it formats
correctly on both POSIX and Windows and remove the manual urllib.parse.quote
usage.
♻️ Duplicate comments (2)
backend/services/integrations/_shared/media_probe.py (1)

10-18: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bound the shared ffprobe call with a timeout.

_run_ffprobe() still uses an unbounded subprocess.check_output, so one hung ffprobe process can stall the entire export path indefinitely. Add a timeout here and translate TimeoutExpired into the same RuntimeError surface.

Proposed fix
 def _run_ffprobe(cmd: list[str], path: str) -> dict[str, Any]:
     try:
-        return json.loads(subprocess.check_output(cmd, text=True, stderr=subprocess.PIPE))
-    except FileNotFoundError:
-        raise RuntimeError("ffprobe not found — install ffmpeg to use editor-export integrations")
+        return json.loads(subprocess.check_output(cmd, text=True, stderr=subprocess.PIPE, timeout=30))
+    except FileNotFoundError as e:
+        raise RuntimeError("ffprobe not found — install ffmpeg to use editor-export integrations") from e
+    except subprocess.TimeoutExpired as e:
+        raise RuntimeError(f"ffprobe timed out for {path}") from e
     except subprocess.CalledProcessError as e:
-        raise RuntimeError(f"ffprobe failed for {path}: {(e.stderr or '').strip() or e}")
-    except json.JSONDecodeError:
-        raise RuntimeError(f"ffprobe returned unexpected output for {path}")
+        raise RuntimeError(f"ffprobe failed for {path}: {(e.stderr or '').strip() or e}") from e
+    except json.JSONDecodeError as e:
+        raise RuntimeError(f"ffprobe returned unexpected output for {path}") from e
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/integrations/_shared/media_probe.py` around lines 10 - 18,
The _run_ffprobe function currently calls subprocess.check_output without a
timeout and can hang; update the call in _run_ffprobe(cmd: list[str], path: str)
to pass a reasonable timeout (e.g., 10s) and add an except
subprocess.TimeoutExpired block that raises a RuntimeError with a message
consistent with the existing errors (include the path and useful details), so
timeouts are surfaced the same way as CalledProcessError and FileNotFoundError.
backend/config_bundle.py (1)

502-503: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh the in-memory path map after switching homes.

Both activation paths only rewrite the marker file. In the same Python process, get_active_home() and config status still read the old paths["home"] until restart, so the new home is not actually active for subsequent operations.

Also applies to: 518-526, 536-543

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/config_bundle.py` around lines 502 - 503, After writing the marker
file with _marker_path().write_text(...), also update the in-memory paths map so
subsequent calls to get_active_home() and "config status" see the new home;
specifically set paths["home"] = str(target) (or the same normalized value
written to the marker) and, if a reload function exists (e.g. _load_paths() /
reload_paths()), call it to refresh any cached state; apply the same change to
the other activation sites around the regions mentioned (the blocks around lines
518-526 and 536-543) so the process-wide state matches the marker file
immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@backend/config_bundle.py`:
- Around line 509-515: The returned dict exposes host-specific data because the
raw manifest (variable manifest) still contains source_home and absolute paths
in path_map; sanitize before returning by creating a deep copy of manifest and
removing or redacting source_home and converting or removing absolute entries in
path_map (or replace them with relative keys/URIs) so callers do not receive
host-specific paths; then return the cleaned copy in the "manifest" key instead
of the original manifest.

In `@backend/services/integrations/_shared/fcpxml.py`:
- Around line 47-48: The file_uri function currently builds a file URI by
concatenating "file://" with urllib.parse.quote of p.resolve(), which produces
incorrect Windows-style URIs; update the implementation of file_uri to return a
proper file URI using Path.as_uri on the resolved path (i.e., call
p.resolve().as_uri()) so it formats correctly on both POSIX and Windows and
remove the manual urllib.parse.quote usage.

---

Duplicate comments:
In `@backend/config_bundle.py`:
- Around line 502-503: After writing the marker file with
_marker_path().write_text(...), also update the in-memory paths map so
subsequent calls to get_active_home() and "config status" see the new home;
specifically set paths["home"] = str(target) (or the same normalized value
written to the marker) and, if a reload function exists (e.g. _load_paths() /
reload_paths()), call it to refresh any cached state; apply the same change to
the other activation sites around the regions mentioned (the blocks around lines
518-526 and 536-543) so the process-wide state matches the marker file
immediately.

In `@backend/services/integrations/_shared/media_probe.py`:
- Around line 10-18: The _run_ffprobe function currently calls
subprocess.check_output without a timeout and can hang; update the call in
_run_ffprobe(cmd: list[str], path: str) to pass a reasonable timeout (e.g., 10s)
and add an except subprocess.TimeoutExpired block that raises a RuntimeError
with a message consistent with the existing errors (include the path and useful
details), so timeouts are surfaced the same way as CalledProcessError and
FileNotFoundError.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8466ba07-4daa-4a12-8a01-842a842d0ba7

📥 Commits

Reviewing files that changed from the base of the PR and between d4a5162 and 0333512.

📒 Files selected for processing (7)
  • backend/cli.py
  • backend/config_bundle.py
  • backend/services/integrations/_shared/fcpxml.py
  • backend/services/integrations/_shared/media_probe.py
  • backend/services/integrations/davinci_resolve/emitter.py
  • backend/services/integrations/davinci_resolve/integration.py
  • src/handlers/integrations.routes.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • backend/services/integrations/davinci_resolve/integration.py
  • backend/services/integrations/davinci_resolve/emitter.py
  • src/handlers/integrations.routes.ts
  • backend/cli.py

- cli: disable suggestion resume when a custom --transcript is supplied
- config/paths: add reload_paths(); refresh in-memory paths on home activate
  (set_active_home + import_config activate) so config status reflects it live
- main: log traceback server-side, return sanitized API error; isolate
  auto-migration failures from task dispatch
- integrations/base: fail fast on duplicate integration name registration
- media_probe: add 30s ffprobe timeout
- paths.ts: guard resolveHome() marker read; fall back to default on error
- integrations.handler: require name for enable/disable
- server: forward keep_caption_overlay through the Web-UI export path
- web-server: tighten uploads boundary check (path.relative, not startsWith);
  return transcript word count instead of the words array
- integrations.html: keyboard-accessible <button> toggle with focus ring
- docs: correct encoder/prompt_files cache + tmp path wording
@nmbrthirteen nmbrthirteen merged commit 5954aba into main Jun 2, 2026
5 checks passed
@nmbrthirteen nmbrthirteen deleted the feat/config-profiles-and-integrations branch June 2, 2026 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant