Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .beamignore
Original file line number Diff line number Diff line change
@@ -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

141 changes: 141 additions & 0 deletions .claude/logs/tool-audit.jsonl

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
]
}
}
160 changes: 160 additions & 0 deletions .github/workflows/deploy-backend-dev.yml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 13 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down Expand Up @@ -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 \
Expand Down
Loading
Loading