File size: 3,217 Bytes
7d07e42
 
 
0a0d999
 
 
 
 
 
7d07e42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a0d999
7d07e42
 
 
 
 
 
 
 
 
 
 
 
 
 
0a0d999
 
 
 
 
7d07e42
 
 
 
02f4591
7d07e42
 
 
 
0a0d999
 
 
7d07e42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a0d999
7d07e42
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""
CV Pipeline API — main app.

Startup strategy (HF free CPU):
- API startup: INSTANT
- /health langsung ready
- Prewarm sequential: OCR → YOLO → Captioner (semua instant/cepat)
- CLIP tetap pure lazy — hanya di-load saat ada classification request
- Frontend polling /ready → tombol aktif saat model siap
"""

from __future__ import annotations

import os
import sys
import time
import threading

os.environ.setdefault("ANONYMIZED_TELEMETRY", "False")
os.environ.setdefault("CHROMA_TELEMETRY_ENABLED", "False")
os.environ.setdefault("POSTHOG_DISABLED", "1")

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from loguru import logger

from .routes import router, get_pipeline
from .readiness import get_readiness
from ..config import get_cv_settings

settings = get_cv_settings()

logger.remove()
logger.add(
    sys.stderr,
    level="INFO",
    colorize=True,
    format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan> - {message}",
)
logger.add("./logs/cv_api.log", rotation="10 MB", retention="7 days")

app = FastAPI(
    title="CV Pipeline API",
    version="1.4.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(router, prefix="/api/v1")


def _sequential_prewarm():
    """
    Load komponen satu per satu.
    OCR/Tesseract: instant
    YOLO ONNX: ~0.1s (12MB, baked di image)
    Captioner (Groq): instant (cuma init httpx client)
    CLIP: pure lazy, tidak di-prewarm
    """
    readiness = get_readiness()
    pipeline = get_pipeline()

    startup_delay = int(os.environ.get("CV_PREWARM_DELAY", "0"))
    logger.info(f"Sequential prewarm dimulai dalam {startup_delay}s...")
    time.sleep(startup_delay)

    targets = [
        ("ocr",       lambda: pipeline.ocr),
        ("yolo",      lambda: pipeline.yolo),
        ("captioner", lambda: pipeline.captioner),
    ]

    for name, getter in targets:
        if readiness.get_status(name).state.value == "ready":
            logger.info(f"  {name}: sudah ready, skip")
            continue
        try:
            readiness.mark_loading(name)
            t0 = time.perf_counter()
            logger.info(f"  loading {name}...")
            _ = getter()
            elapsed = time.perf_counter() - t0
            readiness.mark_ready(name)
            logger.info(f"  {name} ready ({elapsed:.1f}s)")
        except Exception as e:
            readiness.mark_error(name, str(e))
            logger.error(f"  {name} failed: {e}")

    snap = readiness.snapshot()
    logger.info(f"Prewarm selesai. State: {snap['overall']}")


@app.on_event("startup")
async def startup():
    logger.info("CV Pipeline API starting up...")
    logger.info(f"Docs: http://{settings.api_host}:{settings.api_port}/docs")

    prewarm = os.environ.get("CV_PREWARM", "true").lower()
    if prewarm == "false":
        logger.info("CV_PREWARM=false — pure lazy-load mode.")
        return

    logger.info("Starting sequential background prewarm...")
    thread = threading.Thread(
        target=_sequential_prewarm,
        daemon=True,
        name="cv-prewarm-sequential",
    )
    thread.start()