AdarshDRC commited on
Commit
844959b
·
verified ·
1 Parent(s): 59be0e4

Delete src/utils.py

Browse files
Files changed (1) hide show
  1. src/utils.py +0 -178
src/utils.py DELETED
@@ -1,178 +0,0 @@
1
- """
2
- src/utils.py — Pure helper functions shared across the entire codebase.
3
-
4
- All stateless — no I/O, no model calls, no side effects.
5
- Import freely from any module.
6
- """
7
-
8
- import hashlib
9
- import re
10
- from typing import Union
11
- from urllib.parse import urlparse
12
-
13
- import inflect
14
- import numpy as np
15
- from fastapi import Request
16
-
17
- from .config import FACE_THRESHOLD_LOW
18
-
19
- _inflect = inflect.engine()
20
-
21
-
22
- # ════════════════════════════════════════════════════════════════════
23
- # VECTOR HELPERS
24
- # ════════════════════════════════════════════════════════════════════
25
-
26
- def to_list(v) -> list:
27
- """
28
- Convert a numpy array (or any object with .tolist()) to a plain Python list.
29
- Pinecone's SDK requires plain lists for vector values, not numpy arrays.
30
- """
31
- return v.tolist() if hasattr(v, "tolist") else v
32
-
33
-
34
- # ════════════════════════════════════════════════════════════════════
35
- # SCORE NORMALIZATION
36
- # ════════════════════════════════════════════════════════════════════
37
-
38
- def face_ui_score(raw: float, n_faces: int = 1) -> float:
39
- """
40
- Map a raw Pinecone cosine score to a human-readable UI confidence (0.75–0.99).
41
-
42
- Why remap? A raw cosine score of 0.36 (above threshold, genuine match) reads
43
- as "36% confident" to a user, which is misleading. Remapping the valid match
44
- range [FACE_THRESHOLD_LOW, 1.0] onto [0.75, 0.99] communicates that anything
45
- shown IS a match; the percentage communicates relative quality within matches.
46
-
47
- Multi-face boost: +5 % per additional matched face, capped at 0.99.
48
- Rationale — an image containing 3 of 3 searched faces should rank above an
49
- image containing only 1 of 3.
50
- """
51
- lo = FACE_THRESHOLD_LOW
52
- base = 0.75 + ((raw - lo) / (1.0 - lo)) * 0.24
53
- if n_faces > 1:
54
- base *= 1.0 + 0.05 * (n_faces - 1)
55
- return round(min(0.99, base), 4)
56
-
57
-
58
- # ════════════════════════════════════════════════════════════════════
59
- # FILE / PATH HELPERS
60
- # ════════════════════════════════════════════════════════════════════
61
-
62
- def img_hash(image_path: str) -> str:
63
- """
64
- MD5 of the first 64 KB of a file — fast collision-resistant cache key.
65
- Reading the full file for every cache lookup on large images would be slow;
66
- the first 64 KB is almost always unique enough to distinguish images.
67
- """
68
- h = hashlib.md5()
69
- with open(image_path, "rb") as f:
70
- h.update(f.read(65536))
71
- return h.hexdigest()
72
-
73
-
74
- def sanitize_filename(filename: str) -> str:
75
- """
76
- Make a filename safe for use in temp file paths.
77
- Replaces spaces with underscores; strips characters outside [a-zA-Z0-9._-].
78
- """
79
- return re.sub(r"[^\w.\-]", "", re.sub(r"\s+", "_", filename))
80
-
81
-
82
- def standardize_category_name(name: str) -> str:
83
- """
84
- Normalize a user-supplied folder/category name for consistent storage:
85
- - Lowercase and underscore-separated
86
- - No special characters
87
- - Singularized (dogs → dog, shoes → shoe)
88
-
89
- Singularization ensures "Dog", "Dogs", "dogs" all map to the same folder.
90
- inflect.singular_noun() returns False for already-singular words,
91
- so `or clean` keeps the original in that case.
92
- """
93
- clean = re.sub(r"\s+", "_", name.strip().lower())
94
- clean = re.sub(r"[^\w]", "", clean)
95
- return _inflect.singular_noun(clean) or clean
96
-
97
-
98
- # ════════════════════════════════════════════════════════════════════
99
- # CLOUDINARY HELPERS
100
- # ════════════════════════════════════════════════════════════════════
101
-
102
- def get_cloudinary_creds(env_url: str) -> dict:
103
- """
104
- Parse a Cloudinary Environment URL (cloudinary://key:secret@cloud_name)
105
- into separate credential components for SDK calls.
106
- Returns an empty dict for blank/invalid input.
107
- """
108
- if not env_url:
109
- return {}
110
- parsed = urlparse(env_url)
111
- return {
112
- "api_key": parsed.username or "",
113
- "api_secret": parsed.password or "",
114
- "cloud_name": parsed.hostname or "",
115
- }
116
-
117
-
118
- def cld_thumb_url(secure_url: str) -> str:
119
- """
120
- Inject a Cloudinary on-the-fly image transformation into a full-resolution
121
- URL, producing a 400×400 fill thumbnail for grid display.
122
-
123
- Cloudinary transformations are inserted between /upload/ and the version/path:
124
- .../image/upload/v123/folder/img.jpg
125
- → .../image/upload/w_400,h_400,c_fill,q_auto,f_auto/v123/folder/img.jpg
126
-
127
- Returns the original URL unchanged if transformation cannot be applied
128
- (e.g. non-standard URL format or already-transformed URL).
129
- """
130
- marker = "/image/upload/"
131
- idx = secure_url.find(marker)
132
- if idx == -1:
133
- return secure_url
134
- base = secure_url[: idx + len(marker)]
135
- rest = secure_url[idx + len(marker):]
136
- if any(rest.startswith(p) for p in ("w_", "h_", "c_", "q_")):
137
- return secure_url # already has a transformation block
138
- return f"{base}w_400,h_400,c_fill,q_auto,f_auto/{rest}"
139
-
140
-
141
- def url_to_public_id(image_url: str) -> str:
142
- """
143
- Extract the Cloudinary public_id from a secure_url.
144
- Strips the version segment (vXXX) if present.
145
- Returns empty string on failure.
146
-
147
- Example:
148
- https://res.cloudinary.com/demo/image/upload/v1234/folder/img.jpg
149
- → folder/img
150
- """
151
- try:
152
- path = urlparse(image_url).path
153
- parts = path.split("/")
154
- upload_idx = parts.index("upload")
155
- after = parts[upload_idx + 1:]
156
- if after and after[0].startswith("v") and after[0][1:].isdigit():
157
- after = after[1:]
158
- return "/".join(after).rsplit(".", 1)[0]
159
- except Exception:
160
- return ""
161
-
162
-
163
- # ════════════════════════════════════════════════════════════════════
164
- # REQUEST HELPERS
165
- # ════════════════════════════════════════════════════════════════════
166
-
167
- def get_ip(request: Request) -> str:
168
- """
169
- Extract the real client IP from a FastAPI request.
170
- Respects the X-Forwarded-For header set by reverse proxies / CDNs.
171
- """
172
- xff = request.headers.get("X-Forwarded-For", "")
173
- return xff.split(",")[0].strip() if xff else getattr(request.client, "host", "unknown")
174
-
175
-
176
- def is_default_key(key: str, default: str) -> bool:
177
- """Return True if `key` matches the shared demo key (guest mode)."""
178
- return bool(default) and key.strip() == default.strip()