File size: 3,792 Bytes
9344f01
d92e749
9344f01
 
617c061
9344f01
 
 
617c061
9344f01
 
d92e749
9344f01
 
d92e749
 
 
 
9344f01
 
 
264396f
d92e749
617c061
9344f01
 
 
 
 
617c061
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9344f01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3264ba1
 
9344f01
00d343f
 
9344f01
 
 
 
 
 
 
 
1a36a19
9344f01
 
1a36a19
 
 
617c061
 
9344f01
 
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
"""HF Spaces (Gradio SDK + ZeroGPU) 진입점.

HF Spaces 빌더가 자동으로 `python app.py` 를 실행한다. 로컬에서도 같은
파일로 미리보기 가능:

    pip install -e ".[dev,llm,hf]"
    KPAA_LLM_BACKEND=llama_cpp python app.py    # 로컬 GGUF 로 UI 만 미리보기
    # → http://127.0.0.1:7860

HF Spaces 환경에서는 자동으로 `SPACE_ID` 가 잡혀 ZeroGPU 백엔드가 활성화된다.
LAW_OC 는 Space Settings > Secrets 에 등록.
"""
from __future__ import annotations

import os
import sys
from pathlib import Path

# HF Spaces 에서는 `pip install -e .` 가 동작하지 않는다 (requirements.txt 처리
# 시점에 app 파일이 아직 mount 되지 않음). 대신 src/ 를 sys.path 에 prepend.
# 로컬 editable install 환경에서도 무해.
sys.path.insert(0, str(Path(__file__).resolve().parent / "src"))


# ─── monkey-patch: Gradio /api_info schema bug ────────────────────────────
# Gradio 5.x 의 gradio_client.utils 가 JSON Schema 의 `additionalProperties: True`
# (bool, 합법적 형식) 를 dict 로만 가정해서 `if "const" in schema:` 에서 TypeError.
# get_type 와 _json_schema_to_python_type 모두 bool 입력을 안전하게 처리하도록 wrap.
import gradio_client.utils as _gc_utils  # noqa: E402

_orig_get_type = _gc_utils.get_type
_orig_jstpt = _gc_utils._json_schema_to_python_type


def _safe_get_type(schema):
    if not isinstance(schema, dict):
        return ""
    return _orig_get_type(schema)


def _safe_jstpt(schema, defs):
    if not isinstance(schema, dict):
        return "Any"
    return _orig_jstpt(schema, defs)


_gc_utils.get_type = _safe_get_type
_gc_utils._json_schema_to_python_type = _safe_jstpt
# ──────────────────────────────────────────────────────────────────────────


# ─── HF Spaces ZeroGPU startup canary ─────────────────────────────────────
# HF Spaces 의 ZeroGPU 는 startup 시점에 module-level `@spaces.GPU` 함수가
# 적어도 하나 검출되어야 GPU 스케줄을 잡는다. 실제 GPU 작업은
# ZeroGPUBackend.stream_chat 안의 `_run_generate` 에서 일어나지만, 그건 함수
# 호출 시점에야 데코레이트되므로 startup 스캔에서 안 보임.
# 본 카나리는 호출되지 않으며, 단지 detector 통과용.
try:
    import spaces  # type: ignore[import-not-found]

    @spaces.GPU(duration=1)
    def _zerogpu_startup_canary() -> None:
        """HF Spaces ZeroGPU detector 통과용 sentinel."""
        return None
except ImportError:
    pass  # 로컬 dev — spaces 패키지 없음
# ──────────────────────────────────────────────────────────────────────────


from kpaa.ui.gradio import build_app  # noqa: E402


def main() -> None:
    app = build_app()
    # HF Spaces 는 7860 노출 표준. 로컬 미리보기도 동일 포트 사용.
    port = int(os.environ.get("PORT", "7860"))
    # 큐 활성화 — async generator (스트리밍) 이 작동하려면 필수.
    # ssr_mode=False  — Node SSR 서브프로세스 없이 순수 uvicorn 으로 단일 프로세스화.
    # show_api=False  — /api_info 노출 스킵 (위 monkey-patch 와 함께 belt-and-suspenders).
    app.queue(max_size=20).launch(
        server_name="0.0.0.0",
        server_port=port,
        show_error=True,
        ssr_mode=False,
        show_api=False,
    )


if __name__ == "__main__":
    main()