From 57e98165127835c1e0f1ed97dff5313b3827832e Mon Sep 17 00:00:00 2001 From: midcodehub <408124546@qq.com> Date: Wed, 8 Jul 2026 11:47:31 +0800 Subject: [PATCH 01/15] feat: add batch processing system, GPU infrastructure, and pricing page while updating asset versions and project configuration --- .beamignore | 30 + .claude/logs/tool-audit.jsonl | 141 + .claude/settings.local.json | 17 +- .gitignore | 11 + Dockerfile | 26 +- Dockerfile.openvino | 117 + account/credits/index.html | 182 + alternatives/remove-bg/index.html | 2 +- app.js | 91 +- batch/batch.js | 310 ++ batch/index.html | 430 ++ bn/id-photo-maker/index.html | 2 +- bn/index.html | 1797 +++++---- bn/jpg-to-transparent-png/index.html | 2 +- bn/old-photo-restoration/index.html | 2 +- bn/png-to-jpg-white-background/index.html | 2 +- bn/portrait-background-remover/index.html | 2 +- .../index.html | 2 +- bn/watermark-remover/index.html | 2 +- feedback.js | 2 +- fil/id-photo-maker/index.html | 2 +- fil/index.html | 1797 +++++---- fil/jpg-to-transparent-png/index.html | 2 +- fil/old-photo-restoration/index.html | 2 +- fil/png-to-jpg-white-background/index.html | 2 +- fil/portrait-background-remover/index.html | 2 +- .../index.html | 2 +- fil/watermark-remover/index.html | 2 +- gpu-scheduler/package-lock.json | 1618 ++++++++ gpu-scheduler/package.json | 18 + gpu-scheduler/src/index.ts | 215 + gpu-scheduler/tsconfig.json | 21 + gpu-scheduler/wrangler.toml | 25 + hi/id-photo-maker/index.html | 2 +- hi/index.html | 1797 +++++---- hi/jpg-to-transparent-png/index.html | 2 +- hi/old-photo-restoration/index.html | 2 +- hi/png-to-jpg-white-background/index.html | 2 +- hi/portrait-background-remover/index.html | 2 +- .../index.html | 2 +- hi/watermark-remover/index.html | 2 +- how-to-remove-background/gimp/index.html | 2 +- how-to-remove-background/index.html | 2 +- .../powerpoint/index.html | 2 +- id-photo-maker/id-photo.js | 4 +- id-photo-maker/index.html | 2 +- id/id-photo-maker/index.html | 2 +- id/index.html | 1797 +++++---- id/jpg-to-transparent-png/index.html | 2 +- id/old-photo-restoration/index.html | 2 +- id/png-to-jpg-white-background/index.html | 2 +- id/portrait-background-remover/index.html | 2 +- .../index.html | 2 +- id/watermark-remover/index.html | 2 +- index.html | 1950 ++++----- jpg-to-transparent-png/index.html | 2 +- language-menu.js | 2 +- main.py | 287 +- miaocut-seo-design-doc.md | 3521 ----------------- old-photo-restoration-design.md | 68 - old-photo-restoration/index.html | 2 +- old-photo-restoration/old-photo.js | 15 +- output.css | 2 +- png-to-jpg-white-background/index.html | 2 +- png-to-jpg-white-background/png-to-jpg.js | 10 +- portrait-background-remover/index.html | 2 +- pricing/index.html | 129 + privacy/index.html | 2 +- pro-account.js | 177 + pro-gateway/README.md | 99 + pro-gateway/db/schema-batch.sql | 230 ++ pro-gateway/db/schema.sql | 286 ++ pro-gateway/setup-worker-secrets.sh | 72 + pro-gateway/src/index.js | 1003 +++++ pro-gateway/test/smoke-batch.mjs | 141 + pro-gateway/test/smoke.mjs | 190 + pro-gateway/wrangler.toml | 62 + pro-gpu/.beamignore | 27 + pro-gpu/README.md | 58 + pro-gpu/app.py | 536 +++ pro-gpu/cleanup.py | 13 + pro-gpu/estimate_foreground_ml_cupy.py | 232 ++ pro-gpu/kaggle_app.py | 277 ++ pro-gpu/setup-beam.sh | 31 + pro-gpu/test_beam.py | 12 + product-photo-background-remover/index.html | 2 +- pt-br/id-photo-maker/index.html | 2 +- pt-br/index.html | 1797 +++++---- pt-br/jpg-to-transparent-png/index.html | 2 +- pt-br/old-photo-restoration/index.html | 2 +- pt-br/png-to-jpg-white-background/index.html | 2 +- pt-br/portrait-background-remover/index.html | 2 +- .../index.html | 2 +- pt-br/watermark-remover/index.html | 2 +- quantize_birefnet.ipynb | 246 -- scripts/cloudflare-hf-gateway-worker.js | 176 +- secret.txt | 1 + sitemap.xml | 140 +- src/input.css | 18 + terms/index.html | 2 +- ur/id-photo-maker/index.html | 2 +- ur/index.html | 1797 +++++---- ur/jpg-to-transparent-png/index.html | 2 +- ur/old-photo-restoration/index.html | 2 +- ur/png-to-jpg-white-background/index.html | 2 +- ur/portrait-background-remover/index.html | 2 +- .../index.html | 2 +- ur/watermark-remover/index.html | 2 +- watermark-remover/index.html | 2 +- watermark-remover/watermark.js | 8 +- zh/id-photo-maker/index.html | 2 +- zh/index.html | 1797 +++++---- zh/jpg-to-transparent-png/index.html | 2 +- zh/old-photo-restoration/index.html | 2 +- zh/png-to-jpg-white-background/index.html | 2 +- zh/portrait-background-remover/index.html | 2 +- .../index.html | 2 +- zh/watermark-remover/index.html | 2 +- 118 files changed, 15100 insertions(+), 10880 deletions(-) create mode 100644 .beamignore create mode 100644 Dockerfile.openvino create mode 100644 account/credits/index.html create mode 100644 batch/batch.js create mode 100644 batch/index.html create mode 100644 gpu-scheduler/package-lock.json create mode 100644 gpu-scheduler/package.json create mode 100644 gpu-scheduler/src/index.ts create mode 100644 gpu-scheduler/tsconfig.json create mode 100644 gpu-scheduler/wrangler.toml delete mode 100644 miaocut-seo-design-doc.md delete mode 100644 old-photo-restoration-design.md create mode 100644 pricing/index.html create mode 100644 pro-account.js create mode 100644 pro-gateway/README.md create mode 100644 pro-gateway/db/schema-batch.sql create mode 100644 pro-gateway/db/schema.sql create mode 100755 pro-gateway/setup-worker-secrets.sh create mode 100644 pro-gateway/src/index.js create mode 100644 pro-gateway/test/smoke-batch.mjs create mode 100644 pro-gateway/test/smoke.mjs create mode 100644 pro-gateway/wrangler.toml create mode 100644 pro-gpu/.beamignore create mode 100644 pro-gpu/README.md create mode 100644 pro-gpu/app.py create mode 100644 pro-gpu/cleanup.py create mode 100644 pro-gpu/estimate_foreground_ml_cupy.py create mode 100644 pro-gpu/kaggle_app.py create mode 100755 pro-gpu/setup-beam.sh create mode 100644 pro-gpu/test_beam.py delete mode 100644 quantize_birefnet.ipynb create mode 100644 secret.txt diff --git a/.beamignore b/.beamignore new file mode 100644 index 0000000..8ae11be --- /dev/null +++ b/.beamignore @@ -0,0 +1,30 @@ +# Generated by Beam SDK +.beamignore +.git +.idea +.python-version +.vscode +.venv +venv +__pycache__ +.DS_Store +.config +drive/MyDrive +.coverage +.pytest_cache +.ipynb +.ruff_cache +.dockerignore +.ipynb_checkpoints +.env.local +.envrc +**/__pycache__/ +**/.pytest_cache/ +**/node_modules/ +**/.venv/ +*.pyc +.next/ +.circleci +.venv-py39-backup +models + diff --git a/.claude/logs/tool-audit.jsonl b/.claude/logs/tool-audit.jsonl index 3128b26..772bc90 100644 --- a/.claude/logs/tool-audit.jsonl +++ b/.claude/logs/tool-audit.jsonl @@ -709,3 +709,144 @@ {"ts": "2026-06-23T05:20:43Z", "session": "c9cd0dfe-ec58-45b6-9b0e-1dd462a0867e", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} {"ts": "2026-06-23T05:20:50Z", "session": "c9cd0dfe-ec58-45b6-9b0e-1dd462a0867e", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} {"ts": "2026-06-23T05:20:56Z", "session": "c9cd0dfe-ec58-45b6-9b0e-1dd462a0867e", "tool": "Bash", "input_summary": {"command": "{ [ -d .venv ] && source .venv/bin/activate 2>/dev/null; } ; python -c \"compile(open('main.py').read(),'main.py','exec'); print('compile OK')\" && grep -n \"_arena_run_options_for_now\\|_ARENA_SHRINK_RUN_OPTIONS\" main.py"}, "ok": null} +{"ts": "2026-06-23T15:43:52Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro.md"}, "ok": null} +{"ts": "2026-06-23T15:43:53Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-design.md"}, "ok": null} +{"ts": "2026-06-23T15:43:53Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-phase1.md"}, "ok": null} +{"ts": "2026-06-23T15:44:05Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-23T15:44:06Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-23T16:09:28Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "AskUserQuestion", "input_summary": {"questions": [{"question": "这台 GPU 服务器你打算怎么托管?这决定网络/安全/成本模型。", "header": "GPU 宿主", "options": [{"label": "云按需租用", "description": "Runpod / Lambda / Vast.ai 等。弹性、单价低、无需自己运维硬件,用完即停省钱。CF Tunnel 接入最干净。适合先跑起来。"}, {"label": "自建硬件", "description": "家里/机房放 4090/3090 等。长期单位成本最低,一次性投入低,但要自己管电源/散热/网络/可用性,CF Tunnel 暴露。"}, {"label": "大厂云 GPU", "description": "AWS/GCP/阿里云 GPU 实例。稳定、合规、计费透明,但单价明显贵,适合规模上来后。"}], "multiSelect": false}, {"question": "GPU 服务接管哪些流量?这决定成本和 HF Spaces 的去留。", "header": "服务范围", "options": [{"label": "只接管付费路径", "description": "单图 HD + 批量走 GPU;免费匿名单图继续留 HF CPU 当 SEO 漏斗。成本可控,与双网关方案吻合。推荐。"}, {"label": "全部迁到 GPU", "description": "含免费匿名路径都上 GPU,质量统一拉满,HF 仅作故障转移。体验最好但 GPU 成本随免费流量线性增长。"}, {"label": "先只做批量", "description": "GPU 暂时只跑批量付费任务,单图(免费+登录)维持现状。改动面最小,先验证批量变现。"}], "multiSelect": false}, {"question": "批量任务怎么和 GPU 服务对接?这是一个真实的架构分叉。", "header": "批量模式", "options": [{"label": "GPU 端拉取 + 自管 R2", "description": "GPU 服务从队列拉任务、自己读写 R2,在 GPU 端做动态批处理。吞吐最高、最能打满显卡。推荐(批量的核心价值)。"}, {"label": "网关推送给 GPU", "description": "Pro 网关/Worker 把图推给 GPU 的 /batch 接口,GPU 现取现做。实现最简单,MVP 友好,但批处理效率略低。"}, {"label": "本阶段不做批量", "description": "先只把单图同步链路(秒级 HD)在 GPU 上打通,批量后置。"}], "multiSelect": false}], "answers": {"这台 GPU 服务器你打算怎么托管?这决定网络/安全/成本模型。": "云按需租用", "GPU 服务接管哪些流量?这决定成本和 HF Spaces 的去留。": "只接管付费路径", "批量任务怎么和 GPU 服务对接?这是一个真实的架构分叉。": "这个问题我们再深入讨论一下,我的想法是能否和现在的方式一样,直接通过网关上传到gpu服务器,让服务器处理解压和批量处理,它和上传到R2上再拉取下来批量处理对比为什么会效率低呢,主要在哪会效率低?"}}, "ok": null} +{"ts": "2026-06-23T17:09:52Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/private/tmp/gen_nb.py"}, "ok": null} +{"ts": "2026-06-23T17:11:02Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "python3 /private/tmp/gen_nb.py && python3 -c \"import json,sys; nb=json.load(open('benchmark/miaocut_gpu_benchmark.ipynb')); print('valid json, nbformat', nb['nbformat'], 'cells', len(nb['cells']))\""}, "ok": null} +{"ts": "2026-06-23T17:11:21Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "python3 - <<'PY'\nimport json\nnb = json.load(open('benchmark/miaocut_gpu_benchmark.ipynb'))\nimport ast\nok = True\nfor i, c in enumerate(nb['cells']):\n if c['cell_type'] != 'code':\n continue\n src = ''.join(c['source'])\n # 去掉 Colab 的 ! shell 行再做语法检查(这些不是合法 Python)\n lines = [l for l in src.splitlines() if not l.lstrip().startswith('!')]\n code = '\\n'.join(lines)\n try:\n ast.parse(code)\n except SyntaxError as e:\n ok = False\n print(f'cell {i} SyntaxError: "}, "ok": null} +{"ts": "2026-06-23T17:11:50Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/benchmark/README.md"}, "ok": null} +{"ts": "2026-06-23T18:15:35Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/private/tmp/gen_nb.py"}, "ok": null} +{"ts": "2026-06-23T18:15:42Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/private/tmp/gen_nb.py"}, "ok": null} +{"ts": "2026-06-23T18:15:47Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/private/tmp/gen_nb.py"}, "ok": null} +{"ts": "2026-06-23T18:15:54Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/private/tmp/gen_nb.py"}, "ok": null} +{"ts": "2026-06-23T18:16:05Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/private/tmp/gen_nb.py"}, "ok": null} +{"ts": "2026-06-23T18:16:12Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "python3 /private/tmp/gen_nb.py && python3 - <<'PY'\nimport json, ast\nnb = json.load(open('benchmark/miaocut_gpu_benchmark.ipynb'))\nok = True\nfor i, c in enumerate(nb['cells']):\n if c['cell_type'] != 'code': continue\n code = '\\n'.join(l for l in ''.join(c['source']).splitlines() if not l.lstrip().startswith('!'))\n try: ast.parse(code)\n except SyntaxError as e: ok = False; print(f'cell {i} SyntaxError: {e}')\nprint('valid json + ALL CODE CELLS OK' if ok else 'SYNTAX ERRORS')\nPY"}, "ok": null} +{"ts": "2026-06-23T18:44:18Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/private/tmp/gen_nb.py"}, "ok": null} +{"ts": "2026-06-23T18:44:25Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "python3 /private/tmp/gen_nb.py && python3 - <<'PY'\nimport json, ast\nnb = json.load(open('benchmark/miaocut_gpu_benchmark.ipynb'))\nok = True\nfor i, c in enumerate(nb['cells']):\n if c['cell_type'] != 'code': continue\n code = '\\n'.join(l for l in ''.join(c['source']).splitlines() if not l.lstrip().startswith('!'))\n try: ast.parse(code)\n except SyntaxError as e: ok = False; print(f'cell {i} SyntaxError: {e}')\nprint('valid + ALL CODE CELLS OK' if ok else 'SYNTAX ERRORS')\nPY"}, "ok": null} +{"ts": "2026-06-23T19:03:16Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/private/tmp/gen_nb.py"}, "ok": null} +{"ts": "2026-06-23T19:03:25Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "python3 /private/tmp/gen_nb.py && python3 -c \"import json,ast; nb=json.load(open('benchmark/miaocut_gpu_benchmark.ipynb')); [ast.parse('\\n'.join(l for l in ''.join(c['source']).splitlines() if not l.lstrip().startswith('!'))) for c in nb['cells'] if c['cell_type']=='code']; print('valid + code OK')\""}, "ok": null} +{"ts": "2026-06-23T19:25:32Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/private/tmp/gen_nb.py"}, "ok": null} +{"ts": "2026-06-23T19:25:38Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/MEMORY.md"}, "ok": null} +{"ts": "2026-06-23T19:25:50Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-deploy-arch.md"}, "ok": null} +{"ts": "2026-06-23T19:26:00Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-model-choice.md"}, "ok": null} +{"ts": "2026-06-23T19:26:08Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/MEMORY.md"}, "ok": null} +{"ts": "2026-06-23T19:26:18Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "python3 /private/tmp/gen_nb.py && python3 -c \"import json,ast; nb=json.load(open('benchmark/miaocut_gpu_benchmark.ipynb')); [ast.parse('\\n'.join(l for l in ''.join(c['source']).splitlines() if not l.lstrip().startswith('!'))) for c in nb['cells'] if c['cell_type']=='code']; print('notebook 已更新默认配置为 matting,校验通过')\""}, "ok": null} +{"ts": "2026-06-23T19:53:49Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-model-choice.md"}, "ok": null} +{"ts": "2026-06-23T19:54:56Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-23T19:58:23Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "AskUserQuestion", "input_summary": {"questions": [{"question": "serverless GPU 用哪个平台?这决定部署脚本、冷启动方式、计费粒度。", "header": "serverless 平台", "options": [{"label": "Modal", "description": "Python 原生、内存快照让冷启动很快、按秒计费、部署体验最好。适合 FastAPI + 自定义后处理这种场景,我最推荐。"}, {"label": "Runpod Serverless", "description": "GPU 型号选择多、单价低、社区成熟。冷启动比 Modal 略逊,但更省钱。"}, {"label": "Replicate", "description": "把模型打包成 predictor 最省心,但灵活性低(自定义 FastAPI/后处理受限)、单价偏高。"}], "multiSelect": false}, {"question": "缩容策略怎么定?这是省钱和首请求延迟的权衡。", "header": "缩容策略", "options": [{"label": "纯 scale-to-zero", "description": "闲置缩到 0,零常驻成本;首请求要等冷启动(数秒)。早期付费量稀疏,先这样最省钱,体验不行再调。推荐起步。"}, {"label": "matting 保温 + HR 懒加载", "description": "fast 档(小模型)保 1 个常温实例,低延迟;fur 档(HR)容忍冷启动。折中方案。"}, {"label": "保 1 个常温实例", "description": "首请求都低延迟,但有常驻成本(约 $300+/月,取决于卡)。付费量起来后再考虑。"}], "multiSelect": false}, {"question": "GPU 服务第一版接口范围做到哪?", "header": "接口范围", "options": [{"label": "仅透明 PNG", "description": "fast/fur 两档 + 去彩边,输出透明 PNG。最快上线,背景合成/多格式留 Phase 2。推荐先跑通闭环。"}, {"label": "加纯色背景合成", "description": "多一个 bg 参数支持白底/纯色底(电商常用),id-photo 已有服务端合成可复用。接口面稍大。"}, {"label": "背景合成 + 多格式", "description": "再加 webp/jpg 白底输出。接口最全但上线最慢。"}], "multiSelect": false}], "answers": {"serverless GPU 用哪个平台?这决定部署脚本、冷启动方式、计费粒度。": "Beam", "缩容策略怎么定?这是省钱和首请求延迟的权衡。": "纯 scale-to-zero", "GPU 服务第一版接口范围做到哪?": "仅透明 PNG"}}, "ok": null} +{"ts": "2026-06-23T20:01:52Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-23T20:02:10Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-deploy-arch.md"}, "ok": null} +{"ts": "2026-06-23T20:11:58Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-23T20:12:11Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-23T20:12:53Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gpu/app.py"}, "ok": null} +{"ts": "2026-06-23T20:13:19Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gpu/README.md"}, "ok": null} +{"ts": "2026-06-23T20:13:28Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-deploy-arch.md"}, "ok": null} +{"ts": "2026-06-23T20:13:38Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "python3 -m py_compile pro-gpu/app.py && echo \"py_compile OK: pro-gpu/app.py 语法通过\""}, "ok": null} +{"ts": "2026-06-24T02:47:26Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-24T02:47:45Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-deploy-arch.md"}, "ok": null} +{"ts": "2026-06-24T02:52:28Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-24T02:52:44Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-deploy-arch.md"}, "ok": null} +{"ts": "2026-06-24T02:56:39Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/src/index.js"}, "ok": null} +{"ts": "2026-06-24T02:56:40Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/wrangler.toml"}, "ok": null} +{"ts": "2026-06-24T02:59:18Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/README.md"}, "ok": null} +{"ts": "2026-06-24T03:00:56Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/src/index.js"}, "ok": null} +{"ts": "2026-06-24T03:01:17Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/wrangler.toml"}, "ok": null} +{"ts": "2026-06-24T03:01:48Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/README.md"}, "ok": null} +{"ts": "2026-06-24T03:02:32Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "node --check pro-gateway/src/index.js && echo \"node --check OK: pro-gateway/src/index.js\""}, "ok": null} +{"ts": "2026-06-24T03:14:23Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-account.js"}, "ok": null} +{"ts": "2026-06-24T03:14:24Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "node --version; echo \"---\"; grep -n \"API_BASE\\|api2.miaocut\\|XMLHttpRequest\\|remove-background\" /Users/wuchao/git/miaoCut/app.js | head -40"}, "ok": null} +{"ts": "2026-06-24T03:16:38Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-account.js"}, "ok": null} +{"ts": "2026-06-24T03:17:19Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/test/smoke.mjs"}, "ok": null} +{"ts": "2026-06-24T03:17:32Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "node pro-gateway/test/smoke.mjs"}, "ok": null} +{"ts": "2026-06-24T03:19:35Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/app.js"}, "ok": null} +{"ts": "2026-06-24T03:19:36Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/app.js"}, "ok": null} +{"ts": "2026-06-24T03:19:48Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "grep -n \"uploadWithProgress\\|currentProfile\\s*=\\|err\\.status\\|=== 429\\|== 429\\|function handleFile\\|processFile\\|fileInput\\|addEventListener('change'\\|openWaitlistModal('rateDay'\\|MiaoCutPro\\|pro-account\" app.js | head -50"}, "ok": null} +{"ts": "2026-06-24T03:19:58Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/app.js"}, "ok": null} +{"ts": "2026-06-24T03:19:59Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/app.js"}, "ok": null} +{"ts": "2026-06-24T03:22:35Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/app.js"}, "ok": null} +{"ts": "2026-06-24T03:22:45Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/app.js"}, "ok": null} +{"ts": "2026-06-24T03:22:54Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/app.js"}, "ok": null} +{"ts": "2026-06-24T03:23:02Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "node --check app.js && echo \"app.js 语法 OK\"; echo \"---\"; grep -n \"app\\.js\\|pro-account\\|MIAOCUT_PRO_CONFIG\\|\\|&1 | tail -20; echo \"=== 改动文件 ===\"; git status --short | head -30"}, "ok": null} +{"ts": "2026-06-24T03:24:20Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "echo \"=== zh/index.html 是否含 pro-account ===\"; grep -c \"pro-account.js\\|MIAOCUT_PRO_CONFIG\" zh/index.html; echo \"=== sitemap 是否实质变化 ===\"; git diff --stat sitemap.xml; echo \"=== 本地化首页改动行数(应只增脚本) ===\"; git diff --stat zh/index.html bn/index.html"}, "ok": null} +{"ts": "2026-06-24T05:27:18Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "ToolSearch", "input_summary": {"query": "select:WebSearch,WebFetch", "max_results": 5}, "ok": null} +{"ts": "2026-06-24T05:27:43Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "WebSearch", "input_summary": {"query": "visitor to paid conversion rate website freemium tool percentage benchmark"}, "ok": null} +{"ts": "2026-06-24T05:27:44Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "WebSearch", "input_summary": {"query": "freemium free to paid conversion rate benchmark 2025 SaaS consumer app median"}, "ok": null} +{"ts": "2026-06-25T04:14:03Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gpu/app.py"}, "ok": null} +{"ts": "2026-06-25T04:14:14Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "python3 -m py_compile pro-gpu/app.py && echo \"py_compile OK: pro-gpu/app.py(含 @task_queue 批量函数)语法通过\""}, "ok": null} +{"ts": "2026-06-25T04:14:36Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-deploy-arch.md"}, "ok": null} +{"ts": "2026-06-25T04:15:00Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-deploy-arch.md"}, "ok": null} +{"ts": "2026-06-25T04:15:13Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-deploy-arch.md"}, "ok": null} +{"ts": "2026-06-25T04:15:22Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/.claude/projects/-Users-wuchao-git-miaoCut/memory/pro-gpu-deploy-arch.md"}, "ok": null} +{"ts": "2026-06-25T04:15:33Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-25T04:16:05Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-25T04:16:14Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-25T04:16:34Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-25T04:16:47Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-gpu-service.md"}, "ok": null} +{"ts": "2026-06-25T04:33:51Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Bash", "input_summary": {"command": "cd /Users/wuchao/git/miaoCut\necho \"===== 找 wrangler / worker 配置文件 =====\"\nfind . -maxdepth 3 \\( -iname \"wrangler.toml\" -o -iname \"wrangler.json\" -o -iname \"wrangler.jsonc\" \\) 2>/dev/null | grep -v node_modules\necho \"\"\necho \"===== 仓库里任何提到 RATE_LIMITER / ratelimit binding 的地方 =====\"\ngrep -rniE \"RATE_LIMITER|ratelimit|\\[\\[ratelimits\\]\\]|kv_namespace|namespace_id\" . --include=\"*.toml\" --include=\"*.json\" --include=\"*.jsonc\" 2>/dev/null | grep -v node_modules | head -20\necho \"\"\necho \"(若上面都为空,说明 binding"}, "ok": null} +{"ts": "2026-06-25T04:35:07Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/scripts/cloudflare-hf-gateway-worker.js"}, "ok": null} +{"ts": "2026-06-25T04:35:17Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/scripts/cloudflare-hf-gateway-worker.js"}, "ok": null} +{"ts": "2026-06-25T04:35:30Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/scripts/cloudflare-hf-gateway-worker.js"}, "ok": null} +{"ts": "2026-06-25T04:35:40Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Bash", "input_summary": {"command": "cd /Users/wuchao/git/miaoCut\nnode --check scripts/cloudflare-hf-gateway-worker.js && echo \"Worker JS 语法 OK\"\necho \"\"\necho \"===== pro-gateway/wrangler.toml 里有没有 ratelimit/kv binding(确认它是另一个 worker) =====\"\ngrep -niE \"name|ratelimit|kv_namespace|binding|main\" pro-gateway/wrangler.toml 2>/dev/null | head"}, "ok": null} +{"ts": "2026-06-25T04:50:13Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/scripts/cloudflare-hf-gateway-worker.js"}, "ok": null} +{"ts": "2026-06-25T04:50:21Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/scripts/cloudflare-hf-gateway-worker.js"}, "ok": null} +{"ts": "2026-06-25T04:50:51Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/scripts/cloudflare-hf-gateway-worker.js"}, "ok": null} +{"ts": "2026-06-25T04:50:58Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Bash", "input_summary": {"command": "cd /Users/wuchao/git/miaoCut\nnode --check scripts/cloudflare-hf-gateway-worker.js && echo \"Worker JS 语法 OK\"\necho \"\"\necho \"===== 边缘限流那段现在长这样(确认回到官方等价写法) =====\"\nsed -n '14,45p' scripts/cloudflare-hf-gateway-worker.js"}, "ok": null} +{"ts": "2026-06-25T05:04:10Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/scripts/cloudflare-hf-gateway-worker.js"}, "ok": null} +{"ts": "2026-06-25T05:07:41Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Bash", "input_summary": {"command": "cd /Users/wuchao/git/miaoCut\necho \"========== pro-gateway/ 文件树 ==========\"\nfind pro-gateway -type f -not -path '*/node_modules/*' -not -path '*/.git/*' | sort\necho \"\"\necho \"========== pro-gpu/ 文件树 ==========\"\nfind pro-gpu -type f -not -path '*/node_modules/*' -not -path '*/.git/*' | sort\necho \"\"\necho \"========== docs/feature-pro* 文档 ==========\"\nls -la docs/feature-pro*.md 2>/dev/null\necho \"\"\necho \"========== 各文件行数概览 ==========\"\nwc -l pro-gateway/**/*.* pro-gpu/**/*.* 2>/dev/null | tail -30"}, "ok": null} +{"ts": "2026-06-25T05:08:07Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro.md"}, "ok": null} +{"ts": "2026-06-25T05:08:20Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/README.md"}, "ok": null} +{"ts": "2026-06-25T05:08:33Z", "session": "a16e301f-73ed-4c83-ab43-9247848abc4f", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gpu/README.md"}, "ok": null} +{"ts": "2026-06-25T14:51:03Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Bash", "input_summary": {"command": "grep -n \"429\\|limiter\\|limit(\\|semaphore\\|Semaphore\\|RateLimit\\|TooManyRequests\\|503\\|acquire\\|Retry-After\\|retry_after\" main.py"}, "ok": null} +{"ts": "2026-06-25T14:51:10Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:51:14Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:51:18Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:51:22Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:51:26Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:56:41Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Bash", "input_summary": {"command": "grep -n \"expose_headers\\|CORSMiddleware\\|allow_headers\\|X-RateLimit-Scope\\|add_middleware\" main.py"}, "ok": null} +{"ts": "2026-06-25T14:56:45Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:57:49Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:57:53Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Bash", "input_summary": {"command": "grep -n \"id-photo/create\\|id-photo/human-matting\\|async def create_id_photo\\|async def human_matting\\|file.read\\|await image.read\\|await file.read\\|run_id_photo\\|run_rembg(\" main.py | head -50"}, "ok": null} +{"ts": "2026-06-25T14:58:02Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:58:14Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:58:27Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:58:39Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:58:43Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:59:13Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:59:22Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:59:26Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:59:31Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:59:41Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T14:59:49Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T15:00:11Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T15:00:20Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T15:00:29Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T15:00:41Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T15:00:48Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/main.py"}, "ok": null} +{"ts": "2026-06-25T15:01:06Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Bash", "input_summary": {"command": "python -m py_compile main.py && echo \"OK: py_compile passed\""}, "ok": null} +{"ts": "2026-06-25T15:01:09Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Bash", "input_summary": {"command": "grep -n \"reject_if_saturated\\|_saturated_429\\|X-Reason\\|expose_headers\" main.py"}, "ok": null} +{"ts": "2026-06-25T15:01:24Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Bash", "input_summary": {"command": "python - <<'PY'\nimport asyncio\n\nasync def main():\n sem = asyncio.Semaphore(1)\n print(\"初期 locked():\", sem.locked()) # False(空きあり)\n await sem.acquire()\n print(\"acquire 後 locked():\", sem.locked()) # True(満載)→ 予検でここ即429\n sem.release()\n print(\"release 後 locked():\", sem.locked()) # False\n\n # MAX_CONCURRENCY=2 のとき、1枠埋まっても満載ではない\n sem2 = asyncio.Semaphore(2)\n await sem2.acquire()\n print(\"Semaphore(2) 1枠取得後 locked():\", sem2.locked()) # False(まだ空きあり=予検は通す)\n "}, "ok": null} +{"ts": "2026-06-25T15:01:33Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Bash", "input_summary": {"command": "git diff --stat main.py && echo \"---\" && git diff main.py | grep -E \"^\\+\" | grep -v \"^\\+\\+\\+\" | head -80"}, "ok": null} +{"ts": "2026-06-25T15:01:39Z", "session": "64950bfd-f3fc-4676-81c2-5af4e58bb133", "tool": "Bash", "input_summary": {"command": "git --no-pager diff main.py"}, "ok": null} +{"ts": "2026-06-25T15:09:18Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Read", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/db/schema.sql"}, "ok": null} +{"ts": "2026-06-25T15:11:56Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/db/schema-batch.sql"}, "ok": null} +{"ts": "2026-06-25T15:12:38Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gpu/app.py"}, "ok": null} +{"ts": "2026-06-25T15:13:51Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/src/index.js"}, "ok": null} +{"ts": "2026-06-25T15:14:01Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/src/index.js"}, "ok": null} +{"ts": "2026-06-25T15:14:55Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/src/index.js"}, "ok": null} +{"ts": "2026-06-25T15:15:34Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/wrangler.toml"}, "ok": null} +{"ts": "2026-06-25T15:15:45Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/wrangler.toml"}, "ok": null} +{"ts": "2026-06-25T15:15:55Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "node --check pro-gateway/src/index.js && echo \"✅ index.js 语法 OK\"; python3 -m py_compile pro-gpu/app.py && echo \"✅ app.py 语法 OK\"; echo \"--- 重跑 Phase 1 冒烟 ---\"; node pro-gateway/test/smoke.mjs 2>&1 | tail -5"}, "ok": null} +{"ts": "2026-06-25T15:17:28Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/pro-gateway/test/smoke-batch.mjs"}, "ok": null} +{"ts": "2026-06-25T15:18:55Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "node pro-gateway/test/smoke-batch.mjs 2>&1 | grep -v \"MODULE_TYPELESS\\|Reparsing\\|eliminate this\\|trace-warnings\""}, "ok": null} +{"ts": "2026-06-25T15:25:57Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/batch/index.html"}, "ok": null} +{"ts": "2026-06-25T15:27:27Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/batch/batch.js"}, "ok": null} +{"ts": "2026-06-25T15:27:45Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Bash", "input_summary": {"command": "node --check batch/batch.js 2>&1 | grep -v \"MODULE_TYPELESS\\|Reparsing\\|eliminate\\|trace-warnings\" ; node --check pro-account.js 2>&1 | grep -v \"MODULE_TYPELESS\\|Reparsing\\|eliminate\\|trace-warnings\" ; echo \"✅ batch.js + pro-account.js 语法 OK(无报错即通过)\""}, "ok": null} +{"ts": "2026-06-25T15:34:58Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Write", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-deploy.md"}, "ok": null} +{"ts": "2026-06-26T00:30:39Z", "session": "9a9f9a71-c626-4931-8a66-055a0b605f5a", "tool": "Edit", "input_summary": {"file": "/Users/wuchao/git/miaoCut/docs/feature-pro-deploy.md"}, "ok": null} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 62b7ca4..7d30814 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -46,7 +46,22 @@ "Bash(node --check app.js)", "Bash(grep -nA8 \"content\" tailwind.config.js)", "Bash(grep -vE \"^ ?M \\(bn|fil|hi|id|pt-br|ur|zh|ja|ko|fr|de|es|vi|th|ar|ru|tr|it|pl|nl|fa\\)/|how-to-remove|/index\\\\.html$\")", - "Bash(grep -E \"\\\\.\\(py|js|css|md|json\\)$|scripts/\")" + "Bash(grep -E \"\\\\.\\(py|js|css|md|json\\)$|scripts/\")", + "Bash(python3 /private/tmp/gen_nb.py)", + "Bash(python3 -c \"import json,sys; nb=json.load\\(open\\('benchmark/miaocut_gpu_benchmark.ipynb'\\)\\); print\\('valid json, nbformat', nb['nbformat'], 'cells', len\\(nb['cells']\\)\\)\")", + "Bash(python3 -)", + "Bash(python3 -c \"import json,ast; nb=json.load\\(open\\('benchmark/miaocut_gpu_benchmark.ipynb'\\)\\); [ast.parse\\('\\\\n'.join\\(l for l in ''.join\\(c['source']\\).splitlines\\(\\) if not l.lstrip\\(\\).startswith\\('!'\\)\\)\\) for c in nb['cells'] if c['cell_type']=='code']; print\\('valid + code OK'\\)\")", + "Bash(python3 -c \"import json,ast; nb=json.load\\(open\\('benchmark/miaocut_gpu_benchmark.ipynb'\\)\\); [ast.parse\\('\\\\n'.join\\(l for l in ''.join\\(c['source']\\).splitlines\\(\\) if not l.lstrip\\(\\).startswith\\('!'\\)\\)\\) for c in nb['cells'] if c['cell_type']=='code']; print\\('notebook 已更新默认配置为 matting,校验通过'\\)\")", + "Bash(python3 -m py_compile pro-gpu/app.py)", + "Bash(node --check pro-gateway/src/index.js)", + "Bash(node pro-gateway/test/smoke.mjs)", + "WebSearch", + "Bash(python -m py_compile main.py)", + "Bash(python -)", + "Bash(git --no-pager diff main.py)", + "Bash(node pro-gateway/test/smoke-batch.mjs)", + "Bash(node --check batch/batch.js)", + "Bash(node --check pro-account.js)" ] } } diff --git a/.gitignore b/.gitignore index fadfcc3..39d2acd 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,14 @@ models/*.bak .claude .agent .playwright-cli +ads.txt +b27e253973b44d85af293d4f007fba0c.txt +googlebbfb4abe1fc3d9df.html +miaocut-seo-design-doc.md +old-photo-restoration-design.md +quantize_birefnet.ipynb +docs/ +.claude/* +.agents/ +scripts/ +benchmark diff --git a/Dockerfile b/Dockerfile index 39dd087..5f34fc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,9 +50,18 @@ download_url_to_file('https://github.com/enesmsahin/simple-lama-inpainting/relea # ============================================================ FROM python:3.11-slim -# 系统依赖:MediaPipe/OpenCV 运行时需要 libGL/libglib。 +# 系统依赖: +# libgl1 / libglib2.0-0 —— MediaPipe / OpenCV 运行时依赖 +# libjemalloc2 —— 替换 glibc 的默认 malloc;用它主要有两个收益: +# 1) 内存碎片显著小于 glibc ptmalloc,同样负载下 RSS 峰值/steady-state 更低, +# 对 cgroup 内存受限(HF Space 2 vCPU, 16GB 硬顶)尤其有价值。 +# 2) dirty/muzzy decay 机制会真正把空闲页归还给 OS,不像 glibc 那样即使 malloc_trim +# 也常常留在进程里——这是我们过去靠 _malloc_trim + gc.collect 兜底的问题的更彻底解法。 +# ⚠️ 注意 jemalloc 只解"分配器 → OS"这一层;ORT 自己的 arena("ORT → 分配器")仍需靠 +# main.py 的 memory.enable_memory_arena_shrinkage 归还——两者互补,不要因为上了 jemalloc +# 就关掉 ENABLE_ARENA_SHRINKAGE,会让 sharp + fur 两个 session 的 arena 叠加冲顶 OOM。 RUN apt-get update && \ - apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && \ + apt-get install -y --no-install-recommends libgl1 libglib2.0-0 libjemalloc2 && \ useradd -m -u 1000 user && \ mkdir -p /home/user/app /data && \ chown -R user:user /home/user/app /data && \ @@ -91,10 +100,23 @@ COPY --chown=user:user models/birefnet-general-lite-768.onnx /opt/miaocut-models # 模型已内置到镜像;rembg 模型缺失时会自动退到可写缓存目录。 # big-lama(去水印 inpainting 模型)由上面 builder 预置到 /opt/miaocut-models/big-lama.pt, # 用 LAMA_MODEL 指定后 SimpleLama 直接加载本地文件,容器重启不再去 GitHub 重下 196MB。 +# +# ---- jemalloc 相关(见上文 libjemalloc2 系统依赖注释)---- +# LD_PRELOAD 让所有子进程(含 uvicorn 主进程、SimpleLama 的 ProcessPool worker)都跑 jemalloc。 +# MALLOC_CONF 是 jemalloc 的 tuning,取的是 PyTorch 官方 Docker 也在用的一组保守值: +# background_thread:true —— 后台线程做 purge,避免 malloc/free 热路径承担 decay 成本 +# metadata_thp:auto —— 对 jemalloc 自己的元数据用 THP,减少 TLB miss +# dirty_decay_ms:5000 —— 5s 内不再用的 dirty page 归还给 OS +# muzzy_decay_ms:5000 —— 同上,第二层 muzzy state(jemalloc 的 lazy purge 分层) +# 万一 jemalloc 在某个 Space 上表现异常,把这两行注掉重建镜像即可回退到 glibc; +# MALLOC_ARENA_MAX / MALLOC_TRIM_THRESHOLD_ 是 glibc 特有的 tuning,jemalloc 加载后被自动忽略, +# 保留是为了让"关掉 jemalloc 回退 glibc"的场景仍然拿到之前调过的 glibc 参数。 ENV PORT=7860 \ U2NET_HOME=/opt/miaocut-models \ LAMA_MODEL=/opt/miaocut-models/big-lama.pt \ DATA_DIR=/data \ + LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 \ + MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:5000,muzzy_decay_ms:5000" \ MALLOC_ARENA_MAX=2 \ MALLOC_TRIM_THRESHOLD_=131072 diff --git a/Dockerfile.openvino b/Dockerfile.openvino new file mode 100644 index 0000000..686c0a3 --- /dev/null +++ b/Dockerfile.openvino @@ -0,0 +1,117 @@ +# ============================================================ +# Dockerfile.openvino — OpenVINO EP variant (A/B image for one Space) +# ============================================================ +# 与主 Dockerfile 唯一的差异(两处): +# 1) builder 阶段:装完 requirements.txt 后,把 rembg[cpu] 顺带装进来的 onnxruntime 移除, +# 再装 onnxruntime-openvino。这个 wheel 里 bundle 了 OpenVINO 2025.4.1 CPU 内核, +# 同时保留标准 CPUExecutionProvider 作为兜底。 +# ⚠️ 不能把两个 wheel 共存:它们共享 top-level 模块名 `onnxruntime`,共存时 +# import 结果依赖安装顺序,行为不可预测——必须先 rm 掉旧的 site-packages 目录 +# 再装新的(pip uninstall 在 --prefix 模式下不工作,故用 rm)。 +# 2) 运行时 ENV 追加 USE_OPENVINO_EP=1,让 main.py 的 _resolve_providers() 首选 +# OpenVINOExecutionProvider,CPU EP 依然作为兜底(不可用时静默回退)。 +# +# 其余(jemalloc、模型预置、big-lama 预下载、其他 ENV 变量)与主 Dockerfile 完全一致。 +# +# ---- 构建 & 使用 ---- +# docker build -t miaocut-api-openvino -f Dockerfile.openvino . +# # Space 端:把 3 个 Space 中的一个(e.g. miao_cut3)换成这个镜像做灰度, +# # 另外两个继续跑默认 Dockerfile 做对照。 +# +# ---- 期望收益(待 Space 端验证)---- +# BiRefNet-lite(Swin Transformer 结构)在 Xeon 8375C(含 AVX-512 VNNI)上, +# OpenVINO CPU 内核对比 onnxruntime MLAS 通常快 1.5~3× at FP32。见 +# docs/cutout-perf-followups.md 的 §1 里的完整测量协议和决策规则。 +# +# ---- 验证 checklist(部署后第一件事)---- +# 看容器启动日志。main.py 的 _log_session_providers 会打印每个 session 用的 EP: +# Session 'sharp/birefnet-general-lite-768' providers: ['OpenVINOExecutionProvider', 'CPUExecutionProvider'] +# 如果看到 providers 只有 CPUExecutionProvider,说明 OpenVINO EP 未生效 +# (可能是 onnxruntime-openvino 装错了、USE_OPENVINO_EP=0 被下游覆盖了、或 EP 初始化失败), +# 参考 docs/cutout-perf-followups.md 的排错步骤。 +# +# ---- 回退 ---- +# 删除本文件、切回默认 Dockerfile 重建镜像即可。main.py 里没有任何 OpenVINO 专属代码—— +# `_resolve_providers()` 在 OpenVINO EP 不可用时自动回退到纯 CPU,完全对称。 +# ============================================================ + +FROM python:3.11-slim AS builder + +WORKDIR /build + +# 构建阶段也会 import rembg/mediapipe/OpenCV 来预下载模型;缺 libGL 时 python -c 报错。 +RUN apt-get update && \ + apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && \ + rm -rf /var/lib/apt/lists/* + +# 先装完常规依赖(复用主 Dockerfile 的 requirements.txt,避免依赖表分叉维护) +COPY requirements.txt . +RUN pip install --no-cache-dir --prefix=/install -r requirements.txt && \ + # rembg[cpu] 会连带装 onnxruntime;OpenVINO 变体不能与之共存(同一 top-level 模块名) + # pip uninstall 在 --prefix 模式下不工作,只能物理 rm site-packages 里的 onnxruntime 目录 + rm -rf /install/lib/python3.11/site-packages/onnxruntime* && \ + # 装 onnxruntime-openvino:wheel 里 bundle 了 OpenVINO 2025.4.1 CPU/GPU 库(Linux 单独 84MB wheel), + # 不需要在系统层单独装 openvino。pin 到 >=1.20,<2.0 覆盖当前 stable 且避免大版本跳跃。 + pip install --no-cache-dir --prefix=/install "onnxruntime-openvino>=1.20,<2.0" + +# 模型预下载:与主 Dockerfile 完全一致 +RUN mkdir -p /model-cache && \ + U2NET_HOME=/model-cache \ + PYTHONPATH=/install/lib/python3.11/site-packages \ + python -c "from rembg.sessions import sessions_class; \ +names=['isnet-general-use','birefnet-general-lite']; \ +[print(next(sc for sc in sessions_class if sc.name()==n).download_models()) for n in names]" + +# big-lama.pt(去水印 inpainting 模型,196MB)也预下载,避免容器重启后首个去水印请求触发 GitHub 下载 +RUN PYTHONPATH=/install/lib/python3.11/site-packages \ + python -c "from torch.hub import download_url_to_file; \ +download_url_to_file('https://github.com/enesmsahin/simple-lama-inpainting/releases/download/v0.1.0/big-lama.pt', '/model-cache/big-lama.pt')" + +# ============================================================ +FROM python:3.11-slim + +# 与主 Dockerfile 同一套 apt 依赖 + jemalloc(见主 Dockerfile 该行注释了解每个包的用途) +RUN apt-get update && \ + apt-get install -y --no-install-recommends libgl1 libglib2.0-0 libjemalloc2 && \ + useradd -m -u 1000 user && \ + mkdir -p /home/user/app /data && \ + chown -R user:user /home/user/app /data && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /home/user/app + +# 从 builder 拷贝已安装的 Python 包 +COPY --from=builder /install /usr/local +COPY --from=builder --chown=user:user /model-cache /opt/miaocut-models + +# 拷贝后端运行必需文件。前端由 Cloudflare Pages 托管,不放进 Space 镜像。 +COPY --chown=user:user main.py . + +# INT8 量化的 BiRefNet-lite 模型(详见主 Dockerfile 该行注释) +COPY --chown=user:user models/birefnet-general-lite-int8.onnx /opt/miaocut-models/ + +# fur 档 BiRefNet_lite-matting 模型(详见主 Dockerfile 该行注释) +COPY --chown=user:user models/birefnet-lite-matting.onnx /opt/miaocut-models/ + +# sharp 折中档 BiRefNet-general-lite @ 768² 模型(详见主 Dockerfile 该行注释) +COPY --chown=user:user models/birefnet-general-lite-768.onnx /opt/miaocut-models/ + +# ---- ENV:与主 Dockerfile 完全一致,只是最后加了 USE_OPENVINO_EP=1 ---- +# jemalloc / MALLOC_CONF 的详细解释见主 Dockerfile 同名 ENV 块。 +# USE_OPENVINO_EP=1 让 main.py 的 _resolve_providers() 首选 OpenVINOExecutionProvider, +# CPU EP 依然在 providers 列表末尾兜底——两者共存互不干扰。 +ENV PORT=7860 \ + U2NET_HOME=/opt/miaocut-models \ + LAMA_MODEL=/opt/miaocut-models/big-lama.pt \ + DATA_DIR=/data \ + LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 \ + MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:5000,muzzy_decay_ms:5000" \ + MALLOC_ARENA_MAX=2 \ + MALLOC_TRIM_THRESHOLD_=131072 \ + USE_OPENVINO_EP=1 + +USER user + +# 单 worker,不要加 --workers!(限流和并发控制在进程内存里) +EXPOSE 7860 +CMD ["python", "main.py"] diff --git a/account/credits/index.html b/account/credits/index.html new file mode 100644 index 0000000..7431d5c --- /dev/null +++ b/account/credits/index.html @@ -0,0 +1,182 @@ + + + + + + + + Your Credits | MiaoCut + + + + + + + +
+ +
+ +
+ + + + + + +
+ + + + + + + diff --git a/alternatives/remove-bg/index.html b/alternatives/remove-bg/index.html index 0db51f1..f07f749 100644 --- a/alternatives/remove-bg/index.html +++ b/alternatives/remove-bg/index.html @@ -97,7 +97,7 @@ ] } - + diff --git a/app.js b/app.js index 07ca966..6f05f8a 100644 --- a/app.js +++ b/app.js @@ -1101,25 +1101,12 @@ // 埋点 (Umami custom events) // ============================================================ // umami 脚本异步加载,未就绪/被广告拦截时直接 no-op,不让分析故障影响主流程。 - function track(name, data) { + function track(name) { if (typeof umami === 'undefined') return; try { - if (data) umami.track(name, data); else umami.track(name); + umami.track(name); } catch (_) { /* analytics 失败不影响业务 */ } } - function sizeBucket(bytes) { - if (bytes < 500 * 1024) return '<500K'; - if (bytes < 2 * 1024 * 1024) return '500K-2M'; - if (bytes < 5 * 1024 * 1024) return '2M-5M'; - return '5M-10M'; - } - function durationBucket(ms) { - const s = ms / 1000; - if (s < 2) return '<2s'; - if (s < 5) return '2-5s'; - if (s < 10) return '5-10s'; - return '>10s'; - } // ============================================================ // i18n 切换 @@ -1222,7 +1209,7 @@ const from = currentLang; const to = e.target.value; if (to === currentLang) return; - track('lang-switched', { from, to }); + track('lang-switched'); const target = alternateUrlFor(to) + window.location.search + window.location.hash; // 用 localStorage 记住偏好,下次直接打开根域名时可以让用户感知(虽然不再用作语言判定的源) localStorage.setItem('lang', to); @@ -1265,7 +1252,7 @@ currentProfile = next; localStorage.setItem('cutoutProfile', next); updateProfileButtons(); - track('profile-switched', { from, to: next }); + track('profile-switched'); }); }); updateProfileButtons(); @@ -1369,7 +1356,7 @@ modal.classList.remove('hidden'); modal.classList.add('flex'); document.body.classList.add('overflow-hidden'); - track('waitlist-opened', { page: window.MIAOCUT_PAGE_KEY || 'unknown', lang: currentLang, variant: currentVariant }); + track('waitlist-opened'); setTimeout(() => emailInput.focus(), 0); } openWaitlistModal = openModal; // 暴露给外部(抠图 429 调用) @@ -1392,20 +1379,20 @@ }); modal.querySelector('[data-batch-backdrop]').addEventListener('click', () => { - track('batch-door-dismissed', { page: window.MIAOCUT_PAGE_KEY || 'unknown', lang: currentLang, method: 'backdrop' }); + track('batch-door-dismissed'); closeModal(); }); closeButtons.forEach(button => { button.addEventListener('click', () => { - track('batch-door-dismissed', { page: window.MIAOCUT_PAGE_KEY || 'unknown', lang: currentLang, method: 'button' }); + track('batch-door-dismissed'); closeModal(); }); }); document.addEventListener('keydown', (event) => { if (event.key !== 'Escape' || modal.classList.contains('hidden')) return; - track('batch-door-dismissed', { page: window.MIAOCUT_PAGE_KEY || 'unknown', lang: currentLang, method: 'escape' }); + track('batch-door-dismissed'); closeModal(); }); @@ -1442,7 +1429,7 @@ }), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); - track('batch-waitlist-submitted', { page: window.MIAOCUT_PAGE_KEY || 'unknown', lang: currentLang }); + track('batch-waitlist-submitted'); formWrap.classList.add('hidden'); thanksEl.classList.remove('hidden'); const doneButton = modal.querySelector('#batch-thanks-close'); @@ -1450,7 +1437,7 @@ } catch (error) { console.warn('[MiaoCut] Batch waitlist submit failed:', error); setError(t('batchModalError')); - track('batch-waitlist-failed', { page: window.MIAOCUT_PAGE_KEY || 'unknown', lang: currentLang }); + track('batch-waitlist-failed'); } finally { submitButton.disabled = false; submitButton.textContent = t('batchModalSubmit'); @@ -1603,9 +1590,19 @@ function uploadWithProgress(formData) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); - const url = `${API_BASE}/api/remove-background?profile=${encodeURIComponent(currentProfile)}`; + // 登录用户走 Pro 网关(GPU,按 profile 扣分);匿名走免费网关(CPU,含 localhost dev)。 + // MiaoCutPro 未加载或未登录时,url 与鉴权头与原来完全一致,零回归。 + const pro = window.MiaoCutPro; + const loggedIn = !!(pro && pro.isLoggedIn && pro.isLoggedIn()); + const url = loggedIn + ? pro.removeBackgroundUrl(currentProfile) + : `${API_BASE}/api/remove-background?profile=${encodeURIComponent(currentProfile)}`; xhr.open('POST', url); xhr.responseType = 'blob'; + // 登录用户带 Authorization(Supabase JWT);匿名为空 + if (loggedIn && pro.authHeader) { + for (const [k, v] of Object.entries(pro.authHeader())) xhr.setRequestHeader(k, v); + } xhr.upload.onprogress = (e) => { if (!e.lengthComputable) return; @@ -1713,11 +1710,11 @@ document.body.removeChild(a); } - function autoDownloadTransparentResult(file, blob, page) { + function autoDownloadTransparentResult(file, blob) { const url = URL.createObjectURL(blob); downloadUrl(url, `${basenameFromFile(file)}_transparent.png`); setTimeout(() => URL.revokeObjectURL(url), 1000); - track('transparent-download', { page, mode: 'auto' }); + track('transparent-download'); } function bgLabelKey(bg) { @@ -1999,7 +1996,7 @@ const suffix = shapeOverride || resultState.config.downloadSuffix; downloadUrl(url, `${resultState.basename}_${suffix}.png`); setTimeout(() => URL.revokeObjectURL(url), 1000); - track('edited-download', { page: resultState.mode, bg: resultState.bg, size: resultState.size, shape: shapeOverride || resultState.shape }); + track('edited-download'); }, 'image/png'); } @@ -2027,7 +2024,7 @@ const scaleInput = document.getElementById('editor-bg-scale'); if (scaleInput) scaleInput.value = '100'; renderPreview(); - track('background-image-uploaded', { page: resultState.mode, type: file.type.replace('image/', '') }); + track('background-image-uploaded'); } function renderResultEditor() { @@ -2229,7 +2226,7 @@ const shadowSelect = document.getElementById('editor-shadow'); if (shadowSelect) shadowSelect.value = resultState.shadow; renderPreview(); - track('amazon-preset-applied', { page: resultState.mode }); + track('amazon-preset-applied'); }); } const customColor = document.getElementById('editor-custom-color'); @@ -2276,7 +2273,7 @@ }); document.getElementById('download-transparent').addEventListener('click', () => { downloadUrl(resultState.cutoutUrl, `${resultState.basename}_transparent.png`); - track('transparent-download', { page: resultState.mode }); + track('transparent-download'); }); const downloadEdited = document.getElementById('download-edited'); if (downloadEdited) downloadEdited.addEventListener('click', () => downloadCurrentVersion()); @@ -2341,18 +2338,18 @@ async function handleFiles(files) { const file = files[0]; + // 选图即预热 Beam(登录用户):把 GPU 冷启动藏进后续「压缩+上传」窗口;匿名时是 no-op。 + window.MiaoCutPro?.prewarm?.(); + if (!ALLOWED_TYPES.includes(file.type)) { - track('format-rejected', { type: file && file.type ? file.type : 'unknown' }); + track('format-rejected'); alert(t('alertSize')); return; } console.log(`[MiaoCut] 原始图片: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`); - const fileExt = file.type.replace('image/', ''); - // 子页打点带 page 维度,便于在 Umami 后台分析"哪个 landing page 转化最高" const page = (window.MIAOCUT_PAGE_KEY || 'home'); - track('upload-started', { type: fileExt, size: sizeBucket(file.size), profile: currentProfile, page }); - const startedAt = performance.now(); + track('upload-started'); cleanupResultState(); dropzoneContent.classList.add('pointer-events-none'); @@ -2371,15 +2368,10 @@ formData.append('file', uploadFile); const blob = await uploadWithProgress(formData); - track('cutout-success', { - type: fileExt, - size: sizeBucket(file.size), - duration: durationBucket(performance.now() - startedAt), - page, - }); + track('cutout-success'); if (page === 'home') { - autoDownloadTransparentResult(file, blob, page); + autoDownloadTransparentResult(file, blob); shouldResetDropzone = true; } else { await showResultEditor(file, uploadFile, blob); @@ -2393,21 +2385,26 @@ if (error.status === 429) { if (error.scope === 'day') { // 今日免费额度用尽(明天重置)→ 引导升级 Pro(弹预约弹窗) - track('cutout-rate-limited', { type: fileExt, page, scope: 'day' }); + track('cutout-rate-limited'); resetDropzone(); if (typeof openWaitlistModal === 'function') openWaitlistModal('rateDay'); } else { // 手速太快(minute):当天还有额度,给温和提示 + 可选 Pro 批量入口。 // 保留提示(不让 finally 的延时 reset 把它清掉),用户读完可点 Pro 或重新拖图。 shouldResetDropzone = false; - track('cutout-rate-limited', { type: fileExt, page, scope: 'minute' }); + track('cutout-rate-limited'); showRateBurstNotice(Math.max(1, Math.round(error.retryAfter || 30))); } return; } - const reason = error.status ? `http-${error.status}` - : (error.kind || (error.message && /压缩|WebP/.test(error.message) ? 'compress' : 'unknown')); - track('cutout-failed', { type: fileExt, reason, page }); + // 积分不足(登录付费路径专有)→ 引导去充值 + if (error.status === 402) { + track('cutout-insufficient-credits'); + resetDropzone(); + window.location.href = '/pricing/'; + return; + } + track('cutout-failed'); // 用 DOM 拼装而不是 innerHTML 拼字符串,防止 error.message 里若含 HTML 被当标签执行(XSS) dropzoneContent.innerHTML = ''; const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); diff --git a/batch/batch.js b/batch/batch.js new file mode 100644 index 0000000..d62ef7c --- /dev/null +++ b/batch/batch.js @@ -0,0 +1,310 @@ +// ============================================================================= +// MiaoCut 批量去背景 —— 前端流程 +// 建任务 → 逐张直传 R2(预签名) → 开始(冻结+入队) → 轮询进度 → ZIP 下载 +// 复用 pro-account.js 的登录态;所有 Pro API 调用走 MiaoCutPro.proFetch(自动带 JWT)。 +// ============================================================================= +import MiaoCutPro from "/pro-account.js"; + +const CREDIT_COST = { fast: 1, fur: 2 }; +const MAX_IMAGES = 200; +const ALLOWED = ["image/png", "image/jpeg", "image/webp"]; + +const $ = (id) => document.getElementById(id); +const els = { + balance: $("balance"), + loginGate: $("login-gate"), + btnGoogle: $("btn-google"), + btnEmail: $("btn-email"), + loginEmail: $("login-email"), + loginMsg: $("login-msg"), + tool: $("tool"), + profileToggle: $("profile-toggle"), + dropzone: $("dropzone"), + fileInput: $("file-input"), + fileList: $("file-list"), + costLine: $("cost-line"), + btnStart: $("btn-start"), + progress: $("progress"), + progressText: $("progress-text"), + progressBar: $("progress-bar"), + imageGrid: $("image-grid"), + results: $("results"), + resultsSummary: $("results-summary"), + resultsMsg: $("results-msg"), + btnZip: $("btn-zip"), +}; + +let profile = "fast"; +let files = []; // 选中的 File[] +let chips = {}; // image_id → 缩略图 chip 元素 +let lastImages = []; // 最近一次状态的 images(给 ZIP 用) + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +// ---------- 登录态 ---------- +async function render() { + const logged = MiaoCutPro.isLoggedIn(); + els.loginGate.classList.toggle("hidden", logged); + els.tool.classList.toggle("hidden", !logged); + if (logged) { + try { + const b = await MiaoCutPro.balance(); + els.balance.innerHTML = `Balance: ${b.total} credits`; + } catch (_) { /* ignore */ } + } else { + els.balance.textContent = ""; + } +} + +els.btnGoogle.addEventListener("click", async () => { + try { await MiaoCutPro.signInWithGoogle(window.location.href); } + catch (e) { els.loginMsg.textContent = e.message === "auth_unavailable" ? "登录暂未配置(Supabase)。" : "登录失败,请重试。"; } +}); +els.btnEmail.addEventListener("click", async () => { + const email = els.loginEmail.value.trim(); + if (!email) return; + try { + await MiaoCutPro.signInWithEmail(email, window.location.href); + els.loginMsg.textContent = "Magic link sent — check your email."; + } catch (e) { + els.loginMsg.textContent = e.message === "auth_unavailable" ? "登录暂未配置(Supabase)。" : "Failed to send, try again."; + } +}); + +// ---------- profile 切换 ---------- +els.profileToggle.querySelectorAll("button").forEach((b) => { + b.addEventListener("click", () => { + profile = b.dataset.profile; + els.profileToggle.querySelectorAll("button").forEach((x) => x.classList.toggle("active", x === b)); + updateCost(); + }); +}); + +// ---------- 选图 ---------- +els.dropzone.addEventListener("click", () => els.fileInput.click()); +els.fileInput.addEventListener("change", () => addFiles(els.fileInput.files)); +["dragover", "dragenter"].forEach((ev) => + els.dropzone.addEventListener(ev, (e) => { e.preventDefault(); els.dropzone.classList.add("drag"); })); +["dragleave", "drop"].forEach((ev) => + els.dropzone.addEventListener(ev, (e) => { e.preventDefault(); els.dropzone.classList.remove("drag"); })); +els.dropzone.addEventListener("drop", (e) => addFiles(e.dataTransfer.files)); + +function addFiles(fileList) { + for (const f of fileList) { + if (ALLOWED.includes(f.type) && files.length < MAX_IMAGES) files.push(f); + } + els.fileInput.value = ""; + els.fileList.textContent = files.length ? `${files.length} image${files.length > 1 ? "s" : ""} selected` : ""; + updateCost(); +} + +function updateCost() { + const has = files.length > 0; + els.btnStart.classList.toggle("hidden", !has); + els.costLine.classList.toggle("hidden", !has); + if (has) { + const cost = files.length * CREDIT_COST[profile]; + els.costLine.innerHTML = `${files.length} × ${CREDIT_COST[profile]} = ${cost} credits · failed images are not charged`; + } +} + +// ---------- 主流程 ---------- +els.btnStart.addEventListener("click", start); + +async function start() { + if (!files.length) return; + els.btnStart.disabled = true; + els.tool.classList.add("hidden"); + els.progress.classList.remove("hidden"); + els.results.classList.add("hidden"); + + try { + // 1) 建任务 → 拿到每图的预签名 PUT URL + setProgress("Preparing…", 0.02); + const create = await MiaoCutPro.proFetch("/v1/batch", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ profile, files: files.map((f) => ({ name: f.name, size: f.size })) }), + }); + if (create.status === 402) return paywall(); + if (!create.ok) throw new Error(`create_${create.status}`); + const job = await create.json(); + + // 建缩略图 chips(按返回顺序对应 files) + els.imageGrid.innerHTML = ""; + chips = {}; + job.uploads.forEach((up, i) => { + const el = document.createElement("div"); + el.className = "chip"; + el.dataset.id = up.image_id; + el.innerHTML = ` + + +
+ +
+ `; + els.imageGrid.appendChild(el); + chips[up.image_id] = el; + }); + + // 2) 逐张直传 R2(限并发 4,绕过 Worker / CF 100MB) + let uploaded = 0; + await runPool(job.uploads, async (up, i) => { + const el = chips[up.image_id]; + if (el) el.querySelector(".spinner").style.display = "block"; + await putToR2(files[i], up.put_url); + uploaded++; + setBadge(up.image_id, "↑", ""); + setProgress(`Uploading ${uploaded}/${job.uploads.length}`, (uploaded / job.uploads.length) * 0.4); + }, 4); + + // 3) 开始 → 冻结积分 + 入队 Beam + setProgress("Starting…", 0.42); + const startRes = await MiaoCutPro.proFetch(`/v1/batch/${job.job_id}/start`, { method: "POST" }); + if (startRes.status === 402) return paywall(); + if (!startRes.ok) throw new Error(`start_${startRes.status}`); + + // 4) 轮询进度 + await poll(job.job_id, job.uploads.length); + } catch (e) { + setProgress("Something went wrong: " + e.message, 1, true); + els.btnStart.disabled = false; + } +} + +async function poll(jobId, total) { + for (;;) { + await sleep(2000); + const res = await MiaoCutPro.proFetch(`/v1/batch/${jobId}`); + if (!res.ok) { await sleep(2000); continue; } + const data = await res.json(); + lastImages = data.images || []; + for (const im of lastImages) { + if (im.status === "done") { + setBadge(im.image_id, "✓", "done"); + const el = chips[im.image_id]; + if (el && !el.dataset.done) { + el.dataset.done = "1"; + el.dataset.url = im.download_url; + const resImg = el.querySelector(".result"); + resImg.src = im.download_url; + resImg.style.display = "block"; + el.querySelector(".spinner").style.display = "none"; + } + } + else if (im.status === "failed") { + setBadge(im.image_id, "✕", "failed"); + const el = chips[im.image_id]; + if (el) el.querySelector(".spinner").style.display = "none"; + } + } + const resolved = data.succeeded + data.failed; + setProgress(`Processing… ${resolved}/${total} done`, 0.4 + (resolved / total) * 0.6); + if (data.done) { + return showResults(data); + } + } +} + +function showResults(data) { + els.progress.classList.add("hidden"); + els.results.classList.remove("hidden"); + els.resultsSummary.textContent = `Done — ${data.succeeded} succeeded, ${data.failed} failed`; + els.resultsMsg.textContent = data.failed ? "Failed images were not charged." : ""; + els.btnZip.disabled = data.succeeded === 0; + render(); // 刷新余额 +} + +els.btnZip.addEventListener("click", async () => { + const done = lastImages.filter((im) => im.status === "done" && im.download_url); + if (!done.length) return; + els.btnZip.disabled = true; + els.resultsMsg.textContent = "Packing ZIP…"; + try { + const zip = new JSZip(); + let n = 0; + await runPool(done, async (im) => { + const blob = await (await fetch(im.download_url)).blob(); + const base = (im.filename || "image").replace(/\.[^.]+$/, ""); + zip.file(`${base}_removed.webp`, blob); + els.resultsMsg.textContent = `Packing ZIP… ${++n}/${done.length}`; + }, 4); + const out = await zip.generateAsync({ type: "blob" }); + const a = document.createElement("a"); + a.href = URL.createObjectURL(out); + a.download = "miaocut_batch.zip"; + document.body.appendChild(a); a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(a.href), 3000); + els.resultsMsg.textContent = ""; + } catch (e) { + els.resultsMsg.textContent = "ZIP failed: " + e.message; + } finally { + els.btnZip.disabled = false; + } +}); + +// ---------- 工具 ---------- +function putToR2(file, url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open("PUT", url); + xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error("upload_" + xhr.status))); + xhr.onerror = () => reject(new Error("upload_network")); + xhr.send(file); + }); +} + +async function runPool(items, worker, concurrency = 4) { + let i = 0; + const run = async () => { while (i < items.length) { const idx = i++; await worker(items[idx], idx); } }; + await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, run)); +} + +function setBadge(imageId, text, cls) { + const el = chips[imageId]; + if (!el) return; + el.className = "chip" + (cls ? " " + cls : ""); + const b = el.querySelector(".badge"); + if (b) b.textContent = text; +} + +function setProgress(text, frac, isErr) { + els.progressText.textContent = text; + els.progressText.style.color = isErr ? "#dc2626" : ""; + els.progressBar.style.width = Math.max(0, Math.min(1, frac)) * 100 + "%"; +} + +function paywall() { + setProgress("Not enough credits.", 1, true); + setTimeout(() => { window.location.href = "/pricing/"; }, 1200); +} + +// ---------- 启动 ---------- +MiaoCutPro.init().then(render).catch(() => render()); +MiaoCutPro.onAuthChange(render); + +// ---------- Lightbox ---------- +const lightbox = document.getElementById("lightbox"); +const lightboxImg = document.getElementById("lightbox-img"); +const lightboxClose = document.getElementById("lightbox-close"); + +els.imageGrid.addEventListener("click", (e) => { + const chip = e.target.closest(".chip.done"); + if (!chip || !chip.dataset.url) return; + lightboxImg.src = chip.dataset.url; + lightbox.classList.add("show"); +}); + +function closeLightbox() { + lightbox.classList.remove("show"); + setTimeout(() => { lightboxImg.src = ""; }, 300); +} + +lightboxClose.addEventListener("click", closeLightbox); +lightbox.addEventListener("click", (e) => { + if (e.target === lightbox) closeLightbox(); +}); +document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && lightbox.classList.contains("show")) closeLightbox(); +}); diff --git a/batch/index.html b/batch/index.html new file mode 100644 index 0000000..d867c67 --- /dev/null +++ b/batch/index.html @@ -0,0 +1,430 @@ + + + + + + + + Batch Background Remover — MiaoCut + + + + + + + +
+
+ +
+
+ +

Batch Background Remover

+

Upload many images, remove backgrounds in bulk, download all as a ZIP.

+ + +
+

Sign in to use batch processing.

+
+ + + +
+
+
+ + + + + + + + + +
+ + + + + + + + \ No newline at end of file diff --git a/bn/id-photo-maker/index.html b/bn/id-photo-maker/index.html index dc26039..7024073 100644 --- a/bn/id-photo-maker/index.html +++ b/bn/id-photo-maker/index.html @@ -184,7 +184,7 @@ ] } - + diff --git a/bn/index.html b/bn/index.html index a828acd..e3f38d7 100644 --- a/bn/index.html +++ b/bn/index.html @@ -8,19 +8,19 @@ + })(); @@ -232,7 +232,7 @@ 源:src/input.css;构建命令:npm run build:css;输出:output.css 改动 index.html 的 class 后必须重新跑 build:css 并把 output.css 一起提交。 --> - + @@ -249,9 +249,11 @@ - + -
+
@@ -260,39 +262,81 @@
- +
- -
-
-