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/.github/workflows/deploy-backend-dev.yml b/.github/workflows/deploy-backend-dev.yml new file mode 100644 index 0000000..ebe8eb6 --- /dev/null +++ b/.github/workflows/deploy-backend-dev.yml @@ -0,0 +1,160 @@ +name: Deploy Backend Dev + +on: + push: + branches: [ dev ] + paths: + - "main.py" + - "requirements.txt" + - "Dockerfile" + - ".github/workflows/deploy-backend-dev.yml" + workflow_dispatch: + +jobs: + deploy_spaces: + name: Push to Hugging Face Spaces (Dev) + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install huggingface_hub + run: pip install --quiet "huggingface_hub>=0.24" + + - name: Rolling deploy to Dev Hugging Face Spaces + env: + HF_TOKEN: ${{ secrets.HF_TOKEN_DEV }} + GITHUB_SHA: ${{ github.sha }} + run: | + set -euo pipefail + if [ -z "$HF_TOKEN" ]; then + echo "HF_TOKEN_DEV secret is not configured." + exit 1 + fi + python - <<'PY' + import os + import sys + import time + import json + import urllib.request + + from huggingface_hub import HfApi + + api = HfApi(token=os.environ["HF_TOKEN"]) + sha_short = os.environ.get("GITHUB_SHA", "manual")[:8] + + # Dev Space for deployment + SPACES = [ + ("miao_cut_dev1", "wu408124546-miao-cut-dev1.hf.space"), + ] + ALLOW = ["main.py", "Dockerfile", "requirements.txt", "README.md"] + + POLL_INTERVAL = 10 + BUILD_START_TIMEOUT = 240 + BUILD_FINISH_TIMEOUT = 1500 + READY_TIMEOUT = 360 + READY_STABLE_HITS = 2 + + BUILDING_STAGES = {"BUILDING", "RUNNING_BUILDING", "APP_STARTING", "RUNNING_APP_STARTING"} + FATAL_STAGES = {"BUILD_ERROR", "RUNTIME_ERROR", "CONFIG_ERROR", "NO_APP_FILE"} + + def stage_of(repo_id): + try: + return api.get_space_runtime(repo_id).stage or "UNKNOWN" + except Exception as exc: + print(f" (warn) get_space_runtime({repo_id}) failed: {exc}") + return "UNKNOWN" + + def wait_build_start(repo_id): + deadline = time.time() + BUILD_START_TIMEOUT + while time.time() < deadline: + stage = stage_of(repo_id) + if stage in FATAL_STAGES: + raise RuntimeError(f"{repo_id} entered fatal stage during build start: {stage}") + if stage in BUILDING_STAGES: + print(f" [{repo_id}] build started (stage={stage})") + return True + time.sleep(POLL_INTERVAL) + return False + + def wait_build_finish(repo_id): + deadline = time.time() + BUILD_FINISH_TIMEOUT + last = None + while time.time() < deadline: + stage = stage_of(repo_id) + if stage != last: + print(f" [{repo_id}] stage={stage}") + last = stage + if stage in FATAL_STAGES: + raise RuntimeError(f"{repo_id} build failed: stage={stage}") + if stage == "RUNNING": + return + time.sleep(POLL_INTERVAL) + raise TimeoutError(f"{repo_id} did not return to RUNNING within {BUILD_FINISH_TIMEOUT}s") + + def probe(host): + url = f"https://{host}/" + try: + req = urllib.request.Request(url, headers={"User-Agent": "miaocut-rollout-dev"}) + with urllib.request.urlopen(req, timeout=10) as r: + if r.status != 200: + return (False, False) + data = json.loads(r.read().decode("utf-8")) + return (bool(data.get("ok")), bool(data.get("model_ready"))) + except Exception: + return (False, False) + + def wait_http_ready(host): + deadline = time.time() + READY_TIMEOUT + hits = 0 + while time.time() < deadline: + ok, ready = probe(host) + if ok and ready: + hits += 1 + if hits >= READY_STABLE_HITS: + print(f" [{host}] healthy & model_ready ✅") + return + else: + hits = 0 + time.sleep(POLL_INTERVAL) + raise TimeoutError(f"{host} not healthy/model_ready within {READY_TIMEOUT}s") + + def deploy_one(space, host): + repo_id = f"wu408124546/{space}" + print(f"==> [{repo_id}] uploading {ALLOW}") + api.upload_folder( + folder_path=".", + repo_id=repo_id, + repo_type="space", + allow_patterns=ALLOW, + commit_message=f"Deploy {sha_short} from GitHub Actions (rolling dev)", + ) + if wait_build_start(repo_id): + wait_build_finish(repo_id) + else: + print(f" [{repo_id}] no rebuild detected (files unchanged); verifying current health") + wait_http_ready(host) + + failed = None + for i, (space, host) in enumerate(SPACES): + try: + deploy_one(space, host) + print(f" [{space}] done ({i + 1}/{len(SPACES)})\n") + except Exception as exc: + failed = (space, exc) + print(f"!! [{space}] deploy failed: {exc}", file=sys.stderr) + print("!! Aborting rollout — remaining Spaces keep serving the previous version.", + file=sys.stderr) + break + + if failed: + sys.exit(1) + print("Rolling deploy complete: all 3 Dev Spaces updated with zero downtime.") + PY 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..9b55fc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,15 @@ 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 运行时依赖 +# +# 【曾用过 jemalloc,已回退】 +# 之前尝试过 apt install libjemalloc2 + LD_PRELOAD 让 jemalloc 替换 glibc malloc, +# 目标是降低碎片、更快归还内存给 OS。Space 端 A/B 后发现在 2 vCPU CPU-bound FP32 +# 推理场景下(BiRefNet-general-lite-768),jemalloc 的 background_thread 反而抢占 +# vCPU,导致 sharp 推理慢 ~3s(6.5s -> 9.5s median)。当前 Space 内存并不是瓶颈 +# (oom_kills=0,cgroup 用量只到 15%),不值这个速度代价,先撤。 +# 如果将来内存成为瓶颈再回来考虑 jemalloc + background_thread:false 或者 tcmalloc。 RUN apt-get update && \ apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && \ useradd -m -u 1000 user && \ @@ -91,6 +99,10 @@ 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。 +# +# MALLOC_ARENA_MAX / MALLOC_TRIM_THRESHOLD_ 是 glibc 特有的 tuning,配合 main.py 里的 +# _malloc_trim() 主动归还内存,减少 RSS 累积。之前尝试过用 jemalloc 替换 glibc, +# 在 2 vCPU Space 上反而拖慢 sharp 推理 ~3s(见 apt install 注释),已撤回,当前继续用 glibc。 ENV PORT=7860 \ U2NET_HOME=/opt/miaocut-models \ LAMA_MODEL=/opt/miaocut-models/big-lama.pt \ diff --git a/account/credits/index.html b/account/credits/index.html new file mode 100644 index 0000000..34d567b --- /dev/null +++ b/account/credits/index.html @@ -0,0 +1,188 @@ + + + + + + + + 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..4dfd6b6 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(); @@ -1275,7 +1262,7 @@ // API 配置 // ============================================================ const _isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; - const API_BASE = _isLocal ? 'http://localhost:8000' : 'https://api2.miaocut.app'; + const API_BASE = (window.MIAOCUT_PRO_CONFIG && window.MIAOCUT_PRO_CONFIG.FREE_API_BASE) || (_isLocal ? 'http://localhost:8000' : 'https://api2.miaocut.app'); // ============================================================ // Batch Process Pro fake door(同一个弹窗复用为 3 种场景,见 openWaitlistModal) @@ -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..1b5fd5a --- /dev/null +++ b/batch/index.html @@ -0,0 +1,436 @@ + + + + + + + + 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..47629b2 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 @@
- +
- -
-
-