@hybridlabor-api/aos 4.4.1 → 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 (305) 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.md +573 -0
  20. package/README.de.md +0 -8
  21. package/README.md +0 -8
  22. package/README.pt.md +0 -8
  23. package/assets/brand/ao-ant-mark.svg +91 -0
  24. package/assets/brand/bdb-core-mark.svg +34 -0
  25. package/assets/brand/heimdall-guardian-mark.svg +16 -0
  26. package/assets/brand/memb-crystal-mark.svg +18 -0
  27. package/bin/aos-config.mjs +89 -0
  28. package/bin/aos-dashboard.mjs +608 -0
  29. package/bin/aos-uninstall.mjs +227 -0
  30. package/docs/sessions/audit-2026-09-14-silent-failures.md +114 -0
  31. package/docs/skills_table.md +0 -3
  32. package/installer.js +387 -237
  33. package/mcps/RhinoMCP/cc-plugin/.claude/settings.json +10 -0
  34. package/mcps/after-effects-mcp/build/index.js +840 -0
  35. package/mcps/after-effects-mcp/build/scripts/applyEffect.jsx +153 -0
  36. package/mcps/after-effects-mcp/build/scripts/applyEffectTemplate.jsx +218 -0
  37. package/mcps/after-effects-mcp/build/scripts/createComposition.jsx +71 -0
  38. package/mcps/after-effects-mcp/build/scripts/createShapeLayer.jsx +147 -0
  39. package/mcps/after-effects-mcp/build/scripts/createSolidLayer.jsx +114 -0
  40. package/mcps/after-effects-mcp/build/scripts/createTextLayer.jsx +115 -0
  41. package/mcps/after-effects-mcp/build/scripts/getLayerInfo.jsx +192 -0
  42. package/mcps/after-effects-mcp/build/scripts/getProjectInfo.jsx +90 -0
  43. package/mcps/after-effects-mcp/build/scripts/listCompositions.jsx +50 -0
  44. package/mcps/after-effects-mcp/build/scripts/mcp-bridge-auto.jsx +1773 -0
  45. package/mcps/after-effects-mcp/build/scripts/setLayerProperties.jsx +160 -0
  46. package/mcps/computer-use-mcp/dist/client.d.ts +150 -0
  47. package/mcps/computer-use-mcp/dist/client.js +136 -0
  48. package/mcps/computer-use-mcp/dist/entrypoint.d.ts +16 -0
  49. package/mcps/computer-use-mcp/dist/entrypoint.js +26 -0
  50. package/mcps/computer-use-mcp/dist/native.d.ts +212 -0
  51. package/mcps/computer-use-mcp/dist/native.js +50 -0
  52. package/mcps/computer-use-mcp/dist/server.d.ts +32 -0
  53. package/mcps/computer-use-mcp/dist/server.js +342 -0
  54. package/mcps/computer-use-mcp/dist/session.d.ts +101 -0
  55. package/mcps/computer-use-mcp/dist/session.js +2372 -0
  56. package/package.json +13 -4
  57. package/skills/global_config/aos-project-init/SKILL.md +97 -130
  58. package/skills/global_config/aos-project-init/assets/openwikiignore.template +62 -0
  59. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +38 -1
  60. package/skills/global_config/aos-setup/SKILL.md +98 -8
  61. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +21 -2
  62. package/skills/global_config/ask-tim/SKILL.md +2 -7
  63. package/skills/global_config/openwiki-skill/scripts/openwiki_daemon.py +65 -12
  64. package/bin/setup-saas.mjs +0 -792
  65. package/mcps/blender-mcp-server/.dockerignore +0 -19
  66. package/mcps/blender-mcp-server/.github/workflows/ci.yml +0 -67
  67. package/mcps/blender-mcp-server/.github/workflows/publish-pypi.yml +0 -54
  68. package/mcps/blender-mcp-server/CHANGELOG.md +0 -64
  69. package/mcps/blender-mcp-server/CONTRIBUTING.md +0 -107
  70. package/mcps/blender-mcp-server/Dockerfile +0 -19
  71. package/mcps/blender-mcp-server/LICENSE +0 -21
  72. package/mcps/blender-mcp-server/README.md +0 -409
  73. package/mcps/blender-mcp-server/addon/__init__.py +0 -1440
  74. package/mcps/blender-mcp-server/addon/models.py +0 -223
  75. package/mcps/blender-mcp-server/pyproject.toml +0 -120
  76. package/mcps/blender-mcp-server/scripts/blender_bridge_request.py +0 -71
  77. package/mcps/blender-mcp-server/scripts/blender_create_test_cube.py +0 -49
  78. package/mcps/blender-mcp-server/scripts/blender_scene_info.py +0 -38
  79. package/mcps/blender-mcp-server/scripts/build_addon_zip.sh +0 -29
  80. package/mcps/blender-mcp-server/scripts/demos/README.md +0 -186
  81. package/mcps/blender-mcp-server/scripts/demos/dam_break_scene.py +0 -276
  82. package/mcps/blender-mcp-server/scripts/demos/open_dam_break_gui.py +0 -33
  83. package/mcps/blender-mcp-server/scripts/demos/pipe_movement_study.py +0 -518
  84. package/mcps/blender-mcp-server/scripts/demos/pipe_pressure_failure_scene.py +0 -806
  85. package/mcps/blender-mcp-server/scripts/demos/procedural_dam_break_scene.py +0 -600
  86. package/mcps/blender-mcp-server/scripts/demos/run_dam_break.py +0 -510
  87. package/mcps/blender-mcp-server/scripts/launch_blender_gui.py +0 -41
  88. package/mcps/blender-mcp-server/scripts/library/README.md +0 -64
  89. package/mcps/blender-mcp-server/scripts/library/apply_transforms.py +0 -46
  90. package/mcps/blender-mcp-server/scripts/library/camera.py +0 -51
  91. package/mcps/blender-mcp-server/scripts/library/collections.py +0 -65
  92. package/mcps/blender-mcp-server/scripts/library/create_mesh.py +0 -41
  93. package/mcps/blender-mcp-server/scripts/library/effector.py +0 -48
  94. package/mcps/blender-mcp-server/scripts/library/fluid_domain.py +0 -80
  95. package/mcps/blender-mcp-server/scripts/library/fluid_inflow.py +0 -98
  96. package/mcps/blender-mcp-server/scripts/library/frame_range.py +0 -35
  97. package/mcps/blender-mcp-server/scripts/library/keyframes.py +0 -59
  98. package/mcps/blender-mcp-server/scripts/library/rigid_body.py +0 -52
  99. package/mcps/blender-mcp-server/scripts/library/save_blend.py +0 -40
  100. package/mcps/blender-mcp-server/scripts/server_start.sh +0 -26
  101. package/mcps/blender-mcp-server/src/blender_mcp_server/__init__.py +0 -3
  102. package/mcps/blender-mcp-server/src/blender_mcp_server/headless.py +0 -330
  103. package/mcps/blender-mcp-server/src/blender_mcp_server/server.py +0 -606
  104. package/mcps/davinci-mcp-professional/.bandit +0 -35
  105. package/mcps/davinci-mcp-professional/.claude/commands/security-review.md +0 -191
  106. package/mcps/davinci-mcp-professional/.claude/settings.json +0 -1
  107. package/mcps/davinci-mcp-professional/.editorconfig +0 -27
  108. package/mcps/davinci-mcp-professional/.gitattributes +0 -207
  109. package/mcps/davinci-mcp-professional/.github/CODEOWNERS +0 -2
  110. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  111. package/mcps/davinci-mcp-professional/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  112. package/mcps/davinci-mcp-professional/.github/PULL_REQUEST_TEMPLATE.md +0 -20
  113. package/mcps/davinci-mcp-professional/.github/SECURITY.md +0 -97
  114. package/mcps/davinci-mcp-professional/.github/dependabot.yml +0 -27
  115. package/mcps/davinci-mcp-professional/.github/workflows/ci.yml +0 -39
  116. package/mcps/davinci-mcp-professional/ATTRIBUTION.md +0 -4
  117. package/mcps/davinci-mcp-professional/AUTHORS.md +0 -9
  118. package/mcps/davinci-mcp-professional/BUGS.md +0 -55
  119. package/mcps/davinci-mcp-professional/CLAUDE.md +0 -229
  120. package/mcps/davinci-mcp-professional/CONTRIBUTING.md +0 -51
  121. package/mcps/davinci-mcp-professional/COPYING.md +0 -228
  122. package/mcps/davinci-mcp-professional/Doxyfile +0 -379
  123. package/mcps/davinci-mcp-professional/README.md +0 -143
  124. package/mcps/davinci-mcp-professional/SECURITY.md +0 -40
  125. package/mcps/davinci-mcp-professional/USING.md +0 -301
  126. package/mcps/davinci-mcp-professional/VERSION.md +0 -13
  127. package/mcps/davinci-mcp-professional/build.py +0 -169
  128. package/mcps/davinci-mcp-professional/claude_desktop_config_template.json +0 -11
  129. package/mcps/davinci-mcp-professional/hooks/ci-check.sh +0 -29
  130. package/mcps/davinci-mcp-professional/hooks/pre-push +0 -46
  131. package/mcps/davinci-mcp-professional/main.py +0 -40
  132. package/mcps/davinci-mcp-professional/mcp_server.py +0 -30
  133. package/mcps/davinci-mcp-professional/pyproject.toml +0 -115
  134. package/mcps/davinci-mcp-professional/security_audit.py +0 -379
  135. package/mcps/davinci-mcp-professional/src/davinci_mcp/__init__.py +0 -19
  136. package/mcps/davinci-mcp-professional/src/davinci_mcp/cli.py +0 -138
  137. package/mcps/davinci-mcp-professional/src/davinci_mcp/resolve_client.py +0 -342
  138. package/mcps/davinci-mcp-professional/src/davinci_mcp/resources/__init__.py +0 -60
  139. package/mcps/davinci-mcp-professional/src/davinci_mcp/server.py +0 -185
  140. package/mcps/davinci-mcp-professional/src/davinci_mcp/tools/__init__.py +0 -146
  141. package/mcps/davinci-mcp-professional/src/davinci_mcp/types.py +0 -125
  142. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/__init__.py +0 -19
  143. package/mcps/davinci-mcp-professional/src/davinci_mcp/utils/platform.py +0 -239
  144. package/mcps/davinci-mcp-professional/test.py +0 -186
  145. package/mcps/davinci-mcp-professional/uv.lock +0 -2073
  146. package/mcps/davinci-resolve-mcp/.claude/skills/media-analysis.md +0 -44
  147. package/mcps/davinci-resolve-mcp/.clinerules +0 -17
  148. package/mcps/davinci-resolve-mcp/.cursorrules +0 -17
  149. package/mcps/davinci-resolve-mcp/.github/copilot-instructions.md +0 -17
  150. package/mcps/davinci-resolve-mcp/.github/workflows/npm-publish.yml +0 -109
  151. package/mcps/davinci-resolve-mcp/.windsurfrules +0 -17
  152. package/mcps/davinci-resolve-mcp/AGENTS.md +0 -114
  153. package/mcps/davinci-resolve-mcp/CHANGELOG.md +0 -2648
  154. package/mcps/davinci-resolve-mcp/CLAUDE.md +0 -15
  155. package/mcps/davinci-resolve-mcp/LICENSE +0 -21
  156. package/mcps/davinci-resolve-mcp/README.md +0 -173
  157. package/mcps/davinci-resolve-mcp/SECURITY.md +0 -53
  158. package/mcps/davinci-resolve-mcp/bin/davinci-resolve-mcp.mjs +0 -450
  159. package/mcps/davinci-resolve-mcp/examples/README.md +0 -53
  160. package/mcps/davinci-resolve-mcp/examples/markers/README.md +0 -81
  161. package/mcps/davinci-resolve-mcp/examples/media/README.md +0 -94
  162. package/mcps/davinci-resolve-mcp/examples/timeline/README.md +0 -98
  163. package/mcps/davinci-resolve-mcp/install.py +0 -1687
  164. package/mcps/davinci-resolve-mcp/package.json +0 -52
  165. package/mcps/davinci-resolve-mcp/release-notes/v2.22.0.md +0 -51
  166. package/mcps/davinci-resolve-mcp/release-notes/v2.23.0.md +0 -51
  167. package/mcps/davinci-resolve-mcp/release-notes/v2.23.1.md +0 -35
  168. package/mcps/davinci-resolve-mcp/release-notes/v2.24.1.md +0 -32
  169. package/mcps/davinci-resolve-mcp/scripts/audit_api_parity.py +0 -275
  170. package/mcps/davinci-resolve-mcp/scripts/audit_readwrite_symmetry.py +0 -80
  171. package/mcps/davinci-resolve-mcp/scripts/doctor.py +0 -227
  172. package/mcps/davinci-resolve-mcp/scripts/gen_api_limitations.py +0 -144
  173. package/mcps/davinci-resolve-mcp/scripts/live_media_analysis_polish_probe.py +0 -65
  174. package/mcps/davinci-resolve-mcp/scripts/measure_bridge_cost.py +0 -65
  175. package/mcps/davinci-resolve-mcp/scripts/regen_panel_screenshots.py +0 -76
  176. package/mcps/davinci-resolve-mcp/src/__init__.py +0 -3
  177. package/mcps/davinci-resolve-mcp/src/analysis_dashboard.py +0 -15505
  178. package/mcps/davinci-resolve-mcp/src/batch_cli.py +0 -540
  179. package/mcps/davinci-resolve-mcp/src/control_panel.py +0 -13
  180. package/mcps/davinci-resolve-mcp/src/granular/__init__.py +0 -17
  181. package/mcps/davinci-resolve-mcp/src/granular/common.py +0 -741
  182. package/mcps/davinci-resolve-mcp/src/granular/folder.py +0 -420
  183. package/mcps/davinci-resolve-mcp/src/granular/gallery.py +0 -306
  184. package/mcps/davinci-resolve-mcp/src/granular/graph.py +0 -309
  185. package/mcps/davinci-resolve-mcp/src/granular/media_pool.py +0 -679
  186. package/mcps/davinci-resolve-mcp/src/granular/media_pool_item.py +0 -1020
  187. package/mcps/davinci-resolve-mcp/src/granular/media_storage.py +0 -179
  188. package/mcps/davinci-resolve-mcp/src/granular/project.py +0 -1654
  189. package/mcps/davinci-resolve-mcp/src/granular/resolve_control.py +0 -538
  190. package/mcps/davinci-resolve-mcp/src/granular/timeline.py +0 -1075
  191. package/mcps/davinci-resolve-mcp/src/granular/timeline_item.py +0 -2251
  192. package/mcps/davinci-resolve-mcp/src/resolve_mcp_server.py +0 -43
  193. package/mcps/davinci-resolve-mcp/src/server.py +0 -24116
  194. package/mcps/davinci-resolve-mcp/src/utils/__init__.py +0 -3
  195. package/mcps/davinci-resolve-mcp/src/utils/actor_identity.py +0 -46
  196. package/mcps/davinci-resolve-mcp/src/utils/analysis_caps.py +0 -678
  197. package/mcps/davinci-resolve-mcp/src/utils/analysis_memory.py +0 -755
  198. package/mcps/davinci-resolve-mcp/src/utils/analysis_runs.py +0 -307
  199. package/mcps/davinci-resolve-mcp/src/utils/analysis_store.py +0 -945
  200. package/mcps/davinci-resolve-mcp/src/utils/api_truth.py +0 -525
  201. package/mcps/davinci-resolve-mcp/src/utils/app_control.py +0 -319
  202. package/mcps/davinci-resolve-mcp/src/utils/audio_fairlight_live_probe.py +0 -263
  203. package/mcps/davinci-resolve-mcp/src/utils/brain_edits.py +0 -387
  204. package/mcps/davinci-resolve-mcp/src/utils/bridge_metrics.py +0 -76
  205. package/mcps/davinci-resolve-mcp/src/utils/cdl.py +0 -20
  206. package/mcps/davinci-resolve-mcp/src/utils/clip_query.py +0 -85
  207. package/mcps/davinci-resolve-mcp/src/utils/cloud_operations.py +0 -192
  208. package/mcps/davinci-resolve-mcp/src/utils/color_grade_live_probe.py +0 -444
  209. package/mcps/davinci-resolve-mcp/src/utils/contracts.py +0 -102
  210. package/mcps/davinci-resolve-mcp/src/utils/cut_ir.py +0 -115
  211. package/mcps/davinci-resolve-mcp/src/utils/dctl_templates.py +0 -368
  212. package/mcps/davinci-resolve-mcp/src/utils/deep_vision.py +0 -643
  213. package/mcps/davinci-resolve-mcp/src/utils/destructive_hook.py +0 -606
  214. package/mcps/davinci-resolve-mcp/src/utils/edit_engine.py +0 -737
  215. package/mcps/davinci-resolve-mcp/src/utils/embeddings.py +0 -922
  216. package/mcps/davinci-resolve-mcp/src/utils/entities.py +0 -579
  217. package/mcps/davinci-resolve-mcp/src/utils/extension_authoring_live_probe.py +0 -292
  218. package/mcps/davinci-resolve-mcp/src/utils/failure_tracker.py +0 -119
  219. package/mcps/davinci-resolve-mcp/src/utils/fuse_templates.py +0 -1968
  220. package/mcps/davinci-resolve-mcp/src/utils/fusion_composition_live_probe.py +0 -284
  221. package/mcps/davinci-resolve-mcp/src/utils/fusion_group_settings.py +0 -334
  222. package/mcps/davinci-resolve-mcp/src/utils/layout_presets.py +0 -333
  223. package/mcps/davinci-resolve-mcp/src/utils/mcp_stdio.py +0 -32
  224. package/mcps/davinci-resolve-mcp/src/utils/mcp_transport.py +0 -128
  225. package/mcps/davinci-resolve-mcp/src/utils/media_analysis.py +0 -7690
  226. package/mcps/davinci-resolve-mcp/src/utils/media_analysis_jobs.py +0 -805
  227. package/mcps/davinci-resolve-mcp/src/utils/media_pool_changes.py +0 -121
  228. package/mcps/davinci-resolve-mcp/src/utils/media_pool_ingest_live_probe.py +0 -592
  229. package/mcps/davinci-resolve-mcp/src/utils/multicam.py +0 -393
  230. package/mcps/davinci-resolve-mcp/src/utils/object_inspection.py +0 -301
  231. package/mcps/davinci-resolve-mcp/src/utils/page_lock.py +0 -75
  232. package/mcps/davinci-resolve-mcp/src/utils/platform.py +0 -157
  233. package/mcps/davinci-resolve-mcp/src/utils/proc.py +0 -26
  234. package/mcps/davinci-resolve-mcp/src/utils/project_cleanup.py +0 -73
  235. package/mcps/davinci-resolve-mcp/src/utils/project_lifecycle_live_probe.py +0 -376
  236. package/mcps/davinci-resolve-mcp/src/utils/project_lint.py +0 -131
  237. package/mcps/davinci-resolve-mcp/src/utils/project_properties.py +0 -601
  238. package/mcps/davinci-resolve-mcp/src/utils/project_spec.py +0 -459
  239. package/mcps/davinci-resolve-mcp/src/utils/readback.py +0 -91
  240. package/mcps/davinci-resolve-mcp/src/utils/render_deliver_live_probe.py +0 -384
  241. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_governance.py +0 -214
  242. package/mcps/davinci-resolve-mcp/src/utils/resolve_ai_ledger.py +0 -244
  243. package/mcps/davinci-resolve-mcp/src/utils/resolve_busy.py +0 -171
  244. package/mcps/davinci-resolve-mcp/src/utils/resolve_connection.py +0 -77
  245. package/mcps/davinci-resolve-mcp/src/utils/review_annotation_live_probe.py +0 -352
  246. package/mcps/davinci-resolve-mcp/src/utils/script_templates.py +0 -1193
  247. package/mcps/davinci-resolve-mcp/src/utils/shot_relationships.py +0 -544
  248. package/mcps/davinci-resolve-mcp/src/utils/structural_diff.py +0 -175
  249. package/mcps/davinci-resolve-mcp/src/utils/sync_detection.py +0 -889
  250. package/mcps/davinci-resolve-mcp/src/utils/timeline_brain_db.py +0 -826
  251. package/mcps/davinci-resolve-mcp/src/utils/timeline_conform_live_probe.py +0 -280
  252. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_live_probe.py +0 -1092
  253. package/mcps/davinci-resolve-mcp/src/utils/timeline_kernel_probe.py +0 -185
  254. package/mcps/davinci-resolve-mcp/src/utils/timeline_title_text.py +0 -87
  255. package/mcps/davinci-resolve-mcp/src/utils/timeline_versioning.py +0 -730
  256. package/mcps/davinci-resolve-mcp/src/utils/update_check.py +0 -685
  257. package/mcps/vectorworks-mcp/.dockerignore +0 -8
  258. package/mcps/vectorworks-mcp/Dockerfile +0 -28
  259. package/mcps/vectorworks-mcp/README.md +0 -156
  260. package/mcps/vectorworks-mcp/app/__init__.py +0 -2
  261. package/mcps/vectorworks-mcp/app/api.py +0 -52
  262. package/mcps/vectorworks-mcp/app/chunking.py +0 -56
  263. package/mcps/vectorworks-mcp/app/config.py +0 -29
  264. package/mcps/vectorworks-mcp/app/embeddings.py +0 -28
  265. package/mcps/vectorworks-mcp/app/indexer.py +0 -106
  266. package/mcps/vectorworks-mcp/app/main.py +0 -33
  267. package/mcps/vectorworks-mcp/app/mcp_server.py +0 -71
  268. package/mcps/vectorworks-mcp/app/search.py +0 -54
  269. package/mcps/vectorworks-mcp/app/utils.py +0 -14
  270. package/mcps/vectorworks-mcp/data/.gitkeep +0 -1
  271. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_PluginScript.htm +0 -95
  272. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_ScriptEditor.htm +0 -266
  273. package/mcps/vectorworks-mcp/data/vw-app-help/VW2022_Scripting.htm +0 -106
  274. package/mcps/vectorworks-mcp/data/vw-app-help/VW2023_CreatingScriptedPlugins.htm +0 -327
  275. package/mcps/vectorworks-mcp/data/vw-app-help/VW2024_RunningScripts.htm +0 -116
  276. package/mcps/vectorworks-mcp/data/vw-devwiki/VS_Function_Reference_category.html +0 -2129
  277. package/mcps/vectorworks-mcp/data/vw-jp-ref/ObjectsSymbols.html +0 -2035
  278. package/mcps/vectorworks-mcp/data/vw-jp-ref/ScriptFunctionReference.html +0 -29
  279. package/mcps/vectorworks-mcp/design.md +0 -57
  280. package/mcps/vectorworks-mcp/docker-compose.yml +0 -18
  281. package/mcps/vectorworks-mcp/index/.gitkeep +0 -1
  282. package/mcps/vectorworks-mcp/index/meta.jsonl +0 -3956
  283. package/mcps/vectorworks-mcp/index/vw.faiss +0 -0
  284. package/mcps/vectorworks-mcp/requirements.txt +0 -10
  285. package/mcps/vectorworks-mcp/scripts/fetch_docs_minimal.sh +0 -84
  286. package/mcps/vectorworks-mcp/scripts/fetch_github_vectorworks.sh +0 -85
  287. package/mcps/vectorworks-mcp/templates/index.html +0 -40
  288. package/scripts/ecosystem-health-audit.js +0 -257
  289. package/skills/bdb-dev-os-skill/SKILL.md +0 -49
  290. package/skills/bdb-dev-os-skill/package.json +0 -11
  291. package/skills/bdbsaastraining/SKILL.md +0 -316
  292. package/skills/bdbsaastraining/references/exam-pool.md +0 -313
  293. package/skills/bdbsaastraining/references/interaction-modes.md +0 -94
  294. package/skills/bdbsaastraining/references/track-a-ai-agent.md +0 -111
  295. package/skills/bdbsaastraining/references/track-b-wordpress.md +0 -111
  296. package/skills/bdbsaastraining/references/track-c-mailserver.md +0 -107
  297. package/skills/bdbsaastraining/references/track-d-debian.md +0 -109
  298. package/skills/bdbsaastraining/references/track-e-custom.md +0 -83
  299. package/skills/bdbsaastraining/scripts/build_profile.py +0 -529
  300. package/skills/bdbsaastraining/scripts/generate_certificate.py +0 -161
  301. package/skills/bdbsaastraining/scripts/preflight_check.sh +0 -150
  302. package/skills/bdbsaastraining/templates/certificate_template.html +0 -508
  303. package/skills/global_config/bdb-ecosystem-health/SKILL.md +0 -68
  304. package/skills/global_config/bdbsaas-ops/SKILL.md +0 -16
  305. 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
- }