Download app/workers/tasks.py from nenae18/leechard: direct link, hf CLI and curl.
- Browser
- Download file 8.63 kB
-
https://huggingface.co/spaces/nenae18/leechard/resolve/main/app/workers/tasks.py
- Command line
-
hf download hf://spaces/nenae18/leechard/app/workers/tasks.py
-
curl -L -o tasks.py https://huggingface.co/spaces/nenae18/leechard/resolve/main/app/workers/tasks.py
8.63 kB
| """๋น๋๊ธฐ ๋ณํ ํ์ดํ๋ผ์ธ (Celery) โ ๋ฉฑ๋ฑ + lease fencing. | |
| API ์๋ฒ๋ enqueue ๋ง ํ๊ณ ์ฆ์ ์๋ตํ๋ค. ์ด ํ์คํฌ๋: | |
| - ์ด๋ฏธ ์ข ๋ฃ ์ํ(SUCCEEDED/EXPIRED/DELETED)๋ฉด ์ฆ์ return (๋ฉฑ๋ฑ) | |
| - QUEUED ์ธ task ๋ง lock ํ๋(LockContext ๋ฐ๊ธ). ์ด๋ฏธ lock ์ด ์ด์ ์์ผ๋ฉด no-op | |
| - ๋ชจ๋ ์ํ ์ฐ๊ธฐ๋ (locked_by, lock_token, lock_version) fencing ์ผ๋ก ๋ณดํธ | |
| โ reaper/๋ค๋ฅธ worker ์๊ฒ lock ์ด ๋์ด๊ฐ ๋ค ๋ฆ๊ฒ ์ด์๋ worker ๋ ์ด๋ค ๊ฒ๋ ๋ชป ์ด๋ค | |
| - ๊ฒฐ๊ณผ๋ attempt-scoped ๊ฒฝ๋ก์ ์ ์ฅํ๊ณ , SUCCEEDED ์ ์ด์ ์ฑ๊ณตํ worker ๋ง promotion | |
| ํ๋ฆ: | |
| QUEUED โ QC_CHECKING โ QC_PASSED โ AI_PROCESSING | |
| โ POST_PROCESSING โ QA_CHECKING โ SUCCEEDED | |
| ์คํจ ์ ๊ฐ ๋จ๊ณ์์ *_FAILED ๋ก ๋ถ๊ธฐ. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| from app.db.session import SessionLocal | |
| from app.domain.states import TERMINAL_STATES, TaskStatus | |
| from app.services import artifact_repo, task_repo | |
| from app.services.comfyui_client import FaceSwapRequest, get_ai_client | |
| from app.services.photo_qc import get_photo_qc | |
| from app.services.retention import run_retention_cleanup | |
| from app.services.storage import get_storage | |
| from app.services.task_repo import LockContext | |
| from app.services.watermark import apply_ai_watermark | |
| from app.workers.celery_app import celery_app | |
| logger = logging.getLogger(__name__) | |
| STALE = "STALE_ABORT" | |
| def attempt_result_key(task_id: str, attempt_id: str) -> str: | |
| """attempt-scoped ๊ฒฐ๊ณผ ํค. ๋ฎ์ด์ฐ๊ธฐ ๋์ ์๋๋ณ ๋ถ๋ฆฌ โ stale worker ์ถฉ๋ ๋ฐฉ์ง.""" | |
| return f"results/{task_id}/attempt-{attempt_id}.png" | |
| def process_photo(self, task_id: str, dispatch_version: int = 0) -> str: | |
| db = SessionLocal() | |
| worker_id = f"{self.request.hostname or 'worker'}:{self.request.id or os.getpid()}" | |
| ctx: LockContext | None = None | |
| try: | |
| task = task_repo.get_task(db, task_id) | |
| if task is None: | |
| logger.warning("task not found: %s", task_id) | |
| return "NOT_FOUND" | |
| # ๋ฉฑ๋ฑ: ์ด๋ฏธ ์ข ๋ฃ๋ ์์ ์ ์ฌ์ฒ๋ฆฌํ์ง ์๋๋ค. | |
| if task.status in TERMINAL_STATES: | |
| return f"SKIP_TERMINAL:{task.status.value}" | |
| # dispatch fencing: manual retry ๋ฑ์ผ๋ก ์ธ๋๊ฐ ์ฌ๋ผ๊ฐ old dispatch ๋ no-op. | |
| if task.dispatch_version != dispatch_version: | |
| logger.info( | |
| "stale dispatch %s: payload dv=%s != task dv=%s", | |
| task_id, dispatch_version, task.dispatch_version, | |
| ) | |
| return "SKIP_STALE_DISPATCH" | |
| # lock: QUEUED + ๊ฐ์ dispatch_version ์ธ task ๋ง ํ๋(fencing context ๋ฐ๊ธ). | |
| ctx = task_repo.acquire_lock( | |
| db, task_id, worker_id, dispatch_version=dispatch_version | |
| ) | |
| if ctx is None: | |
| logger.info("no-op (lock held or not QUEUED): %s", task_id) | |
| return "SKIP_LOCKED" | |
| db.refresh(task) | |
| storage = get_storage() | |
| qc = get_photo_qc() | |
| ai = get_ai_client() | |
| # 1) QUEUED -> QC_CHECKING | |
| if not task_repo.fenced_transition( | |
| db, ctx, TaskStatus.QC_CHECKING, expected=TaskStatus.QUEUED | |
| ): | |
| return STALE | |
| original = storage.get_bytes(task.original_key) | |
| # ํฐ ์ฌ์ง์ EXIF ๋ฐฉํฅ ํ๊ทธ๋ก ํฝ์ ์ด ๋ํ์ ธ ์ ์ฅ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค. AI ์์ง์ | |
| # ๋ด๋ถ์์ ๋ฐฉํฅ์ ์ ๊ทํ(upright)ํด ๊ฒฐ๊ณผ๋ฅผ ๋ด๋ฏ๋ก, ์ฌ๊ธฐ์ ์๋ณธ๋ ๋ฏธ๋ฆฌ ์ ๊ทํํ์ง | |
| # ์์ผ๋ฉด QC/AI/QA/์ถ๋ ฅ์ด ์๋ก ๋ค๋ฅธ ๋ฐฉํฅ(์ ์น๋ ํฌ๊ธฐ)์ ๋ณด๊ฒ ๋์ด ์ ์ ๊ฒฐ๊ณผ๊ฐ | |
| # OUTPUT_SIZE_MISMATCH ๋ก ์๋ชป ๊ฑธ๋ฆฐ๋ค. ์ ์ฌ ์งํ ํ ๋ฒ๋ง ์ ๊ทํํด ๋ชจ๋ ์ผ์น์ํจ๋ค. | |
| from app.services.gemini_client import normalize_image_orientation_bytes | |
| original = normalize_image_orientation_bytes(original) | |
| # 2) ์ ๋ ฅ QC | |
| in_qc = qc.check_input(original, task.original_mime or "image/jpeg") | |
| if not in_qc.passed: | |
| task_repo.fenced_transition( | |
| db, ctx, TaskStatus.QC_FAILED, expected=TaskStatus.QC_CHECKING, | |
| clear_lock=True, error_code=in_qc.code, error_detail=in_qc.detail, | |
| failure_code=in_qc.code, failure_stage="QC_CHECKING", | |
| ) | |
| return TaskStatus.QC_FAILED.value | |
| if not task_repo.fenced_transition( | |
| db, ctx, TaskStatus.QC_PASSED, expected=TaskStatus.QC_CHECKING | |
| ): | |
| return STALE | |
| # 3) AI ๋ณํ (์ผ๊ตด ์์ญ๋ง) | |
| if not task_repo.fenced_transition( | |
| db, ctx, TaskStatus.AI_PROCESSING, expected=TaskStatus.QC_PASSED | |
| ): | |
| return STALE | |
| try: | |
| ai_res = ai.face_only_transform( | |
| FaceSwapRequest( | |
| image_bytes=original, | |
| mime=task.original_mime or "image/jpeg", | |
| face_boxes=in_qc.face_boxes, | |
| requested_gender=task.requested_gender, | |
| beauty_level=task.beauty_level or "balanced", | |
| ) | |
| ) | |
| except Exception as exc: # noqa: BLE001 | |
| logger.exception("AI transform failed") | |
| code = getattr(exc, "code", "AI_ERROR") | |
| task_repo.fenced_transition( | |
| db, ctx, TaskStatus.AI_FAILED, expected=TaskStatus.AI_PROCESSING, | |
| clear_lock=True, error_code=code, error_detail=str(exc), | |
| failure_code=code, failure_message=str(exc), | |
| failure_stage="AI_PROCESSING", | |
| ) | |
| return TaskStatus.AI_FAILED.value | |
| # 4) ํ์ฒ๋ฆฌ (๋ฆฌ์ฌ์ด์ฆ/์๋ณด์ ์๋ฆฌ) โ mock ํต๊ณผ | |
| if not task_repo.fenced_transition( | |
| db, ctx, TaskStatus.POST_PROCESSING, expected=TaskStatus.AI_PROCESSING | |
| ): | |
| return STALE | |
| result_bytes = ai_res.image_bytes | |
| # 5) ๊ฒฐ๊ณผ QA โ ์ผ๊ตด ์ธ ์์ญ ๋ณด์กด ๊ฒ์ฆ | |
| if not task_repo.fenced_transition( | |
| db, ctx, TaskStatus.QA_CHECKING, expected=TaskStatus.POST_PROCESSING | |
| ): | |
| return STALE | |
| out_qc = qc.check_output(original, result_bytes) | |
| if not out_qc.passed: | |
| task_repo.fenced_transition( | |
| db, ctx, TaskStatus.QA_FAILED, expected=TaskStatus.QA_CHECKING, | |
| clear_lock=True, error_code=out_qc.code, error_detail=out_qc.detail, | |
| failure_code=out_qc.code, failure_stage="QA_CHECKING", | |
| ) | |
| return TaskStatus.QA_FAILED.value | |
| # 6) AI ๊ณ ์ง ์ํฐ๋งํฌ + ๋ฉํ๋ฐ์ดํฐ (stub/real ๋์ผ). ๊ณต์ /๋ค์ด๋ก๋๋๋ | |
| # ์ต์ข ์ด๋ฏธ์ง์๋ง ์ ์ฉํ๋ฉฐ, ์ผ๊ตด/ํค์ด๋ฅผ ๊ฐ๋ฆฌ์ง ์๋ ์ฌ๋ฐฑ ๋ชจ์๋ฆฌ์ ํ์ํ๋ค. | |
| result_bytes = apply_ai_watermark(result_bytes) | |
| # 7) attempt-scoped ๊ฒฝ๋ก์ ์ ์ฅ + artifact TEMP ๊ธฐ๋ก | |
| # (stale worker ๋ ์๊ธฐ attempt ๊ฒฝ๋ก์๋ง ์ด๋ค โ ์ถฉ๋ ์์) | |
| result_key = attempt_result_key(task.task_id, ctx.current_attempt_id) | |
| storage.put_bytes(result_key, result_bytes, "image/png") | |
| result_url = storage.public_url(result_key) | |
| artifact_repo.record_temp(db, task.task_id, ctx.current_attempt_id, result_key) | |
| # 7) SUCCEEDED ์ ์ด + result_key promotion (์์์ , fencing). | |
| # ์ฑ๊ณตํ worker ๋ง artifact ๋ฅผ PROMOTED ๋ก ์น๊ฒฉํ๊ณ , stale ์ STALE ๋ก ํ์. | |
| if not task_repo.fenced_succeed( | |
| db, ctx, result_key=result_key, result_url=result_url | |
| ): | |
| logger.info("lost SUCCEEDED race (stale): %s", task_id) | |
| artifact_repo.mark_stale(db, task.task_id, ctx.current_attempt_id) | |
| return STALE | |
| artifact_repo.mark_promoted(db, task.task_id, ctx.current_attempt_id) | |
| return TaskStatus.SUCCEEDED.value | |
| except Exception as exc: # noqa: BLE001 | |
| logger.exception("pipeline crashed for %s", task_id) | |
| if ctx is not None: | |
| try: | |
| task_repo.release_lock(db, ctx) | |
| except Exception: # noqa: BLE001 | |
| pass | |
| raise self.retry(exc=exc, countdown=5) | |
| finally: | |
| db.close() | |
| def retention_cleanup() -> dict: | |
| db = SessionLocal() | |
| try: | |
| return run_retention_cleanup(db) | |
| finally: | |
| db.close() | |