akagtag commited on
Commit
21f188b
Β·
1 Parent(s): 80a3e32

Serve frontend API routes via Gradio Server

Browse files
Files changed (4) hide show
  1. app.py +78 -99
  2. src/api/main.py +20 -0
  3. tests/test_api.py +28 -0
  4. tests/test_zero_gpu_contract.py +7 -5
app.py CHANGED
@@ -1,27 +1,21 @@
1
  from __future__ import annotations
2
 
3
- import io
4
- import mimetypes
5
  import os
6
  import sys
7
  import traceback
8
- from pathlib import Path
9
 
10
- import gradio as gr
11
- import uvicorn
12
- from fastapi import HTTPException, UploadFile
13
- from starlette.datastructures import Headers
14
 
15
- from src.api.main import IMAGE_TYPES, VIDEO_TYPES, app as api_app
16
- from src.api.main import detect_image, detect_video
17
-
18
-
19
- def _is_hf_space() -> bool:
20
- return os.environ.get("SPACE_ID", "").startswith("akagtag/")
21
-
22
-
23
- def _is_test_mode() -> bool:
24
- return "PYTEST_CURRENT_TEST" in os.environ or "pytest" in sys.modules
25
 
26
 
27
  def _install_excepthook() -> None:
@@ -31,93 +25,78 @@ def _install_excepthook() -> None:
31
  sys.excepthook = handle_exception
32
 
33
 
34
- def _build_upload(path: str) -> UploadFile:
35
- file_path = Path(path)
36
- content_type = (mimetypes.guess_type(file_path.name)[0] or "application/octet-stream").lower()
37
- return UploadFile(
38
- filename=file_path.name,
39
- file=io.BytesIO(file_path.read_bytes()),
40
- headers=Headers({"content-type": content_type}),
41
- )
42
 
43
 
44
- def _response_to_markdown(response) -> tuple[str, str, dict]:
45
- summary = (
46
- f"## {response.verdict}\n"
47
- f"Confidence: {response.confidence:.3f}\n\n"
48
- f"Attributed generator: `{response.attributed_generator}`\n"
49
- f"Processing time: `{response.processing_time_ms:.2f} ms`"
50
- )
51
- return summary, response.explanation, response.model_dump()
52
-
53
-
54
- async def analyze_media(file_path: str | None) -> tuple[str, str, dict]:
55
- if not file_path:
56
- return "Upload an image or video.", "", {}
57
-
58
- upload = _build_upload(file_path)
59
- content_type = (upload.content_type or "").split(";")[0].strip().lower()
60
-
61
- try:
62
- if content_type in IMAGE_TYPES:
63
- response = await detect_image(upload)
64
- elif content_type in VIDEO_TYPES:
65
- response = await detect_video(upload)
66
- else:
67
- return f"Unsupported file type: `{content_type or 'unknown'}`", "", {}
68
- except HTTPException as exc:
69
- return f"Request failed: `{exc.status_code}`", str(exc.detail), {}
70
- except Exception as exc:
71
- return "Analysis failed.", str(exc), {}
72
-
73
- return _response_to_markdown(response)
74
-
75
-
76
- with gr.Blocks(
77
- title="GenAI-DeepDetect",
78
- theme=gr.themes.Base(primary_hue="red", font=["DM Sans", "sans-serif"]),
79
- ) as demo:
80
- gr.Markdown(
81
- "# GenAI-DeepDetect\n"
82
- "Gradio runs at the Space root.\n"
83
- "The same detection backend powers both the UI and API routes."
84
- )
85
 
86
- with gr.Row():
87
- with gr.Column(scale=1):
88
- media = gr.File(
89
- label="Upload Image or Video",
90
- type="filepath",
91
- file_types=["image", "video"],
92
- )
93
- analyze_btn = gr.Button("Analyze", variant="primary")
94
- with gr.Column(scale=2):
95
- verdict = gr.Markdown(label="Verdict")
96
- explanation = gr.Markdown(label="Explanation")
97
-
98
- details = gr.JSON(label="Detection Response")
99
- analyze_btn.click(
100
- fn=analyze_media,
101
- inputs=[media],
102
- outputs=[verdict, explanation, details],
103
- )
104
 
 
 
 
105
 
106
- app = gr.mount_gradio_app(
107
- api_app,
108
- demo,
109
- path="/",
110
- show_error=True,
111
- ssr_mode=False,
112
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
 
115
  if __name__ == "__main__":
116
- if _is_hf_space() and not _is_test_mode():
117
- _install_excepthook()
118
- demo.launch(
119
- show_error=True,
120
- ssr_mode=False,
121
- )
122
- else:
123
- uvicorn.run(app, host="0.0.0.0", port=7860, workers=1)
 
1
  from __future__ import annotations
2
 
 
 
3
  import os
4
  import sys
5
  import traceback
 
6
 
7
+ from fastapi import File, UploadFile
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.responses import HTMLResponse, RedirectResponse
10
+ from gradio import Server
11
 
12
+ from src.api.demo_page import render_demo
13
+ from src.api.main import detect_image as api_detect_image
14
+ from src.api.main import detect_video as api_detect_video
15
+ from src.api.main import health as api_health
16
+ from src.api.main import health_models as api_health_models
17
+ from src.api.main import preload as api_preload
18
+ from src.types import DetectionResponse
 
 
 
19
 
20
 
21
  def _install_excepthook() -> None:
 
25
  sys.excepthook = handle_exception
26
 
27
 
28
+ app = Server()
29
+ app.add_middleware(
30
+ CORSMiddleware,
31
+ allow_origins=["*"],
32
+ allow_methods=["*"],
33
+ allow_headers=["*"],
34
+ )
 
35
 
36
 
37
+ @app.api(name="ping")
38
+ def ping() -> str:
39
+ return "ok"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ @app.on_event("startup")
43
+ async def startup() -> None:
44
+ await api_preload()
45
 
46
+
47
+ @app.get("/", response_class=HTMLResponse)
48
+ async def root() -> HTMLResponse:
49
+ return HTMLResponse(render_demo())
50
+
51
+
52
+ @app.get("/gradio")
53
+ async def gradio_compat_redirect() -> RedirectResponse:
54
+ return RedirectResponse(url="/", status_code=307)
55
+
56
+
57
+ @app.get("/health")
58
+ async def health() -> dict:
59
+ return await api_health()
60
+
61
+
62
+ @app.get("/api/health")
63
+ async def api_health() -> dict:
64
+ return await health()
65
+
66
+
67
+ @app.get("/health/models")
68
+ async def health_models() -> dict[str, object]:
69
+ return await api_health_models()
70
+
71
+
72
+ @app.get("/api/health/models")
73
+ async def api_health_models() -> dict[str, object]:
74
+ return await health_models()
75
+
76
+
77
+ @app.post("/detect/image", response_model=DetectionResponse)
78
+ async def detect_image(file: UploadFile = File(...)) -> DetectionResponse:
79
+ return await api_detect_image(file)
80
+
81
+
82
+ @app.post("/api/detect/image", response_model=DetectionResponse)
83
+ async def api_detect_image_route(file: UploadFile = File(...)) -> DetectionResponse:
84
+ return await detect_image(file)
85
+
86
+
87
+ @app.post("/detect/video", response_model=DetectionResponse)
88
+ async def detect_video(file: UploadFile = File(...)) -> DetectionResponse:
89
+ return await api_detect_video(file)
90
+
91
+
92
+ @app.post("/api/detect/video", response_model=DetectionResponse)
93
+ async def api_detect_video_route(file: UploadFile = File(...)) -> DetectionResponse:
94
+ return await detect_video(file)
95
 
96
 
97
  if __name__ == "__main__":
98
+ _install_excepthook()
99
+ app.launch(
100
+ show_error=True,
101
+ ssr_mode=False,
102
+ )
 
 
 
src/api/main.py CHANGED
@@ -419,12 +419,22 @@ async def health() -> dict:
419
  }
420
 
421
 
 
 
 
 
 
422
  @app.get("/health/models")
423
  async def health_models() -> dict[str, object]:
424
  """Return the pretrained model inventory used by each engine."""
425
  return _model_inventory()
426
 
427
 
 
 
 
 
 
428
  def _assign_processing_time(results: list[EngineResult], ms: float) -> list[EngineResult]:
429
  for result in results:
430
  result.processing_time_ms = round(ms, 2)
@@ -627,6 +637,11 @@ async def detect_image(file: UploadFile = File(...)) -> DetectionResponse:
627
  )
628
 
629
 
 
 
 
 
 
630
  @app.post("/detect/video", response_model=DetectionResponse)
631
  async def detect_video(file: UploadFile = File(...)) -> DetectionResponse:
632
  t0 = time.monotonic()
@@ -694,3 +709,8 @@ async def detect_video(file: UploadFile = File(...)) -> DetectionResponse:
694
  metadata_text,
695
  t0,
696
  )
 
 
 
 
 
 
419
  }
420
 
421
 
422
+ @app.get("/api/health")
423
+ async def api_health() -> dict:
424
+ return await health()
425
+
426
+
427
  @app.get("/health/models")
428
  async def health_models() -> dict[str, object]:
429
  """Return the pretrained model inventory used by each engine."""
430
  return _model_inventory()
431
 
432
 
433
+ @app.get("/api/health/models")
434
+ async def api_health_models() -> dict[str, object]:
435
+ return await health_models()
436
+
437
+
438
  def _assign_processing_time(results: list[EngineResult], ms: float) -> list[EngineResult]:
439
  for result in results:
440
  result.processing_time_ms = round(ms, 2)
 
637
  )
638
 
639
 
640
+ @app.post("/api/detect/image", response_model=DetectionResponse)
641
+ async def api_detect_image(file: UploadFile = File(...)) -> DetectionResponse:
642
+ return await detect_image(file)
643
+
644
+
645
  @app.post("/detect/video", response_model=DetectionResponse)
646
  async def detect_video(file: UploadFile = File(...)) -> DetectionResponse:
647
  t0 = time.monotonic()
 
709
  metadata_text,
710
  t0,
711
  )
712
+
713
+
714
+ @app.post("/api/detect/video", response_model=DetectionResponse)
715
+ async def api_detect_video(file: UploadFile = File(...)) -> DetectionResponse:
716
+ return await detect_video(file)
tests/test_api.py CHANGED
@@ -48,6 +48,18 @@ def test_health_models_returns_inventory(client):
48
  assert "stable_diffusion" in data["generator_labels"]
49
 
50
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  # ── GET / ─────────────────────────────────────────────────────────────────────
52
 
53
  def test_gradio_compat_redirect(client):
@@ -126,6 +138,14 @@ def test_detect_image_processing_time_positive(client, jpeg_bytes):
126
  assert data["processing_time_ms"] >= 0
127
 
128
 
 
 
 
 
 
 
 
 
129
  # ── POST /detect/video ────────────────────────────────────────────────────────
130
 
131
  def test_detect_video_wrong_type_returns_415(client, jpeg_bytes):
@@ -134,3 +154,11 @@ def test_detect_video_wrong_type_returns_415(client, jpeg_bytes):
134
  files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")},
135
  )
136
  assert r.status_code == 415
 
 
 
 
 
 
 
 
 
48
  assert "stable_diffusion" in data["generator_labels"]
49
 
50
 
51
+ def test_api_prefixed_health_returns_200(client):
52
+ r = client.get("/api/health")
53
+ assert r.status_code == 200
54
+
55
+
56
+ def test_api_prefixed_health_models_returns_inventory(client):
57
+ data = client.get("/api/health/models").json()
58
+ assert "fingerprint" in data
59
+ assert "coherence" in data
60
+ assert "sstgnn" in data
61
+
62
+
63
  # ── GET / ─────────────────────────────────────────────────────────────────────
64
 
65
  def test_gradio_compat_redirect(client):
 
138
  assert data["processing_time_ms"] >= 0
139
 
140
 
141
+ def test_api_prefixed_detect_image_returns_200(client, jpeg_bytes):
142
+ r = client.post(
143
+ "/api/detect/image",
144
+ files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")},
145
+ )
146
+ assert r.status_code == 200
147
+
148
+
149
  # ── POST /detect/video ────────────────────────────────────────────────────────
150
 
151
  def test_detect_video_wrong_type_returns_415(client, jpeg_bytes):
 
154
  files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")},
155
  )
156
  assert r.status_code == 415
157
+
158
+
159
+ def test_api_prefixed_detect_video_wrong_type_returns_415(client, jpeg_bytes):
160
+ r = client.post(
161
+ "/api/detect/video",
162
+ files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")},
163
+ )
164
+ assert r.status_code == 415
tests/test_zero_gpu_contract.py CHANGED
@@ -34,13 +34,15 @@ def test_readme_declares_gradio_space_metadata():
34
  def test_app_mounts_gradio_onto_fastapi():
35
  source = (ROOT / "app.py").read_text(encoding="utf-8")
36
 
37
- assert "from src.api.main import IMAGE_TYPES, VIDEO_TYPES, app as api_app" in source
38
- assert "app = gr.mount_gradio_app(" in source
39
- assert 'path="/"' in source
40
- assert 'demo.launch(' in source
 
 
41
  assert 'show_error=True' in source
42
  assert 'ssr_mode=False' in source
43
- assert 'uvicorn.run(app, host="0.0.0.0", port=7860, workers=1)' in source
44
 
45
 
46
  def test_api_gradio_compat_redirect_exists():
 
34
  def test_app_mounts_gradio_onto_fastapi():
35
  source = (ROOT / "app.py").read_text(encoding="utf-8")
36
 
37
+ assert "from gradio import Server" in source
38
+ assert "app = Server()" in source
39
+ assert '@app.get("/api/health")' in source
40
+ assert '@app.post("/api/detect/image"' in source
41
+ assert '@app.post("/api/detect/video"' in source
42
+ assert "app.launch(" in source
43
  assert 'show_error=True' in source
44
  assert 'ssr_mode=False' in source
45
+ assert "uvicorn.run" not in source
46
 
47
 
48
  def test_api_gradio_compat_redirect_exists():