@hybridlabor-api/aos 4.4.0 → 4.4.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/.agents/skills/firecrawl/SKILL.md +149 -0
  2. package/.agents/skills/firecrawl/rules/install.md +82 -0
  3. package/.agents/skills/firecrawl/rules/security.md +26 -0
  4. package/.agents/skills/firecrawl-agent/SKILL.md +58 -0
  5. package/.agents/skills/firecrawl-build/SKILL.md +39 -0
  6. package/.agents/skills/firecrawl-build-interact/SKILL.md +68 -0
  7. package/.agents/skills/firecrawl-build-onboarding/SKILL.md +103 -0
  8. package/.agents/skills/firecrawl-build-onboarding/references/auth-flow.md +39 -0
  9. package/.agents/skills/firecrawl-build-onboarding/references/project-setup.md +20 -0
  10. package/.agents/skills/firecrawl-build-onboarding/references/sdk-installation.md +17 -0
  11. package/.agents/skills/firecrawl-build-scrape/SKILL.md +69 -0
  12. package/.agents/skills/firecrawl-build-search/SKILL.md +69 -0
  13. package/.agents/skills/firecrawl-crawl/SKILL.md +59 -0
  14. package/.agents/skills/firecrawl-download/SKILL.md +70 -0
  15. package/.agents/skills/firecrawl-interact/SKILL.md +84 -0
  16. package/.agents/skills/firecrawl-map/SKILL.md +51 -0
  17. package/.agents/skills/firecrawl-scrape/SKILL.md +69 -0
  18. package/.agents/skills/firecrawl-search/SKILL.md +60 -0
  19. package/.claude/hooks/memb-inject.mjs +6 -0
  20. package/CLAUDE.md +573 -0
  21. package/README.de.md +0 -8
  22. package/README.md +0 -8
  23. package/README.pt.md +0 -8
  24. package/assets/brand/ao-ant-mark.svg +91 -0
  25. package/assets/brand/bdb-core-mark.svg +34 -0
  26. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  27. package/assets/brand/memb-crystal-mark.svg +18 -0
  28. package/bin/aos-config.mjs +89 -0
  29. package/bin/aos-dashboard.mjs +608 -0
  30. package/bin/aos-uninstall.mjs +227 -0
  31. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  32. package/docs/skills_table.md +0 -3
  33. package/installer.js +428 -237
  34. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  35. package/mcps/after-effects-mcp/build/index.js +840 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  37. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  38. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  39. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  40. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  41. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  42. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  43. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  44. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  45. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  46. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  47. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  48. package/mcps/computer-use-mcp/dist/client.js +136 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  50. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  51. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  52. package/mcps/computer-use-mcp/dist/native.js +50 -0
  53. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  54. package/mcps/computer-use-mcp/dist/server.js +342 -0
  55. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  56. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  57. package/package.json +13 -4
  58. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  59. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  60. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  61. package/skills/global_config/aos-setup/SKILL.md +110 -11
  62. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +48 -4
  63. package/skills/global_config/ask-tim/SKILL.md +2 -7
  64. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  65. package/bin/setup-saas.mjs +0 -792
  66. package/mcps/blender-mcp-server/.dockerignore +0 -19
  67. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  68. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  69. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  70. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  71. package/mcps/blender-mcp-server/Dockerfile +0 -19
  72. package/mcps/blender-mcp-server/LICENSE +0 -21
  73. package/mcps/blender-mcp-server/README.md +0 -409
  74. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  75. package/mcps/blender-mcp-server/addon/models.py +0 -223
  76. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  77. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  78. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  79. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  80. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  81. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  82. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  83. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  85. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  86. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  87. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  88. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  89. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  90. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  91. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  92. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  93. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  94. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  95. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  96. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  97. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  98. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  99. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  100. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  101. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  104. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  105. package/mcps/davinci-mcp-professional/.bandit +0 -35
  106. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  107. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  108. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  109. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  110. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  112. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  113. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  114. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  115. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  116. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  117. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  118. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  119. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  120. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  121. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  122. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  123. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  124. package/mcps/davinci-mcp-professional/README.md +0 -143
  125. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  126. package/mcps/davinci-mcp-professional/USING.md +0 -301
  127. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  128. package/mcps/davinci-mcp-professional/build.py +0 -169
  129. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  130. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  131. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  132. package/mcps/davinci-mcp-professional/main.py +0 -40
  133. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  134. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  135. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  144. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  145. package/mcps/davinci-mcp-professional/test.py +0 -186
  146. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  147. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  148. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  149. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  151. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  152. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  153. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  154. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  155. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  156. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  157. package/mcps/davinci-resolve-mcp/README.md +0 -173
  158. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  159. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  160. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  161. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  162. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  163. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  164. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  165. package/mcps/davinci-resolve-mcp/package.json +0 -52
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  169. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  170. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  171. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  172. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  173. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  174. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  176. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  177. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  178. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  179. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  180. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  181. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  182. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  183. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  184. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  185. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  187. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  188. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  189. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  190. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  192. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  193. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  194. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  195. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  196. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  200. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  201. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  202. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  203. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  204. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  205. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  206. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  207. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  208. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  209. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  210. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  211. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  212. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  213. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  214. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  215. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  216. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  217. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  218. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  219. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  220. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  222. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  223. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  225. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  227. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  229. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  230. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  231. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  232. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  233. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  234. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  235. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  237. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  238. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  239. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  240. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  241. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  245. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  246. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  247. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  248. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  249. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  250. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  256. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  257. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  258. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  259. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  260. package/mcps/vectorworks-mcp/README.md +0 -156
  261. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  262. package/mcps/vectorworks-mcp/app/api.py +0 -52
  263. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  264. package/mcps/vectorworks-mcp/app/config.py +0 -29
  265. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  266. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  267. package/mcps/vectorworks-mcp/app/main.py +0 -33
  268. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  269. package/mcps/vectorworks-mcp/app/search.py +0 -54
  270. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  271. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  276. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  277. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  279. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  280. package/mcps/vectorworks-mcp/design.md +0 -57
  281. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  282. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  283. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  284. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  285. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  286. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  287. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  288. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  289. package/scripts/ecosystem-health-audit.js +0 -257
  290. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  291. package/skills/bdb-dev-os-skill/package.json +0 -11
  292. package/skills/bdbsaastraining/SKILL.md +0 -316
  293. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  294. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  295. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  296. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  297. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  298. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  299. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  300. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  301. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  302. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  303. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  304. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  305. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  306. package/skills/global_config/bdbsaashost/SKILL.md +0 -125
@@ -1,737 +0,0 @@
1
- """Edit-engine planning layer (Phase E of the analysis program).
2
-
3
- Pure evidence + planning: this module reads the DB-canonical analysis store
4
- and produces dry-run plans with a per-decision rationale. It never imports
5
- or touches Resolve — execution (timeline creation, lifts, swaps) lives in
6
- server.py behind the confirm-token gate and the destructive hook, which
7
- supplies versioning + brain_edits for free.
8
-
9
- Plans persist under ``memory/edit_plans/<plan_id>.json`` with a content
10
- fingerprint; execution revalidates the fingerprint so a stale plan cannot
11
- run against a changed project.
12
-
13
- Loops:
14
- - E1 selects — rank shots by select potential / best moments (deep-tier
15
- subjective rows, with description fallbacks), story-spine order, build a
16
- NEW selects timeline (additive; failure costs nothing).
17
- - E2 tighten — find dead air (transcript gaps within each timeline item's
18
- source range) and propose lifts toward a stated goal, applied to a
19
- DUPLICATE of the timeline, never the original.
20
- - E3 swap — rank alternate shots for a timeline item via the
21
- embeddings similarity index.
22
- """
23
-
24
- from __future__ import annotations
25
-
26
- import hashlib
27
- import json
28
- import os
29
- import time
30
- import uuid
31
- from typing import Any, Dict, List, Optional, Sequence, Tuple
32
-
33
- from src.utils import analysis_memory, analysis_store, timeline_brain_db
34
-
35
- PLAN_DIR_NAME = "edit_plans"
36
- DEFAULT_HANDLE_SECONDS = 0.25
37
- DEFAULT_MIN_PAUSE_SECONDS = 1.5
38
-
39
- _SELECT_RANK = {"high": 3, "medium": 2, "low": 1}
40
-
41
-
42
- def _now() -> str:
43
- return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
44
-
45
-
46
- def _plan_dir(project_root: str) -> str:
47
- return os.path.join(analysis_memory.memory_dir(project_root), PLAN_DIR_NAME)
48
-
49
-
50
- def _plan_fingerprint(plan: Dict[str, Any]) -> str:
51
- body = {k: v for k, v in plan.items() if k not in ("fingerprint", "saved_at")}
52
- return hashlib.sha256(json.dumps(body, sort_keys=True, default=str).encode("utf-8")).hexdigest()[:16]
53
-
54
-
55
- def save_plan(project_root: str, plan: Dict[str, Any]) -> Dict[str, Any]:
56
- analysis_memory.ensure_memory_structure(project_root)
57
- os.makedirs(_plan_dir(project_root), exist_ok=True)
58
- plan = dict(plan)
59
- plan.setdefault("plan_id", uuid.uuid4().hex[:12])
60
- plan["saved_at"] = _now()
61
- plan["fingerprint"] = _plan_fingerprint(plan)
62
- path = os.path.join(_plan_dir(project_root), f"{plan['plan_id']}.json")
63
- tmp = path + ".tmp"
64
- with open(tmp, "w", encoding="utf-8") as handle:
65
- json.dump(plan, handle, indent=2, default=str)
66
- os.replace(tmp, path)
67
- return plan
68
-
69
-
70
- def load_plan(project_root: str, plan_id: str) -> Optional[Dict[str, Any]]:
71
- path = os.path.join(_plan_dir(project_root), f"{str(plan_id)}.json")
72
- try:
73
- with open(path, "r", encoding="utf-8") as handle:
74
- plan = json.load(handle)
75
- except (OSError, json.JSONDecodeError):
76
- return None
77
- if not isinstance(plan, dict):
78
- return None
79
- if plan.get("fingerprint") != _plan_fingerprint(plan):
80
- return {"_corrupt": True, "plan_id": plan_id}
81
- return plan
82
-
83
-
84
- def list_plans(project_root: str, *, limit: int = 20, include_corrupt: bool = False) -> Dict[str, Any]:
85
- """List saved plans, newest first.
86
-
87
- `include_corrupt=True` (panel browser) surfaces fingerprint-mismatched
88
- plans as ``{"plan_id", "corrupt": True}`` warning rows instead of hiding
89
- them; the default keeps the MCP-action shape unchanged.
90
- """
91
- directory = _plan_dir(project_root)
92
- rows: List[Dict[str, Any]] = []
93
- if os.path.isdir(directory):
94
- for name in sorted(os.listdir(directory), reverse=True):
95
- if not name.endswith(".json"):
96
- continue
97
- plan = load_plan(project_root, name[:-5])
98
- if not plan or plan.get("_corrupt"):
99
- if include_corrupt:
100
- rows.append({"plan_id": name[:-5], "corrupt": True})
101
- continue
102
- rows.append({
103
- "plan_id": plan.get("plan_id"),
104
- "kind": plan.get("kind"),
105
- "saved_at": plan.get("saved_at"),
106
- "executed_at": plan.get("executed_at"),
107
- "summary": plan.get("summary"),
108
- })
109
- rows.sort(key=lambda r: str(r.get("saved_at") or ""), reverse=True)
110
- return {"success": True, "plans": rows[: max(1, int(limit))]}
111
-
112
-
113
- def mark_plan_executed(project_root: str, plan_id: str, result_summary: Dict[str, Any]) -> None:
114
- plan = load_plan(project_root, plan_id)
115
- if not plan or plan.get("_corrupt"):
116
- return
117
- plan["executed_at"] = _now()
118
- plan["execution_summary"] = result_summary
119
- plan["fingerprint"] = _plan_fingerprint(plan)
120
- path = os.path.join(_plan_dir(project_root), f"{plan_id}.json")
121
- tmp = path + ".tmp"
122
- with open(tmp, "w", encoding="utf-8") as handle:
123
- json.dump(plan, handle, indent=2, default=str)
124
- os.replace(tmp, path)
125
-
126
-
127
- # ── shared evidence helpers ──────────────────────────────────────────────────
128
-
129
-
130
- def _shot_groups(shot_row: Dict[str, Any]) -> Dict[str, Any]:
131
- extra = shot_row.get("extra_json")
132
- if not extra:
133
- return {}
134
- try:
135
- groups = json.loads(extra)
136
- except (TypeError, ValueError):
137
- return {}
138
- return groups if isinstance(groups, dict) else {}
139
-
140
-
141
- def _clip_fps(clip_row: Dict[str, Any]) -> float:
142
- fps = clip_row.get("fps")
143
- try:
144
- fps = float(fps)
145
- except (TypeError, ValueError):
146
- fps = 0.0
147
- return fps if fps > 0 else 24.0
148
-
149
-
150
- # ── E1: selects assembly ─────────────────────────────────────────────────────
151
-
152
-
153
- def plan_selects(
154
- project_root: str,
155
- *,
156
- timeline_name: Optional[str] = None,
157
- max_duration_seconds: Optional[float] = None,
158
- min_select_potential: str = "medium",
159
- handle_seconds: float = DEFAULT_HANDLE_SECONDS,
160
- max_shots: int = 60,
161
- ) -> Dict[str, Any]:
162
- """Rank shots into a selects plan (story-spine order, additive)."""
163
- conn = timeline_brain_db.connect(project_root)
164
- clips = {str(r["clip_uuid"]): dict(r) for r in conn.execute(
165
- "SELECT * FROM clips ORDER BY clip_name COLLATE NOCASE"
166
- ).fetchall()}
167
- if not clips:
168
- return {"success": False, "error": "No analyzed clips in the DB — analyze (or db_ingest) first."}
169
- min_rank = _SELECT_RANK.get(str(min_select_potential).lower(), 2)
170
-
171
- candidates: List[Dict[str, Any]] = []
172
- clip_order = {uuid_: i for i, uuid_ in enumerate(clips)}
173
- for shot_row in conn.execute(
174
- "SELECT * FROM shots ORDER BY clip_uuid, shot_index"
175
- ).fetchall():
176
- shot = dict(shot_row)
177
- clip = clips.get(str(shot["clip_uuid"]))
178
- if not clip or not clip.get("resolve_clip_id"):
179
- continue
180
- start = shot.get("time_seconds_start")
181
- end = shot.get("time_seconds_end")
182
- if start is None or end is None or float(end) - float(start) < 0.4:
183
- continue
184
- groups = _shot_groups(shot)
185
- editorial = groups.get("editorial") if isinstance(groups.get("editorial"), dict) else {}
186
- select_potential = str(editorial.get("select_potential") or "").lower()
187
- best_moment = editorial.get("best_moment") if isinstance(editorial.get("best_moment"), dict) else None
188
- rank = _SELECT_RANK.get(select_potential, 0)
189
- evidence: List[str] = []
190
- if rank:
191
- evidence.append(f"editorial.select_potential={select_potential} (deep vision)")
192
- if best_moment:
193
- evidence.append(f"best_moment at {best_moment.get('time_seconds')}s: {best_moment.get('why')}")
194
- if rank == 0:
195
- # Standard-analyzed clips have no deep editorial fields — fall back
196
- # to clip-level select potential so E1 works day one.
197
- clip_sp = conn.execute(
198
- """
199
- SELECT value_json FROM subjective_fields
200
- WHERE entity_type='clip' AND entity_uuid=? AND superseded_at IS NULL
201
- AND field_path='editorial_classification.select_potential'
202
- """,
203
- (shot["clip_uuid"],),
204
- ).fetchone()
205
- if clip_sp:
206
- try:
207
- value = str(json.loads(clip_sp["value_json"])).lower()
208
- rank = _SELECT_RANK.get(value, 0)
209
- if rank:
210
- evidence.append(f"clip-level select_potential={value} (no per-shot deep pass yet)")
211
- except (TypeError, ValueError):
212
- pass
213
- if rank < min_rank:
214
- continue
215
- candidates.append({
216
- "clip_uuid": shot["clip_uuid"],
217
- "clip_name": clip.get("clip_name"),
218
- "resolve_clip_id": clip.get("resolve_clip_id"),
219
- "shot_uuid": shot["shot_uuid"],
220
- "shot_index": shot["shot_index"],
221
- "time_seconds_start": float(start),
222
- "time_seconds_end": float(end),
223
- "duration_seconds": round(float(end) - float(start), 3),
224
- "fps": _clip_fps(clip),
225
- "rank": rank,
226
- "description": shot.get("description"),
227
- "rationale": "; ".join(evidence) or "shot present in analysis",
228
- "_order": (clip_order[str(shot["clip_uuid"])], int(shot["shot_index"])),
229
- })
230
-
231
- if not candidates:
232
- return {
233
- "success": False,
234
- "error": (
235
- f"No shots at select_potential >= {min_select_potential}. Run a deep pass "
236
- "(media_analysis action='deepen') or lower min_select_potential."
237
- ),
238
- }
239
-
240
- # Highest rank wins the budget; story-spine order for the final sequence.
241
- candidates.sort(key=lambda c: (-c["rank"], c["_order"]))
242
- chosen: List[Dict[str, Any]] = []
243
- total = 0.0
244
- for candidate in candidates[: max(1, int(max_shots) * 3)]:
245
- duration = candidate["duration_seconds"] + 2 * float(handle_seconds)
246
- if max_duration_seconds and total + duration > float(max_duration_seconds) and chosen:
247
- continue
248
- chosen.append(candidate)
249
- total += duration
250
- if len(chosen) >= int(max_shots):
251
- break
252
- chosen.sort(key=lambda c: c["_order"])
253
-
254
- decisions = []
255
- clip_infos = []
256
- for candidate in chosen:
257
- fps = candidate["fps"]
258
- clip_row = clips.get(str(candidate["clip_uuid"])) or {}
259
- clip_duration = clip_row.get("duration_seconds")
260
- src_start = max(0.0, candidate["time_seconds_start"] - float(handle_seconds))
261
- src_end = candidate["time_seconds_end"] + float(handle_seconds)
262
- if isinstance(clip_duration, (int, float)) and clip_duration:
263
- src_end = min(src_end, float(clip_duration))
264
- start_frame = int(round(src_start * fps))
265
- end_frame = max(start_frame + 1, int(round(src_end * fps)) - 1)
266
- decision = {k: v for k, v in candidate.items() if not k.startswith("_")}
267
- decision["source_frame_range"] = [start_frame, end_frame]
268
- decisions.append(decision)
269
- clip_infos.append({
270
- "clip_id": candidate["resolve_clip_id"],
271
- "start_frame": start_frame,
272
- "end_frame": end_frame,
273
- })
274
-
275
- name = timeline_name or f"Selects — {_now()[:10]}"
276
- plan = save_plan(project_root, {
277
- "kind": "selects",
278
- "timeline_name": name,
279
- "decisions": decisions,
280
- "clip_infos": clip_infos,
281
- "estimated_duration_seconds": round(total, 2),
282
- "summary": f"{len(decisions)} shots, ~{round(total, 1)}s → new timeline '{name}'",
283
- "settings": {
284
- "min_select_potential": min_select_potential,
285
- "max_duration_seconds": max_duration_seconds,
286
- "handle_seconds": handle_seconds,
287
- },
288
- })
289
- return {
290
- "success": True,
291
- "status": "plan_ready",
292
- "plan_id": plan["plan_id"],
293
- "kind": "selects",
294
- "timeline_name": name,
295
- "decision_count": len(decisions),
296
- "estimated_duration_seconds": plan["estimated_duration_seconds"],
297
- "decisions": decisions,
298
- "note": (
299
- "Dry-run plan. Execute with edit_engine(action='execute_selects', "
300
- "params={plan_id}) — a NEW timeline is created; nothing existing is touched."
301
- ),
302
- }
303
-
304
-
305
- # ── E2: tighten ──────────────────────────────────────────────────────────────
306
-
307
-
308
- def _speech_intervals(conn, clip_uuid: str) -> List[Tuple[float, float]]:
309
- rows = conn.execute(
310
- """
311
- SELECT start_seconds, end_seconds FROM transcript_segments
312
- WHERE clip_uuid = ? AND start_seconds IS NOT NULL AND end_seconds IS NOT NULL
313
- ORDER BY start_seconds
314
- """,
315
- (clip_uuid,),
316
- ).fetchall()
317
- return [(float(r["start_seconds"]), float(r["end_seconds"])) for r in rows]
318
-
319
-
320
- def _gaps_in_range(
321
- intervals: Sequence[Tuple[float, float]],
322
- start: float,
323
- end: float,
324
- *,
325
- min_gap: float,
326
- ) -> List[Tuple[float, float]]:
327
- """Sub-ranges of [start, end] not covered by any interval, >= min_gap."""
328
- gaps: List[Tuple[float, float]] = []
329
- cursor = start
330
- for s, e in sorted(intervals):
331
- if e <= start or s >= end:
332
- continue
333
- s, e = max(s, start), min(e, end)
334
- if s - cursor >= min_gap:
335
- gaps.append((cursor, s))
336
- cursor = max(cursor, e)
337
- if end - cursor >= min_gap:
338
- gaps.append((cursor, end))
339
- return gaps
340
-
341
-
342
- def plan_tighten(
343
- project_root: str,
344
- *,
345
- items: Sequence[Dict[str, Any]],
346
- timeline_name: str,
347
- timeline_fps: float,
348
- target_ratio: Optional[float] = None,
349
- min_pause_seconds: float = DEFAULT_MIN_PAUSE_SECONDS,
350
- handle_seconds: float = DEFAULT_HANDLE_SECONDS,
351
- include_audio: bool = True,
352
- ) -> Dict[str, Any]:
353
- """Propose dead-air lifts for a timeline.
354
-
355
- `items` rows come from the server (Resolve read): each needs
356
- {timeline_start_frame, timeline_end_frame, source_start_frame,
357
- media_ref (clip id / path / hash), item_name?}. Optionally each row may
358
- carry {audio_track_indices: [int, ...]} naming the audio tracks that hold
359
- the item's linked audio. Lifts are returned in timeline frames, latest-first
360
- ready.
361
-
362
- When ``include_audio`` (the default), each kept video range is mirrored onto
363
- matching audio range(s) so the assembled variant carries sound — a
364
- speech-driven cut would otherwise come out silent (see #67). Audio is
365
- mirrored to the item's detected ``audio_track_indices``; absent that, it
366
- falls back to audio track 1, which is where a single linked A/V clip's audio
367
- lives.
368
- """
369
- if not items:
370
- return {"success": False, "error": "No timeline items supplied"}
371
- fps = float(timeline_fps) if timeline_fps and float(timeline_fps) > 0 else 24.0
372
- conn = timeline_brain_db.connect(project_root)
373
-
374
- lifts: List[Dict[str, Any]] = []
375
- skipped: List[Dict[str, Any]] = []
376
- item_specs: List[Dict[str, Any]] = [] # per usable item: source mapping for keep-range rebuild
377
- timeline_total_frames = 0
378
- for item_index, item in enumerate(items):
379
- try:
380
- tl_start = int(item["timeline_start_frame"])
381
- tl_end = int(item["timeline_end_frame"])
382
- src_start_frame = int(item.get("source_start_frame") or 0)
383
- except (KeyError, TypeError, ValueError):
384
- skipped.append({"item": item.get("item_name"), "reason": "missing frame fields"})
385
- continue
386
- timeline_total_frames += max(0, tl_end - tl_start)
387
- clip_uuid = analysis_store.resolve_clip_uuid(
388
- conn, item.get("media_ref")
389
- ) or analysis_store.resolve_clip_uuid(conn, item.get("media_path"))
390
- if not clip_uuid:
391
- skipped.append({"item": item.get("item_name"), "reason": "no analysis for source media (db_ingest or analyze first)"})
392
- item_specs.append({"item_index": item_index, "unanalyzed": True,
393
- "resolve_clip_id": None, "item": item})
394
- continue
395
- clip_row = conn.execute("SELECT * FROM clips WHERE clip_uuid = ?", (clip_uuid,)).fetchone()
396
- clip_fps = _clip_fps(dict(clip_row)) if clip_row else fps
397
- src_start_sec = src_start_frame / clip_fps
398
- src_end_sec = src_start_sec + (tl_end - tl_start) / fps
399
- spec = {
400
- "item_index": item_index,
401
- "item": item,
402
- "clip_uuid": clip_uuid,
403
- "clip_fps": clip_fps,
404
- "resolve_clip_id": dict(clip_row).get("resolve_clip_id") if clip_row else None,
405
- "src_start_sec": src_start_sec,
406
- "src_end_sec": src_end_sec,
407
- }
408
- item_specs.append(spec)
409
- speech = _speech_intervals(conn, clip_uuid)
410
- if not speech:
411
- skipped.append({"item": item.get("item_name"), "reason": "no transcript segments — dead-air evidence unavailable"})
412
- continue
413
- for gap_start, gap_end in _gaps_in_range(speech, src_start_sec, src_end_sec, min_gap=float(min_pause_seconds)):
414
- # Keep handles on both sides of the lift.
415
- lift_start_sec = gap_start + float(handle_seconds)
416
- lift_end_sec = gap_end - float(handle_seconds)
417
- if lift_end_sec - lift_start_sec < 0.2:
418
- continue
419
- lift_start = tl_start + int(round((lift_start_sec - src_start_sec) * fps))
420
- lift_end = tl_start + int(round((lift_end_sec - src_start_sec) * fps))
421
- if lift_end <= lift_start:
422
- continue
423
- lifts.append({
424
- "kind": "dead_air",
425
- "action": "lift",
426
- "timeline_start_frame": lift_start,
427
- "timeline_end_frame": lift_end,
428
- "duration_seconds": round((lift_end - lift_start) / fps, 3),
429
- "item_name": item.get("item_name"),
430
- "item_index": item_index,
431
- "clip_uuid": clip_uuid,
432
- "source_lift_seconds": [round(lift_start_sec, 3), round(lift_end_sec, 3)],
433
- "rationale": (
434
- f"No speech from {round(gap_start, 2)}s to {round(gap_end, 2)}s in the source "
435
- f"transcript ({round(gap_end - gap_start, 2)}s pause; handles kept)."
436
- ),
437
- "evidence": {
438
- "source_gap_seconds": [round(gap_start, 3), round(gap_end, 3)],
439
- "basis": "transcript_segments",
440
- },
441
- })
442
-
443
- if not lifts:
444
- return {
445
- "success": False,
446
- "error": "No dead-air lifts found",
447
- "skipped": skipped,
448
- "note": f"min_pause_seconds={min_pause_seconds}; items without transcripts are skipped.",
449
- }
450
-
451
- lifts.sort(key=lambda l: -l["duration_seconds"])
452
- if target_ratio:
453
- target_frames = timeline_total_frames * float(target_ratio)
454
- chosen: List[Dict[str, Any]] = []
455
- removed = 0.0
456
- for lift in lifts:
457
- if removed >= target_frames:
458
- break
459
- chosen.append(lift)
460
- removed += (lift["timeline_end_frame"] - lift["timeline_start_frame"])
461
- lifts = chosen
462
- # Latest-first application order so earlier spans stay valid.
463
- lifts.sort(key=lambda l: -l["timeline_start_frame"])
464
-
465
- # Keep ranges: per item, the complement of its selected lifts, expressed as
466
- # media-pool SOURCE frame ranges. Execution assembles a tightened VARIANT
467
- # timeline from these (true partial trims; the original is never mutated).
468
- keep_ranges: List[Dict[str, Any]] = []
469
- lifts_by_item: Dict[int, List[Dict[str, Any]]] = {}
470
- for lift in lifts:
471
- lifts_by_item.setdefault(int(lift["item_index"]), []).append(lift)
472
- for spec in item_specs:
473
- item = spec["item"]
474
- if spec.get("unanalyzed") or not spec.get("resolve_clip_id"):
475
- # Items we can't trim ride along whole when their clip is known;
476
- # otherwise they were already reported in `skipped`.
477
- continue
478
- clip_fps = spec["clip_fps"]
479
- cursor = spec["src_start_sec"]
480
- segments: List[Tuple[float, float]] = []
481
- for lift in sorted(lifts_by_item.get(spec["item_index"], []), key=lambda l: l["source_lift_seconds"][0]):
482
- lift_start_sec, lift_end_sec = lift["source_lift_seconds"]
483
- if lift_start_sec - cursor > 0.05:
484
- segments.append((cursor, lift_start_sec))
485
- cursor = max(cursor, lift_end_sec)
486
- if spec["src_end_sec"] - cursor > 0.05:
487
- segments.append((cursor, spec["src_end_sec"]))
488
- audio_indices: List[int] = []
489
- if include_audio:
490
- audio_indices = [int(i) for i in (item.get("audio_track_indices") or []) if int(i) > 0]
491
- if not audio_indices:
492
- audio_indices = [1]
493
- for seg_start, seg_end in segments:
494
- start_frame = int(round(seg_start * clip_fps))
495
- end_frame = max(start_frame + 1, int(round(seg_end * clip_fps)) - 1)
496
- keep_ranges.append({
497
- "clip_id": spec["resolve_clip_id"],
498
- "start_frame": start_frame,
499
- "end_frame": end_frame,
500
- "track_type": "video",
501
- "track_index": int(item.get("track_index") or 1),
502
- })
503
- # Mirror each kept video range onto its linked audio track(s) with
504
- # identical source frames so the variant stays frame-locked and
505
- # audible. mediaType 2 pulls the same media-pool item's audio.
506
- for audio_index in audio_indices:
507
- keep_ranges.append({
508
- "clip_id": spec["resolve_clip_id"],
509
- "start_frame": start_frame,
510
- "end_frame": end_frame,
511
- "track_type": "audio",
512
- "media_type": 2,
513
- "track_index": audio_index,
514
- })
515
-
516
- removed_frames = sum(l["timeline_end_frame"] - l["timeline_start_frame"] for l in lifts)
517
- audio_keep_range_count = sum(1 for r in keep_ranges if r.get("track_type") == "audio")
518
- video_keep_range_count = len(keep_ranges) - audio_keep_range_count
519
- plan = save_plan(project_root, {
520
- "kind": "tighten",
521
- "timeline_name": timeline_name,
522
- "timeline_fps": fps,
523
- "lifts": lifts,
524
- "keep_ranges": keep_ranges,
525
- "include_audio": bool(include_audio),
526
- "skipped": skipped,
527
- "summary": (
528
- f"{len(lifts)} dead-air lifts, ~{round(removed_frames / fps, 1)}s removed "
529
- f"from '{timeline_name}' (assembled as a tightened variant"
530
- f"{', video + audio' if include_audio else ', video only'})"
531
- ),
532
- "settings": {
533
- "target_ratio": target_ratio,
534
- "min_pause_seconds": min_pause_seconds,
535
- "handle_seconds": handle_seconds,
536
- "include_audio": bool(include_audio),
537
- },
538
- })
539
- return {
540
- "success": True,
541
- "status": "plan_ready",
542
- "plan_id": plan["plan_id"],
543
- "kind": "tighten",
544
- "timeline_name": timeline_name,
545
- "lift_count": len(lifts),
546
- "estimated_removed_seconds": round(removed_frames / fps, 2),
547
- "lifts": lifts,
548
- "keep_range_count": len(keep_ranges),
549
- "video_keep_range_count": video_keep_range_count,
550
- "audio_keep_range_count": audio_keep_range_count,
551
- "include_audio": bool(include_audio),
552
- "skipped": skipped,
553
- "note": (
554
- "Dry-run plan. Execute with edit_engine(action='execute_tighten', "
555
- "params={plan_id}) — a tightened VARIANT timeline is assembled from "
556
- "the keep ranges; the original timeline is never mutated. "
557
- + (
558
- f"Audio is mirrored onto matching tracks ({audio_keep_range_count} "
559
- "audio ranges) so the variant is audible."
560
- if include_audio
561
- else "include_audio=False: the variant will be VIDEO-ONLY (silent)."
562
- )
563
- ),
564
- }
565
-
566
-
567
- # ── E3: swap alternates ──────────────────────────────────────────────────────
568
-
569
-
570
- def plan_swap(
571
- project_root: str,
572
- *,
573
- item: Dict[str, Any],
574
- timeline_name: str,
575
- timeline_fps: float,
576
- kind: str = "visual",
577
- limit: int = 5,
578
- ) -> Dict[str, Any]:
579
- """Rank alternate shots for one timeline item via the similarity index.
580
-
581
- `item` comes from the server: {timeline_start_frame, timeline_end_frame,
582
- source_start_frame, media_ref, item_name?, track_index?}.
583
- """
584
- from src.utils import embeddings
585
-
586
- conn = timeline_brain_db.connect(project_root)
587
- clip_uuid = analysis_store.resolve_clip_uuid(
588
- conn, item.get("media_ref")
589
- ) or analysis_store.resolve_clip_uuid(conn, item.get("media_path"))
590
- if not clip_uuid:
591
- return {"success": False, "error": "No analysis for the item's source media (db_ingest or analyze first)"}
592
- clip_row = conn.execute("SELECT * FROM clips WHERE clip_uuid = ?", (clip_uuid,)).fetchone()
593
- clip_fps = _clip_fps(dict(clip_row)) if clip_row else 24.0
594
- fps = float(timeline_fps) if timeline_fps and float(timeline_fps) > 0 else 24.0
595
- try:
596
- tl_start = int(item["timeline_start_frame"])
597
- tl_end = int(item["timeline_end_frame"])
598
- src_start_frame = int(item.get("source_start_frame") or 0)
599
- except (KeyError, TypeError, ValueError):
600
- return {"success": False, "error": "item requires timeline_start_frame/timeline_end_frame/source_start_frame"}
601
- src_mid_sec = src_start_frame / clip_fps + ((tl_end - tl_start) / fps) / 2.0
602
- shot_row = conn.execute(
603
- """
604
- SELECT * FROM shots
605
- WHERE clip_uuid = ? AND time_seconds_start <= ? AND time_seconds_end > ?
606
- """,
607
- (clip_uuid, src_mid_sec, src_mid_sec),
608
- ).fetchone()
609
- if not shot_row:
610
- return {"success": False, "error": f"No analyzed shot covers source time {round(src_mid_sec, 2)}s"}
611
- shot = dict(shot_row)
612
-
613
- found = embeddings.find_similar(
614
- project_root,
615
- shot_uuid=shot["shot_uuid"],
616
- kind=kind,
617
- entity_types=["shot"],
618
- limit=int(limit) * 2,
619
- )
620
- if not found.get("success"):
621
- return found
622
- duration_frames = tl_end - tl_start
623
- needed_seconds = duration_frames / fps
624
-
625
- # Vision-confirmed alt takes outrank raw cosine similarity (spec §4).
626
- from src.utils import shot_relationships as _shot_relationships
627
- confirmed_alts = set(_shot_relationships.confirmed_alt_take_shot_uuids(conn, shot["shot_uuid"]))
628
-
629
- def _viable_alternate(
630
- *, clip_uuid: Any, shot_uuid_: Any, shot_index: Any, description: Any,
631
- alt_start: Any, alt_end: Any, score: Any, rationale: str,
632
- ) -> Optional[Dict[str, Any]]:
633
- alt_clip = conn.execute(
634
- "SELECT * FROM clips WHERE clip_uuid = ?", (clip_uuid,)
635
- ).fetchone()
636
- if not alt_clip or not alt_clip["resolve_clip_id"]:
637
- return None
638
- if alt_start is None or alt_end is None:
639
- return None
640
- if (float(alt_end) - float(alt_start)) < needed_seconds:
641
- return None # alternate too short to fill the slot
642
- alt = dict(alt_clip)
643
- alt_fps = _clip_fps(alt)
644
- start_frame = int(round(float(alt_start) * alt_fps))
645
- end_frame = start_frame + int(round(needed_seconds * alt_fps)) - 1
646
- return {
647
- "score": score,
648
- "clip_uuid": clip_uuid,
649
- "clip_name": alt.get("clip_name"),
650
- "resolve_clip_id": alt["resolve_clip_id"],
651
- "shot_uuid": shot_uuid_,
652
- "shot_index": shot_index,
653
- "description": description,
654
- "source_frame_range": [start_frame, end_frame],
655
- "confirmed_alt_take": str(shot_uuid_) in confirmed_alts,
656
- "rationale": rationale,
657
- }
658
-
659
- alternates: List[Dict[str, Any]] = []
660
- seen_shot_uuids: set = set()
661
- for hit in found.get("results") or []:
662
- hit_uuid = str(hit.get("entity_uuid"))
663
- is_confirmed = hit_uuid in confirmed_alts
664
- basis = (
665
- f"vision-confirmed alt_take_of relationship (cosine {hit.get('score')} agrees)"
666
- if is_confirmed
667
- else f"cosine {hit.get('score')} to the current shot ({kind} embedding)"
668
- )
669
- alternate = _viable_alternate(
670
- clip_uuid=hit.get("clip_uuid"), shot_uuid_=hit.get("entity_uuid"),
671
- shot_index=hit.get("shot_index"), description=hit.get("description"),
672
- alt_start=hit.get("time_seconds_start"), alt_end=hit.get("time_seconds_end"),
673
- score=hit.get("score"),
674
- rationale=f"{basis}; long enough to fill the slot exactly",
675
- )
676
- if alternate:
677
- alternates.append(alternate)
678
- seen_shot_uuids.add(hit_uuid)
679
- # Confirmed alt takes the cosine search missed still belong in the list.
680
- for alt_uuid in confirmed_alts - seen_shot_uuids:
681
- alt_shot = conn.execute("SELECT * FROM shots WHERE shot_uuid = ?", (alt_uuid,)).fetchone()
682
- if not alt_shot:
683
- continue
684
- alt_shot = dict(alt_shot)
685
- alternate = _viable_alternate(
686
- clip_uuid=alt_shot.get("clip_uuid"), shot_uuid_=alt_uuid,
687
- shot_index=alt_shot.get("shot_index"), description=alt_shot.get("description"),
688
- alt_start=alt_shot.get("time_seconds_start"), alt_end=alt_shot.get("time_seconds_end"),
689
- score=None,
690
- rationale="vision-confirmed alt_take_of relationship (not surfaced by the cosine search); long enough to fill the slot exactly",
691
- )
692
- if alternate:
693
- alternates.append(alternate)
694
- alternates.sort(key=lambda a: (not a.get("confirmed_alt_take"), -(a.get("score") or 0.0)))
695
- alternates = alternates[: int(limit)]
696
- if not alternates:
697
- return {
698
- "success": False,
699
- "error": "No viable alternates (similar shots were too short or their clips are not in this Resolve project)",
700
- }
701
-
702
- plan = save_plan(project_root, {
703
- "kind": "swap",
704
- "timeline_name": timeline_name,
705
- "timeline_fps": fps,
706
- "item": {
707
- "timeline_start_frame": tl_start,
708
- "timeline_end_frame": tl_end,
709
- "track_index": item.get("track_index") or 1,
710
- "item_name": item.get("item_name"),
711
- "current_shot_uuid": shot["shot_uuid"],
712
- "current_description": shot.get("description"),
713
- },
714
- "alternates": alternates,
715
- "summary": (
716
- f"{len(alternates)} alternates for '{item.get('item_name') or 'item'}' "
717
- f"on '{timeline_name}' (slot {tl_start}-{tl_end})"
718
- ),
719
- })
720
- return {
721
- "success": True,
722
- "status": "plan_ready",
723
- "plan_id": plan["plan_id"],
724
- "kind": "swap",
725
- "timeline_name": timeline_name,
726
- "current_shot": {
727
- "shot_uuid": shot["shot_uuid"],
728
- "shot_index": shot["shot_index"],
729
- "description": shot.get("description"),
730
- },
731
- "alternates": alternates,
732
- "note": (
733
- "Dry-run plan. Execute with edit_engine(action='execute_swap', "
734
- "params={plan_id, alternate_index}) — the item is replaced on a "
735
- "version-archived timeline (lift + positioned append, same slot)."
736
- ),
737
- }