Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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
|
| 15 |
SYSTEM_PROMPT = (
|
| 16 |
"You are a marine-biology assistant. Return ONLY a valid JSON object with this schema:\n"
|
| 17 |
-
'{"candidates":[{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
"Rules:\n"
|
| 19 |
-
"- Up to {TOP_K} items.\n"
|
| 20 |
"- confidence is a float in [0,1].\n"
|
| 21 |
-
"-
|
| 22 |
-
"-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
if not text:
|
| 59 |
-
return
|
| 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 |
-
|
| 79 |
-
return
|
| 80 |
|
| 81 |
-
# =====
|
| 82 |
def call_openrouter(img: Image.Image, top_k: int) -> Dict:
|
| 83 |
if not API_KEY:
|
| 84 |
-
return
|
| 85 |
-
|
| 86 |
headers = {
|
| 87 |
"Authorization": f"Bearer {API_KEY}",
|
| 88 |
"Content-Type": "application/json",
|
| 89 |
}
|
| 90 |
-
if X_TITLE:
|
| 91 |
-
|
| 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,
|
| 102 |
-
"max_tokens":
|
| 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):
|
| 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.
|
| 124 |
-
|
| 125 |
-
return {"candidates":[{"common_name":"unknown","scientific_name":"unknown","confidence":0.0,"blurb":f"Upstream error: {last_err}"}]}
|
| 126 |
|
| 127 |
-
# ===== Gradio UI
|
| 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 (
|
| 139 |
-
gr.Markdown("## Underwater Species Recognizer (
|
| 140 |
f"**Model:** `{MODEL_ID}` \n"
|
| 141 |
-
"
|
| 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")
|