@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.
- package/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +42 -22
- package/server/api/assets/notes.js +194 -144
- package/server/api/assets.js +891 -896
- package/server/api/board.js +134 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +465 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1035 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +351 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +152 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/edit-sketch.js +167 -0
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/look-at-board.js +124 -0
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +190 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/read-user-view.js +59 -0
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +112 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/sketch-on-board.js +337 -0
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/mcp/tools/write-on-board.js +130 -0
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
- 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
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/board-tasklist.js +130 -0
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/index.js +3 -0
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +72 -60
- package/server/lib/board-graph-export.js +191 -0
- package/server/lib/board-groups.js +167 -0
- package/server/lib/board-hero.js +76 -0
- package/server/lib/board-kind-sizes.js +85 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/chalk.js +121 -0
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/sketch-layout.js +277 -0
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-limits.js +12 -0
- package/server/projects/board-sanitize.js +252 -0
- package/server/projects/board-store.js +507 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +60 -54
- package/server/projects/viewpoint-store.js +78 -0
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +39 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
- package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
- package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
- package/web/dist/assets/arc-D_khH_xN.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
- package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
- package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
- package/web/dist/assets/channel-DGkccMO1.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
- package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
- package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
- package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
- package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
- package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
- package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
- package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
- package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
- package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
- package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
- package/web/dist/assets/index-GAamh1p4.js +2152 -0
- package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
- package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
- package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
- package/web/dist/assets/layout-CnXiF_NB.js +1 -0
- package/web/dist/assets/linear-BMW0u6G6.js +1 -0
- package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
- package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
- package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
- package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
- package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
- package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
- package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
- package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
- 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()
|