Spaces:
Running
Running
File size: 12,705 Bytes
b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c f3eb1eb 1ef11dc 9319a7c b2ebfa6 ad56f8a b2ebfa6 ad56f8a b2ebfa6 ad56f8a b2ebfa6 9319a7c b2ebfa6 1ef11dc b2ebfa6 1ef11dc b2ebfa6 1ef11dc b2ebfa6 1ef11dc b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 e39d48e b2ebfa6 e39d48e b2ebfa6 9319a7c b2ebfa6 e39d48e b2ebfa6 e39d48e 0cfcf78 e39d48e 0cfcf78 e39d48e 0cfcf78 e39d48e 0cfcf78 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 9319a7c b2ebfa6 e39d48e | 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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 | """
database.py β SQLite CRUD para animais e avistamentos.
Usa /data/ em producao (HF Storage Bucket) ou ./data/ localmente.
"""
import json
import os
import sqlite3
import uuid
from pathlib import Path
from typing import Optional
import numpy as np
from PIL import Image
# βββ Paths βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_hf_data = Path("/data")
DATA_DIR = _hf_data if (_hf_data.exists() and os.access(_hf_data, os.W_OK)) else Path("./data")
DB_PATH = DATA_DIR / "viralata.db"
PHOTOS_DIR = DATA_DIR / "photos"
SCHEMA = Path(__file__).parent.parent / "db" / "schema.sql"
class Database:
def __init__(self):
DATA_DIR.mkdir(parents=True, exist_ok=True)
PHOTOS_DIR.mkdir(parents=True, exist_ok=True)
self._init_db()
# βββ Internal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _conn(self) -> sqlite3.Connection:
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
try:
conn.execute("PRAGMA journal_mode=WAL")
except sqlite3.OperationalError:
pass
conn.execute("PRAGMA foreign_keys=ON")
return conn
def _init_db(self):
with self._conn() as conn:
if SCHEMA.exists():
conn.executescript(SCHEMA.read_text())
else:
conn.executescript(
"CREATE TABLE IF NOT EXISTS animals ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" species TEXT NOT NULL,"
" description TEXT,"
" embedding BLOB,"
" first_seen DATETIME DEFAULT CURRENT_TIMESTAMP,"
" last_seen DATETIME DEFAULT CURRENT_TIMESTAMP,"
" sighting_count INTEGER DEFAULT 1"
");"
"CREATE TABLE IF NOT EXISTS sightings ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" animal_id INTEGER NOT NULL REFERENCES animals(id),"
" photo_path TEXT,"
" latitude REAL,"
" longitude REAL,"
" notes TEXT,"
" created_at DATETIME DEFAULT CURRENT_TIMESTAMP"
");"
)
# Incremental migrations β safe to run on every startup
cols = {row[1] for row in conn.execute("PRAGMA table_info(sightings)").fetchall()}
if "is_help_event" not in cols:
conn.execute("ALTER TABLE sightings ADD COLUMN is_help_event INTEGER NOT NULL DEFAULT 0")
if "help_type" not in cols:
conn.execute("ALTER TABLE sightings ADD COLUMN help_type TEXT")
acols = {row[1] for row in conn.execute("PRAGMA table_info(animals)").fetchall()}
if "name" not in acols:
conn.execute("ALTER TABLE animals ADD COLUMN name TEXT")
# βββ Photos ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def save_photo(self, image: Image.Image, animal_id: Optional[int] = None) -> str:
"""Salva foto organizada por animal_id. Ex: photos/animal_42/abc123.jpg"""
from datetime import datetime
filename = f"{uuid.uuid4().hex}.jpg"
if animal_id is not None:
subdir = PHOTOS_DIR / f"animal_{animal_id}"
else:
# fallback: pasta por data
subdir = PHOTOS_DIR / datetime.now().strftime("%Y-%m-%d")
subdir.mkdir(parents=True, exist_ok=True)
path = subdir / filename
image.save(str(path), format="JPEG", quality=85)
rel = path.relative_to(DATA_DIR)
return str(rel).replace("\\", "/")
def photo_url(self, relative_path: Optional[str]) -> Optional[str]:
if not relative_path:
return None
full = DATA_DIR / relative_path
return str(full) if full.exists() else None
# βββ Animals βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def create_animal(self, description: dict, embedding: list,
name: Optional[str] = None) -> int:
emb_blob = np.array(embedding, dtype=np.float32).tobytes()
with self._conn() as conn:
cur = conn.execute(
"INSERT INTO animals (species, name, description, embedding) VALUES (?, ?, ?, ?)",
(
description.get("species", "dog"),
name or None,
json.dumps(description, ensure_ascii=False),
emb_blob,
),
)
return cur.lastrowid
def update_animal_name(self, animal_id: int, name: str):
with self._conn() as conn:
conn.execute("UPDATE animals SET name = ? WHERE id = ?", (name or None, animal_id))
def update_animal(self, animal_id: int):
with self._conn() as conn:
conn.execute(
"UPDATE animals SET sighting_count = sighting_count + 1,"
" last_seen = CURRENT_TIMESTAMP WHERE id = ?",
(animal_id,),
)
def get_animal(self, animal_id: int) -> Optional[dict]:
with self._conn() as conn:
row = conn.execute(
"SELECT * FROM animals WHERE id = ?", (animal_id,)
).fetchone()
return dict(row) if row else None
def get_all_animals_with_embeddings(self) -> list:
with self._conn() as conn:
rows = conn.execute(
"SELECT id, species, description, embedding FROM animals WHERE embedding IS NOT NULL"
).fetchall()
result = []
for row in rows:
d = dict(row)
blob = d.pop("embedding")
try:
d["embedding"] = np.frombuffer(blob, dtype=np.float32).tolist()
except Exception:
d["embedding"] = None
result.append(d)
return result
# βββ Sightings βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def add_sighting(
self,
animal_id: int,
photo_path: Optional[str],
lat: Optional[float],
lng: Optional[float],
notes: Optional[str],
is_help_event: bool = False,
help_type: Optional[str] = None,
):
with self._conn() as conn:
conn.execute(
"INSERT INTO sightings"
" (animal_id, photo_path, latitude, longitude, notes, is_help_event, help_type)"
" VALUES (?, ?, ?, ?, ?, ?, ?)",
(animal_id, photo_path, lat, lng, notes or "",
1 if is_help_event else 0, help_type),
)
def get_animal_sightings(self, animal_id: int) -> list:
with self._conn() as conn:
rows = conn.execute(
"SELECT *, CAST(julianday('now') - julianday(created_at) AS INTEGER) AS days_ago"
" FROM sightings WHERE animal_id = ? AND is_help_event = 0"
" ORDER BY created_at DESC",
(animal_id,),
).fetchall()
return [dict(r) for r in rows]
def get_help_events(self, animal_id: int) -> list:
"""Retorna apenas registros de ajuda (is_help_event=1) com URL de foto."""
with self._conn() as conn:
rows = conn.execute(
"SELECT * FROM sightings WHERE animal_id = ? AND is_help_event = 1"
" ORDER BY created_at DESC",
(animal_id,),
).fetchall()
result = []
for row in rows:
d = dict(row)
d["photo_url"] = self.photo_url(d.get("photo_path")) or ""
result.append(d)
return result
def get_animal_detail(self, animal_id: int) -> Optional[dict]:
"""Retorna animal + avistamentos normais + help_events separados."""
animal = self.get_animal(animal_id)
if not animal:
return None
sightings = self.get_animal_sightings(animal_id)
help_events = self.get_help_events(animal_id)
for s in sightings:
s["photo_url"] = self.photo_url(s.get("photo_path")) or ""
return {"animal": animal, "sightings": sightings, "help_events": help_events}
# βββ Map data ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_map_data(self, species: str = "all", timeframe: str = "all") -> list:
filters = []
params = []
if species in ("dog", "cat"):
filters.append("a.species = ?")
params.append(species)
if timeframe == "today":
filters.append("date(a.last_seen) = date('now')")
elif timeframe == "week":
filters.append("a.last_seen >= datetime('now', '-7 days')")
where = ("WHERE " + " AND ".join(filters)) if filters else ""
sql = (
"SELECT"
" a.id,"
" a.species,"
" a.sighting_count AS count,"
" a.description,"
" strftime('%d/%m/%Y', a.last_seen) AS last_seen,"
" CAST(julianday('now') - julianday(a.last_seen) AS INTEGER) AS days_since,"
" s.latitude AS lat,"
" s.longitude AS lng,"
" (SELECT photo_path FROM sightings"
" WHERE animal_id = a.id AND photo_path IS NOT NULL"
" ORDER BY created_at DESC LIMIT 1) AS last_photo"
" FROM animals a"
" JOIN sightings s ON s.id = ("
" SELECT id FROM sightings"
" WHERE animal_id = a.id AND latitude IS NOT NULL"
" ORDER BY created_at DESC LIMIT 1"
" )"
" " + where +
" ORDER BY a.last_seen DESC"
)
with self._conn() as conn:
rows = conn.execute(sql, params).fetchall()
result = []
for row in rows:
d = dict(row)
try:
desc_obj = json.loads(d.get("description") or "{}")
d["desc"] = desc_obj.get("description_text") or (
" ".join(filter(None, [
desc_obj.get("size", ""),
desc_obj.get("primary_color", ""),
desc_obj.get("breed_estimate", ""),
]))
).strip()
except Exception:
d["desc"] = ""
d.pop("description", None)
d["photo_url"] = self.photo_url(d.get("last_photo")) or ""
result.append(d)
return result
# βββ Animals list ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_recent_animals(self, limit: int = 30) -> list:
sql = (
"SELECT"
" a.*,"
" CAST(julianday('now') - julianday(a.last_seen) AS INTEGER) AS days_since,"
" strftime('%d/%m', a.last_seen) AS last_seen_short,"
" (SELECT photo_path FROM sightings"
" WHERE animal_id = a.id AND photo_path IS NOT NULL"
" ORDER BY created_at DESC LIMIT 1) AS last_photo_path"
" FROM animals a"
" ORDER BY a.last_seen DESC"
" LIMIT ?"
)
with self._conn() as conn:
rows = conn.execute(sql, (limit,)).fetchall()
result = []
for row in rows:
d = dict(row)
d["last_photo_url"] = self.photo_url(d.get("last_photo_path")) or ""
result.append(d)
return result
def total_sightings(self) -> int:
with self._conn() as conn:
return conn.execute("SELECT COUNT(*) FROM sightings").fetchone()[0]
def total_animals(self) -> int:
with self._conn() as conn:
return conn.execute("SELECT COUNT(*) FROM animals").fetchone()[0]
|