Adibrino commited on
Commit
b31d7cd
·
1 Parent(s): 02ac074
Files changed (2) hide show
  1. .env +1 -1
  2. app.py +95 -95
.env CHANGED
@@ -1,4 +1,4 @@
1
  MODEL_NAME=adibrino/LAPOR-AI
2
  ALLOWED_ORIGINS=https://lalim.vercel.app,http://localhost:8000,http://127.0.0.1:8000
3
- SERVICE_CODES_MAP="{'DPRKPCK': 'Perumahan Rakyat, Kawasan Permukiman dan Cipta Karya', 'DPUBM': 'Pekerjaan Umum Bina Marga', 'DPUSDA': 'Pekerjaan Umum Sumber Daya Air', 'DLH': 'Lingkungan Hidup', 'DINSOS': 'Sosial', 'BPBD': 'Penanggulangan Bencana Daerah', 'DISHUB': 'Perhubungan', 'DINKES': 'Kesehatan', 'SATPOLPP': 'Satuan Polisi Pamong Praja', 'DISKOMINFO': 'Komunikasi dan Informatika', 'DISNAKERTRANS': 'Tenaga Kerja dan Transmigrasi', 'DIPERTAKP': 'Pertanian dan Ketahanan Pangan', 'DISNAK': 'Peternakan', 'DKP': 'Kelautan dan Perikanan', 'DINDIK': 'Pendidikan', 'DISBUDPAR': 'Kebudayaan dan Pariwisata', 'DISPERINDAG': 'Perindustrian dan Perdagangan', 'DPMPTSP': 'Penanaman Modal dan Pelayanan Terpadu Satu Pintu', 'DISKOPUKM': 'Koperasi, Usaha Kecil dan Menengah', 'DISPORA': 'Kepemudaan dan Olahraga', 'DISPERPUSIP': 'Perpustakaan dan Kearsipan', 'BAPPEDA': 'Perencanaan Pembangunan Daerah', 'BAPENDA': 'Pajak dan Pendapatan Daerah', 'DP3AK': 'Pemberdayaan Perempuan, Perlindungan Anak dan Kependudukan'}"
4
  IS_PRODUCTION=false
 
1
  MODEL_NAME=adibrino/LAPOR-AI
2
  ALLOWED_ORIGINS=https://lalim.vercel.app,http://localhost:8000,http://127.0.0.1:8000
3
+ SERVICE_CODES_MAP={"DPRKPCK": "Perumahan Rakyat, Kawasan Permukiman dan Cipta Karya", "DPUBM": "Pekerjaan Umum Bina Marga", "DPUSDA": "Pekerjaan Umum Sumber Daya Air", "DLH": "Lingkungan Hidup", "DINSOS": "Sosial", "BPBD": "Penanggulangan Bencana Daerah", "DISHUB": "Perhubungan", "DINKES": "Kesehatan", "SATPOLPP": "Satuan Polisi Pamong Praja", "DISKOMINFO": "Komunikasi dan Informatika", "DISNAKERTRANS": "Tenaga Kerja dan Transmigrasi", "DIPERTAKP": "Pertanian dan Ketahanan Pangan", "DISNAK": "Peternakan", "DKP": "Kelautan dan Perikanan", "DINDIK": "Pendidikan", "DISBUDPAR": "Kebudayaan dan Pariwisata", "DISPERINDAG": "Perindustrian dan Perdagangan", "DPMPTSP": "Penanaman Modal dan Pelayanan Terpadu Satu Pintu", "DISKOPUKM": "Koperasi, Usaha Kecil dan Menengah", "DISPORA": "Kepemudaan dan Olahraga", "DISPERPUSIP": "Perpustakaan dan Kearsipan", "BAPPEDA": "Perencanaan Pembangunan Daerah", "BAPENDA": "Pajak dan Pendapatan Daerah", "DP3AK": "Pemberdayaan Perempuan, Perlindungan Anak dan Kependudukan"}
4
  IS_PRODUCTION=false
app.py CHANGED
@@ -9,13 +9,13 @@ import shutil
9
  from typing import List, Any, Dict, Union
10
 
11
  from fastapi import FastAPI, UploadFile, File, Form
12
- from fastapi.responses import JSONResponse
13
  from fastapi.middleware.cors import CORSMiddleware
14
  import uvicorn
15
  from PIL import Image
16
  from dotenv import load_dotenv
17
  import ollama
18
- import spaces
19
  import gradio as gr
20
 
21
  load_dotenv()
@@ -25,15 +25,9 @@ MODEL_NAME: str = os.getenv("MODEL_NAME") or "adibrino/LAPOR-AI"
25
  IS_PRODUCTION: str = os.getenv("IS_PRODUCTION", "false")
26
 
27
  SERVICE_MAP_STR = os.getenv("SERVICE_CODES_MAP", "{}")
28
- try:
29
- SERVICE_MAP = json.loads(SERVICE_MAP_STR)
30
- except json.JSONDecodeError:
31
- SERVICE_MAP = {}
32
 
33
- if ALLOWED_ORIGINS_RAW == "*":
34
- ALLOWED_ORIGINS = ["*"]
35
- else:
36
- ALLOWED_ORIGINS = [origin.strip() for origin in ALLOWED_ORIGINS_RAW.split(",")]
37
 
38
  print(f"ALLOWED_ORIGINS: {ALLOWED_ORIGINS}")
39
  print(f"MODEL_NAME: {MODEL_NAME}")
@@ -85,7 +79,7 @@ def process_image_to_base64(image_bytes: bytes) -> Union[str, None]:
85
  return None
86
 
87
  @spaces.GPU(duration=60)
88
- def run_inference(text_laporan: str, base64_images: List[str]) -> Dict[str, Any]:
89
  print("Starting GPU Inference...")
90
 
91
  try:
@@ -94,11 +88,11 @@ def run_inference(text_laporan: str, base64_images: List[str]) -> Dict[str, Any]
94
  print("Model not found in GPU context, pulling again...")
95
  subprocess.run(["ollama", "pull", MODEL_NAME], check=True)
96
 
97
- response: Any = ollama.chat(
98
  model=MODEL_NAME,
99
  messages=[{
100
  'role': 'user',
101
- 'content': text_laporan,
102
  'images': base64_images if base64_images else None # type: ignore
103
  }],
104
  format='json',
@@ -106,105 +100,111 @@ def run_inference(text_laporan: str, base64_images: List[str]) -> Dict[str, Any]
106
  )
107
 
108
  if isinstance(response, dict):
109
- return response
110
  return dict(response)
111
 
112
  @app.get("/")
113
  def health_check():
114
- return {"status": "Python Backend with ZeroGPU is running."}
115
 
116
  @app.post("/api/analyze")
117
- async def analyze(
118
- laporan: str = Form(...),
119
  images: List[UploadFile] = File(...)
120
  ):
121
- if not laporan or len(laporan) < 10:
122
- return JSONResponse(
123
- status_code=400,
124
- content={"status": "error", "message": "Deskripsi laporan wajib diisi minimal 10 karakter."}
125
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- if not images:
128
  return JSONResponse(
129
- status_code=400,
130
- content={"status": "error", "message": "Wajib melampirkan minimal 1 foto bukti."}
131
  )
132
-
133
- base64_images: List[str] = []
134
- for img_file in images:
135
- content = await img_file.read()
136
- if len(content) > 0:
137
- b64 = process_image_to_base64(content)
138
- if b64:
139
- base64_images.append(b64)
140
 
141
- if not base64_images:
142
- return JSONResponse(
143
- status_code=400,
144
- content={"status": "error", "message": "File gambar tidak valid/corrupt."}
145
- )
146
-
147
- max_retries = 3
148
- last_exception = None
149
-
150
- for attempt in range(max_retries):
151
- try:
152
- print(f"Attempting AI Analysis... ({attempt + 1}/{max_retries})")
153
-
154
- response_raw = run_inference(laporan, base64_images)
155
-
156
- if 'message' not in response_raw or 'content' not in response_raw['message']:
157
- raise ValueError("Empty response structure from AI")
158
-
159
- content_str = response_raw['message']['content']
160
-
161
- ai_content = json.loads(content_str)
162
-
163
- required_keys = ["title", "category", "priority", "service_code"]
164
- missing_keys = [key for key in required_keys if key not in ai_content]
165
- if missing_keys:
166
- raise ValueError(f"Missing keys in JSON: {missing_keys}")
167
-
168
- service_code = ai_content["service_code"]
169
- if service_code not in SERVICE_MAP:
170
- print(f"Warning: Service code {service_code} unknown.")
171
-
172
- priority = str(ai_content["priority"]).lower()
173
- if priority not in ['high', 'medium', 'low']:
174
- priority = 'medium'
175
- ai_content["priority"] = priority
176
-
177
- data = {
178
- "status": "success",
179
- "data": ai_content,
180
- "meta": {
181
- "model": MODEL_NAME,
182
- 'processing_time_sec': (response_raw.get("total_duration", 0)) / 1e9,
183
- "images_analyzed": len(base64_images),
184
- "attempts": attempt + 1
185
- }
186
- }
187
-
188
- print("AI Success")
189
- return data
190
-
191
- except Exception as e:
192
- print(f"Attempt {attempt + 1} failed: {str(e)}")
193
- last_exception = e
194
- time.sleep(1)
195
- continue
196
-
197
- return JSONResponse(
198
- status_code=500,
199
- content={"status": "error", "message": f"AI Failed: {str(last_exception)}"}
200
- )
201
-
202
  if __name__ == "__main__":
203
  with gr.Blocks() as demo:
204
  gr.Markdown("# LAPOR AI API Backend")
205
  gr.Markdown("This space hosts the API at `/api/analyze`.")
206
  gr.Markdown(f"**Model:** {MODEL_NAME}")
207
 
208
- app = gr.mount_gradio_app(app, demo, path="/")
209
 
210
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
9
  from typing import List, Any, Dict, Union
10
 
11
  from fastapi import FastAPI, UploadFile, File, Form
12
+ from fastapi.responses import JSONResponse, Response
13
  from fastapi.middleware.cors import CORSMiddleware
14
  import uvicorn
15
  from PIL import Image
16
  from dotenv import load_dotenv
17
  import ollama
18
+ import spaces # type: ignore
19
  import gradio as gr
20
 
21
  load_dotenv()
 
25
  IS_PRODUCTION: str = os.getenv("IS_PRODUCTION", "false")
26
 
27
  SERVICE_MAP_STR = os.getenv("SERVICE_CODES_MAP", "{}")
28
+ SERVICE_MAP = json.loads(SERVICE_MAP_STR)
 
 
 
29
 
30
+ ALLOWED_ORIGINS = ["*"] if ALLOWED_ORIGINS_RAW == "*" else [origin.strip() for origin in ALLOWED_ORIGINS_RAW.split(",")]
 
 
 
31
 
32
  print(f"ALLOWED_ORIGINS: {ALLOWED_ORIGINS}")
33
  print(f"MODEL_NAME: {MODEL_NAME}")
 
79
  return None
80
 
81
  @spaces.GPU(duration=60)
82
+ def run_inference(report_text: str, base64_images: List[str]) -> Dict[str, Any]:
83
  print("Starting GPU Inference...")
84
 
85
  try:
 
88
  print("Model not found in GPU context, pulling again...")
89
  subprocess.run(["ollama", "pull", MODEL_NAME], check=True)
90
 
91
+ response: Any = ollama.chat( # type: ignore
92
  model=MODEL_NAME,
93
  messages=[{
94
  'role': 'user',
95
+ 'content': report_text,
96
  'images': base64_images if base64_images else None # type: ignore
97
  }],
98
  format='json',
 
100
  )
101
 
102
  if isinstance(response, dict):
103
+ return response # type: ignore
104
  return dict(response)
105
 
106
  @app.get("/")
107
  def health_check():
108
+ return Response("Python Backend is running.")
109
 
110
  @app.post("/api/analyze")
111
+ async def analyze( # type: ignore
112
+ report: str = Form(...),
113
  images: List[UploadFile] = File(...)
114
  ):
115
+ try:
116
+ if not report or len(report) < 10:
117
+ return JSONResponse(
118
+ status_code=400,
119
+ content={"status": "error", "message": "Deskripsi laporan wajib diisi minimal 10 karakter."}
120
+ )
121
+
122
+ if not images:
123
+ return JSONResponse(
124
+ status_code=400,
125
+ content={"status": "error", "message": "Wajib melampirkan minimal 1 foto bukti."}
126
+ )
127
+
128
+ base64_images: List[str] = []
129
+ for img_file in images:
130
+ content = await img_file.read()
131
+ if len(content) > 0:
132
+ b64 = process_image_to_base64(content)
133
+ if b64:
134
+ base64_images.append(b64)
135
+
136
+ if not base64_images:
137
+ return JSONResponse(
138
+ status_code=400,
139
+ content={"status": "error", "message": "File gambar tidak valid/corrupt."}
140
+ )
141
+
142
+ max_retries = 3
143
+ last_exception = None
144
+
145
+ print("Report Text:", report)
146
+
147
+ for attempt in range(max_retries):
148
+ try:
149
+ print(f"Attempting AI Analysis... ({attempt + 1}/{max_retries})")
150
+
151
+ response_raw = run_inference(report, base64_images)
152
+
153
+ if 'message' not in response_raw or 'content' not in response_raw['message']:
154
+ raise ValueError("Empty response structure from AI")
155
+
156
+ content_str = response_raw['message']['content']
157
+
158
+ ai_content = json.loads(content_str)
159
+
160
+ required_keys = ["title", "category", "priority", "service_code"]
161
+ missing_keys = [key for key in required_keys if key not in ai_content]
162
+ if missing_keys:
163
+ raise ValueError(f"Missing keys in JSON: {missing_keys}")
164
+
165
+ service_code = ai_content["service_code"]
166
+ if service_code not in SERVICE_MAP:
167
+ print(f"Warning: Service code {service_code} unknown.")
168
+
169
+ priority = str(ai_content["priority"]).lower()
170
+ if priority not in ['high', 'medium', 'low']:
171
+ priority = 'medium'
172
+ ai_content["priority"] = priority
173
+
174
+ data = { # type: ignore
175
+ "status": "success",
176
+ "data": ai_content,
177
+ "meta": {
178
+ "model": MODEL_NAME,
179
+ 'processing_time_sec': (response_raw.get("total_duration", 0)) / 1e9,
180
+ "images_analyzed": len(base64_images),
181
+ "attempts": attempt + 1
182
+ }
183
+ }
184
+
185
+ print("AI Success")
186
+ print(json.dumps(data, indent=2, ensure_ascii=True))
187
+
188
+ return data # type: ignore
189
+ except Exception as e:
190
+ print(f"Attempt {attempt + 1} failed: {str(e)}")
191
+ last_exception = e
192
+ time.sleep(1)
193
+ continue
194
 
 
195
  return JSONResponse(
196
+ status_code=500,
197
+ content={"status": "error", "message": f"AI Failed: {str(last_exception)}"}
198
  )
199
+ except Exception as e:
200
+ raise e
 
 
 
 
 
 
201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  if __name__ == "__main__":
203
  with gr.Blocks() as demo:
204
  gr.Markdown("# LAPOR AI API Backend")
205
  gr.Markdown("This space hosts the API at `/api/analyze`.")
206
  gr.Markdown(f"**Model:** {MODEL_NAME}")
207
 
208
+ app = gr.mount_gradio_app(app, demo, path="/") # type: ignore
209
 
210
  uvicorn.run(app, host="0.0.0.0", port=7860)