@hybridlabor-api/aos 4.2.0 → 4.4.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 (1098) hide show
  1. package/.claude/hooks/memb-inject.mjs +121 -0
  2. package/.claude/settings.json +10 -0
  3. package/CLAUDE.md +37 -84
  4. package/CODEX.md +31 -12
  5. package/GEMINI.md +51 -58
  6. package/README.de.md +13 -14
  7. package/README.md +13 -14
  8. package/README.pt.md +13 -14
  9. package/THIRD_PARTY_NOTICES.md +54 -0
  10. package/docs/skills_table.md +0 -2
  11. package/installer.js +29 -16
  12. package/package.json +21 -2
  13. package/scripts/validate-skills.mjs +402 -0
  14. package/skills/basic/godmode-engineering/SKILL.md +1 -1
  15. package/skills/basic/godmode-shipping/SKILL.md +1 -1
  16. package/skills/github-repo/SKILL.md +1 -0
  17. package/skills/global_config/agent-tool-builder/SKILL.md +5 -4
  18. package/skills/global_config/ai-product/SKILL.md +3 -2
  19. package/skills/global_config/aos-project-init/SKILL.md +194 -0
  20. package/skills/global_config/aos-project-init/assets/AGENTS.template.md +56 -0
  21. package/skills/global_config/aos-project-init/scripts/aos-project-doctor.mjs +137 -0
  22. package/skills/global_config/aos-setup/SKILL.md +222 -0
  23. package/skills/global_config/aos-setup/scripts/aos-doctor.mjs +262 -0
  24. package/skills/global_config/ask-tim/SKILL.md +4 -2
  25. package/skills/global_config/{bdb-adobe-suite-mcp.md → bdb-adobe-suite-mcp/SKILL.md} +1 -0
  26. package/skills/global_config/{bdb-after-effects-mcp.md → bdb-after-effects-mcp/SKILL.md} +1 -0
  27. package/skills/global_config/{bdb-blender-mcp.md → bdb-blender-mcp/SKILL.md} +1 -0
  28. package/skills/global_config/{bdb-computer-use-mcp.md → bdb-computer-use-mcp/SKILL.md} +1 -0
  29. package/skills/global_config/{bdb-davinci-mcp.md → bdb-davinci-mcp/SKILL.md} +1 -0
  30. package/skills/global_config/bdb-ecosystem-health/SKILL.md +3 -3
  31. package/skills/global_config/{bdb-grandma3-mcp.md → bdb-grandma3-mcp/SKILL.md} +1 -0
  32. package/skills/global_config/{bdb-memb-mcp.md → bdb-memb-mcp/SKILL.md} +10 -0
  33. package/skills/global_config/{bdb-resolume-mcp.md → bdb-resolume-mcp/SKILL.md} +1 -0
  34. package/skills/global_config/{bdb-rhino-mcp.md → bdb-rhino-mcp/SKILL.md} +1 -0
  35. package/skills/global_config/{bdb-touchdesigner-mcp.md → bdb-touchdesigner-mcp/SKILL.md} +1 -0
  36. package/skills/global_config/{bdb-unreal-mcp.md → bdb-unreal-mcp/SKILL.md} +1 -0
  37. package/skills/global_config/{bdb-vectorworks-mcp.md → bdb-vectorworks-mcp/SKILL.md} +1 -0
  38. package/skills/global_config/bdbsaashost/SKILL.md +50 -50
  39. package/skills/global_config/browser-automation/SKILL.md +4 -4
  40. package/skills/global_config/crewai/SKILL.md +3 -2
  41. package/skills/global_config/debugger/SKILL.md +3 -6
  42. package/skills/global_config/git-advanced-workflows/SKILL.md +0 -1
  43. package/skills/global_config/github-actions-templates/SKILL.md +0 -2
  44. package/skills/global_config/neon-postgres/SKILL.md +3 -2
  45. package/skills/global_config/playwright-skill/SKILL.md +1 -1
  46. package/skills/global_config/posix-shell-pro/SKILL.md +0 -1
  47. package/skills/global_config/postgres-best-practices/SKILL.md +1 -1
  48. package/skills/global_config/prompt-engineering-patterns/SKILL.md +0 -1
  49. package/skills/global_config/rag-engineer/SKILL.md +4 -3
  50. package/skills/global_config/react-best-practices/SKILL.md +1 -1
  51. package/skills/global_config/remotion/SKILL.md +0 -1
  52. package/skills/global_config/seo/SKILL.md +6 -41
  53. package/skills/global_config/systematic-debugging/CREATION-LOG.md +1 -1
  54. package/skills/global_config/systematic-debugging/root-cause-tracing.md +1 -1
  55. package/skills/global_config/turborepo-caching/SKILL.md +0 -1
  56. package/skills/global_config/using-neon/SKILL.md +1 -47
  57. package/skills/global_config/vector-database-engineer/SKILL.md +0 -1
  58. package/skills/global_config/web-artifacts-builder/LICENSE.txt +1 -1
  59. package/skills/global_config/web-artifacts-builder/SKILL.md +1 -1
  60. package/skills/global_config/webapp-testing/LICENSE.txt +1 -1
  61. package/skills/global_config/webapp-testing/SKILL.md +1 -1
  62. package/assets/Give_AI_Agents_Control_of_Creative_Software.mp4 +0 -0
  63. package/mcps/RhinoMCP/connector/icon-dark.png +0 -3
  64. package/mcps/RhinoMCP/connector/icon-light.png +0 -3
  65. package/mcps/RhinoMCP/connector/screenshots/casa-rhino.png +0 -3
  66. package/mcps/RhinoMCP/docs/assets/css/custom.css +0 -640
  67. package/mcps/RhinoMCP/docs/content/_index.md +0 -88
  68. package/mcps/RhinoMCP/docs/content/docs/_index.md +0 -35
  69. package/mcps/RhinoMCP/docs/content/docs/advanced/_index.md +0 -37
  70. package/mcps/RhinoMCP/docs/content/docs/advanced/bulk-file-processing.md +0 -70
  71. package/mcps/RhinoMCP/docs/content/docs/advanced/headless-render-farm.md +0 -60
  72. package/mcps/RhinoMCP/docs/content/docs/advanced/make-this-parametric.md +0 -56
  73. package/mcps/RhinoMCP/docs/content/docs/advanced/sketch-to-model.md +0 -58
  74. package/mcps/RhinoMCP/docs/content/docs/advanced/upgrade-gh1-to-gh2.md +0 -51
  75. package/mcps/RhinoMCP/docs/content/docs/developers/_index.md +0 -39
  76. package/mcps/RhinoMCP/docs/content/docs/developers/dev-troubleshooting.md +0 -15
  77. package/mcps/RhinoMCP/docs/content/docs/developers/plugin-prototyping.md +0 -56
  78. package/mcps/RhinoMCP/docs/content/docs/developers/upgrade-gh1-to-gh2.md +0 -98
  79. package/mcps/RhinoMCP/docs/content/docs/developers/upgrade-to-netcore.md +0 -53
  80. package/mcps/RhinoMCP/docs/content/docs/developers/windows-to-cross-platform.md +0 -58
  81. package/mcps/RhinoMCP/docs/content/docs/developers/wpf-to-eto.md +0 -64
  82. package/mcps/RhinoMCP/docs/content/docs/getting-started/_index.md +0 -45
  83. package/mcps/RhinoMCP/docs/content/docs/getting-started/cc-plugin.md +0 -116
  84. package/mcps/RhinoMCP/docs/content/docs/getting-started/connector.md +0 -65
  85. package/mcps/RhinoMCP/docs/content/docs/getting-started/copilot.md +0 -61
  86. package/mcps/RhinoMCP/docs/content/docs/getting-started/lm-studio.md +0 -62
  87. package/mcps/RhinoMCP/docs/content/docs/troubleshooting.md +0 -66
  88. package/mcps/RhinoMCP/docs/content/docs/try-it-out/_index.md +0 -39
  89. package/mcps/RhinoMCP/docs/content/docs/try-it-out/examples/_index.md +0 -25
  90. package/mcps/RhinoMCP/docs/content/docs/try-it-out/examples/build-grasshopper.md +0 -26
  91. package/mcps/RhinoMCP/docs/content/docs/try-it-out/examples/generate-variations.md +0 -25
  92. package/mcps/RhinoMCP/docs/content/docs/try-it-out/examples/inspect-and-report.md +0 -26
  93. package/mcps/RhinoMCP/docs/content/docs/try-it-out/examples/layers.md +0 -28
  94. package/mcps/RhinoMCP/docs/content/docs/try-it-out/examples/model-from-image.md +0 -27
  95. package/mcps/RhinoMCP/docs/content/docs/try-it-out/recipes.md +0 -178
  96. package/mcps/RhinoMCP/docs/data/icons.yaml +0 -3
  97. package/mcps/RhinoMCP/docs/go.mod +0 -5
  98. package/mcps/RhinoMCP/docs/go.sum +0 -4
  99. package/mcps/RhinoMCP/docs/hugo.yaml +0 -78
  100. package/mcps/RhinoMCP/docs/layouts/_partials/sidebar.html +0 -333
  101. package/mcps/RhinoMCP/docs/layouts/docs/list.html +0 -29
  102. package/mcps/RhinoMCP/docs/layouts/docs/single.html +0 -29
  103. package/mcps/RhinoMCP/docs/layouts/partials/custom/head-end.html +0 -3
  104. package/mcps/RhinoMCP/docs/layouts/shortcodes/download-asset.html +0 -21
  105. package/mcps/RhinoMCP/docs/layouts/shortcodes/github.html +0 -21
  106. package/mcps/RhinoMCP/docs/layouts/shortcodes/prompt.html +0 -23
  107. package/mcps/RhinoMCP/docs/layouts/shortcodes/yak.html +0 -21
  108. package/mcps/RhinoMCP/docs/static/developer/sel-arc.png +0 -3
  109. package/mcps/RhinoMCP/docs/static/developer/voxelizer-gh2-conversion.png +0 -3
  110. package/mcps/RhinoMCP/docs/static/examples/ai-kettle.png +0 -3
  111. package/mcps/RhinoMCP/docs/static/examples/casa_rhino_shapes.png +0 -3
  112. package/mcps/RhinoMCP/docs/static/examples/kettle.jpg +0 -0
  113. package/mcps/RhinoMCP/docs/static/examples/raygun-layers.png +0 -3
  114. package/mcps/RhinoMCP/docs/static/examples/raygun-thumbnail.png +0 -3
  115. package/mcps/RhinoMCP/docs/static/examples/twisty-tower-gh.png +0 -3
  116. package/mcps/RhinoMCP/docs/static/favicon.svg +0 -26
  117. package/mcps/RhinoMCP/docs/static/images/landing-dark.png +0 -3
  118. package/mcps/RhinoMCP/docs/static/images/landing.png +0 -3
  119. package/mcps/RhinoMCP/docs/static/images/logo-dark.svg +0 -26
  120. package/mcps/RhinoMCP/docs/static/images/logo.svg +0 -26
  121. package/mcps/RhinoMCP/docs/static/local-llm.png +0 -3
  122. package/mcps/RhinoMCP/rhino/plugin/icon.png +0 -3
  123. package/mcps/RhinoMCP/tests/Acp.Tests/Acp.Tests.csproj +0 -29
  124. package/mcps/RhinoMCP/tests/Acp.Tests/CoverageTests.cs +0 -52
  125. package/mcps/RhinoMCP/tests/Acp.Tests/Fakes.cs +0 -90
  126. package/mcps/RhinoMCP/tests/Acp.Tests/GlobalUsings.cs +0 -1
  127. package/mcps/RhinoMCP/tests/Acp.Tests/LoopbackTests.cs +0 -59
  128. package/mcps/RhinoMCP/tests/Acp.Tests/RobustnessTests.cs +0 -124
  129. package/mcps/RhinoMCP/tests/Acp.Tests/SerializationTests.cs +0 -94
  130. package/mcps/RhinoMCP/tests/Integration.Tests/AdoptedSlotTests.cs +0 -136
  131. package/mcps/RhinoMCP/tests/Integration.Tests/AutoSpawnNotificationTests.cs +0 -63
  132. package/mcps/RhinoMCP/tests/Integration.Tests/CloseSlotAgentTests.cs +0 -56
  133. package/mcps/RhinoMCP/tests/Integration.Tests/CloseSlotTests.cs +0 -37
  134. package/mcps/RhinoMCP/tests/Integration.Tests/DocLifecycleTests.cs +0 -42
  135. package/mcps/RhinoMCP/tests/Integration.Tests/GlobalUsings.cs +0 -3
  136. package/mcps/RhinoMCP/tests/Integration.Tests/GrasshopperStartTests.cs +0 -56
  137. package/mcps/RhinoMCP/tests/Integration.Tests/Harness/ReturnResult.cs +0 -28
  138. package/mcps/RhinoMCP/tests/Integration.Tests/Harness/RhinoMcpFixture.cs +0 -90
  139. package/mcps/RhinoMCP/tests/Integration.Tests/Harness/RhinoMcpRouter.cs +0 -111
  140. package/mcps/RhinoMCP/tests/Integration.Tests/Harness/RhinoRouterPaths.cs +0 -83
  141. package/mcps/RhinoMCP/tests/Integration.Tests/Harness/RouterFixture.cs +0 -50
  142. package/mcps/RhinoMCP/tests/Integration.Tests/Integration.Tests.csproj +0 -31
  143. package/mcps/RhinoMCP/tests/Integration.Tests/ListSlotsShapeTests.cs +0 -55
  144. package/mcps/RhinoMCP/tests/Integration.Tests/ListSlotsTests.cs +0 -19
  145. package/mcps/RhinoMCP/tests/Integration.Tests/MacSharedProcessTests.cs +0 -87
  146. package/mcps/RhinoMCP/tests/Integration.Tests/MultiRouterTests.cs +0 -74
  147. package/mcps/RhinoMCP/tests/Integration.Tests/PluginToolTests.cs +0 -115
  148. package/mcps/RhinoMCP/tests/Integration.Tests/README.md +0 -35
  149. package/mcps/RhinoMCP/tests/Integration.Tests/RhinoCrashHandlingTests.cs +0 -107
  150. package/mcps/RhinoMCP/tests/Integration.Tests/SlotReapingTests.cs +0 -84
  151. package/mcps/RhinoMCP/tests/Integration.Tests/SpawnErrorTests.cs +0 -52
  152. package/mcps/RhinoMCP/tests/Integration.Tests/SpawnSlotTests.cs +0 -66
  153. package/mcps/RhinoMCP/tests/Integration.Tests/ToolDispatchBySlotTests.cs +0 -53
  154. package/mcps/RhinoMCP/tests/Integration.Tests/WindowsProcessIsolationTests.cs +0 -62
  155. package/mcps/RhinoMCP/tests/Router.Tests/CodegenTests.cs +0 -125
  156. package/mcps/RhinoMCP/tests/Router.Tests/ReturnResultTests.cs +0 -90
  157. package/mcps/RhinoMCP/tests/Router.Tests/RhinoCrashReportFinderTests.cs +0 -207
  158. package/mcps/RhinoMCP/tests/Router.Tests/RhinoLocatorOverrideTests.cs +0 -61
  159. package/mcps/RhinoMCP/tests/Router.Tests/RhinoLocatorTests.cs +0 -38
  160. package/mcps/RhinoMCP/tests/Router.Tests/RhinoManagerResolutionTests.cs +0 -151
  161. package/mcps/RhinoMCP/tests/Router.Tests/Router.Tests.csproj +0 -31
  162. package/mcps/RhinoMCP/tests/Router.Tests/RouterConfigTests.cs +0 -92
  163. package/mcps/RhinoMCP/tests/Router.Tests/SlotStoreLaunchingAbsenceTests.cs +0 -70
  164. package/mcps/RhinoMCP/tests/Router.Tests/SlotStorePortReservationTests.cs +0 -92
  165. package/mcps/RhinoMCP/tests/Router.Tests/SpawnDiagnosticsTests.cs +0 -100
  166. package/mcps/RhinoMCP/tests/Router.Tests/SpawnSlotToolDiagnoseTests.cs +0 -66
  167. package/mcps/RhinoMCP/tests/Server.Tests/McpConfigEditTests.cs +0 -223
  168. package/mcps/RhinoMCP/tests/Server.Tests/ParameterBinderTests.cs +0 -208
  169. package/mcps/RhinoMCP/tests/Server.Tests/ProtocolTests.cs +0 -200
  170. package/mcps/RhinoMCP/tests/Server.Tests/ResourceRegistryTests.cs +0 -218
  171. package/mcps/RhinoMCP/tests/Server.Tests/ResultUnwrapperTests.cs +0 -53
  172. package/mcps/RhinoMCP/tests/Server.Tests/SchemaBuilderTests.cs +0 -166
  173. package/mcps/RhinoMCP/tests/Server.Tests/Server.Tests.csproj +0 -40
  174. package/mcps/RhinoMCP/tests/StreamJson.Tests/AcpMessageMapperTests.cs +0 -82
  175. package/mcps/RhinoMCP/tests/StreamJson.Tests/AgentPromptsTests.cs +0 -56
  176. package/mcps/RhinoMCP/tests/StreamJson.Tests/AgentRegistryOverlayTests.cs +0 -88
  177. package/mcps/RhinoMCP/tests/StreamJson.Tests/AgentRegistryResolutionTests.cs +0 -152
  178. package/mcps/RhinoMCP/tests/StreamJson.Tests/AgentRunnerTurnTests.cs +0 -149
  179. package/mcps/RhinoMCP/tests/StreamJson.Tests/ClaudeStreamJsonParserTests.cs +0 -118
  180. package/mcps/RhinoMCP/tests/StreamJson.Tests/CodexStreamJsonParserTests.cs +0 -100
  181. package/mcps/RhinoMCP/tests/StreamJson.Tests/ConversationStorePruneTests.cs +0 -94
  182. package/mcps/RhinoMCP/tests/StreamJson.Tests/ConversationStoreRoundTripTests.cs +0 -70
  183. package/mcps/RhinoMCP/tests/StreamJson.Tests/ConversationTests.cs +0 -153
  184. package/mcps/RhinoMCP/tests/StreamJson.Tests/ConversationUsageTests.cs +0 -56
  185. package/mcps/RhinoMCP/tests/StreamJson.Tests/GlobalUsings.cs +0 -12
  186. package/mcps/RhinoMCP/tests/StreamJson.Tests/Shims.cs +0 -93
  187. package/mcps/RhinoMCP/tests/StreamJson.Tests/StreamJson.Tests.csproj +0 -56
  188. package/mcps/RhinoMCP/tests/StreamJson.Tests/StreamJsonAgentLoopbackTests.cs +0 -75
  189. package/mcps/RhinoMCP/tests/StreamJson.Tests/StreamJsonParserEdgeCaseTests.cs +0 -128
  190. package/mcps/RhinoMCP/tests/StreamJson.Tests/TokenUsageTests.cs +0 -40
  191. package/mcps/RhinoMCP/tests/launcher/launcher.test.mjs +0 -461
  192. package/mcps/RhinoMCP/tests/launcher/package.json +0 -8
  193. package/mcps/ae-mcp/docs/demo.png +0 -0
  194. package/mcps/ae-mcp/docs/demo1.png +0 -0
  195. package/mcps/ae-mcp/docs/demo3.png +0 -0
  196. package/mcps/ae-mcp/docs/video.png +0 -0
  197. package/mcps/after-effects-mcp/assets/MCP Auto Bridge.png +0 -0
  198. package/mcps/bdb-remoteos-mcp/tests/__init__.py +0 -1
  199. package/mcps/bdb-remoteos-mcp/tests/mock_incus.py +0 -60
  200. package/mcps/bdb-remoteos-mcp/tests/test_mcp_server.py +0 -27
  201. package/mcps/bdb-remoteos-mcp/tests/test_security_redteam.py +0 -349
  202. package/mcps/bdb-remoteos-mcp/tests/test_webhook.py +0 -61
  203. package/mcps/blender-mcp/assets/addon-instructions.png +0 -0
  204. package/mcps/blender-mcp/assets/hammer-icon.png +0 -0
  205. package/mcps/blender-mcp-server/docs/architecture.md +0 -209
  206. package/mcps/blender-mcp-server/docs/images/demo_render.png +0 -0
  207. package/mcps/blender-mcp-server/docs/python-execute-design.md +0 -515
  208. package/mcps/blender-mcp-server/tests/__init__.py +0 -0
  209. package/mcps/blender-mcp-server/tests/test_addon.py +0 -953
  210. package/mcps/blender-mcp-server/tests/test_server.py +0 -364
  211. package/mcps/davinci-mcp-professional/docs/README.md +0 -67
  212. package/mcps/davinci-mcp-professional/docs/html/README_8md.html +0 -113
  213. package/mcps/davinci-mcp-professional/docs/html/____init_____8py.html +0 -126
  214. package/mcps/davinci-mcp-professional/docs/html/____init_____8py.js +0 -6
  215. package/mcps/davinci-mcp-professional/docs/html/____init_____8py_source.html +0 -125
  216. package/mcps/davinci-mcp-professional/docs/html/__version_8py.html +0 -130
  217. package/mcps/davinci-mcp-professional/docs/html/__version_8py.js +0 -9
  218. package/mcps/davinci-mcp-professional/docs/html/__version_8py_source.html +0 -130
  219. package/mcps/davinci-mcp-professional/docs/html/annotated.html +0 -123
  220. package/mcps/davinci-mcp-professional/docs/html/annotated_dup.js +0 -21
  221. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveClient-members.html +0 -131
  222. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveClient.html +0 -706
  223. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveClient.js +0 -26
  224. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveConnectionError.html +0 -143
  225. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveConnectionError__coll__graph.map +0 -7
  226. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveConnectionError__coll__graph.md5 +0 -1
  227. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveConnectionError__coll__graph.png +0 -0
  228. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveConnectionError__inherit__graph.map +0 -7
  229. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveConnectionError__inherit__graph.md5 +0 -1
  230. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveConnectionError__inherit__graph.png +0 -0
  231. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveError.html +0 -143
  232. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveError__coll__graph.map +0 -5
  233. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveError__coll__graph.md5 +0 -1
  234. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveError__coll__graph.png +0 -0
  235. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveError__inherit__graph.map +0 -9
  236. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveError__inherit__graph.md5 +0 -1
  237. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveError__inherit__graph.png +0 -0
  238. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveNotRunningError.html +0 -143
  239. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveNotRunningError__coll__graph.map +0 -7
  240. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveNotRunningError__coll__graph.md5 +0 -1
  241. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveNotRunningError__coll__graph.png +0 -0
  242. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveNotRunningError__inherit__graph.map +0 -7
  243. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveNotRunningError__inherit__graph.md5 +0 -1
  244. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1resolve__client_1_1DaVinciResolveNotRunningError__inherit__graph.png +0 -0
  245. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1server_1_1DaVinciMCPServer-members.html +0 -115
  246. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1server_1_1DaVinciMCPServer.html +0 -325
  247. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1server_1_1DaVinciMCPServer.js +0 -10
  248. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciMediaPool-members.html +0 -110
  249. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciMediaPool.html +0 -188
  250. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciMediaPool.js +0 -5
  251. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciMediaPool__coll__graph.map +0 -5
  252. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciMediaPool__coll__graph.md5 +0 -1
  253. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciMediaPool__coll__graph.png +0 -0
  254. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciMediaPool__inherit__graph.map +0 -5
  255. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciMediaPool__inherit__graph.md5 +0 -1
  256. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciMediaPool__inherit__graph.png +0 -0
  257. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProject-members.html +0 -113
  258. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProject.html +0 -248
  259. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProject.js +0 -8
  260. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProjectManager-members.html +0 -113
  261. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProjectManager.html +0 -252
  262. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProjectManager.js +0 -8
  263. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProjectManager__coll__graph.map +0 -5
  264. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProjectManager__coll__graph.md5 +0 -1
  265. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProjectManager__coll__graph.png +0 -0
  266. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProjectManager__inherit__graph.map +0 -5
  267. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProjectManager__inherit__graph.md5 +0 -1
  268. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProjectManager__inherit__graph.png +0 -0
  269. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProject__coll__graph.map +0 -5
  270. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProject__coll__graph.md5 +0 -1
  271. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProject__coll__graph.png +0 -0
  272. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProject__inherit__graph.map +0 -5
  273. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProject__inherit__graph.md5 +0 -1
  274. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciProject__inherit__graph.png +0 -0
  275. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciResolveApp-members.html +0 -115
  276. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciResolveApp.html +0 -288
  277. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciResolveApp.js +0 -10
  278. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciResolveApp__coll__graph.map +0 -5
  279. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciResolveApp__coll__graph.md5 +0 -1
  280. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciResolveApp__coll__graph.png +0 -0
  281. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciResolveApp__inherit__graph.map +0 -5
  282. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciResolveApp__inherit__graph.md5 +0 -1
  283. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciResolveApp__inherit__graph.png +0 -0
  284. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciTimeline-members.html +0 -110
  285. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciTimeline.html +0 -188
  286. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciTimeline.js +0 -5
  287. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciTimeline__coll__graph.map +0 -5
  288. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciTimeline__coll__graph.md5 +0 -1
  289. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciTimeline__coll__graph.png +0 -0
  290. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciTimeline__inherit__graph.map +0 -5
  291. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciTimeline__inherit__graph.md5 +0 -1
  292. package/mcps/davinci-mcp-professional/docs/html/classdavinci__mcp_1_1types_1_1DaVinciTimeline__inherit__graph.png +0 -0
  293. package/mcps/davinci-mcp-professional/docs/html/classes.html +0 -111
  294. package/mcps/davinci-mcp-professional/docs/html/cli_8py.html +0 -135
  295. package/mcps/davinci-mcp-professional/docs/html/cli_8py.js +0 -11
  296. package/mcps/davinci-mcp-professional/docs/html/cli_8py_source.html +0 -249
  297. package/mcps/davinci-mcp-professional/docs/html/clipboard.js +0 -61
  298. package/mcps/davinci-mcp-professional/docs/html/cookie.js +0 -58
  299. package/mcps/davinci-mcp-professional/docs/html/dir_1a1069c9a5427270b3ccd00c87d405fe.html +0 -127
  300. package/mcps/davinci-mcp-professional/docs/html/dir_1a1069c9a5427270b3ccd00c87d405fe.js +0 -4
  301. package/mcps/davinci-mcp-professional/docs/html/dir_1a1069c9a5427270b3ccd00c87d405fe_dep.map +0 -4
  302. package/mcps/davinci-mcp-professional/docs/html/dir_1a1069c9a5427270b3ccd00c87d405fe_dep.md5 +0 -1
  303. package/mcps/davinci-mcp-professional/docs/html/dir_1a1069c9a5427270b3ccd00c87d405fe_dep.png +0 -0
  304. package/mcps/davinci-mcp-professional/docs/html/dir_6a5346f4427b2e9426ba2ccfd9f0b603.html +0 -128
  305. package/mcps/davinci-mcp-professional/docs/html/dir_6a5346f4427b2e9426ba2ccfd9f0b603.js +0 -5
  306. package/mcps/davinci-mcp-professional/docs/html/dir_6a5346f4427b2e9426ba2ccfd9f0b603_dep.map +0 -4
  307. package/mcps/davinci-mcp-professional/docs/html/dir_6a5346f4427b2e9426ba2ccfd9f0b603_dep.md5 +0 -1
  308. package/mcps/davinci-mcp-professional/docs/html/dir_6a5346f4427b2e9426ba2ccfd9f0b603_dep.png +0 -0
  309. package/mcps/davinci-mcp-professional/docs/html/dir_77c8a4d399bde190a30252eceec792ef.html +0 -127
  310. package/mcps/davinci-mcp-professional/docs/html/dir_77c8a4d399bde190a30252eceec792ef.js +0 -4
  311. package/mcps/davinci-mcp-professional/docs/html/dir_77c8a4d399bde190a30252eceec792ef_dep.map +0 -4
  312. package/mcps/davinci-mcp-professional/docs/html/dir_77c8a4d399bde190a30252eceec792ef_dep.md5 +0 -1
  313. package/mcps/davinci-mcp-professional/docs/html/dir_77c8a4d399bde190a30252eceec792ef_dep.png +0 -0
  314. package/mcps/davinci-mcp-professional/docs/html/dir_f94b29b73815c35a8351f4403bcc4f74.html +0 -129
  315. package/mcps/davinci-mcp-professional/docs/html/dir_f94b29b73815c35a8351f4403bcc4f74.js +0 -12
  316. package/mcps/davinci-mcp-professional/docs/html/doxygen.css +0 -2540
  317. package/mcps/davinci-mcp-professional/docs/html/doxygen.svg +0 -28
  318. package/mcps/davinci-mcp-professional/docs/html/doxygen_crawl.html +0 -170
  319. package/mcps/davinci-mcp-professional/docs/html/dynsections.js +0 -198
  320. package/mcps/davinci-mcp-professional/docs/html/files.html +0 -123
  321. package/mcps/davinci-mcp-professional/docs/html/files_dup.js +0 -4
  322. package/mcps/davinci-mcp-professional/docs/html/functions.html +0 -190
  323. package/mcps/davinci-mcp-professional/docs/html/functions_func.html +0 -184
  324. package/mcps/davinci-mcp-professional/docs/html/functions_vars.html +0 -110
  325. package/mcps/davinci-mcp-professional/docs/html/graph_legend.html +0 -165
  326. package/mcps/davinci-mcp-professional/docs/html/graph_legend.md5 +0 -1
  327. package/mcps/davinci-mcp-professional/docs/html/graph_legend.png +0 -0
  328. package/mcps/davinci-mcp-professional/docs/html/hierarchy.html +0 -123
  329. package/mcps/davinci-mcp-professional/docs/html/hierarchy.js +0 -18
  330. package/mcps/davinci-mcp-professional/docs/html/index.html +0 -200
  331. package/mcps/davinci-mcp-professional/docs/html/index.js +0 -9
  332. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_0.map +0 -3
  333. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_0.md5 +0 -1
  334. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_0.png +0 -0
  335. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_1.map +0 -3
  336. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_1.md5 +0 -1
  337. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_1.png +0 -0
  338. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_2.map +0 -9
  339. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_2.md5 +0 -1
  340. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_2.png +0 -0
  341. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_3.map +0 -13
  342. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_3.md5 +0 -1
  343. package/mcps/davinci-mcp-professional/docs/html/inherit_graph_3.png +0 -0
  344. package/mcps/davinci-mcp-professional/docs/html/inherits.html +0 -145
  345. package/mcps/davinci-mcp-professional/docs/html/jquery.js +0 -204
  346. package/mcps/davinci-mcp-professional/docs/html/menu.js +0 -131
  347. package/mcps/davinci-mcp-professional/docs/html/menudata.js +0 -73
  348. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp.html +0 -209
  349. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp.js +0 -34
  350. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1__version.html +0 -260
  351. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1cli.html +0 -303
  352. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1resolve__client.html +0 -147
  353. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1resolve__client.js +0 -8
  354. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1resources.html +0 -140
  355. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1server.html +0 -144
  356. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1server.js +0 -5
  357. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1tools.html +0 -140
  358. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1types.html +0 -216
  359. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1types.js +0 -13
  360. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1utils.html +0 -155
  361. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1utils.js +0 -11
  362. package/mcps/davinci-mcp-professional/docs/html/namespacedavinci__mcp_1_1utils_1_1platform.html +0 -232
  363. package/mcps/davinci-mcp-professional/docs/html/namespacemembers.html +0 -173
  364. package/mcps/davinci-mcp-professional/docs/html/namespacemembers_func.html +0 -114
  365. package/mcps/davinci-mcp-professional/docs/html/namespacemembers_vars.html +0 -121
  366. package/mcps/davinci-mcp-professional/docs/html/namespaces.html +0 -119
  367. package/mcps/davinci-mcp-professional/docs/html/namespaces_dup.js +0 -4
  368. package/mcps/davinci-mcp-professional/docs/html/navtree.css +0 -327
  369. package/mcps/davinci-mcp-professional/docs/html/navtree.js +0 -901
  370. package/mcps/davinci-mcp-professional/docs/html/navtreedata.js +0 -68
  371. package/mcps/davinci-mcp-professional/docs/html/navtreeindex0.js +0 -212
  372. package/mcps/davinci-mcp-professional/docs/html/platform_8py.html +0 -130
  373. package/mcps/davinci-mcp-professional/docs/html/platform_8py.js +0 -8
  374. package/mcps/davinci-mcp-professional/docs/html/platform_8py_source.html +0 -245
  375. package/mcps/davinci-mcp-professional/docs/html/resolve__client_8py.html +0 -132
  376. package/mcps/davinci-mcp-professional/docs/html/resolve__client_8py.js +0 -8
  377. package/mcps/davinci-mcp-professional/docs/html/resolve__client_8py_source.html +0 -501
  378. package/mcps/davinci-mcp-professional/docs/html/resources_2____init_____8py.html +0 -125
  379. package/mcps/davinci-mcp-professional/docs/html/resources_2____init_____8py.js +0 -4
  380. package/mcps/davinci-mcp-professional/docs/html/resources_2____init_____8py_source.html +0 -169
  381. package/mcps/davinci-mcp-professional/docs/html/search/all_0.js +0 -22
  382. package/mcps/davinci-mcp-professional/docs/html/search/all_1.js +0 -4
  383. package/mcps/davinci-mcp-professional/docs/html/search/all_10.js +0 -4
  384. package/mcps/davinci-mcp-professional/docs/html/search/all_11.js +0 -4
  385. package/mcps/davinci-mcp-professional/docs/html/search/all_12.js +0 -4
  386. package/mcps/davinci-mcp-professional/docs/html/search/all_13.js +0 -4
  387. package/mcps/davinci-mcp-professional/docs/html/search/all_2.js +0 -4
  388. package/mcps/davinci-mcp-professional/docs/html/search/all_3.js +0 -15
  389. package/mcps/davinci-mcp-professional/docs/html/search/all_4.js +0 -26
  390. package/mcps/davinci-mcp-professional/docs/html/search/all_5.js +0 -4
  391. package/mcps/davinci-mcp-professional/docs/html/search/all_6.js +0 -6
  392. package/mcps/davinci-mcp-professional/docs/html/search/all_7.js +0 -25
  393. package/mcps/davinci-mcp-professional/docs/html/search/all_8.js +0 -7
  394. package/mcps/davinci-mcp-professional/docs/html/search/all_9.js +0 -9
  395. package/mcps/davinci-mcp-professional/docs/html/search/all_a.js +0 -6
  396. package/mcps/davinci-mcp-professional/docs/html/search/all_b.js +0 -6
  397. package/mcps/davinci-mcp-professional/docs/html/search/all_c.js +0 -9
  398. package/mcps/davinci-mcp-professional/docs/html/search/all_d.js +0 -10
  399. package/mcps/davinci-mcp-professional/docs/html/search/all_e.js +0 -11
  400. package/mcps/davinci-mcp-professional/docs/html/search/all_f.js +0 -5
  401. package/mcps/davinci-mcp-professional/docs/html/search/classes_0.js +0 -13
  402. package/mcps/davinci-mcp-professional/docs/html/search/files_0.js +0 -5
  403. package/mcps/davinci-mcp-professional/docs/html/search/files_1.js +0 -4
  404. package/mcps/davinci-mcp-professional/docs/html/search/files_2.js +0 -4
  405. package/mcps/davinci-mcp-professional/docs/html/search/files_3.js +0 -5
  406. package/mcps/davinci-mcp-professional/docs/html/search/files_4.js +0 -4
  407. package/mcps/davinci-mcp-professional/docs/html/search/files_5.js +0 -4
  408. package/mcps/davinci-mcp-professional/docs/html/search/functions_0.js +0 -9
  409. package/mcps/davinci-mcp-professional/docs/html/search/functions_1.js +0 -4
  410. package/mcps/davinci-mcp-professional/docs/html/search/functions_2.js +0 -10
  411. package/mcps/davinci-mcp-professional/docs/html/search/functions_3.js +0 -4
  412. package/mcps/davinci-mcp-professional/docs/html/search/functions_4.js +0 -25
  413. package/mcps/davinci-mcp-professional/docs/html/search/functions_5.js +0 -6
  414. package/mcps/davinci-mcp-professional/docs/html/search/functions_6.js +0 -7
  415. package/mcps/davinci-mcp-professional/docs/html/search/functions_7.js +0 -4
  416. package/mcps/davinci-mcp-professional/docs/html/search/functions_8.js +0 -5
  417. package/mcps/davinci-mcp-professional/docs/html/search/functions_9.js +0 -4
  418. package/mcps/davinci-mcp-professional/docs/html/search/functions_a.js +0 -4
  419. package/mcps/davinci-mcp-professional/docs/html/search/functions_b.js +0 -6
  420. package/mcps/davinci-mcp-professional/docs/html/search/namespaces_0.js +0 -13
  421. package/mcps/davinci-mcp-professional/docs/html/search/pages_0.js +0 -4
  422. package/mcps/davinci-mcp-professional/docs/html/search/pages_1.js +0 -6
  423. package/mcps/davinci-mcp-professional/docs/html/search/pages_2.js +0 -5
  424. package/mcps/davinci-mcp-professional/docs/html/search/pages_3.js +0 -4
  425. package/mcps/davinci-mcp-professional/docs/html/search/pages_4.js +0 -5
  426. package/mcps/davinci-mcp-professional/docs/html/search/pages_5.js +0 -4
  427. package/mcps/davinci-mcp-professional/docs/html/search/pages_6.js +0 -4
  428. package/mcps/davinci-mcp-professional/docs/html/search/pages_7.js +0 -4
  429. package/mcps/davinci-mcp-professional/docs/html/search/pages_8.js +0 -5
  430. package/mcps/davinci-mcp-professional/docs/html/search/pages_9.js +0 -5
  431. package/mcps/davinci-mcp-professional/docs/html/search/pages_a.js +0 -6
  432. package/mcps/davinci-mcp-professional/docs/html/search/pages_b.js +0 -4
  433. package/mcps/davinci-mcp-professional/docs/html/search/pages_c.js +0 -4
  434. package/mcps/davinci-mcp-professional/docs/html/search/pages_d.js +0 -4
  435. package/mcps/davinci-mcp-professional/docs/html/search/search.css +0 -377
  436. package/mcps/davinci-mcp-professional/docs/html/search/search.js +0 -708
  437. package/mcps/davinci-mcp-professional/docs/html/search/searchdata.js +0 -33
  438. package/mcps/davinci-mcp-professional/docs/html/search/variables_0.js +0 -14
  439. package/mcps/davinci-mcp-professional/docs/html/search/variables_1.js +0 -4
  440. package/mcps/davinci-mcp-professional/docs/html/search/variables_2.js +0 -4
  441. package/mcps/davinci-mcp-professional/docs/html/search/variables_3.js +0 -5
  442. package/mcps/davinci-mcp-professional/docs/html/search/variables_4.js +0 -4
  443. package/mcps/davinci-mcp-professional/docs/html/search/variables_5.js +0 -5
  444. package/mcps/davinci-mcp-professional/docs/html/search/variables_6.js +0 -5
  445. package/mcps/davinci-mcp-professional/docs/html/search/variables_7.js +0 -4
  446. package/mcps/davinci-mcp-professional/docs/html/search/variables_8.js +0 -4
  447. package/mcps/davinci-mcp-professional/docs/html/search/variables_9.js +0 -4
  448. package/mcps/davinci-mcp-professional/docs/html/server_8py.html +0 -129
  449. package/mcps/davinci-mcp-professional/docs/html/server_8py.js +0 -5
  450. package/mcps/davinci-mcp-professional/docs/html/server_8py_source.html +0 -318
  451. package/mcps/davinci-mcp-professional/docs/html/tabs.css +0 -1
  452. package/mcps/davinci-mcp-professional/docs/html/tools_2____init_____8py.html +0 -125
  453. package/mcps/davinci-mcp-professional/docs/html/tools_2____init_____8py.js +0 -4
  454. package/mcps/davinci-mcp-professional/docs/html/tools_2____init_____8py_source.html +0 -253
  455. package/mcps/davinci-mcp-professional/docs/html/types_8py.html +0 -137
  456. package/mcps/davinci-mcp-professional/docs/html/types_8py.js +0 -13
  457. package/mcps/davinci-mcp-professional/docs/html/types_8py_source.html +0 -309
  458. package/mcps/davinci-mcp-professional/docs/html/utils_2____init_____8py.html +0 -125
  459. package/mcps/davinci-mcp-professional/docs/html/utils_2____init_____8py.js +0 -4
  460. package/mcps/davinci-mcp-professional/docs/html/utils_2____init_____8py_source.html +0 -123
  461. package/mcps/davinci-mcp-professional/tests/README.md +0 -49
  462. package/mcps/davinci-mcp-professional/tests/__init__.py +0 -1
  463. package/mcps/davinci-mcp-professional/tests/conftest.py +0 -75
  464. package/mcps/davinci-mcp-professional/tests/test_security.py +0 -251
  465. package/mcps/davinci-resolve-mcp/docs/README.md +0 -56
  466. package/mcps/davinci-resolve-mcp/docs/SKILL.md +0 -1635
  467. package/mcps/davinci-resolve-mcp/docs/authoring/fuse-dctl-authoring.md +0 -242
  468. package/mcps/davinci-resolve-mcp/docs/authoring/script-plugin-authoring.md +0 -195
  469. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/README.md +0 -152
  470. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/references/category-patterns.md +0 -780
  471. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/references/controls.md +0 -259
  472. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/references/format.md +0 -568
  473. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/references/gotchas.md +0 -168
  474. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/references/thumbnails.md +0 -106
  475. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Blur.setting +0 -46
  476. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Color Tint.setting +0 -78
  477. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Cross Dissolve.setting +0 -57
  478. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Fade Title.setting +0 -111
  479. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Glow.setting +0 -55
  480. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Gradient.setting +0 -52
  481. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Iris.setting +0 -127
  482. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Lower Third.setting +0 -192
  483. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Noise.setting +0 -74
  484. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Solid Color.setting +0 -55
  485. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Title.setting +0 -105
  486. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Vignette.setting +0 -98
  487. package/mcps/davinci-resolve-mcp/docs/authoring/setting-files/templates/MCP Test Wipe.setting +0 -98
  488. package/mcps/davinci-resolve-mcp/docs/contributing.md +0 -82
  489. package/mcps/davinci-resolve-mcp/docs/guides/color-decision-guide.md +0 -427
  490. package/mcps/davinci-resolve-mcp/docs/guides/control-panel.md +0 -234
  491. package/mcps/davinci-resolve-mcp/docs/guides/editorial-decision-guide.md +0 -165
  492. package/mcps/davinci-resolve-mcp/docs/guides/media-analysis-guide.md +0 -763
  493. package/mcps/davinci-resolve-mcp/docs/guides/multicam-setup-guide.md +0 -138
  494. package/mcps/davinci-resolve-mcp/docs/images/control-panel/01-overview.png +0 -0
  495. package/mcps/davinci-resolve-mcp/docs/images/control-panel/02-review-bin-grid.png +0 -0
  496. package/mcps/davinci-resolve-mcp/docs/images/control-panel/03-clip-detail.png +0 -0
  497. package/mcps/davinci-resolve-mcp/docs/images/control-panel/04-transcript.png +0 -0
  498. package/mcps/davinci-resolve-mcp/docs/images/control-panel/05-shot-detail.png +0 -0
  499. package/mcps/davinci-resolve-mcp/docs/images/control-panel/06-inventory.png +0 -0
  500. package/mcps/davinci-resolve-mcp/docs/images/control-panel/07-diagnostics-resolve.png +0 -0
  501. package/mcps/davinci-resolve-mcp/docs/images/control-panel/08-preferences-analysis.png +0 -0
  502. package/mcps/davinci-resolve-mcp/docs/images/control-panel/09-ai-console.png +0 -0
  503. package/mcps/davinci-resolve-mcp/docs/images/control-panel/10-history.png +0 -0
  504. package/mcps/davinci-resolve-mcp/docs/images/control-panel/11-edit-plans.png +0 -0
  505. package/mcps/davinci-resolve-mcp/docs/install.md +0 -208
  506. package/mcps/davinci-resolve-mcp/docs/integrations/workflow-integrations.md +0 -120
  507. package/mcps/davinci-resolve-mcp/docs/kernels/README.md +0 -28
  508. package/mcps/davinci-resolve-mcp/docs/kernels/audio-fairlight-kernel.md +0 -86
  509. package/mcps/davinci-resolve-mcp/docs/kernels/color-grade-kernel.md +0 -103
  510. package/mcps/davinci-resolve-mcp/docs/kernels/extension-authoring-kernel.md +0 -101
  511. package/mcps/davinci-resolve-mcp/docs/kernels/fusion-composition-kernel.md +0 -121
  512. package/mcps/davinci-resolve-mcp/docs/kernels/media-pool-ingest-kernel.md +0 -147
  513. package/mcps/davinci-resolve-mcp/docs/kernels/project-lifecycle-kernel.md +0 -120
  514. package/mcps/davinci-resolve-mcp/docs/kernels/render-deliver-kernel.md +0 -92
  515. package/mcps/davinci-resolve-mcp/docs/kernels/review-annotation-kernel.md +0 -110
  516. package/mcps/davinci-resolve-mcp/docs/kernels/timeline-conform-interchange-kernel.md +0 -102
  517. package/mcps/davinci-resolve-mcp/docs/kernels/timeline-edit-kernel.md +0 -189
  518. package/mcps/davinci-resolve-mcp/docs/notes/codec-plugin-notes.md +0 -136
  519. package/mcps/davinci-resolve-mcp/docs/notes/dctl-notes.md +0 -234
  520. package/mcps/davinci-resolve-mcp/docs/notes/fusion-template-notes.md +0 -136
  521. package/mcps/davinci-resolve-mcp/docs/notes/lut-notes.md +0 -136
  522. package/mcps/davinci-resolve-mcp/docs/notes/openfx-notes.md +0 -120
  523. package/mcps/davinci-resolve-mcp/docs/process/release-process.md +0 -180
  524. package/mcps/davinci-resolve-mcp/docs/reference/api-coverage.md +0 -521
  525. package/mcps/davinci-resolve-mcp/docs/reference/api-limitations.md +0 -259
  526. package/mcps/davinci-resolve-mcp/docs/reference/readwrite-symmetry.md +0 -25
  527. package/mcps/davinci-resolve-mcp/docs/reference/resolve_scripting_api.txt +0 -1024
  528. package/mcps/davinci-resolve-mcp/tests/_error_envelope_helpers.py +0 -35
  529. package/mcps/davinci-resolve-mcp/tests/benchmark_server.py +0 -270
  530. package/mcps/davinci-resolve-mcp/tests/create_test_timeline.py +0 -177
  531. package/mcps/davinci-resolve-mcp/tests/live_api_gap_verification.py +0 -274
  532. package/mcps/davinci-resolve-mcp/tests/live_audio_fairlight_validation.py +0 -77
  533. package/mcps/davinci-resolve-mcp/tests/live_auto_sync_settings_check.py +0 -76
  534. package/mcps/davinci-resolve-mcp/tests/live_color_grade_validation.py +0 -89
  535. package/mcps/davinci-resolve-mcp/tests/live_delete_governance_check.py +0 -71
  536. package/mcps/davinci-resolve-mcp/tests/live_duplicate_clips_validation.py +0 -831
  537. package/mcps/davinci-resolve-mcp/tests/live_edit_engine_validation.py +0 -277
  538. package/mcps/davinci-resolve-mcp/tests/live_enum_settings_check.py +0 -89
  539. package/mcps/davinci-resolve-mcp/tests/live_extension_authoring_validation.py +0 -77
  540. package/mcps/davinci-resolve-mcp/tests/live_fuse_diagnose.py +0 -230
  541. package/mcps/davinci-resolve-mcp/tests/live_fuse_smoke.py +0 -250
  542. package/mcps/davinci-resolve-mcp/tests/live_fusion_composition_validation.py +0 -82
  543. package/mcps/davinci-resolve-mcp/tests/live_fusion_keyframe_validation.py +0 -116
  544. package/mcps/davinci-resolve-mcp/tests/live_fusion_mask_title_validation.py +0 -286
  545. package/mcps/davinci-resolve-mcp/tests/live_marker_validation.py +0 -439
  546. package/mcps/davinci-resolve-mcp/tests/live_media_pool_ingest_validation.py +0 -98
  547. package/mcps/davinci-resolve-mcp/tests/live_metadata_field_inventory_validation.py +0 -379
  548. package/mcps/davinci-resolve-mcp/tests/live_metadata_publish_validation.py +0 -192
  549. package/mcps/davinci-resolve-mcp/tests/live_multicam_setup_validation.py +0 -198
  550. package/mcps/davinci-resolve-mcp/tests/live_path_probe.py +0 -119
  551. package/mcps/davinci-resolve-mcp/tests/live_project_lifecycle_validation.py +0 -77
  552. package/mcps/davinci-resolve-mcp/tests/live_render_deliver_validation.py +0 -82
  553. package/mcps/davinci-resolve-mcp/tests/live_review_annotation_validation.py +0 -91
  554. package/mcps/davinci-resolve-mcp/tests/live_script_smoke.py +0 -141
  555. package/mcps/davinci-resolve-mcp/tests/live_sync_event_validation.py +0 -165
  556. package/mcps/davinci-resolve-mcp/tests/live_timeline_conform_validation.py +0 -77
  557. package/mcps/davinci-resolve-mcp/tests/live_v233_validation.py +0 -282
  558. package/mcps/davinci-resolve-mcp/tests/test-after-restart.bat +0 -83
  559. package/mcps/davinci-resolve-mcp/tests/test-after-restart.sh +0 -87
  560. package/mcps/davinci-resolve-mcp/tests/test_action_help.py +0 -47
  561. package/mcps/davinci-resolve-mcp/tests/test_action_list_drift.py +0 -163
  562. package/mcps/davinci-resolve-mcp/tests/test_ai_console_dispatch.py +0 -91
  563. package/mcps/davinci-resolve-mcp/tests/test_analysis_caps.py +0 -210
  564. package/mcps/davinci-resolve-mcp/tests/test_analysis_runs.py +0 -157
  565. package/mcps/davinci-resolve-mcp/tests/test_analysis_store.py +0 -366
  566. package/mcps/davinci-resolve-mcp/tests/test_api_limitations_doc.py +0 -56
  567. package/mcps/davinci-resolve-mcp/tests/test_api_truth.py +0 -64
  568. package/mcps/davinci-resolve-mcp/tests/test_api_truth_mitigations.py +0 -45
  569. package/mcps/davinci-resolve-mcp/tests/test_append_clip_infos_result_handling.py +0 -679
  570. package/mcps/davinci-resolve-mcp/tests/test_audio_fairlight_probe.py +0 -216
  571. package/mcps/davinci-resolve-mcp/tests/test_auto_run.py +0 -76
  572. package/mcps/davinci-resolve-mcp/tests/test_auto_sync_audio.py +0 -171
  573. package/mcps/davinci-resolve-mcp/tests/test_batch_cli.py +0 -253
  574. package/mcps/davinci-resolve-mcp/tests/test_brain_edits.py +0 -218
  575. package/mcps/davinci-resolve-mcp/tests/test_bridge_metrics.py +0 -52
  576. package/mcps/davinci-resolve-mcp/tests/test_bulk_match_to_hero.py +0 -219
  577. package/mcps/davinci-resolve-mcp/tests/test_caps_events_and_history.py +0 -147
  578. package/mcps/davinci-resolve-mcp/tests/test_caps_integration.py +0 -315
  579. package/mcps/davinci-resolve-mcp/tests/test_cdl_and_install_config.py +0 -372
  580. package/mcps/davinci-resolve-mcp/tests/test_clip_query.py +0 -57
  581. package/mcps/davinci-resolve-mcp/tests/test_cloud_settings.py +0 -62
  582. package/mcps/davinci-resolve-mcp/tests/test_color_grade_probe.py +0 -230
  583. package/mcps/davinci-resolve-mcp/tests/test_confirm_token.py +0 -85
  584. package/mcps/davinci-resolve-mcp/tests/test_confirm_token_preferences.py +0 -22
  585. package/mcps/davinci-resolve-mcp/tests/test_contracts.py +0 -81
  586. package/mcps/davinci-resolve-mcp/tests/test_create_subtitles_settings.py +0 -111
  587. package/mcps/davinci-resolve-mcp/tests/test_custom_timeline.py +0 -137
  588. package/mcps/davinci-resolve-mcp/tests/test_cut_executor.py +0 -87
  589. package/mcps/davinci-resolve-mcp/tests/test_cut_ir.py +0 -82
  590. package/mcps/davinci-resolve-mcp/tests/test_dashboard_transport.py +0 -47
  591. package/mcps/davinci-resolve-mcp/tests/test_deep_vision.py +0 -311
  592. package/mcps/davinci-resolve-mcp/tests/test_description_xml_shape.py +0 -71
  593. package/mcps/davinci-resolve-mcp/tests/test_destructive_decorator_coverage.py +0 -92
  594. package/mcps/davinci-resolve-mcp/tests/test_destructive_hook.py +0 -245
  595. package/mcps/davinci-resolve-mcp/tests/test_destructive_registry_drift.py +0 -108
  596. package/mcps/davinci-resolve-mcp/tests/test_edit_engine.py +0 -574
  597. package/mcps/davinci-resolve-mcp/tests/test_embeddings.py +0 -302
  598. package/mcps/davinci-resolve-mcp/tests/test_entities.py +0 -225
  599. package/mcps/davinci-resolve-mcp/tests/test_error_envelope.py +0 -179
  600. package/mcps/davinci-resolve-mcp/tests/test_exhaustive_wave_a.py +0 -101
  601. package/mcps/davinci-resolve-mcp/tests/test_extension_authoring_probe.py +0 -77
  602. package/mcps/davinci-resolve-mcp/tests/test_extract_frames.py +0 -42
  603. package/mcps/davinci-resolve-mcp/tests/test_extract_source_frame_ranges.py +0 -129
  604. package/mcps/davinci-resolve-mcp/tests/test_failure_tracker.py +0 -103
  605. package/mcps/davinci-resolve-mcp/tests/test_fuse_dctl_authoring.py +0 -596
  606. package/mcps/davinci-resolve-mcp/tests/test_fusion_comp_targeting.py +0 -269
  607. package/mcps/davinci-resolve-mcp/tests/test_fusion_composition_probe.py +0 -156
  608. package/mcps/davinci-resolve-mcp/tests/test_fusion_group_settings.py +0 -284
  609. package/mcps/davinci-resolve-mcp/tests/test_fusion_mask_title.py +0 -205
  610. package/mcps/davinci-resolve-mcp/tests/test_fusion_node_ops.py +0 -212
  611. package/mcps/davinci-resolve-mcp/tests/test_governance_identity.py +0 -155
  612. package/mcps/davinci-resolve-mcp/tests/test_grade_evidence_base.py +0 -132
  613. package/mcps/davinci-resolve-mcp/tests/test_import.py +0 -153
  614. package/mcps/davinci-resolve-mcp/tests/test_improvements.py +0 -334
  615. package/mcps/davinci-resolve-mcp/tests/test_inventory_exclude_bins.py +0 -139
  616. package/mcps/davinci-resolve-mcp/tests/test_live_api.py +0 -315
  617. package/mcps/davinci-resolve-mcp/tests/test_marker_params.py +0 -234
  618. package/mcps/davinci-resolve-mcp/tests/test_mcp_prompts.py +0 -77
  619. package/mcps/davinci-resolve-mcp/tests/test_mcp_resources.py +0 -117
  620. package/mcps/davinci-resolve-mcp/tests/test_mcp_transport.py +0 -74
  621. package/mcps/davinci-resolve-mcp/tests/test_media_analysis.py +0 -4637
  622. package/mcps/davinci-resolve-mcp/tests/test_media_pool_append_verification.py +0 -153
  623. package/mcps/davinci-resolve-mcp/tests/test_media_pool_changes.py +0 -105
  624. package/mcps/davinci-resolve-mcp/tests/test_media_pool_delete_governance.py +0 -69
  625. package/mcps/davinci-resolve-mcp/tests/test_media_pool_ingest_probe.py +0 -546
  626. package/mcps/davinci-resolve-mcp/tests/test_media_pool_walk.py +0 -79
  627. package/mcps/davinci-resolve-mcp/tests/test_multicam_setup.py +0 -178
  628. package/mcps/davinci-resolve-mcp/tests/test_object_inspection.py +0 -140
  629. package/mcps/davinci-resolve-mcp/tests/test_open_control_panel.py +0 -172
  630. package/mcps/davinci-resolve-mcp/tests/test_p1_param_validation.py +0 -57
  631. package/mcps/davinci-resolve-mcp/tests/test_p1_whitelist_and_delete.py +0 -81
  632. package/mcps/davinci-resolve-mcp/tests/test_page_lock.py +0 -52
  633. package/mcps/davinci-resolve-mcp/tests/test_panel_docs_drift.py +0 -77
  634. package/mcps/davinci-resolve-mcp/tests/test_partial_success_manifest.py +0 -113
  635. package/mcps/davinci-resolve-mcp/tests/test_persistence_safety.py +0 -147
  636. package/mcps/davinci-resolve-mcp/tests/test_prefer_handle.py +0 -89
  637. package/mcps/davinci-resolve-mcp/tests/test_project_cleanup.py +0 -111
  638. package/mcps/davinci-resolve-mcp/tests/test_project_lifecycle_probe.py +0 -256
  639. package/mcps/davinci-resolve-mcp/tests/test_project_lint.py +0 -79
  640. package/mcps/davinci-resolve-mcp/tests/test_project_spec.py +0 -246
  641. package/mcps/davinci-resolve-mcp/tests/test_propose_grade.py +0 -116
  642. package/mcps/davinci-resolve-mcp/tests/test_provenance.py +0 -88
  643. package/mcps/davinci-resolve-mcp/tests/test_readback.py +0 -77
  644. package/mcps/davinci-resolve-mcp/tests/test_readwrite_audit.py +0 -43
  645. package/mcps/davinci-resolve-mcp/tests/test_reel_name_writeback.py +0 -149
  646. package/mcps/davinci-resolve-mcp/tests/test_render_deliver_probe.py +0 -250
  647. package/mcps/davinci-resolve-mcp/tests/test_resolve20_api.py +0 -282
  648. package/mcps/davinci-resolve-mcp/tests/test_resolve21_actions.py +0 -386
  649. package/mcps/davinci-resolve-mcp/tests/test_resolve_ai_governance.py +0 -92
  650. package/mcps/davinci-resolve-mcp/tests/test_resolve_ai_ledger.py +0 -89
  651. package/mcps/davinci-resolve-mcp/tests/test_resolve_busy.py +0 -134
  652. package/mcps/davinci-resolve-mcp/tests/test_review_annotation_probe.py +0 -162
  653. package/mcps/davinci-resolve-mcp/tests/test_sampling_modes.py +0 -293
  654. package/mcps/davinci-resolve-mcp/tests/test_script_plugin.py +0 -750
  655. package/mcps/davinci-resolve-mcp/tests/test_shot_relationships.py +0 -254
  656. package/mcps/davinci-resolve-mcp/tests/test_spec_lint_wiring.py +0 -311
  657. package/mcps/davinci-resolve-mcp/tests/test_static_undefined_names.py +0 -45
  658. package/mcps/davinci-resolve-mcp/tests/test_structural_diff.py +0 -67
  659. package/mcps/davinci-resolve-mcp/tests/test_sync_detection.py +0 -213
  660. package/mcps/davinci-resolve-mcp/tests/test_thumbnail_image.py +0 -55
  661. package/mcps/davinci-resolve-mcp/tests/test_timeline_brain_db.py +0 -230
  662. package/mcps/davinci-resolve-mcp/tests/test_timeline_conform_probe.py +0 -229
  663. package/mcps/davinci-resolve-mcp/tests/test_timeline_editorial_helpers.py +0 -88
  664. package/mcps/davinci-resolve-mcp/tests/test_timeline_export_enum.py +0 -59
  665. package/mcps/davinci-resolve-mcp/tests/test_timeline_kernel_probe.py +0 -70
  666. package/mcps/davinci-resolve-mcp/tests/test_timeline_title_text_utils.py +0 -64
  667. package/mcps/davinci-resolve-mcp/tests/test_timeline_versioning.py +0 -419
  668. package/mcps/davinci-resolve-mcp/tests/test_tool_choice_hint.py +0 -42
  669. package/mcps/davinci-resolve-mcp/tests/test_tool_exposure.py +0 -159
  670. package/mcps/davinci-resolve-mcp/tests/test_tool_registration.py +0 -91
  671. package/mcps/davinci-resolve-mcp/tests/test_transcript_read.py +0 -74
  672. package/mcps/davinci-resolve-mcp/tests/test_update_check.py +0 -171
  673. package/mcps/davinci-resolve-mcp/tests/test_update_hardening.py +0 -171
  674. package/mcps/davinci-resolve-mcp/tests/test_update_history.py +0 -116
  675. package/mcps/davinci-resolve-mcp/tests/test_v232_helpers.py +0 -265
  676. package/mcps/davinci-resolve-mcp/tests/test_v233_helpers.py +0 -307
  677. package/mcps/davinci-resolve-mcp/tests/test_validation_guards.py +0 -108
  678. package/mcps/davinci-resolve-mcp/tests/test_verification_stats.py +0 -32
  679. package/mcps/davinci-resolve-mcp/tests/test_waveb_p2_p3.py +0 -65
  680. package/mcps/golem-rhino-mcp/Nexus-AI-Agents.png +0 -0
  681. package/mcps/golem-rhino-mcp/docs/ARCHITECTURE.md +0 -277
  682. package/mcps/golem-rhino-mcp/docs/PROTOCOL.md +0 -247
  683. package/mcps/golem-rhino-mcp/docs/TOOL_REFERENCE.md +0 -1571
  684. package/mcps/golem-rhino-mcp/docs/TROUBLESHOOTING.md +0 -396
  685. package/mcps/golem-rhino-mcp/mcp_server/Screenshot 2026-03-20 at 18.33.06.png +0 -0
  686. package/mcps/golem-rhino-mcp/mcp_server/Screenshot 2026-03-21 at 1.43.01.png +0 -0
  687. package/mcps/golem-rhino-mcp/nexus-pngs/CAIO.png +0 -0
  688. package/mcps/golem-rhino-mcp/nexus-pngs/CEO.png +0 -0
  689. package/mcps/golem-rhino-mcp/nexus-pngs/CFO.png +0 -0
  690. package/mcps/golem-rhino-mcp/nexus-pngs/CHRO.png +0 -0
  691. package/mcps/golem-rhino-mcp/nexus-pngs/CISO.png +0 -0
  692. package/mcps/golem-rhino-mcp/nexus-pngs/CLO.png +0 -0
  693. package/mcps/golem-rhino-mcp/nexus-pngs/CMO.png +0 -0
  694. package/mcps/golem-rhino-mcp/nexus-pngs/COO.png +0 -0
  695. package/mcps/golem-rhino-mcp/nexus-pngs/CPO.png +0 -0
  696. package/mcps/golem-rhino-mcp/nexus-pngs/CRO.png +0 -0
  697. package/mcps/golem-rhino-mcp/nexus-pngs/CTO.png +0 -0
  698. package/mcps/golem-rhino-mcp/nexus-pngs/king-hippo.png +0 -0
  699. package/mcps/golem-rhino-mcp/nexus-pngs/tier-0-csuite.png +0 -0
  700. package/mcps/golem-rhino-mcp/screenshots/city_monument.png +0 -0
  701. package/mcps/golem-rhino-mcp/screenshots/city_skyline.png +0 -0
  702. package/mcps/golem-rhino-mcp/screenshots/city_street.png +0 -0
  703. package/mcps/golem-rhino-mcp/screenshots/city_wide.png +0 -0
  704. package/mcps/golem-rhino-mcp/tests/__init__.py +0 -0
  705. package/mcps/golem-rhino-mcp/tests/conftest.py +0 -150
  706. package/mcps/golem-rhino-mcp/tests/test_connection.py +0 -543
  707. package/mcps/golem-rhino-mcp/tests/test_integration.py +0 -344
  708. package/mcps/golem-rhino-mcp/tests/test_models.py +0 -749
  709. package/mcps/golem-rhino-mcp/tests/test_protocol.py +0 -334
  710. package/mcps/golem-rhino-mcp/tests/test_tools_mock.py +0 -689
  711. package/mcps/tdmcp/.github/social-preview.png +0 -0
  712. package/mcps/tdmcp/docs/.vitepress/config.ts +0 -397
  713. package/mcps/tdmcp/docs/AI_PARTY_LLM_TRAINING_PLAN.md +0 -118
  714. package/mcps/tdmcp/docs/AI_PARTY_POC.md +0 -142
  715. package/mcps/tdmcp/docs/AI_PARTY_POC_PLAN.md +0 -112
  716. package/mcps/tdmcp/docs/CREATIVE_RAG.md +0 -346
  717. package/mcps/tdmcp/docs/DEPLOYMENT.md +0 -113
  718. package/mcps/tdmcp/docs/PROJECT_RAG.md +0 -477
  719. package/mcps/tdmcp/docs/ROADMAP.md +0 -1346
  720. package/mcps/tdmcp/docs/guide/ai-controlled-party.md +0 -215
  721. package/mcps/tdmcp/docs/guide/body-tracking.md +0 -169
  722. package/mcps/tdmcp/docs/guide/codex.md +0 -107
  723. package/mcps/tdmcp/docs/guide/components.md +0 -126
  724. package/mcps/tdmcp/docs/guide/dashboard-foh.md +0 -93
  725. package/mcps/tdmcp/docs/guide/faq.md +0 -62
  726. package/mcps/tdmcp/docs/guide/first-visual.md +0 -76
  727. package/mcps/tdmcp/docs/guide/generators.md +0 -230
  728. package/mcps/tdmcp/docs/guide/glossary.md +0 -123
  729. package/mcps/tdmcp/docs/guide/install.md +0 -200
  730. package/mcps/tdmcp/docs/guide/local-copilot.md +0 -154
  731. package/mcps/tdmcp/docs/guide/lops-integration.md +0 -133
  732. package/mcps/tdmcp/docs/guide/mcp-resources.md +0 -96
  733. package/mcps/tdmcp/docs/guide/mediapipe-adapters.md +0 -90
  734. package/mcps/tdmcp/docs/guide/physical-installations.md +0 -129
  735. package/mcps/tdmcp/docs/guide/prompt-cookbook.md +0 -2285
  736. package/mcps/tdmcp/docs/guide/recipes.md +0 -72
  737. package/mcps/tdmcp/docs/guide/session-profile.md +0 -90
  738. package/mcps/tdmcp/docs/guide/shader-park.md +0 -94
  739. package/mcps/tdmcp/docs/guide/show-timelines.md +0 -125
  740. package/mcps/tdmcp/docs/guide/troubleshooting.md +0 -121
  741. package/mcps/tdmcp/docs/guide/what-is-tdmcp.md +0 -62
  742. package/mcps/tdmcp/docs/index.md +0 -49
  743. package/mcps/tdmcp/docs/privacy.md +0 -85
  744. package/mcps/tdmcp/docs/pt/CREATIVE_RAG.md +0 -370
  745. package/mcps/tdmcp/docs/pt/PROJECT_RAG.md +0 -420
  746. package/mcps/tdmcp/docs/pt/guide/ai-controlled-party.md +0 -218
  747. package/mcps/tdmcp/docs/pt/guide/body-tracking.md +0 -175
  748. package/mcps/tdmcp/docs/pt/guide/codex.md +0 -113
  749. package/mcps/tdmcp/docs/pt/guide/components.md +0 -129
  750. package/mcps/tdmcp/docs/pt/guide/dashboard-foh.md +0 -95
  751. package/mcps/tdmcp/docs/pt/guide/faq.md +0 -62
  752. package/mcps/tdmcp/docs/pt/guide/first-visual.md +0 -81
  753. package/mcps/tdmcp/docs/pt/guide/generators.md +0 -243
  754. package/mcps/tdmcp/docs/pt/guide/glossary.md +0 -127
  755. package/mcps/tdmcp/docs/pt/guide/install.md +0 -196
  756. package/mcps/tdmcp/docs/pt/guide/local-copilot.md +0 -164
  757. package/mcps/tdmcp/docs/pt/guide/lops-integration.md +0 -143
  758. package/mcps/tdmcp/docs/pt/guide/mcp-resources.md +0 -96
  759. package/mcps/tdmcp/docs/pt/guide/mediapipe-adapters.md +0 -93
  760. package/mcps/tdmcp/docs/pt/guide/physical-installations.md +0 -135
  761. package/mcps/tdmcp/docs/pt/guide/prompt-cookbook.md +0 -2342
  762. package/mcps/tdmcp/docs/pt/guide/recipes.md +0 -59
  763. package/mcps/tdmcp/docs/pt/guide/session-profile.md +0 -92
  764. package/mcps/tdmcp/docs/pt/guide/shader-park.md +0 -95
  765. package/mcps/tdmcp/docs/pt/guide/show-timelines.md +0 -128
  766. package/mcps/tdmcp/docs/pt/guide/troubleshooting.md +0 -109
  767. package/mcps/tdmcp/docs/pt/guide/what-is-tdmcp.md +0 -61
  768. package/mcps/tdmcp/docs/pt/index.md +0 -49
  769. package/mcps/tdmcp/docs/pt/privacy.md +0 -91
  770. package/mcps/tdmcp/docs/pt/reference/architecture.md +0 -178
  771. package/mcps/tdmcp/docs/pt/reference/environment.md +0 -70
  772. package/mcps/tdmcp/docs/public/favicon.svg +0 -12
  773. package/mcps/tdmcp/docs/public/logo-400.png +0 -0
  774. package/mcps/tdmcp/docs/public/og-image.jpg +0 -0
  775. package/mcps/tdmcp/docs/public/robots.txt +0 -4
  776. package/mcps/tdmcp/docs/reference/API_STABILITY.md +0 -116
  777. package/mcps/tdmcp/docs/reference/architecture.md +0 -191
  778. package/mcps/tdmcp/docs/reference/bridge-api.md +0 -155
  779. package/mcps/tdmcp/docs/reference/cli.md +0 -288
  780. package/mcps/tdmcp/docs/reference/coverage-harness.md +0 -61
  781. package/mcps/tdmcp/docs/reference/environment.md +0 -116
  782. package/mcps/tdmcp/docs/reference/packages.md +0 -98
  783. package/mcps/tdmcp/docs/reference/tool-contract.md +0 -74
  784. package/mcps/tdmcp/docs/superpowers/plans/2026-05-29-shader-park-animus-integrations.md +0 -76
  785. package/mcps/tdmcp/docs/superpowers/plans/2026-06-02-doctor-fix-textport-auto-install.md +0 -131
  786. package/mcps/tdmcp/docs/superpowers/plans/2026-06-06-body-bubbles.md +0 -138
  787. package/mcps/tdmcp/docs/superpowers/plans/2026-06-08-deep-doppelganger-implementation.md +0 -201
  788. package/mcps/tdmcp/docs/superpowers/plans/2026-06-10-ai-party-ollama-integration.md +0 -55
  789. package/mcps/tdmcp/docs/superpowers/plans/2026-06-24-kinect-wall-harp-calibration-wizard.md +0 -53
  790. package/mcps/tdmcp/docs/superpowers/plans/2026-06-24-kinect-wall-harp-external-kinect.md +0 -286
  791. package/mcps/tdmcp/docs/superpowers/specs/2026-05-31-create-vector-lines-design.md +0 -308
  792. package/mcps/tdmcp/docs/superpowers/specs/2026-06-01-ai-controlled-party-plan.md +0 -461
  793. package/mcps/tdmcp/docs/superpowers/specs/2026-06-04-ai-party-ui24r-scene-arming-design.md +0 -296
  794. package/mcps/tdmcp/docs/superpowers/specs/2026-06-08-ai-controlled-party-producer-poc.md +0 -232
  795. package/mcps/tdmcp/docs/superpowers/specs/2026-06-08-deep-doppelganger-design.md +0 -136
  796. package/mcps/tdmcp/docs/superpowers/specs/2026-06-10-hermes-ai-party-poc.md +0 -383
  797. package/mcps/tdmcp/docs/superpowers/specs/2026-06-23-kinect-wall-harp-design.md +0 -340
  798. package/mcps/tdmcp/docs/superpowers/specs/2026-06-24-kinect-environment-diagnostic-design.md +0 -151
  799. package/mcps/tdmcp/docs/superpowers/specs/2026-06-24-kinect-wall-harp-external-kinect-design.md +0 -104
  800. package/mcps/tdmcp/training/showintent/data/curated/.gitkeep +0 -1
  801. package/mcps/tdmcp/training/showintent/data/curated/curated-approved.jsonl +0 -1
  802. package/mcps/tdmcp/training/showintent/data/curation_template.csv +0 -1
  803. package/mcps/tdmcp/training/showintent/data/generated/.gitkeep +0 -1
  804. package/mcps/tdmcp/training/showintent/data/generated/showintent-synthetic.jsonl +0 -2000
  805. package/mcps/tdmcp/training/showintent/data/raw/.gitkeep +0 -1
  806. package/mcps/tdmcp/training/showintent/data/splits/.gitkeep +0 -1
  807. package/mcps/tdmcp/training/showintent/data/splits/train-compact.jsonl +0 -1800
  808. package/mcps/tdmcp/training/showintent/data/splits/train.jsonl +0 -1800
  809. package/mcps/tdmcp/training/showintent/data/splits/validation-compact.jsonl +0 -200
  810. package/mcps/tdmcp/training/showintent/data/splits/validation.jsonl +0 -200
  811. package/mcps/touchdesigner-mcp/assets/import.png +0 -0
  812. package/mcps/touchdesigner-mcp/assets/nodes_list.png +0 -0
  813. package/mcps/touchdesigner-mcp/assets/particle_on_youtube.png +0 -0
  814. package/mcps/touchdesigner-mcp/assets/textport.png +0 -0
  815. package/mcps/touchdesigner-mcp/assets/tutorial.png +0 -0
  816. package/mcps/touchdesigner-mcp/assets/tutorial_docker.png +0 -0
  817. package/mcps/touchdesigner-mcp/docs/architecture.md +0 -1245
  818. package/mcps/touchdesigner-mcp/docs/development.ja.md +0 -197
  819. package/mcps/touchdesigner-mcp/docs/development.md +0 -212
  820. package/mcps/touchdesigner-mcp/docs/installation.ja.md +0 -370
  821. package/mcps/touchdesigner-mcp/docs/installation.md +0 -406
  822. package/mcps/unreal_mcp/Public/icon.png +0 -0
  823. package/mcps/unreal_mcp/docs/Engine-API-Reference.md +0 -2251
  824. package/mcps/unreal_mcp/docs/Roadmap.md +0 -2269
  825. package/mcps/unreal_mcp/docs/editor-plugin-extension.md +0 -373
  826. package/mcps/unreal_mcp/docs/handler-mapping.md +0 -1170
  827. package/mcps/unreal_mcp/docs/native-automation-progress.md +0 -431
  828. package/mcps/unreal_mcp/docs/testing-guide.md +0 -149
  829. package/mcps/unreal_mcp/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Domains/Test/McpAutomationBridge_TestHandlers.cpp +0 -74
  830. package/mcps/unreal_mcp/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Tests/McpCustomHandlerAliasTests.cpp +0 -191
  831. package/mcps/unreal_mcp/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Tests/McpEnvironmentSnapshotSchemaTests.cpp +0 -192
  832. package/mcps/unreal_mcp/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/Tests/McpPropertyReflectionImportTests.cpp +0 -269
  833. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentAcpClientProtocolAutomation.cpp +0 -152
  834. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentAcpProtocolTestHelpers.cpp +0 -136
  835. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentAcpProtocolTestHelpers.h +0 -49
  836. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentAutomationTestDelegates.h +0 -12
  837. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentAutomationTests.cpp +0 -34
  838. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentAutomationWidgetHelpers.cpp +0 -116
  839. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentAutomationWidgetHelpers.h +0 -15
  840. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentFakeAcpScript.cpp +0 -76
  841. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentPanelAutomation.h +0 -35
  842. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentPanelAutomationFixture.cpp +0 -118
  843. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentPanelHistoryChecks.cpp +0 -162
  844. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentPanelLayoutChecks.cpp +0 -158
  845. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentPanelTranscriptChecks.cpp +0 -117
  846. package/mcps/unreal_mcp/plugins/UnrealAgent/Source/UnrealAgent/Private/Tests/UnrealAgentStudioKitAutomationChecks.cpp +0 -155
  847. package/mcps/unreal_mcp/tests/AGENTS.md +0 -73
  848. package/mcps/unreal_mcp/tests/expectation-utils.mjs +0 -29
  849. package/mcps/unreal_mcp/tests/heartbeat-progress.test.mjs +0 -143
  850. package/mcps/unreal_mcp/tests/integration/get_ai_info_characterization.mjs +0 -216
  851. package/mcps/unreal_mcp/tests/integration.mjs +0 -94
  852. package/mcps/unreal_mcp/tests/mcp-tools/core/control-actor.test.mjs +0 -114
  853. package/mcps/unreal_mcp/tests/mcp-tools/core/control-editor.test.mjs +0 -86
  854. package/mcps/unreal_mcp/tests/mcp-tools/core/inspect.test.mjs +0 -85
  855. package/mcps/unreal_mcp/tests/mcp-tools/core/manage-asset.test.mjs +0 -302
  856. package/mcps/unreal_mcp/tests/mcp-tools/core/manage-blueprint.test.mjs +0 -331
  857. package/mcps/unreal_mcp/tests/mcp-tools/core/manage-level.test.mjs +0 -73
  858. package/mcps/unreal_mcp/tests/mcp-tools/core/manage-tools.test.mjs +0 -30
  859. package/mcps/unreal_mcp/tests/mcp-tools/core/system-control.test.mjs +0 -205
  860. package/mcps/unreal_mcp/tests/mcp-tools/gameplay/animation-physics.test.mjs +0 -277
  861. package/mcps/unreal_mcp/tests/mcp-tools/gameplay/manage-ai.test.mjs +0 -299
  862. package/mcps/unreal_mcp/tests/mcp-tools/gameplay/manage-character.test.mjs +0 -85
  863. package/mcps/unreal_mcp/tests/mcp-tools/gameplay/manage-combat.test.mjs +0 -106
  864. package/mcps/unreal_mcp/tests/mcp-tools/gameplay/manage-effect.test.mjs +0 -108
  865. package/mcps/unreal_mcp/tests/mcp-tools/gameplay/manage-gas.test.mjs +0 -96
  866. package/mcps/unreal_mcp/tests/mcp-tools/gameplay/manage-interaction.test.mjs +0 -122
  867. package/mcps/unreal_mcp/tests/mcp-tools/gameplay/manage-inventory.test.mjs +0 -146
  868. package/mcps/unreal_mcp/tests/mcp-tools/utility/behavior-tree-get-tree.test.mjs +0 -233
  869. package/mcps/unreal_mcp/tests/mcp-tools/utility/manage-audio.test.mjs +0 -160
  870. package/mcps/unreal_mcp/tests/mcp-tools/utility/manage-behavior-tree.test.mjs +0 -223
  871. package/mcps/unreal_mcp/tests/mcp-tools/utility/manage-networking.test.mjs +0 -635
  872. package/mcps/unreal_mcp/tests/mcp-tools/utility/manage-sequence.test.mjs +0 -95
  873. package/mcps/unreal_mcp/tests/mcp-tools/world/build-environment.test.mjs +0 -282
  874. package/mcps/unreal_mcp/tests/mcp-tools/world/manage-geometry.test.mjs +0 -130
  875. package/mcps/unreal_mcp/tests/mcp-tools/world/manage-level-structure.test.mjs +0 -149
  876. package/mcps/unreal_mcp/tests/mcp-tools/world/manage-pcg.test.mjs +0 -471
  877. package/mcps/unreal_mcp/tests/mcp-tools/world/rendering-cases.cjs +0 -775
  878. package/mcps/unreal_mcp/tests/mcp-tools/world/rendering.test.mjs +0 -15
  879. package/mcps/unreal_mcp/tests/native-mcp-parity-audit.mjs +0 -260
  880. package/mcps/unreal_mcp/tests/native-mcp-source-parser.mjs +0 -229
  881. package/mcps/unreal_mcp/tests/native-mcp-typescript-parity-parser.mjs +0 -121
  882. package/mcps/unreal_mcp/tests/parameter-audit-cli.mjs +0 -23
  883. package/mcps/unreal_mcp/tests/parameter-audit-context.mjs +0 -14
  884. package/mcps/unreal_mcp/tests/parameter-audit-coverage.mjs +0 -164
  885. package/mcps/unreal_mcp/tests/parameter-audit-schema.mjs +0 -190
  886. package/mcps/unreal_mcp/tests/parameter-audit-suites.mjs +0 -218
  887. package/mcps/unreal_mcp/tests/parameter-combination-audit.mjs +0 -37
  888. package/mcps/unreal_mcp/tests/test-runner-response-utils.mjs +0 -132
  889. package/mcps/unreal_mcp/tests/test-runner.mjs +0 -1798
  890. package/mcps/unreal_mcp/tests/unit/automation/bridge_host_validation.test.ts +0 -165
  891. package/mcps/unreal_mcp/tests/unit/config.test.ts +0 -205
  892. package/mcps/unreal_mcp/tests/unit/native_mcp_parity_audit.test.ts +0 -281
  893. package/mcps/unreal_mcp/tests/unit/native_mcp_parity_comment_filtering.test.ts +0 -175
  894. package/mcps/unreal_mcp/tests/unit/parameter_audit_schema.test.ts +0 -273
  895. package/mcps/unreal_mcp/tests/unit/plugin/automation_event_stream_contracts.test.ts +0 -195
  896. package/mcps/unreal_mcp/tests/unit/plugin/environment_asset_name_contracts.test.ts +0 -115
  897. package/mcps/unreal_mcp/tests/unit/plugin/environment_build_adapter_contracts.test.ts +0 -377
  898. package/mcps/unreal_mcp/tests/unit/plugin/environment_snapshot_path_contracts.test.ts +0 -122
  899. package/mcps/unreal_mcp/tests/unit/plugin/handler_routing_contracts.test.ts +0 -120
  900. package/mcps/unreal_mcp/tests/unit/plugin/level_lighting_contracts.test.ts +0 -125
  901. package/mcps/unreal_mcp/tests/unit/plugin/plugin_manifest_contracts.test.ts +0 -154
  902. package/mcps/unreal_mcp/tests/unit/plugin/plugin_packaging_contracts.test.ts +0 -124
  903. package/mcps/unreal_mcp/tests/unit/plugin/runtime_behavior_contracts.test.ts +0 -372
  904. package/mcps/unreal_mcp/tests/unit/plugin/security_contracts.test.ts +0 -81
  905. package/mcps/unreal_mcp/tests/unit/plugin/source_structure.test.ts +0 -101
  906. package/mcps/unreal_mcp/tests/unit/plugin/source_structure_contracts.test.ts +0 -103
  907. package/mcps/unreal_mcp/tests/unit/plugin/websocket_origin_contracts.test.ts +0 -25
  908. package/mcps/unreal_mcp/tests/unit/server_shutdown_contracts.test.ts +0 -33
  909. package/mcps/unreal_mcp/tests/unit/source_structure.test.ts +0 -79
  910. package/mcps/unreal_mcp/tests/unit/test_runner.test.ts +0 -342
  911. package/mcps/unreal_mcp/tests/unit/test_runner_response_utils.test.ts +0 -105
  912. package/mcps/unreal_mcp/tests/unit/tools/asset_handlers_security.test.ts +0 -246
  913. package/mcps/unreal_mcp/tests/unit/tools/behavior_tree_get_tree.test.ts +0 -20
  914. package/mcps/unreal_mcp/tests/unit/tools/blueprint_handlers.test.ts +0 -122
  915. package/mcps/unreal_mcp/tests/unit/tools/editor.test.ts +0 -56
  916. package/mcps/unreal_mcp/tests/unit/tools/handler_structure.test.ts +0 -59
  917. package/mcps/unreal_mcp/tests/unit/tools/inspect_handlers.test.ts +0 -231
  918. package/mcps/unreal_mcp/tests/unit/tools/level.test.ts +0 -56
  919. package/mcps/unreal_mcp/tests/unit/tools/level_security.test.ts +0 -45
  920. package/mcps/unreal_mcp/tests/unit/tools/lighting_timeout_propagation.test.ts +0 -123
  921. package/mcps/windows-computer-use-mcp/cua-reports/app-1781542686.png +0 -0
  922. package/mcps/windows-computer-use-mcp/cua-reports/bridge-1781542694.png +0 -0
  923. package/mcps/windows-computer-use-mcp/cua-reports/bridge-ok-1781442393.png +0 -0
  924. package/mcps/windows-computer-use-mcp/cua-reports/bridge-ok-1781442915.png +0 -0
  925. package/mcps/windows-computer-use-mcp/cua-reports/bridge-snap-1781444105.png +0 -0
  926. package/mcps/windows-computer-use-mcp/cua-reports/bridge-snap-1781444214.png +0 -0
  927. package/mcps/windows-computer-use-mcp/cua-reports/bridge-snap-1781457035.png +0 -0
  928. package/mcps/windows-computer-use-mcp/cua-reports/cua-smoke-1781440024.png +0 -0
  929. package/mcps/windows-computer-use-mcp/cua-reports/cua-smoke-1781440234.png +0 -0
  930. package/mcps/windows-computer-use-mcp/cua-reports/cua-smoke-1781442383.png +0 -0
  931. package/mcps/windows-computer-use-mcp/cua-reports/cua-smoke-1781442906.png +0 -0
  932. package/mcps/windows-computer-use-mcp/cua-reports/cua-smoke-1781444096.png +0 -0
  933. package/mcps/windows-computer-use-mcp/cua-reports/cua-smoke-1781444209.png +0 -0
  934. package/mcps/windows-computer-use-mcp/cua-reports/cua-smoke-1781457029.png +0 -0
  935. package/mcps/windows-computer-use-mcp/cua-reports/nav/dashboard-1781542713.png +0 -0
  936. package/mcps/windows-computer-use-mcp/cua-reports/nav/help-1781542738.png +0 -0
  937. package/mcps/windows-computer-use-mcp/cua-reports/nav/logging-1781542722.png +0 -0
  938. package/mcps/windows-computer-use-mcp/cua-reports/nav/nav-dashboard-1781444225.png +0 -0
  939. package/mcps/windows-computer-use-mcp/cua-reports/nav/nav-dashboard-1781457051.png +0 -0
  940. package/mcps/windows-computer-use-mcp/cua-reports/nav/nav-help-1781444245.png +0 -0
  941. package/mcps/windows-computer-use-mcp/cua-reports/nav/nav-help-1781457075.png +0 -0
  942. package/mcps/windows-computer-use-mcp/cua-reports/nav/nav-logging-1781444232.png +0 -0
  943. package/mcps/windows-computer-use-mcp/cua-reports/nav/nav-logging-1781457059.png +0 -0
  944. package/mcps/windows-computer-use-mcp/cua-reports/nav/nav-settings-1781444239.png +0 -0
  945. package/mcps/windows-computer-use-mcp/cua-reports/nav/nav-settings-1781457067.png +0 -0
  946. package/mcps/windows-computer-use-mcp/cua-reports/nav/settings-1781542730.png +0 -0
  947. package/mcps/windows-computer-use-mcp/cua-reports/nav-dashboard-1781442925.png +0 -0
  948. package/mcps/windows-computer-use-mcp/cua-reports/nav-help-1781442941.png +0 -0
  949. package/mcps/windows-computer-use-mcp/cua-reports/nav-logging-1781442930.png +0 -0
  950. package/mcps/windows-computer-use-mcp/cua-reports/nav-settings-1781442936.png +0 -0
  951. package/mcps/windows-computer-use-mcp/docs/ARCHITECTURE.md +0 -46
  952. package/mcps/windows-computer-use-mcp/docs/ASSESSMENT_BY_CURSOR_2026-06-14_CUA_NSIS.md +0 -383
  953. package/mcps/windows-computer-use-mcp/docs/AUTOMATION_ASSERT_SPEC.md +0 -202
  954. package/mcps/windows-computer-use-mcp/docs/CUA_PARITY.md +0 -39
  955. package/mcps/windows-computer-use-mcp/docs/DEVELOPMENT.md +0 -51
  956. package/mcps/windows-computer-use-mcp/docs/MEMOPS_CUA.md +0 -147
  957. package/mcps/windows-computer-use-mcp/docs/PRD.md +0 -93
  958. package/mcps/windows-computer-use-mcp/docs/README.md +0 -34
  959. package/mcps/windows-computer-use-mcp/docs/ROADMAP.md +0 -142
  960. package/mcps/windows-computer-use-mcp/docs/SAFETY.md +0 -138
  961. package/mcps/windows-computer-use-mcp/docs/TESTING.md +0 -90
  962. package/mcps/windows-computer-use-mcp/docs/TOOLS.md +0 -80
  963. package/mcps/windows-computer-use-mcp/docs/TOOL_DOCSTRING_STANDARD.md +0 -538
  964. package/mcps/windows-computer-use-mcp/docs/_stale/AHK_VS_PYWINAUTO_COMPARISON.md +0 -353
  965. package/mcps/windows-computer-use-mcp/docs/_stale/ASSESSMENT.md +0 -54
  966. package/mcps/windows-computer-use-mcp/docs/_stale/ASSESSMENT_2026-06-08.md +0 -309
  967. package/mcps/windows-computer-use-mcp/docs/_stale/COMPLETE_DOCUMENTATION_STRUCTURE.md +0 -5
  968. package/mcps/windows-computer-use-mcp/docs/_stale/CUA_ASSISTANT_TODO.md +0 -84
  969. package/mcps/windows-computer-use-mcp/docs/_stale/CUA_PARITY_ROADMAP.md +0 -81
  970. package/mcps/windows-computer-use-mcp/docs/_stale/CUA_ROADMAP.md +0 -306
  971. package/mcps/windows-computer-use-mcp/docs/_stale/DESKTOP_APP.md +0 -125
  972. package/mcps/windows-computer-use-mcp/docs/_stale/DOCUMENTATION_INDEX.md +0 -59
  973. package/mcps/windows-computer-use-mcp/docs/_stale/GLAMA_AI_CRITICISM_ANALYSIS.md +0 -259
  974. package/mcps/windows-computer-use-mcp/docs/_stale/GLAMA_AI_PLATFORM.md +0 -175
  975. package/mcps/windows-computer-use-mcp/docs/_stale/GLAMA_AI_RESCAN_GUIDE.md +0 -216
  976. package/mcps/windows-computer-use-mcp/docs/_stale/GOLD_STATUS_ACHIEVEMENT.md +0 -197
  977. package/mcps/windows-computer-use-mcp/docs/_stale/LLM_REPO_CONTEXT.md +0 -7
  978. package/mcps/windows-computer-use-mcp/docs/_stale/MCP_PRODUCTION_CHECKLIST.md +0 -105
  979. package/mcps/windows-computer-use-mcp/docs/_stale/OFFICE_BACKGROUND_MATRIX.md +0 -19
  980. package/mcps/windows-computer-use-mcp/docs/_stale/OPERATOR_PROTOCOL.md +0 -37
  981. package/mcps/windows-computer-use-mcp/docs/_stale/ORGANIZATION_SUMMARY.md +0 -305
  982. package/mcps/windows-computer-use-mcp/docs/_stale/RENAME-TO-AUTOMATION-MCP.md +0 -191
  983. package/mcps/windows-computer-use-mcp/docs/_stale/STATUS_REPORT.md +0 -288
  984. package/mcps/windows-computer-use-mcp/docs/_stale/TARGETS_PAGE_SKETCH.md +0 -977
  985. package/mcps/windows-computer-use-mcp/docs/_stale/TOOL_DOCSTRING_MIGRATION.md +0 -365
  986. package/mcps/windows-computer-use-mcp/docs/_stale/USAGE_SCENARIOS.md +0 -697
  987. package/mcps/windows-computer-use-mcp/docs/_stale/desktop-state-tool.md +0 -464
  988. package/mcps/windows-computer-use-mcp/docs/_stale/development/README.md +0 -18
  989. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/AI_DEVELOPMENT_RULES.md +0 -928
  990. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/AI_DEVELOPMENT_TOOLS_COMPARISON.md +0 -364
  991. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/BULLETPROOF_MCP_GUIDE.md +0 -793
  992. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/DEBUGGING_LESSONS_LEARNED.md +0 -635
  993. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/DEVELOPMENT_PAIN_POINTS.md +0 -699
  994. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/FILE_VALIDATION_COMPLETE.md +0 -410
  995. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/FILE_VALIDATION_GUIDE.md +0 -575
  996. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/LINK_PARSER_FIX.md +0 -769
  997. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/MCP_SYNC_DEBUGGING_GUIDE.md +0 -512
  998. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/PYTHON_DEPENDENCY_HELL_FIX.md +0 -568
  999. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/PYTHON_SNIPPETS_USAGE_GUIDE.md +0 -717
  1000. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/README.md +0 -25
  1001. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/SYNC_HEALTH_IMPROVEMENTS_SUMMARY.md +0 -496
  1002. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/SYNC_HEALTH_INTEGRATION.md +0 -381
  1003. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/SYSTEMATIC_PROJECT_UPDATES.md +0 -532
  1004. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/mcp-prompt-templates-vs-docstrings.md +0 -657
  1005. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/mcp-tool-docstring-best-practices.md +0 -439
  1006. package/mcps/windows-computer-use-mcp/docs/_stale/development/archive/mcp-tool-documentation-standards.md +0 -1153
  1007. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/CI_CD_GLAMA_OPTIMIZATION_GUIDE.md +0 -850
  1008. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/GLAMA_AI_CRITICISM_ANALYSIS.md +0 -259
  1009. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/GLAMA_AI_OPTIMIZATION_SUMMARY.md +0 -204
  1010. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/GLAMA_AI_PLATFORM.md +0 -175
  1011. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/GLAMA_AI_RESCAN_GUIDE.md +0 -216
  1012. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/GLAMA_GITHUB_APP_SETUP.md +0 -199
  1013. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/GLAMA_INTEGRATION.md +0 -181
  1014. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/GLAMA_RESCAN_EMAIL.txt +0 -43
  1015. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/GOLD_STATUS_ACHIEVEMENT.md +0 -197
  1016. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/GOLD_STATUS_UPDATE_2025_10_08.md +0 -378
  1017. package/mcps/windows-computer-use-mcp/docs/_stale/glama-platform/README.md +0 -632
  1018. package/mcps/windows-computer-use-mcp/docs/_stale/legacy/requirements-fastmcp.txt +0 -8
  1019. package/mcps/windows-computer-use-mcp/docs/_stale/legacy/requirements_minimal.txt +0 -4
  1020. package/mcps/windows-computer-use-mcp/docs/_stale/mcp-technical/CLAUDE_DESKTOP_DEBUGGING.md +0 -561
  1021. package/mcps/windows-computer-use-mcp/docs/_stale/mcp-technical/CONTAINERIZATION_GUIDELINES.md +0 -707
  1022. package/mcps/windows-computer-use-mcp/docs/_stale/mcp-technical/MCP_PRODUCTION_CHECKLIST.md +0 -101
  1023. package/mcps/windows-computer-use-mcp/docs/_stale/mcp-technical/MONITORING_STACK_DEPLOYMENT.md +0 -532
  1024. package/mcps/windows-computer-use-mcp/docs/_stale/mcp-technical/README.md +0 -266
  1025. package/mcps/windows-computer-use-mcp/docs/_stale/mcp-technical/TROUBLESHOOTING_FASTMCP_2.12.md +0 -670
  1026. package/mcps/windows-computer-use-mcp/docs/_stale/mcpb-packaging/MCPB_BUILDING_GUIDE.md +0 -1921
  1027. package/mcps/windows-computer-use-mcp/docs/_stale/mcpb-packaging/MCPB_IMPLEMENTATION_SUMMARY.md +0 -387
  1028. package/mcps/windows-computer-use-mcp/docs/_stale/mcpb-packaging/PYPI_PUBLISHING_GUIDE.md +0 -789
  1029. package/mcps/windows-computer-use-mcp/docs/_stale/mcpb-packaging/README.md +0 -404
  1030. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/COMMUNITY_AND_SUPPORT.md +0 -976
  1031. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/LANGUAGE_SERVER_PROTOCOL_EXPLAINED.md +0 -416
  1032. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/MCP_TOOL_CORRUPTION_ISSUE.md +0 -279
  1033. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/NOTEPADPP_ALTERNATIVES.md +0 -308
  1034. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/NOTEPADPP_COLOR_FIX_2025_10_08.md +0 -187
  1035. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/NOTEPADPP_COMPLETE_GUIDE.md +0 -1259
  1036. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/NOTEPADPP_NUCLEAR_REINSTALL_2025_10_08.md +0 -196
  1037. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/NPP_WHITE_ON_WHITE_BATTLE_PLAN.md +0 -336
  1038. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/PLUGIN_ECOSYSTEM_COMPREHENSIVE.md +0 -887
  1039. package/mcps/windows-computer-use-mcp/docs/_stale/notepadpp/README.md +0 -472
  1040. package/mcps/windows-computer-use-mcp/docs/_stale/repository-protection/BACKUP_AND_RECOVERY_GUIDE.md +0 -455
  1041. package/mcps/windows-computer-use-mcp/docs/_stale/repository-protection/BRANCH_PROTECTION_SETTINGS.md +0 -236
  1042. package/mcps/windows-computer-use-mcp/docs/_stale/repository-protection/BRANCH_STRATEGY_AND_AI_WORKFLOW.md +0 -616
  1043. package/mcps/windows-computer-use-mcp/docs/_stale/repository-protection/README.md +0 -333
  1044. package/mcps/windows-computer-use-mcp/docs/_stale/serena/SERENA_COPY_TO_OTHER_REPOS.md +0 -159
  1045. package/mcps/windows-computer-use-mcp/docs/_stale/serena/SERENA_GUIDE.md +0 -632
  1046. package/mcps/windows-computer-use-mcp/docs/_stale/serena/SERENA_INSTALL_AND_TEST.md +0 -569
  1047. package/mcps/windows-computer-use-mcp/docs/_stale/serena/SERENA_NEXT_STEPS.md +0 -351
  1048. package/mcps/windows-computer-use-mcp/docs/_stale/serena/SERENA_QUICK_INSTALL.md +0 -195
  1049. package/mcps/windows-computer-use-mcp/docs/_stale/state-tool-implementation.md +0 -498
  1050. package/mcps/windows-computer-use-mcp/docs/composing-with-playwright.md +0 -55
  1051. package/mcps/windows-computer-use-mcp/docs/cua-nsis-certification.md +0 -101
  1052. package/mcps/windows-computer-use-mcp/docs/ocr.md +0 -107
  1053. package/mcps/windows-computer-use-mcp/docs/py-stack.md +0 -47
  1054. package/mcps/windows-computer-use-mcp/native/icons/icon.png +0 -0
  1055. package/mcps/windows-computer-use-mcp/screenshot_demo.png +0 -0
  1056. package/mcps/windows-computer-use-mcp/src/windows_computer_use_mcp/templates/vroidstudio/default/export_complete_toast.png +0 -0
  1057. package/mcps/windows-computer-use-mcp/src/windows_computer_use_mcp/templates/vroidstudio/default/export_dialog_title.png +0 -0
  1058. package/mcps/windows-computer-use-mcp/src/windows_computer_use_mcp/templates/vroidstudio/default/file_dialog_path_field.png +0 -0
  1059. package/mcps/windows-computer-use-mcp/src/windows_computer_use_mcp/templates/vroidstudio/default/sample_tile_female.png +0 -0
  1060. package/mcps/windows-computer-use-mcp/tests/README.md +0 -155
  1061. package/mcps/windows-computer-use-mcp/tests/__init__.py +0 -1
  1062. package/mcps/windows-computer-use-mcp/tests/conftest.py +0 -281
  1063. package/mcps/windows-computer-use-mcp/tests/conftest_env.py +0 -116
  1064. package/mcps/windows-computer-use-mcp/tests/e2e/conftest.py +0 -96
  1065. package/mcps/windows-computer-use-mcp/tests/e2e/test_libreoffice_cua_loop.py +0 -72
  1066. package/mcps/windows-computer-use-mcp/tests/test_app_integration.py +0 -83
  1067. package/mcps/windows-computer-use-mcp/tests/test_app_profiles.py +0 -28
  1068. package/mcps/windows-computer-use-mcp/tests/test_automation_assert.py +0 -185
  1069. package/mcps/windows-computer-use-mcp/tests/test_automation_dialog.py +0 -43
  1070. package/mcps/windows-computer-use-mcp/tests/test_automation_elements.py +0 -138
  1071. package/mcps/windows-computer-use-mcp/tests/test_automation_keyboard.py +0 -57
  1072. package/mcps/windows-computer-use-mcp/tests/test_automation_mouse.py +0 -57
  1073. package/mcps/windows-computer-use-mcp/tests/test_automation_shortcut.py +0 -41
  1074. package/mcps/windows-computer-use-mcp/tests/test_automation_task.py +0 -115
  1075. package/mcps/windows-computer-use-mcp/tests/test_automation_windows.py +0 -105
  1076. package/mcps/windows-computer-use-mcp/tests/test_cameras_api.py +0 -43
  1077. package/mcps/windows-computer-use-mcp/tests/test_cua_parity.py +0 -51
  1078. package/mcps/windows-computer-use-mcp/tests/test_dependency_failures.py +0 -39
  1079. package/mcps/windows-computer-use-mcp/tests/test_desktop_state.py +0 -99
  1080. package/mcps/windows-computer-use-mcp/tests/test_face_recognition.py +0 -238
  1081. package/mcps/windows-computer-use-mcp/tests/test_face_recognition_api.py +0 -161
  1082. package/mcps/windows-computer-use-mcp/tests/test_images/README.md +0 -28
  1083. package/mcps/windows-computer-use-mcp/tests/test_mission.py +0 -42
  1084. package/mcps/windows-computer-use-mcp/tests/test_portmanteau_windows_mock.py +0 -173
  1085. package/mcps/windows-computer-use-mcp/tests/test_portmanteau_windows_real.py +0 -139
  1086. package/mcps/windows-computer-use-mcp/tests/test_retry.py +0 -26
  1087. package/mcps/windows-computer-use-mcp/tests/test_safety_api.py +0 -24
  1088. package/mcps/windows-computer-use-mcp/tests/test_template_library.py +0 -53
  1089. package/mcps/windows-computer-use-mcp/tests/test_utils.py +0 -185
  1090. package/mcps/windows-computer-use-mcp/web_sota/src-tauri/icons/icon.png +0 -0
  1091. package/skills/global_config/google-sheets-automation/SKILL.md +0 -150
  1092. package/skills/global_config/senior-fullstack/SKILL.md +0 -221
  1093. package/skills/global_config/senior-fullstack/references/architecture_patterns.md +0 -103
  1094. package/skills/global_config/senior-fullstack/references/development_workflows.md +0 -103
  1095. package/skills/global_config/senior-fullstack/references/tech_stack_guide.md +0 -103
  1096. package/skills/global_config/senior-fullstack/scripts/code_quality_analyzer.py +0 -114
  1097. package/skills/global_config/senior-fullstack/scripts/fullstack_scaffolder.py +0 -114
  1098. package/skills/global_config/senior-fullstack/scripts/project_scaffolder.py +0 -114
@@ -1,4637 +0,0 @@
1
- import asyncio
2
- import json
3
- import os
4
- import shutil
5
- import sqlite3
6
- import subprocess
7
- import tempfile
8
- import unittest
9
- import unittest.mock
10
- from typing import Any, Dict, Optional
11
-
12
- from src import server as _server_module
13
- from src.server import (
14
- _apply_media_analysis_clip_markers,
15
- _apply_sync_event_markers,
16
- _media_analysis_apply_setup_defaults,
17
- _media_analysis_capabilities_for_request,
18
- _media_analysis_effective_preferences,
19
- _media_analysis_merge_metadata_field,
20
- _media_analysis_marker_candidates_from_report,
21
- _media_analysis_metadata_writeback_enabled,
22
- _media_analysis_missing_capabilities_response,
23
- _media_analysis_publish_confirmed,
24
- _media_analysis_timed_marker_decision,
25
- _media_analysis_provenance_metadata,
26
- _media_analysis_records_from_target,
27
- _media_analysis_report_metadata_candidates,
28
- _publish_clip_metadata_from_analysis,
29
- setup,
30
- )
31
- from mcp import types as mcp_types
32
- from src.analysis_dashboard import (
33
- DashboardState,
34
- HTML,
35
- discover_project_contexts,
36
- list_analyzed_clips,
37
- get_analyzed_clip,
38
- get_analyzed_clip_shot,
39
- get_clip_frame_path,
40
- read_clip_corrections,
41
- _analysis_status_by_clip,
42
- )
43
- from src.utils import update_check
44
- from src.utils.media_analysis import (
45
- HOST_CHAT_PATHS_PROVIDER,
46
- VISION_SCHEMA_REFERENCE,
47
- analysis_request_signature,
48
- analysis_index_status,
49
- analysis_root_coverage,
50
- build_analysis_index,
51
- build_coverage_report,
52
- build_host_chat_paths_payload,
53
- build_plan,
54
- cleanup_artifacts,
55
- commit_visual_analysis,
56
- _cut_boundary_analysis,
57
- detect_capabilities,
58
- execute_plan,
59
- execute_plan_async,
60
- executing_clips,
61
- plan_requires_capabilities,
62
- load_report,
63
- mark_registry_stale_for_clip,
64
- query_analysis_index,
65
- registry_entry_superseded_info,
66
- resolve_output_root,
67
- _sample_times,
68
- summarize_reports,
69
- build_clip_index,
70
- clip_directory_hash,
71
- clip_index_path,
72
- load_clip_index,
73
- resolve_clip_directory,
74
- stable_clip_directory,
75
- stable_clip_hash,
76
- stable_clip_match_hashes,
77
- update_analysis_registry,
78
- vision_is_pending_host_analysis,
79
- )
80
- from src.utils.media_analysis_jobs import (
81
- batch_job_status,
82
- cancel_batch_job,
83
- create_batch_job,
84
- create_batch_job_from_paths,
85
- list_batch_jobs,
86
- resume_batch_job,
87
- run_batch_job_slice,
88
- )
89
-
90
-
91
- class ClipStub:
92
- def __init__(self, name, clip_id, file_path, media_id=None, third_party=None, metadata=None):
93
- self.name = name
94
- self.clip_id = clip_id
95
- self.file_path = file_path
96
- self.media_id = media_id or f"media-{clip_id}"
97
- self.third_party = dict(third_party or {})
98
- self.metadata = dict(metadata or {})
99
-
100
- def GetName(self):
101
- return self.name
102
-
103
- def GetUniqueId(self):
104
- return self.clip_id
105
-
106
- def GetMediaId(self):
107
- return self.media_id
108
-
109
- def GetClipProperty(self, key=""):
110
- props = {
111
- "File Path": self.file_path,
112
- "Type": "Video + Audio",
113
- "Duration": "00:00:04:00",
114
- "FPS": "24",
115
- "Resolution": "1920x1080",
116
- }
117
- if key:
118
- return props.get(key)
119
- return props
120
-
121
- def GetThirdPartyMetadata(self, key=""):
122
- if key:
123
- return self.third_party.get(key, "")
124
- return dict(self.third_party)
125
-
126
- def GetMetadata(self, key=""):
127
- if key:
128
- return self.metadata.get(key, "")
129
- return dict(self.metadata)
130
-
131
-
132
- class MarkerClipStub:
133
- def __init__(self, clip_id="clip-123"):
134
- self.clip_id = clip_id
135
- self.markers = {}
136
-
137
- def GetUniqueId(self):
138
- return self.clip_id
139
-
140
- def AddMarker(self, frame, color, name, note, duration, custom_data=""):
141
- self.markers[frame] = {
142
- "color": color,
143
- "name": name,
144
- "note": note,
145
- "duration": duration,
146
- "customData": custom_data,
147
- }
148
- return True
149
-
150
- def GetMarkers(self):
151
- return self.markers
152
-
153
-
154
- class MarkerFolderStub:
155
- def __init__(self, clips):
156
- self.clips = clips
157
-
158
- def GetClipList(self):
159
- return self.clips
160
-
161
- def GetSubFolderList(self):
162
- return []
163
-
164
-
165
- class MarkerMediaPoolStub:
166
- def __init__(self, clips):
167
- self.root = MarkerFolderStub(clips)
168
-
169
- def GetRootFolder(self):
170
- return self.root
171
-
172
-
173
- class MarkerProjectStub:
174
- def __init__(self, clips):
175
- self.media_pool = MarkerMediaPoolStub(clips)
176
-
177
- def GetMediaPool(self):
178
- return self.media_pool
179
-
180
-
181
- class SamplingSessionStub:
182
- def __init__(self, sampling=False, context=False):
183
- self.sampling = sampling
184
- self.context = context
185
-
186
- def check_client_capability(self, capability):
187
- if capability.sampling is None:
188
- return False
189
- if not self.sampling:
190
- return False
191
- if capability.sampling.context is not None and not self.context:
192
- return False
193
- return True
194
-
195
-
196
- class SamplingContextStub:
197
- def __init__(self, sampling=False, context=False):
198
- self.request_context = type(
199
- "RequestContextStub",
200
- (),
201
- {"session": SamplingSessionStub(sampling=sampling, context=context)},
202
- )()
203
-
204
-
205
- class SamplingRequestSessionStub(SamplingSessionStub):
206
- def __init__(self, sampling=False, context=False, response_text=None):
207
- super().__init__(sampling=sampling, context=context)
208
- self.response_text = response_text or json.dumps({"success": True, "clip_summary": "sampled"})
209
- self.created = False
210
-
211
- async def create_message(self, *args, **kwargs):
212
- self.created = True
213
- self.create_message_args = args
214
- self.create_message_kwargs = kwargs
215
- return type(
216
- "SamplingResultStub",
217
- (),
218
- {"content": mcp_types.TextContent(type="text", text=self.response_text)},
219
- )()
220
-
221
-
222
- class SamplingRequestContextStub:
223
- def __init__(self, sampling=False, context=False, response_text=None):
224
- self.session = SamplingRequestSessionStub(
225
- sampling=sampling,
226
- context=context,
227
- response_text=response_text,
228
- )
229
- self.request_context = type(
230
- "RequestContextStub",
231
- (),
232
- {"session": self.session},
233
- )()
234
- self.request_id = "request-123"
235
-
236
-
237
- class FolderStub:
238
- def __init__(self, name, clips=None, subfolders=None):
239
- self.name = name
240
- self.clips = clips or []
241
- self.subfolders = subfolders or []
242
-
243
- def GetName(self):
244
- return self.name
245
-
246
- def GetUniqueId(self):
247
- return f"folder-{self.name}"
248
-
249
- def GetClipList(self):
250
- return self.clips
251
-
252
- def GetSubFolderList(self):
253
- return self.subfolders
254
-
255
-
256
- class MediaPoolStub:
257
- def __init__(self, root, selected=None):
258
- self.root = root
259
- self.selected = selected or []
260
-
261
- def GetRootFolder(self):
262
- return self.root
263
-
264
- def GetSelectedClips(self):
265
- return self.selected
266
-
267
-
268
- class TimelineItemStub:
269
- def __init__(self, name, item_id, clip, start=0, end=24, source_start=0):
270
- self.name = name
271
- self.item_id = item_id
272
- self.clip = clip
273
- self.start = start
274
- self.end = end
275
- self.source_start = source_start
276
-
277
- def GetName(self):
278
- return self.name
279
-
280
- def GetUniqueId(self):
281
- return self.item_id
282
-
283
- def GetStart(self):
284
- return self.start
285
-
286
- def GetEnd(self):
287
- return self.end
288
-
289
- def GetDuration(self):
290
- return self.end - self.start
291
-
292
- def GetSourceStartFrame(self):
293
- return self.source_start
294
-
295
- def GetMediaPoolItem(self):
296
- return self.clip
297
-
298
-
299
- class TimelineStub:
300
- def __init__(self, video_tracks=None, audio_tracks=None):
301
- self.tracks = {
302
- "video": video_tracks or {},
303
- "audio": audio_tracks or {},
304
- "subtitle": {},
305
- }
306
-
307
- def GetName(self):
308
- return "Edit Timeline"
309
-
310
- def GetUniqueId(self):
311
- return "timeline-123"
312
-
313
- def GetTrackCount(self, track_type):
314
- return max(self.tracks.get(track_type, {}).keys() or [0])
315
-
316
- def GetItemListInTrack(self, track_type, track_index):
317
- return self.tracks.get(track_type, {}).get(track_index, [])
318
-
319
-
320
- class TimelineProjectStub:
321
- def __init__(self, timeline):
322
- self.timeline = timeline
323
-
324
- def GetCurrentTimeline(self):
325
- return self.timeline
326
-
327
-
328
- class MediaAnalysisPlanningTests(unittest.TestCase):
329
- def _write_synthetic_media(self, source):
330
- cmd = [
331
- "ffmpeg",
332
- "-hide_banner",
333
- "-loglevel",
334
- "error",
335
- "-y",
336
- "-f",
337
- "lavfi",
338
- "-i",
339
- "testsrc2=size=160x90:rate=24:duration=2",
340
- "-f",
341
- "lavfi",
342
- "-i",
343
- "sine=frequency=1000:duration=2",
344
- "-c:v",
345
- "mpeg4",
346
- "-c:a",
347
- "aac",
348
- "-shortest",
349
- source,
350
- ]
351
- subprocess.run(cmd, check=True)
352
-
353
- def test_cut_boundary_analysis_flags_flash_candidates_and_samples_boundaries(self):
354
- cut_analysis = _cut_boundary_analysis(
355
- 10.0,
356
- [
357
- {"time_seconds": 2.0},
358
- {"time_seconds": 2.1},
359
- {"time_seconds": 6.0},
360
- ],
361
- 24.0,
362
- )
363
-
364
- self.assertTrue(cut_analysis["success"])
365
- self.assertEqual(cut_analysis["cut_count"], 3)
366
- self.assertTrue(cut_analysis["likely_edited_sequence"])
367
- self.assertGreaterEqual(len(cut_analysis["flash_frame_candidates"]), 1)
368
- self.assertEqual(cut_analysis["flash_frame_candidates"][0]["reason"], "adjacent scene detections bound a very short segment")
369
-
370
- samples = _sample_times(
371
- 10.0,
372
- [{"time_seconds": 2.0}, {"time_seconds": 2.1}, {"time_seconds": 6.0}],
373
- 8,
374
- fps=24.0,
375
- cut_analysis=cut_analysis,
376
- )
377
- reasons = {row["selection_reason"] for row in samples}
378
- # Flash candidates surface as their own sampled frames. cut_before/cut_after
379
- # candidates are emitted by _sample_times but get deduped against shot_start
380
- # and shot_end reservations when shot boundaries coincide with cut points
381
- # (which is the normal case — shots are defined by cuts). The cut-boundary
382
- # coverage is therefore visible as shot_start / shot_end samples here.
383
- self.assertIn("flash_candidate", reasons)
384
- self.assertIn("shot_start", reasons)
385
- self.assertIn("shot_end", reasons)
386
-
387
- def test_output_root_uses_davinci_resolve_mcp_name(self):
388
- out = resolve_output_root(
389
- project_name="Example Project",
390
- project_id="project-123",
391
- source_paths=["/Volumes/ExampleMedia/Camera/A001/clip.mov"],
392
- )
393
-
394
- self.assertTrue(out["success"])
395
- self.assertIn("davinci-resolve-mcp-analysis", out["project_root"])
396
- self.assertIn("example-project", out["project_root"])
397
-
398
- def test_dashboard_context_switch_scopes_active_project_root_and_related_versions(self):
399
- with tempfile.TemporaryDirectory() as tmp:
400
- analysis_root = os.path.join(tmp, "analysis")
401
- previous = resolve_output_root(
402
- project_name="Example Project",
403
- project_id="version-001",
404
- analysis_root=analysis_root,
405
- create=True,
406
- )["project_root"]
407
- state = DashboardState("Example Project", "version-002", analysis_root)
408
- projects = discover_project_contexts(state.base_analysis_root, state.context())
409
-
410
- self.assertIn(previous, projects["related_project_roots"])
411
- self.assertIn(state.project_root, projects["related_project_roots"])
412
-
413
- switched = state.set_context({"project_root": previous, "project_name": "Example Project"})
414
-
415
- self.assertTrue(switched["success"])
416
- self.assertEqual(switched["active"]["project_root"], previous)
417
- self.assertEqual(state.project_root, previous)
418
-
419
- def test_dashboard_project_navigation_uses_context_dropdown(self):
420
- self.assertNotIn('<button class="control-tab" data-panel-target="projects">Projects</button>', HTML)
421
- self.assertIn("const VIEW_ALL_PROJECTS_VALUE", HTML)
422
- self.assertIn(".filter(context => context.active || context.resolve_current)", HTML)
423
- self.assertNotIn(".filter(context => context.can_load_resolve !== false || context.active || context.resolve_current)", HTML)
424
- self.assertIn("View All Projects", HTML)
425
- self.assertIn("setPanel('projects')", HTML)
426
-
427
- def test_dashboard_menu_buttons_open_without_navigation(self):
428
- menu_branch = HTML.split("if (control.classList.contains('has-menu')) {", 1)[1].split("return;", 1)[0]
429
- self.assertIn("toggleNavDropdown(control);", menu_branch)
430
- self.assertNotIn("setPanel(", menu_branch)
431
-
432
- def test_output_root_rejects_source_adjacent_writes(self):
433
- with tempfile.TemporaryDirectory() as tmp:
434
- source_dir = os.path.join(tmp, "camera-card")
435
- os.makedirs(source_dir)
436
- source = os.path.join(source_dir, "A001_C001.mov")
437
- out = resolve_output_root(
438
- project_name="Example Project",
439
- project_id="project-123",
440
- analysis_root=source_dir,
441
- source_paths=[source],
442
- )
443
-
444
- self.assertFalse(out["success"])
445
- self.assertIn("source media directory", out["errors"][0])
446
-
447
- def test_stable_clip_directory_sanitizes_clip_name(self):
448
- dirname = stable_clip_directory({
449
- "clip_name": "../A001 C001.mov",
450
- "clip_id": "clip-123",
451
- "file_path": "/Volumes/ExampleMedia/A001 C001.mov",
452
- })
453
-
454
- self.assertNotIn("..", dirname)
455
- self.assertNotIn("/", dirname)
456
- self.assertTrue(dirname.startswith("a001-c001.mov-"))
457
-
458
- def test_stable_clip_hash_survives_media_pool_rename(self):
459
- """The folder hash is anchored to stable Resolve ids, not the display name."""
460
- before = {
461
- "clip_name": "c0001.mp4",
462
- "clip_id": "clip-123",
463
- "file_path": "/Volumes/Media/c0001.mp4",
464
- }
465
- after = dict(before, clip_name="ice_c0001.mp4")
466
-
467
- # The leading slug changes, but the trailing hash is identical.
468
- self.assertNotEqual(stable_clip_directory(before), stable_clip_directory(after))
469
- self.assertEqual(stable_clip_hash(before), stable_clip_hash(after))
470
- self.assertEqual(
471
- clip_directory_hash(stable_clip_directory(before)),
472
- clip_directory_hash(stable_clip_directory(after)),
473
- )
474
-
475
- def test_clip_directory_hash_extracts_trailing_hex_only(self):
476
- self.assertEqual(clip_directory_hash("c0001.mp4-c923852545ed"), "c923852545ed")
477
- self.assertEqual(clip_directory_hash("ice_c0001.mp4-c923852545ed"), "c923852545ed")
478
- # A bare hash folder (no slug) is also a valid clip report folder.
479
- self.assertEqual(clip_directory_hash("c923852545ed"), "c923852545ed")
480
- # No trailing 12-char hex token -> not a clip report folder.
481
- self.assertIsNone(clip_directory_hash("not-a-clip-folder"))
482
- self.assertIsNone(clip_directory_hash("plainname"))
483
-
484
- def test_canonical_hash_agrees_across_resolve_and_path_based_records(self):
485
- """Same media -> same canonical hash whether or not clip_id is present.
486
-
487
- The Resolve inventory carries clip_id; path-based batch records do not.
488
- Anchoring the canonical hash to the (normalized) file path removes that
489
- cross-basis mismatch so both surfaces agree on one folder.
490
- """
491
- resolve_record = {
492
- "clip_name": "c0001.mp4",
493
- "clip_id": "clip-123",
494
- "media_id": "media-9",
495
- "file_path": "/Volumes/Media/c0001.mp4",
496
- }
497
- path_based = {
498
- "clip_name": "c0001.mp4",
499
- "clip_id": None,
500
- "media_id": None,
501
- "file_path": "/Volumes/Media/c0001.mp4",
502
- }
503
- self.assertEqual(stable_clip_hash(resolve_record), stable_clip_hash(path_based))
504
- # Each record can still resolve the other's folder via the match set.
505
- self.assertIn(stable_clip_hash(path_based), stable_clip_match_hashes(resolve_record))
506
- self.assertIn(stable_clip_hash(resolve_record), stable_clip_match_hashes(path_based))
507
-
508
- def test_resolve_clip_directory_reuses_legacy_clip_id_folder(self):
509
- """Migration: a folder written under the old clip_id-first basis is
510
- reused on the next write instead of being orphaned under a new name."""
511
- with tempfile.TemporaryDirectory() as tmp:
512
- project_root = os.path.join(tmp, "project-root")
513
- record = {
514
- "clip_name": "c0001.mp4",
515
- "clip_id": "clip-123",
516
- "file_path": "/Volumes/Media/c0001.mp4",
517
- }
518
- # Legacy folder name: slug + hash(clip_id) (pre-canonical scheme).
519
- from src.utils.media_analysis import short_hash
520
- legacy_dir = os.path.join(
521
- project_root, "clips", f"c0001.mp4-{short_hash('clip-123', 12)}"
522
- )
523
- os.makedirs(legacy_dir, exist_ok=True)
524
-
525
- resolved = resolve_clip_directory(project_root, record)
526
- self.assertEqual(os.path.realpath(resolved), os.path.realpath(legacy_dir))
527
-
528
- def test_resolve_clip_directory_reuses_folder_after_rename(self):
529
- """A renamed clip writes back into its existing folder (no orphan)."""
530
- with tempfile.TemporaryDirectory() as tmp:
531
- project_root = os.path.join(tmp, "project-root")
532
- record = {
533
- "clip_name": "c0001.mp4",
534
- "clip_id": "clip-123",
535
- "file_path": "/Volumes/Media/c0001.mp4",
536
- }
537
- original = resolve_clip_directory(project_root, record)
538
- os.makedirs(original, exist_ok=True)
539
-
540
- renamed = dict(record, clip_name="ice_c0001.mp4")
541
- self.assertEqual(
542
- os.path.realpath(resolve_clip_directory(project_root, renamed)),
543
- os.path.realpath(original),
544
- )
545
-
546
- def test_resolve_clip_directory_mints_canonical_path_when_absent(self):
547
- with tempfile.TemporaryDirectory() as tmp:
548
- project_root = os.path.join(tmp, "project-root")
549
- record = {
550
- "clip_name": "c0001.mp4",
551
- "clip_id": "clip-123",
552
- "file_path": "/Volumes/Media/c0001.mp4",
553
- }
554
- resolved = resolve_clip_directory(project_root, record)
555
- self.assertEqual(
556
- os.path.basename(resolved.rstrip("/")), stable_clip_directory(record)
557
- )
558
-
559
- def _write_report(self, project_root, folder, clip_block):
560
- clip_dir = os.path.join(project_root, "clips", folder)
561
- os.makedirs(clip_dir, exist_ok=True)
562
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as handle:
563
- json.dump({"clip": clip_block}, handle)
564
- return clip_dir
565
-
566
- def test_clip_index_indexes_all_stable_ids_from_report(self):
567
- with tempfile.TemporaryDirectory() as tmp:
568
- project_root = os.path.join(tmp, "project-root")
569
- self._write_report(
570
- project_root,
571
- "c0001.mp4-deadbeef0001",
572
- {"clip_id": "clip-123", "media_id": "media-9",
573
- "file_path": "/Volumes/Media/c0001.mp4", "clip_name": "c0001.mp4"},
574
- )
575
- index = build_clip_index(project_root)
576
- self.assertTrue(os.path.isfile(clip_index_path(project_root)))
577
- h2f = index["hash_to_folder"]
578
- # Every stable id of the clip resolves to the same folder.
579
- for record in (
580
- {"file_path": "/Volumes/Media/c0001.mp4"},
581
- {"clip_id": "clip-123"},
582
- {"media_id": "media-9"},
583
- ):
584
- hashes = stable_clip_match_hashes(record)
585
- self.assertTrue(any(h in h2f for h in hashes), record)
586
- folders = {h2f[h] for h in hashes if h in h2f}
587
- self.assertEqual(folders, {"c0001.mp4-deadbeef0001"})
588
-
589
- def test_clip_index_matches_offline_clip_without_file_path(self):
590
- """The edge the manifest exists for: a clip analyzed while online (folder
591
- hashed on file path) that the live inventory later reports offline with
592
- no file path, only a clip_id. The index still resolves it. #51."""
593
- with tempfile.TemporaryDirectory() as tmp:
594
- project_root = os.path.join(tmp, "project-root")
595
- online = {
596
- "clip_name": "c0001.mp4",
597
- "clip_id": "clip-123",
598
- "file_path": "/Volumes/Media/c0001.mp4",
599
- }
600
- folder = stable_clip_directory(online)
601
- self._write_report(project_root, folder, dict(online))
602
-
603
- # Live record went offline: no file_path, basis falls back to clip_id.
604
- offline = {"clip_name": "c0001.mp4", "clip_id": "clip-123", "file_path": None}
605
- offline["clip_key"] = stable_clip_directory(offline)
606
- self.assertNotEqual(offline["clip_key"], folder)
607
- self.assertNotIn(
608
- stable_clip_hash(online), stable_clip_match_hashes(offline)
609
- ) # a folder-name scan would miss it
610
-
611
- status = _analysis_status_by_clip(project_root, [offline])
612
- entry = status.get(offline["clip_key"])
613
- self.assertIsNotNone(entry)
614
- self.assertEqual(entry["analysis_status"], "analyzed")
615
-
616
- def test_clip_index_rebuilds_when_report_added(self):
617
- with tempfile.TemporaryDirectory() as tmp:
618
- project_root = os.path.join(tmp, "project-root")
619
- self._write_report(
620
- project_root, "a-aaaaaaaa0001",
621
- {"clip_id": "clip-a", "file_path": "/m/a.mp4", "clip_name": "a.mp4"},
622
- )
623
- first = load_clip_index(project_root)
624
- self.assertEqual(len(first["hash_to_folder"]) >= 1, True)
625
-
626
- # Add a second report; a stale signature must trigger a rebuild.
627
- self._write_report(
628
- project_root, "b-bbbbbbbb0002",
629
- {"clip_id": "clip-b", "file_path": "/m/b.mp4", "clip_name": "b.mp4"},
630
- )
631
- second = load_clip_index(project_root)
632
- self.assertNotEqual(first["signature"], second["signature"])
633
- self.assertIn(stable_clip_hash({"clip_id": "clip-b", "file_path": "/m/b.mp4"}),
634
- second["hash_to_folder"])
635
-
636
- def test_analysis_status_counts_renamed_clip_as_analyzed(self):
637
- """Issue #51: a clip renamed after analysis still counts as analyzed."""
638
- with tempfile.TemporaryDirectory() as tmp:
639
- project_root = os.path.join(tmp, "project-root")
640
- record = {
641
- "clip_name": "c0001.mp4",
642
- "clip_id": "clip-123",
643
- "file_path": "/Volumes/Media/c0001.mp4",
644
- }
645
- # Report was written under the ORIGINAL name.
646
- original_dir = os.path.join(project_root, "clips", stable_clip_directory(record))
647
- os.makedirs(original_dir, exist_ok=True)
648
- with open(os.path.join(original_dir, "analysis.json"), "w", encoding="utf-8") as handle:
649
- json.dump({"clip": {"clip_id": "clip-123"}}, handle)
650
-
651
- # The clip is now renamed in the Media Pool; clip_key is recomputed
652
- # from the NEW name (as resolve_media_inventory would do).
653
- renamed = dict(record, clip_name="ice_c0001.mp4")
654
- renamed["clip_key"] = stable_clip_directory(renamed)
655
- self.assertFalse(
656
- os.path.isdir(os.path.join(project_root, "clips", renamed["clip_key"]))
657
- )
658
-
659
- status = _analysis_status_by_clip(project_root, [renamed])
660
- entry = status.get(renamed["clip_key"])
661
- self.assertIsNotNone(entry)
662
- self.assertEqual(entry["analysis_status"], "analyzed")
663
- self.assertTrue(os.path.isfile(entry["analysis_report_path"]))
664
-
665
- def test_analysis_status_counts_renamed_clip_from_reused_job_report(self):
666
- """Issue #51: rename also resolves via the jobs DB when the report is
667
- a reused batch report living outside the local clips/ dir."""
668
- with tempfile.TemporaryDirectory() as tmp:
669
- project_root = os.path.join(tmp, "active-root")
670
- os.makedirs(project_root, exist_ok=True)
671
- record = {
672
- "clip_name": "c0001.mp4",
673
- "clip_id": "clip-123",
674
- "file_path": "/Volumes/Media/c0001.mp4",
675
- }
676
- old_key = stable_clip_directory(record)
677
-
678
- # The actual report lives in a *different* project root (batch reuse),
679
- # not under {project_root}/clips/.
680
- source_dir = os.path.join(tmp, "source-root", "clips", old_key)
681
- os.makedirs(source_dir, exist_ok=True)
682
- report_path = os.path.join(source_dir, "analysis.json")
683
- with open(report_path, "w", encoding="utf-8") as handle:
684
- json.dump({"clip": {"clip_id": "clip-123"}}, handle)
685
-
686
- # The jobs DB recorded the clip under its OLD clip_key.
687
- conn = sqlite3.connect(os.path.join(project_root, "jobs.sqlite"))
688
- try:
689
- conn.execute(
690
- "CREATE TABLE jobs (job_id TEXT, name TEXT, updated_at TEXT)"
691
- )
692
- conn.execute(
693
- "CREATE TABLE job_clips (job_id TEXT, clip_key TEXT, status TEXT, "
694
- "cache_status TEXT, report_path TEXT, error TEXT, updated_at TEXT)"
695
- )
696
- conn.execute(
697
- "INSERT INTO jobs (job_id, name, updated_at) VALUES (?, ?, ?)",
698
- ("job-1", "batch", "2026-06-01T10:00:00Z"),
699
- )
700
- conn.execute(
701
- "INSERT INTO job_clips (job_id, clip_key, status, cache_status, "
702
- "report_path, error, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
703
- ("job-1", old_key, "skipped", "reused", report_path, None,
704
- "2026-06-01T10:00:00Z"),
705
- )
706
- conn.commit()
707
- finally:
708
- conn.close()
709
-
710
- # Clip renamed in the Media Pool -> clip_key recomputed from new name.
711
- renamed = dict(record, clip_name="ice_c0001.mp4")
712
- renamed["clip_key"] = stable_clip_directory(renamed)
713
- self.assertNotEqual(renamed["clip_key"], old_key)
714
-
715
- status = _analysis_status_by_clip(project_root, [renamed])
716
- entry = status.get(renamed["clip_key"])
717
- self.assertIsNotNone(entry)
718
- self.assertEqual(entry["analysis_status"], "analyzed")
719
-
720
- def test_capability_detection_never_installs(self):
721
- caps = detect_capabilities(env={})
722
-
723
- self.assertTrue(caps["success"])
724
- self.assertTrue(caps["no_auto_install"])
725
- self.assertTrue(caps["vision"]["enabled_by_default"])
726
- self.assertEqual(caps["vision"]["default_provider"], HOST_CHAT_PATHS_PROVIDER)
727
- self.assertTrue(caps["vision"]["available"])
728
-
729
- def test_request_capabilities_report_host_chat_paths_vision(self):
730
- with tempfile.TemporaryDirectory() as tmp:
731
- previous = os.environ.get("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS")
732
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = os.path.join(tmp, "prefs.json")
733
- try:
734
- report = _media_analysis_capabilities_for_request(None)
735
- self.assertTrue(report["vision"]["enabled_by_default"])
736
- self.assertTrue(report["vision"]["available"])
737
- self.assertEqual(report["vision"]["provider"], HOST_CHAT_PATHS_PROVIDER)
738
- self.assertEqual(report["vision"]["availability"], "ready")
739
- self.assertEqual(
740
- report["vision"]["host_chat_paths"]["commit_action"],
741
- {"tool": "media_analysis", "action": "commit_vision"},
742
- )
743
- self.assertEqual(
744
- report["vision"]["host_chat_paths"]["schema_reference"],
745
- VISION_SCHEMA_REFERENCE,
746
- )
747
- finally:
748
- if previous is None:
749
- os.environ.pop("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS", None)
750
- else:
751
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = previous
752
-
753
- def test_host_chat_paths_payload_emits_frame_paths_and_commit_action(self):
754
- with tempfile.TemporaryDirectory() as tmp:
755
- frame_path = os.path.join(tmp, "frame.jpg")
756
- with open(frame_path, "wb") as handle:
757
- handle.write(b"sample-frame-bytes")
758
-
759
- payload = build_host_chat_paths_payload(
760
- record={
761
- "clip_id": "clip-xyz-1",
762
- "clip_name": "A001_C001.mov",
763
- "file_path": "/Volumes/Media/A001_C001.mov",
764
- },
765
- motion={
766
- "analysis_keyframes": [{
767
- "frame_path": frame_path,
768
- "time_seconds": 0.0,
769
- "selection_reason": "first_usable",
770
- }],
771
- "effective_sample_budget": 1,
772
- "overall_motion_level": "low",
773
- "cut_analysis": {"cut_count": 0, "flash_frame_candidates": []},
774
- },
775
- options={"vision": {"enabled": True, "provider": HOST_CHAT_PATHS_PROVIDER}},
776
- artifacts={"clip_dir": os.path.join(tmp, "clips", "test")},
777
- )
778
-
779
- self.assertTrue(payload["success"])
780
- self.assertEqual(payload["status"], "pending_host_analysis")
781
- self.assertEqual(payload["provider"], HOST_CHAT_PATHS_PROVIDER)
782
- self.assertEqual(payload["frame_paths"], [os.path.realpath(frame_path)])
783
- self.assertEqual(payload["frame_count"], 1)
784
- self.assertEqual(payload["schema_reference"], VISION_SCHEMA_REFERENCE)
785
- self.assertEqual(payload["commit_action"]["tool"], "media_analysis")
786
- self.assertEqual(payload["commit_action"]["action"], "commit_vision")
787
- self.assertEqual(payload["commit_action"]["params"]["clip_id"], "clip-xyz-1")
788
- self.assertEqual(payload["commit_action"]["params"]["vision_token"], payload["vision_token"])
789
- self.assertTrue(vision_is_pending_host_analysis(payload))
790
-
791
- def test_host_chat_paths_payload_emits_shot_table_keyed_to_frame_indices(self):
792
- """Deferred payload must list every shot range with the frame indices that fall in it.
793
-
794
- The host chat needs this table to author one shot_descriptions entry per shot,
795
- grounded in the specific frame_indices for that shot rather than nearest neighbours.
796
- """
797
- with tempfile.TemporaryDirectory() as tmp:
798
- frames_dir = os.path.join(tmp, "frames")
799
- os.makedirs(frames_dir, exist_ok=True)
800
- paths = []
801
- times = [0.5, 1.5, 4.5, 5.5, 9.0]
802
- for index, _ in enumerate(times, 1):
803
- p = os.path.join(frames_dir, f"sampled_{index:04d}.jpg")
804
- with open(p, "wb") as handle:
805
- handle.write(b"x")
806
- paths.append(p)
807
- payload = build_host_chat_paths_payload(
808
- record={
809
- "clip_id": "clip-shots",
810
- "clip_name": "A001.mov",
811
- "file_path": "/Volumes/Media/A001.mov",
812
- },
813
- motion={
814
- "analysis_keyframes": [
815
- {"frame_path": paths[idx], "time_seconds": t, "selection_reason": "cut_before"}
816
- for idx, t in enumerate(times)
817
- ],
818
- "overall_motion_level": "high",
819
- "cut_analysis": {
820
- "cut_count": 2,
821
- "shot_ranges": [
822
- {"index": 1, "start": 0.0, "end": 2.0},
823
- {"index": 2, "start": 2.0, "end": 6.0},
824
- {"index": 3, "start": 6.0, "end": 10.0},
825
- ],
826
- "flash_frame_candidates": [],
827
- },
828
- },
829
- options={"vision": {"enabled": True, "provider": HOST_CHAT_PATHS_PROVIDER}},
830
- artifacts={"clip_dir": os.path.join(tmp, "clips", "shots")},
831
- )
832
-
833
- shot_table = payload["shot_table"]
834
- self.assertEqual([row["shot_index"] for row in shot_table], [1, 2, 3])
835
- by_index = {row["shot_index"]: row for row in shot_table}
836
- self.assertEqual(by_index[1]["frame_indices"], [1, 2])
837
- self.assertEqual(by_index[2]["frame_indices"], [3, 4])
838
- self.assertEqual(by_index[3]["frame_indices"], [5])
839
- for row in shot_table:
840
- self.assertTrue(row["has_in_shot_frame"])
841
- self.assertIn("shot_descriptions", payload["prompt"])
842
- self.assertIn("shot_table", payload["instructions"])
843
-
844
- def test_commit_visual_analysis_applies_shot_descriptions_per_shot_marker(self):
845
- """Each shot marker must inherit its own shot_descriptions[shot_index] description.
846
-
847
- Regression for the bug where _visual_description_for_time copied the
848
- temporally-nearest analysis_keyframe's description into every adjacent shot,
849
- producing wrong labels for shots without a per-shot keyframe.
850
- """
851
- with tempfile.TemporaryDirectory() as tmp:
852
- project_root = os.path.join(tmp, "project-root")
853
- clip_dir = os.path.join(project_root, "clips", "shotty")
854
- os.makedirs(clip_dir, exist_ok=True)
855
- existing = {
856
- "success": True,
857
- "analysis_version": "0.2",
858
- "analysis_signature": {"signature_hash": "feedface"},
859
- "clip": {"clip_id": "clip-shots", "clip_name": "A001.mov", "file_path": "/Volumes/Media/A001.mov"},
860
- "technical": {"video": [{"frame_rate": 24}]},
861
- "readthrough": {
862
- "scenes": {"items": []},
863
- "cut_analysis": {
864
- "cut_points": [],
865
- "shot_ranges": [
866
- {"index": 1, "start": 0.0, "end": 2.0},
867
- {"index": 2, "start": 2.0, "end": 6.0},
868
- {"index": 3, "start": 6.0, "end": 10.0},
869
- ],
870
- "flash_frame_candidates": [],
871
- },
872
- },
873
- "motion": {"analysis_keyframes": [], "overall_motion_level": "high"},
874
- "transcription": {"success": True, "segments": []},
875
- "visual": {
876
- "success": True,
877
- "status": "pending_host_analysis",
878
- "provider": HOST_CHAT_PATHS_PROVIDER,
879
- "vision_token": "tok-shots-aaaaaaaa",
880
- "frame_paths": [],
881
- },
882
- "analysis_profile": {"vision_enabled": True, "transcription_enabled": False},
883
- "vision_status": "pending_host_analysis",
884
- "vision_token": "tok-shots-aaaaaaaa",
885
- }
886
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as handle:
887
- json.dump(existing, handle)
888
-
889
- result = commit_visual_analysis(
890
- project_root=project_root,
891
- clip_id="clip-shots",
892
- vision_token="tok-shots-aaaaaaaa",
893
- visual={
894
- "clip_summary": "Three distinct shots: opener, B-roll, outro.",
895
- "shot_descriptions": [
896
- {"shot_index": 1, "time_seconds_start": 0.0, "time_seconds_end": 2.0,
897
- "description": "Opening fisheye on rental car at driver door."},
898
- {"shot_index": 2, "time_seconds_start": 2.0, "time_seconds_end": 6.0,
899
- "description": "Aerial driveway, no subjects in frame."},
900
- {"shot_index": 3, "time_seconds_start": 6.0, "time_seconds_end": 10.0,
901
- "description": "Interior driver POV at the wheel."},
902
- ],
903
- "analysis_keyframes": [
904
- {"time_seconds": 0.5, "description": "Opening fisheye keyframe."},
905
- ],
906
- },
907
- )
908
-
909
- self.assertTrue(result["success"])
910
- with open(result["marker_plan_json"], "r", encoding="utf-8") as handle:
911
- plan = json.load(handle)
912
- shot_markers = [m for m in plan["markers"] if m["type"] == "shot"]
913
- by_index = {m["id"]: m for m in shot_markers}
914
- self.assertEqual(by_index["shot-001"]["visual_description"], "Opening fisheye on rental car at driver door.")
915
- self.assertEqual(by_index["shot-002"]["visual_description"], "Aerial driveway, no subjects in frame.")
916
- self.assertEqual(by_index["shot-003"]["visual_description"], "Interior driver POV at the wheel.")
917
-
918
- def test_commit_visual_analysis_falls_back_when_shot_description_missing(self):
919
- """When a shot has no shot_descriptions entry and no in-range keyframe,
920
- the fallback must NOT pick a far-away keyframe — it should land on the
921
- clip_summary-tagged fallback so the marker is honest about being unverified.
922
- """
923
- with tempfile.TemporaryDirectory() as tmp:
924
- project_root = os.path.join(tmp, "project-root")
925
- clip_dir = os.path.join(project_root, "clips", "partial")
926
- os.makedirs(clip_dir, exist_ok=True)
927
- existing = {
928
- "success": True,
929
- "analysis_version": "0.2",
930
- "clip": {"clip_id": "clip-partial", "clip_name": "B001.mov", "file_path": "/Volumes/Media/B001.mov"},
931
- "technical": {"video": [{"frame_rate": 24}]},
932
- "readthrough": {
933
- "scenes": {"items": []},
934
- "cut_analysis": {
935
- "cut_points": [],
936
- "shot_ranges": [
937
- {"index": 1, "start": 0.0, "end": 2.0},
938
- {"index": 2, "start": 2.0, "end": 6.0},
939
- {"index": 3, "start": 6.0, "end": 10.0},
940
- ],
941
- "flash_frame_candidates": [],
942
- },
943
- },
944
- "motion": {"analysis_keyframes": [], "overall_motion_level": "high"},
945
- "transcription": {"success": True, "segments": []},
946
- "visual": {"success": True, "status": "pending_host_analysis", "provider": HOST_CHAT_PATHS_PROVIDER, "vision_token": "tok-partial-cccccccc"},
947
- "analysis_profile": {"vision_enabled": True},
948
- "vision_status": "pending_host_analysis",
949
- "vision_token": "tok-partial-cccccccc",
950
- }
951
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as handle:
952
- json.dump(existing, handle)
953
-
954
- result = commit_visual_analysis(
955
- project_root=project_root,
956
- clip_id="clip-partial",
957
- vision_token="tok-partial-cccccccc",
958
- visual={
959
- "clip_summary": "Cold open: rental car gag.",
960
- "shot_descriptions": [
961
- {"shot_index": 1, "time_seconds_start": 0.0, "time_seconds_end": 2.0,
962
- "description": "Opening fisheye on rental car."},
963
- ],
964
- "analysis_keyframes": [
965
- {"time_seconds": 0.5, "description": "Opening fisheye keyframe."},
966
- ],
967
- },
968
- )
969
-
970
- self.assertTrue(result["success"])
971
- with open(result["marker_plan_json"], "r", encoding="utf-8") as handle:
972
- plan = json.load(handle)
973
- shot_markers = {m["id"]: m for m in plan["markers"] if m["type"] == "shot"}
974
- self.assertEqual(shot_markers["shot-001"]["visual_description"], "Opening fisheye on rental car.")
975
- # Shot 2 has no per-shot description and no in-range keyframe → fallback to clip_summary
976
- # tagged so reviewers can tell it was inherited, not authored for this shot.
977
- self.assertIn("clip summary", shot_markers["shot-002"]["visual_description"].lower())
978
- self.assertNotIn("Opening fisheye", shot_markers["shot-002"]["visual_description"])
979
- self.assertIn("clip summary", shot_markers["shot-003"]["visual_description"].lower())
980
- self.assertNotIn("Opening fisheye", shot_markers["shot-003"]["visual_description"])
981
-
982
- def test_commit_visual_analysis_merges_into_existing_report(self):
983
- with tempfile.TemporaryDirectory() as tmp:
984
- project_root = os.path.join(tmp, "project-root")
985
- clip_dir = os.path.join(project_root, "clips", "demo-clip")
986
- os.makedirs(clip_dir, exist_ok=True)
987
- existing = {
988
- "success": True,
989
- "analysis_version": "0.1",
990
- "analysis_signature": {"signature_hash": "deadbeef"},
991
- "clip": {"clip_id": "clip-xyz-1", "clip_name": "A001_C001.mov", "file_path": "/Volumes/Media/A001_C001.mov"},
992
- "technical": {"video": [{"frame_rate": 24}]},
993
- "readthrough": {"scenes": {"items": []}, "cut_analysis": {"cut_points": [], "shot_ranges": []}},
994
- "motion": {"analysis_keyframes": [], "overall_motion_level": "low"},
995
- "transcription": {"success": True, "segments": []},
996
- "visual": {
997
- "success": True,
998
- "status": "pending_host_analysis",
999
- "provider": HOST_CHAT_PATHS_PROVIDER,
1000
- "vision_token": "abcdef1234567890",
1001
- "frame_paths": [],
1002
- },
1003
- "analysis_profile": {"vision_enabled": True, "transcription_enabled": False},
1004
- "vision_status": "pending_host_analysis",
1005
- "vision_token": "abcdef1234567890",
1006
- }
1007
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as handle:
1008
- json.dump(existing, handle)
1009
-
1010
- result = commit_visual_analysis(
1011
- project_root=project_root,
1012
- clip_id="clip-xyz-1",
1013
- vision_token="abcdef1234567890",
1014
- visual={
1015
- "clip_summary": "Static wide shot of an empty room.",
1016
- "editorial_classification": {"primary_use": "establishing", "select_potential": "low", "reason": "no subject"},
1017
- "editing_notes": {"best_moments": [], "qc_flags": [], "search_tags": ["empty", "wide"]},
1018
- },
1019
- )
1020
-
1021
- self.assertTrue(result["success"])
1022
- self.assertTrue(os.path.isfile(result["analysis_json"]))
1023
- self.assertTrue(os.path.isfile(result["visual_json"]))
1024
- self.assertTrue(os.path.isfile(result["marker_plan_json"]))
1025
- with open(result["analysis_json"], "r", encoding="utf-8") as handle:
1026
- updated = json.load(handle)
1027
- self.assertEqual(updated["visual"]["clip_summary"], "Static wide shot of an empty room.")
1028
- self.assertEqual(updated["visual"]["provider"], HOST_CHAT_PATHS_PROVIDER)
1029
- self.assertNotIn("vision_status", updated)
1030
- self.assertNotIn("vision_token", updated)
1031
- self.assertIn("vision_committed_at", updated)
1032
- self.assertIn("empty", updated["visual"]["editing_notes"]["search_tags"])
1033
-
1034
- def test_commit_visual_analysis_rejects_mismatched_vision_token(self):
1035
- with tempfile.TemporaryDirectory() as tmp:
1036
- project_root = os.path.join(tmp, "project-root")
1037
- clip_dir = os.path.join(project_root, "clips", "demo-clip")
1038
- os.makedirs(clip_dir, exist_ok=True)
1039
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as handle:
1040
- json.dump({
1041
- "clip": {"clip_id": "clip-xyz-2"},
1042
- "visual": {"vision_token": "real-token-aaaaaaaa"},
1043
- }, handle)
1044
-
1045
- result = commit_visual_analysis(
1046
- project_root=project_root,
1047
- clip_id="clip-xyz-2",
1048
- vision_token="wrong-token-bbbbbbbb",
1049
- visual={"clip_summary": "anything"},
1050
- )
1051
-
1052
- self.assertFalse(result["success"])
1053
- self.assertIn("vision_token mismatch", (result["error"].get("message","") if isinstance(result["error"], dict) else result["error"]))
1054
-
1055
- def test_publish_clip_metadata_uses_pre_resolved_report_path_bypassing_reanalysis(self):
1056
- """commit_vision auto-publish must NOT re-run analysis after a successful merge.
1057
-
1058
- When the wrapper passes `_pre_resolved_report_paths={clip_id: analysis_json}`,
1059
- `_publish_clip_metadata_from_analysis` must read the just-committed report
1060
- directly and skip `execute_media_analysis_plan_async`. The previous behavior
1061
- re-ran analysis, and any cache-reuse miss (path normalization drift,
1062
- signature mismatch, record-shape difference) surfaced as a silent-lie
1063
- `status=pending_host_vision_analysis` in the response even though the
1064
- on-disk artifact was finalized.
1065
- """
1066
- class _PublishClipStub:
1067
- def __init__(self, clip_id, clip_name, file_path):
1068
- self._id = clip_id
1069
- self._name = clip_name
1070
- self._props = {
1071
- "File Path": file_path,
1072
- "Duration": "00:00:06:00",
1073
- "FPS": "24",
1074
- "Resolution": "160x90",
1075
- "Type": "Video",
1076
- }
1077
- self.metadata = {}
1078
- self.third_party = {}
1079
- self.markers = {}
1080
-
1081
- def GetUniqueId(self):
1082
- return self._id
1083
-
1084
- def GetName(self):
1085
- return self._name
1086
-
1087
- def GetClipProperty(self, key=""):
1088
- if key in (None, ""):
1089
- return dict(self._props)
1090
- return self._props.get(key, "")
1091
-
1092
- def GetMediaId(self):
1093
- return self._id
1094
-
1095
- def GetMetadata(self, key=""):
1096
- if key in (None, ""):
1097
- return dict(self.metadata)
1098
- return self.metadata.get(key, "")
1099
-
1100
- def GetThirdPartyMetadata(self, key=""):
1101
- if key in (None, ""):
1102
- return dict(self.third_party)
1103
- return self.third_party.get(key, "")
1104
-
1105
- def SetMetadata(self, key, value):
1106
- self.metadata[key] = value
1107
- return True
1108
-
1109
- def SetThirdPartyMetadata(self, key, value):
1110
- self.third_party[key] = value
1111
- return True
1112
-
1113
- def AddMarker(self, frame, color, name, note, duration, custom_data=""):
1114
- self.markers[frame] = {"color": color, "name": name, "note": note, "duration": duration}
1115
- return True
1116
-
1117
- def GetMarkers(self):
1118
- return self.markers
1119
-
1120
- with tempfile.TemporaryDirectory() as tmp:
1121
- project_root = os.path.join(tmp, "20260517_sample-fc314309e4")
1122
- clip_dir = os.path.join(project_root, "clips", "demo-clip")
1123
- os.makedirs(clip_dir, exist_ok=True)
1124
- media_file = os.path.join(tmp, "demo.mp4")
1125
- with open(media_file, "wb") as h:
1126
- h.write(b"\x00" * 1024)
1127
-
1128
- report = {
1129
- "success": True,
1130
- "analysis_version": "0.2",
1131
- "analyzed_at": "2026-05-19T10:00:00Z",
1132
- "source_file": media_file,
1133
- "clip": {
1134
- "clip_id": "clip-pub-1",
1135
- "clip_name": "demo.mp4",
1136
- "file_path": media_file,
1137
- "duration_seconds": 6.0,
1138
- "fps": 24.0,
1139
- },
1140
- "technical": {"video": [{"duration_seconds": 6.0, "frame_rate": 24}]},
1141
- "readthrough": {"scenes": {"items": []}, "cut_analysis": {"cut_points": [], "shot_ranges": []}},
1142
- "motion": {"analysis_keyframes": [], "overall_motion_level": "low"},
1143
- "transcription": {"success": True, "segments": []},
1144
- "visual": {
1145
- "success": True,
1146
- "provider": HOST_CHAT_PATHS_PROVIDER,
1147
- "clip_summary": "Wide static shot.",
1148
- "editorial_classification": {"primary_use": "establishing", "select_potential": "medium", "reason": ""},
1149
- "content": {"locations": ["studio"], "people_visible": "unknown", "actions": [], "objects": [], "visible_text": [], "notable_audio_context": []},
1150
- "shot_and_style": {"shot_sizes": ["wide"], "camera_motion": ["static"], "composition_notes": "", "lighting_mood": "", "color_mood": ""},
1151
- "slate": {"slate_visible": False, "scene": "", "shot": "", "take": "", "camera": "", "roll": "", "date": "", "production": "", "visible_text": [], "confidence": {}},
1152
- "motion": {"overall_level": "low", "motion_events": [], "quiet_regions": []},
1153
- "cut_understanding": {"cut_count": 0, "likely_edited_sequence": False, "flash_frame_candidates": [], "notes": []},
1154
- "shot_descriptions": [
1155
- {"shot_index": 0, "description": "Wide static shot of a studio.", "time_seconds_start": 0.0, "time_seconds_end": 6.0, "qc_flags": []},
1156
- ],
1157
- "editing_notes": {"best_moments": [], "continuity_flags": [], "qc_flags": [], "search_tags": ["wide", "studio"]},
1158
- "confidence": {"visual": "high", "motion": "computed", "transcript": "unavailable"},
1159
- },
1160
- "analysis_signature": {"signature_hash": "test-abcdef"},
1161
- "analysis_profile": {"depth": "standard", "vision_enabled": True, "transcription_enabled": False},
1162
- "vision_committed_at": "2026-05-19T11:00:00Z",
1163
- "clip_analysis_markers": {"markers": [], "marker_count": 0},
1164
- }
1165
- report_path = os.path.join(clip_dir, "analysis.json")
1166
- with open(report_path, "w") as handle:
1167
- json.dump(report, handle)
1168
-
1169
- clip = _PublishClipStub("clip-pub-1", "demo.mp4", media_file)
1170
- mp = MarkerMediaPoolStub([clip])
1171
-
1172
- class _ProjectStub:
1173
- def __init__(self, media_pool):
1174
- self._mp = media_pool
1175
- def GetMediaPool(self):
1176
- return self._mp
1177
- def GetName(self):
1178
- return "20260517_Sample"
1179
- def GetUniqueId(self):
1180
- return "51657265-33d8-44d6-b000-fcdb46b72d67"
1181
-
1182
- proj = _ProjectStub(mp)
1183
-
1184
- called = {"execute_plan": False}
1185
- async def _sentinel_execute_plan(*args, **kwargs):
1186
- called["execute_plan"] = True
1187
- raise AssertionError(
1188
- "execute_media_analysis_plan_async called despite _pre_resolved_report_paths bypass"
1189
- )
1190
-
1191
- original_exec = _server_module.execute_media_analysis_plan_async
1192
- _server_module.execute_media_analysis_plan_async = _sentinel_execute_plan
1193
- try:
1194
- publish_params = {
1195
- "target": {"type": "clip", "clip_id": "clip-pub-1"},
1196
- "analysis_root": project_root,
1197
- "publish_metadata": True,
1198
- "confirm": True,
1199
- "dry_run": False,
1200
- "timed_markers": "no",
1201
- "vision": {"enabled": True, "provider": HOST_CHAT_PATHS_PROVIDER},
1202
- "transcription": {"enabled": False},
1203
- "fields": ["Description"],
1204
- "_pre_resolved_report_paths": {"clip-pub-1": report_path},
1205
- }
1206
- result = asyncio.run(
1207
- _publish_clip_metadata_from_analysis(proj, publish_params, None)
1208
- )
1209
- finally:
1210
- _server_module.execute_media_analysis_plan_async = original_exec
1211
-
1212
- self.assertFalse(called["execute_plan"], "fast path must skip execute_plan")
1213
- self.assertNotEqual(
1214
- result.get("status"), "pending_host_vision_analysis",
1215
- f"auto-publish silently lied with pending_host_vision_analysis; result={result}",
1216
- )
1217
- self.assertTrue(result.get("success"), f"publish failed: {result}")
1218
- self.assertTrue(
1219
- (result.get("analysis_manifest") or {}).get("pre_resolved"),
1220
- "manifest should be marked pre_resolved when the fast path was used",
1221
- )
1222
-
1223
- def test_review_api_list_clip_shot_frame(self):
1224
- """V2 Review API: bin grid + clip detail + shot detail + frame serving."""
1225
- with tempfile.TemporaryDirectory() as tmp:
1226
- project_root = os.path.join(tmp, "demo-project-aabbccdd")
1227
- clip_dir = os.path.join(project_root, "clips", "demo-clip")
1228
- os.makedirs(os.path.join(clip_dir, "frames"), exist_ok=True)
1229
- report = {
1230
- "clip": {"clip_id": "clip-rev-1", "clip_name": "Demo.mov"},
1231
- "analyzed_at": "2026-05-19T10:00:00Z",
1232
- "technical": {"video": [{"duration_seconds": 6.0}]},
1233
- "motion": {"analysis_keyframes": [
1234
- {"index": 1, "time_seconds": 0.1, "selection_reason": "shot_start"},
1235
- {"index": 2, "time_seconds": 3.0, "selection_reason": "shot_progress"},
1236
- ]},
1237
- "visual": {
1238
- "clip_summary": "Test summary.",
1239
- "editorial_classification": {"primary_use": "establishing", "select_potential": "high"},
1240
- "editing_notes": {"search_tags": ["test"]},
1241
- "shot_descriptions": [
1242
- {
1243
- "shot_index": 0,
1244
- "description": "Wide shot.",
1245
- "time_seconds_start": 0.0,
1246
- "time_seconds_end": 6.0,
1247
- "frame_indices_used": [1, 2],
1248
- "visual": {"shot_size": "wide"},
1249
- },
1250
- ],
1251
- },
1252
- }
1253
- with open(os.path.join(clip_dir, "analysis.json"), "w") as h:
1254
- json.dump(report, h)
1255
- jpeg = b"\xff\xd8\xff\xd9"
1256
- with open(os.path.join(clip_dir, "frames", "sampled_0001.jpg"), "wb") as h:
1257
- h.write(jpeg)
1258
-
1259
- listing = list_analyzed_clips(project_root)
1260
- self.assertTrue(listing["success"])
1261
- self.assertEqual(listing["count"], 1)
1262
- self.assertEqual(listing["clips"][0]["clip_id"], "clip-rev-1")
1263
- self.assertGreaterEqual(listing["clips"][0]["representative_frame_index"], 1)
1264
-
1265
- detail = get_analyzed_clip(project_root, "clip-rev-1")
1266
- self.assertTrue(detail["success"])
1267
- self.assertEqual(detail["shot_count"], 1)
1268
- self.assertEqual(detail["clip_summary"], "Test summary.")
1269
-
1270
- shot = get_analyzed_clip_shot(project_root, "clip-rev-1", 0)
1271
- self.assertTrue(shot["success"])
1272
- self.assertEqual(shot["shot"]["description"], "Wide shot.")
1273
- self.assertEqual(len(shot["frames"]), 2)
1274
-
1275
- self.assertIsNotNone(get_clip_frame_path(project_root, "clip-rev-1", 1))
1276
- self.assertIsNone(get_clip_frame_path(project_root, "clip-rev-1", 99))
1277
-
1278
- corrections = read_clip_corrections(project_root, "clip-rev-1")
1279
- self.assertTrue(corrections["success"])
1280
- self.assertEqual(corrections["current_field_count"], 0)
1281
-
1282
- def test_review_api_follows_reusable_batch_report_paths(self):
1283
- """Review API includes reusable reports referenced by the active project's job DB."""
1284
- with tempfile.TemporaryDirectory() as tmp:
1285
- active_root = os.path.join(tmp, "active-project-aabbccdd")
1286
- source_root = os.path.join(tmp, "source-project-eeff001122")
1287
- clip_dir = os.path.join(source_root, "clips", "reused-clip")
1288
- os.makedirs(os.path.join(clip_dir, "frames"), exist_ok=True)
1289
- report_path = os.path.join(clip_dir, "analysis.json")
1290
- report = {
1291
- "clip": {"clip_id": "clip-reused-1", "clip_name": "Reused.mov"},
1292
- "analyzed_at": "2026-05-19T10:00:00Z",
1293
- "technical": {"video": [{"duration_seconds": 6.0}]},
1294
- "motion": {"analysis_keyframes": [
1295
- {"index": 1, "time_seconds": 0.1, "selection_reason": "shot_start"},
1296
- ]},
1297
- "visual": {
1298
- "clip_summary": "Reusable summary.",
1299
- "editorial_classification": {"primary_use": "b_roll", "select_potential": "medium"},
1300
- "editing_notes": {"search_tags": ["reused"]},
1301
- "shot_descriptions": [
1302
- {
1303
- "shot_index": 1,
1304
- "description": "Reusable shot.",
1305
- "time_seconds_start": 0.0,
1306
- "time_seconds_end": 6.0,
1307
- "frame_indices_used": [1],
1308
- },
1309
- ],
1310
- },
1311
- }
1312
- with open(report_path, "w", encoding="utf-8") as handle:
1313
- json.dump(report, handle)
1314
- with open(os.path.join(clip_dir, "frames", "sampled_0001.jpg"), "wb") as handle:
1315
- handle.write(b"\xff\xd8\xff\xd9")
1316
-
1317
- os.makedirs(active_root, exist_ok=True)
1318
- conn = sqlite3.connect(os.path.join(active_root, "jobs.sqlite"))
1319
- try:
1320
- conn.execute(
1321
- "CREATE TABLE job_clips (clip_key TEXT, report_path TEXT, status TEXT, updated_at TEXT)"
1322
- )
1323
- conn.execute(
1324
- "INSERT INTO job_clips (clip_key, report_path, status, updated_at) VALUES (?, ?, ?, ?)",
1325
- ("reused-clip", report_path, "skipped", "2026-05-19T11:00:00Z"),
1326
- )
1327
- conn.commit()
1328
- finally:
1329
- conn.close()
1330
-
1331
- listing = list_analyzed_clips(active_root)
1332
- self.assertTrue(listing["success"])
1333
- self.assertEqual(listing["count"], 1)
1334
- self.assertEqual(listing["clips"][0]["clip_id"], "clip-reused-1")
1335
-
1336
- detail = get_analyzed_clip(active_root, "clip-reused-1")
1337
- self.assertTrue(detail["success"])
1338
- self.assertEqual(detail["clip_summary"], "Reusable summary.")
1339
-
1340
- self.assertIsNotNone(get_clip_frame_path(active_root, "clip-reused-1", 1))
1341
-
1342
- index = build_analysis_index(active_root)
1343
- self.assertTrue(index["success"])
1344
- self.assertEqual(index["counts"]["clips"], 1)
1345
- search = query_analysis_index(active_root, "Reusable", limit=5)
1346
- self.assertTrue(search["success"])
1347
- self.assertGreaterEqual(search["result_count"], 1)
1348
-
1349
- def test_commit_visual_analysis_preserves_human_corrections(self):
1350
- """V2 trust-but-fix-optionally contract: re-analysis preserves human edits."""
1351
- with tempfile.TemporaryDirectory() as tmp:
1352
- project_root = os.path.join(tmp, "project-root")
1353
- clip_dir = os.path.join(project_root, "clips", "demo-clip")
1354
- os.makedirs(clip_dir, exist_ok=True)
1355
- existing = {
1356
- "success": True,
1357
- "clip": {"clip_id": "clip-cor-1", "clip_name": "A001.mov", "file_path": "/tmp/A001.mov"},
1358
- "technical": {"video": [{"frame_rate": 24}]},
1359
- "readthrough": {"scenes": {"items": []}, "cut_analysis": {"cut_points": [], "shot_ranges": []}},
1360
- "motion": {"analysis_keyframes": [], "overall_motion_level": "low"},
1361
- "transcription": {"success": True, "segments": []},
1362
- "visual": {
1363
- "success": True,
1364
- "status": "pending_host_analysis",
1365
- "provider": HOST_CHAT_PATHS_PROVIDER,
1366
- },
1367
- "analysis_profile": {"vision_enabled": True, "transcription_enabled": False},
1368
- }
1369
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as handle:
1370
- json.dump(existing, handle)
1371
-
1372
- # Editor previously corrected the editorial_classification.primary_use
1373
- # for the clip, and a shot-level visual.shot_size for shot_index=0.
1374
- corrections = {
1375
- "schema_version": "2.0",
1376
- "clip_id": "clip-cor-1",
1377
- "current": {
1378
- "clip:clip-cor-1:editorial_classification.primary_use": {
1379
- "value": "interview",
1380
- "source": "human",
1381
- "author": "sam@bradfordoperations.com",
1382
- "timestamp": "2026-05-19T10:00:00Z",
1383
- },
1384
- "shot:0:visual.shot_size": {
1385
- "value": "medium_close",
1386
- "source": "human",
1387
- "author": "sam@bradfordoperations.com",
1388
- "timestamp": "2026-05-19T10:01:00Z",
1389
- },
1390
- },
1391
- "changelog": [],
1392
- }
1393
- with open(os.path.join(clip_dir, "corrections.json"), "w", encoding="utf-8") as handle:
1394
- json.dump(corrections, handle)
1395
-
1396
- # Machine re-analysis tries to overwrite with different values.
1397
- result = commit_visual_analysis(
1398
- project_root=project_root,
1399
- clip_id="clip-cor-1",
1400
- visual={
1401
- "clip_summary": "Reanalyzed clip summary.",
1402
- "editorial_classification": {
1403
- "primary_use": "establishing",
1404
- "select_potential": "low",
1405
- "reason": "machine reread",
1406
- },
1407
- "shot_descriptions": [
1408
- {"shot_index": 0, "description": "Wide opening shot.", "visual": {"shot_size": "wide"}},
1409
- ],
1410
- },
1411
- )
1412
-
1413
- self.assertTrue(result["success"], msg=result.get("error"))
1414
- metrics = result.get("corrections") or {}
1415
- self.assertEqual(metrics.get("preserved_count"), 2)
1416
- self.assertGreaterEqual(metrics.get("changelog_added"), 2)
1417
-
1418
- with open(result["analysis_json"], "r", encoding="utf-8") as handle:
1419
- updated = json.load(handle)
1420
- self.assertEqual(updated["visual"]["editorial_classification"]["primary_use"], "interview")
1421
- shot_zero = next(
1422
- s for s in updated["visual"]["shot_descriptions"]
1423
- if s.get("shot_index") == 0
1424
- )
1425
- self.assertEqual(shot_zero["visual"]["shot_size"], "medium_close")
1426
- # Machine description on the same shot is not a human-edited field, so it sticks
1427
- self.assertEqual(shot_zero.get("description"), "Wide opening shot.")
1428
-
1429
- with open(os.path.join(clip_dir, "corrections.json"), "r", encoding="utf-8") as handle:
1430
- updated_corrections = json.load(handle)
1431
- reasons = {entry.get("change_reason") for entry in updated_corrections["changelog"]}
1432
- self.assertIn("preserved across re-analysis", reasons)
1433
-
1434
- def test_publish_metadata_candidates_map_visual_report_to_resolve_fields(self):
1435
- report = {
1436
- "analysis_version": "0.1",
1437
- "summary": "Fallback summary",
1438
- "clip": {"clip_id": "clip-123", "clip_name": "A001_C001.mov"},
1439
- "technical_warnings": ["interlace analysis did not complete"],
1440
- "analysis_signature": {"signature_hash": "abc123"},
1441
- "visual": {
1442
- "clip_summary": "Interview shot with Taylor at the kitchen table.",
1443
- "editorial_classification": {
1444
- "primary_use": "interview",
1445
- "select_potential": "high",
1446
- "reason": "Clean eye-line and usable delivery.",
1447
- },
1448
- "content": {
1449
- "people": ["Taylor"],
1450
- "actions": ["speaking"],
1451
- "objects": ["coffee mug"],
1452
- "visible_text": ["Scene 12A Take 3"],
1453
- },
1454
- "slate": {
1455
- "slate_visible": True,
1456
- "visible_text": ["Scene 12A Take 3"],
1457
- "confidence": {"overall": "medium"},
1458
- },
1459
- "editing_notes": {
1460
- "best_moments": ["00:00:04 strong answer"],
1461
- "qc_flags": ["minor background noise"],
1462
- "search_tags": ["kitchen", "answer"],
1463
- },
1464
- },
1465
- }
1466
- detection = {
1467
- "files": [{
1468
- "clip_id": "clip-123",
1469
- "events": [{"type": "slate_clap", "time_seconds": 4.0, "frame": 96, "confidence": 0.91}],
1470
- }],
1471
- }
1472
-
1473
- proposal = _media_analysis_report_metadata_candidates(report, detection=detection)
1474
-
1475
- self.assertEqual(proposal["fields"]["Description"], "Interview shot with Taylor at the kitchen table.")
1476
- self.assertIn("kitchen", proposal["fields"]["Keywords"])
1477
- self.assertIn("slate clap", proposal["fields"]["Keywords"])
1478
- self.assertEqual(proposal["fields"]["People"], ["Taylor"])
1479
- self.assertIn("Confirmed slate clap", proposal["fields"]["Comments"])
1480
- self.assertIn("minor background noise", proposal["fields"]["Comments"])
1481
-
1482
- def test_publish_metadata_candidates_require_visual_confirmation_for_slate(self):
1483
- report = {
1484
- "analysis_version": "0.1",
1485
- "summary": "Fallback summary",
1486
- "clip": {"clip_id": "clip-123", "clip_name": "A001_C001.mov"},
1487
- "visual": {
1488
- "success": True,
1489
- "clip_summary": "Wide shot in a driveway.",
1490
- "content": {"actions": ["walking"]},
1491
- "editing_notes": {"search_tags": ["driveway"]},
1492
- },
1493
- }
1494
- detection = {
1495
- "files": [{
1496
- "clip_id": "clip-123",
1497
- "events": [{"type": "slate_clap", "time_seconds": 4.0, "frame": 96, "confidence": 0.91}],
1498
- }],
1499
- }
1500
-
1501
- audio_only = _media_analysis_report_metadata_candidates(report, detection=detection)
1502
- self.assertNotIn("slate clap", audio_only["fields"]["Keywords"])
1503
- self.assertNotIn("slate clap", audio_only["fields"]["Comments"])
1504
- self.assertFalse(audio_only["evidence"]["slate_visual_confirmed"])
1505
-
1506
- confirmed = _media_analysis_report_metadata_candidates(
1507
- report,
1508
- detection=detection,
1509
- slate_review={"slate_visible": True, "confidence": {"overall": "high"}},
1510
- )
1511
- self.assertIn("slate clap", confirmed["fields"]["Keywords"])
1512
- self.assertIn("Confirmed slate clap", confirmed["fields"]["Comments"])
1513
- self.assertTrue(confirmed["evidence"]["slate_visual_confirmed"])
1514
-
1515
- def test_publish_metadata_candidates_require_visual_summary_when_requested(self):
1516
- report = {
1517
- "analysis_version": "0.1",
1518
- "summary": "Fallback summary",
1519
- "clip": {"clip_id": "clip-123", "clip_name": "A001_C001.mov"},
1520
- "visual": {
1521
- "success": False,
1522
- "status": "skipped",
1523
- "reason": "Sampling unavailable.",
1524
- },
1525
- }
1526
-
1527
- proposal = _media_analysis_report_metadata_candidates(
1528
- report,
1529
- fields=["Description", "Comments"],
1530
- require_visual_description=True,
1531
- )
1532
-
1533
- self.assertNotIn("Description", proposal["fields"])
1534
- self.assertNotIn("Comments", proposal["fields"])
1535
- self.assertFalse(proposal["evidence"]["visual_analysis_succeeded"])
1536
-
1537
- def test_publish_metadata_candidates_do_not_put_transcript_in_comments(self):
1538
- report = {
1539
- "analysis_version": "0.1",
1540
- "clip": {"clip_id": "clip-123", "clip_name": "A001_C001.mov"},
1541
- "visual": {
1542
- "success": True,
1543
- "clip_summary": "Interview close-up with a clean answer.",
1544
- "content": {"people": ["Taylor"]},
1545
- },
1546
- "transcription": {
1547
- "success": True,
1548
- "backend": "mock",
1549
- "language": "en",
1550
- "text": "This is the readable answer that should appear in metadata comments.",
1551
- "segments": [
1552
- {"start": 0.0, "end": 1.2, "text": "This is the readable answer."},
1553
- {"start": 1.2, "end": 2.4, "text": "It should appear in metadata comments."},
1554
- ],
1555
- },
1556
- }
1557
-
1558
- proposal = _media_analysis_report_metadata_candidates(report, fields=["Description", "Comments", "People"])
1559
-
1560
- self.assertEqual(proposal["fields"]["Description"], "Interview close-up with a clean answer.")
1561
- self.assertNotIn("Transcript excerpt", proposal["fields"]["Comments"])
1562
- self.assertNotIn("readable answer", proposal["fields"]["Comments"])
1563
- self.assertEqual(proposal["fields"]["People"], ["Taylor"])
1564
-
1565
- def test_publish_metadata_candidates_gate_slate_fields_by_confidence(self):
1566
- report = {
1567
- "clip": {"clip_id": "clip-123"},
1568
- "visual": {"clip_summary": "Slate shot."},
1569
- }
1570
- fields = ["Scene", "Shot", "Take", "Camera #"]
1571
- low = _media_analysis_report_metadata_candidates(
1572
- report,
1573
- fields=fields,
1574
- slate_review={
1575
- "slate_visible": True,
1576
- "fields": {"scene": "12A", "take": "3", "camera": "B"},
1577
- "confidence": {"overall": "medium"},
1578
- },
1579
- )
1580
- high = _media_analysis_report_metadata_candidates(
1581
- report,
1582
- fields=fields,
1583
- slate_review={
1584
- "slate_visible": True,
1585
- "fields": {"scene": "12A", "take": "3", "camera": "B"},
1586
- "confidence": {"overall": "high"},
1587
- },
1588
- )
1589
-
1590
- self.assertEqual(low["fields"]["Scene"], "")
1591
- self.assertEqual(high["fields"]["Scene"], "12A")
1592
- self.assertEqual(high["fields"]["Take"], "3")
1593
- self.assertEqual(high["fields"]["Camera #"], "B")
1594
-
1595
- def test_publish_metadata_candidates_support_metadata_write_alias_fields(self):
1596
- report = {
1597
- "clip": {"clip_id": "clip-123"},
1598
- "visual": {
1599
- "clip_summary": "Slate shot.",
1600
- "content": {"actions": ["clap"]},
1601
- "editing_notes": {"search_tags": ["sync"]},
1602
- },
1603
- }
1604
- proposal = _media_analysis_report_metadata_candidates(
1605
- report,
1606
- fields=["Keyword", "Roll Card #"],
1607
- slate_review={
1608
- "slate_visible": True,
1609
- "fields": {"roll": "A001"},
1610
- "confidence": {"overall": "high"},
1611
- },
1612
- )
1613
-
1614
- self.assertIn("sync", proposal["fields"]["Keyword"])
1615
- self.assertEqual(proposal["fields"]["Roll Card #"], "A001")
1616
-
1617
- def test_publish_metadata_marker_candidates_built_but_writeback_gated_off_in_v2(self):
1618
- prefs_tmp = tempfile.TemporaryDirectory()
1619
- self.addCleanup(prefs_tmp.cleanup)
1620
- previous_prefs = os.environ.get("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS")
1621
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = os.path.join(prefs_tmp.name, "prefs.json")
1622
-
1623
- def restore_prefs_env():
1624
- if previous_prefs is None:
1625
- os.environ.pop("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS", None)
1626
- else:
1627
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = previous_prefs
1628
-
1629
- self.addCleanup(restore_prefs_env)
1630
-
1631
- report = {
1632
- "clip": {"clip_id": "clip-123"},
1633
- "visual": {
1634
- "editing_notes": {
1635
- "best_moments": ["00:00:04 strong answer"],
1636
- "qc_flags": ["00:00:05:12 small audio pop"],
1637
- },
1638
- },
1639
- "clip_analysis_markers": {
1640
- "markers": [
1641
- {
1642
- "id": "shot-001",
1643
- "type": "shot",
1644
- "name": "Shot 001",
1645
- "color": "Blue",
1646
- "start_frame": 0,
1647
- "duration_frames": 48,
1648
- "visual_description": "Wide shot in a driveway.",
1649
- "source": "scene_detection",
1650
- },
1651
- {
1652
- "id": "black-or-title-001",
1653
- "type": "qc_warning",
1654
- "subtype": "black_or_title",
1655
- "name": "QC: Black/Very Dark Range",
1656
- "color": "Red",
1657
- "start_frame": 48,
1658
- "duration_frames": 12,
1659
- "visual_description": "Detected black or very dark picture.",
1660
- "source": "blackdetect",
1661
- },
1662
- ],
1663
- },
1664
- }
1665
- record = {"clip_id": "clip-123", "fps": "24"}
1666
- sync_event = {"type": "slate_clap", "time_seconds": 1.25, "frame": 30, "confidence": 0.91}
1667
-
1668
- disabled = _media_analysis_marker_candidates_from_report(
1669
- report,
1670
- record,
1671
- sync_event,
1672
- None,
1673
- {"timed_markers": "no"},
1674
- slate_review={"slate_visible": True, "confidence": {"overall": "high"}},
1675
- )
1676
- unconfirmed = _media_analysis_marker_candidates_from_report(report, record, sync_event, "/tmp/report.json", {"write_markers": True})
1677
- enabled = _media_analysis_marker_candidates_from_report(
1678
- report,
1679
- record,
1680
- sync_event,
1681
- "/tmp/report.json",
1682
- {"write_markers": True},
1683
- slate_review={"slate_visible": True, "confidence": {"overall": "high"}},
1684
- )
1685
-
1686
- # V2 architecture: machine markers (per-shot, qc_warning, best_moment) are
1687
- # dropped from Resolve writeback entirely. The marker candidate list is
1688
- # still built (it's persisted to clip_analysis_markers.json and consumed
1689
- # by the DB / control panel), but the writeback gate (`enabled`) is
1690
- # always False so nothing is added to Resolve clip markers.
1691
- self.assertFalse(disabled["enabled"])
1692
- self.assertFalse(unconfirmed["enabled"], "V2 gate disables writeback regardless of write_markers=True")
1693
- self.assertFalse(enabled["enabled"], "V2 gate disables writeback even with slate visually confirmed")
1694
-
1695
- # The candidate list itself still contains the expected markers — only
1696
- # the writeback step is gated. This keeps the DB/control-panel path
1697
- # working while removing the Resolve-side clutter.
1698
- self.assertIn("Shot 001", [marker["name"] for marker in unconfirmed["markers"]])
1699
- self.assertIn("QC: Black/Very Dark Range", [marker["name"] for marker in unconfirmed["markers"]])
1700
- self.assertIn("Best Moment", [marker["name"] for marker in unconfirmed["markers"]])
1701
- self.assertIn("QC Warning", [marker["name"] for marker in unconfirmed["markers"]])
1702
- self.assertEqual(unconfirmed["skipped"][0]["reason"], "slate_not_visually_confirmed")
1703
- self.assertEqual([marker["name"] for marker in enabled["markers"]], [
1704
- "Shot 001",
1705
- "Slate Clap",
1706
- "QC: Black/Very Dark Range",
1707
- "Best Moment",
1708
- "QC Warning",
1709
- ])
1710
- self.assertEqual([marker["frame"] for marker in enabled["markers"]], [0, 30, 48, 96, 132])
1711
-
1712
- # _apply_media_analysis_clip_markers is unit-callable and still works
1713
- # when invoked directly with a marker list — it's just that the publish
1714
- # path never calls it under V2 because the writeback gate is off.
1715
- clip = MarkerClipStub()
1716
- applied = _apply_media_analysis_clip_markers(clip, enabled["markers"], {})
1717
- self.assertTrue(applied["success"])
1718
- self.assertEqual(len(clip.GetMarkers()), 5)
1719
-
1720
- with tempfile.TemporaryDirectory() as tmp:
1721
- previous = os.environ.get("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS")
1722
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = os.path.join(tmp, "prefs.json")
1723
- try:
1724
- self.assertTrue(_media_analysis_publish_confirmed({"write_markers": True, "dry_run": False}))
1725
- setup("set_defaults", {"ask_before_metadata_publish": True})
1726
- self.assertFalse(_media_analysis_publish_confirmed({"write_markers": True, "dry_run": False}))
1727
- finally:
1728
- if previous is None:
1729
- os.environ.pop("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS", None)
1730
- else:
1731
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = previous
1732
-
1733
- def test_sync_event_marker_write_requires_visual_slate_confirmation(self):
1734
- clip = MarkerClipStub("clip-123")
1735
- project = MarkerProjectStub([clip])
1736
- slate_suggestion = {
1737
- "scope": "media_pool_item",
1738
- "clip_id": "clip-123",
1739
- "clip_name": "A001_C001.mov",
1740
- "event_type": "slate_clap",
1741
- "event_time_seconds": 1.0,
1742
- "event_frame": 24,
1743
- "confidence": 0.95,
1744
- "eligible": True,
1745
- "requires_confirmation": True,
1746
- "marker": {
1747
- "frame": 24,
1748
- "color": "Cyan",
1749
- "name": "Sync: slate clap",
1750
- "note": "Detected slate clap at 1.0s.",
1751
- "duration": 1,
1752
- "custom_data": "mcp.sync_event:clip-123:slate_clap:24",
1753
- },
1754
- }
1755
- detection = {"files": [{"marker_suggestions": [slate_suggestion]}]}
1756
-
1757
- unconfirmed = _apply_sync_event_markers(project, detection, {"confirm": True})
1758
- self.assertTrue(unconfirmed["success"])
1759
- self.assertEqual(unconfirmed["added"], 0)
1760
- self.assertEqual(unconfirmed["skipped"][0]["reason"], "slate_not_visually_confirmed")
1761
- self.assertEqual(clip.GetMarkers(), {})
1762
-
1763
- confirmed_suggestion = dict(slate_suggestion)
1764
- confirmed_suggestion["slate_review"] = {"slate_visible": True, "confidence": {"overall": "high"}}
1765
- confirmed = _apply_sync_event_markers(
1766
- project,
1767
- {"files": [{"marker_suggestions": [confirmed_suggestion]}]},
1768
- {"confirm": True},
1769
- )
1770
- self.assertTrue(confirmed["success"])
1771
- self.assertEqual(confirmed["added"], 1)
1772
- self.assertIn(24, clip.GetMarkers())
1773
-
1774
- def test_timed_marker_decision_records_choice_but_v2_gate_disables_writeback(self):
1775
- """V2 architecture drops per-shot/qc/best_moment machine markers.
1776
-
1777
- `_media_analysis_timed_marker_decision` still records the user's choice
1778
- and saved-default preferences (so the API surface is unchanged), but
1779
- the V2 gate (V2_MACHINE_MARKER_WRITEBACK_ENABLED=False) forces
1780
- `enabled=False` and `prompt_required=False` regardless of the
1781
- underlying choice. The saved-default preference is still persisted for
1782
- forward-compat when the gate is reopened.
1783
- """
1784
- with tempfile.TemporaryDirectory() as tmp:
1785
- previous = os.environ.get("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS")
1786
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = os.path.join(tmp, "prefs.json")
1787
- try:
1788
- defaulted = _media_analysis_timed_marker_decision({})
1789
- self.assertFalse(defaulted["enabled"])
1790
- self.assertFalse(defaulted["prompt_required"])
1791
- self.assertEqual(defaulted["source"], "v2_disabled")
1792
-
1793
- ask = _media_analysis_timed_marker_decision({"timed_markers": "ask"})
1794
- self.assertFalse(ask["enabled"])
1795
- self.assertFalse(ask["prompt_required"])
1796
- self.assertEqual(ask["source"], "v2_disabled")
1797
-
1798
- yes = _media_analysis_timed_marker_decision({"timed_markers": "yes"})
1799
- self.assertFalse(yes["enabled"])
1800
- self.assertEqual(yes["source"], "v2_disabled")
1801
-
1802
- default_yes = _media_analysis_timed_marker_decision({"timed_markers": "default_yes"})
1803
- self.assertFalse(default_yes["enabled"])
1804
- self.assertEqual(default_yes["saved_default"], "yes")
1805
-
1806
- saved = _media_analysis_timed_marker_decision({})
1807
- self.assertFalse(saved["enabled"])
1808
- self.assertFalse(saved["prompt_required"])
1809
-
1810
- default_no = _media_analysis_timed_marker_decision({"timed_markers": "default_no"})
1811
- self.assertFalse(default_no["enabled"])
1812
- self.assertEqual(default_no["saved_default"], "no")
1813
- finally:
1814
- if previous is None:
1815
- os.environ.pop("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS", None)
1816
- else:
1817
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = previous
1818
-
1819
- def test_setup_tool_sets_conversation_defaults(self):
1820
- with tempfile.TemporaryDirectory() as tmp:
1821
- previous_analysis = os.environ.get("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS")
1822
- previous_updates = os.environ.get(update_check.ENV_STATE_PATH)
1823
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = os.path.join(tmp, "analysis-prefs.json")
1824
- os.environ[update_check.ENV_STATE_PATH] = os.path.join(tmp, "update-state.json")
1825
- try:
1826
- configured = setup("set_defaults", {
1827
- "defaults": {
1828
- "media_analysis": {
1829
- "timed_markers_default": "default_yes",
1830
- "slate_detection_default": "yes",
1831
- "vision_default": "technical_only",
1832
- "transcription_default": "no",
1833
- "analysis_persistence": "keep_reports",
1834
- "metadata_publish_fields": ["Description", "Comments", "Scene"],
1835
- "metadata_overwrite_policy": "fill_empty",
1836
- "timed_marker_types": ["best_moments"],
1837
- "timed_marker_colors": {"best_moments": "Pink"},
1838
- "max_timed_markers_per_clip": 2,
1839
- "include_confidence_scores": False,
1840
- "include_source_time_notes": False,
1841
- "analysis_summary_style": "qc",
1842
- "report_format": "full",
1843
- "preferred_analysis_root": os.path.join(tmp, "analysis"),
1844
- "preferred_generated_media_folder": os.path.join(tmp, "generated"),
1845
- "default_post_operation_page": "edit",
1846
- "marker_custom_data": "minimal",
1847
- "metadata_writeback_default": False,
1848
- "ask_before_metadata_publish": True,
1849
- "dry_run_first_default": False,
1850
- },
1851
- "updates": {"mode": "notify", "check_interval_hours": 6, "snooze_hours": 3},
1852
- },
1853
- })
1854
- self.assertTrue(configured["success"])
1855
- self.assertEqual(configured["defaults"]["media_analysis"]["timed_markers_default"], "yes")
1856
- self.assertEqual(configured["defaults"]["media_analysis"]["slate_detection_default"], "yes")
1857
- self.assertEqual(configured["defaults"]["media_analysis"]["vision_default"], "technical_only")
1858
- self.assertEqual(configured["defaults"]["media_analysis"]["analysis_persistence"], "keep_reports")
1859
- self.assertEqual(configured["defaults"]["media_analysis"]["metadata_publish_fields"], ["Description", "Comments", "Scene"])
1860
- self.assertEqual(configured["defaults"]["media_analysis"]["metadata_overwrite_policy"], "fill_empty")
1861
- self.assertEqual(configured["defaults"]["media_analysis"]["timed_marker_types"], ["best_moments"])
1862
- self.assertEqual(configured["defaults"]["media_analysis"]["timed_marker_colors"]["best_moments"], "Pink")
1863
- self.assertEqual(configured["defaults"]["media_analysis"]["max_timed_markers_per_clip"], 2)
1864
- self.assertFalse(configured["defaults"]["media_analysis"]["include_confidence_scores"])
1865
- self.assertFalse(configured["defaults"]["media_analysis"]["metadata_writeback_default"])
1866
- self.assertEqual(configured["defaults"]["updates"]["mode"], "notify")
1867
- self.assertEqual(configured["defaults"]["updates"]["check_interval_hours"], 6)
1868
- self.assertEqual(configured["defaults"]["updates"]["snooze_hours"], 3)
1869
-
1870
- applied = _media_analysis_apply_setup_defaults("publish_clip_metadata", {"confirm": True})
1871
- self.assertFalse(applied["publish_metadata"])
1872
- self.assertEqual(applied["fields"], ["Description", "Comments", "Scene"])
1873
- self.assertEqual(applied["merge_policy"], "fill_empty")
1874
- self.assertFalse(applied["dry_run"])
1875
- self.assertEqual(applied["slate_detection"]["enabled"], True)
1876
- self.assertEqual(applied["vision"]["enabled"], False)
1877
-
1878
- report = {
1879
- "clip": {"clip_id": "clip-123"},
1880
- "visual": {"editing_notes": {"best_moments": ["00:00:04 strong answer"], "qc_flags": ["00:00:05:12 pop"]}},
1881
- }
1882
- markers = _media_analysis_marker_candidates_from_report(report, {"clip_id": "clip-123", "fps": "24"}, {"frame": 12, "confidence": 0.8}, "/tmp/report.json", applied)
1883
- self.assertEqual(len(markers["markers"]), 1)
1884
- self.assertEqual(markers["markers"][0]["name"], "Best Moment")
1885
- self.assertEqual(markers["markers"][0]["color"], "Pink")
1886
- self.assertNotIn("analysis_report_path", markers["markers"][0]["custom_data"])
1887
-
1888
- cleared = setup("clear_defaults", {
1889
- "keys": ["media_analysis", "updates.mode", "updates.check_interval_hours", "updates.snooze_hours"],
1890
- })
1891
- self.assertTrue(cleared["success"])
1892
- # After clear_defaults, timed_markers_default falls back to the
1893
- # server default ("ask"), which is normalized to None in the
1894
- # effective-prefs response (only "yes"/"no" survive).
1895
- self.assertIsNone(cleared["defaults"]["media_analysis"]["timed_markers_default"])
1896
- self.assertEqual(cleared["defaults"]["media_analysis"]["vision_default"], _media_analysis_effective_preferences()["vision_default"])
1897
- self.assertEqual(cleared["defaults"]["updates"]["mode"], "prompt")
1898
- self.assertEqual(cleared["defaults"]["updates"]["check_interval_hours"], 24.0)
1899
- self.assertEqual(cleared["defaults"]["updates"]["snooze_hours"], 24.0)
1900
-
1901
- unknown = setup("set_defaults", {"defaults": {"not_a_real_default": True}})
1902
- self.assertIn("error", unknown)
1903
- finally:
1904
- if previous_analysis is None:
1905
- os.environ.pop("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS", None)
1906
- else:
1907
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = previous_analysis
1908
- if previous_updates is None:
1909
- os.environ.pop(update_check.ENV_STATE_PATH, None)
1910
- else:
1911
- os.environ[update_check.ENV_STATE_PATH] = previous_updates
1912
-
1913
- def test_media_analysis_defaults_turn_on_host_chat_paths_vision(self):
1914
- with tempfile.TemporaryDirectory() as tmp:
1915
- previous = os.environ.get("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS")
1916
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = os.path.join(tmp, "prefs.json")
1917
- try:
1918
- applied = _media_analysis_apply_setup_defaults("analyze_clip", {})
1919
- self.assertEqual(applied["vision"], {"enabled": True, "provider": HOST_CHAT_PATHS_PROVIDER})
1920
- self.assertEqual(applied["transcription"], {"enabled": True, "allow_model_download": True})
1921
- self.assertTrue(applied["publish_metadata"])
1922
- self.assertTrue(_media_analysis_metadata_writeback_enabled(applied))
1923
- self.assertEqual(applied["_setup_defaults_applied"]["vision_default"], "on")
1924
- self.assertEqual(applied["_setup_defaults_applied"]["transcription_default"], "yes")
1925
- self.assertTrue(applied["_setup_defaults_applied"]["metadata_writeback_default"])
1926
-
1927
- with open(os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"], "w", encoding="utf-8") as handle:
1928
- json.dump({"transcription_default": "bogus"}, handle)
1929
- self.assertEqual(_media_analysis_effective_preferences()["transcription_default"], "yes")
1930
-
1931
- writeback_off = _media_analysis_apply_setup_defaults("analyze_clip", {"publish_metadata": False})
1932
- self.assertFalse(_media_analysis_metadata_writeback_enabled(writeback_off))
1933
-
1934
- visuals_off = _media_analysis_apply_setup_defaults("analyze_clip", {"include_visuals": False})
1935
- self.assertEqual(visuals_off["vision"], {"enabled": False})
1936
- self.assertFalse(visuals_off["_setup_defaults_applied"]["include_visuals"])
1937
-
1938
- visuals_on = _media_analysis_apply_setup_defaults("analyze_clip", {"includeVisuals": True})
1939
- self.assertEqual(visuals_on["vision"], {"enabled": True, "provider": HOST_CHAT_PATHS_PROVIDER})
1940
- self.assertTrue(visuals_on["_setup_defaults_applied"]["include_visuals"])
1941
-
1942
- vision_bool = _media_analysis_apply_setup_defaults("analyze_clip", {"vision": True})
1943
- self.assertEqual(vision_bool["vision"], {"enabled": True, "provider": HOST_CHAT_PATHS_PROVIDER})
1944
- self.assertTrue(vision_bool["_setup_defaults_applied"]["vision_shorthand"])
1945
-
1946
- vision_false = _media_analysis_apply_setup_defaults("analyze_clip", {"vision": False})
1947
- self.assertEqual(vision_false["vision"], {"enabled": False})
1948
- self.assertFalse(vision_false["_setup_defaults_applied"]["vision_shorthand"])
1949
-
1950
- transcription_on = _media_analysis_apply_setup_defaults("analyze_clip", {"include_transcription": True})
1951
- self.assertEqual(transcription_on["transcription"], {"enabled": True, "allow_model_download": True})
1952
- self.assertTrue(transcription_on["_setup_defaults_applied"]["include_transcription"])
1953
-
1954
- publish_defaults = _media_analysis_apply_setup_defaults("publish_clip_metadata", {})
1955
- self.assertFalse(publish_defaults["dry_run"])
1956
- self.assertTrue(publish_defaults["confirm"])
1957
- self.assertEqual(publish_defaults["fields"], [
1958
- "Description",
1959
- "Comments",
1960
- "Keywords",
1961
- "People",
1962
- "Scene",
1963
- "Shot",
1964
- "Take",
1965
- "Camera #",
1966
- "Roll Card #",
1967
- ])
1968
- self.assertEqual(publish_defaults["slate_detection"]["enabled"], True)
1969
- finally:
1970
- if previous is None:
1971
- os.environ.pop("DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS", None)
1972
- else:
1973
- os.environ["DAVINCI_RESOLVE_MCP_MEDIA_ANALYSIS_PREFS"] = previous
1974
-
1975
- def test_unlimited_timed_marker_limit_keeps_all_candidates(self):
1976
- report = {
1977
- "visual": {
1978
- "editing_notes": {
1979
- "best_moments": [
1980
- "00:00:01 strong answer",
1981
- "00:00:02 useful reaction",
1982
- "00:00:03 clean transition",
1983
- ]
1984
- }
1985
- }
1986
- }
1987
- markers = _media_analysis_marker_candidates_from_report(
1988
- report,
1989
- {"clip_id": "clip-123", "fps": "24"},
1990
- None,
1991
- "/tmp/report.json",
1992
- {"markers": {"marker_types": ["best_moments"], "max_markers": 0}},
1993
- )
1994
- self.assertEqual(len(markers["markers"]), 3)
1995
- self.assertFalse([item for item in markers["skipped"] if item.get("reason") == "max_markers_reached"])
1996
-
1997
- def test_publish_metadata_merge_updates_owned_blocks_and_dedupes_lists(self):
1998
- comments = _media_analysis_merge_metadata_field(
1999
- "Comments",
2000
- "Assistant note\n\n[DaVinci Resolve MCP Analysis]\nold\n[/DaVinci Resolve MCP Analysis]",
2001
- "Summary: new analysis",
2002
- {},
2003
- )
2004
- keywords = _media_analysis_merge_metadata_field(
2005
- "Keywords",
2006
- "kitchen, interview",
2007
- ["Interview", "answer"],
2008
- {},
2009
- )
2010
- scene = _media_analysis_merge_metadata_field("Scene", "12B", "12A", {})
2011
-
2012
- self.assertTrue(comments["changed"])
2013
- self.assertIn("Assistant note", comments["value"])
2014
- self.assertIn("Summary: new analysis", comments["value"])
2015
- self.assertEqual(keywords["value"], "kitchen, interview, answer")
2016
- self.assertEqual(keywords["added"], ["answer"])
2017
- self.assertFalse(scene["changed"])
2018
- self.assertEqual(scene["reason"], "preserved_existing_fill_empty_field")
2019
-
2020
- def test_publish_metadata_provenance_uses_namespaced_third_party_keys(self):
2021
- provenance = _media_analysis_provenance_metadata(
2022
- {
2023
- "analysis_version": "0.1",
2024
- "analysis_signature": {"signature_hash": "abc123"},
2025
- },
2026
- "/tmp/analysis.json",
2027
- ["Description", "Keywords"],
2028
- )
2029
-
2030
- self.assertEqual(provenance["davinci_resolve_mcp.analysis_signature"], "abc123")
2031
- self.assertEqual(provenance["davinci_resolve_mcp.analysis_report_path"], "/tmp/analysis.json")
2032
- self.assertIn("Description", provenance["davinci_resolve_mcp.changed_fields"])
2033
-
2034
- def test_build_plan_reports_artifacts_under_project_root(self):
2035
- with tempfile.TemporaryDirectory() as tmp:
2036
- source = os.path.join(tmp, "source", "A001_C001.mov")
2037
- os.makedirs(os.path.dirname(source))
2038
- records = [{
2039
- "clip_id": "clip-123",
2040
- "clip_name": "A001_C001.mov",
2041
- "file_path": source,
2042
- "media_id": "media-123",
2043
- }]
2044
- plan = build_plan(
2045
- project_name="Example Project",
2046
- project_id="project-123",
2047
- records=records,
2048
- target={"type": "clip", "clip_id": "clip-123"},
2049
- params={"analysis_root": os.path.join(tmp, "analysis"), "depth": "standard"},
2050
- capabilities={
2051
- "tools": {
2052
- "ffprobe": {"available": True},
2053
- "ffmpeg": {"available": True},
2054
- },
2055
- "transcription": {"available": False},
2056
- "vision": {"available": False},
2057
- },
2058
- )
2059
-
2060
- self.assertTrue(plan["success"])
2061
- artifact = plan["clips"][0]["artifacts"]["analysis_json"]
2062
- self.assertTrue(artifact.startswith(plan["output_root"]["project_root"]))
2063
- marker_artifact = plan["clips"][0]["artifacts"]["marker_plan_json"]
2064
- self.assertTrue(marker_artifact.startswith(plan["output_root"]["project_root"]))
2065
- self.assertTrue(marker_artifact.endswith("clip_analysis_markers.json"))
2066
- self.assertEqual(plan["analysis_keyframe_budget_per_clip"], 8)
2067
-
2068
- def test_build_plan_allows_chat_context_vision_without_external_provider_env(self):
2069
- with tempfile.TemporaryDirectory() as tmp:
2070
- source = os.path.join(tmp, "source", "A001_C001.mov")
2071
- os.makedirs(os.path.dirname(source))
2072
- records = [{
2073
- "clip_id": "clip-123",
2074
- "clip_name": "A001_C001.mov",
2075
- "file_path": source,
2076
- "media_id": "media-123",
2077
- }]
2078
- plan = build_plan(
2079
- project_name="Example Project",
2080
- project_id="project-123",
2081
- records=records,
2082
- target={"type": "clip", "clip_id": "clip-123"},
2083
- params={
2084
- "analysis_root": os.path.join(tmp, "analysis"),
2085
- "depth": "standard",
2086
- "transcription": {"enabled": False},
2087
- "vision": {"enabled": True, "provider": "chat_context"},
2088
- },
2089
- capabilities={
2090
- "tools": {
2091
- "ffprobe": {"available": True},
2092
- "ffmpeg": {"available": True},
2093
- },
2094
- "transcription": {"available": False},
2095
- "vision": {"available": False, "provider": None},
2096
- },
2097
- )
2098
-
2099
- self.assertTrue(plan["success"])
2100
- self.assertEqual(plan["capability_gaps"], [])
2101
-
2102
- def test_build_plan_hints_when_transcription_available_but_explicitly_disabled(self):
2103
- with tempfile.TemporaryDirectory() as tmp:
2104
- source = os.path.join(tmp, "source", "A001_C001.mov")
2105
- os.makedirs(os.path.dirname(source))
2106
- records = [{
2107
- "clip_id": "clip-123",
2108
- "clip_name": "A001_C001.mov",
2109
- "file_path": source,
2110
- "media_id": "media-123",
2111
- }]
2112
- plan = build_plan(
2113
- project_name="Example Project",
2114
- project_id="project-123",
2115
- records=records,
2116
- target={"type": "clip", "clip_id": "clip-123"},
2117
- params={
2118
- "analysis_root": os.path.join(tmp, "analysis"),
2119
- "depth": "standard",
2120
- "transcription": {"enabled": False},
2121
- },
2122
- capabilities={
2123
- "tools": {
2124
- "ffprobe": {"available": True},
2125
- "ffmpeg": {"available": True},
2126
- },
2127
- "transcription": {"available": True, "backends": ["whisper_cli"]},
2128
- "vision": {"available": False},
2129
- },
2130
- )
2131
-
2132
- self.assertTrue(plan["success"])
2133
- self.assertTrue(any("Transcription is available but disabled" in note for note in plan["notes"]))
2134
-
2135
- def test_build_plan_defaults_transcription_on_for_standard_depth(self):
2136
- with tempfile.TemporaryDirectory() as tmp:
2137
- source = os.path.join(tmp, "source", "A001_C001.mov")
2138
- os.makedirs(os.path.dirname(source))
2139
- records = [{
2140
- "clip_id": "clip-123",
2141
- "clip_name": "A001_C001.mov",
2142
- "file_path": source,
2143
- "media_id": "media-123",
2144
- }]
2145
- plan = build_plan(
2146
- project_name="Example Project",
2147
- project_id="project-123",
2148
- records=records,
2149
- target={"type": "clip", "clip_id": "clip-123"},
2150
- params={"analysis_root": os.path.join(tmp, "analysis"), "depth": "standard"},
2151
- capabilities={
2152
- "tools": {
2153
- "ffprobe": {"available": True},
2154
- "ffmpeg": {"available": True},
2155
- },
2156
- "transcription": {"available": True, "backends": ["mock"]},
2157
- "vision": {"available": False},
2158
- },
2159
- )
2160
-
2161
- self.assertTrue(plan["success"])
2162
- self.assertEqual(plan["capability_gaps"], [])
2163
- self.assertTrue(plan["clips"][0]["analysis_signature"]["layers"]["transcription"]["enabled"])
2164
- self.assertIn("transcript_json", plan["clips"][0]["artifacts"])
2165
-
2166
- def test_build_plan_blocks_default_transcription_when_backend_missing(self):
2167
- with tempfile.TemporaryDirectory() as tmp:
2168
- source = os.path.join(tmp, "source", "A001_C001.mov")
2169
- os.makedirs(os.path.dirname(source))
2170
- records = [{
2171
- "clip_id": "clip-123",
2172
- "clip_name": "A001_C001.mov",
2173
- "file_path": source,
2174
- "media_id": "media-123",
2175
- }]
2176
- plan = build_plan(
2177
- project_name="Example Project",
2178
- project_id="project-123",
2179
- records=records,
2180
- target={"type": "clip", "clip_id": "clip-123"},
2181
- params={"analysis_root": os.path.join(tmp, "analysis"), "depth": "standard"},
2182
- capabilities={
2183
- "tools": {
2184
- "ffprobe": {"available": True},
2185
- "ffmpeg": {"available": True},
2186
- },
2187
- "transcription": {"available": False, "backends": []},
2188
- "vision": {"available": False},
2189
- },
2190
- )
2191
- response = _media_analysis_missing_capabilities_response(plan)
2192
-
2193
- self.assertTrue(plan["success"])
2194
- self.assertIn({"capability": "transcription_backend", "required_for": ["transcription"]}, plan["capability_gaps"])
2195
- self.assertIn("transcription", plan["install_guidance"]["missing"])
2196
- self.assertFalse(response["success"])
2197
- self.assertEqual(response["status"], "missing_required_capabilities")
2198
- self.assertIn("install", response["next_step"].lower())
2199
-
2200
- def test_build_plan_reuses_existing_complete_report(self):
2201
- with tempfile.TemporaryDirectory() as tmp:
2202
- source = os.path.join(tmp, "source", "A001_C001.mov")
2203
- os.makedirs(os.path.dirname(source))
2204
- open(source, "wb").close()
2205
- record = {
2206
- "clip_id": "clip-123",
2207
- "clip_name": "A001_C001.mov",
2208
- "file_path": source,
2209
- "media_id": "media-123",
2210
- }
2211
- root = resolve_output_root(
2212
- project_name="Example Project",
2213
- project_id="project-123",
2214
- analysis_root=os.path.join(tmp, "analysis"),
2215
- source_paths=[source],
2216
- create=True,
2217
- )["project_root"]
2218
- clip_dir = os.path.join(root, "clips", stable_clip_directory(record))
2219
- os.makedirs(clip_dir)
2220
- report_path = os.path.join(clip_dir, "analysis.json")
2221
- with open(report_path, "w", encoding="utf-8") as f:
2222
- json.dump({
2223
- "success": True,
2224
- "source_file": source,
2225
- "clip": record,
2226
- "technical": {"format": {"duration_seconds": 1.0}},
2227
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
2228
- "motion": {"success": True, "analysis_keyframes": []},
2229
- "transcription": {"success": True, "status": "skipped"},
2230
- "visual": {"success": True, "status": "skipped"},
2231
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
2232
- }, f)
2233
- params = {
2234
- "analysis_root": os.path.join(tmp, "analysis"),
2235
- "depth": "standard",
2236
- "dry_run": False,
2237
- "transcription": {"enabled": False},
2238
- }
2239
- caps = {
2240
- "tools": {
2241
- "ffprobe": {"available": True},
2242
- "ffmpeg": {"available": True},
2243
- },
2244
- "transcription": {"available": False},
2245
- "vision": {"available": False},
2246
- }
2247
- plan = build_plan(
2248
- project_name="Example Project",
2249
- project_id="project-123",
2250
- records=[record],
2251
- target={"type": "clip", "clip_id": "clip-123"},
2252
- params=params,
2253
- capabilities=caps,
2254
- )
2255
- manifest = execute_plan(plan, params=params, capabilities=caps)
2256
-
2257
- self.assertTrue(plan["success"])
2258
- self.assertEqual(plan["reusable_clip_count"], 1)
2259
- self.assertTrue(plan["clips"][0]["skip_execution"])
2260
- self.assertEqual(plan["clips"][0]["existing_report"]["path"], report_path)
2261
- self.assertTrue(manifest["success"])
2262
- self.assertTrue(manifest["clips"][0]["reused"])
2263
- self.assertEqual(manifest["clips"][0]["analysis_json"], report_path)
2264
- self.assertEqual(manifest["clips"][0]["cache_status"], "reusable")
2265
-
2266
- def test_build_plan_detects_stale_source_signature(self):
2267
- with tempfile.TemporaryDirectory() as tmp:
2268
- source = os.path.join(tmp, "source", "A001_C001.mov")
2269
- os.makedirs(os.path.dirname(source))
2270
- with open(source, "wb") as f:
2271
- f.write(b"current")
2272
- record = {
2273
- "clip_id": "clip-123",
2274
- "clip_name": "A001_C001.mov",
2275
- "file_path": source,
2276
- "media_id": "media-123",
2277
- }
2278
- root = resolve_output_root(
2279
- project_name="Example Project",
2280
- project_id="project-123",
2281
- analysis_root=os.path.join(tmp, "analysis"),
2282
- source_paths=[source],
2283
- create=True,
2284
- )["project_root"]
2285
- clip_dir = os.path.join(root, "clips", stable_clip_directory(record))
2286
- os.makedirs(clip_dir)
2287
- signature = analysis_request_signature(
2288
- record,
2289
- "standard",
2290
- {"transcription": {"enabled": False}, "vision": {}},
2291
- 8,
2292
- )
2293
- signature["source_file"]["size_bytes"] = 999
2294
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as f:
2295
- json.dump({
2296
- "success": True,
2297
- "analysis_version": "0.1",
2298
- "analysis_signature": signature,
2299
- "analyzed_at": "2026-05-13T12:00:00Z",
2300
- "source_file": source,
2301
- "clip": record,
2302
- "technical": {"format": {"duration_seconds": 1.0}},
2303
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
2304
- "motion": {"success": True, "analysis_keyframes": []},
2305
- "transcription": {"success": True, "status": "skipped"},
2306
- "visual": {"success": True, "status": "skipped"},
2307
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
2308
- }, f)
2309
- plan = build_plan(
2310
- project_name="Example Project",
2311
- project_id="project-123",
2312
- records=[record],
2313
- target={"type": "clip", "clip_id": "clip-123"},
2314
- params={
2315
- "analysis_root": os.path.join(tmp, "analysis"),
2316
- "depth": "standard",
2317
- "transcription": {"enabled": False},
2318
- },
2319
- capabilities={
2320
- "tools": {
2321
- "ffprobe": {"available": True},
2322
- "ffmpeg": {"available": True},
2323
- },
2324
- "transcription": {"available": False},
2325
- "vision": {"available": False},
2326
- },
2327
- )
2328
-
2329
- self.assertTrue(plan["success"])
2330
- self.assertEqual(plan["reusable_clip_count"], 0)
2331
- self.assertEqual(plan["stale_or_incomplete_clip_count"], 1)
2332
- self.assertFalse(plan["clips"][0].get("skip_execution", False))
2333
- self.assertIn("source_size_bytes_changed", plan["clips"][0]["existing_report"]["cache_issues"])
2334
-
2335
- def test_build_plan_reuses_report_from_same_name_project_version_root(self):
2336
- with tempfile.TemporaryDirectory() as tmp:
2337
- source = os.path.join(tmp, "source", "A001_C001.mov")
2338
- os.makedirs(os.path.dirname(source))
2339
- with open(source, "wb") as handle:
2340
- handle.write(b"current")
2341
- record = {
2342
- "clip_id": "clip-current-version",
2343
- "clip_name": "A001_C001.mov",
2344
- "file_path": source,
2345
- "media_id": "media-current-version",
2346
- }
2347
- analysis_root = os.path.join(tmp, "analysis")
2348
- previous_root = resolve_output_root(
2349
- project_name="Example Project",
2350
- project_id="version-001",
2351
- analysis_root=analysis_root,
2352
- source_paths=[source],
2353
- create=True,
2354
- )["project_root"]
2355
- active_root = resolve_output_root(
2356
- project_name="Example Project",
2357
- project_id="version-002",
2358
- analysis_root=analysis_root,
2359
- source_paths=[source],
2360
- create=False,
2361
- )["project_root"]
2362
- clip_dir = os.path.join(previous_root, "clips", stable_clip_directory(record))
2363
- os.makedirs(clip_dir)
2364
- signature = analysis_request_signature(record, "standard", {"transcription": {"enabled": False}, "vision": {}}, 8)
2365
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as handle:
2366
- json.dump({
2367
- "success": True,
2368
- "analysis_version": "0.1",
2369
- "analysis_signature": signature,
2370
- "analyzed_at": "2026-05-17T12:00:00Z",
2371
- "source_file": source,
2372
- "clip": record,
2373
- "technical": {"format": {"duration_seconds": 1.0}},
2374
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
2375
- "motion": {"success": True, "analysis_keyframes": []},
2376
- "transcription": {"success": True, "status": "skipped"},
2377
- "visual": {"success": True, "status": "skipped"},
2378
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
2379
- }, handle)
2380
-
2381
- plan = build_plan(
2382
- project_name="Example Project",
2383
- project_id="version-002",
2384
- records=[record],
2385
- target={"type": "clips", "clip_ids": ["clip-current-version"]},
2386
- params={
2387
- "analysis_root": analysis_root,
2388
- "depth": "standard",
2389
- "transcription": {"enabled": False},
2390
- "reuse_project_roots": [previous_root],
2391
- },
2392
- capabilities={
2393
- "tools": {
2394
- "ffprobe": {"available": True},
2395
- "ffmpeg": {"available": True},
2396
- },
2397
- "transcription": {"available": False},
2398
- "vision": {"available": False},
2399
- },
2400
- )
2401
-
2402
- self.assertTrue(plan["success"])
2403
- self.assertEqual(plan["output_root"]["project_root"], active_root)
2404
- self.assertEqual(plan["reusable_clip_count"], 1)
2405
- self.assertTrue(plan["clips"][0]["skip_execution"])
2406
- self.assertEqual(plan["clips"][0]["existing_report"]["project_root"], previous_root)
2407
- self.assertTrue(plan["clips"][0]["artifacts"]["analysis_json"].startswith(active_root))
2408
-
2409
- def test_build_plan_searches_related_project_roots_by_default(self):
2410
- with tempfile.TemporaryDirectory() as tmp:
2411
- source = os.path.join(tmp, "source", "A001_C001.mov")
2412
- os.makedirs(os.path.dirname(source))
2413
- with open(source, "wb") as handle:
2414
- handle.write(b"current")
2415
- record = {
2416
- "clip_id": "clip-current-version",
2417
- "clip_name": "A001_C001.mov",
2418
- "file_path": source,
2419
- "media_id": "media-current-version",
2420
- }
2421
- analysis_root = os.path.join(tmp, "analysis")
2422
- previous_root = resolve_output_root(
2423
- project_name="Example Project",
2424
- project_id="version-001",
2425
- analysis_root=analysis_root,
2426
- source_paths=[source],
2427
- create=True,
2428
- )["project_root"]
2429
- active_root = resolve_output_root(
2430
- project_name="Example Project",
2431
- project_id="version-002",
2432
- analysis_root=analysis_root,
2433
- source_paths=[source],
2434
- create=False,
2435
- )["project_root"]
2436
- clip_dir = os.path.join(previous_root, "clips", stable_clip_directory(record))
2437
- os.makedirs(clip_dir)
2438
- report_path = os.path.join(clip_dir, "analysis.json")
2439
- signature = analysis_request_signature(record, "standard", {"transcription": {"enabled": False}, "vision": {}}, 8)
2440
- with open(report_path, "w", encoding="utf-8") as handle:
2441
- json.dump({
2442
- "success": True,
2443
- "analysis_version": "0.2",
2444
- "analysis_signature": signature,
2445
- "analyzed_at": "2026-05-17T12:00:00Z",
2446
- "source_file": source,
2447
- "clip": record,
2448
- "technical": {"format": {"duration_seconds": 1.0}},
2449
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
2450
- "motion": {"success": True, "analysis_keyframes": []},
2451
- "transcription": {"success": True, "status": "skipped"},
2452
- "visual": {"success": True, "status": "skipped"},
2453
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
2454
- }, handle)
2455
-
2456
- plan = build_plan(
2457
- project_name="Example Project",
2458
- project_id="version-002",
2459
- records=[record],
2460
- target={"type": "clips", "clip_ids": ["clip-current-version"]},
2461
- params={
2462
- "analysis_root": analysis_root,
2463
- "depth": "standard",
2464
- "transcription": {"enabled": False},
2465
- },
2466
- capabilities={
2467
- "tools": {
2468
- "ffprobe": {"available": True},
2469
- "ffmpeg": {"available": True},
2470
- },
2471
- "transcription": {"available": False},
2472
- "vision": {"available": False},
2473
- },
2474
- )
2475
-
2476
- self.assertTrue(plan["success"])
2477
- self.assertEqual(plan["output_root"]["project_root"], active_root)
2478
- self.assertIn(previous_root, plan["related_project_roots"])
2479
- self.assertIn(previous_root, plan["reuse_project_roots"])
2480
- self.assertEqual(plan["reusable_clip_count"], 1)
2481
- self.assertTrue(plan["clips"][0]["skip_execution"])
2482
- self.assertEqual(plan["clips"][0]["existing_report"]["path"], report_path)
2483
-
2484
- def test_build_plan_can_disable_related_project_root_search(self):
2485
- with tempfile.TemporaryDirectory() as tmp:
2486
- source = os.path.join(tmp, "source", "A001_C001.mov")
2487
- os.makedirs(os.path.dirname(source))
2488
- with open(source, "wb") as handle:
2489
- handle.write(b"current")
2490
- record = {
2491
- "clip_id": "clip-current-version",
2492
- "clip_name": "A001_C001.mov",
2493
- "file_path": source,
2494
- "media_id": "media-current-version",
2495
- }
2496
- analysis_root = os.path.join(tmp, "analysis")
2497
- previous_root = resolve_output_root(
2498
- project_name="Example Project",
2499
- project_id="version-001",
2500
- analysis_root=analysis_root,
2501
- source_paths=[source],
2502
- create=True,
2503
- )["project_root"]
2504
- os.makedirs(os.path.join(previous_root, "clips", stable_clip_directory(record)))
2505
- with open(os.path.join(previous_root, "clips", stable_clip_directory(record), "analysis.json"), "w", encoding="utf-8") as handle:
2506
- json.dump({
2507
- "success": True,
2508
- "analysis_version": "0.2",
2509
- "analysis_signature": analysis_request_signature(record, "standard", {"transcription": {"enabled": False}, "vision": {}}, 8),
2510
- "analyzed_at": "2026-05-17T12:00:00Z",
2511
- "source_file": source,
2512
- "clip": record,
2513
- "technical": {"format": {"duration_seconds": 1.0}},
2514
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
2515
- "motion": {"success": True, "analysis_keyframes": []},
2516
- "transcription": {"success": True, "status": "skipped"},
2517
- "visual": {"success": True, "status": "skipped"},
2518
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
2519
- }, handle)
2520
-
2521
- plan = build_plan(
2522
- project_name="Example Project",
2523
- project_id="version-002",
2524
- records=[record],
2525
- target={"type": "clips", "clip_ids": ["clip-current-version"]},
2526
- params={
2527
- "analysis_root": analysis_root,
2528
- "depth": "standard",
2529
- "transcription": {"enabled": False},
2530
- "search_related_project_roots": False,
2531
- },
2532
- capabilities={
2533
- "tools": {
2534
- "ffprobe": {"available": True},
2535
- "ffmpeg": {"available": True},
2536
- },
2537
- "transcription": {"available": False},
2538
- "vision": {"available": False},
2539
- },
2540
- )
2541
-
2542
- self.assertTrue(plan["success"])
2543
- self.assertEqual(plan["related_project_roots"], [])
2544
- self.assertEqual(plan["reusable_clip_count"], 0)
2545
- self.assertEqual(plan["clips"][0]["cache_status"], "miss")
2546
-
2547
- def test_build_plan_reuses_report_path_from_clip_record_metadata(self):
2548
- with tempfile.TemporaryDirectory() as tmp:
2549
- source = os.path.join(tmp, "source", "A001_C001.mov")
2550
- os.makedirs(os.path.dirname(source))
2551
- with open(source, "wb") as handle:
2552
- handle.write(b"current")
2553
- analysis_root = os.path.join(tmp, "analysis")
2554
- previous_root = resolve_output_root(
2555
- project_name="Example Project",
2556
- project_id="version-001",
2557
- analysis_root=analysis_root,
2558
- source_paths=[source],
2559
- create=True,
2560
- )["project_root"]
2561
- active_root = resolve_output_root(
2562
- project_name="Example Project",
2563
- project_id="version-002",
2564
- analysis_root=analysis_root,
2565
- source_paths=[source],
2566
- create=False,
2567
- )["project_root"]
2568
- record = {
2569
- "clip_id": "clip-current-version",
2570
- "clip_name": "A001_C001.mov",
2571
- "file_path": source,
2572
- "media_id": "media-current-version",
2573
- }
2574
- clip_dir = os.path.join(previous_root, "clips", stable_clip_directory(record))
2575
- os.makedirs(clip_dir)
2576
- report_path = os.path.join(clip_dir, "analysis.json")
2577
- signature = analysis_request_signature(record, "standard", {"transcription": {"enabled": False}, "vision": {}}, 8)
2578
- with open(report_path, "w", encoding="utf-8") as handle:
2579
- json.dump({
2580
- "success": True,
2581
- "analysis_version": "0.2",
2582
- "analysis_signature": signature,
2583
- "analyzed_at": "2026-05-17T12:00:00Z",
2584
- "source_file": source,
2585
- "clip": record,
2586
- "technical": {"format": {"duration_seconds": 1.0}},
2587
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
2588
- "motion": {"success": True, "analysis_keyframes": []},
2589
- "transcription": {"success": True, "status": "skipped"},
2590
- "visual": {"success": True, "status": "skipped"},
2591
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
2592
- }, handle)
2593
-
2594
- record_with_metadata = {
2595
- **record,
2596
- "analysis_report_path": report_path,
2597
- "published_analysis_signature": signature["signature_hash"],
2598
- }
2599
- plan = build_plan(
2600
- project_name="Example Project",
2601
- project_id="version-002",
2602
- records=[record_with_metadata],
2603
- target={"type": "clips", "clip_ids": ["clip-current-version"]},
2604
- params={
2605
- "analysis_root": analysis_root,
2606
- "depth": "standard",
2607
- "transcription": {"enabled": False},
2608
- },
2609
- capabilities={
2610
- "tools": {
2611
- "ffprobe": {"available": True},
2612
- "ffmpeg": {"available": True},
2613
- },
2614
- "transcription": {"available": False},
2615
- "vision": {"available": False},
2616
- },
2617
- )
2618
-
2619
- self.assertTrue(plan["success"])
2620
- self.assertEqual(plan["output_root"]["project_root"], active_root)
2621
- self.assertEqual(plan["reusable_clip_count"], 1)
2622
- self.assertTrue(plan["clips"][0]["skip_execution"])
2623
- self.assertEqual(plan["clips"][0]["existing_report"]["path"], report_path)
2624
- self.assertEqual(plan["clips"][0]["existing_report"]["project_root"], previous_root)
2625
- self.assertEqual(plan["clips"][0]["existing_report"]["source"], "record_analysis_report_path")
2626
- self.assertIn("Resolve clip metadata", plan["clips"][0]["reuse_reason"])
2627
-
2628
- def test_records_from_target_carries_published_analysis_report_path(self):
2629
- clip = ClipStub(
2630
- "A001_C001.mov",
2631
- "clip-123",
2632
- "/tmp/A001_C001.mov",
2633
- third_party={
2634
- "davinci_resolve_mcp.analysis_report_path": "/tmp/analysis/project/clips/a001/analysis.json",
2635
- "davinci_resolve_mcp.analysis_signature": "abc123",
2636
- "davinci_resolve_mcp.published_at": "2026-05-17T12:00:00Z",
2637
- },
2638
- )
2639
- mp = MarkerMediaPoolStub([clip])
2640
-
2641
- records, target, warnings, err = _media_analysis_records_from_target(
2642
- mp,
2643
- {"target": {"type": "clip", "clip_id": "clip-123"}},
2644
- )
2645
-
2646
- self.assertIsNone(err)
2647
- self.assertEqual(warnings, [])
2648
- self.assertEqual(target["type"], "clip")
2649
- self.assertEqual(records[0]["analysis_report_path"], "/tmp/analysis/project/clips/a001/analysis.json")
2650
- self.assertEqual(records[0]["published_analysis_signature"], "abc123")
2651
- self.assertEqual(records[0]["published_analysis_at"], "2026-05-17T12:00:00Z")
2652
-
2653
- def test_records_from_target_flags_standard_metadata_provenance(self):
2654
- clip = ClipStub(
2655
- "A001_C001.mov",
2656
- "clip-123",
2657
- "/tmp/A001_C001.mov",
2658
- metadata={
2659
- "Comments": "Assistant note\n\n[DaVinci Resolve MCP Analysis]\nold\n[/DaVinci Resolve MCP Analysis]",
2660
- },
2661
- )
2662
- mp = MarkerMediaPoolStub([clip])
2663
-
2664
- records, _, warnings, err = _media_analysis_records_from_target(
2665
- mp,
2666
- {"target": {"type": "clip", "clip_id": "clip-123"}},
2667
- )
2668
-
2669
- self.assertIsNone(err)
2670
- self.assertEqual(warnings, [])
2671
- self.assertTrue(records[0]["analysis_metadata_present"])
2672
- self.assertEqual(records[0]["analysis_metadata_fields"], ["Comments"])
2673
- self.assertIn("standard_metadata_fields", records[0]["analysis_provenance"])
2674
-
2675
- def test_build_plan_reuses_global_registry_when_related_search_disabled(self):
2676
- with tempfile.TemporaryDirectory() as tmp:
2677
- source = os.path.join(tmp, "source", "A001_C001.mov")
2678
- os.makedirs(os.path.dirname(source))
2679
- with open(source, "wb") as handle:
2680
- handle.write(b"current")
2681
- record = {
2682
- "clip_id": "clip-current-version",
2683
- "clip_name": "A001_C001.mov",
2684
- "file_path": source,
2685
- "media_id": "media-current-version",
2686
- }
2687
- analysis_root = os.path.join(tmp, "analysis")
2688
- previous_root = resolve_output_root(
2689
- project_name="Example Project",
2690
- project_id="version-001",
2691
- analysis_root=analysis_root,
2692
- source_paths=[source],
2693
- create=True,
2694
- )["project_root"]
2695
- clip_dir = os.path.join(previous_root, "clips", stable_clip_directory(record))
2696
- os.makedirs(clip_dir)
2697
- report_path = os.path.join(clip_dir, "analysis.json")
2698
- signature = analysis_request_signature(record, "standard", {"transcription": {"enabled": False}, "vision": {}}, 8)
2699
- with open(report_path, "w", encoding="utf-8") as handle:
2700
- json.dump({
2701
- "success": True,
2702
- "analysis_version": "0.2",
2703
- "analysis_signature": signature,
2704
- "analyzed_at": "2026-05-17T12:00:00Z",
2705
- "source_file": source,
2706
- "clip": record,
2707
- "technical": {"format": {"duration_seconds": 1.0}},
2708
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
2709
- "motion": {"success": True, "analysis_keyframes": []},
2710
- "transcription": {"success": True, "status": "skipped"},
2711
- "visual": {"success": True, "status": "skipped"},
2712
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
2713
- }, handle)
2714
- registry = update_analysis_registry(previous_root, report_paths=[report_path])
2715
- self.assertTrue(registry["success"])
2716
-
2717
- plan = build_plan(
2718
- project_name="Example Project",
2719
- project_id="version-002",
2720
- records=[record],
2721
- target={"type": "clips", "clip_ids": ["clip-current-version"]},
2722
- params={
2723
- "analysis_root": analysis_root,
2724
- "depth": "standard",
2725
- "transcription": {"enabled": False},
2726
- "search_related_project_roots": False,
2727
- },
2728
- capabilities={
2729
- "tools": {
2730
- "ffprobe": {"available": True},
2731
- "ffmpeg": {"available": True},
2732
- },
2733
- "transcription": {"available": False},
2734
- "vision": {"available": False},
2735
- },
2736
- )
2737
-
2738
- self.assertTrue(plan["success"])
2739
- self.assertEqual(plan["related_project_roots"], [])
2740
- self.assertEqual(plan["reusable_clip_count"], 1)
2741
- self.assertTrue(plan["clips"][0]["skip_execution"])
2742
- self.assertEqual(plan["clips"][0]["existing_report"]["source"], "analysis_registry")
2743
- self.assertEqual(plan["reuse_summary"]["sources"]["analysis_registry"], 1)
2744
-
2745
- def test_plan_blocks_silent_reanalysis_when_provenance_report_missing(self):
2746
- with tempfile.TemporaryDirectory() as tmp:
2747
- source = os.path.join(tmp, "source", "A001_C001.mov")
2748
- os.makedirs(os.path.dirname(source))
2749
- with open(source, "wb") as handle:
2750
- handle.write(b"current")
2751
- record = {
2752
- "clip_id": "clip-current-version",
2753
- "clip_name": "A001_C001.mov",
2754
- "file_path": source,
2755
- "media_id": "media-current-version",
2756
- "analysis_report_path": os.path.join(tmp, "analysis", "missing", "clips", "a001", "analysis.json"),
2757
- "published_analysis_signature": "abc123",
2758
- }
2759
- plan = build_plan(
2760
- project_name="Example Project",
2761
- project_id="version-002",
2762
- records=[record],
2763
- target={"type": "clips", "clip_ids": ["clip-current-version"]},
2764
- params={
2765
- "analysis_root": os.path.join(tmp, "analysis"),
2766
- "depth": "standard",
2767
- "transcription": {"enabled": False},
2768
- },
2769
- capabilities={
2770
- "tools": {
2771
- "ffprobe": {"available": True},
2772
- "ffmpeg": {"available": True},
2773
- },
2774
- "transcription": {"available": False},
2775
- "vision": {"available": False},
2776
- },
2777
- )
2778
- executed = execute_plan(plan, params={"transcription": {"enabled": False}}, capabilities={
2779
- "tools": {
2780
- "ffprobe": {"available": True},
2781
- "ffmpeg": {"available": True},
2782
- },
2783
- "transcription": {"available": False},
2784
- "vision": {"available": False},
2785
- })
2786
-
2787
- self.assertTrue(plan["success"])
2788
- self.assertEqual(plan["reuse_blocked_clip_count"], 1)
2789
- self.assertEqual(plan["clips"][0]["cache_status"], "reuse_blocked")
2790
- self.assertIn("analysis_report_path_missing", plan["clips"][0]["reuse_block_issues"])
2791
- self.assertFalse(executed["success"])
2792
- self.assertEqual(executed["status"], "reuse_blocked")
2793
-
2794
- def test_force_refresh_bypasses_reuse_block(self):
2795
- with tempfile.TemporaryDirectory() as tmp:
2796
- source = os.path.join(tmp, "source", "A001_C001.mov")
2797
- os.makedirs(os.path.dirname(source))
2798
- with open(source, "wb") as handle:
2799
- handle.write(b"current")
2800
- record = {
2801
- "clip_id": "clip-current-version",
2802
- "clip_name": "A001_C001.mov",
2803
- "file_path": source,
2804
- "media_id": "media-current-version",
2805
- "analysis_report_path": os.path.join(tmp, "analysis", "missing", "clips", "a001", "analysis.json"),
2806
- "published_analysis_signature": "abc123",
2807
- }
2808
-
2809
- plan = build_plan(
2810
- project_name="Example Project",
2811
- project_id="version-002",
2812
- records=[record],
2813
- target={"type": "clips", "clip_ids": ["clip-current-version"]},
2814
- params={
2815
- "analysis_root": os.path.join(tmp, "analysis"),
2816
- "depth": "standard",
2817
- "transcription": {"enabled": False},
2818
- "force_refresh": True,
2819
- },
2820
- capabilities={
2821
- "tools": {
2822
- "ffprobe": {"available": True},
2823
- "ffmpeg": {"available": True},
2824
- },
2825
- "transcription": {"available": False},
2826
- "vision": {"available": False},
2827
- },
2828
- )
2829
-
2830
- self.assertTrue(plan["success"])
2831
- self.assertEqual(plan["reuse_blocked_clip_count"], 0)
2832
- self.assertEqual(plan["clips"][0]["cache_status"], "refresh_forced")
2833
-
2834
- def test_build_plan_force_refresh_bypasses_reuse(self):
2835
- with tempfile.TemporaryDirectory() as tmp:
2836
- source = os.path.join(tmp, "source", "A001_C001.mov")
2837
- os.makedirs(os.path.dirname(source))
2838
- open(source, "wb").close()
2839
- record = {
2840
- "clip_id": "clip-123",
2841
- "clip_name": "A001_C001.mov",
2842
- "file_path": source,
2843
- "media_id": "media-123",
2844
- }
2845
- root = resolve_output_root(
2846
- project_name="Example Project",
2847
- project_id="project-123",
2848
- analysis_root=os.path.join(tmp, "analysis"),
2849
- source_paths=[source],
2850
- create=True,
2851
- )["project_root"]
2852
- clip_dir = os.path.join(root, "clips", stable_clip_directory(record))
2853
- os.makedirs(clip_dir)
2854
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as f:
2855
- json.dump({
2856
- "success": True,
2857
- "source_file": source,
2858
- "clip": record,
2859
- "technical": {"format": {"duration_seconds": 1.0}},
2860
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
2861
- "motion": {"success": True, "analysis_keyframes": []},
2862
- "transcription": {"success": True, "status": "skipped"},
2863
- "visual": {"success": True, "status": "skipped"},
2864
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
2865
- }, f)
2866
- plan = build_plan(
2867
- project_name="Example Project",
2868
- project_id="project-123",
2869
- records=[record],
2870
- target={"type": "clip", "clip_id": "clip-123"},
2871
- params={
2872
- "analysis_root": os.path.join(tmp, "analysis"),
2873
- "depth": "standard",
2874
- "force_refresh": True,
2875
- },
2876
- capabilities={
2877
- "tools": {
2878
- "ffprobe": {"available": True},
2879
- "ffmpeg": {"available": True},
2880
- },
2881
- "transcription": {"available": False},
2882
- "vision": {"available": False},
2883
- },
2884
- )
2885
-
2886
- self.assertTrue(plan["success"])
2887
- self.assertEqual(plan["reusable_clip_count"], 0)
2888
- self.assertEqual(plan["clips"][0]["cache_status"], "refresh_forced")
2889
- self.assertNotIn("existing_report", plan["clips"][0])
2890
-
2891
- def test_build_plan_does_not_reuse_when_requested_vision_missing(self):
2892
- with tempfile.TemporaryDirectory() as tmp:
2893
- source = os.path.join(tmp, "source", "A001_C001.mov")
2894
- os.makedirs(os.path.dirname(source))
2895
- open(source, "wb").close()
2896
- record = {
2897
- "clip_id": "clip-123",
2898
- "clip_name": "A001_C001.mov",
2899
- "file_path": source,
2900
- "media_id": "media-123",
2901
- }
2902
- root = resolve_output_root(
2903
- project_name="Example Project",
2904
- project_id="project-123",
2905
- analysis_root=os.path.join(tmp, "analysis"),
2906
- source_paths=[source],
2907
- create=True,
2908
- )["project_root"]
2909
- clip_dir = os.path.join(root, "clips", stable_clip_directory(record))
2910
- os.makedirs(clip_dir)
2911
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as f:
2912
- json.dump({
2913
- "success": True,
2914
- "source_file": source,
2915
- "clip": record,
2916
- "technical": {"format": {"duration_seconds": 1.0}},
2917
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
2918
- "motion": {"success": True, "analysis_keyframes": []},
2919
- "transcription": {"success": True, "status": "skipped"},
2920
- "visual": {"success": True, "status": "skipped"},
2921
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
2922
- }, f)
2923
- plan = build_plan(
2924
- project_name="Example Project",
2925
- project_id="project-123",
2926
- records=[record],
2927
- target={"type": "clip", "clip_id": "clip-123"},
2928
- params={
2929
- "analysis_root": os.path.join(tmp, "analysis"),
2930
- "depth": "standard",
2931
- "vision": {"enabled": True, "provider": "mock"},
2932
- "transcription": {"enabled": False},
2933
- },
2934
- capabilities={
2935
- "tools": {
2936
- "ffprobe": {"available": True},
2937
- "ffmpeg": {"available": True},
2938
- },
2939
- "transcription": {"available": False},
2940
- "vision": {"available": True, "provider": "mock"},
2941
- },
2942
- )
2943
-
2944
- self.assertTrue(plan["success"])
2945
- self.assertEqual(plan["reusable_clip_count"], 0)
2946
- self.assertFalse(plan["clips"][0].get("skip_execution", False))
2947
- self.assertEqual(plan["clips"][0]["existing_report"]["missing_layers"], ["vision"])
2948
-
2949
- def test_bin_target_recurses_and_dedupes_sources(self):
2950
- with tempfile.TemporaryDirectory() as tmp:
2951
- path_a = os.path.join(tmp, "A001_C001.mov")
2952
- path_b = os.path.join(tmp, "A001_C002.mov")
2953
- root = FolderStub("Master", clips=[
2954
- ClipStub("A001_C001.mov", "clip-1", path_a),
2955
- ClipStub("A001_C001 duplicate.mov", "clip-2", path_a),
2956
- ], subfolders=[
2957
- FolderStub("Day 01", clips=[ClipStub("A001_C002.mov", "clip-3", path_b)])
2958
- ])
2959
- records, target, warnings, err = _media_analysis_records_from_target(
2960
- MediaPoolStub(root),
2961
- {"target": {"type": "bin", "path": "Master", "recursive": True}},
2962
- )
2963
-
2964
- self.assertIsNone(err)
2965
- self.assertEqual(target["type"], "bin")
2966
- self.assertEqual(len(records), 2)
2967
- self.assertTrue(any("Deduped 1" in warning for warning in warnings))
2968
-
2969
- def test_project_string_target_normalizes(self):
2970
- with tempfile.TemporaryDirectory() as tmp:
2971
- path_a = os.path.join(tmp, "A001_C001.mov")
2972
- root = FolderStub("Master", clips=[ClipStub("A001_C001.mov", "clip-1", path_a)])
2973
- records, target, warnings, err = _media_analysis_records_from_target(
2974
- MediaPoolStub(root),
2975
- {"target": "project"},
2976
- )
2977
-
2978
- self.assertIsNone(err)
2979
- self.assertEqual(warnings, [])
2980
- self.assertEqual(target["type"], "project")
2981
- self.assertTrue(target["recursive"])
2982
- self.assertEqual(len(records), 1)
2983
-
2984
- def test_selected_string_target_normalizes(self):
2985
- with tempfile.TemporaryDirectory() as tmp:
2986
- path_a = os.path.join(tmp, "A001_C001.mov")
2987
- selected = ClipStub("A001_C001.mov", "clip-1", path_a)
2988
- records, target, warnings, err = _media_analysis_records_from_target(
2989
- MediaPoolStub(FolderStub("Master"), selected=[selected]),
2990
- {"target": "selected"},
2991
- )
2992
-
2993
- self.assertIsNone(err)
2994
- self.assertEqual(warnings, [])
2995
- self.assertEqual(target["type"], "clip")
2996
- self.assertTrue(target["selected"])
2997
- self.assertEqual(len(records), 1)
2998
-
2999
- def test_clips_target_uses_explicit_clip_ids_and_rejects_non_analyzable(self):
3000
- with tempfile.TemporaryDirectory() as tmp:
3001
- path_a = os.path.join(tmp, "A001_C001.mov")
3002
- with open(path_a, "wb") as handle:
3003
- handle.write(b"placeholder")
3004
- good = ClipStub("A001_C001.mov", "clip-1", path_a)
3005
- missing = ClipStub("MISSING.mov", "clip-2", os.path.join(tmp, "missing.mov"))
3006
- root = FolderStub("Master", clips=[good, missing])
3007
- records, target, warnings, err = _media_analysis_records_from_target(
3008
- MediaPoolStub(root),
3009
- {"target": {"type": "clips", "clip_ids": ["clip-1", "clip-2"]}},
3010
- )
3011
-
3012
- self.assertIsNone(err)
3013
- self.assertEqual(target["type"], "clips")
3014
- self.assertEqual(len(records), 1)
3015
- self.assertEqual(records[0]["clip_id"], "clip-1")
3016
- self.assertTrue(any("Skipping non-analyzable clip" in warning for warning in warnings))
3017
-
3018
- def test_sequence_target_collects_used_assets_and_occurrences(self):
3019
- with tempfile.TemporaryDirectory() as tmp:
3020
- path_a = os.path.join(tmp, "A001_C001.mov")
3021
- path_b = os.path.join(tmp, "A001_C002.wav")
3022
- clip_a = ClipStub("A001_C001.mov", "clip-1", path_a)
3023
- clip_a_duplicate = ClipStub("A001_C001 copy.mov", "clip-2", path_a)
3024
- clip_b = ClipStub("A001_C002.wav", "clip-3", path_b)
3025
- timeline = TimelineStub(
3026
- video_tracks={1: [
3027
- TimelineItemStub("A001_C001", "item-1", clip_a, start=0, end=48),
3028
- TimelineItemStub("A001_C001 alt", "item-2", clip_a_duplicate, start=72, end=96),
3029
- ]},
3030
- audio_tracks={1: [TimelineItemStub("A001_C002", "item-3", clip_b, start=0, end=96)]},
3031
- )
3032
- records, target, warnings, err = _media_analysis_records_from_target(
3033
- MediaPoolStub(FolderStub("Master")),
3034
- {"target": "sequence"},
3035
- project=TimelineProjectStub(timeline),
3036
- )
3037
-
3038
- self.assertIsNone(err)
3039
- self.assertEqual(warnings, [])
3040
- self.assertEqual(target["type"], "sequence")
3041
- self.assertEqual(target["timeline"]["occurrence_count"], 3)
3042
- self.assertEqual(target["timeline"]["asset_count"], 2)
3043
- self.assertEqual(len(records), 2)
3044
- record_a = next(record for record in records if record["file_path"] == path_a)
3045
- self.assertEqual(len(record_a["timeline_occurrences"]), 2)
3046
- self.assertEqual(record_a["timeline_occurrences"][0]["timeline_item_id"], "item-1")
3047
-
3048
- def test_invalid_scalar_target_returns_clean_error(self):
3049
- records, target, warnings, err = _media_analysis_records_from_target(
3050
- None,
3051
- {"target": 123},
3052
- )
3053
-
3054
- self.assertIsNone(records)
3055
- self.assertEqual(warnings, [])
3056
- self.assertIn("_invalid_target", target)
3057
- self.assertIn("error", err)
3058
-
3059
- def test_execute_standard_pipeline_with_synthetic_media(self):
3060
- if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
3061
- self.skipTest("ffmpeg/ffprobe not installed")
3062
- with tempfile.TemporaryDirectory() as tmp:
3063
- source_dir = os.path.join(tmp, "source")
3064
- analysis_dir = os.path.join(tmp, "davinci-resolve-mcp-analysis")
3065
- os.makedirs(source_dir)
3066
- source = os.path.join(source_dir, "synthetic_motion.mp4")
3067
- self._write_synthetic_media(source)
3068
-
3069
- records = [{
3070
- "clip_id": "clip-motion",
3071
- "clip_name": "synthetic_motion.mp4",
3072
- "file_path": source,
3073
- "media_id": "media-motion",
3074
- }]
3075
- params = {
3076
- "analysis_root": analysis_dir,
3077
- "depth": "standard",
3078
- "dry_run": False,
3079
- "max_analysis_frames": 4,
3080
- "transcription": {
3081
- "enabled": True,
3082
- "backend": "mock",
3083
- "segments": [{
3084
- "start": 0,
3085
- "end": 1.5,
3086
- "text": "Synthetic tone.",
3087
- "words": [
3088
- {"start": 0.0, "end": 0.7, "word": "Synthetic"},
3089
- {"start": 0.7, "end": 1.2, "word": "tone."},
3090
- ],
3091
- }],
3092
- },
3093
- "vision": {"enabled": True, "provider": "mock"},
3094
- }
3095
- caps = detect_capabilities(env={"DAVINCI_RESOLVE_MCP_VISION_PROVIDER": "mock"})
3096
- plan = build_plan(
3097
- project_name="Example Project",
3098
- project_id="project-123",
3099
- records=records,
3100
- target={"type": "file", "path": source},
3101
- params=params,
3102
- capabilities=caps,
3103
- )
3104
- manifest = execute_plan(plan, params=params, capabilities=caps)
3105
- summary = summarize_reports(plan["output_root"]["project_root"])
3106
- manifest_report = load_report(plan["output_root"]["project_root"])
3107
- self.assertTrue(plan["success"])
3108
- self.assertEqual(plan["capability_gaps"], [])
3109
- self.assertTrue(manifest["success"])
3110
- self.assertEqual(manifest["successful_clip_count"], 1)
3111
- self.assertTrue(manifest["index"]["success"])
3112
- index = analysis_index_status(plan["output_root"]["project_root"])
3113
- self.assertTrue(index["exists"])
3114
- self.assertEqual(index["counts"]["clips"], 1)
3115
- artifacts = manifest["clips"][0]["artifacts"]
3116
- self.assertTrue(os.path.exists(artifacts["analysis_json"]))
3117
- self.assertTrue(os.path.exists(artifacts["technical_json"]))
3118
- self.assertTrue(os.path.exists(artifacts["motion_json"]))
3119
- self.assertTrue(os.path.exists(artifacts["transcript_json"]))
3120
- self.assertTrue(os.path.exists(artifacts["transcript_srt"]))
3121
- self.assertTrue(os.path.exists(artifacts["transcript_vtt"]))
3122
- self.assertTrue(os.path.exists(artifacts["visual_json"]))
3123
- self.assertTrue(os.path.exists(artifacts["marker_plan_json"]))
3124
- self.assertTrue(summary["success"])
3125
- self.assertEqual(summary["clip_reports"], 1)
3126
- self.assertTrue(manifest_report["success"])
3127
- clip_report = load_report(plan["output_root"]["project_root"], report_path=artifacts["analysis_json"])["report"]
3128
- self.assertEqual(clip_report["transcription"]["text"], "Synthetic tone.")
3129
- self.assertEqual(clip_report["transcription"]["segments"][0]["text"], "Synthetic tone.")
3130
- self.assertGreaterEqual(clip_report["clip_analysis_markers"]["marker_count"], 1)
3131
- self.assertTrue(clip_report["clip_analysis_markers"]["write_to_resolve_default"])
3132
- self.assertTrue(clip_report["readthrough"]["cut_analysis"]["success"])
3133
- self.assertIn("cut_count", clip_report["cut_analysis"])
3134
- self.assertGreaterEqual(
3135
- clip_report["motion"]["effective_sample_budget"],
3136
- clip_report["motion"]["requested_sample_budget"],
3137
- )
3138
- self.assertIn("cut_boundary_pair_coverage", clip_report["motion"])
3139
- self.assertIn("cut_analysis", clip_report["clip_analysis_markers"])
3140
- with open(artifacts["transcript_json"], "r", encoding="utf-8") as handle:
3141
- transcript_json = json.load(handle)
3142
- self.assertEqual(transcript_json["text"], "Synthetic tone.")
3143
- self.assertEqual(transcript_json["segments"][0]["text"], "Synthetic tone.")
3144
- self.assertEqual(transcript_json["segments"][0]["words"][0]["word"], "Synthetic")
3145
- with open(artifacts["marker_plan_json"], "r", encoding="utf-8") as handle:
3146
- marker_plan_json = json.load(handle)
3147
- self.assertEqual(marker_plan_json["schema"], "davinci_resolve_mcp.clip_analysis_markers.v1")
3148
- self.assertTrue(marker_plan_json["resolve_marker_writeback"]["enabled"])
3149
- self.assertEqual(marker_plan_json["color_scheme"]["qc_warning"], "Red")
3150
- with open(artifacts["transcript_srt"], "r", encoding="utf-8") as handle:
3151
- transcript_srt = handle.read()
3152
- self.assertIn("00:00:00,000 --> 00:00:01,500", transcript_srt)
3153
- self.assertIn("Synthetic tone.", transcript_srt)
3154
- with open(artifacts["transcript_vtt"], "r", encoding="utf-8") as handle:
3155
- transcript_vtt = handle.read()
3156
- self.assertIn("WEBVTT", transcript_vtt)
3157
- self.assertIn("00:00:00.000 --> 00:00:01.500", transcript_vtt)
3158
- cleanup = cleanup_artifacts(plan["output_root"]["project_root"])
3159
- self.assertTrue(cleanup["success"])
3160
- self.assertEqual(sorted(os.listdir(source_dir)), ["synthetic_motion.mp4"])
3161
-
3162
- def test_execute_session_pipeline_returns_reports_and_cleans_artifacts(self):
3163
- if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
3164
- self.skipTest("ffmpeg/ffprobe not installed")
3165
- with tempfile.TemporaryDirectory() as tmp:
3166
- source_dir = os.path.join(tmp, "source")
3167
- session_root = os.path.join(tmp, "session-analysis")
3168
- os.makedirs(source_dir)
3169
- source = os.path.join(source_dir, "synthetic_session.mp4")
3170
- self._write_synthetic_media(source)
3171
-
3172
- records = [{
3173
- "clip_id": "clip-session",
3174
- "clip_name": "synthetic_session.mp4",
3175
- "file_path": source,
3176
- "media_id": "media-session",
3177
- }]
3178
- params = {
3179
- "analysis_root": session_root,
3180
- "depth": "standard",
3181
- "dry_run": False,
3182
- "session_only": True,
3183
- "cleanup_frames": True,
3184
- "max_analysis_frames": 4,
3185
- "transcription": {
3186
- "enabled": True,
3187
- "backend": "mock",
3188
- "segments": [{"start": 0, "end": 1.0, "text": "Session transcript."}],
3189
- },
3190
- "vision": {"enabled": True, "provider": "mock"},
3191
- }
3192
- caps = detect_capabilities(env={"DAVINCI_RESOLVE_MCP_VISION_PROVIDER": "mock"})
3193
- plan = build_plan(
3194
- project_name="Example Project",
3195
- project_id="project-session",
3196
- records=records,
3197
- target={"type": "file", "path": source},
3198
- params=params,
3199
- capabilities=caps,
3200
- )
3201
- manifest = execute_plan(plan, params=params, capabilities=caps)
3202
-
3203
- self.assertTrue(plan["success"])
3204
- self.assertTrue(manifest["success"])
3205
- self.assertTrue(manifest["session_only"])
3206
- self.assertFalse(manifest["persistent"])
3207
- self.assertTrue(manifest["artifacts_cleaned_up"])
3208
- self.assertEqual(manifest["successful_clip_count"], 1)
3209
- self.assertEqual(len(manifest["reports"]), 1)
3210
- self.assertEqual(manifest["reports"][0]["clip"]["clip_id"], "clip-session")
3211
- self.assertIn("clip_analysis_markers", manifest["reports"][0])
3212
- self.assertGreaterEqual(manifest["reports"][0]["clip_analysis_markers"]["marker_count"], 1)
3213
- self.assertEqual(manifest["project_summary"]["clip_reports"], 1)
3214
- self.assertFalse(os.path.exists(plan["output_root"]["project_root"]))
3215
- self.assertEqual(sorted(os.listdir(source_dir)), ["synthetic_session.mp4"])
3216
-
3217
- def test_execute_emits_pending_host_analysis_when_vision_requested(self):
3218
- if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
3219
- self.skipTest("ffmpeg/ffprobe not installed")
3220
- with tempfile.TemporaryDirectory() as tmp:
3221
- source_dir = os.path.join(tmp, "source")
3222
- analysis_dir = os.path.join(tmp, "davinci-resolve-mcp-analysis")
3223
- os.makedirs(source_dir)
3224
- source = os.path.join(source_dir, "synthetic_requires_vision.mp4")
3225
- self._write_synthetic_media(source)
3226
-
3227
- records = [{
3228
- "clip_id": "clip-requires-vision",
3229
- "clip_name": "synthetic_requires_vision.mp4",
3230
- "file_path": source,
3231
- "media_id": "media-requires-vision",
3232
- }]
3233
- params = {
3234
- "analysis_root": analysis_dir,
3235
- "depth": "standard",
3236
- "dry_run": False,
3237
- "session_only": True,
3238
- "cleanup_frames": False,
3239
- "max_analysis_frames": 1,
3240
- "transcription": {"enabled": False},
3241
- "vision": {"enabled": True, "provider": HOST_CHAT_PATHS_PROVIDER},
3242
- }
3243
- caps = detect_capabilities(env={})
3244
- plan = build_plan(
3245
- project_name="Example Project",
3246
- project_id="project-requires-vision",
3247
- records=records,
3248
- target={"type": "file", "path": source},
3249
- params=params,
3250
- capabilities=caps,
3251
- )
3252
-
3253
- manifest = execute_plan(plan, params=params, capabilities=caps)
3254
-
3255
- self.assertTrue(plan["success"])
3256
- self.assertTrue(manifest["success"])
3257
- self.assertTrue(manifest["vision_pending"])
3258
- self.assertEqual(manifest["vision_pending_clip_count"], 1)
3259
- self.assertEqual(manifest["successful_clip_count"], 1)
3260
- self.assertEqual(manifest["failed_clip_count"], 0)
3261
- self.assertEqual(manifest["pending_action"]["action"], "commit_vision")
3262
- clip_row = manifest["clips"][0]
3263
- self.assertEqual(clip_row["vision_status"], "pending_host_analysis")
3264
- self.assertEqual(clip_row["visual"]["provider"], HOST_CHAT_PATHS_PROVIDER)
3265
- self.assertTrue(clip_row["visual"]["frame_paths"])
3266
- self.assertEqual(clip_row["visual"]["commit_action"]["action"], "commit_vision")
3267
- self.assertEqual(sorted(os.listdir(source_dir)), ["synthetic_requires_vision.mp4"])
3268
-
3269
- def test_execute_with_custom_vision_runner_writes_structured_visual_report(self):
3270
- """A custom vision_runner (e.g. a future provider) can short-circuit the
3271
- host_chat_paths deferred payload and return a final visual report directly.
3272
- The runner contract: same signature as before, return dict matches schema."""
3273
- if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
3274
- self.skipTest("ffmpeg/ffprobe not installed")
3275
- with tempfile.TemporaryDirectory() as tmp:
3276
- source_dir = os.path.join(tmp, "source")
3277
- analysis_dir = os.path.join(tmp, "davinci-resolve-mcp-analysis")
3278
- os.makedirs(source_dir)
3279
- source = os.path.join(source_dir, "synthetic_custom_runner.mp4")
3280
- self._write_synthetic_media(source)
3281
-
3282
- records = [{
3283
- "clip_id": "clip-custom-runner",
3284
- "clip_name": "synthetic_custom_runner.mp4",
3285
- "file_path": source,
3286
- "media_id": "media-custom-runner",
3287
- }]
3288
- params = {
3289
- "analysis_root": analysis_dir,
3290
- "depth": "standard",
3291
- "dry_run": False,
3292
- "session_only": True,
3293
- "cleanup_frames": True,
3294
- "max_analysis_frames": 3,
3295
- "transcription": {"enabled": False},
3296
- "vision": {"enabled": True, "provider": HOST_CHAT_PATHS_PROVIDER},
3297
- }
3298
- caps = detect_capabilities(env={})
3299
- plan = build_plan(
3300
- project_name="Example Project",
3301
- project_id="project-custom-runner",
3302
- records=records,
3303
- target={"type": "file", "path": source},
3304
- params=params,
3305
- capabilities=caps,
3306
- )
3307
-
3308
- async def fake_runner(record, motion, options, artifacts, capabilities):
3309
- self.assertTrue(any(row.get("frame_path") for row in motion["analysis_keyframes"]))
3310
- self.assertIn("cut_analysis", motion)
3311
- return {
3312
- "success": True,
3313
- "provider": "custom_test_runner",
3314
- "clip_summary": "Custom-runner visual report.",
3315
- "editorial_classification": {
3316
- "primary_use": "unknown",
3317
- "select_potential": "medium",
3318
- "reason": "Test runner supplied structured analysis.",
3319
- },
3320
- "content": {
3321
- "locations": [],
3322
- "people_visible": "none",
3323
- "actions": ["test pattern"],
3324
- "objects": [],
3325
- "visible_text": [],
3326
- "notable_audio_context": [],
3327
- },
3328
- "shot_and_style": {
3329
- "shot_sizes": [],
3330
- "camera_motion": ["computed"],
3331
- "composition_notes": "",
3332
- "lighting_mood": "",
3333
- "color_mood": "",
3334
- },
3335
- "motion": {
3336
- "overall_level": motion.get("overall_motion_level"),
3337
- "motion_events": [],
3338
- "quiet_regions": [],
3339
- },
3340
- "analysis_keyframes": [],
3341
- "editing_notes": {
3342
- "best_moments": [],
3343
- "continuity_flags": [],
3344
- "qc_flags": [],
3345
- "search_tags": ["custom-runner"],
3346
- },
3347
- "confidence": {
3348
- "visual": "medium",
3349
- "motion": "computed",
3350
- "transcript": "unavailable",
3351
- },
3352
- }
3353
-
3354
- manifest = asyncio.run(execute_plan_async(
3355
- plan,
3356
- params=params,
3357
- capabilities=caps,
3358
- vision_runner=fake_runner,
3359
- ))
3360
-
3361
- self.assertTrue(manifest["success"])
3362
- self.assertFalse(manifest["vision_pending"])
3363
- self.assertTrue(manifest["artifacts_cleaned_up"])
3364
- self.assertEqual(manifest["reports"][0]["visual"]["provider"], "custom_test_runner")
3365
- self.assertEqual(manifest["reports"][0]["visual"]["editing_notes"]["search_tags"], ["custom-runner"])
3366
- self.assertFalse(os.path.exists(plan["output_root"]["project_root"]))
3367
- self.assertEqual(sorted(os.listdir(source_dir)), ["synthetic_custom_runner.mp4"])
3368
-
3369
- def test_reused_report_is_ingested_into_current_root_db(self):
3370
- """Cross-root report reuse must land DB rows + a lockstep export in
3371
- the CURRENT root, keyed to the CURRENT project's clip identity —
3372
- otherwise media_ref lookups (edit_engine planners, panel readers)
3373
- find nothing while the manifest claims success (Phase 3 pilot bug)."""
3374
- from tests.test_analysis_store import make_report
3375
- from src.utils import analysis_store, timeline_brain_db
3376
-
3377
- with tempfile.TemporaryDirectory() as tmp:
3378
- source_dir = os.path.join(tmp, "source")
3379
- os.makedirs(source_dir)
3380
- source = os.path.join(source_dir, "sample clip.mp4")
3381
- with open(source, "wb") as handle:
3382
- handle.write(b"placeholder")
3383
- # A prior report in ANOTHER project's root, under the OLD clip id.
3384
- other_root = os.path.join(tmp, "other-root")
3385
- other_clip_dir = os.path.join(other_root, "clips", "old-dir")
3386
- os.makedirs(other_clip_dir)
3387
- prior = make_report()
3388
- prior["clip"] = dict(prior["clip"], clip_id="old-resolve-id", file_path=source)
3389
- prior_path = os.path.join(other_clip_dir, "analysis.json")
3390
- with open(prior_path, "w", encoding="utf-8") as handle:
3391
- json.dump(prior, handle)
3392
-
3393
- analysis_dir = os.path.join(tmp, "analysis-root")
3394
- record = {
3395
- "clip_id": "new-resolve-id",
3396
- "clip_name": "Sample Clip.mp4",
3397
- "file_path": source,
3398
- "media_id": "new-media-id",
3399
- }
3400
- plan = build_plan(
3401
- project_name="Reuse Project",
3402
- project_id="project-reuse-db",
3403
- records=[record],
3404
- target={"type": "file", "path": source},
3405
- params={"analysis_root": analysis_dir, "depth": "standard",
3406
- "session_only": True},
3407
- capabilities=detect_capabilities(env={}),
3408
- )
3409
- # Force the reuse path the way the registry matcher would.
3410
- clip_plan = plan["clips"][0]
3411
- clip_plan["skip_execution"] = True
3412
- clip_plan["cache_status"] = "reusable"
3413
- clip_plan["existing_report"] = {"path": prior_path}
3414
-
3415
- manifest = asyncio.run(execute_plan_async(plan, params={"depth": "standard"}))
3416
- self.assertTrue(manifest["success"], manifest)
3417
- row = manifest["clips"][0]
3418
- self.assertTrue(row["reused"])
3419
- project_root = plan["output_root"]["project_root"]
3420
- # The export now lives in THIS root (provenance kept in reused_from).
3421
- self.assertTrue(row["analysis_json"].startswith(project_root))
3422
- self.assertTrue(os.path.isfile(row["analysis_json"]))
3423
- self.assertEqual(row["reused_from"], prior_path)
3424
- # DB rows landed under the CURRENT clip identity.
3425
- self.addCleanup(timeline_brain_db.close_all)
3426
- conn = timeline_brain_db.connect(project_root)
3427
- clip_row = conn.execute(
3428
- "SELECT resolve_clip_id, file_path FROM clips"
3429
- ).fetchone()
3430
- self.assertIsNotNone(clip_row)
3431
- self.assertEqual(clip_row["resolve_clip_id"], "new-resolve-id")
3432
- self.assertGreater(
3433
- conn.execute("SELECT COUNT(*) FROM transcript_segments").fetchone()[0], 0)
3434
- # The media_ref lookup the edit-engine planners depend on.
3435
- self.assertIsNotNone(analysis_store.resolve_clip_uuid(conn, "new-resolve-id"))
3436
-
3437
- def test_executing_clips_helper_excludes_pure_reuse(self):
3438
- """The capability gate must key off clips that still need fresh
3439
- analysis, not the plan's requested-options gaps. A clip is exempt only
3440
- when it both skips execution AND has an existing report path."""
3441
- reused = {"skip_execution": True, "existing_report": {"path": "/x/a.json"}}
3442
- fresh = {"skip_execution": False}
3443
- # skip_execution flagged but no report path on disk -> still executes.
3444
- no_path = {"skip_execution": True, "existing_report": {}}
3445
- self.assertEqual(executing_clips({"clips": [reused]}), [])
3446
- self.assertFalse(plan_requires_capabilities({"clips": [reused]}))
3447
- self.assertEqual(executing_clips({"clips": [reused, fresh]}), [fresh])
3448
- self.assertTrue(plan_requires_capabilities({"clips": [reused, fresh]}))
3449
- self.assertEqual(executing_clips({"clips": [no_path]}), [no_path])
3450
- self.assertTrue(plan_requires_capabilities({"clips": [no_path]}))
3451
- self.assertFalse(plan_requires_capabilities({"clips": []}))
3452
-
3453
- def test_all_reused_plan_executes_despite_capability_gaps(self):
3454
- """build_plan records capability_gaps from the requested options before
3455
- the reuse decision runs. When every clip is satisfied by an existing
3456
- reusable report, execution only re-keys/imports it — no fresh
3457
- transcription/vision happens — so the missing-capability gate must NOT
3458
- block. Regression for the gap the PR-68 inner fix left on the entry
3459
- points (server analyze action, metadata publish, batch job creation)."""
3460
- from tests.test_analysis_store import make_report
3461
- from src.utils import timeline_brain_db
3462
-
3463
- with tempfile.TemporaryDirectory() as tmp:
3464
- source_dir = os.path.join(tmp, "source")
3465
- os.makedirs(source_dir)
3466
- source = os.path.join(source_dir, "reuse clip.mp4")
3467
- with open(source, "wb") as handle:
3468
- handle.write(b"placeholder")
3469
- prior_dir = os.path.join(tmp, "prior", "clips", "old-dir")
3470
- os.makedirs(prior_dir)
3471
- prior = make_report()
3472
- prior["clip"] = dict(prior["clip"], clip_id="old-id", file_path=source)
3473
- prior_path = os.path.join(prior_dir, "analysis.json")
3474
- with open(prior_path, "w", encoding="utf-8") as handle:
3475
- json.dump(prior, handle)
3476
-
3477
- record = {
3478
- "clip_id": "new-id",
3479
- "clip_name": "Reuse Clip.mp4",
3480
- "file_path": source,
3481
- "media_id": "new-media",
3482
- }
3483
- # Default transcription is on, but NO backend is available here ->
3484
- # build_plan records a capability gap from the requested options.
3485
- no_backend_caps = {
3486
- "tools": {
3487
- "ffprobe": {"available": True},
3488
- "ffmpeg": {"available": True},
3489
- },
3490
- "transcription": {"available": False, "backends": []},
3491
- "vision": {"available": False},
3492
- }
3493
- plan = build_plan(
3494
- project_name="Reuse Gate",
3495
- project_id="project-reuse-gate",
3496
- records=[record],
3497
- target={"type": "file", "path": source},
3498
- params={"analysis_root": os.path.join(tmp, "analysis-root"),
3499
- "depth": "standard", "session_only": True},
3500
- capabilities=no_backend_caps,
3501
- )
3502
- self.assertTrue(plan["capability_gaps"], plan)
3503
- clip_plan = plan["clips"][0]
3504
- clip_plan["skip_execution"] = True
3505
- clip_plan["cache_status"] = "reusable"
3506
- clip_plan["existing_report"] = {"path": prior_path}
3507
-
3508
- self.addCleanup(timeline_brain_db.close_all)
3509
- manifest = asyncio.run(execute_plan_async(
3510
- plan,
3511
- params={"depth": "standard"},
3512
- capabilities=no_backend_caps,
3513
- ))
3514
- self.assertTrue(manifest["success"], manifest)
3515
- self.assertNotIn("capability_gaps", manifest)
3516
- self.assertTrue(manifest["clips"][0]["reused"])
3517
-
3518
- def test_summarize_and_index_db_vs_json_parity(self):
3519
- """Phase 5 — summarize_reports and build_analysis_index source from
3520
- the DB-canonical store when the root is fully ingested, falling back
3521
- WHOLESALE to the JSON walk otherwise. Both paths must be semantically
3522
- identical: the export is lockstep with the DB by construction."""
3523
- from tests.test_analysis_store import make_report
3524
- from src.utils import analysis_store, timeline_brain_db
3525
- from src.utils.media_analysis import (
3526
- build_analysis_index, query_analysis_index, summarize_reports,
3527
- )
3528
-
3529
- def _normalized(summary):
3530
- out = json.loads(json.dumps(summary))
3531
- out.pop("source", None)
3532
- (out.get("provenance") or {}).pop("generated_at", None)
3533
- out["technical_warnings"] = sorted(map(str, out.get("technical_warnings") or []))
3534
- out["search_tags"] = sorted(out.get("search_tags") or [])
3535
- prov = out.get("provenance") or {}
3536
- prov["source_reports"] = sorted(
3537
- prov.get("source_reports") or [], key=lambda r: str(r.get("clip_id")))
3538
- prov["missing_reports"] = sorted(
3539
- prov.get("missing_reports") or [], key=lambda r: str(r.get("report_path")))
3540
- return out
3541
-
3542
- with tempfile.TemporaryDirectory() as tmp:
3543
- root = os.path.join(tmp, "analysis-root")
3544
- os.makedirs(root)
3545
- self.addCleanup(timeline_brain_db.close_all)
3546
- report = make_report()
3547
- ingest = analysis_store.ingest_report(root, report, clip_dir="par-aaaaaaaaaaaa")
3548
- self.assertTrue(ingest["success"], ingest)
3549
- report_b = make_report()
3550
- report_b["clip"] = dict(report_b["clip"], clip_id="parity-2", clip_name="B.mp4",
3551
- file_path="/media/b.mp4", media_id="parity-2-m")
3552
- ingest_b = analysis_store.ingest_report(root, report_b, clip_dir="par-bbbbbbbbbbbb")
3553
- # The pipeline writes the lockstep export after ingest; mirror it.
3554
- for clip_uuid, clip_dir in ((ingest["clip_uuid"], "par-aaaaaaaaaaaa"),
3555
- (ingest_b["clip_uuid"], "par-bbbbbbbbbbbb")):
3556
- target = os.path.join(root, "clips", clip_dir, "analysis.json")
3557
- os.makedirs(os.path.dirname(target), exist_ok=True)
3558
- self.assertTrue(analysis_store.export_report_file(root, clip_uuid, target))
3559
-
3560
- db_summary = summarize_reports(root)
3561
- self.assertEqual(db_summary["source"], "db")
3562
- self.assertEqual(db_summary["clip_reports"], 2)
3563
- db_index = build_analysis_index(root)
3564
- self.assertTrue(db_index["success"], db_index)
3565
- self.assertEqual(db_index["report_sources"], {"db": 2, "json": 0})
3566
- db_query = query_analysis_index(root, query="sample")
3567
-
3568
- # Force the JSON path by making the export unavailable.
3569
- with unittest.mock.patch.object(analysis_store, "export_report", return_value=None):
3570
- json_summary = summarize_reports(root)
3571
- json_index = build_analysis_index(root)
3572
- self.assertEqual(json_summary["source"], "json")
3573
- self.assertEqual(json_index["report_sources"], {"db": 0, "json": 2})
3574
- json_query = query_analysis_index(root, query="sample")
3575
-
3576
- self.assertEqual(_normalized(db_summary), _normalized(json_summary))
3577
- self.assertEqual(db_index["counts"], json_index["counts"])
3578
- self.assertEqual(
3579
- [r.get("clip_id") for r in db_query.get("results") or []],
3580
- [r.get("clip_id") for r in json_query.get("results") or []],
3581
- )
3582
-
3583
- def test_summarize_mixed_root_falls_back_wholesale(self):
3584
- from tests.test_analysis_store import make_report
3585
- from src.utils import analysis_store, timeline_brain_db
3586
- from src.utils.media_analysis import summarize_reports
3587
-
3588
- with tempfile.TemporaryDirectory() as tmp:
3589
- root = os.path.join(tmp, "analysis-root")
3590
- os.makedirs(root)
3591
- self.addCleanup(timeline_brain_db.close_all)
3592
- ingest = analysis_store.ingest_report(root, make_report(), clip_dir="mix-aaaaaaaaaaaa")
3593
- target = os.path.join(root, "clips", "mix-aaaaaaaaaaaa", "analysis.json")
3594
- os.makedirs(os.path.dirname(target), exist_ok=True)
3595
- self.assertTrue(analysis_store.export_report_file(root, ingest["clip_uuid"], target))
3596
- # A second report on disk that was never ingested (mixed root).
3597
- stray_dir = os.path.join(root, "clips", "mix-stray")
3598
- os.makedirs(stray_dir)
3599
- stray = make_report()
3600
- stray["clip"] = dict(stray["clip"], clip_id="stray-1", clip_name="Stray.mp4",
3601
- file_path="/media/stray.mp4")
3602
- with open(os.path.join(stray_dir, "analysis.json"), "w", encoding="utf-8") as fh:
3603
- json.dump(stray, fh)
3604
- summary = summarize_reports(root)
3605
- self.assertEqual(summary["source"], "json")
3606
- self.assertEqual(summary["clip_reports"], 2)
3607
-
3608
- def test_summarize_pre_v9_root_uses_json(self):
3609
- from tests.test_analysis_store import make_report
3610
- from src.utils.media_analysis import summarize_reports
3611
-
3612
- with tempfile.TemporaryDirectory() as tmp:
3613
- root = os.path.join(tmp, "analysis-root")
3614
- clip_dir = os.path.join(root, "clips", "legacy-clip")
3615
- os.makedirs(clip_dir)
3616
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as fh:
3617
- json.dump(make_report(), fh)
3618
- summary = summarize_reports(root)
3619
- self.assertEqual(summary["source"], "json")
3620
- self.assertEqual(summary["clip_reports"], 1)
3621
-
3622
- def test_build_analysis_index_queries_reports_without_image_columns(self):
3623
- with tempfile.TemporaryDirectory() as tmp:
3624
- source_dir = os.path.join(tmp, "source")
3625
- os.makedirs(source_dir)
3626
- source = os.path.join(source_dir, "A001_C001.mov")
3627
- with open(source, "wb") as handle:
3628
- handle.write(b"synthetic source placeholder")
3629
-
3630
- record = {
3631
- "clip_id": "clip-index",
3632
- "clip_name": "A001_C001.mov",
3633
- "file_path": source,
3634
- "media_id": "media-index",
3635
- "bin_path": "Master/Day 01",
3636
- "fps": "24",
3637
- "timeline_occurrences": [
3638
- {
3639
- "timeline_id": "timeline-main",
3640
- "timeline_name": "Assembly",
3641
- "track_type": "video",
3642
- "track_index": 1,
3643
- "item_index": 0,
3644
- "start_frame": 100,
3645
- "end_frame": 196,
3646
- }
3647
- ],
3648
- }
3649
- root = resolve_output_root(
3650
- project_name="Example Project",
3651
- project_id="project-index",
3652
- analysis_root=os.path.join(tmp, "analysis"),
3653
- source_paths=[source],
3654
- create=True,
3655
- )["project_root"]
3656
- clip_dir = os.path.join(root, "clips", stable_clip_directory(record))
3657
- os.makedirs(clip_dir)
3658
- signature = analysis_request_signature(
3659
- record,
3660
- "standard",
3661
- {
3662
- "transcription": {"enabled": True, "backend": "mock"},
3663
- "vision": {"enabled": True, "provider": "mock"},
3664
- },
3665
- 4,
3666
- )
3667
- report = {
3668
- "success": True,
3669
- "analysis_version": "0.1",
3670
- "analysis_signature": signature,
3671
- "analyzed_at": "2026-05-17T12:00:00Z",
3672
- "source_file": source,
3673
- "clip": record,
3674
- "summary": "Reflective kitchen interview b-roll, medium motion",
3675
- "technical_warnings": ["Container frame rate and average frame rate differ; possible VFR media"],
3676
- "technical": {
3677
- "format": {"duration_seconds": 4.0, "size_bytes": 24},
3678
- "video": [{"frame_rate": 24.0, "duration_seconds": 4.0}],
3679
- "warnings": ["Container frame rate and average frame rate differ; possible VFR media"],
3680
- },
3681
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
3682
- "motion": {
3683
- "success": True,
3684
- "overall_motion_level": "medium",
3685
- "average_frame_delta": 0.04,
3686
- "max_frame_delta": 0.12,
3687
- },
3688
- "transcription": {
3689
- "success": True,
3690
- "text": "A childhood memory is described quietly.",
3691
- "segments": [
3692
- {"start": 0.5, "end": 2.4, "text": "A childhood memory is described quietly."}
3693
- ],
3694
- },
3695
- "visual": {
3696
- "success": True,
3697
- "clip_summary": "Person moves through a kitchen in a reflective mood.",
3698
- "content": {
3699
- "locations": ["kitchen"],
3700
- "actions": ["walking"],
3701
- "objects": ["coffee mug"],
3702
- "visible_text": [],
3703
- "notable_audio_context": ["quiet room tone"],
3704
- },
3705
- "editing_notes": {
3706
- "best_moments": ["00:00:01.000 thoughtful pause"],
3707
- "qc_flags": [],
3708
- "search_tags": ["reflective", "kitchen"],
3709
- },
3710
- },
3711
- "analysis_keyframes": [
3712
- {
3713
- "index": 1,
3714
- "time_seconds": 1.0,
3715
- "selection_reason": "midpoint",
3716
- "frame_path": "/tmp/should-not-be-stored.jpg",
3717
- "metrics": {"mean_luma": 91.5},
3718
- "delta_from_previous": 0.12,
3719
- }
3720
- ],
3721
- "clip_analysis_markers": {
3722
- "success": True,
3723
- "duration_seconds": 4.0,
3724
- "fps": 24.0,
3725
- "timeline_occurrences": record["timeline_occurrences"],
3726
- "markers": [
3727
- {
3728
- "id": "best-moment-001",
3729
- "type": "best_moment",
3730
- "color": "Green",
3731
- "name": "Best Moment",
3732
- "start_seconds": 1.0,
3733
- "end_seconds": 2.0,
3734
- "start_frame": 24,
3735
- "duration_frames": 24,
3736
- "visual_description": "Quiet reflective moment near the kitchen counter.",
3737
- "sound_note": "Soft room tone.",
3738
- "transcript_text": "childhood memory",
3739
- "source": "visual_editing_notes",
3740
- "confidence": "model_suggested",
3741
- }
3742
- ],
3743
- },
3744
- }
3745
- with open(os.path.join(clip_dir, "analysis.json"), "w", encoding="utf-8") as handle:
3746
- json.dump(report, handle)
3747
-
3748
- built = build_analysis_index(root)
3749
- status = analysis_index_status(root)
3750
- transcript_results = query_analysis_index(root, "childhood memory", result_types="transcript")
3751
- marker_results = query_analysis_index(root, "reflective kitchen", result_types=["marker", "clip"])
3752
-
3753
- self.assertTrue(built["success"])
3754
- self.assertEqual(built["image_blob_policy"], "excluded")
3755
- self.assertEqual(built["counts"]["clips"], 1)
3756
- self.assertEqual(built["counts"]["markers"], 1)
3757
- self.assertEqual(built["counts"]["transcript_segments"], 1)
3758
- self.assertTrue(status["exists"])
3759
- self.assertEqual(status["counts"]["analysis_keyframes"], 1)
3760
- self.assertGreaterEqual(transcript_results["result_count"], 1)
3761
- self.assertEqual(transcript_results["results"][0]["result_type"], "transcript")
3762
- self.assertGreaterEqual(marker_results["result_count"], 1)
3763
-
3764
- conn = sqlite3.connect(built["index_path"])
3765
- try:
3766
- columns = [
3767
- row[1]
3768
- for row in conn.execute("PRAGMA table_info(analysis_keyframes)").fetchall()
3769
- ]
3770
- self.assertNotIn("frame_path", columns)
3771
- self.assertIsNone(
3772
- conn.execute(
3773
- "SELECT sql FROM sqlite_master WHERE sql LIKE ?",
3774
- ("%should-not-be-stored%",),
3775
- ).fetchone()
3776
- )
3777
- finally:
3778
- conn.close()
3779
-
3780
- def test_query_analysis_index_reports_missing_index(self):
3781
- with tempfile.TemporaryDirectory() as tmp:
3782
- root = resolve_output_root(
3783
- project_name="Example Project",
3784
- project_id="project-missing-index",
3785
- analysis_root=os.path.join(tmp, "analysis"),
3786
- source_paths=[],
3787
- create=True,
3788
- )["project_root"]
3789
- status = analysis_index_status(root)
3790
- result = query_analysis_index(root, "anything")
3791
-
3792
- self.assertTrue(status["success"])
3793
- self.assertFalse(status["exists"])
3794
- self.assertFalse(result["success"])
3795
- self.assertIn("Analysis index not found", (result["error"].get("message","") if isinstance(result["error"], dict) else result["error"]))
3796
-
3797
- def test_batch_job_slice_runs_and_builds_index(self):
3798
- if not shutil.which("ffmpeg") or not shutil.which("ffprobe"):
3799
- self.skipTest("ffmpeg/ffprobe not installed")
3800
- with tempfile.TemporaryDirectory() as tmp:
3801
- source_dir = os.path.join(tmp, "source")
3802
- analysis_dir = os.path.join(tmp, "analysis")
3803
- os.makedirs(source_dir)
3804
- source_a = os.path.join(source_dir, "job_a.mp4")
3805
- source_b = os.path.join(source_dir, "job_b.mp4")
3806
- self._write_synthetic_media(source_a)
3807
- self._write_synthetic_media(source_b)
3808
- records = [
3809
- {"clip_id": "clip-a", "clip_name": "job_a.mp4", "file_path": source_a, "media_id": "media-a"},
3810
- {"clip_id": "clip-b", "clip_name": "job_b.mp4", "file_path": source_b, "media_id": "media-b"},
3811
- ]
3812
- params = {
3813
- "analysis_root": analysis_dir,
3814
- "depth": "standard",
3815
- "max_analysis_frames": 2,
3816
- "transcription": {
3817
- "enabled": True,
3818
- "backend": "mock",
3819
- "segments": [{"start": 0, "end": 1.0, "text": "Batch transcript."}],
3820
- },
3821
- "vision": {"enabled": True, "provider": "mock"},
3822
- }
3823
- created = create_batch_job(
3824
- project_name="Example Project",
3825
- project_id="project-job",
3826
- records=records,
3827
- target={"type": "file_list"},
3828
- params=params,
3829
- name="Synthetic batch",
3830
- )
3831
- self.assertTrue(created["success"])
3832
- job = created["job"]
3833
- self.assertEqual(job["total_clips"], 2)
3834
- self.assertEqual(job["pending_clips"], 2)
3835
-
3836
- first = run_batch_job_slice(job["project_root"], job["job_id"], max_clips=1)
3837
- self.assertTrue(first["success"])
3838
- self.assertEqual(first["processed_count"], 1)
3839
- mid = batch_job_status(job["project_root"], job["job_id"])
3840
- mid_index = analysis_index_status(job["project_root"])
3841
- self.assertEqual(mid["succeeded_clips"], 1)
3842
- self.assertEqual(mid["pending_clips"], 1)
3843
- self.assertTrue(mid_index["exists"])
3844
- self.assertEqual(mid_index["counts"]["clips"], 1)
3845
-
3846
- second = run_batch_job_slice(job["project_root"], job["job_id"], max_clips=5)
3847
- self.assertTrue(second["success"])
3848
- final = batch_job_status(job["project_root"], job["job_id"])
3849
- self.assertEqual(final["status"], "completed")
3850
- self.assertEqual(final["succeeded_clips"], 2)
3851
- self.assertTrue(os.path.exists(final["paths"]["progress_json"]))
3852
- self.assertTrue(os.path.exists(final["paths"]["events_jsonl"]))
3853
-
3854
- jobs = list_batch_jobs(job["project_root"])
3855
- index = analysis_index_status(job["project_root"])
3856
- search = query_analysis_index(job["project_root"], "Batch transcript", result_types="transcript")
3857
- self.assertEqual(jobs["count"], 1)
3858
- self.assertTrue(index["exists"])
3859
- self.assertEqual(index["counts"]["clips"], 2)
3860
- self.assertGreaterEqual(search["result_count"], 1)
3861
-
3862
- def test_batch_job_cancel_and_resume(self):
3863
- with tempfile.TemporaryDirectory() as tmp:
3864
- source_dir = os.path.join(tmp, "source")
3865
- analysis_dir = os.path.join(tmp, "analysis")
3866
- os.makedirs(source_dir)
3867
- source = os.path.join(source_dir, "job_cancel.mov")
3868
- with open(source, "wb") as handle:
3869
- handle.write(b"placeholder")
3870
- created = create_batch_job_from_paths(
3871
- project_name="Example Project",
3872
- project_id="project-job-cancel",
3873
- paths=[source],
3874
- analysis_root=analysis_dir,
3875
- params={"depth": "quick", "transcription": {"enabled": False}},
3876
- name="Cancelable batch",
3877
- )
3878
- self.assertTrue(created["success"])
3879
- job = created["job"]
3880
-
3881
- canceled = cancel_batch_job(job["project_root"], job["job_id"])
3882
- self.assertEqual(canceled["status"], "canceled")
3883
- resumed = resume_batch_job(job["project_root"], job["job_id"])
3884
- self.assertEqual(resumed["status"], "queued")
3885
- self.assertEqual(resumed["pending_clips"], 1)
3886
-
3887
-
3888
- class MediaAnalysisCoverageTests(unittest.TestCase):
3889
- """Pre-flight coverage_report assessment used by editorial / color / online guardrails."""
3890
-
3891
- def _write_report(
3892
- self,
3893
- *,
3894
- analysis_root: str,
3895
- project_name: str,
3896
- project_id: str,
3897
- source: str,
3898
- record: Dict[str, Any],
3899
- depth: str = "standard",
3900
- options: Optional[Dict[str, Any]] = None,
3901
- analyzed_at: str = "2026-05-20T12:00:00Z",
3902
- source_trust: str = "auto",
3903
- with_transcription: bool = False,
3904
- with_visual: bool = False,
3905
- signature_override: Optional[Dict[str, Any]] = None,
3906
- ) -> str:
3907
- opts = options or {"transcription": {"enabled": with_transcription}, "vision": {"enabled": with_visual}}
3908
- project_root = resolve_output_root(
3909
- project_name=project_name,
3910
- project_id=project_id,
3911
- analysis_root=analysis_root,
3912
- source_paths=[source],
3913
- create=True,
3914
- )["project_root"]
3915
- clip_dir = os.path.join(project_root, "clips", stable_clip_directory(record))
3916
- os.makedirs(clip_dir, exist_ok=True)
3917
- report_path = os.path.join(clip_dir, "analysis.json")
3918
- signature = signature_override or analysis_request_signature(record, depth, opts, 8)
3919
- report = {
3920
- "success": True,
3921
- "analysis_version": "0.2",
3922
- "analysis_signature": signature,
3923
- "analysis_profile": {
3924
- "depth": depth,
3925
- "analysis_keyframe_budget": 8,
3926
- "transcription_enabled": with_transcription,
3927
- "vision_enabled": with_visual,
3928
- "source_trust": source_trust,
3929
- },
3930
- "analyzed_at": analyzed_at,
3931
- "source_file": source,
3932
- "clip": record,
3933
- "technical": {"format": {"duration_seconds": 1.0}},
3934
- "readthrough": {"success": True, "cut_analysis": {"success": True}},
3935
- "motion": {"success": True, "analysis_keyframes": [{"time_seconds": 0.0}], "overall_motion_level": "low"},
3936
- "transcription": (
3937
- {"success": True, "text": "hello world", "segments": [{"start": 0.0, "end": 1.0, "text": "hello world"}]}
3938
- if with_transcription else {"success": True, "status": "skipped"}
3939
- ),
3940
- "visual": (
3941
- {"success": True, "clip_summary": "A test clip.", "shot_descriptions": [{"shot_index": 1, "description": "Clip overview."}]}
3942
- if with_visual else {"success": True, "status": "skipped"}
3943
- ),
3944
- "clip_analysis_markers": {"success": True, "marker_count": 1, "markers": []},
3945
- }
3946
- with open(report_path, "w", encoding="utf-8") as handle:
3947
- json.dump(report, handle)
3948
- update_analysis_registry(project_root, report_paths=[report_path])
3949
- return report_path
3950
-
3951
- def _base_capabilities(self) -> Dict[str, Any]:
3952
- return {
3953
- "tools": {"ffprobe": {"available": True}, "ffmpeg": {"available": True}},
3954
- "transcription": {"available": False},
3955
- "vision": {"available": False},
3956
- }
3957
-
3958
- def test_coverage_report_empty_target_returns_zero_summary(self):
3959
- with tempfile.TemporaryDirectory() as tmp:
3960
- coverage = build_coverage_report(
3961
- project_name="Empty Project",
3962
- project_id="v1",
3963
- records=[],
3964
- target={"type": "bin", "path": "Master"},
3965
- params={"analysis_root": os.path.join(tmp, "analysis"), "depth": "standard"},
3966
- capabilities=self._base_capabilities(),
3967
- )
3968
- self.assertTrue(coverage["success"])
3969
- self.assertEqual(coverage["summary"]["clips_total"], 0)
3970
- self.assertEqual(coverage["summary"]["clips_analyzed"], 0)
3971
- self.assertEqual(coverage["clips"], [])
3972
- self.assertIn("evidence base: no clips in target", coverage["evidence_base"])
3973
-
3974
- def test_coverage_report_all_current_reports_100_percent(self):
3975
- with tempfile.TemporaryDirectory() as tmp:
3976
- source = os.path.join(tmp, "source", "A001.mov")
3977
- os.makedirs(os.path.dirname(source))
3978
- with open(source, "wb") as handle:
3979
- handle.write(b"x" * 100)
3980
- record = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
3981
- analysis_root = os.path.join(tmp, "analysis")
3982
- self._write_report(
3983
- analysis_root=analysis_root,
3984
- project_name="Current Project",
3985
- project_id="v1",
3986
- source=source,
3987
- record=record,
3988
- source_trust="medium",
3989
- )
3990
-
3991
- coverage = build_coverage_report(
3992
- project_name="Current Project",
3993
- project_id="v1",
3994
- records=[record],
3995
- target={"type": "bin", "path": "Master"},
3996
- params={
3997
- "analysis_root": analysis_root,
3998
- "depth": "standard",
3999
- "transcription": {"enabled": False},
4000
- },
4001
- capabilities=self._base_capabilities(),
4002
- )
4003
- self.assertTrue(coverage["success"])
4004
- self.assertEqual(coverage["summary"]["clips_total"], 1)
4005
- self.assertEqual(coverage["summary"]["clips_analyzed"], 1)
4006
- self.assertEqual(coverage["summary"]["coverage_percent"], 100.0)
4007
- self.assertEqual(coverage["summary"]["source_trust_distribution"], {"medium": 1})
4008
- clip = coverage["clips"][0]
4009
- self.assertTrue(clip["analyzed"])
4010
- self.assertEqual(clip["cache_status"], "reusable")
4011
- self.assertEqual(clip["source_trust"], "medium")
4012
- self.assertIn("technical", clip["layers_present"])
4013
- self.assertIn("motion", clip["layers_present"])
4014
- self.assertIn("evidence base: 1/1 clips analyzed (100%)", coverage["evidence_base"])
4015
-
4016
- def test_coverage_report_flags_missing_transcription_layer(self):
4017
- with tempfile.TemporaryDirectory() as tmp:
4018
- source = os.path.join(tmp, "source", "A001.mov")
4019
- os.makedirs(os.path.dirname(source))
4020
- with open(source, "wb") as handle:
4021
- handle.write(b"x" * 100)
4022
- record = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
4023
- analysis_root = os.path.join(tmp, "analysis")
4024
- # Report exists but without transcription
4025
- self._write_report(
4026
- analysis_root=analysis_root,
4027
- project_name="Missing Layer Project",
4028
- project_id="v1",
4029
- source=source,
4030
- record=record,
4031
- with_transcription=False,
4032
- )
4033
-
4034
- # Request requires transcription — report is incomplete
4035
- coverage = build_coverage_report(
4036
- project_name="Missing Layer Project",
4037
- project_id="v1",
4038
- records=[record],
4039
- target={"type": "bin", "path": "Master"},
4040
- params={
4041
- "analysis_root": analysis_root,
4042
- "depth": "standard",
4043
- "transcription": {"enabled": True},
4044
- },
4045
- capabilities=self._base_capabilities(),
4046
- )
4047
- self.assertEqual(coverage["summary"]["clips_stale"], 1)
4048
- clip = coverage["clips"][0]
4049
- self.assertFalse(clip["analyzed"])
4050
- self.assertIn("transcription", clip["missing_layers"])
4051
- self.assertIn("missing layers", clip["recommended_action"])
4052
- self.assertIn("transcription", clip["recommended_action"])
4053
-
4054
- def test_coverage_report_marks_signature_drift_as_stale(self):
4055
- with tempfile.TemporaryDirectory() as tmp:
4056
- source = os.path.join(tmp, "source", "A001.mov")
4057
- os.makedirs(os.path.dirname(source))
4058
- with open(source, "wb") as handle:
4059
- handle.write(b"original")
4060
- record = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
4061
- analysis_root = os.path.join(tmp, "analysis")
4062
- # Persist report with the current signature.
4063
- self._write_report(
4064
- analysis_root=analysis_root,
4065
- project_name="Signature Drift Project",
4066
- project_id="v1",
4067
- source=source,
4068
- record=record,
4069
- )
4070
- # Now modify the source file so its mtime+size change → signature mismatch.
4071
- import time as _time
4072
- _time.sleep(0.05)
4073
- with open(source, "wb") as handle:
4074
- handle.write(b"changed bytes here change change change")
4075
-
4076
- coverage = build_coverage_report(
4077
- project_name="Signature Drift Project",
4078
- project_id="v1",
4079
- records=[record],
4080
- target={"type": "bin", "path": "Master"},
4081
- params={
4082
- "analysis_root": analysis_root,
4083
- "depth": "standard",
4084
- "transcription": {"enabled": False},
4085
- },
4086
- capabilities=self._base_capabilities(),
4087
- )
4088
- clip = coverage["clips"][0]
4089
- self.assertEqual(clip["cache_status"], "stale_or_incomplete")
4090
- self.assertTrue(any("source_" in reason for reason in clip["staleness_reasons"]))
4091
- self.assertEqual(coverage["summary"]["clips_stale"], 1)
4092
-
4093
- def test_coverage_report_surfaces_reuse_blocked_from_provenance_orphan(self):
4094
- with tempfile.TemporaryDirectory() as tmp:
4095
- source = os.path.join(tmp, "source", "A001.mov")
4096
- os.makedirs(os.path.dirname(source))
4097
- with open(source, "wb") as handle:
4098
- handle.write(b"x" * 100)
4099
- record = {
4100
- "clip_id": "clip-1",
4101
- "clip_name": "A001.mov",
4102
- "file_path": source,
4103
- "media_id": "media-1",
4104
- # Provenance claims a report exists, but the path is missing
4105
- "analysis_report_path": os.path.join(tmp, "analysis", "ghost", "clips", "missing", "analysis.json"),
4106
- "published_analysis_signature": "ghost-signature",
4107
- }
4108
- analysis_root = os.path.join(tmp, "analysis")
4109
-
4110
- coverage = build_coverage_report(
4111
- project_name="Orphan Provenance Project",
4112
- project_id="v1",
4113
- records=[record],
4114
- target={"type": "bin", "path": "Master"},
4115
- params={
4116
- "analysis_root": analysis_root,
4117
- "depth": "standard",
4118
- "transcription": {"enabled": False},
4119
- },
4120
- capabilities=self._base_capabilities(),
4121
- )
4122
- clip = coverage["clips"][0]
4123
- self.assertTrue(clip["reuse_blocked"])
4124
- self.assertEqual(clip["cache_status"], "reuse_blocked")
4125
- self.assertEqual(coverage["summary"]["clips_reuse_blocked"], 1)
4126
- self.assertIn("force_refresh=true", clip["recommended_action"])
4127
-
4128
- def test_coverage_report_min_source_trust_filters_below_threshold(self):
4129
- with tempfile.TemporaryDirectory() as tmp:
4130
- source = os.path.join(tmp, "source", "A001.mov")
4131
- os.makedirs(os.path.dirname(source))
4132
- with open(source, "wb") as handle:
4133
- handle.write(b"x" * 100)
4134
- record = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
4135
- analysis_root = os.path.join(tmp, "analysis")
4136
- self._write_report(
4137
- analysis_root=analysis_root,
4138
- project_name="Trust Filter Project",
4139
- project_id="v1",
4140
- source=source,
4141
- record=record,
4142
- source_trust="low",
4143
- )
4144
-
4145
- coverage = build_coverage_report(
4146
- project_name="Trust Filter Project",
4147
- project_id="v1",
4148
- records=[record],
4149
- target={"type": "bin", "path": "Master"},
4150
- params={
4151
- "analysis_root": analysis_root,
4152
- "depth": "standard",
4153
- "transcription": {"enabled": False},
4154
- "min_source_trust": "high",
4155
- },
4156
- capabilities=self._base_capabilities(),
4157
- )
4158
- clip = coverage["clips"][0]
4159
- # Report exists and is layer-complete, but trust is below threshold
4160
- self.assertEqual(coverage["min_source_trust"], "high")
4161
- self.assertTrue(clip["below_min_source_trust"])
4162
- self.assertEqual(clip["source_trust"], "low")
4163
- self.assertEqual(coverage["summary"]["clips_needs_higher_trust"], 1)
4164
- # And it should NOT be counted as analyzed for evidence-base purposes
4165
- self.assertEqual(coverage["summary"]["clips_analyzed"], 0)
4166
-
4167
- def test_mark_registry_stale_flags_matching_entry(self):
4168
- with tempfile.TemporaryDirectory() as tmp:
4169
- source = os.path.join(tmp, "source", "A001.mov")
4170
- os.makedirs(os.path.dirname(source))
4171
- with open(source, "wb") as handle:
4172
- handle.write(b"x" * 100)
4173
- record = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
4174
- analysis_root = os.path.join(tmp, "analysis")
4175
- report_path = self._write_report(
4176
- analysis_root=analysis_root,
4177
- project_name="Relink Project",
4178
- project_id="v1",
4179
- source=source,
4180
- record=record,
4181
- )
4182
-
4183
- result = mark_registry_stale_for_clip(
4184
- project_name="Relink Project",
4185
- project_id="v1",
4186
- analysis_root=analysis_root,
4187
- clip_id="clip-1",
4188
- reason="replace_clip",
4189
- )
4190
- self.assertTrue(result["success"])
4191
- self.assertEqual(result["matched"], 1)
4192
-
4193
- superseded = registry_entry_superseded_info(
4194
- resolve_output_root(
4195
- project_name="Relink Project",
4196
- project_id="v1",
4197
- analysis_root=analysis_root,
4198
- source_paths=[source],
4199
- create=False,
4200
- )["project_root"],
4201
- report_path,
4202
- )
4203
- self.assertIsNotNone(superseded)
4204
- self.assertTrue(superseded["superseded_by_relink"])
4205
- self.assertEqual(superseded["superseded_reason"], "replace_clip")
4206
-
4207
- def test_mark_registry_stale_survives_registry_rebuild(self):
4208
- """update_analysis_registry must preserve superseded_by_relink across rebuilds."""
4209
- with tempfile.TemporaryDirectory() as tmp:
4210
- source = os.path.join(tmp, "source", "A001.mov")
4211
- os.makedirs(os.path.dirname(source))
4212
- with open(source, "wb") as handle:
4213
- handle.write(b"x" * 100)
4214
- record = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
4215
- analysis_root = os.path.join(tmp, "analysis")
4216
- report_path = self._write_report(
4217
- analysis_root=analysis_root,
4218
- project_name="Rebuild Project",
4219
- project_id="v1",
4220
- source=source,
4221
- record=record,
4222
- )
4223
- mark_registry_stale_for_clip(
4224
- project_name="Rebuild Project",
4225
- project_id="v1",
4226
- analysis_root=analysis_root,
4227
- clip_id="clip-1",
4228
- reason="replace_clip",
4229
- )
4230
-
4231
- project_root = resolve_output_root(
4232
- project_name="Rebuild Project",
4233
- project_id="v1",
4234
- analysis_root=analysis_root,
4235
- source_paths=[source],
4236
- create=False,
4237
- )["project_root"]
4238
- # Rebuild — simulates what commit_visual_analysis does after analysis.
4239
- update_analysis_registry(project_root, report_paths=[report_path])
4240
-
4241
- superseded = registry_entry_superseded_info(project_root, report_path)
4242
- self.assertIsNotNone(superseded, "superseded flag must survive registry rebuild")
4243
- self.assertEqual(superseded["superseded_reason"], "replace_clip")
4244
-
4245
- def test_coverage_report_surfaces_superseded_by_relink(self):
4246
- with tempfile.TemporaryDirectory() as tmp:
4247
- source = os.path.join(tmp, "source", "A001.mov")
4248
- os.makedirs(os.path.dirname(source))
4249
- with open(source, "wb") as handle:
4250
- handle.write(b"x" * 100)
4251
- record = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
4252
- analysis_root = os.path.join(tmp, "analysis")
4253
- self._write_report(
4254
- analysis_root=analysis_root,
4255
- project_name="Relink Coverage Project",
4256
- project_id="v1",
4257
- source=source,
4258
- record=record,
4259
- )
4260
- mark_registry_stale_for_clip(
4261
- project_name="Relink Coverage Project",
4262
- project_id="v1",
4263
- analysis_root=analysis_root,
4264
- clip_id="clip-1",
4265
- reason="replace_clip",
4266
- )
4267
-
4268
- coverage = build_coverage_report(
4269
- project_name="Relink Coverage Project",
4270
- project_id="v1",
4271
- records=[record],
4272
- target={"type": "bin", "path": "Master"},
4273
- params={
4274
- "analysis_root": analysis_root,
4275
- "depth": "standard",
4276
- "transcription": {"enabled": False},
4277
- },
4278
- capabilities=self._base_capabilities(),
4279
- )
4280
- clip = coverage["clips"][0]
4281
- self.assertTrue(clip["superseded_by_relink"])
4282
- self.assertFalse(clip["analyzed"]) # relink defeats reusability
4283
- self.assertEqual(coverage["summary"]["clips_stale"], 1)
4284
- self.assertEqual(coverage["summary"]["clips_analyzed"], 0)
4285
- self.assertIn("replaced", clip["recommended_action"].lower() + " " + clip["recommended_action"].lower())
4286
- self.assertIn("re-analyze", clip["recommended_action"].lower())
4287
-
4288
- def test_coverage_report_fires_reuse_blocked_on_signature_drifted_provenance(self):
4289
- """Provenance points to a report that exists but is signature-drifted → reuse_blocked, not silent stale."""
4290
- with tempfile.TemporaryDirectory() as tmp:
4291
- source = os.path.join(tmp, "source", "A001.mov")
4292
- os.makedirs(os.path.dirname(source))
4293
- with open(source, "wb") as handle:
4294
- handle.write(b"original")
4295
- record_initial = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
4296
- analysis_root = os.path.join(tmp, "analysis")
4297
- report_path = self._write_report(
4298
- analysis_root=analysis_root,
4299
- project_name="Signature Drift Provenance Project",
4300
- project_id="v1",
4301
- source=source,
4302
- record=record_initial,
4303
- )
4304
- # Drift the source so signature no longer matches
4305
- import time as _time
4306
- _time.sleep(0.05)
4307
- with open(source, "wb") as handle:
4308
- handle.write(b"changed bytes here change change change")
4309
-
4310
- record_with_provenance = {
4311
- **record_initial,
4312
- "analysis_report_path": report_path,
4313
- "published_analysis_signature": "any-signature-string",
4314
- }
4315
- coverage = build_coverage_report(
4316
- project_name="Signature Drift Provenance Project",
4317
- project_id="v1",
4318
- records=[record_with_provenance],
4319
- target={"type": "bin", "path": "Master"},
4320
- params={
4321
- "analysis_root": analysis_root,
4322
- "depth": "standard",
4323
- "transcription": {"enabled": False},
4324
- },
4325
- capabilities=self._base_capabilities(),
4326
- )
4327
- clip = coverage["clips"][0]
4328
- self.assertTrue(clip["reuse_blocked"])
4329
- self.assertEqual(coverage["summary"]["clips_reuse_blocked"], 1)
4330
- self.assertIn("force_refresh=true", clip["recommended_action"])
4331
-
4332
- def test_coverage_report_does_not_fire_reuse_blocked_without_provenance(self):
4333
- """Plain stale report without Resolve-side provenance must NOT escalate to reuse_blocked."""
4334
- with tempfile.TemporaryDirectory() as tmp:
4335
- source = os.path.join(tmp, "source", "A001.mov")
4336
- os.makedirs(os.path.dirname(source))
4337
- with open(source, "wb") as handle:
4338
- handle.write(b"original")
4339
- record = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
4340
- analysis_root = os.path.join(tmp, "analysis")
4341
- self._write_report(
4342
- analysis_root=analysis_root,
4343
- project_name="No Provenance Project",
4344
- project_id="v1",
4345
- source=source,
4346
- record=record,
4347
- )
4348
- import time as _time
4349
- _time.sleep(0.05)
4350
- with open(source, "wb") as handle:
4351
- handle.write(b"drift drift drift drift drift drift")
4352
-
4353
- coverage = build_coverage_report(
4354
- project_name="No Provenance Project",
4355
- project_id="v1",
4356
- records=[record], # no analysis_report_path field
4357
- target={"type": "bin", "path": "Master"},
4358
- params={
4359
- "analysis_root": analysis_root,
4360
- "depth": "standard",
4361
- "transcription": {"enabled": False},
4362
- },
4363
- capabilities=self._base_capabilities(),
4364
- )
4365
- clip = coverage["clips"][0]
4366
- self.assertFalse(clip["reuse_blocked"])
4367
- self.assertEqual(clip["cache_status"], "stale_or_incomplete")
4368
- self.assertEqual(coverage["summary"]["clips_reuse_blocked"], 0)
4369
- self.assertEqual(coverage["summary"]["clips_stale"], 1)
4370
-
4371
- def test_analysis_root_coverage_summarizes_disk(self):
4372
- """Standalone coverage helper powers the control panel Readiness card."""
4373
- with tempfile.TemporaryDirectory() as tmp:
4374
- source = os.path.join(tmp, "source", "A001.mov")
4375
- os.makedirs(os.path.dirname(source))
4376
- with open(source, "wb") as handle:
4377
- handle.write(b"x" * 100)
4378
- record_a = {"clip_id": "clip-a", "clip_name": "A001.mov", "file_path": source, "media_id": "media-a"}
4379
- record_b = {
4380
- "clip_id": "clip-b",
4381
- "clip_name": "B001.mov",
4382
- "file_path": os.path.join(tmp, "source", "B001.mov"),
4383
- "media_id": "media-b",
4384
- }
4385
- os.makedirs(os.path.dirname(record_b["file_path"]), exist_ok=True)
4386
- with open(record_b["file_path"], "wb") as handle:
4387
- handle.write(b"y" * 100)
4388
- analysis_root = os.path.join(tmp, "analysis")
4389
- self._write_report(
4390
- analysis_root=analysis_root,
4391
- project_name="Standalone Coverage Project",
4392
- project_id="v1",
4393
- source=source,
4394
- record=record_a,
4395
- source_trust="high",
4396
- with_transcription=True,
4397
- )
4398
- self._write_report(
4399
- analysis_root=analysis_root,
4400
- project_name="Standalone Coverage Project",
4401
- project_id="v1",
4402
- source=record_b["file_path"],
4403
- record=record_b,
4404
- source_trust="medium",
4405
- )
4406
- mark_registry_stale_for_clip(
4407
- project_name="Standalone Coverage Project",
4408
- project_id="v1",
4409
- analysis_root=analysis_root,
4410
- clip_id="clip-b",
4411
- reason="replace_clip",
4412
- )
4413
-
4414
- project_root = resolve_output_root(
4415
- project_name="Standalone Coverage Project",
4416
- project_id="v1",
4417
- analysis_root=analysis_root,
4418
- source_paths=[source],
4419
- create=False,
4420
- )["project_root"]
4421
- payload = analysis_root_coverage(project_root)
4422
-
4423
- self.assertTrue(payload["success"])
4424
- summary = payload["summary"]
4425
- self.assertEqual(summary["clips_total_with_reports"], 2)
4426
- self.assertEqual(summary["clips_signed"], 2)
4427
- self.assertEqual(summary["clips_superseded_by_relink"], 1)
4428
- self.assertEqual(summary["source_trust_distribution"], {"high": 1, "medium": 1})
4429
- # Transcription was enabled on clip-a only.
4430
- self.assertEqual(summary["layer_coverage"].get("transcription"), 1)
4431
- self.assertEqual(summary["layer_coverage"].get("technical"), 2)
4432
- # The superseded clip should appear first in the prioritized list.
4433
- self.assertTrue(payload["analyzed_clips"][0]["superseded_by_relink"])
4434
-
4435
- def test_coverage_report_persists_source_trust_via_analysis_profile(self):
4436
- """source_trust persisted by _build_clip_analysis_report flows through to coverage."""
4437
- with tempfile.TemporaryDirectory() as tmp:
4438
- source = os.path.join(tmp, "source", "A001.mov")
4439
- os.makedirs(os.path.dirname(source))
4440
- with open(source, "wb") as handle:
4441
- handle.write(b"x" * 100)
4442
- record = {"clip_id": "clip-1", "clip_name": "A001.mov", "file_path": source, "media_id": "media-1"}
4443
- analysis_root = os.path.join(tmp, "analysis")
4444
- self._write_report(
4445
- analysis_root=analysis_root,
4446
- project_name="Trust Persistence Project",
4447
- project_id="v1",
4448
- source=source,
4449
- record=record,
4450
- source_trust="high",
4451
- )
4452
-
4453
- coverage = build_coverage_report(
4454
- project_name="Trust Persistence Project",
4455
- project_id="v1",
4456
- records=[record],
4457
- target={"type": "bin", "path": "Master"},
4458
- params={
4459
- "analysis_root": analysis_root,
4460
- "depth": "standard",
4461
- "transcription": {"enabled": False},
4462
- },
4463
- capabilities=self._base_capabilities(),
4464
- )
4465
- self.assertEqual(coverage["clips"][0]["source_trust"], "high")
4466
- self.assertEqual(coverage["summary"]["source_trust_distribution"], {"high": 1})
4467
-
4468
-
4469
- class PathExistenceProbeTests(unittest.TestCase):
4470
- """Parallel/cached file-existence probing for the Resolve media inventory."""
4471
-
4472
- def setUp(self):
4473
- from src import analysis_dashboard as dash
4474
- self.dash = dash
4475
- dash._PATH_EXISTS_CACHE.clear()
4476
-
4477
- def test_fresh_probe_reports_real_and_missing_paths(self):
4478
- with tempfile.TemporaryDirectory() as tmp:
4479
- real = os.path.join(tmp, "a.mov")
4480
- open(real, "w").close()
4481
- missing = os.path.join(tmp, "gone.mov")
4482
- result = self.dash._probe_paths_exist([real, missing, None, ""], probe=True)
4483
- self.assertTrue(result[real])
4484
- self.assertFalse(result[missing])
4485
- # Empty/None entries are ignored, not probed.
4486
- self.assertNotIn("", result)
4487
-
4488
- def test_background_poll_reuses_cache_without_restating(self):
4489
- with tempfile.TemporaryDirectory() as tmp:
4490
- real = os.path.join(tmp, "a.mov")
4491
- open(real, "w").close()
4492
- # Warm the cache with a real probe, then delete the file.
4493
- self.dash._probe_paths_exist([real], probe=True)
4494
- os.remove(real)
4495
- # Background poll must not stat — it trusts the cached True.
4496
- result = self.dash._probe_paths_exist([real], probe=False)
4497
- self.assertTrue(result[real])
4498
-
4499
- def test_background_poll_assumes_present_for_uncached_path(self):
4500
- # No cache entry + no probe → trust Resolve's own status, assume present.
4501
- result = self.dash._probe_paths_exist(["/nonexistent/never-probed.mov"], probe=False)
4502
- self.assertTrue(result["/nonexistent/never-probed.mov"])
4503
-
4504
- def test_fresh_probe_restats_after_cache_cleared(self):
4505
- with tempfile.TemporaryDirectory() as tmp:
4506
- real = os.path.join(tmp, "a.mov")
4507
- open(real, "w").close()
4508
- self.dash._probe_paths_exist([real], probe=True)
4509
- os.remove(real)
4510
- self.dash._PATH_EXISTS_CACHE.clear() # simulate TTL expiry
4511
- result = self.dash._probe_paths_exist([real], probe=True)
4512
- self.assertFalse(result[real])
4513
-
4514
- def test_finalize_clip_record_sets_status_from_existence(self):
4515
- rec = {"file_path": "/x/b.mov", "clip_name": "b", "media_type": "Video", "_props": {}}
4516
- self.dash._finalize_clip_record(rec, True)
4517
- self.assertEqual(rec["status"], "online")
4518
- self.assertTrue(rec["file_exists"])
4519
- self.assertNotIn("_props", rec)
4520
-
4521
- rec2 = {"file_path": "/x/c.mov", "clip_name": "c", "media_type": "Video", "_props": {}}
4522
- self.dash._finalize_clip_record(rec2, False)
4523
- self.assertEqual(rec2["status"], "missing_file")
4524
- self.assertFalse(rec2["file_exists"])
4525
-
4526
-
4527
- class InventoryCacheReuseTests(unittest.TestCase):
4528
- """Background-poll reuse of the cached Resolve walk + analysis overlay."""
4529
-
4530
- def setUp(self):
4531
- from src import analysis_dashboard as dash
4532
- self.dash = dash
4533
- dash._INVENTORY_CACHE.clear()
4534
-
4535
- def _entry(self):
4536
- analyzed = {"file_path": "/x/a.mov", "clip_name": "a", "media_type": "Video",
4537
- "status": "online", "source_clip": True, "analyzable": True,
4538
- "selected": True, "clip_key": "a-key"}
4539
- plain = {"file_path": "/x/b.mov", "clip_name": "b", "media_type": "Video",
4540
- "status": "online", "source_clip": True, "analyzable": True,
4541
- "selected": False, "clip_key": "b-key"}
4542
- return {"base_records": [analyzed, plain], "project": {"name": "P", "id": "1"},
4543
- "selected_count": 1, "truncated": False, "limit": 500, "warnings": []}
4544
-
4545
- def test_assemble_applies_overlay_without_contaminating_base(self):
4546
- with tempfile.TemporaryDirectory() as tmp:
4547
- entry = self._entry()
4548
- payload = self.dash._assemble_inventory_payload(tmp, entry)
4549
- self.assertTrue(payload["resolve_available"])
4550
- self.assertEqual(payload["counts"]["total"], 2)
4551
- self.assertEqual(payload["counts"]["analyzed"], 0)
4552
- self.assertEqual(payload["counts"]["selected"], 1)
4553
- # The cached base must stay free of the per-call analysis overlay.
4554
- self.assertNotIn("analysis_status", entry["base_records"][0])
4555
-
4556
- def test_overlay_reflects_new_analysis_without_resolve(self):
4557
- # The whole point of poll-reuse: analysis progress is picked up from disk
4558
- # on every assemble, with no Resolve walk.
4559
- with tempfile.TemporaryDirectory() as tmp:
4560
- entry = self._entry()
4561
- self.assertEqual(self.dash._assemble_inventory_payload(tmp, entry)["counts"]["analyzed"], 0)
4562
- os.makedirs(os.path.join(tmp, "clips", "a-key"))
4563
- with open(os.path.join(tmp, "clips", "a-key", "analysis.json"), "w") as fh:
4564
- fh.write("{}")
4565
- self.assertEqual(self.dash._assemble_inventory_payload(tmp, entry)["counts"]["analyzed"], 1)
4566
-
4567
- def _without_resolve(self):
4568
- # Make the Resolve probe deterministically absent so these tests pass
4569
- # whether or not a live Resolve instance happens to be running.
4570
- return unittest.mock.patch.object(
4571
- self.dash, "_connect_resolve_read_only",
4572
- return_value=(None, "Resolve unavailable (stubbed for test)"),
4573
- )
4574
-
4575
- def test_reuse_cached_serves_from_cache(self):
4576
- with tempfile.TemporaryDirectory() as tmp:
4577
- self.dash._store_cached_inventory(tmp, self._entry())
4578
- with self._without_resolve():
4579
- payload = self.dash.resolve_media_inventory(tmp, reuse_cached=True)
4580
- self.assertTrue(payload["resolve_available"])
4581
- self.assertEqual(payload["counts"]["total"], 2)
4582
-
4583
- def test_reuse_miss_falls_through_to_build(self):
4584
- # No cache entry → full build; without Resolve that surfaces as unavailable
4585
- # rather than silently returning empty cached data.
4586
- with self._without_resolve():
4587
- payload = self.dash.resolve_media_inventory("/no/such/cache/root", reuse_cached=True)
4588
- self.assertFalse(payload["resolve_available"])
4589
-
4590
- def test_resolve_identity_lock_is_reentrant(self):
4591
- # _resolve_identity is lock-decorated and calls _connect (same RLock):
4592
- # must return, not deadlock, when Resolve is absent.
4593
- ident = self.dash._resolve_identity()
4594
- self.assertIn("available", ident)
4595
-
4596
- def test_reuse_serves_cache_when_project_id_matches(self):
4597
- with tempfile.TemporaryDirectory() as tmp:
4598
- self.dash._store_cached_inventory(tmp, self._entry()) # cached id == "1"
4599
- original = self.dash._current_resolve_project_id
4600
- self.dash._current_resolve_project_id = lambda: ("1", None)
4601
- try:
4602
- payload = self.dash.resolve_media_inventory(tmp, reuse_cached=True)
4603
- finally:
4604
- self.dash._current_resolve_project_id = original
4605
- self.assertTrue(payload["resolve_available"])
4606
- self.assertEqual(payload["counts"]["total"], 2)
4607
-
4608
- def test_reuse_rebuilds_when_resolve_project_switched(self):
4609
- with tempfile.TemporaryDirectory() as tmp:
4610
- self.dash._store_cached_inventory(tmp, self._entry()) # cached id == "1"
4611
- original = self.dash._current_resolve_project_id
4612
- self.dash._current_resolve_project_id = lambda: ("2", None) # different project
4613
- try:
4614
- with self._without_resolve():
4615
- payload = self.dash.resolve_media_inventory(tmp, reuse_cached=True)
4616
- finally:
4617
- self.dash._current_resolve_project_id = original
4618
- # Confirmed mismatch → full rebuild, which is unavailable without Resolve.
4619
- self.assertFalse(payload["resolve_available"])
4620
-
4621
- def test_reuse_keeps_cache_when_current_project_unknown(self):
4622
- # Transient inability to read the current project must not trigger an
4623
- # expensive rebuild on every poll — keep serving the cache.
4624
- with tempfile.TemporaryDirectory() as tmp:
4625
- self.dash._store_cached_inventory(tmp, self._entry())
4626
- original = self.dash._current_resolve_project_id
4627
- self.dash._current_resolve_project_id = lambda: (None, "Resolve unavailable")
4628
- try:
4629
- payload = self.dash.resolve_media_inventory(tmp, reuse_cached=True)
4630
- finally:
4631
- self.dash._current_resolve_project_id = original
4632
- self.assertTrue(payload["resolve_available"])
4633
- self.assertEqual(payload["counts"]["total"], 2)
4634
-
4635
-
4636
- if __name__ == "__main__":
4637
- unittest.main()