@xiaobuyu/nodesign 0.0.1 → 0.0.3

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 (358) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +42 -22
  9. package/server/api/assets/notes.js +194 -144
  10. package/server/api/assets.js +891 -896
  11. package/server/api/board.js +134 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +465 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1035 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +351 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +152 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  120. package/server/engine/mcp/tools/explain-style.js +237 -237
  121. package/server/engine/mcp/tools/export-handoff.js +133 -133
  122. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  123. package/server/engine/mcp/tools/generate-image.js +842 -842
  124. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  125. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  126. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  127. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  128. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  129. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  130. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  131. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  132. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  133. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  134. package/server/engine/mcp/tools/highlight.js +68 -68
  135. package/server/engine/mcp/tools/list-pages.js +222 -222
  136. package/server/engine/mcp/tools/look-at-board.js +124 -0
  137. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  138. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  139. package/server/engine/mcp/tools/organize-board.js +64 -64
  140. package/server/engine/mcp/tools/paint-still.js +434 -434
  141. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  142. package/server/engine/mcp/tools/preview-deck.js +130 -130
  143. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  144. package/server/engine/mcp/tools/publish-site.js +102 -102
  145. package/server/engine/mcp/tools/query-elements.js +195 -195
  146. package/server/engine/mcp/tools/read-board.js +190 -103
  147. package/server/engine/mcp/tools/read-document.js +93 -93
  148. package/server/engine/mcp/tools/read-page.js +264 -264
  149. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  150. package/server/engine/mcp/tools/read-user-view.js +59 -0
  151. package/server/engine/mcp/tools/record-decision.js +140 -140
  152. package/server/engine/mcp/tools/relate-on-board.js +112 -110
  153. package/server/engine/mcp/tools/report-issue.js +115 -115
  154. package/server/engine/mcp/tools/roll-film.js +266 -266
  155. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  156. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  157. package/server/engine/mcp/tools/screenshot.js +583 -583
  158. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  159. package/server/engine/mcp/tools/tier-gate.js +96 -96
  160. package/server/engine/mcp/tools/trace-motion.js +215 -215
  161. package/server/engine/mcp/tools/web-search.js +548 -548
  162. package/server/engine/mcp/tools/write-on-board.js +130 -0
  163. package/server/engine/motion/inventory.js +349 -349
  164. package/server/engine/perception/session.js +235 -235
  165. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  174. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
  175. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  176. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  177. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
  178. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  179. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  180. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
  181. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  182. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  183. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  185. package/server/engine/runs/active-runs.js +698 -698
  186. package/server/engine/runs/board-tasklist.js +130 -0
  187. package/server/engine/runs/live-turn.js +252 -252
  188. package/server/engine/runs/store.js +364 -364
  189. package/server/engine/runs/turn-relay.js +217 -217
  190. package/server/engine/runtime/workspace.js +164 -164
  191. package/server/index.js +3 -0
  192. package/server/lib/artifact-file-path.js +80 -80
  193. package/server/lib/artifact-target.js +284 -284
  194. package/server/lib/asset-refs.js +239 -239
  195. package/server/lib/async-queue.js +104 -104
  196. package/server/lib/auto-relations.js +130 -130
  197. package/server/lib/binding-types.js +72 -60
  198. package/server/lib/board-graph-export.js +191 -0
  199. package/server/lib/board-groups.js +167 -0
  200. package/server/lib/board-hero.js +76 -0
  201. package/server/lib/board-kind-sizes.js +85 -62
  202. package/server/lib/board-relations.js +133 -133
  203. package/server/lib/browse-proxy.js +180 -180
  204. package/server/lib/canvas-id.js +36 -36
  205. package/server/lib/chalk.js +121 -0
  206. package/server/lib/cover.js +227 -227
  207. package/server/lib/danbooru-tags.js +291 -291
  208. package/server/lib/doc-extract.js +156 -156
  209. package/server/lib/docx/build-from-source.js +260 -260
  210. package/server/lib/docx/build.js +490 -490
  211. package/server/lib/docx/dump-styles.js +287 -287
  212. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  213. package/server/lib/docx/merge-runs.js +129 -129
  214. package/server/lib/docx/numbering.js +218 -218
  215. package/server/lib/docx/order.js +152 -152
  216. package/server/lib/docx/rawzip.js +172 -172
  217. package/server/lib/docx/render.js +92 -92
  218. package/server/lib/docx/text-lint.js +225 -225
  219. package/server/lib/docx/tokens.js +335 -335
  220. package/server/lib/docx/units.js +29 -29
  221. package/server/lib/docx/xml.js +291 -291
  222. package/server/lib/docx-pages.js +187 -187
  223. package/server/lib/export-collect.js +289 -289
  224. package/server/lib/export-package.js +210 -210
  225. package/server/lib/html-srcset.js +145 -145
  226. package/server/lib/image-variant.js +460 -460
  227. package/server/lib/ingress/forward-openai-chat.js +305 -305
  228. package/server/lib/ingress/openai-chat.js +491 -491
  229. package/server/lib/ingress/session-notice.js +66 -66
  230. package/server/lib/ingress/session-routes.js +68 -68
  231. package/server/lib/ingress/slot-probe.js +130 -130
  232. package/server/lib/ingress/upstream-billing.js +52 -52
  233. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  234. package/server/lib/ingress/upstream-truncation.js +48 -48
  235. package/server/lib/issues-store.js +182 -182
  236. package/server/lib/kinds/deck.js +72 -72
  237. package/server/lib/kinds/docx.js +283 -283
  238. package/server/lib/kinds/file-kinds.js +113 -113
  239. package/server/lib/kinds/index.js +202 -202
  240. package/server/lib/kinds/site.js +161 -161
  241. package/server/lib/model-ingress.js +543 -543
  242. package/server/lib/moderation.js +255 -255
  243. package/server/lib/notice-store.js +103 -103
  244. package/server/lib/plugin-install.js +104 -104
  245. package/server/lib/plugin-validator.js +721 -721
  246. package/server/lib/publish-store.js +115 -115
  247. package/server/lib/quick-summary.js +52 -52
  248. package/server/lib/quota.js +270 -270
  249. package/server/lib/rate-window.js +29 -29
  250. package/server/lib/reference-assets.js +92 -92
  251. package/server/lib/region-shot.js +135 -135
  252. package/server/lib/safe-path.js +73 -73
  253. package/server/lib/sdk-session.js +33 -33
  254. package/server/lib/showcase-store.js +92 -92
  255. package/server/lib/site-publish.js +465 -465
  256. package/server/lib/sketch-layout.js +277 -0
  257. package/server/lib/ssrf-guard.js +432 -432
  258. package/server/lib/task-scan.js +158 -158
  259. package/server/lib/tavern-json.js +154 -154
  260. package/server/lib/video-variant.js +237 -237
  261. package/server/lib/workspace-path.js +33 -33
  262. package/server/ops/fix-sdk-musl.mjs +40 -40
  263. package/server/ops/install-macos-fonts.sh +114 -114
  264. package/server/ops/macos-fonts.conf +336 -336
  265. package/server/ops/sandbox-shim/bwrap +57 -57
  266. package/server/projects/assets-summary.js +119 -119
  267. package/server/projects/auto-name.js +53 -53
  268. package/server/projects/board-limits.js +12 -0
  269. package/server/projects/board-sanitize.js +252 -0
  270. package/server/projects/board-store.js +507 -640
  271. package/server/projects/move-entry.js +103 -103
  272. package/server/projects/store.js +270 -270
  273. package/server/projects/ui-config.js +60 -54
  274. package/server/projects/viewpoint-store.js +78 -0
  275. package/server/projects/workspace-templates.js +53 -53
  276. package/server/projects/workspace.js +1025 -1025
  277. package/server/runtime/local-config.js +177 -177
  278. package/server/runtime/profile.js +72 -72
  279. package/server/services/rembg-service.py +334 -334
  280. package/server/shared/README.md +25 -25
  281. package/server/shared/deck.js +54 -54
  282. package/server/shared/time.js +49 -49
  283. package/server/style-pipeline/README.md +53 -53
  284. package/server/ws/broker.js +39 -36
  285. package/server/ws/browse-channel.js +177 -177
  286. package/server/ws/index.js +386 -386
  287. package/web/README.md +89 -89
  288. package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
  289. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
  290. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
  291. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  292. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  293. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  294. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  295. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  296. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  297. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  298. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  299. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  300. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  301. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  302. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  303. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  304. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  305. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  306. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  307. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  308. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  309. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  310. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  311. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  312. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  313. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  314. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  315. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  316. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  317. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  318. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  319. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  320. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  321. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  322. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  323. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  324. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  325. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  326. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  327. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  328. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  329. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  330. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  331. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  332. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  333. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  334. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  335. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  336. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  337. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  338. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  339. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  340. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  341. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  342. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
  343. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  344. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  345. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  346. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  347. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  348. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  349. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  350. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  351. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  352. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  353. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  354. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  355. package/web/dist/index.html +17 -17
  356. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
  357. package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
  358. package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
@@ -1,334 +1,334 @@
1
- #!/usr/bin/env python3
2
- """rembg-service — long-running FastAPI service over Unix socket.
3
-
4
- server/index.js spawn 这个进程在自己启动时;自己 shutdown 时 SIGTERM 它。
5
- 所有 onnxruntime session 在内存里 warm 缓存,避免 per-call cold spawn 税
6
- (每次省 ~20-40s)。
7
-
8
- 启动顺序(2026-05-11 重构):
9
- 1. bind Unix socket(立刻 ready,~1s)
10
- 2. uvicorn startup event 起后台 _preload(models) task 异步预热
11
- 3. /remove 请求路径:未预热则同步 load(per-model asyncio.Lock 防并发重 load)
12
- 避免老版"先 preload → 再 bind socket"路径在 birefnet+CoreML 卡死时
13
- socket 永不创建的死锁。
14
-
15
- Endpoints:
16
- GET /health
17
- 返回 {"ok": true, "loaded_models": [...], "preload_done": bool}(也用作 isAvailable 探活)
18
-
19
- POST /remove
20
- Headers:
21
- X-Model: <rembg model name> default 'isnet-general-use'(安全档)
22
- X-Alpha-Matting: 0|1 default 0
23
- Body: raw image bytes (PNG/JPEG/WEBP/...)
24
- Returns: RGBA PNG bytes (200) or JSON error (4xx/5xx)
25
-
26
- Env:
27
- NODESIGN_REMBG_SOCKET Unix socket path (default /tmp/nodesign-rembg.sock)
28
- NODESIGN_REMBG_PRELOAD 逗号分隔的 model 列表,启动时异步预加载
29
- 例:'isnet-general-use,birefnet-general-lite'
30
- NODESIGN_REMBG_PROVIDERS 逗号分隔 onnxruntime providers 覆盖默认。
31
- 未设时:darwin 默认 'CPUExecutionProvider' 单一
32
- 项绕过 CoreML+birefnet 卡死 bug(onnxruntime
33
- 1.19.2 验证),其它平台传 None 让 ort 自选
34
- (Linux CUDA EP / Windows DML 等)。
35
- NODESIGN_REMBG_AM_MAX_DIM alpha matting 的分辨率上限(长边 px,默认 1024)。
36
- 见下方 _remove_with_am_cap 的长注释。0 = 不限制。
37
- """
38
- import asyncio
39
- import atexit
40
- import ctypes
41
- import ctypes.util
42
- import gc
43
- import io
44
- import os
45
- import sys
46
- import warnings
47
-
48
- # urllib3 v2 + LibreSSL(Apple python)只是警告,silence 防污染 stderr
49
- warnings.filterwarnings('ignore')
50
-
51
- from fastapi import FastAPI, HTTPException, Request
52
- from fastapi.responses import Response
53
- from PIL import Image
54
- import onnxruntime as ort
55
- from rembg import remove, new_session
56
-
57
-
58
- def _make_malloc_trim():
59
- """glibc 的 malloc_trim(0):把 free 出来但还挂在堆上的内存还给内核。
60
-
61
- 只对 glibc 有效(这台是 Debian,有)。musl / macOS 拿不到符号就返回一个空
62
- 函数,调用处不用关心平台差异。
63
- """
64
- try:
65
- libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
66
- trim = libc.malloc_trim
67
- trim.argtypes = [ctypes.c_size_t]
68
- trim.restype = ctypes.c_int
69
- return lambda: trim(0)
70
- except Exception:
71
- return lambda: None
72
-
73
-
74
- _malloc_trim = _make_malloc_trim()
75
-
76
-
77
- def _resolve_providers():
78
- """决定传给 rembg new_session 的 providers 列表。
79
-
80
- darwin(Apple Silicon Mac)必须排除 CoreMLExecutionProvider:
81
- onnxruntime 1.19.2 在 birefnet-general-lite 上 CoreML graph compile
82
- 卡死(>110s 未返回;CPU-only providers 2.5s 完成)。
83
-
84
- Linux / Windows 走 None = ort 自选,保留 CUDA EP / DML 等加速。
85
- Env NODESIGN_REMBG_PROVIDERS 覆盖一切(escape hatch + ort 升级后复测用)。
86
-
87
- 将来 onnxruntime 升级时重测:
88
- .venv-rembg/bin/python3 -c "from rembg import new_session; \\
89
- import time; t=time.time(); new_session('birefnet-general-lite'); \\
90
- print(time.time()-t)"
91
- < 30s 返回即可考虑放开 darwin 默认。
92
- """
93
- env = os.environ.get('NODESIGN_REMBG_PROVIDERS', '').strip()
94
- if env:
95
- return [p.strip() for p in env.split(',') if p.strip()]
96
- if sys.platform == 'darwin':
97
- return ['CPUExecutionProvider']
98
- return None # ort 自选
99
-
100
-
101
- PROVIDERS = _resolve_providers()
102
-
103
- app = FastAPI()
104
-
105
- # 模型 session 缓存:model name → onnxruntime session
106
- sessions = {}
107
- # per-model asyncio.Lock 防并发同一 model 重复 new_session(preload + 第一个
108
- # /remove 撞车场景)。dict 默认 lazy 建 Lock。
109
- _session_locks: dict = {}
110
- # preload 任务完成与否(仅 /health 报告用)
111
- _preload_done = False
112
-
113
-
114
- def _get_lock(model_name: str) -> asyncio.Lock:
115
- if model_name not in _session_locks:
116
- _session_locks[model_name] = asyncio.Lock()
117
- return _session_locks[model_name]
118
-
119
-
120
- def _mem_tuned_options():
121
- """省内存优先的 onnxruntime SessionOptions。
122
-
123
- 起因:birefnet-general-lite 只有 214MB 权重,但一次 1.57MP 推理的峰值 RSS
124
- 到 2.4GB,在 3.9G 无 swap 的机器上直接被 OOM killer 杀掉(2026-07-29 两次、
125
- 07-31 复现一次,dmesg 有记录)。杀的是这个常驻进程,连带 fast 档一起死,
126
- 所以整档被 env 禁掉了。
127
-
128
- 钱花在哪:ort 默认开 CPU arena 分配器,它按需成倍扩张且**不还给系统**,
129
- 对 birefnet 这种 transformer backbone(大量临时激活)峰值会翻好几倍。
130
- enable_cpu_mem_arena=False —— 用完即还,峰值大幅下降,代价是分配器调用
131
- 多一点(单核上这点开销远小于被 OOM 杀掉)
132
- enable_mem_pattern=False —— 内存复用模式表本身也要预分配一大块
133
- intra/inter_op=1 —— 这台机器就 1 核,多线程只是每条线程再占一份
134
- 临时缓冲,一点也不快
135
-
136
- isnet 那档本来就只占 997MB,加上这些也不会变慢到哪去,统一走同一份配置。
137
- """
138
- opts = ort.SessionOptions()
139
- opts.enable_cpu_mem_arena = False
140
- opts.enable_mem_pattern = False
141
- opts.intra_op_num_threads = int(os.environ.get('NODESIGN_REMBG_THREADS', '1'))
142
- opts.inter_op_num_threads = 1
143
- return opts
144
-
145
-
146
- def _load_session_sync(model_name: str):
147
- """同步 load——锁内调用。onnxruntime new_session 是阻塞 CPU 工作,
148
- 用 asyncio.to_thread 在外层包裹。"""
149
- if model_name not in sessions:
150
- sessions[model_name] = new_session(
151
- model_name, providers=PROVIDERS, sess_opts=_mem_tuned_options(),
152
- )
153
- return sessions[model_name]
154
-
155
-
156
- async def ensure_session(model_name: str):
157
- """异步获取 session:已 load 直接返;未 load 锁内同步 load 然后返。"""
158
- if model_name in sessions:
159
- return sessions[model_name]
160
- async with _get_lock(model_name):
161
- # double-check:另一 task 可能已经 load 完
162
- if model_name in sessions:
163
- return sessions[model_name]
164
- return await asyncio.to_thread(_load_session_sync, model_name)
165
-
166
-
167
- async def _preload(models):
168
- """启动后台预热——串行 load 防同时撑爆内存。失败单 model 不影响整体。"""
169
- global _preload_done
170
- for m in models:
171
- print(f'[rembg-service] preloading {m}...', file=sys.stderr, flush=True)
172
- try:
173
- await ensure_session(m)
174
- print(f'[rembg-service] ✓ {m} ready', file=sys.stderr, flush=True)
175
- except Exception as e:
176
- print(f'[rembg-service] ✗ {m} failed: {type(e).__name__}: {e}',
177
- file=sys.stderr, flush=True)
178
- _preload_done = True
179
- print(f'[rembg-service] preload done, loaded={sorted(sessions.keys())}',
180
- file=sys.stderr, flush=True)
181
-
182
-
183
- @app.on_event('startup')
184
- async def _on_startup():
185
- preload = os.environ.get('NODESIGN_REMBG_PRELOAD', '').strip()
186
- if not preload:
187
- return
188
- models = [s.strip() for s in preload.split(',') if s.strip()]
189
- if models:
190
- asyncio.create_task(_preload(models))
191
-
192
-
193
- # 在途请求数。launcher 的 RSS 看门狗只在这个数为 0 时才敢回收本进程,
194
- # 否则会把用户正在等的那次抠图打断。
195
- _inflight = 0
196
-
197
-
198
- def _self_rss_mb():
199
- """本进程 RSS(MB)。读 /proc 而不是引 psutil:少一个依赖,Linux 上够用。"""
200
- try:
201
- with open('/proc/self/status') as f:
202
- for line in f:
203
- if line.startswith('VmRSS:'):
204
- return round(int(line.split()[1]) / 1024)
205
- except OSError:
206
- pass
207
- return None
208
-
209
-
210
- @app.get('/health')
211
- def health():
212
- return {
213
- 'ok': True,
214
- 'loaded_models': sorted(sessions.keys()),
215
- 'preload_done': _preload_done,
216
- # 下面两个给 launcher 的看门狗用(见 rembg-launcher.js)
217
- 'inflight': _inflight,
218
- 'rss_mb': _self_rss_mb(),
219
- }
220
-
221
-
222
- AM_MAX_DIM = int(os.environ.get('NODESIGN_REMBG_AM_MAX_DIM', '1024'))
223
-
224
-
225
- def _remove_with_am_cap(image_bytes, session, alpha_matting):
226
- """抠图。开 alpha matting 时把 AM 那一步限制在 AM_MAX_DIM 之内。
227
-
228
- alpha matting 走 pymatting,它对 W×H 的图建一个 (W·H)×(W·H) 的稀疏拉普拉斯
229
- 矩阵再解方程组,内存和时间都随**像素数**走。这里的生图动辄 1.6MP 到 6.5MP,
230
- 不限制的话解算既慢又吃内存,原来 "fast 加 AM" 试过一次没通就是卡在这。
231
-
232
- 限制之后:AM 在 ≤1024 长边上算(≤1MP),算出来的 alpha 再 LANCZOS 放大回原
233
- 尺寸,跟**原分辨率的 RGB** 合成。输出仍是全分辨率,只有边缘的柔和过渡是在低
234
- 分辨率上解出来再插值的。代价是边缘比全分辨率 AM 略"雾"一点;收益是这一档在
235
- 这台机器上第一次变得可用。
236
-
237
- ⚠️ 这个上限**不是** birefnet 档 OOM 的解药,别把两件事记混。2026-07-31 实测:
238
- birefnet-general-lite 在 alpha_matting=0 的情况下,一张 1.57MP 的图峰值 RSS
239
- 就到 2435MB,照样被 OOM killer 杀掉(可用内存 2234MB)。撑爆的是模型推理本身,
240
- 跟 AM 无关。ort 的 arena/mem_pattern 调优只把 fast 从 997MB 压到 892MB,对
241
- birefnet 那 2.4GB 基本没动。birefnet 档要能用,得先给这台机器加 swap。
242
-
243
- 不开 AM 时原样走 rembg,一个像素都不动。
244
- """
245
- if not alpha_matting:
246
- return remove(image_bytes, session=session, alpha_matting=False)
247
-
248
- src = Image.open(io.BytesIO(image_bytes))
249
- src.load()
250
- w, h = src.size
251
- long_edge = max(w, h)
252
-
253
- if AM_MAX_DIM <= 0 or long_edge <= AM_MAX_DIM:
254
- return remove(image_bytes, session=session, alpha_matting=True)
255
-
256
- scale = AM_MAX_DIM / long_edge
257
- small = src.convert('RGB').resize(
258
- (max(1, round(w * scale)), max(1, round(h * scale))), Image.LANCZOS,
259
- )
260
- buf = io.BytesIO()
261
- small.save(buf, format='PNG')
262
- cut_small = remove(buf.getvalue(), session=session, alpha_matting=True)
263
-
264
- alpha = Image.open(io.BytesIO(cut_small)).convert('RGBA').getchannel('A')
265
- alpha = alpha.resize((w, h), Image.LANCZOS)
266
-
267
- out = src.convert('RGB')
268
- out.putalpha(alpha)
269
- result = io.BytesIO()
270
- out.save(result, format='PNG')
271
- return result.getvalue()
272
-
273
-
274
- @app.post('/remove')
275
- async def remove_endpoint(request: Request):
276
- global _inflight
277
- # 缺省换成 isnet(2026-08-02):原来的 birefnet-general-lite 是个陷阱 ——
278
- # 不带 X-Model 的直连调用会加载 2.4GB 峰值的模型,在这台机器上直接 OOM
279
- # 连坐整个 service(当天真踩过)。工具层永远显式传 model,缺省只服务于
280
- # 手工调试,就该是安全档。
281
- model = request.headers.get('x-model', 'isnet-general-use')
282
- alpha_matting = request.headers.get('x-alpha-matting', '0') == '1'
283
- image_bytes = await request.body()
284
- if not image_bytes:
285
- raise HTTPException(400, detail='empty body')
286
- _inflight += 1
287
- try:
288
- session = await ensure_session(model)
289
- # rembg remove 是 CPU 密集——丢线程池防阻塞 event loop
290
- rgba = await asyncio.to_thread(
291
- _remove_with_am_cap, image_bytes, session, alpha_matting,
292
- )
293
- return Response(content=rgba, media_type='image/png')
294
- except Exception as e:
295
- print(f'[rembg-service] /remove error: {type(e).__name__}: {e}',
296
- file=sys.stderr, flush=True)
297
- raise HTTPException(500, detail=f'{type(e).__name__}: {e}')
298
- finally:
299
- _inflight -= 1
300
- # 主动还内存:pymatting 的稀疏矩阵和 PIL 的全分辨率图都是大块 numpy /
301
- # buffer,Python 的分配器不一定马上还给系统。gc 一次 + malloc_trim 把
302
- # 空闲堆顶还回去,能明显压低 RSS 高水位(2026-07-31 事故就是高水位不降
303
- # 一路把机器推进内存抖动)。拿不到 malloc_trim 的平台静默跳过。
304
- gc.collect()
305
- _malloc_trim()
306
-
307
-
308
- def cleanup_socket(socket_path: str):
309
- """退出时删 socket file,方便下次启动重新 bind。"""
310
- try:
311
- if os.path.exists(socket_path):
312
- os.unlink(socket_path)
313
- print(f'[rembg-service] cleaned up {socket_path}', file=sys.stderr, flush=True)
314
- except OSError:
315
- pass
316
-
317
-
318
- def main():
319
- socket_path = os.environ.get('NODESIGN_REMBG_SOCKET', '/tmp/nodesign-rembg.sock')
320
- # bind 前清掉旧 socket(上次没干净退出会留残骸;SIGKILL 不走 atexit)
321
- cleanup_socket(socket_path)
322
- atexit.register(cleanup_socket, socket_path)
323
-
324
- providers_label = ','.join(PROVIDERS) if PROVIDERS else '<ort-default>'
325
- print(f'[rembg-service] providers={providers_label}', file=sys.stderr, flush=True)
326
- print(f'[rembg-service] listening on {socket_path}', file=sys.stderr, flush=True)
327
-
328
- import uvicorn
329
- # log_level=warning 静音 uvicorn access logs(每次请求一行很吵)
330
- uvicorn.run(app, uds=socket_path, log_level='warning', access_log=False)
331
-
332
-
333
- if __name__ == '__main__':
334
- main()
1
+ #!/usr/bin/env python3
2
+ """rembg-service — long-running FastAPI service over Unix socket.
3
+
4
+ server/index.js spawn 这个进程在自己启动时;自己 shutdown 时 SIGTERM 它。
5
+ 所有 onnxruntime session 在内存里 warm 缓存,避免 per-call cold spawn 税
6
+ (每次省 ~20-40s)。
7
+
8
+ 启动顺序(2026-05-11 重构):
9
+ 1. bind Unix socket(立刻 ready,~1s)
10
+ 2. uvicorn startup event 起后台 _preload(models) task 异步预热
11
+ 3. /remove 请求路径:未预热则同步 load(per-model asyncio.Lock 防并发重 load)
12
+ 避免老版"先 preload → 再 bind socket"路径在 birefnet+CoreML 卡死时
13
+ socket 永不创建的死锁。
14
+
15
+ Endpoints:
16
+ GET /health
17
+ 返回 {"ok": true, "loaded_models": [...], "preload_done": bool}(也用作 isAvailable 探活)
18
+
19
+ POST /remove
20
+ Headers:
21
+ X-Model: <rembg model name> default 'isnet-general-use'(安全档)
22
+ X-Alpha-Matting: 0|1 default 0
23
+ Body: raw image bytes (PNG/JPEG/WEBP/...)
24
+ Returns: RGBA PNG bytes (200) or JSON error (4xx/5xx)
25
+
26
+ Env:
27
+ NODESIGN_REMBG_SOCKET Unix socket path (default /tmp/nodesign-rembg.sock)
28
+ NODESIGN_REMBG_PRELOAD 逗号分隔的 model 列表,启动时异步预加载
29
+ 例:'isnet-general-use,birefnet-general-lite'
30
+ NODESIGN_REMBG_PROVIDERS 逗号分隔 onnxruntime providers 覆盖默认。
31
+ 未设时:darwin 默认 'CPUExecutionProvider' 单一
32
+ 项绕过 CoreML+birefnet 卡死 bug(onnxruntime
33
+ 1.19.2 验证),其它平台传 None 让 ort 自选
34
+ (Linux CUDA EP / Windows DML 等)。
35
+ NODESIGN_REMBG_AM_MAX_DIM alpha matting 的分辨率上限(长边 px,默认 1024)。
36
+ 见下方 _remove_with_am_cap 的长注释。0 = 不限制。
37
+ """
38
+ import asyncio
39
+ import atexit
40
+ import ctypes
41
+ import ctypes.util
42
+ import gc
43
+ import io
44
+ import os
45
+ import sys
46
+ import warnings
47
+
48
+ # urllib3 v2 + LibreSSL(Apple python)只是警告,silence 防污染 stderr
49
+ warnings.filterwarnings('ignore')
50
+
51
+ from fastapi import FastAPI, HTTPException, Request
52
+ from fastapi.responses import Response
53
+ from PIL import Image
54
+ import onnxruntime as ort
55
+ from rembg import remove, new_session
56
+
57
+
58
+ def _make_malloc_trim():
59
+ """glibc 的 malloc_trim(0):把 free 出来但还挂在堆上的内存还给内核。
60
+
61
+ 只对 glibc 有效(这台是 Debian,有)。musl / macOS 拿不到符号就返回一个空
62
+ 函数,调用处不用关心平台差异。
63
+ """
64
+ try:
65
+ libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
66
+ trim = libc.malloc_trim
67
+ trim.argtypes = [ctypes.c_size_t]
68
+ trim.restype = ctypes.c_int
69
+ return lambda: trim(0)
70
+ except Exception:
71
+ return lambda: None
72
+
73
+
74
+ _malloc_trim = _make_malloc_trim()
75
+
76
+
77
+ def _resolve_providers():
78
+ """决定传给 rembg new_session 的 providers 列表。
79
+
80
+ darwin(Apple Silicon Mac)必须排除 CoreMLExecutionProvider:
81
+ onnxruntime 1.19.2 在 birefnet-general-lite 上 CoreML graph compile
82
+ 卡死(>110s 未返回;CPU-only providers 2.5s 完成)。
83
+
84
+ Linux / Windows 走 None = ort 自选,保留 CUDA EP / DML 等加速。
85
+ Env NODESIGN_REMBG_PROVIDERS 覆盖一切(escape hatch + ort 升级后复测用)。
86
+
87
+ 将来 onnxruntime 升级时重测:
88
+ .venv-rembg/bin/python3 -c "from rembg import new_session; \\
89
+ import time; t=time.time(); new_session('birefnet-general-lite'); \\
90
+ print(time.time()-t)"
91
+ < 30s 返回即可考虑放开 darwin 默认。
92
+ """
93
+ env = os.environ.get('NODESIGN_REMBG_PROVIDERS', '').strip()
94
+ if env:
95
+ return [p.strip() for p in env.split(',') if p.strip()]
96
+ if sys.platform == 'darwin':
97
+ return ['CPUExecutionProvider']
98
+ return None # ort 自选
99
+
100
+
101
+ PROVIDERS = _resolve_providers()
102
+
103
+ app = FastAPI()
104
+
105
+ # 模型 session 缓存:model name → onnxruntime session
106
+ sessions = {}
107
+ # per-model asyncio.Lock 防并发同一 model 重复 new_session(preload + 第一个
108
+ # /remove 撞车场景)。dict 默认 lazy 建 Lock。
109
+ _session_locks: dict = {}
110
+ # preload 任务完成与否(仅 /health 报告用)
111
+ _preload_done = False
112
+
113
+
114
+ def _get_lock(model_name: str) -> asyncio.Lock:
115
+ if model_name not in _session_locks:
116
+ _session_locks[model_name] = asyncio.Lock()
117
+ return _session_locks[model_name]
118
+
119
+
120
+ def _mem_tuned_options():
121
+ """省内存优先的 onnxruntime SessionOptions。
122
+
123
+ 起因:birefnet-general-lite 只有 214MB 权重,但一次 1.57MP 推理的峰值 RSS
124
+ 到 2.4GB,在 3.9G 无 swap 的机器上直接被 OOM killer 杀掉(2026-07-29 两次、
125
+ 07-31 复现一次,dmesg 有记录)。杀的是这个常驻进程,连带 fast 档一起死,
126
+ 所以整档被 env 禁掉了。
127
+
128
+ 钱花在哪:ort 默认开 CPU arena 分配器,它按需成倍扩张且**不还给系统**,
129
+ 对 birefnet 这种 transformer backbone(大量临时激活)峰值会翻好几倍。
130
+ enable_cpu_mem_arena=False —— 用完即还,峰值大幅下降,代价是分配器调用
131
+ 多一点(单核上这点开销远小于被 OOM 杀掉)
132
+ enable_mem_pattern=False —— 内存复用模式表本身也要预分配一大块
133
+ intra/inter_op=1 —— 这台机器就 1 核,多线程只是每条线程再占一份
134
+ 临时缓冲,一点也不快
135
+
136
+ isnet 那档本来就只占 997MB,加上这些也不会变慢到哪去,统一走同一份配置。
137
+ """
138
+ opts = ort.SessionOptions()
139
+ opts.enable_cpu_mem_arena = False
140
+ opts.enable_mem_pattern = False
141
+ opts.intra_op_num_threads = int(os.environ.get('NODESIGN_REMBG_THREADS', '1'))
142
+ opts.inter_op_num_threads = 1
143
+ return opts
144
+
145
+
146
+ def _load_session_sync(model_name: str):
147
+ """同步 load——锁内调用。onnxruntime new_session 是阻塞 CPU 工作,
148
+ 用 asyncio.to_thread 在外层包裹。"""
149
+ if model_name not in sessions:
150
+ sessions[model_name] = new_session(
151
+ model_name, providers=PROVIDERS, sess_opts=_mem_tuned_options(),
152
+ )
153
+ return sessions[model_name]
154
+
155
+
156
+ async def ensure_session(model_name: str):
157
+ """异步获取 session:已 load 直接返;未 load 锁内同步 load 然后返。"""
158
+ if model_name in sessions:
159
+ return sessions[model_name]
160
+ async with _get_lock(model_name):
161
+ # double-check:另一 task 可能已经 load 完
162
+ if model_name in sessions:
163
+ return sessions[model_name]
164
+ return await asyncio.to_thread(_load_session_sync, model_name)
165
+
166
+
167
+ async def _preload(models):
168
+ """启动后台预热——串行 load 防同时撑爆内存。失败单 model 不影响整体。"""
169
+ global _preload_done
170
+ for m in models:
171
+ print(f'[rembg-service] preloading {m}...', file=sys.stderr, flush=True)
172
+ try:
173
+ await ensure_session(m)
174
+ print(f'[rembg-service] ✓ {m} ready', file=sys.stderr, flush=True)
175
+ except Exception as e:
176
+ print(f'[rembg-service] ✗ {m} failed: {type(e).__name__}: {e}',
177
+ file=sys.stderr, flush=True)
178
+ _preload_done = True
179
+ print(f'[rembg-service] preload done, loaded={sorted(sessions.keys())}',
180
+ file=sys.stderr, flush=True)
181
+
182
+
183
+ @app.on_event('startup')
184
+ async def _on_startup():
185
+ preload = os.environ.get('NODESIGN_REMBG_PRELOAD', '').strip()
186
+ if not preload:
187
+ return
188
+ models = [s.strip() for s in preload.split(',') if s.strip()]
189
+ if models:
190
+ asyncio.create_task(_preload(models))
191
+
192
+
193
+ # 在途请求数。launcher 的 RSS 看门狗只在这个数为 0 时才敢回收本进程,
194
+ # 否则会把用户正在等的那次抠图打断。
195
+ _inflight = 0
196
+
197
+
198
+ def _self_rss_mb():
199
+ """本进程 RSS(MB)。读 /proc 而不是引 psutil:少一个依赖,Linux 上够用。"""
200
+ try:
201
+ with open('/proc/self/status') as f:
202
+ for line in f:
203
+ if line.startswith('VmRSS:'):
204
+ return round(int(line.split()[1]) / 1024)
205
+ except OSError:
206
+ pass
207
+ return None
208
+
209
+
210
+ @app.get('/health')
211
+ def health():
212
+ return {
213
+ 'ok': True,
214
+ 'loaded_models': sorted(sessions.keys()),
215
+ 'preload_done': _preload_done,
216
+ # 下面两个给 launcher 的看门狗用(见 rembg-launcher.js)
217
+ 'inflight': _inflight,
218
+ 'rss_mb': _self_rss_mb(),
219
+ }
220
+
221
+
222
+ AM_MAX_DIM = int(os.environ.get('NODESIGN_REMBG_AM_MAX_DIM', '1024'))
223
+
224
+
225
+ def _remove_with_am_cap(image_bytes, session, alpha_matting):
226
+ """抠图。开 alpha matting 时把 AM 那一步限制在 AM_MAX_DIM 之内。
227
+
228
+ alpha matting 走 pymatting,它对 W×H 的图建一个 (W·H)×(W·H) 的稀疏拉普拉斯
229
+ 矩阵再解方程组,内存和时间都随**像素数**走。这里的生图动辄 1.6MP 到 6.5MP,
230
+ 不限制的话解算既慢又吃内存,原来 "fast 加 AM" 试过一次没通就是卡在这。
231
+
232
+ 限制之后:AM 在 ≤1024 长边上算(≤1MP),算出来的 alpha 再 LANCZOS 放大回原
233
+ 尺寸,跟**原分辨率的 RGB** 合成。输出仍是全分辨率,只有边缘的柔和过渡是在低
234
+ 分辨率上解出来再插值的。代价是边缘比全分辨率 AM 略"雾"一点;收益是这一档在
235
+ 这台机器上第一次变得可用。
236
+
237
+ ⚠️ 这个上限**不是** birefnet 档 OOM 的解药,别把两件事记混。2026-07-31 实测:
238
+ birefnet-general-lite 在 alpha_matting=0 的情况下,一张 1.57MP 的图峰值 RSS
239
+ 就到 2435MB,照样被 OOM killer 杀掉(可用内存 2234MB)。撑爆的是模型推理本身,
240
+ 跟 AM 无关。ort 的 arena/mem_pattern 调优只把 fast 从 997MB 压到 892MB,对
241
+ birefnet 那 2.4GB 基本没动。birefnet 档要能用,得先给这台机器加 swap。
242
+
243
+ 不开 AM 时原样走 rembg,一个像素都不动。
244
+ """
245
+ if not alpha_matting:
246
+ return remove(image_bytes, session=session, alpha_matting=False)
247
+
248
+ src = Image.open(io.BytesIO(image_bytes))
249
+ src.load()
250
+ w, h = src.size
251
+ long_edge = max(w, h)
252
+
253
+ if AM_MAX_DIM <= 0 or long_edge <= AM_MAX_DIM:
254
+ return remove(image_bytes, session=session, alpha_matting=True)
255
+
256
+ scale = AM_MAX_DIM / long_edge
257
+ small = src.convert('RGB').resize(
258
+ (max(1, round(w * scale)), max(1, round(h * scale))), Image.LANCZOS,
259
+ )
260
+ buf = io.BytesIO()
261
+ small.save(buf, format='PNG')
262
+ cut_small = remove(buf.getvalue(), session=session, alpha_matting=True)
263
+
264
+ alpha = Image.open(io.BytesIO(cut_small)).convert('RGBA').getchannel('A')
265
+ alpha = alpha.resize((w, h), Image.LANCZOS)
266
+
267
+ out = src.convert('RGB')
268
+ out.putalpha(alpha)
269
+ result = io.BytesIO()
270
+ out.save(result, format='PNG')
271
+ return result.getvalue()
272
+
273
+
274
+ @app.post('/remove')
275
+ async def remove_endpoint(request: Request):
276
+ global _inflight
277
+ # 缺省换成 isnet(2026-08-02):原来的 birefnet-general-lite 是个陷阱 ——
278
+ # 不带 X-Model 的直连调用会加载 2.4GB 峰值的模型,在这台机器上直接 OOM
279
+ # 连坐整个 service(当天真踩过)。工具层永远显式传 model,缺省只服务于
280
+ # 手工调试,就该是安全档。
281
+ model = request.headers.get('x-model', 'isnet-general-use')
282
+ alpha_matting = request.headers.get('x-alpha-matting', '0') == '1'
283
+ image_bytes = await request.body()
284
+ if not image_bytes:
285
+ raise HTTPException(400, detail='empty body')
286
+ _inflight += 1
287
+ try:
288
+ session = await ensure_session(model)
289
+ # rembg remove 是 CPU 密集——丢线程池防阻塞 event loop
290
+ rgba = await asyncio.to_thread(
291
+ _remove_with_am_cap, image_bytes, session, alpha_matting,
292
+ )
293
+ return Response(content=rgba, media_type='image/png')
294
+ except Exception as e:
295
+ print(f'[rembg-service] /remove error: {type(e).__name__}: {e}',
296
+ file=sys.stderr, flush=True)
297
+ raise HTTPException(500, detail=f'{type(e).__name__}: {e}')
298
+ finally:
299
+ _inflight -= 1
300
+ # 主动还内存:pymatting 的稀疏矩阵和 PIL 的全分辨率图都是大块 numpy /
301
+ # buffer,Python 的分配器不一定马上还给系统。gc 一次 + malloc_trim 把
302
+ # 空闲堆顶还回去,能明显压低 RSS 高水位(2026-07-31 事故就是高水位不降
303
+ # 一路把机器推进内存抖动)。拿不到 malloc_trim 的平台静默跳过。
304
+ gc.collect()
305
+ _malloc_trim()
306
+
307
+
308
+ def cleanup_socket(socket_path: str):
309
+ """退出时删 socket file,方便下次启动重新 bind。"""
310
+ try:
311
+ if os.path.exists(socket_path):
312
+ os.unlink(socket_path)
313
+ print(f'[rembg-service] cleaned up {socket_path}', file=sys.stderr, flush=True)
314
+ except OSError:
315
+ pass
316
+
317
+
318
+ def main():
319
+ socket_path = os.environ.get('NODESIGN_REMBG_SOCKET', '/tmp/nodesign-rembg.sock')
320
+ # bind 前清掉旧 socket(上次没干净退出会留残骸;SIGKILL 不走 atexit)
321
+ cleanup_socket(socket_path)
322
+ atexit.register(cleanup_socket, socket_path)
323
+
324
+ providers_label = ','.join(PROVIDERS) if PROVIDERS else '<ort-default>'
325
+ print(f'[rembg-service] providers={providers_label}', file=sys.stderr, flush=True)
326
+ print(f'[rembg-service] listening on {socket_path}', file=sys.stderr, flush=True)
327
+
328
+ import uvicorn
329
+ # log_level=warning 静音 uvicorn access logs(每次请求一行很吵)
330
+ uvicorn.run(app, uds=socket_path, log_level='warning', access_log=False)
331
+
332
+
333
+ if __name__ == '__main__':
334
+ main()