Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
64ac460
feat(pixel-perfect): define tool contracts
huyanxius Aug 20, 2026
c80dcd3
feat(pixel-perfect): add bounded native adapters
huyanxius Aug 20, 2026
13aa8a0
feat(pixel-perfect): compose local grid tools
huyanxius Aug 20, 2026
313a5e8
feat(pixel-perfect): expose standalone file API
huyanxius Aug 20, 2026
02fb019
build(pixel-perfect): declare image inspection dependency
huyanxius Aug 20, 2026
57eb318
build(pixel-perfect): enforce pipeline isolation
huyanxius Aug 20, 2026
a30a0aa
build(pixel-perfect): package native tool binaries
huyanxius Aug 20, 2026
38d1185
test(pixel-perfect): cover standalone tool boundaries
huyanxius Aug 20, 2026
9c01648
docs(pixel-perfect): publish file API contract
huyanxius Aug 20, 2026
da07ddd
refactor(pixel-perfect): call the native extension directly
huyanxius Aug 21, 2026
2182cda
test(pixel-perfect): cover the PyO3 adapter contract
huyanxius Aug 21, 2026
56b825c
build(pixel-perfect): define the PyO3 extension package
huyanxius Aug 21, 2026
be6e23c
feat(pixel-perfect): expose Rust algorithms through PyO3
huyanxius Aug 21, 2026
ef58d29
test(pixel-perfect): verify the native binding contract
huyanxius Aug 21, 2026
2a6cf23
docs(pixel-perfect): document native binding usage
huyanxius Aug 21, 2026
94e955c
chore(pixel-perfect): ignore native build output
huyanxius Aug 21, 2026
cbbae0b
build(pixel-perfect): install the native wheel in containers
huyanxius Aug 21, 2026
cfde2b9
fix(pixel-perfect): preserve oversized upload errors
huyanxius Aug 21, 2026
1e4cd1d
test(pixel-perfect): cover oversized multipart uploads
huyanxius Aug 21, 2026
bc337f3
refactor(pixel-perfect): centralize concurrency settings
huyanxius Aug 21, 2026
6560dc8
docs(pixel-perfect): publish the concurrency setting
huyanxius Aug 21, 2026
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
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
**
!backend/
!backend/**
!native/
!native/pixel-perfect/
!native/pixel-perfect/**

backend/.venv/
backend/.pytest_cache/
backend/.ruff_cache/
backend/**/__pycache__/
native/pixel-perfect/**/target/
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ WINDUP_CORS_ORIGINS=
# 跨域正则匹配(默认关闭;如需 Vercel 预览域名,请限制到自己的项目名前缀)
WINDUP_CORS_ORIGIN_REGEX=

# ── 独立完美像素工具 ──
# 单个进程同时处理的图片数;Rust 运算会释放 GIL,但仍受本机 CPU / 内存约束。
PIXEL_PERFECT_CONCURRENCY=1

# ── MQ(Redis Stream 轻量消息队列) ──
# 本地/Compose 须同时起 web + worker,否则生成任务会一直 PENDING、邮件不会发出。
# docker compose up -d backend worker
Expand Down
50 changes: 43 additions & 7 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
# ── 后端 Dockerfile ──────────────────────────────────────────────────
# 多阶段构建:builder 装依赖 → runtime 只拷贝产物,镜像更小

# ── 阶段 1: 构建 ──
# ── 阶段 0: 构建 PyO3 原生扩展 wheel ──
FROM rust:1.89-slim AS rust-toolchain

FROM python:3.12-slim AS pixel-native-builder

COPY --from=rust-toolchain /usr/local/cargo /usr/local/cargo
COPY --from=rust-toolchain /usr/local/rustup /usr/local/rustup
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

ENV PATH="/usr/local/cargo/bin:$PATH" \
CARGO_HOME=/usr/local/cargo \
RUSTUP_HOME=/usr/local/rustup \
UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/ \
UV_HTTP_TIMEOUT=180

RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc libc6-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /build/native/pixel-perfect
COPY native/pixel-perfect/crates/detector/ crates/detector/
COPY native/pixel-perfect/crates/reconstructor/ crates/reconstructor/
COPY native/pixel-perfect/bindings/python/ bindings/python/
RUN uv tool run --from maturin==1.14.1 maturin build \
--release \
--locked \
--manifest-path bindings/python/Cargo.toml \
--out /wheels

# ── 阶段 1: 构建 Python 环境 ──
FROM python:3.12-slim AS builder

# 安装 uv(比 pip 快 10x)
Expand All @@ -17,19 +46,24 @@ ENV UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/ \
WORKDIR /app

# 先拷贝依赖定义,利用 Docker layer cache
COPY pyproject.toml uv.lock ./
COPY packages/common/pyproject.toml packages/common/
COPY packages/framework/pyproject.toml packages/framework/
COPY packages/ai_engine/pyproject.toml packages/ai_engine/
COPY packages/app/pyproject.toml packages/app/
COPY backend/pyproject.toml backend/uv.lock ./
COPY backend/packages/common/pyproject.toml packages/common/
COPY backend/packages/framework/pyproject.toml packages/framework/
COPY backend/packages/ai_engine/pyproject.toml packages/ai_engine/
COPY backend/packages/app/pyproject.toml packages/app/

# 安装依赖(不含 dev 依赖)
RUN uv sync --frozen --no-dev --no-install-workspace

# 拷贝源码并安装
COPY packages/ packages/
COPY backend/packages/ packages/
RUN uv sync --frozen --no-dev

# 原生扩展是显式工具的可选源码依赖,由镜像在构建期安装;缺失时应用仍可启动。
COPY --from=pixel-native-builder /wheels /wheels
RUN uv pip install --python /app/.venv/bin/python /wheels/*.whl \
&& /app/.venv/bin/python -c "import windup_pixel_perfect_native"

# ── 阶段 2: 运行时 ──
FROM python:3.12-slim AS runtime

Expand All @@ -38,6 +72,8 @@ WORKDIR /app
# 从 builder 拷贝虚拟环境和包
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/packages /app/packages
COPY native/pixel-perfect/crates/detector/LICENSE /licenses/pixel-grid-detector/LICENSE
COPY native/pixel-perfect/crates/reconstructor/LICENSE /licenses/pixel-grid-reconstructor/LICENSE

# 把 venv/bin 加入 PATH
ENV PATH="/app/.venv/bin:$PATH"
Expand Down
1 change: 1 addition & 0 deletions backend/packages/app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"pydantic[email]>=2.7",
"sqlalchemy>=2.0",
"python-multipart>=0.0.9",
"pillow>=10.4",
]

[project.scripts]
Expand Down
22 changes: 22 additions & 0 deletions backend/packages/app/src/windup_app/bootstrap/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from windup_framework.config.pixel_perfect import settings as pixel_perfect_settings
from windup_framework.db import Base, engine

# 模型导入:触发 Base.metadata 注册,确保 create_all 能发现所有表
Expand All @@ -29,15 +30,20 @@
from windup_app.web.api.auth import router as auth_router
from windup_app.web.api.character import router as character_router
from windup_app.server.orchestrator import task_repo
from windup_app.server.pixel_perfect import create_pixel_perfect_tool
from windup_app.server.orchestrator.render3d_service import default_operations, precheck_master
from windup_app.web.api.generation import router as generation_router
from windup_app.web.api.media import router as media_router
from windup_app.web.api.pixel_perfect import router as pixel_perfect_router
from windup_app.web.api.project import router as project_router
from windup_app.web.api.quota import router as quota_router
from windup_app.web.api.render3d import router as render3d_router
from windup_app.web.api.workflow_run import router as workflow_run_router
from windup_app.web.handler.exception_handlers import register_exception_handlers
from windup_app.web.middleware.auth import AuthMiddleware
from windup_app.web.middleware.pixel_perfect_limits import (
PixelPerfectRequestLimitsMiddleware,
)
from windup_framework.mq.publisher import MqPublisher
from windup_framework.mq.relay import relay_pending_messages
from windup_framework.providers import create_chat_model
Expand Down Expand Up @@ -102,6 +108,9 @@ def create_app() -> FastAPI:
app = FastAPI(title="windup", version="0.1.0", lifespan=_lifespan)
app.state.mq_publisher = MqPublisher()
app.state.chat_model_factory = create_chat_model
app.state.pixel_perfect_tool = create_pixel_perfect_tool(
max_concurrency=pixel_perfect_settings.concurrency
)
# 起名器在 composition root 注入,避免 web→character.service 碰到 ai_engine。
# LangChainCharacterNamer 构造期不创建 ChatOpenAI;缺 AI_API_KEY 时应用仍能启动。
# 测试若已注入假 namer,不要覆盖。
Expand All @@ -114,19 +123,32 @@ def health() -> dict[str, str]:

# 中间件(add_middleware 后加的先执行:请求先进 CORS → 再进 Auth → 最后到路由)
app.add_middleware(AuthMiddleware)
app.add_middleware(
PixelPerfectRequestLimitsMiddleware,
max_concurrency=pixel_perfect_settings.concurrency,
)
app.add_middleware(
CORSMiddleware,
allow_origins=_cors_origins(),
allow_origin_regex=_cors_origin_regex(),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=[
"X-Pixel-Cols",
"X-Pixel-Rows",
"X-Pixel-Step-X",
"X-Pixel-Step-Y",
"X-Pixel-Consensus",
"X-Pixel-Confidence",
],
)
app.include_router(auth_router)
app.include_router(project_router)
app.include_router(character_router)
app.include_router(workflow_run_router)
app.include_router(media_router)
app.include_router(pixel_perfect_router)
app.include_router(generation_router)
app.include_router(quota_router)
app.include_router(render3d_router)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from windup_app.server.pixel_perfect.errors import (
PixelPerfectBusyError,
PixelPerfectError,
PixelPerfectInputError,
PixelPerfectUnavailableError,
)
from windup_app.server.pixel_perfect.factory import create_pixel_perfect_tool
from windup_app.server.pixel_perfect.model import GridDetection, PixelPerfectResult
from windup_app.server.pixel_perfect.native import (
NativeGridDetector,
NativeGridReconstructor,
)
from windup_app.server.pixel_perfect.service import (
GridDetector,
GridReconstructor,
PixelPerfectTool,
)

__all__ = [
"GridDetection",
"GridDetector",
"GridReconstructor",
"NativeGridDetector",
"NativeGridReconstructor",
"PixelPerfectBusyError",
"PixelPerfectError",
"PixelPerfectInputError",
"PixelPerfectResult",
"PixelPerfectTool",
"PixelPerfectUnavailableError",
"create_pixel_perfect_tool",
]
17 changes: 17 additions & 0 deletions backend/packages/app/src/windup_app/server/pixel_perfect/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""完美像素工具可以映射为稳定 API 结果的失败类型。"""


class PixelPerfectError(Exception):
"""本地工具可预期失败的基类。"""


class PixelPerfectBusyError(PixelPerfectError):
pass


Comment thread
huyanxius marked this conversation as resolved.
class PixelPerfectInputError(PixelPerfectError):
pass


class PixelPerfectUnavailableError(PixelPerfectError):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""完美像素工具在应用 composition root 使用的本地装配。"""

from windup_app.server.pixel_perfect.native import (
NativeGridDetector,
NativeGridReconstructor,
)
from windup_app.server.pixel_perfect.service import PixelPerfectTool


def create_pixel_perfect_tool(*, max_concurrency: int) -> PixelPerfectTool:
return PixelPerfectTool(
detector=NativeGridDetector(),
reconstructor=NativeGridReconstructor(),
max_concurrency=max_concurrency,
)
18 changes: 18 additions & 0 deletions backend/packages/app/src/windup_app/server/pixel_perfect/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""完美像素工具在检测、重建与 API 之间传递的最小契约。"""

from dataclasses import dataclass


@dataclass(frozen=True)
class GridDetection:
cols: int
rows: int
step_x: float
step_y: float
consensus: str
confidence: str


@dataclass(frozen=True)
class PixelPerfectResult(GridDetection):
png: bytes
116 changes: 116 additions & 0 deletions backend/packages/app/src/windup_app/server/pixel_perfect/native.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""PyO3 原生扩展与 Python 工具编排之间的适配层。"""

from collections.abc import Callable
from importlib import import_module
import math
from typing import Protocol

from windup_app.server.pixel_perfect.errors import (
PixelPerfectInputError,
PixelPerfectUnavailableError,
)
from windup_app.server.pixel_perfect.model import GridDetection


class PixelPerfectNativeModule(Protocol):
def detect(self, source: bytes, mode: str) -> object: ...

def reconstruct(
self,
source: bytes,
cols: int,
rows: int,
colors: int,
) -> object: ...


NativeModuleLoader = Callable[[], PixelPerfectNativeModule]


def _load_installed_module() -> PixelPerfectNativeModule:
return import_module("windup_pixel_perfect_native")


def _load_native(loader: NativeModuleLoader) -> PixelPerfectNativeModule:
try:
return loader()
except (ImportError, OSError) as error:
raise PixelPerfectUnavailableError(
"本地像素原生扩展未安装或无法加载"
) from error


class NativeGridDetector:
def __init__(
self, module_loader: NativeModuleLoader = _load_installed_module
) -> None:
self._module_loader = module_loader

def detect(self, source: bytes) -> GridDetection:
native = _load_native(self._module_loader)
try:
payload = native.detect(source, "full")
except ValueError as error:
raise PixelPerfectInputError(
str(error) or "原生检测器拒绝了输入"
) from error
except Exception as error:
raise PixelPerfectUnavailableError("原生检测器调用失败") from error

expected = {
"cols",
"rows",
"step_x",
"step_y",
"consensus",
"confidence",
}
if not isinstance(payload, dict) or set(payload) != expected:
raise PixelPerfectUnavailableError("检测器返回字段不符合约定")
try:
result = GridDetection(**payload)
except TypeError as error:
raise PixelPerfectUnavailableError("检测器返回类型不符合约定") from error
if (
not isinstance(result.cols, int)
or isinstance(result.cols, bool)
or not isinstance(result.rows, int)
or isinstance(result.rows, bool)
or result.cols < 1
or result.rows < 1
or not isinstance(result.step_x, (int, float))
or isinstance(result.step_x, bool)
or not isinstance(result.step_y, (int, float))
or isinstance(result.step_y, bool)
or not math.isfinite(result.step_x)
or not math.isfinite(result.step_y)
or result.step_x <= 0
or result.step_y <= 0
or not isinstance(result.consensus, str)
or result.confidence not in {"high", "medium", "low"}
):
raise PixelPerfectUnavailableError("检测器返回值超出约定")
return result


class NativeGridReconstructor:
def __init__(
self, module_loader: NativeModuleLoader = _load_installed_module
) -> None:
self._module_loader = module_loader

def reconstruct(self, source: bytes, *, cols: int, rows: int, colors: int) -> bytes:
native = _load_native(self._module_loader)
try:
output = native.reconstruct(source, cols, rows, colors)
except ValueError as error:
raise PixelPerfectInputError(
str(error) or "原生重建器拒绝了输入"
) from error
except Exception as error:
raise PixelPerfectUnavailableError("原生重建器调用失败") from error
if not isinstance(output, bytes):
raise PixelPerfectUnavailableError("重建器返回类型不符合约定")
if len(output) > 32 * 1024 * 1024:
raise PixelPerfectUnavailableError("重建器返回数据过大")
return output
Loading
Loading