@1agh/maude 0.42.0 → 0.44.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 (332) hide show
  1. package/apps/studio/.ai/cache/_stats.json +7 -0
  2. package/apps/studio/acp/bootstrap-brief.ts +12 -2
  3. package/apps/studio/acp/bridge.ts +44 -10
  4. package/apps/studio/acp/env.ts +23 -0
  5. package/apps/studio/acp/login-state.ts +263 -0
  6. package/apps/studio/acp/plugin-bootstrap.ts +31 -25
  7. package/apps/studio/acp/probe.ts +152 -1
  8. package/apps/studio/annotations-layer.tsx +1451 -324
  9. package/apps/studio/annotations-model.ts +58 -0
  10. package/apps/studio/api.ts +568 -24
  11. package/apps/studio/bin/_audio-search.mjs +140 -0
  12. package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
  13. package/apps/studio/bin/_import-asset.mjs +815 -0
  14. package/apps/studio/bin/_import-brand.mjs +635 -0
  15. package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
  16. package/apps/studio/bin/_import-tokens.mjs +1201 -0
  17. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  18. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  19. package/apps/studio/bin/_transcribe.mjs +419 -0
  20. package/apps/studio/bin/_transcribe.test.mjs +80 -0
  21. package/apps/studio/bin/_video-playwright.mjs +141 -71
  22. package/apps/studio/bin/audio-search.sh +28 -0
  23. package/apps/studio/bin/generate.sh +154 -0
  24. package/apps/studio/bin/import-asset.sh +34 -0
  25. package/apps/studio/bin/import-brand.sh +29 -0
  26. package/apps/studio/bin/import-tokens.sh +33 -0
  27. package/apps/studio/bin/ingest-footage.sh +27 -0
  28. package/apps/studio/bin/photo-adjust.sh +163 -0
  29. package/apps/studio/bin/photo-bg-remove.sh +258 -0
  30. package/apps/studio/bin/probe-footage.sh +28 -0
  31. package/apps/studio/bin/read-annotations.mjs +125 -2
  32. package/apps/studio/bin/transcribe.sh +39 -0
  33. package/apps/studio/canvas-edit.ts +656 -12
  34. package/apps/studio/canvas-lib.tsx +637 -10
  35. package/apps/studio/canvas-pipeline.ts +65 -7
  36. package/apps/studio/canvas-shell.tsx +156 -16
  37. package/apps/studio/client/app.jsx +2385 -699
  38. package/apps/studio/client/export-center.jsx +35 -1
  39. package/apps/studio/client/generate-dialog.jsx +352 -0
  40. package/apps/studio/client/github.js +18 -0
  41. package/apps/studio/client/inspector-controls.jsx +781 -0
  42. package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
  43. package/apps/studio/client/panels/ChatPanel.jsx +15 -33
  44. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  45. package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
  46. package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
  47. package/apps/studio/client/panels/ReadinessList.jsx +227 -7
  48. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  49. package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
  50. package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
  51. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  52. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  53. package/apps/studio/client/photo-knobs.jsx +432 -0
  54. package/apps/studio/client/styles/3-shell-maude.css +376 -28
  55. package/apps/studio/client/styles/4-components.css +223 -0
  56. package/apps/studio/client/styles/6-acp-chat.css +86 -0
  57. package/apps/studio/client/tour/quick-setup-tour.js +39 -0
  58. package/apps/studio/commands/annotation-strokes-command.ts +13 -3
  59. package/apps/studio/commands/edit-source-command.ts +31 -4
  60. package/apps/studio/config.schema.json +70 -0
  61. package/apps/studio/context-menu.tsx +42 -8
  62. package/apps/studio/context.ts +16 -0
  63. package/apps/studio/design-setup-readiness.ts +115 -0
  64. package/apps/studio/dist/client.bundle.js +5572 -21
  65. package/apps/studio/dist/comment-mount.js +2 -2
  66. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  67. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  68. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  69. package/apps/studio/dist/styles.css +1 -1
  70. package/apps/studio/dom-selection.ts +25 -0
  71. package/apps/studio/draw/palette.ts +34 -0
  72. package/apps/studio/exporters/jobs.ts +47 -5
  73. package/apps/studio/exporters/video.ts +24 -4
  74. package/apps/studio/footage/schema.test.ts +287 -0
  75. package/apps/studio/footage/schema.ts +662 -0
  76. package/apps/studio/footage-store.ts +235 -0
  77. package/apps/studio/fs-watch.ts +10 -0
  78. package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
  79. package/apps/studio/generation/adapters/gemini.ts +542 -0
  80. package/apps/studio/generation/adapters/groq.test.ts +80 -0
  81. package/apps/studio/generation/adapters/groq.ts +247 -0
  82. package/apps/studio/generation/audio-library.test.ts +95 -0
  83. package/apps/studio/generation/audio-library.ts +156 -0
  84. package/apps/studio/generation/captions.test.ts +78 -0
  85. package/apps/studio/generation/captions.ts +132 -0
  86. package/apps/studio/generation/download.ts +76 -0
  87. package/apps/studio/generation/elevenlabs.test.ts +223 -0
  88. package/apps/studio/generation/gemini.test.ts +350 -0
  89. package/apps/studio/generation/jobs.test.ts +128 -0
  90. package/apps/studio/generation/jobs.ts +243 -0
  91. package/apps/studio/generation/keys.test.ts +75 -0
  92. package/apps/studio/generation/keys.ts +162 -0
  93. package/apps/studio/generation/prefs.test.ts +95 -0
  94. package/apps/studio/generation/prefs.ts +82 -0
  95. package/apps/studio/generation/registry.ts +88 -0
  96. package/apps/studio/generation/types.test.ts +123 -0
  97. package/apps/studio/generation/types.ts +266 -0
  98. package/apps/studio/generation/whisper-models.test.ts +152 -0
  99. package/apps/studio/generation/whisper-models.ts +257 -0
  100. package/apps/studio/generation/whisper-spike-results.md +61 -0
  101. package/apps/studio/git/endpoints.ts +68 -7
  102. package/apps/studio/git/service.ts +199 -21
  103. package/apps/studio/github/service.ts +67 -0
  104. package/apps/studio/handoff.ts +3 -2
  105. package/apps/studio/http.ts +1039 -22
  106. package/apps/studio/input-router.tsx +10 -0
  107. package/apps/studio/inspect.ts +53 -1
  108. package/apps/studio/media/intro.mp4 +0 -0
  109. package/apps/studio/media-commit-chain.ts +119 -0
  110. package/apps/studio/paths.ts +20 -0
  111. package/apps/studio/photo/filters.ts +285 -0
  112. package/apps/studio/photo/pipeline.ts +607 -0
  113. package/apps/studio/photo/schema.ts +367 -0
  114. package/apps/studio/photo-store.ts +154 -0
  115. package/apps/studio/readiness.ts +108 -20
  116. package/apps/studio/runtime-bundle.ts +29 -6
  117. package/apps/studio/scaffold-design.ts +1145 -0
  118. package/apps/studio/server.ts +18 -2
  119. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  120. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  121. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  122. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  123. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  124. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  125. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  126. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  127. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  128. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  129. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  130. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  131. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  132. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  133. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  134. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  135. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  136. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  137. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  138. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  139. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  140. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  141. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  142. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  143. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  144. package/apps/studio/stickers/life-style/best.png +0 -0
  145. package/apps/studio/stickers/life-style/come-on.png +0 -0
  146. package/apps/studio/stickers/life-style/cut.png +0 -0
  147. package/apps/studio/stickers/life-style/detox.png +0 -0
  148. package/apps/studio/stickers/life-style/just.png +0 -0
  149. package/apps/studio/stickers/life-style/manifest.json +68 -0
  150. package/apps/studio/stickers/life-style/mirror.png +0 -0
  151. package/apps/studio/stickers/life-style/nice.png +0 -0
  152. package/apps/studio/stickers/life-style/objects.png +0 -0
  153. package/apps/studio/stickers/life-style/ok.png +0 -0
  154. package/apps/studio/stickers/life-style/queen.png +0 -0
  155. package/apps/studio/stickers/life-style/star.png +0 -0
  156. package/apps/studio/stickers/life-style/sun.png +0 -0
  157. package/apps/studio/stickers/life-style/water.png +0 -0
  158. package/apps/studio/stickers/life-style/yeah.png +0 -0
  159. package/apps/studio/stickers/life-style/you-can.png +0 -0
  160. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  161. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  162. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  163. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  164. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  165. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  166. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  167. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  168. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  169. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  170. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  171. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  172. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  173. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  174. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  175. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  176. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  177. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  178. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  179. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  180. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  181. package/apps/studio/stickers/project-status/group-100.png +0 -0
  182. package/apps/studio/stickers/project-status/group-101.png +0 -0
  183. package/apps/studio/stickers/project-status/group-102.png +0 -0
  184. package/apps/studio/stickers/project-status/group-103.png +0 -0
  185. package/apps/studio/stickers/project-status/group-104.png +0 -0
  186. package/apps/studio/stickers/project-status/group-105.png +0 -0
  187. package/apps/studio/stickers/project-status/group-106.png +0 -0
  188. package/apps/studio/stickers/project-status/group-107.png +0 -0
  189. package/apps/studio/stickers/project-status/group-108.png +0 -0
  190. package/apps/studio/stickers/project-status/group-109.png +0 -0
  191. package/apps/studio/stickers/project-status/group-110.png +0 -0
  192. package/apps/studio/stickers/project-status/group-111.png +0 -0
  193. package/apps/studio/stickers/project-status/group-112.png +0 -0
  194. package/apps/studio/stickers/project-status/group-113.png +0 -0
  195. package/apps/studio/stickers/project-status/group-114.png +0 -0
  196. package/apps/studio/stickers/project-status/group-115.png +0 -0
  197. package/apps/studio/stickers/project-status/group-117.png +0 -0
  198. package/apps/studio/stickers/project-status/group-118.png +0 -0
  199. package/apps/studio/stickers/project-status/group-119.png +0 -0
  200. package/apps/studio/stickers/project-status/group-120.png +0 -0
  201. package/apps/studio/stickers/project-status/group-121.png +0 -0
  202. package/apps/studio/stickers/project-status/group-122.png +0 -0
  203. package/apps/studio/stickers/project-status/group-41.png +0 -0
  204. package/apps/studio/stickers/project-status/group-42.png +0 -0
  205. package/apps/studio/stickers/project-status/group-43.png +0 -0
  206. package/apps/studio/stickers/project-status/group-44.png +0 -0
  207. package/apps/studio/stickers/project-status/group-45.png +0 -0
  208. package/apps/studio/stickers/project-status/group-46.png +0 -0
  209. package/apps/studio/stickers/project-status/group-47.png +0 -0
  210. package/apps/studio/stickers/project-status/group-48.png +0 -0
  211. package/apps/studio/stickers/project-status/group-49.png +0 -0
  212. package/apps/studio/stickers/project-status/group-50.png +0 -0
  213. package/apps/studio/stickers/project-status/group-51.png +0 -0
  214. package/apps/studio/stickers/project-status/group-52.png +0 -0
  215. package/apps/studio/stickers/project-status/group-53.png +0 -0
  216. package/apps/studio/stickers/project-status/group-54.png +0 -0
  217. package/apps/studio/stickers/project-status/group-55.png +0 -0
  218. package/apps/studio/stickers/project-status/group-56.png +0 -0
  219. package/apps/studio/stickers/project-status/group-57.png +0 -0
  220. package/apps/studio/stickers/project-status/group-58.png +0 -0
  221. package/apps/studio/stickers/project-status/group-59.png +0 -0
  222. package/apps/studio/stickers/project-status/group-60.png +0 -0
  223. package/apps/studio/stickers/project-status/group-61.png +0 -0
  224. package/apps/studio/stickers/project-status/group-62.png +0 -0
  225. package/apps/studio/stickers/project-status/group-63.png +0 -0
  226. package/apps/studio/stickers/project-status/group-64.png +0 -0
  227. package/apps/studio/stickers/project-status/group-65.png +0 -0
  228. package/apps/studio/stickers/project-status/group-66.png +0 -0
  229. package/apps/studio/stickers/project-status/group-67.png +0 -0
  230. package/apps/studio/stickers/project-status/group-68.png +0 -0
  231. package/apps/studio/stickers/project-status/group-69.png +0 -0
  232. package/apps/studio/stickers/project-status/group-70.png +0 -0
  233. package/apps/studio/stickers/project-status/group-71.png +0 -0
  234. package/apps/studio/stickers/project-status/group-72.png +0 -0
  235. package/apps/studio/stickers/project-status/group-73.png +0 -0
  236. package/apps/studio/stickers/project-status/group-74.png +0 -0
  237. package/apps/studio/stickers/project-status/group-75.png +0 -0
  238. package/apps/studio/stickers/project-status/group-76.png +0 -0
  239. package/apps/studio/stickers/project-status/group-77.png +0 -0
  240. package/apps/studio/stickers/project-status/group-78.png +0 -0
  241. package/apps/studio/stickers/project-status/group-79.png +0 -0
  242. package/apps/studio/stickers/project-status/group-80.png +0 -0
  243. package/apps/studio/stickers/project-status/group-81.png +0 -0
  244. package/apps/studio/stickers/project-status/group-82.png +0 -0
  245. package/apps/studio/stickers/project-status/group-83.png +0 -0
  246. package/apps/studio/stickers/project-status/group-84.png +0 -0
  247. package/apps/studio/stickers/project-status/group-85.png +0 -0
  248. package/apps/studio/stickers/project-status/group-86.png +0 -0
  249. package/apps/studio/stickers/project-status/group-87.png +0 -0
  250. package/apps/studio/stickers/project-status/group-88.png +0 -0
  251. package/apps/studio/stickers/project-status/group-89.png +0 -0
  252. package/apps/studio/stickers/project-status/group-90.png +0 -0
  253. package/apps/studio/stickers/project-status/group-91.png +0 -0
  254. package/apps/studio/stickers/project-status/group-92.png +0 -0
  255. package/apps/studio/stickers/project-status/group-93.png +0 -0
  256. package/apps/studio/stickers/project-status/group-94.png +0 -0
  257. package/apps/studio/stickers/project-status/group-96.png +0 -0
  258. package/apps/studio/stickers/project-status/group-97.png +0 -0
  259. package/apps/studio/stickers/project-status/group-98.png +0 -0
  260. package/apps/studio/stickers/project-status/group-99.png +0 -0
  261. package/apps/studio/stickers/project-status/manifest.json +328 -0
  262. package/apps/studio/test/_helpers.ts +15 -2
  263. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  264. package/apps/studio/test/acp-bridge.test.ts +34 -1
  265. package/apps/studio/test/acp-commands.test.ts +2 -1
  266. package/apps/studio/test/acp-env.test.ts +30 -0
  267. package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
  268. package/apps/studio/test/acp-session-plugins.test.ts +72 -1
  269. package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
  270. package/apps/studio/test/annotations-layer.test.ts +174 -0
  271. package/apps/studio/test/asset-api.test.ts +3 -3
  272. package/apps/studio/test/canvas-edit.test.ts +17 -6
  273. package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
  274. package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
  275. package/apps/studio/test/csrf-write-guard.test.ts +23 -1
  276. package/apps/studio/test/design-setup-readiness.test.ts +155 -0
  277. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  278. package/apps/studio/test/edit-source-command.test.ts +23 -0
  279. package/apps/studio/test/element-structural-edit.test.ts +154 -0
  280. package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
  281. package/apps/studio/test/footage-store.test.ts +100 -0
  282. package/apps/studio/test/generate-route.test.ts +106 -0
  283. package/apps/studio/test/git-branches.test.ts +97 -0
  284. package/apps/studio/test/github-api.test.ts +56 -1
  285. package/apps/studio/test/import-asset-browser.test.ts +64 -0
  286. package/apps/studio/test/import-asset-route.test.ts +71 -0
  287. package/apps/studio/test/import-asset.test.ts +399 -0
  288. package/apps/studio/test/import-brand-browser.test.ts +88 -0
  289. package/apps/studio/test/import-brand-route.test.ts +120 -0
  290. package/apps/studio/test/import-brand.test.ts +206 -0
  291. package/apps/studio/test/import-tokens.test.ts +722 -0
  292. package/apps/studio/test/input-router.test.ts +33 -0
  293. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  294. package/apps/studio/test/media-commit-chain.test.ts +210 -0
  295. package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
  296. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  297. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  298. package/apps/studio/test/photo-filters.test.ts +149 -0
  299. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  300. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  301. package/apps/studio/test/read-annotations.test.ts +164 -0
  302. package/apps/studio/test/readiness.test.ts +13 -11
  303. package/apps/studio/test/scaffold-design.test.ts +122 -0
  304. package/apps/studio/test/shell-importmap.test.ts +49 -0
  305. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  306. package/apps/studio/test/timeline-parse.test.ts +57 -0
  307. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  308. package/apps/studio/test/tour-overlay.test.tsx +16 -0
  309. package/apps/studio/test/undo-stack.test.ts +33 -0
  310. package/apps/studio/test/video-asset.test.ts +5 -5
  311. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  312. package/apps/studio/text-caret.ts +239 -0
  313. package/apps/studio/tool-palette.tsx +49 -21
  314. package/apps/studio/ui-prefs.ts +130 -0
  315. package/apps/studio/undo-stack.ts +94 -2
  316. package/apps/studio/use-annotation-resize.tsx +4 -4
  317. package/apps/studio/use-canvas-media-drop.tsx +40 -1
  318. package/apps/studio/use-chrome-visibility.tsx +8 -2
  319. package/apps/studio/use-selection-set.tsx +21 -0
  320. package/apps/studio/video-comp.tsx +48 -7
  321. package/apps/studio/whats-new.json +149 -0
  322. package/apps/studio/ws.ts +5 -0
  323. package/cli/bin/maude.mjs +6 -6
  324. package/cli/commands/design.mjs +67 -3
  325. package/cli/lib/gitignore-block.mjs +2 -0
  326. package/cli/lib/pkg-root.mjs +107 -0
  327. package/cli/lib/pkg-root.test.mjs +79 -0
  328. package/cli/lib/reconstruct-toolset.test.mjs +194 -0
  329. package/cli/lib/update-check.mjs +8 -0
  330. package/package.json +13 -11
  331. package/plugins/design/dependencies.json +18 -0
  332. package/plugins/design/templates/_shell.html +1 -0
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env bash
2
+ # photo-adjust.sh — feature-photo-editor (Stage G, Task 19). Headless PARAMETRIC
3
+ # photo edit: assemble a PhotoEdit from flags and PUT it to the running dev
4
+ # server's /_api/photo-edit route so an agent (or a human) can tune a photo
5
+ # without opening a browser or clicking a slider.
6
+ #
7
+ # Deliberately the THIN, NON-BROWSER sibling of photo-bg-remove.sh. Background
8
+ # removal needs a browser round-trip because @imgly runs the ML model client-
9
+ # side; parametric edits are pure JSON, so a direct curl to the already-validated
10
+ # route (the photo-store cap stack — validatePhotoEdit + sha8 + containment +
11
+ # size cap — is the gate) is the whole job. DO NOT "fix" this asymmetry by making
12
+ # this verb heavier: there is no inference step here, so a harness canvas +
13
+ # agent-browser would be pure overhead.
14
+ #
15
+ # Reached via `maude design photo-adjust` (never a raw bin path — DDR-062).
16
+ #
17
+ # Usage:
18
+ # photo-adjust.sh --asset <assets/<sha8>.<ext> | <sha8>> [--root <repo>]
19
+ # [--brightness N] [--contrast N] [--saturation N] [--exposure N]
20
+ # [--hue N] [--sepia N] [--grayscale N] [--invert N]
21
+ # [--duotone "#aabbcc,#ddeeff[,intensity]"]
22
+ # [--grain "amount[,size]"]
23
+ # [--pattern "type[,scale,opacity,blend]"]
24
+ # [--mask "preset[,strength]"]
25
+ # [--replace] (overwrite the sidecar instead of merging onto it)
26
+ # [--reset] (clear the sidecar to neutral / unedited)
27
+ #
28
+ # Adjustment ranges (normalized, 0 = neutral): brightness/contrast/saturation/
29
+ # exposure ∈ −1..1, hue ∈ −180..180 (deg), sepia/grayscale/invert ∈ 0..1.
30
+ # Merges onto the existing sidecar by default (successive calls accumulate).
31
+ #
32
+ # Requires a running dev server (caller runs `maude design server-up` first);
33
+ # reads the port from <designRoot>/_server.json.
34
+ #
35
+ # Stdout (last line): the sidecar path `assets/<sha8>.photo.json` (for $(...)).
36
+ # Stderr: progress / diagnostics.
37
+ # Exit: 0 ok / 1 server or asset problem / 2 bad args / 3 server rejected edit.
38
+
39
+ set -euo pipefail
40
+
41
+ ASSET="" REPO="" REPLACE=0 RESET=0
42
+ BRIGHTNESS="" CONTRAST="" SATURATION="" EXPOSURE="" HUE="" SEPIA="" GRAYSCALE="" INVERT=""
43
+ DUOTONE="" GRAIN="" PATTERN="" MASK=""
44
+
45
+ while [ $# -gt 0 ]; do
46
+ case "$1" in
47
+ --asset) ASSET="$2"; shift 2 ;;
48
+ --root) REPO="$2"; shift 2 ;;
49
+ --brightness) BRIGHTNESS="$2"; shift 2 ;;
50
+ --contrast) CONTRAST="$2"; shift 2 ;;
51
+ --saturation) SATURATION="$2"; shift 2 ;;
52
+ --exposure) EXPOSURE="$2"; shift 2 ;;
53
+ --hue) HUE="$2"; shift 2 ;;
54
+ --sepia) SEPIA="$2"; shift 2 ;;
55
+ --grayscale) GRAYSCALE="$2"; shift 2 ;;
56
+ --invert) INVERT="$2"; shift 2 ;;
57
+ --duotone) DUOTONE="$2"; shift 2 ;;
58
+ --grain) GRAIN="$2"; shift 2 ;;
59
+ --pattern) PATTERN="$2"; shift 2 ;;
60
+ --mask) MASK="$2"; shift 2 ;;
61
+ --replace) REPLACE=1; shift ;;
62
+ --reset) RESET=1; shift ;;
63
+ --help|-h) sed -n '2,39p' "$0" | sed 's/^# \?//'; exit 0 ;;
64
+ *) echo "photo-adjust.sh: unknown arg '$1'" >&2; exit 2 ;;
65
+ esac
66
+ done
67
+
68
+ [ -n "$ASSET" ] || { echo "photo-adjust.sh: --asset is required" >&2; exit 2; }
69
+ command -v jq >/dev/null 2>&1 || { echo "photo-adjust.sh: jq is required" >&2; exit 1; }
70
+
71
+ if [ -z "$REPO" ]; then
72
+ REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
73
+ fi
74
+ DESIGN_ROOT="$REPO/.design"
75
+ [ -d "$DESIGN_ROOT" ] || { echo "photo-adjust.sh: no .design/ under $REPO" >&2; exit 1; }
76
+
77
+ STATE="$DESIGN_ROOT/_server.json"
78
+ [ -f "$STATE" ] || { echo "photo-adjust.sh: no _server.json (start the dev server first)" >&2; exit 1; }
79
+ PORT=$(jq -r '.port // empty' "$STATE" 2>/dev/null)
80
+ [ -n "$PORT" ] || { echo "photo-adjust.sh: could not read port from $STATE" >&2; exit 1; }
81
+
82
+ BASE_URL="http://127.0.0.1:$PORT/_api/photo-edit"
83
+ # URL-encode the asset param minimally (slashes are safe as a query value here).
84
+ ENC_ASSET=$(printf '%s' "$ASSET" | jq -sRr @uri)
85
+ URL="$BASE_URL?asset=$ENC_ASSET"
86
+
87
+ # --reset short-circuits: write a neutral sidecar.
88
+ if [ "$RESET" -eq 1 ]; then
89
+ RESP=$(curl -s -X PUT -H 'Content-Type: application/json' -d '{}' "$URL")
90
+ OK=$(printf '%s' "$RESP" | jq -r '.ok // false' 2>/dev/null || echo false)
91
+ [ "$OK" = "true" ] || { echo "photo-adjust.sh: reset rejected: $RESP" >&2; exit 3; }
92
+ printf '%s\n' "$(printf '%s' "$RESP" | jq -r '.path')"
93
+ exit 0
94
+ fi
95
+
96
+ # Assemble the patch from set flags (unset flags are omitted entirely).
97
+ PATCH=$(jq -n \
98
+ --arg brightness "$BRIGHTNESS" --arg contrast "$CONTRAST" --arg saturation "$SATURATION" \
99
+ --arg exposure "$EXPOSURE" --arg hue "$HUE" --arg sepia "$SEPIA" \
100
+ --arg grayscale "$GRAYSCALE" --arg invert "$INVERT" \
101
+ --arg duotone "$DUOTONE" --arg grain "$GRAIN" --arg pattern "$PATTERN" --arg mask "$MASK" '
102
+ def n($s): if $s == "" then null else ($s | tonumber) end;
103
+ def adj:
104
+ { brightness: n($brightness), contrast: n($contrast), saturation: n($saturation),
105
+ exposure: n($exposure), hue: n($hue), sepia: n($sepia),
106
+ grayscale: n($grayscale), invert: n($invert) }
107
+ | with_entries(select(.value != null));
108
+ def duo:
109
+ if $duotone == "" then null
110
+ else ($duotone | split(",")) as $p
111
+ | { enabled: true, colorA: $p[0], colorB: $p[1] }
112
+ + (if ($p | length) > 2 then { intensity: ($p[2] | tonumber) } else {} end)
113
+ end;
114
+ def gr:
115
+ if $grain == "" then null
116
+ else ($grain | split(",")) as $p
117
+ | { enabled: true, amount: ($p[0] | tonumber) }
118
+ + (if ($p | length) > 1 then { size: ($p[1] | tonumber) } else {} end)
119
+ end;
120
+ def pat:
121
+ if $pattern == "" then null
122
+ else ($pattern | split(",")) as $p
123
+ | { enabled: true, type: $p[0] }
124
+ + (if ($p | length) > 1 then { scale: ($p[1] | tonumber) } else {} end)
125
+ + (if ($p | length) > 2 then { opacity: ($p[2] | tonumber) } else {} end)
126
+ + (if ($p | length) > 3 then { blend: $p[3] } else {} end)
127
+ end;
128
+ def msk:
129
+ if $mask == "" then null
130
+ else ($mask | split(",")) as $p
131
+ | { preset: $p[0] }
132
+ + (if ($p | length) > 1 then { strength: ($p[1] | tonumber) } else {} end)
133
+ end;
134
+ ({}
135
+ + (adj as $a | if ($a | length) > 0 then { adjustments: $a } else {} end)
136
+ + (duo as $d | if $d then { duotone: $d } else {} end)
137
+ + (gr as $g | if $g then { grain: $g } else {} end)
138
+ + (pat as $p | if $p then { pattern: $p } else {} end)
139
+ + (msk as $m | if $m then { mask: $m } else {} end))
140
+ ')
141
+
142
+ if [ "$(printf '%s' "$PATCH" | jq -r 'keys | length')" = "0" ]; then
143
+ echo "photo-adjust.sh: no edit flags given (nothing to do)" >&2
144
+ exit 2
145
+ fi
146
+
147
+ if [ "$REPLACE" -eq 1 ]; then
148
+ BODY="$PATCH"
149
+ else
150
+ # Merge onto the current sidecar (deep object merge via jq's `*`), dropping the
151
+ # server-stamped `version` (it's re-stamped on write).
152
+ BASE=$(curl -s "$URL" 2>/dev/null || echo '{}')
153
+ printf '%s' "$BASE" | jq -e . >/dev/null 2>&1 || BASE='{}'
154
+ BODY=$(jq -n --argjson base "$BASE" --argjson patch "$PATCH" '($base | del(.version)) * $patch')
155
+ fi
156
+
157
+ RESP=$(curl -s -X PUT -H 'Content-Type: application/json' -d "$BODY" "$URL")
158
+ OK=$(printf '%s' "$RESP" | jq -r '.ok // false' 2>/dev/null || echo false)
159
+ if [ "$OK" != "true" ]; then
160
+ echo "photo-adjust.sh: server rejected the edit: $RESP" >&2
161
+ exit 3
162
+ fi
163
+ printf '%s\n' "$(printf '%s' "$RESP" | jq -r '.path')"
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env bash
2
+ # photo-bg-remove.sh — feature-photo-editor (Stage G, Task 18). Headless
3
+ # background removal: mirrors draw-proof.sh's structure (throwaway harness
4
+ # canvas + agent-browser drive) but the "proof" here isn't a screenshot ladder
5
+ # — it's the client-side @imgly ML pass actually running and reporting back
6
+ # through DOM attributes agent-browser polls, since there's no other channel
7
+ # to cross the process boundary out of a headless browser.
8
+ #
9
+ # Design note (the decision this task was originally deferred on): the harness
10
+ # canvas runs inside the split-origin (DDR-054) canvas iframe, but BOTH
11
+ # `/_api/asset` and `/_api/photo-edit` are already canvas-safe routes (see
12
+ # their CANVAS_SAFE_API comments in http.ts) — so the harness component
13
+ # (`PhotoBgRemoveHarness`, canvas-lib.tsx) posts the matte and the updated
14
+ # PhotoEdit sidecar directly from the canvas origin. No cross-origin relay,
15
+ # no base64-through-a-DOM-attribute round-trip needed.
16
+ #
17
+ # Reached via `maude design photo-bg-remove` (never a raw bin path — DDR-062).
18
+ #
19
+ # Security hardening (fix-photo-editor-followup-debt, Stage A): `--asset` is
20
+ # validated against the strict `assets/<sha8>.<ext>` shape (mirrors
21
+ # photo-store.ts's SHA_RE) rather than a loose glob — a permissive check
22
+ # previously let a crafted `--asset` value splice unescaped content into the
23
+ # generated harness `.tsx`. The harness-file write is symlink-safe (write to a
24
+ # freshly O_EXCL-created tmp path, then atomic `mv` into place — `mv`/rename(2)
25
+ # replaces a destination symlink's OWN dirent rather than following it, so even
26
+ # a pre-planted symlink at the harness path can't redirect the write into a
27
+ # real reviewed canvas file). The stdout deliverable is shape-validated before
28
+ # print. `--slug` failure hard-fails instead of downgrading to an unsanitized
29
+ # fallback. Concurrent invocations against the SAME asset serialize via a
30
+ # `mkdir`-based lock (portable — `flock` isn't available on macOS by default).
31
+ #
32
+ # Usage:
33
+ # photo-bg-remove.sh --asset <assets/<sha8>.<ext>> [--root <repo>]
34
+ # [--slug <name>] [--timeout <secs>]
35
+ #
36
+ # Requires a running dev server (caller runs `maude design server-up` first);
37
+ # reads the port from <designRoot>/_server.json. Requires agent-browser (the
38
+ # ML pass needs a real WASM/WebGPU-capable browser driving real DOM state —
39
+ # there is no non-browser path, unlike photo-adjust.sh's plain curl).
40
+ #
41
+ # Stdout (last line): the new matte asset path `assets/<sha8>.png` (for $(...)).
42
+ # Stderr: progress / diagnostics.
43
+ # Exit: 0 success / 1 missing input, server, or agent-browser / 2 bad args
44
+ # / 3 drive/navigation failed / 4 the harness itself reported an error
45
+ # (or produced a malformed result) / 5 timed out waiting for the ML
46
+ # pass to finish / 6 another run is already in progress for this asset
47
+
48
+ set -uo pipefail
49
+
50
+ ASSET="" SLUG="" REPO="" TIMEOUT=150
51
+ # Mirrors photo-store.ts's SHA_RE shape: lowercase hex sha8 (8-64 chars),
52
+ # case-insensitive extension. Shared between --asset validation (Task 1) and
53
+ # the stdout-deliverable validation (Task 3) so the two checks can't drift.
54
+ ASSET_SHAPE_RE='^assets/[0-9a-f]{8,64}\.[a-zA-Z0-9]+$'
55
+
56
+ while [ $# -gt 0 ]; do
57
+ case "$1" in
58
+ --asset) ASSET="$2"; shift 2 ;;
59
+ --slug) SLUG="$2"; shift 2 ;;
60
+ --root) REPO="$2"; shift 2 ;;
61
+ --timeout) TIMEOUT="$2"; shift 2 ;;
62
+ --help|-h)
63
+ sed -n '2,39p' "$0" | sed 's/^# \?//'
64
+ exit 0
65
+ ;;
66
+ *)
67
+ echo "photo-bg-remove.sh: unknown arg '$1' (try --help)" >&2
68
+ exit 2
69
+ ;;
70
+ esac
71
+ done
72
+
73
+ [ -n "$ASSET" ] || { echo "photo-bg-remove.sh: --asset <assets/<sha8>.<ext>> required" >&2; exit 2; }
74
+ # Strip a leading designRoot-relative slash/prefix if the caller passed one.
75
+ ASSET="${ASSET#/}"
76
+ # Strict shape check (Task 1) — replaces the old `case "$ASSET" in assets/*)`
77
+ # glob, which only checked the PREFIX and let anything after it (quotes,
78
+ # backticks, `../`, JSX-breaking sequences) ride unescaped into the generated
79
+ # harness .tsx below. `[[ =~ ]]` against a character-class regex is the bash
80
+ # equivalent of photo-store.ts's `SHA_RE` hex check — `..`, `/`, quotes, and
81
+ # any non-hex/non-alnum content can never match, so nothing but a clean
82
+ # `assets/<sha8>.<ext>` reference survives validation. Captures the bare sha8
83
+ # for reuse as a filesystem-safe lock key (Task 6).
84
+ if [[ "$ASSET" =~ ^assets/([0-9a-f]{8,64})\.[a-zA-Z0-9]+$ ]]; then
85
+ ASSET_KEY="${BASH_REMATCH[1]}"
86
+ else
87
+ echo "photo-bg-remove.sh: --asset must look like assets/<sha8>.<ext> (lowercase hex, 8-64 chars), got '$ASSET'" >&2
88
+ exit 2
89
+ fi
90
+
91
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
92
+ [ -n "$SLUG" ] || SLUG=$(basename "$ASSET" | sed -E 's/\.[^.]+$//')
93
+ # Hard-fail (Task 4) instead of downgrading to a lowercased-but-unstripped
94
+ # fallback on slug.sh failure — a safety subprocess's failure must never
95
+ # silently continue with a less-safe code path. This repo has documented
96
+ # history (DDR-045) of sibling-script resolution breaking specifically in
97
+ # packaged/compiled distributions, so this failure mode is realistic.
98
+ # (SLUG_INPUT is captured separately so a failed substitution — which still
99
+ # assigns its, here empty, stdout to $SLUG — doesn't blank the diagnostic.)
100
+ SLUG_INPUT="$SLUG"
101
+ if ! SLUG="$(bash "$SCRIPT_DIR/slug.sh" "$SLUG_INPUT" 2>/dev/null)"; then
102
+ echo "photo-bg-remove.sh: slug.sh failed to normalize slug '$SLUG_INPUT' — refusing to fall back to an unsanitized value" >&2
103
+ exit 1
104
+ fi
105
+ # Assigned this early (not just before the agent-browser open call below) so
106
+ # it's always bound by the time `trap cleanup EXIT` is installed — a security
107
+ # review of this plan found that `cleanup()` unconditionally reads $SESSION,
108
+ # and under `set -u` an exit on one of the harness-write failure paths (before
109
+ # SESSION used to be assigned) would abort mid-cleanup on the unbound
110
+ # variable, skipping the `rmdir "$LOCK_DIR"` below it and leaking the lock.
111
+ SESSION="photo-bgremove-$SLUG-$$"
112
+
113
+ # ---------- resolve repo + design root + port ----------
114
+ if [ -z "$REPO" ]; then
115
+ REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
116
+ fi
117
+ DESIGN_ROOT="$REPO/.design"
118
+ [ -d "$DESIGN_ROOT" ] || { echo "photo-bg-remove.sh: no .design/ under $REPO" >&2; exit 1; }
119
+ [ -f "$DESIGN_ROOT/$ASSET" ] || { echo "photo-bg-remove.sh: asset not found: $DESIGN_ROOT/$ASSET" >&2; exit 1; }
120
+
121
+ STATE="$DESIGN_ROOT/_server.json"
122
+ [ -f "$STATE" ] || { echo "photo-bg-remove.sh: $STATE missing — run \`maude design server-up\` first" >&2; exit 1; }
123
+ if command -v jq >/dev/null 2>&1; then
124
+ PORT=$(jq -r .port "$STATE" 2>/dev/null)
125
+ else
126
+ PORT=$(sed -nE 's/.*"port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
127
+ fi
128
+ [ -n "$PORT" ] || { echo "photo-bg-remove.sh: no port in $STATE" >&2; exit 1; }
129
+
130
+ # ---------- resolve agent-browser (hard dependency — see header) ----------
131
+ # Mirrors screenshot.sh's MAUDE_AGENT_BROWSER override (DDR-144 attacker-F4 — an
132
+ # explicit single-binary pointer, never a PATH prepend a same-user attacker
133
+ # could shadow) and its ensure-browser.sh chrome resolution.
134
+ AB="${MAUDE_AGENT_BROWSER:-agent-browser}"
135
+ command -v "$AB" >/dev/null 2>&1 || {
136
+ echo "photo-bg-remove.sh: agent-browser not found (required — the ML pass needs a real WASM/WebGPU browser, there is no non-browser fallback here)" >&2
137
+ exit 1
138
+ }
139
+ if [ -z "${AGENT_BROWSER_EXECUTABLE_PATH:-}" ]; then
140
+ ES_FLAGS="--quiet"
141
+ [ -z "${MAUDE_DEV_SERVER_ROOT:-}" ] && ES_FLAGS="--quiet --no-download"
142
+ BROWSER_PATH="$(bash "$SCRIPT_DIR/ensure-browser.sh" $ES_FLAGS 2>/dev/null)"
143
+ [ -n "$BROWSER_PATH" ] && export AGENT_BROWSER_EXECUTABLE_PATH="$BROWSER_PATH"
144
+ fi
145
+
146
+ # ---------- per-asset concurrency guard (Task 6) ----------
147
+ # `mkdir` is atomic on every POSIX filesystem and needs no extra binary —
148
+ # `flock` isn't available on macOS by default, so this is the portable choice.
149
+ # Keyed on the asset's sha8 (already validated hex — safe as a path segment),
150
+ # NOT global, so two different assets still run concurrently. Fail-fast rather
151
+ # than queue/wait: a full job queue is out of scope for this backstop, whose
152
+ # only job is refusing to let a runaway loop spawn unbounded concurrent
153
+ # browser+WASM processes against the same asset.
154
+ PHOTO_DIR="$DESIGN_ROOT/_photo"
155
+ mkdir -p "$PHOTO_DIR"
156
+ LOCK_DIR="$PHOTO_DIR/.lock.$ASSET_KEY"
157
+ TMP_HARNESS=""
158
+ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
159
+ echo "photo-bg-remove.sh: another photo-bg-remove run is already in progress for asset '$ASSET' — refusing to run concurrently against the same asset (lock: ${LOCK_DIR#$REPO/})" >&2
160
+ exit 6
161
+ fi
162
+
163
+ cleanup() {
164
+ "$AB" --session "$SESSION" close >/dev/null 2>&1 || true
165
+ [ -n "$TMP_HARNESS" ] && rm -f "$TMP_HARNESS" 2>/dev/null
166
+ rmdir "$LOCK_DIR" 2>/dev/null || true
167
+ }
168
+ trap cleanup EXIT
169
+
170
+ # ---------- generate the harness canvas (symlink-safe — Task 2) ----------
171
+ # Write to a freshly O_CREAT|O_EXCL-created tmp path in the SAME directory
172
+ # (same filesystem, so the rename below is atomic), then `mv` into place.
173
+ # `mv`/rename(2) replaces the destination NAME, never dereferences it — so
174
+ # even a pre-planted symlink at $HARNESS_TSX gets its dirent swapped out
175
+ # rather than followed, unlike the bare `{ ... } > "$HARNESS_TSX"` truncating
176
+ # redirect this replaces (which WOULD follow a symlink and could splice
177
+ # attacker-controlled JSX into whatever real file it pointed at). `set -C`
178
+ # (noclobber) makes bash open the tmp path with O_EXCL, so a pre-existing tmp
179
+ # path (including a dangling symlink) fails the write instead of following it
180
+ # — the bash equivalent of sync/atomic-write.ts's Node `wx` flag (DDR-054 §2c).
181
+ HARNESS_TSX="$PHOTO_DIR/$SLUG.bgremove.tsx"
182
+ TMP_HARNESS="$PHOTO_DIR/.$SLUG.bgremove.tsx.tmp.$$.$RANDOM$RANDOM"
183
+ if ! (
184
+ set -C
185
+ {
186
+ echo "import { PhotoBgRemoveHarness } from '@maude/canvas-lib';"
187
+ echo
188
+ echo "// AUTO-GENERATED by \`maude design photo-bg-remove\` — headless bg-removal proof"
189
+ echo "// harness. Lives under _photo/ (gitignored); safe to delete + regenerate."
190
+ echo "export default function PhotoBgRemoveCanvas() {"
191
+ printf ' return <PhotoBgRemoveHarness source=%s />;\n' "\"$ASSET\""
192
+ echo "}"
193
+ } > "$TMP_HARNESS"
194
+ ); then
195
+ echo "photo-bg-remove.sh: failed to create harness scratch file (possible pre-existing path at $TMP_HARNESS)" >&2
196
+ exit 1
197
+ fi
198
+ if ! mv "$TMP_HARNESS" "$HARNESS_TSX"; then
199
+ echo "photo-bg-remove.sh: atomic rename of harness file failed" >&2
200
+ exit 1
201
+ fi
202
+ TMP_HARNESS="" # renamed away — nothing left for cleanup() to remove
203
+ echo "→ harness canvas: ${HARNESS_TSX#$REPO/}" >&2
204
+
205
+ REL="_photo/$SLUG.bgremove.tsx"
206
+ URL="http://localhost:$PORT/_canvas-shell.html?canvas=$REL"
207
+ SEL="[data-photo-bgremove-status]"
208
+
209
+ echo "→ driving $URL (session $SESSION)" >&2
210
+ if ! "$AB" --session "$SESSION" open "$URL" >&2; then
211
+ echo "photo-bg-remove.sh: agent-browser failed to open the harness canvas" >&2
212
+ exit 3
213
+ fi
214
+
215
+ # ---------- poll the harness's status attribute ----------
216
+ # The ML pass includes a first-use ~40 MB model-weight fetch (Task 11/12) —
217
+ # this can dominate the wait far more than inference itself, hence the
218
+ # generous default timeout.
219
+ ELAPSED=0
220
+ STATUS="pending"
221
+ while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
222
+ STATUS="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-status 2>/dev/null | tr -d '"')"
223
+ case "$STATUS" in
224
+ done|error) break ;;
225
+ esac
226
+ sleep 2
227
+ ELAPSED=$((ELAPSED + 2))
228
+ done
229
+
230
+ if [ "$STATUS" = "error" ]; then
231
+ ERR="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-error 2>/dev/null | tr -d '"')"
232
+ echo "photo-bg-remove.sh: harness reported an error: ${ERR:-unknown}" >&2
233
+ exit 4
234
+ fi
235
+
236
+ if [ "$STATUS" != "done" ]; then
237
+ echo "photo-bg-remove.sh: timed out after ${TIMEOUT}s waiting for the ML pass (still '$STATUS')" >&2
238
+ exit 5
239
+ fi
240
+
241
+ RESULT="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-result 2>/dev/null | tr -d '"')"
242
+ [ -n "$RESULT" ] || { echo "photo-bg-remove.sh: harness reported done but no result asset" >&2; exit 4; }
243
+ # Shape-validate the deliverable (Task 3) before it's ever trusted as far as
244
+ # stdout — the harness runs inside the (untrusted, DDR-054) canvas origin, so
245
+ # a malformed/crafted `data-photo-bgremove-result` value is treated exactly
246
+ # like the empty-result case above rather than printed verbatim.
247
+ if [[ ! "$RESULT" =~ $ASSET_SHAPE_RE ]]; then
248
+ echo "photo-bg-remove.sh: harness reported a malformed result asset '$RESULT'" >&2
249
+ exit 4
250
+ fi
251
+
252
+ echo "→ background removed → $RESULT" >&2
253
+ # Last stdout line = the new matte asset path, for $(maude design photo-bg-remove ...) capture.
254
+ printf '%s\n' "$RESULT"
255
+
256
+ # Clean up the harness scratch file on the SUCCESS path only (Task 5) — left
257
+ # in place on any error exit above, where it's useful for debugging.
258
+ rm -f "$HARNESS_TSX"
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bash
2
+ # probe-footage.sh — thin shim over _probe-footage-playwright.mjs; reached via
3
+ # `maude design probe-footage` (DDR-062), never a raw bin path. Extracts keyframe
4
+ # PNGs from one clip in headless Chromium for the footage-analyst's vision pass
5
+ # (feature-footage-analysis-director, Task 4). See the .mjs for the rationale.
6
+ #
7
+ # Usage:
8
+ # probe-footage.sh <assets/<sha8>.<ext>> --root <repo> [--design-root .design]
9
+ # [--frames N] [--out-dir DIR]
10
+ #
11
+ # stdout on success = a JSON manifest { asset, durationSec, width, height, frames }.
12
+ # Exit: 0 ok · 2 usage · 3 no browser · 4 decode/probe error · 1 other.
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+
15
+ case "$1" in
16
+ --help|-h) sed -n '2,14p' "$0" | sed 's/^# \?//'; exit 0 ;;
17
+ esac
18
+
19
+ # The probe imports `playwright` (Chromium). Prefer node (the runtime the other
20
+ # playwright shims run under); fall back to bun in a dev tree.
21
+ if command -v node >/dev/null 2>&1; then
22
+ exec node "$SCRIPT_DIR/_probe-footage-playwright.mjs" "$@"
23
+ elif command -v bun >/dev/null 2>&1; then
24
+ exec bun run "$SCRIPT_DIR/_probe-footage-playwright.mjs" "$@"
25
+ else
26
+ echo "probe-footage.sh: node or bun is required." >&2
27
+ exit 1
28
+ fi
@@ -105,8 +105,15 @@ Args:
105
105
  --json No-op (JSON is the only output form).
106
106
 
107
107
  FigJam v3 additive fields per annotation: z (render order), groupIds (deepest →
108
- shallowest), author ("ai" for annotate-verb strokes), and from/to host ids on
109
- bound arrows.
108
+ shallowest), author ("ai" for annotate-verb strokes), authorName/authorId
109
+ (human-drawn stickies), and from/to host ids on bound arrows.
110
+
111
+ Section annotations additionally carry members: an array of every OTHER
112
+ annotation whose center falls inside the section's rect, in SPATIAL reading
113
+ order (top-to-bottom, left-to-right — not paint order), each as
114
+ { id, tool, order, x, y, w, h, href? }. Answers "what's in this section, and
115
+ in what order" for prompts like "make a video / Instagram carousel from
116
+ this section" without hand-computing containment.
110
117
 
111
118
  Emits JSON to stdout. A missing annotation file emits [] (exit 0).`;
112
119
 
@@ -395,6 +402,31 @@ function readUnknown(tool, attrs, inner) {
395
402
  // provenance, render order (z = document position). Applied uniformly after
396
403
  // the per-tool extraction; arrows additionally surface bound endpoints as
397
404
  // `from`/`to` host ids so a bound diagram reads back as a GRAPH.
405
+ const AUTHOR_NAME_MAX_LEN = 64;
406
+
407
+ // Phase 3 (whiteboard-improvements) — mirrors annotations-model.ts's
408
+ // sanitizeAuthorName (same code-point ranges: control chars, bidi
409
+ // overrides/isolates, zero-width chars, BOM) so a peer-synced
410
+ // `.annotations.svg`'s authorName/authorId reads identically through
411
+ // either surface. Every string this CLI returns is already treated as DATA
412
+ // by the whiteboard skill's trust model — this additionally strips the
413
+ // characters that could visually spoof an author name.
414
+ function sanitizeAuthorName(raw) {
415
+ let cleaned = '';
416
+ for (const ch of raw) {
417
+ const cp = ch.codePointAt(0) ?? 0;
418
+ const unsafe =
419
+ cp <= 0x1f ||
420
+ (cp >= 0x7f && cp <= 0x9f) ||
421
+ (cp >= 0x200b && cp <= 0x200f) ||
422
+ (cp >= 0x202a && cp <= 0x202e) ||
423
+ (cp >= 0x2066 && cp <= 0x2069) ||
424
+ cp === 0xfeff;
425
+ if (!unsafe) cleaned += ch;
426
+ }
427
+ return cleaned.trim().slice(0, AUTHOR_NAME_MAX_LEN);
428
+ }
429
+
398
430
  function withShared(o, attrs, z) {
399
431
  o.z = z;
400
432
  const g = attr(attrs, 'data-group-ids');
@@ -403,6 +435,16 @@ function withShared(o, attrs, z) {
403
435
  if (ids.length) o.groupIds = ids;
404
436
  }
405
437
  if (attr(attrs, 'data-author') === 'ai') o.author = 'ai';
438
+ const authorName = attr(attrs, 'data-author-name');
439
+ if (authorName) {
440
+ const cleaned = sanitizeAuthorName(decodeEntities(authorName));
441
+ if (cleaned) o.authorName = cleaned;
442
+ }
443
+ const authorId = attr(attrs, 'data-author-id');
444
+ if (authorId) {
445
+ const cleanedId = sanitizeAuthorName(decodeEntities(authorId));
446
+ if (cleanedId) o.authorId = cleanedId;
447
+ }
406
448
  if (o.tool === 'arrow') {
407
449
  const sb = parseBind(attr(attrs, 'data-start-bind'));
408
450
  if (sb) o.from = sb.hostId;
@@ -615,6 +657,85 @@ function anchorToElement(ann, elements) {
615
657
  };
616
658
  }
617
659
 
660
+ // ─────────────────────────────────────────────────────────────────────────────
661
+ // Section membership + reading order (Phase 5, feature-whiteboard-
662
+ // annotation-improvements). A common flow: the user drops several media/
663
+ // stickies INTO a section, then asks "make a video from this" / "turn this
664
+ // into an Instagram carousel" — the agent needs to know exactly which
665
+ // annotations live in a given section, and in what visual order, without
666
+ // hand-computing containment or guessing from raw x/y. Mirrors
667
+ // `findElement`'s smallest-containing-rect convention (annotation CENTER
668
+ // inside a rect), applied against SECTION rects instead of DOM element
669
+ // rects.
670
+
671
+ /**
672
+ * Spatial reading order — top-to-bottom, then left-to-right — NOT `z`
673
+ * (paint/document order): a user can drop stickies in any order they like,
674
+ * and assembling a carousel/video in PAINT order would visibly scramble it.
675
+ * Rows are bucketed by a Y-tolerance band (half the smallest member's
676
+ * height, floored at 16 units) so items a few units off from each other
677
+ * still read as "the same row"; `z` only breaks a true tie (same band, same
678
+ * x) between two members.
679
+ */
680
+ function attachSectionMembers(annotations) {
681
+ const sections = annotations.filter((a) => a.tool === 'section' && a.id);
682
+ if (!sections.length) return annotations;
683
+
684
+ const centerOf = (a) => {
685
+ if (a.x == null || a.y == null) return null;
686
+ return { cx: a.x + (a.w || 0) / 2, cy: a.y + (a.h || 0) / 2 };
687
+ };
688
+
689
+ const membersBySection = new Map();
690
+ for (const section of sections) {
691
+ const sx1 = section.x;
692
+ const sy1 = section.y;
693
+ const sx2 = section.x + (section.w || 0);
694
+ const sy2 = section.y + (section.h || 0);
695
+ const candidates = [];
696
+ for (const a of annotations) {
697
+ if (a.tool === 'section' || !a.id) continue;
698
+ const c = centerOf(a);
699
+ if (!c) continue;
700
+ if (c.cx < sx1 || c.cx > sx2 || c.cy < sy1 || c.cy > sy2) continue;
701
+ candidates.push({ ann: a, c });
702
+ }
703
+ if (!candidates.length) {
704
+ membersBySection.set(section.id, []);
705
+ continue;
706
+ }
707
+ const heights = candidates.map((m) => m.ann.h || 0).filter((h) => h > 0);
708
+ const minHeight = heights.length ? Math.min(...heights) : 40;
709
+ const band = Math.max(16, minHeight / 2);
710
+ candidates.sort((m1, m2) => {
711
+ const dy = m1.c.cy - m2.c.cy;
712
+ if (Math.abs(dy) > band) return dy;
713
+ const dx = m1.c.cx - m2.c.cx;
714
+ if (dx !== 0) return dx;
715
+ return (m1.ann.z ?? 0) - (m2.ann.z ?? 0);
716
+ });
717
+ membersBySection.set(
718
+ section.id,
719
+ candidates.map((m, i) => ({
720
+ id: m.ann.id,
721
+ tool: m.ann.tool,
722
+ order: i,
723
+ x: m.ann.x,
724
+ y: m.ann.y,
725
+ w: m.ann.w,
726
+ h: m.ann.h,
727
+ ...(m.ann.href ? { href: m.ann.href } : null),
728
+ }))
729
+ );
730
+ }
731
+
732
+ return annotations.map((a) =>
733
+ a.tool === 'section' && membersBySection.has(a.id)
734
+ ? { ...a, members: membersBySection.get(a.id) }
735
+ : a
736
+ );
737
+ }
738
+
618
739
  // ─────────────────────────────────────────────────────────────────────────────
619
740
  // Main
620
741
 
@@ -674,6 +795,7 @@ function main() {
674
795
  if (artboards.length) {
675
796
  annotations = annotations.map((a) => anchorToArtboard(a, artboards));
676
797
  }
798
+ annotations = attachSectionMembers(annotations);
677
799
 
678
800
  if (args.graph) {
679
801
  process.stdout.write(`${JSON.stringify({ annotations, graph: buildGraph(annotations) })}\n`);
@@ -689,6 +811,7 @@ function main() {
689
811
  // canvas-rects manifest without a second implementation.
690
812
  export {
691
813
  anchorToElement,
814
+ attachSectionMembers,
692
815
  buildGraph,
693
816
  fileSlug,
694
817
  findElement,
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # transcribe.sh — feature-ai-media-generation (Phase 2, DDR-164). Local, no-key
3
+ # speech-to-text for automatic subtitles: run whisper.cpp on an audio/video file
4
+ # and emit SRT/VTT through the shared captions.ts reflow. The DEFAULT subtitle
5
+ # path — free, offline, private, word-timestamped — with cloud STT (ElevenLabs
6
+ # Scribe / Groq Whisper) as the fallback when whisper.cpp isn't installed.
7
+ #
8
+ # Reached via `maude design transcribe` (never a raw bin path — DDR-062). No dev
9
+ # server needed; the work is a local whisper.cpp spawn (mirrors probe-footage).
10
+ #
11
+ # Usage:
12
+ # transcribe.sh --source <assets/x.mp4|file> [--root <repo>]
13
+ # [--provider whisper|elevenlabs|groq]
14
+ # [--format srt|vtt|both] [--model <id>] [--whisper <bin>]
15
+ # [--lang <code>] [--segments]
16
+ #
17
+ # The engine is an EXPLICIT user choice (Task 2.6, DDR-164), never an automatic
18
+ # fallback: --provider wins, else the project's generation.transcription.provider
19
+ # config, else local whisper — and it SAYS which it chose. A chosen cloud engine
20
+ # (elevenlabs / groq) needs the dev server running (the key is resolved
21
+ # server-side) and an assets/<sha8> source; whisper is local, free, no key.
22
+ #
23
+ # whisper.cpp is a SOFT dependency (plugins/design/dependencies.json). A ggml
24
+ # model is required (--model / $MAUDE_WHISPER_MODEL); whisper.cpp ships no default
25
+ # and does not auto-download. See _transcribe.mjs for the download pointer.
26
+ #
27
+ # Stdout (last line): the produced .srt path (for $(...) capture).
28
+ # Exit: 0 ok / 1 dependency-or-input problem / 3 transcription failed.
29
+
30
+ set -euo pipefail
31
+
32
+ if ! command -v bun >/dev/null 2>&1; then
33
+ echo "transcribe.sh: bun is required (hard dependency — see plugins/design/dependencies.json)." >&2
34
+ echo " Install: curl -fsSL https://bun.sh/install | bash" >&2
35
+ exit 1
36
+ fi
37
+
38
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
+ exec bun run "$SCRIPT_DIR/_transcribe.mjs" "$@"