taaaranis commited on
Commit
3dab94c
·
verified ·
1 Parent(s): d847091

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -34
app.py CHANGED
@@ -11,20 +11,45 @@ TOP_K_DEF = int(os.getenv("TOP_K", "3"))
11
  X_TITLE = os.getenv("X_TITLE", "Undersea Species API")
12
  REFERER = os.getenv("REFERER", "")
13
 
14
- # ===== System prompt (EN only; strict JSON) =====
15
  SYSTEM_PROMPT = (
16
  "You are a marine-biology assistant. Return ONLY a valid JSON object with this schema:\n"
17
- '{"candidates":[{"common_name": string, "scientific_name": string, "confidence": number, "blurb": string}]}\n'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  "Rules:\n"
19
- "- Up to {TOP_K} items.\n"
20
  "- confidence is a float in [0,1].\n"
21
- "- If species is uncertain, use a higher-rank taxon (order/class) and mention uncertainty in blurb.\n"
22
- "- Output JSON only. No explanations, no markdown, no code fences, no nested JSON inside strings."
 
 
 
 
 
 
 
 
 
 
23
  )
24
 
25
  # ===== Helpers =====
26
  def pil_from_any(img_input: Any) -> Image.Image:
27
- """Accepts Gradio PIL, URL, data-url, or base64 string."""
28
  if isinstance(img_input, Image.Image):
29
  return img_input.convert("RGB")
30
  if isinstance(img_input, str):
@@ -36,7 +61,6 @@ def pil_from_any(img_input: Any) -> Image.Image:
36
  r = requests.get(img_input, timeout=30)
37
  r.raise_for_status()
38
  return Image.open(io.BytesIO(r.content)).convert("RGB")
39
- # bare base64
40
  try:
41
  data = base64.b64decode(img_input)
42
  return Image.open(io.BytesIO(data)).convert("RGB")
@@ -51,20 +75,35 @@ def pil_to_data_url(img: Image.Image) -> str:
51
  return f"data:image/png;base64,{b64}"
52
 
53
  def coerce_json(text: str) -> Dict:
54
- """
55
- Best-effort: parse JSON, or extract the last {...} block.
56
- Always return dict with 'candidates' array.
57
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  if not text:
59
- return {"candidates":[{"common_name":"unknown","scientific_name":"unknown","confidence":0.0,"blurb":"empty response"}]}
60
- # try direct
61
  try:
62
  obj = json.loads(text)
63
  if isinstance(obj, dict) and "candidates" in obj:
64
  return obj
65
  except Exception:
66
  pass
67
- # try to extract last {...}
68
  import re
69
  m = re.findall(r"\{(?:[^{}]|(?R))*\}", text, flags=re.DOTALL)
70
  if m:
@@ -75,31 +114,27 @@ def coerce_json(text: str) -> Dict:
75
  return obj
76
  except Exception:
77
  continue
78
- # fallback
79
- return {"candidates":[{"common_name":"unknown","scientific_name":"unknown","confidence":0.0,"blurb":text[:400]}]}
80
 
81
- # ===== OpenRouter call (OpenAI-compatible chat.completions) =====
82
  def call_openrouter(img: Image.Image, top_k: int) -> Dict:
83
  if not API_KEY:
84
- return {"candidates":[{"common_name":"unknown","scientific_name":"unknown","confidence":0.0,"blurb":"API_KEY not set"}]}
85
-
86
  headers = {
87
  "Authorization": f"Bearer {API_KEY}",
88
  "Content-Type": "application/json",
89
  }
90
- if X_TITLE:
91
- headers["X-Title"] = X_TITLE
92
- if REFERER:
93
- headers["HTTP-Referer"] = REFERER
94
 
95
  data_url = pil_to_data_url(img)
96
  sys_prompt = SYSTEM_PROMPT.replace("{TOP_K}", str(top_k))
97
-
98
  payload = {
99
  "model": MODEL_ID,
100
  "response_format": {"type": "json_object"},
101
- "temperature": 0, # strict output
102
- "max_tokens": 700,
103
  "messages": [
104
  {"role": "system", "content": sys_prompt},
105
  {"role": "user", "content": [
@@ -110,7 +145,7 @@ def call_openrouter(img: Image.Image, top_k: int) -> Dict:
110
  }
111
 
112
  last_err = ""
113
- for i in range(2): # light retry
114
  try:
115
  resp = requests.post(API_URL, headers=headers, json=payload, timeout=90)
116
  if resp.ok:
@@ -120,11 +155,10 @@ def call_openrouter(img: Image.Image, top_k: int) -> Dict:
120
  last_err = f"{resp.status_code}: {resp.text[:600]}"
121
  except Exception as e:
122
  last_err = str(e)[:600]
123
- time.sleep(0.4 * (i + 1))
124
-
125
- return {"candidates":[{"common_name":"unknown","scientific_name":"unknown","confidence":0.0,"blurb":f"Upstream error: {last_err}"}]}
126
 
127
- # ===== Gradio UI & API =====
128
  def classify(image, top_k=TOP_K_DEF):
129
  pil = pil_from_any(image)
130
  res = call_openrouter(pil, int(top_k))
@@ -135,10 +169,10 @@ def classify(image, top_k=TOP_K_DEF):
135
  "timestamp": time.time()
136
  }
137
 
138
- with gr.Blocks(title="Underwater Species (OpenRouter + GPT-4o-mini)") as demo:
139
- gr.Markdown("## Underwater Species Recognizer (OpenRouter Vision LLM)\n"
140
  f"**Model:** `{MODEL_ID}` \n"
141
- "Set `API_KEY`, `API_URL`, `MODEL_ID` in Space Secrets.")
142
  with gr.Row():
143
  with gr.Column(scale=1):
144
  img = gr.Image(type="pil", label="Underwater image")
 
11
  X_TITLE = os.getenv("X_TITLE", "Undersea Species API")
12
  REFERER = os.getenv("REFERER", "")
13
 
14
+ # ===== System Prompt =====
15
  SYSTEM_PROMPT = (
16
  "You are a marine-biology assistant. Return ONLY a valid JSON object with this schema:\n"
17
+ '{"candidates":[{'
18
+ '"common_name": string, '
19
+ '"scientific_name": string, '
20
+ '"confidence": number, '
21
+ '"blurb": string, '
22
+ '"fun_facts": [string], '
23
+ '"summary": string, '
24
+ '"why_it_matters": string, '
25
+ '"conservation_status": string, '
26
+ '"status_source": string, '
27
+ '"local_guidelines": [string], '
28
+ '"species_type": string, '
29
+ '"occurrence_data": string, '
30
+ '"taxonomy": string, '
31
+ '"depth_range": string, '
32
+ '"observation_period": string'
33
+ '}]} \n'
34
  "Rules:\n"
35
+ f"- Up to {{TOP_K}} items.\n"
36
  "- confidence is a float in [0,1].\n"
37
+ "- fun_facts: 1–3 short, engaging facts.\n"
38
+ "- summary: 2–3 sentences, must include habitat, diet, role.\n"
39
+ "- why_it_matters: 1 short highlight (e.g., 'Indicator of reef health').\n"
40
+ "- conservation_status: use IUCN categories (e.g., 'Endangered', 'Vulnerable').\n"
41
+ "- status_source: always 'IUCN Red List' if from IUCN.\n"
42
+ "- local_guidelines: safety/protection rules for divers.\n"
43
+ "- species_type: fish, mammal, reptile, coral, etc.\n"
44
+ "- occurrence_data: how often it’s sighted.\n"
45
+ "- taxonomy: scientific classification.\n"
46
+ "- depth_range: typical depth (e.g., '5–30m').\n"
47
+ "- observation_period: seasonal or yearly occurrence.\n"
48
+ "- Output JSON only. No markdown, no explanations."
49
  )
50
 
51
  # ===== Helpers =====
52
  def pil_from_any(img_input: Any) -> Image.Image:
 
53
  if isinstance(img_input, Image.Image):
54
  return img_input.convert("RGB")
55
  if isinstance(img_input, str):
 
61
  r = requests.get(img_input, timeout=30)
62
  r.raise_for_status()
63
  return Image.open(io.BytesIO(r.content)).convert("RGB")
 
64
  try:
65
  data = base64.b64decode(img_input)
66
  return Image.open(io.BytesIO(data)).convert("RGB")
 
75
  return f"data:image/png;base64,{b64}"
76
 
77
  def coerce_json(text: str) -> Dict:
78
+ default = {
79
+ "candidates": [
80
+ {
81
+ "common_name": "unknown",
82
+ "scientific_name": "unknown",
83
+ "confidence": 0.0,
84
+ "blurb": "empty response",
85
+ "fun_facts": [],
86
+ "summary": "No data",
87
+ "why_it_matters": "No data",
88
+ "conservation_status": "Unknown",
89
+ "status_source": "Unknown",
90
+ "local_guidelines": [],
91
+ "species_type": "unknown",
92
+ "occurrence_data": "No data",
93
+ "taxonomy": "No data",
94
+ "depth_range": "Unknown",
95
+ "observation_period": "Unknown"
96
+ }
97
+ ]
98
+ }
99
  if not text:
100
+ return default
 
101
  try:
102
  obj = json.loads(text)
103
  if isinstance(obj, dict) and "candidates" in obj:
104
  return obj
105
  except Exception:
106
  pass
 
107
  import re
108
  m = re.findall(r"\{(?:[^{}]|(?R))*\}", text, flags=re.DOTALL)
109
  if m:
 
114
  return obj
115
  except Exception:
116
  continue
117
+ default["candidates"][0]["blurb"] = text[:400]
118
+ return default
119
 
120
+ # ===== API Call =====
121
  def call_openrouter(img: Image.Image, top_k: int) -> Dict:
122
  if not API_KEY:
123
+ return coerce_json("API_KEY not set")
 
124
  headers = {
125
  "Authorization": f"Bearer {API_KEY}",
126
  "Content-Type": "application/json",
127
  }
128
+ if X_TITLE: headers["X-Title"] = X_TITLE
129
+ if REFERER: headers["HTTP-Referer"] = REFERER
 
 
130
 
131
  data_url = pil_to_data_url(img)
132
  sys_prompt = SYSTEM_PROMPT.replace("{TOP_K}", str(top_k))
 
133
  payload = {
134
  "model": MODEL_ID,
135
  "response_format": {"type": "json_object"},
136
+ "temperature": 0,
137
+ "max_tokens": 900,
138
  "messages": [
139
  {"role": "system", "content": sys_prompt},
140
  {"role": "user", "content": [
 
145
  }
146
 
147
  last_err = ""
148
+ for i in range(2):
149
  try:
150
  resp = requests.post(API_URL, headers=headers, json=payload, timeout=90)
151
  if resp.ok:
 
155
  last_err = f"{resp.status_code}: {resp.text[:600]}"
156
  except Exception as e:
157
  last_err = str(e)[:600]
158
+ time.sleep(0.5 * (i + 1))
159
+ return coerce_json(f"Upstream error: {last_err}")
 
160
 
161
+ # ===== Gradio UI =====
162
  def classify(image, top_k=TOP_K_DEF):
163
  pil = pil_from_any(image)
164
  res = call_openrouter(pil, int(top_k))
 
169
  "timestamp": time.time()
170
  }
171
 
172
+ with gr.Blocks(title="Underwater Species Recognition (Educational + Conservation)") as demo:
173
+ gr.Markdown("## 🌊 Underwater Species Recognizer (AI + Conservation)\n"
174
  f"**Model:** `{MODEL_ID}` \n"
175
+ "Upload an underwater photo to identify species, learn facts, and see conservation status.")
176
  with gr.Row():
177
  with gr.Column(scale=1):
178
  img = gr.Image(type="pil", label="Underwater image")