@1agh/maude 0.42.0 → 0.43.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 (236) 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/annotations-layer.tsx +1055 -278
  4. package/apps/studio/annotations-model.ts +58 -0
  5. package/apps/studio/api.ts +173 -22
  6. package/apps/studio/bin/_ingest-footage.mjs +386 -0
  7. package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
  8. package/apps/studio/bin/_video-playwright.mjs +141 -71
  9. package/apps/studio/bin/ingest-footage.sh +27 -0
  10. package/apps/studio/bin/photo-adjust.sh +163 -0
  11. package/apps/studio/bin/photo-bg-remove.sh +160 -0
  12. package/apps/studio/bin/probe-footage.sh +28 -0
  13. package/apps/studio/bin/read-annotations.mjs +125 -2
  14. package/apps/studio/canvas-edit.ts +459 -10
  15. package/apps/studio/canvas-lib.tsx +523 -5
  16. package/apps/studio/canvas-pipeline.ts +65 -7
  17. package/apps/studio/canvas-shell.tsx +156 -16
  18. package/apps/studio/client/app.jsx +451 -34
  19. package/apps/studio/client/export-center.jsx +35 -1
  20. package/apps/studio/client/github.js +4 -0
  21. package/apps/studio/client/panels/GitPanel.jsx +8 -0
  22. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
  23. package/apps/studio/client/panels/StickerPicker.jsx +160 -0
  24. package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
  25. package/apps/studio/client/photo-knobs.jsx +437 -0
  26. package/apps/studio/client/styles/3-shell-maude.css +42 -0
  27. package/apps/studio/commands/edit-source-command.ts +31 -4
  28. package/apps/studio/context-menu.tsx +31 -6
  29. package/apps/studio/dist/client.bundle.js +5561 -19
  30. package/apps/studio/dist/comment-mount.js +2 -2
  31. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  32. package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
  33. package/apps/studio/dist/runtime/pixi-js.js +519 -519
  34. package/apps/studio/dist/styles.css +1 -1
  35. package/apps/studio/dom-selection.ts +25 -0
  36. package/apps/studio/exporters/jobs.ts +47 -5
  37. package/apps/studio/exporters/video.ts +24 -4
  38. package/apps/studio/footage/schema.test.ts +195 -0
  39. package/apps/studio/footage/schema.ts +484 -0
  40. package/apps/studio/footage-store.ts +235 -0
  41. package/apps/studio/fs-watch.ts +10 -0
  42. package/apps/studio/git/endpoints.ts +68 -7
  43. package/apps/studio/git/service.ts +196 -20
  44. package/apps/studio/github/service.ts +67 -0
  45. package/apps/studio/handoff.ts +3 -2
  46. package/apps/studio/http.ts +173 -14
  47. package/apps/studio/input-router.tsx +10 -0
  48. package/apps/studio/inspect.ts +53 -1
  49. package/apps/studio/paths.ts +10 -0
  50. package/apps/studio/photo/filters.ts +285 -0
  51. package/apps/studio/photo/pipeline.ts +607 -0
  52. package/apps/studio/photo/schema.ts +367 -0
  53. package/apps/studio/photo-store.ts +154 -0
  54. package/apps/studio/runtime-bundle.ts +29 -6
  55. package/apps/studio/server.ts +5 -1
  56. package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
  57. package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
  58. package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
  59. package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
  60. package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
  61. package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
  62. package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
  63. package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
  64. package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
  65. package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
  66. package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
  67. package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
  68. package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
  69. package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
  70. package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
  71. package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
  72. package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
  73. package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
  74. package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
  75. package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
  76. package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
  77. package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
  78. package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
  79. package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
  80. package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
  81. package/apps/studio/stickers/life-style/best.png +0 -0
  82. package/apps/studio/stickers/life-style/come-on.png +0 -0
  83. package/apps/studio/stickers/life-style/cut.png +0 -0
  84. package/apps/studio/stickers/life-style/detox.png +0 -0
  85. package/apps/studio/stickers/life-style/just.png +0 -0
  86. package/apps/studio/stickers/life-style/manifest.json +68 -0
  87. package/apps/studio/stickers/life-style/mirror.png +0 -0
  88. package/apps/studio/stickers/life-style/nice.png +0 -0
  89. package/apps/studio/stickers/life-style/objects.png +0 -0
  90. package/apps/studio/stickers/life-style/ok.png +0 -0
  91. package/apps/studio/stickers/life-style/queen.png +0 -0
  92. package/apps/studio/stickers/life-style/star.png +0 -0
  93. package/apps/studio/stickers/life-style/sun.png +0 -0
  94. package/apps/studio/stickers/life-style/water.png +0 -0
  95. package/apps/studio/stickers/life-style/yeah.png +0 -0
  96. package/apps/studio/stickers/life-style/you-can.png +0 -0
  97. package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
  98. package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
  99. package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
  100. package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
  101. package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
  102. package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
  103. package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
  104. package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
  105. package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
  106. package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
  107. package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
  108. package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
  109. package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
  110. package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
  111. package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
  112. package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
  113. package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
  114. package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
  115. package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
  116. package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
  117. package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
  118. package/apps/studio/stickers/project-status/group-100.png +0 -0
  119. package/apps/studio/stickers/project-status/group-101.png +0 -0
  120. package/apps/studio/stickers/project-status/group-102.png +0 -0
  121. package/apps/studio/stickers/project-status/group-103.png +0 -0
  122. package/apps/studio/stickers/project-status/group-104.png +0 -0
  123. package/apps/studio/stickers/project-status/group-105.png +0 -0
  124. package/apps/studio/stickers/project-status/group-106.png +0 -0
  125. package/apps/studio/stickers/project-status/group-107.png +0 -0
  126. package/apps/studio/stickers/project-status/group-108.png +0 -0
  127. package/apps/studio/stickers/project-status/group-109.png +0 -0
  128. package/apps/studio/stickers/project-status/group-110.png +0 -0
  129. package/apps/studio/stickers/project-status/group-111.png +0 -0
  130. package/apps/studio/stickers/project-status/group-112.png +0 -0
  131. package/apps/studio/stickers/project-status/group-113.png +0 -0
  132. package/apps/studio/stickers/project-status/group-114.png +0 -0
  133. package/apps/studio/stickers/project-status/group-115.png +0 -0
  134. package/apps/studio/stickers/project-status/group-117.png +0 -0
  135. package/apps/studio/stickers/project-status/group-118.png +0 -0
  136. package/apps/studio/stickers/project-status/group-119.png +0 -0
  137. package/apps/studio/stickers/project-status/group-120.png +0 -0
  138. package/apps/studio/stickers/project-status/group-121.png +0 -0
  139. package/apps/studio/stickers/project-status/group-122.png +0 -0
  140. package/apps/studio/stickers/project-status/group-41.png +0 -0
  141. package/apps/studio/stickers/project-status/group-42.png +0 -0
  142. package/apps/studio/stickers/project-status/group-43.png +0 -0
  143. package/apps/studio/stickers/project-status/group-44.png +0 -0
  144. package/apps/studio/stickers/project-status/group-45.png +0 -0
  145. package/apps/studio/stickers/project-status/group-46.png +0 -0
  146. package/apps/studio/stickers/project-status/group-47.png +0 -0
  147. package/apps/studio/stickers/project-status/group-48.png +0 -0
  148. package/apps/studio/stickers/project-status/group-49.png +0 -0
  149. package/apps/studio/stickers/project-status/group-50.png +0 -0
  150. package/apps/studio/stickers/project-status/group-51.png +0 -0
  151. package/apps/studio/stickers/project-status/group-52.png +0 -0
  152. package/apps/studio/stickers/project-status/group-53.png +0 -0
  153. package/apps/studio/stickers/project-status/group-54.png +0 -0
  154. package/apps/studio/stickers/project-status/group-55.png +0 -0
  155. package/apps/studio/stickers/project-status/group-56.png +0 -0
  156. package/apps/studio/stickers/project-status/group-57.png +0 -0
  157. package/apps/studio/stickers/project-status/group-58.png +0 -0
  158. package/apps/studio/stickers/project-status/group-59.png +0 -0
  159. package/apps/studio/stickers/project-status/group-60.png +0 -0
  160. package/apps/studio/stickers/project-status/group-61.png +0 -0
  161. package/apps/studio/stickers/project-status/group-62.png +0 -0
  162. package/apps/studio/stickers/project-status/group-63.png +0 -0
  163. package/apps/studio/stickers/project-status/group-64.png +0 -0
  164. package/apps/studio/stickers/project-status/group-65.png +0 -0
  165. package/apps/studio/stickers/project-status/group-66.png +0 -0
  166. package/apps/studio/stickers/project-status/group-67.png +0 -0
  167. package/apps/studio/stickers/project-status/group-68.png +0 -0
  168. package/apps/studio/stickers/project-status/group-69.png +0 -0
  169. package/apps/studio/stickers/project-status/group-70.png +0 -0
  170. package/apps/studio/stickers/project-status/group-71.png +0 -0
  171. package/apps/studio/stickers/project-status/group-72.png +0 -0
  172. package/apps/studio/stickers/project-status/group-73.png +0 -0
  173. package/apps/studio/stickers/project-status/group-74.png +0 -0
  174. package/apps/studio/stickers/project-status/group-75.png +0 -0
  175. package/apps/studio/stickers/project-status/group-76.png +0 -0
  176. package/apps/studio/stickers/project-status/group-77.png +0 -0
  177. package/apps/studio/stickers/project-status/group-78.png +0 -0
  178. package/apps/studio/stickers/project-status/group-79.png +0 -0
  179. package/apps/studio/stickers/project-status/group-80.png +0 -0
  180. package/apps/studio/stickers/project-status/group-81.png +0 -0
  181. package/apps/studio/stickers/project-status/group-82.png +0 -0
  182. package/apps/studio/stickers/project-status/group-83.png +0 -0
  183. package/apps/studio/stickers/project-status/group-84.png +0 -0
  184. package/apps/studio/stickers/project-status/group-85.png +0 -0
  185. package/apps/studio/stickers/project-status/group-86.png +0 -0
  186. package/apps/studio/stickers/project-status/group-87.png +0 -0
  187. package/apps/studio/stickers/project-status/group-88.png +0 -0
  188. package/apps/studio/stickers/project-status/group-89.png +0 -0
  189. package/apps/studio/stickers/project-status/group-90.png +0 -0
  190. package/apps/studio/stickers/project-status/group-91.png +0 -0
  191. package/apps/studio/stickers/project-status/group-92.png +0 -0
  192. package/apps/studio/stickers/project-status/group-93.png +0 -0
  193. package/apps/studio/stickers/project-status/group-94.png +0 -0
  194. package/apps/studio/stickers/project-status/group-96.png +0 -0
  195. package/apps/studio/stickers/project-status/group-97.png +0 -0
  196. package/apps/studio/stickers/project-status/group-98.png +0 -0
  197. package/apps/studio/stickers/project-status/group-99.png +0 -0
  198. package/apps/studio/stickers/project-status/manifest.json +328 -0
  199. package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
  200. package/apps/studio/test/acp-commands.test.ts +2 -1
  201. package/apps/studio/test/asset-api.test.ts +3 -3
  202. package/apps/studio/test/canvas-edit.test.ts +17 -6
  203. package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
  204. package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
  205. package/apps/studio/test/edit-source-command.test.ts +23 -0
  206. package/apps/studio/test/element-structural-edit.test.ts +46 -0
  207. package/apps/studio/test/footage-store.test.ts +100 -0
  208. package/apps/studio/test/git-branches.test.ts +97 -0
  209. package/apps/studio/test/github-api.test.ts +56 -1
  210. package/apps/studio/test/input-router.test.ts +33 -0
  211. package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
  212. package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
  213. package/apps/studio/test/photo-edit-api.test.ts +201 -0
  214. package/apps/studio/test/photo-filters.test.ts +149 -0
  215. package/apps/studio/test/photo-pipeline.test.ts +106 -0
  216. package/apps/studio/test/photo-taxonomy.test.ts +96 -0
  217. package/apps/studio/test/read-annotations.test.ts +164 -0
  218. package/apps/studio/test/shell-importmap.test.ts +49 -0
  219. package/apps/studio/test/text-editability-stamp.test.ts +131 -0
  220. package/apps/studio/test/timeline-parse.test.ts +57 -0
  221. package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
  222. package/apps/studio/test/undo-stack.test.ts +33 -0
  223. package/apps/studio/test/video-asset.test.ts +5 -5
  224. package/apps/studio/test/video-render-bridge.test.ts +23 -1
  225. package/apps/studio/text-caret.ts +239 -0
  226. package/apps/studio/tool-palette.tsx +43 -20
  227. package/apps/studio/undo-stack.ts +94 -2
  228. package/apps/studio/use-annotation-resize.tsx +4 -4
  229. package/apps/studio/use-canvas-media-drop.tsx +38 -1
  230. package/apps/studio/use-selection-set.tsx +21 -0
  231. package/apps/studio/video-comp.tsx +48 -7
  232. package/apps/studio/whats-new.json +53 -0
  233. package/cli/commands/design.mjs +34 -3
  234. package/cli/lib/gitignore-block.mjs +1 -0
  235. package/package.json +10 -10
  236. package/plugins/design/templates/_shell.html +1 -0
@@ -126,84 +126,132 @@ try {
126
126
  // — both fall through to the frame-step path below.
127
127
  const useRenderer = !!renderLib && format !== 'gif' && !!compId;
128
128
 
129
+ // When the whole-comp renderer path throws we DON'T fail the export. The most
130
+ // common cause is a `RangeError: Maximum call stack size exceeded` from
131
+ // @remotion/web-renderer's guardless recursive DOM precompositing on a
132
+ // deeply-nested comp (RCA issue-video-mp4-rendermediaonweb-stack-overflow):
133
+ // one native recursion level per nested opacity/transform/filter/mask layer,
134
+ // no depth cap. Instead we degrade to the frame-step screenshot + mediabunny
135
+ // path below, which goes through Chromium's native compositor and is immune to
136
+ // that recursion. The one cost is audio — the frame-step path is video-only —
137
+ // so a comp that overflows the audio renderer still exports, just muted. That
138
+ // beats a hard failure with no file. `video.ts` always passes --encode-lib
139
+ // (only --render-lib is conditional), so the fallback has all it needs here.
140
+ let renderFallbackReason = null;
141
+
129
142
  if (useRenderer) {
130
- const t0 = Date.now();
131
- const libSrc = readFileSync(renderLib, 'utf8');
132
- await page.addScriptTag({ content: libSrc, type: 'module' });
133
- await page.waitForFunction(() => typeof window.__maudeRenderVideo__ === 'function', {
134
- timeout: timeoutMs,
135
- });
143
+ try {
144
+ const t0 = Date.now();
145
+ const libSrc = readFileSync(renderLib, 'utf8');
146
+ await page.addScriptTag({ content: libSrc, type: 'module' });
147
+ await page.waitForFunction(() => typeof window.__maudeRenderVideo__ === 'function', {
148
+ timeout: timeoutMs,
149
+ });
136
150
 
137
- const explicitFrames = Number(framesArg);
138
- const frameRange =
139
- Number.isFinite(explicitFrames) && explicitFrames > 0 ? [0, explicitFrames - 1] : null;
151
+ const explicitFrames = Number(framesArg);
152
+ const frameRange =
153
+ Number.isFinite(explicitFrames) && explicitFrames > 0 ? [0, explicitFrames - 1] : null;
140
154
 
141
- const rendered = await page.evaluate(
142
- async ({ compId, container, scale, muted, frameRange, licenseKey }) => {
143
- return window.__maude_render_video__(compId, {
144
- container,
145
- scale,
146
- muted,
155
+ const rendered = await page.evaluate(
156
+ async ({ compId, container, scale, muted, frameRange, licenseKey }) => {
157
+ return window.__maude_render_video__(compId, {
158
+ container,
159
+ scale,
160
+ muted,
161
+ frameRange,
162
+ licenseKey,
163
+ });
164
+ },
165
+ {
166
+ compId,
167
+ container: format,
168
+ scale: deviceScaleFactor,
169
+ muted: !wantAudio,
147
170
  frameRange,
148
171
  licenseKey,
149
- });
150
- },
151
- {
152
- compId,
153
- container: format,
154
- scale: deviceScaleFactor,
155
- muted: !wantAudio,
156
- frameRange,
157
- licenseKey,
158
- }
159
- );
172
+ }
173
+ );
160
174
 
161
- writeFileSync(out, Buffer.from(rendered.b64, 'base64'));
175
+ writeFileSync(out, Buffer.from(rendered.b64, 'base64'));
162
176
 
163
- // Comp meta for the summary (diagnostic only — video.ts reads `container`).
164
- const compMeta = await page.evaluate(
165
- (id) =>
166
- (typeof window.__maude_comps__ === 'function' ? window.__maude_comps__() : []).find(
167
- (c) => c.id === id
168
- ) ?? null,
169
- compId
170
- );
171
- const frameCount = frameRange
172
- ? frameRange[1] - frameRange[0] + 1
173
- : (compMeta?.durationInFrames ?? 0);
174
-
175
- const result = {
176
- fps: compMeta?.fps ?? null,
177
- frameCount,
178
- width: Math.round((compMeta?.width ?? 0) * deviceScaleFactor),
179
- height: Math.round((compMeta?.height ?? 0) * deviceScaleFactor),
180
- out,
181
- bytes: rendered.bytes,
182
- container: rendered.container,
183
- codec: rendered.videoCodec,
184
- audioCodec: rendered.audioCodec,
185
- };
186
- const ms = Date.now() - t0;
187
- console.error(
188
- `✓ rendered ${rendered.container}/${rendered.videoCodec}` +
189
- `${rendered.audioCodec ? `+${rendered.audioCodec}` : ' (muted)'} → ${out} (${rendered.bytes} B) in ${ms}ms`
190
- );
191
- console.log(JSON.stringify(result));
192
- } else {
193
- await frameStepCapture({
194
- page,
195
- handle,
196
- compId,
197
- fpsArg,
198
- framesArg,
199
- dumpDir,
200
- encodeLib,
201
- format,
202
- out,
203
- mode,
204
- deviceScaleFactor,
205
- timeoutMs,
206
- });
177
+ // Comp meta for the summary (diagnostic only — video.ts reads `container`).
178
+ const compMeta = await page.evaluate(
179
+ (id) =>
180
+ (typeof window.__maude_comps__ === 'function' ? window.__maude_comps__() : []).find(
181
+ (c) => c.id === id
182
+ ) ?? null,
183
+ compId
184
+ );
185
+ const frameCount = frameRange
186
+ ? frameRange[1] - frameRange[0] + 1
187
+ : (compMeta?.durationInFrames ?? 0);
188
+
189
+ const result = {
190
+ fps: compMeta?.fps ?? null,
191
+ frameCount,
192
+ width: Math.round((compMeta?.width ?? 0) * deviceScaleFactor),
193
+ height: Math.round((compMeta?.height ?? 0) * deviceScaleFactor),
194
+ out,
195
+ bytes: rendered.bytes,
196
+ container: rendered.container,
197
+ codec: rendered.videoCodec,
198
+ audioCodec: rendered.audioCodec,
199
+ };
200
+ const ms = Date.now() - t0;
201
+ console.error(
202
+ `✓ rendered ${rendered.container}/${rendered.videoCodec}` +
203
+ `${rendered.audioCodec ? `+${rendered.audioCodec}` : ' (muted)'} → ${out} (${rendered.bytes} B) in ${ms}ms`
204
+ );
205
+ console.log(JSON.stringify(result));
206
+ } catch (err) {
207
+ // Collapse newlines — the message carries an in-page (untrusted canvas,
208
+ // DDR-054) error + stack, and it flows to console.error / the job's
209
+ // persisted error, so a forged "\n[FATAL] …" line can't be injected into
210
+ // the dev-server's stderr or the history ledger.
211
+ renderFallbackReason = oneLine(err instanceof Error ? err.message : String(err));
212
+ console.error(
213
+ `⚠ renderMediaOnWeb failed (${renderFallbackReason}); falling back to ` +
214
+ 'frame-step screenshot capture — video will export WITHOUT audio'
215
+ );
216
+ }
217
+ }
218
+
219
+ // Frame-step path: the normal route for gif / ordinary / comp-less artboards
220
+ // (useRenderer === false), AND the graceful-degradation fallback when the
221
+ // renderer above threw (renderFallbackReason !== null).
222
+ if (!useRenderer || renderFallbackReason !== null) {
223
+ try {
224
+ await frameStepCapture({
225
+ page,
226
+ handle,
227
+ compId,
228
+ fpsArg,
229
+ framesArg,
230
+ dumpDir,
231
+ encodeLib,
232
+ format,
233
+ out,
234
+ mode,
235
+ deviceScaleFactor,
236
+ timeoutMs,
237
+ fallbackReason: renderFallbackReason,
238
+ });
239
+ } catch (fallbackErr) {
240
+ // If we only reached the frame-step path because the renderer already
241
+ // failed, surface BOTH failures so a genuine bug isn't hidden behind the
242
+ // fallback's own error.
243
+ if (renderFallbackReason !== null) {
244
+ const fb = oneLine(
245
+ fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
246
+ );
247
+ throw new Error(
248
+ `video export failed on both paths — renderMediaOnWeb: ${renderFallbackReason}; ` +
249
+ `frame-step fallback: ${fb}`,
250
+ { cause: fallbackErr }
251
+ );
252
+ }
253
+ throw fallbackErr;
254
+ }
207
255
  }
208
256
  } finally {
209
257
  await browser.close();
@@ -227,6 +275,10 @@ async function frameStepCapture({
227
275
  mode,
228
276
  deviceScaleFactor,
229
277
  timeoutMs,
278
+ // Set (to the renderer's error message) only when this run is the
279
+ // graceful-degradation fallback for a failed renderMediaOnWeb path; stamps
280
+ // `degraded`/`audioDropped` onto the stdout summary so video.ts can warn.
281
+ fallbackReason = null,
230
282
  }) {
231
283
  const rect0 = await handle.evaluate((el) => {
232
284
  const r = el.getBoundingClientRect();
@@ -352,10 +404,22 @@ async function frameStepCapture({
352
404
  { b64, isGif }
353
405
  );
354
406
  }
407
+ // Machine-readable progress (stdout) — spawnShim parses `MAUDE_PROGRESS`
408
+ // lines → onProgress → job.progress → the live bar in the Exports panel, so
409
+ // a long frame-step render (the renderMediaOnWeb-overflow fallback) shows
410
+ // "N of M" instead of a static "Exporting…". Filtered out of the summary
411
+ // stdout the exporter reads. Human log stays throttled on stderr.
412
+ console.log(`MAUDE_PROGRESS {"current":${f + 1},"total":${frameCount}}`);
355
413
  if (f % 30 === 0) console.error(`frame ${f + 1}/${frameCount}`);
356
414
  }
357
415
 
358
416
  let result = { fps, frameCount, width: outW, height: outH, framePaths };
417
+ // Degradation marker — this run only encodes video (no audio), so when we're
418
+ // standing in for a failed audio renderer, tell the exporter the export is
419
+ // degraded and audio was dropped (RCA issue-video-mp4-rendermediaonweb-stack-overflow).
420
+ if (fallbackReason !== null) {
421
+ result = { ...result, degraded: true, audioDropped: true, fallbackReason };
422
+ }
359
423
  if (encoding) {
360
424
  const enc = await page.evaluate(
361
425
  async ({ isGif }) =>
@@ -450,3 +514,9 @@ async function seekFrame(page, frame, fps, mode) {
450
514
  function cssEscape(s) {
451
515
  return String(s).replace(/["\\\]]/g, '\\$&');
452
516
  }
517
+
518
+ /** Collapse CR/LF runs to a single space — used to neutralize log/ledger
519
+ * injection from untrusted in-page (canvas-origin, DDR-054) error text. */
520
+ function oneLine(s) {
521
+ return String(s).replace(/[\r\n]+/g, ' ');
522
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ # ingest-footage.sh — thin shim over _ingest-footage.mjs; reached via
3
+ # `maude design ingest-footage` (DDR-062), never a raw bin path. See
4
+ # _ingest-footage.mjs for the full rationale (feature-footage-analysis-director).
5
+ #
6
+ # Usage:
7
+ # ingest-footage.sh <dir> --root <repo> [--design-root .design]
8
+ # [--recursive] [--max-bytes N]
9
+ #
10
+ # stdout on success = a JSON manifest { clips, skipped, assetsDir }.
11
+ # Exit: 0 ok (even with skips) · 2 usage · 6 write/containment error · 1 other.
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+
14
+ case "$1" in
15
+ --help|-h) sed -n '2,14p' "$0" | sed 's/^# \?//'; exit 0 ;;
16
+ esac
17
+
18
+ # Prefer node (always present with a maude install); fall back to bun in a dev
19
+ # tree that has bun but a shimmed node. The module is pure Node ESM — no .ts.
20
+ if command -v node >/dev/null 2>&1; then
21
+ exec node "$SCRIPT_DIR/_ingest-footage.mjs" "$@"
22
+ elif command -v bun >/dev/null 2>&1; then
23
+ exec bun run "$SCRIPT_DIR/_ingest-footage.mjs" "$@"
24
+ else
25
+ echo "ingest-footage.sh: node or bun is required." >&2
26
+ exit 1
27
+ fi
@@ -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,160 @@
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
+ # Usage:
20
+ # photo-bg-remove.sh --asset <assets/<sha8>.<ext>> [--root <repo>]
21
+ # [--slug <name>] [--timeout <secs>]
22
+ #
23
+ # Requires a running dev server (caller runs `maude design server-up` first);
24
+ # reads the port from <designRoot>/_server.json. Requires agent-browser (the
25
+ # ML pass needs a real WASM/WebGPU-capable browser driving real DOM state —
26
+ # there is no non-browser path, unlike photo-adjust.sh's plain curl).
27
+ #
28
+ # Stdout (last line): the new matte asset path `assets/<sha8>.png` (for $(...)).
29
+ # Stderr: progress / diagnostics.
30
+ # Exit: 0 success / 1 missing input, server, or agent-browser / 2 bad args
31
+ # / 3 drive/navigation failed / 4 the harness itself reported an error
32
+ # / 5 timed out waiting for the ML pass to finish.
33
+
34
+ set -uo pipefail
35
+
36
+ ASSET="" SLUG="" REPO="" TIMEOUT=150
37
+
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --asset) ASSET="$2"; shift 2 ;;
41
+ --slug) SLUG="$2"; shift 2 ;;
42
+ --root) REPO="$2"; shift 2 ;;
43
+ --timeout) TIMEOUT="$2"; shift 2 ;;
44
+ --help|-h)
45
+ sed -n '2,29p' "$0" | sed 's/^# \?//'
46
+ exit 0
47
+ ;;
48
+ *)
49
+ echo "photo-bg-remove.sh: unknown arg '$1' (try --help)" >&2
50
+ exit 2
51
+ ;;
52
+ esac
53
+ done
54
+
55
+ [ -n "$ASSET" ] || { echo "photo-bg-remove.sh: --asset <assets/<sha8>.<ext>> required" >&2; exit 2; }
56
+ # Strip a leading designRoot-relative slash/prefix if the caller passed one.
57
+ ASSET="${ASSET#/}"
58
+ case "$ASSET" in
59
+ assets/*) : ;;
60
+ *) echo "photo-bg-remove.sh: --asset must look like assets/<sha8>.<ext>, got '$ASSET'" >&2; exit 2 ;;
61
+ esac
62
+
63
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
64
+ [ -n "$SLUG" ] || SLUG=$(basename "$ASSET" | sed -E 's/\.[^.]+$//')
65
+ SLUG=$(bash "$SCRIPT_DIR/slug.sh" "$SLUG" 2>/dev/null || printf '%s' "$SLUG" | tr '[:upper:]' '[:lower:]')
66
+
67
+ # ---------- resolve repo + design root + port ----------
68
+ if [ -z "$REPO" ]; then
69
+ REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
70
+ fi
71
+ DESIGN_ROOT="$REPO/.design"
72
+ [ -d "$DESIGN_ROOT" ] || { echo "photo-bg-remove.sh: no .design/ under $REPO" >&2; exit 1; }
73
+ [ -f "$DESIGN_ROOT/$ASSET" ] || { echo "photo-bg-remove.sh: asset not found: $DESIGN_ROOT/$ASSET" >&2; exit 1; }
74
+
75
+ STATE="$DESIGN_ROOT/_server.json"
76
+ [ -f "$STATE" ] || { echo "photo-bg-remove.sh: $STATE missing — run \`maude design server-up\` first" >&2; exit 1; }
77
+ if command -v jq >/dev/null 2>&1; then
78
+ PORT=$(jq -r .port "$STATE" 2>/dev/null)
79
+ else
80
+ PORT=$(sed -nE 's/.*"port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
81
+ fi
82
+ [ -n "$PORT" ] || { echo "photo-bg-remove.sh: no port in $STATE" >&2; exit 1; }
83
+
84
+ # ---------- resolve agent-browser (hard dependency — see header) ----------
85
+ # Mirrors screenshot.sh's MAUDE_AGENT_BROWSER override (DDR-144 attacker-F4 — an
86
+ # explicit single-binary pointer, never a PATH prepend a same-user attacker
87
+ # could shadow) and its ensure-browser.sh chrome resolution.
88
+ AB="${MAUDE_AGENT_BROWSER:-agent-browser}"
89
+ command -v "$AB" >/dev/null 2>&1 || {
90
+ 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
91
+ exit 1
92
+ }
93
+ if [ -z "${AGENT_BROWSER_EXECUTABLE_PATH:-}" ]; then
94
+ ES_FLAGS="--quiet"
95
+ [ -z "${MAUDE_DEV_SERVER_ROOT:-}" ] && ES_FLAGS="--quiet --no-download"
96
+ BROWSER_PATH="$(bash "$SCRIPT_DIR/ensure-browser.sh" $ES_FLAGS 2>/dev/null)"
97
+ [ -n "$BROWSER_PATH" ] && export AGENT_BROWSER_EXECUTABLE_PATH="$BROWSER_PATH"
98
+ fi
99
+
100
+ # ---------- generate the harness canvas ----------
101
+ PHOTO_DIR="$DESIGN_ROOT/_photo"
102
+ mkdir -p "$PHOTO_DIR"
103
+ HARNESS_TSX="$PHOTO_DIR/$SLUG.bgremove.tsx"
104
+ {
105
+ echo "import { PhotoBgRemoveHarness } from '@maude/canvas-lib';"
106
+ echo
107
+ echo "// AUTO-GENERATED by \`maude design photo-bg-remove\` — headless bg-removal proof"
108
+ echo "// harness. Lives under _photo/ (gitignored); safe to delete + regenerate."
109
+ echo "export default function PhotoBgRemoveCanvas() {"
110
+ printf ' return <PhotoBgRemoveHarness source=%s />;\n' "\"$ASSET\""
111
+ echo "}"
112
+ } > "$HARNESS_TSX"
113
+ echo "→ harness canvas: ${HARNESS_TSX#$REPO/}" >&2
114
+
115
+ REL="_photo/$SLUG.bgremove.tsx"
116
+ URL="http://localhost:$PORT/_canvas-shell.html?canvas=$REL"
117
+ SESSION="photo-bgremove-$SLUG-$$"
118
+ SEL="[data-photo-bgremove-status]"
119
+
120
+ cleanup() { "$AB" --session "$SESSION" close >/dev/null 2>&1 || true; }
121
+ trap cleanup EXIT
122
+
123
+ echo "→ driving $URL (session $SESSION)" >&2
124
+ if ! "$AB" --session "$SESSION" open "$URL" >&2; then
125
+ echo "photo-bg-remove.sh: agent-browser failed to open the harness canvas" >&2
126
+ exit 3
127
+ fi
128
+
129
+ # ---------- poll the harness's status attribute ----------
130
+ # The ML pass includes a first-use ~40 MB model-weight fetch (Task 11/12) —
131
+ # this can dominate the wait far more than inference itself, hence the
132
+ # generous default timeout.
133
+ ELAPSED=0
134
+ STATUS="pending"
135
+ while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
136
+ STATUS="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-status 2>/dev/null | tr -d '"')"
137
+ case "$STATUS" in
138
+ done|error) break ;;
139
+ esac
140
+ sleep 2
141
+ ELAPSED=$((ELAPSED + 2))
142
+ done
143
+
144
+ if [ "$STATUS" = "error" ]; then
145
+ ERR="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-error 2>/dev/null | tr -d '"')"
146
+ echo "photo-bg-remove.sh: harness reported an error: ${ERR:-unknown}" >&2
147
+ exit 4
148
+ fi
149
+
150
+ if [ "$STATUS" != "done" ]; then
151
+ echo "photo-bg-remove.sh: timed out after ${TIMEOUT}s waiting for the ML pass (still '$STATUS')" >&2
152
+ exit 5
153
+ fi
154
+
155
+ RESULT="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-result 2>/dev/null | tr -d '"')"
156
+ [ -n "$RESULT" ] || { echo "photo-bg-remove.sh: harness reported done but no result asset" >&2; exit 4; }
157
+
158
+ echo "→ background removed → $RESULT" >&2
159
+ # Last stdout line = the new matte asset path, for $(maude design photo-bg-remove ...) capture.
160
+ printf '%s\n' "$RESULT"
@@ -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