LogicGoInfotechSpaces commited on
Commit
49b5e1e
·
1 Parent(s): 7f8c8a6

Add MongoDB media click logging with per-user category counts

Browse files

- Add MONGODB_ADMIN connection for admin database
- Implement log_media_click() with user/category normalization
- Support user_id from ObjectId, integer (deterministic), or auto-generate
- Support category_id and categoryId form fields
- Add endpoint defaults for colorization endpoints
- Update /upload and /colorize endpoints in all main files
- Update Postman collection with new fields
- Store data in media_clicks collection with userId, categories array, click_count, and timestamps

Files changed (6) hide show
  1. app/config.py +4 -0
  2. app/database.py +178 -13
  3. app/main.py +50 -4
  4. app/main_fastai.py +32 -10
  5. app/main_sdxl.py +51 -19
  6. postman_collection.json +51 -0
app/config.py CHANGED
@@ -58,6 +58,10 @@ class Settings(BaseSettings):
58
  # MONGODB_URI should be set in Hugging Face Space secrets
59
  MONGODB_URI: str = os.getenv("MONGODB_URI", "")
60
  MONGODB_DB_NAME: str = os.getenv("MONGODB_DB_NAME", "colorization_db")
 
 
 
 
61
 
62
  class Config:
63
  env_file = ".env"
 
58
  # MONGODB_URI should be set in Hugging Face Space secrets
59
  MONGODB_URI: str = os.getenv("MONGODB_URI", "")
60
  MONGODB_DB_NAME: str = os.getenv("MONGODB_DB_NAME", "colorization_db")
61
+ MONGODB_ADMIN: str = os.getenv("MONGODB_ADMIN", "")
62
+ MONGODB_ADMIN_DB_NAME: str = os.getenv("MONGODB_ADMIN_DB_NAME", None) or os.getenv("MONGODB_DB_NAME", "colorization_db")
63
+ DEFAULT_CATEGORY_COLORIZATION: str = os.getenv("DEFAULT_CATEGORY_COLORIZATION", "")
64
+ DEFAULT_CATEGORY_FALLBACK: str = os.getenv("DEFAULT_CATEGORY_FALLBACK", "69368f722e46bd68ae188984")
65
 
66
  class Config:
67
  env_file = ".env"
app/database.py CHANGED
@@ -1,18 +1,23 @@
1
- """
2
- MongoDB database connection and logging utilities
3
- """
4
- import os
5
- import logging
6
- from datetime import datetime
7
- from typing import Optional, Dict, Any
8
- from pymongo import MongoClient
9
- from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
 
10
 
11
  logger = logging.getLogger(__name__)
12
 
13
- # MongoDB connection
14
- _client: Optional[MongoClient] = None
15
- _db = None
 
 
 
 
16
 
17
  def get_mongodb_client() -> Optional[MongoClient]:
18
  """Get or create MongoDB client"""
@@ -48,6 +53,103 @@ def get_database():
48
  logger.warning("MongoDB client not available")
49
  return _db
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  def log_api_call(
52
  endpoint: str,
53
  method: str,
@@ -198,12 +300,75 @@ def log_colorization(
198
  logger.error("Failed to log colorization to MongoDB: %s", str(e))
199
  return False
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  def close_connection():
202
  """Close MongoDB connection"""
203
- global _client, _db
204
  if _client:
205
  _client.close()
206
  _client = None
207
  _db = None
208
  logger.info("MongoDB connection closed")
 
 
 
 
 
209
 
 
1
+ """
2
+ MongoDB database connection and logging utilities, including admin media click logging.
3
+ """
4
+ import os
5
+ import logging
6
+ from datetime import datetime
7
+ from typing import Optional, Dict, Any, Union
8
+ from bson import ObjectId
9
+ from pymongo import MongoClient
10
+ from pymongo.errors import ConnectionFailure, ServerSelectionTimeoutError
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
+ # MongoDB connection
15
+ _client: Optional[MongoClient] = None
16
+ _db = None
17
+
18
+ # Admin MongoDB connection (media_clicks)
19
+ _admin_client: Optional[MongoClient] = None
20
+ _admin_db = None
21
 
22
  def get_mongodb_client() -> Optional[MongoClient]:
23
  """Get or create MongoDB client"""
 
53
  logger.warning("MongoDB client not available")
54
  return _db
55
 
56
+
57
+ def get_admin_client() -> Optional[MongoClient]:
58
+ """Get or create admin MongoDB client (for media_clicks collection)."""
59
+ global _admin_client
60
+ if _admin_client is None:
61
+ mongodb_uri = os.getenv("MONGODB_ADMIN")
62
+ if not mongodb_uri:
63
+ logger.warning("MONGODB_ADMIN environment variable not set. Admin MongoDB features will be disabled.")
64
+ return None
65
+ try:
66
+ _admin_client = MongoClient(
67
+ mongodb_uri,
68
+ serverSelectionTimeoutMS=5000,
69
+ connectTimeoutMS=5000,
70
+ )
71
+ _admin_client.admin.command("ping")
72
+ logger.info("Admin MongoDB connection established successfully")
73
+ except (ConnectionFailure, ServerSelectionTimeoutError) as exc:
74
+ logger.error("Failed to connect to Admin MongoDB: %s", str(exc))
75
+ _admin_client = None
76
+ return _admin_client
77
+
78
+
79
+ def get_admin_database():
80
+ """Get admin database instance."""
81
+ global _admin_db
82
+ if _admin_db is None:
83
+ client = get_admin_client()
84
+ if client:
85
+ db_name = os.getenv("MONGODB_ADMIN_DB_NAME", os.getenv("MONGODB_DB_NAME", "colorization_db"))
86
+ _admin_db = client[db_name]
87
+ else:
88
+ logger.warning("Admin MongoDB client not available")
89
+ return _admin_db
90
+
91
+
92
+ def _normalize_object_id(raw_value: Optional[Union[str, int, ObjectId]]) -> ObjectId:
93
+ """Normalize user id inputs into a deterministic ObjectId."""
94
+ if isinstance(raw_value, ObjectId):
95
+ return raw_value
96
+
97
+ if raw_value is None:
98
+ return ObjectId()
99
+
100
+ try:
101
+ if isinstance(raw_value, int) or (isinstance(raw_value, str) and raw_value.strip().lstrip("-").isdigit()):
102
+ int_value = int(str(raw_value).strip())
103
+ hex_str = format(abs(int_value), "x").zfill(24)[-24:]
104
+ if ObjectId.is_valid(hex_str):
105
+ return ObjectId(hex_str)
106
+ except Exception as exc: # pragma: no cover - defensive
107
+ logger.debug("Numeric user id normalization failed: %s", str(exc))
108
+
109
+ if isinstance(raw_value, str):
110
+ candidate = raw_value.strip()
111
+ if ObjectId.is_valid(candidate):
112
+ return ObjectId(candidate)
113
+
114
+ return ObjectId()
115
+
116
+
117
+ def _objectid_from_any(value: str) -> ObjectId:
118
+ """Convert arbitrary string into an ObjectId deterministically when possible."""
119
+ if ObjectId.is_valid(value):
120
+ return ObjectId(value)
121
+ try:
122
+ hex_str = value.encode("utf-8").hex().zfill(24)[-24:]
123
+ if ObjectId.is_valid(hex_str):
124
+ return ObjectId(hex_str)
125
+ except Exception as exc: # pragma: no cover - defensive
126
+ logger.debug("Category id normalization failed: %s", str(exc))
127
+ return ObjectId()
128
+
129
+
130
+ def _resolve_category_id(
131
+ category_id: Optional[str],
132
+ endpoint_path: Optional[str],
133
+ default_category_id: Optional[str],
134
+ ) -> ObjectId:
135
+ """Pick category id from explicit value, endpoint default, or fallback."""
136
+ endpoint_map = {
137
+ "colorization": os.getenv("DEFAULT_CATEGORY_COLORIZATION"),
138
+ "upload": os.getenv("DEFAULT_CATEGORY_COLORIZATION"),
139
+ "colorize": os.getenv("DEFAULT_CATEGORY_COLORIZATION"),
140
+ }
141
+ normalized_endpoint = None
142
+ if endpoint_path:
143
+ normalized_endpoint = endpoint_path.strip("/").split("/")[0].lower() or None
144
+
145
+ chosen = category_id
146
+ if not chosen and normalized_endpoint and endpoint_map.get(normalized_endpoint):
147
+ chosen = endpoint_map[normalized_endpoint]
148
+ if not chosen:
149
+ chosen = default_category_id or os.getenv("DEFAULT_CATEGORY_FALLBACK", "69368f722e46bd68ae188984")
150
+
151
+ return _objectid_from_any(chosen)
152
+
153
  def log_api_call(
154
  endpoint: str,
155
  method: str,
 
300
  logger.error("Failed to log colorization to MongoDB: %s", str(e))
301
  return False
302
 
303
+
304
+ def log_media_click(
305
+ user_id: Optional[Union[str, int, ObjectId]],
306
+ category_id: Optional[str],
307
+ *,
308
+ endpoint_path: Optional[str] = None,
309
+ default_category_id: Optional[str] = None,
310
+ ) -> bool:
311
+ """Log media clicks into the admin MongoDB (media_clicks collection)."""
312
+ try:
313
+ db = get_admin_database()
314
+ if db is None:
315
+ logger.warning("Admin MongoDB not available, skipping media click log")
316
+ return False
317
+
318
+ collection = db["media_clicks"]
319
+
320
+ # Drop legacy index to avoid duplicate key errors (best effort)
321
+ try:
322
+ collection.drop_index("user_id_1_header_1_media_id_1")
323
+ except Exception as exc:
324
+ logger.debug("Legacy index drop skipped: %s", str(exc))
325
+
326
+ user_object_id = _normalize_object_id(user_id)
327
+ category_object_id = _resolve_category_id(category_id, endpoint_path, default_category_id)
328
+ now = datetime.utcnow()
329
+
330
+ update_existing = collection.update_one(
331
+ {"userId": user_object_id, "categories.categoryId": category_object_id},
332
+ {
333
+ "$inc": {"categories.$.click_count": 1},
334
+ "$set": {"categories.$.lastClickedAt": now, "updatedAt": now},
335
+ },
336
+ )
337
+
338
+ if update_existing.matched_count == 0:
339
+ collection.update_one(
340
+ {"userId": user_object_id},
341
+ {
342
+ "$setOnInsert": {"createdAt": now},
343
+ "$set": {"updatedAt": now},
344
+ "$push": {
345
+ "categories": {
346
+ "categoryId": category_object_id,
347
+ "click_count": 1,
348
+ "lastClickedAt": now,
349
+ }
350
+ },
351
+ },
352
+ upsert=True,
353
+ )
354
+
355
+ logger.info("Media click logged for user %s", str(user_object_id))
356
+ return True
357
+ except Exception as exc:
358
+ logger.error("Failed to log media click to admin MongoDB: %s", str(exc))
359
+ return False
360
+
361
  def close_connection():
362
  """Close MongoDB connection"""
363
+ global _client, _db, _admin_client, _admin_db
364
  if _client:
365
  _client.close()
366
  _client = None
367
  _db = None
368
  logger.info("MongoDB connection closed")
369
+ if _admin_client:
370
+ _admin_client.close()
371
+ _admin_client = None
372
+ _admin_db = None
373
+ logger.info("Admin MongoDB connection closed")
374
 
app/main.py CHANGED
@@ -1,4 +1,5 @@
1
- from fastapi import FastAPI, File, UploadFile, HTTPException, Header, Request
 
2
  from fastapi.responses import FileResponse
3
  from huggingface_hub import hf_hub_download
4
  import uuid
@@ -8,7 +9,18 @@ import json
8
  from PIL import Image
9
  import torch
10
  from torchvision import transforms
11
- from app.database import get_database, log_api_call, log_image_upload, log_colorization, close_connection
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  # -------------------------------------------------
14
  # 🚀 FastAPI App
@@ -43,6 +55,8 @@ RESULTS_DIR = "/tmp/results"
43
  os.makedirs(UPLOAD_DIR, exist_ok=True)
44
  os.makedirs(RESULTS_DIR, exist_ok=True)
45
 
 
 
46
  # -------------------------------------------------
47
  # 🧠 Load GAN Colorization Model
48
  # -------------------------------------------------
@@ -114,6 +128,10 @@ def verify_app_check_token(token: str):
114
  raise HTTPException(status_code=401, detail="Invalid Firebase App Check token")
115
  return True
116
 
 
 
 
 
117
  # -------------------------------------------------
118
  # 📤 Upload Image
119
  # -------------------------------------------------
@@ -121,11 +139,16 @@ def verify_app_check_token(token: str):
121
  async def upload_image(
122
  request: Request,
123
  file: UploadFile = File(...),
124
- x_firebase_appcheck: str = Header(None)
 
 
 
125
  ):
126
  verify_app_check_token(x_firebase_appcheck)
127
 
128
  ip_address = request.client.host if request.client else None
 
 
129
 
130
  if not file.content_type.startswith("image/"):
131
  log_api_call(
@@ -160,6 +183,7 @@ async def upload_image(
160
  filename=file.filename or image_id,
161
  file_size=file_size,
162
  content_type=file.content_type or "image/jpeg",
 
163
  ip_address=ip_address
164
  )
165
 
@@ -169,9 +193,17 @@ async def upload_image(
169
  status_code=200,
170
  request_data={"filename": file.filename, "content_type": file.content_type},
171
  response_data=response_data,
 
172
  ip_address=ip_address
173
  )
174
 
 
 
 
 
 
 
 
175
  return response_data
176
 
177
  # -------------------------------------------------
@@ -181,7 +213,10 @@ async def upload_image(
181
  async def colorize(
182
  request: Request,
183
  file: UploadFile = File(...),
184
- x_firebase_appcheck: str = Header(None)
 
 
 
185
  ):
186
  import time
187
  start_time = time.time()
@@ -189,6 +224,8 @@ async def colorize(
189
  verify_app_check_token(x_firebase_appcheck)
190
 
191
  ip_address = request.client.host if request.client else None
 
 
192
 
193
  if not file.content_type.startswith("image/"):
194
  log_api_call(
@@ -225,6 +262,7 @@ async def colorize(
225
  result_id=result_id_clean,
226
  model_type="gan",
227
  processing_time=processing_time,
 
228
  ip_address=ip_address
229
  )
230
 
@@ -234,9 +272,17 @@ async def colorize(
234
  status_code=200,
235
  request_data={"filename": file.filename, "content_type": file.content_type},
236
  response_data=response_data,
 
237
  ip_address=ip_address
238
  )
239
 
 
 
 
 
 
 
 
240
  return response_data
241
 
242
  # -------------------------------------------------
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Header, Request, Form
2
+ from typing import Optional
3
  from fastapi.responses import FileResponse
4
  from huggingface_hub import hf_hub_download
5
  import uuid
 
9
  from PIL import Image
10
  import torch
11
  from torchvision import transforms
12
+ from app.database import (
13
+ get_database,
14
+ log_api_call,
15
+ log_image_upload,
16
+ log_colorization,
17
+ log_media_click,
18
+ close_connection,
19
+ )
20
+ try:
21
+ from firebase_admin import auth as firebase_auth
22
+ except ImportError:
23
+ firebase_auth = None
24
 
25
  # -------------------------------------------------
26
  # 🚀 FastAPI App
 
55
  os.makedirs(UPLOAD_DIR, exist_ok=True)
56
  os.makedirs(RESULTS_DIR, exist_ok=True)
57
 
58
+ MEDIA_CLICK_DEFAULT_CATEGORY = os.getenv("DEFAULT_CATEGORY_FALLBACK", "69368f722e46bd68ae188984")
59
+
60
  # -------------------------------------------------
61
  # 🧠 Load GAN Colorization Model
62
  # -------------------------------------------------
 
128
  raise HTTPException(status_code=401, detail="Invalid Firebase App Check token")
129
  return True
130
 
131
+ def _resolve_user_id(request: Request, supplied_user_id: Optional[str]) -> Optional[str]:
132
+ """Return supplied user_id if provided, otherwise None (will auto-generate in log_media_click)."""
133
+ return supplied_user_id
134
+
135
  # -------------------------------------------------
136
  # 📤 Upload Image
137
  # -------------------------------------------------
 
139
  async def upload_image(
140
  request: Request,
141
  file: UploadFile = File(...),
142
+ x_firebase_appcheck: str = Header(None),
143
+ user_id: Optional[str] = Form(None),
144
+ category_id: Optional[str] = Form(None),
145
+ categoryId: Optional[str] = Form(None),
146
  ):
147
  verify_app_check_token(x_firebase_appcheck)
148
 
149
  ip_address = request.client.host if request.client else None
150
+ effective_user_id = _resolve_user_id(request, user_id)
151
+ effective_category_id = category_id or categoryId
152
 
153
  if not file.content_type.startswith("image/"):
154
  log_api_call(
 
183
  filename=file.filename or image_id,
184
  file_size=file_size,
185
  content_type=file.content_type or "image/jpeg",
186
+ user_id=effective_user_id,
187
  ip_address=ip_address
188
  )
189
 
 
193
  status_code=200,
194
  request_data={"filename": file.filename, "content_type": file.content_type},
195
  response_data=response_data,
196
+ user_id=effective_user_id,
197
  ip_address=ip_address
198
  )
199
 
200
+ log_media_click(
201
+ user_id=effective_user_id,
202
+ category_id=effective_category_id,
203
+ endpoint_path=str(request.url.path),
204
+ default_category_id=MEDIA_CLICK_DEFAULT_CATEGORY,
205
+ )
206
+
207
  return response_data
208
 
209
  # -------------------------------------------------
 
213
  async def colorize(
214
  request: Request,
215
  file: UploadFile = File(...),
216
+ x_firebase_appcheck: str = Header(None),
217
+ user_id: Optional[str] = Form(None),
218
+ category_id: Optional[str] = Form(None),
219
+ categoryId: Optional[str] = Form(None),
220
  ):
221
  import time
222
  start_time = time.time()
 
224
  verify_app_check_token(x_firebase_appcheck)
225
 
226
  ip_address = request.client.host if request.client else None
227
+ effective_user_id = _resolve_user_id(request, user_id)
228
+ effective_category_id = category_id or categoryId
229
 
230
  if not file.content_type.startswith("image/"):
231
  log_api_call(
 
262
  result_id=result_id_clean,
263
  model_type="gan",
264
  processing_time=processing_time,
265
+ user_id=effective_user_id,
266
  ip_address=ip_address
267
  )
268
 
 
272
  status_code=200,
273
  request_data={"filename": file.filename, "content_type": file.content_type},
274
  response_data=response_data,
275
+ user_id=effective_user_id,
276
  ip_address=ip_address
277
  )
278
 
279
+ log_media_click(
280
+ user_id=effective_user_id,
281
+ category_id=effective_category_id,
282
+ endpoint_path=str(request.url.path),
283
+ default_category_id=MEDIA_CLICK_DEFAULT_CATEGORY,
284
+ )
285
+
286
  return response_data
287
 
288
  # -------------------------------------------------
app/main_fastai.py CHANGED
@@ -18,7 +18,7 @@ import logging
18
  from pathlib import Path
19
  from typing import Optional
20
 
21
- from fastapi import FastAPI, UploadFile, File, HTTPException, Depends, Request
22
  from fastapi.responses import FileResponse, JSONResponse
23
  from fastapi.middleware.cors import CORSMiddleware
24
  from fastapi.staticfiles import StaticFiles
@@ -37,7 +37,14 @@ from huggingface_hub import from_pretrained_fastai
37
 
38
  from app.config import settings
39
  from app.pytorch_colorizer import PyTorchColorizer
40
- from app.database import get_database, log_api_call, log_image_upload, log_colorization, close_connection
 
 
 
 
 
 
 
41
 
42
  # Configure logging
43
  logging.basicConfig(
@@ -200,6 +207,11 @@ async def verify_request(request: Request):
200
  # Neither token required nor provided → allow (App Check disabled)
201
  return True
202
 
 
 
 
 
 
203
  @app.get("/api")
204
  async def api_info(request: Request):
205
  """API info endpoint"""
@@ -357,6 +369,9 @@ def colorize_pil(image: Image.Image) -> Image.Image:
357
  async def colorize_api(
358
  request: Request,
359
  file: UploadFile = File(...),
 
 
 
360
  verified: bool = Depends(verify_request)
361
  ):
362
  """
@@ -366,10 +381,9 @@ async def colorize_api(
366
  import time
367
  start_time = time.time()
368
 
369
- user_id = None
370
- if hasattr(request, 'state') and hasattr(request.state, 'user'):
371
- user_id = request.state.user.get("uid")
372
-
373
  ip_address = request.client.host if request.client else None
374
 
375
  # Allow fallback colorization even if model isn't loaded
@@ -382,7 +396,7 @@ async def colorize_api(
382
  method="POST",
383
  status_code=400,
384
  error="File must be an image",
385
- user_id=user_id,
386
  ip_address=ip_address
387
  )
388
  raise HTTPException(status_code=400, detail="File must be an image")
@@ -409,7 +423,7 @@ async def colorize_api(
409
  result_id=result_id,
410
  model_type=model_type,
411
  processing_time=processing_time,
412
- user_id=user_id,
413
  ip_address=ip_address
414
  )
415
 
@@ -419,9 +433,17 @@ async def colorize_api(
419
  status_code=200,
420
  request_data={"filename": file.filename, "content_type": file.content_type},
421
  response_data={"result_id": result_id, "filename": output_filename},
422
- user_id=user_id,
423
  ip_address=ip_address
424
  )
 
 
 
 
 
 
 
 
425
 
426
  # Return the image file
427
  return FileResponse(
@@ -437,7 +459,7 @@ async def colorize_api(
437
  method="POST",
438
  status_code=500,
439
  error=error_msg,
440
- user_id=user_id,
441
  ip_address=ip_address
442
  )
443
  raise HTTPException(status_code=500, detail=f"Error colorizing image: {error_msg}")
 
18
  from pathlib import Path
19
  from typing import Optional
20
 
21
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Depends, Request, Form
22
  from fastapi.responses import FileResponse, JSONResponse
23
  from fastapi.middleware.cors import CORSMiddleware
24
  from fastapi.staticfiles import StaticFiles
 
37
 
38
  from app.config import settings
39
  from app.pytorch_colorizer import PyTorchColorizer
40
+ from app.database import (
41
+ get_database,
42
+ log_api_call,
43
+ log_image_upload,
44
+ log_colorization,
45
+ log_media_click,
46
+ close_connection,
47
+ )
48
 
49
  # Configure logging
50
  logging.basicConfig(
 
207
  # Neither token required nor provided → allow (App Check disabled)
208
  return True
209
 
210
+
211
+ def _resolve_user_id(request: Request, supplied_user_id: Optional[str]) -> Optional[str]:
212
+ """Return supplied user_id if provided, otherwise None (will auto-generate in log_media_click)."""
213
+ return supplied_user_id
214
+
215
  @app.get("/api")
216
  async def api_info(request: Request):
217
  """API info endpoint"""
 
369
  async def colorize_api(
370
  request: Request,
371
  file: UploadFile = File(...),
372
+ user_id: Optional[str] = Form(None),
373
+ category_id: Optional[str] = Form(None),
374
+ categoryId: Optional[str] = Form(None),
375
  verified: bool = Depends(verify_request)
376
  ):
377
  """
 
381
  import time
382
  start_time = time.time()
383
 
384
+ effective_user_id = _resolve_user_id(request, user_id)
385
+ effective_category_id = category_id or categoryId
386
+
 
387
  ip_address = request.client.host if request.client else None
388
 
389
  # Allow fallback colorization even if model isn't loaded
 
396
  method="POST",
397
  status_code=400,
398
  error="File must be an image",
399
+ user_id=effective_user_id,
400
  ip_address=ip_address
401
  )
402
  raise HTTPException(status_code=400, detail="File must be an image")
 
423
  result_id=result_id,
424
  model_type=model_type,
425
  processing_time=processing_time,
426
+ user_id=effective_user_id,
427
  ip_address=ip_address
428
  )
429
 
 
433
  status_code=200,
434
  request_data={"filename": file.filename, "content_type": file.content_type},
435
  response_data={"result_id": result_id, "filename": output_filename},
436
+ user_id=effective_user_id,
437
  ip_address=ip_address
438
  )
439
+
440
+ # Best-effort media click tracking (admin DB)
441
+ log_media_click(
442
+ user_id=effective_user_id,
443
+ category_id=effective_category_id,
444
+ endpoint_path=str(request.url.path),
445
+ default_category_id=settings.DEFAULT_CATEGORY_FALLBACK,
446
+ )
447
 
448
  # Return the image file
449
  return FileResponse(
 
459
  method="POST",
460
  status_code=500,
461
  error=error_msg,
462
+ user_id=effective_user_id,
463
  ip_address=ip_address
464
  )
465
  raise HTTPException(status_code=500, detail=f"Error colorizing image: {error_msg}")
app/main_sdxl.py CHANGED
@@ -9,7 +9,7 @@ import logging
9
  from pathlib import Path
10
  from typing import Optional, Tuple
11
 
12
- from fastapi import FastAPI, UploadFile, File, HTTPException, Depends, Request, Body
13
  from fastapi.responses import FileResponse, JSONResponse
14
  from fastapi.middleware.cors import CORSMiddleware
15
  from fastapi.staticfiles import StaticFiles
@@ -25,7 +25,14 @@ from pydantic import BaseModel, EmailStr
25
  from huggingface_hub import InferenceClient
26
 
27
  from app.config import settings
28
- from app.database import get_database, log_api_call, log_image_upload, log_colorization, close_connection
 
 
 
 
 
 
 
29
 
30
  # Configure logging
31
  logging.basicConfig(
@@ -300,6 +307,11 @@ async def verify_request(request: Request):
300
  return True
301
 
302
 
 
 
 
 
 
303
  # ========== Auth Endpoints ==========
304
 
305
  @app.post("/auth/register", response_model=TokenResponse)
@@ -626,16 +638,18 @@ def colorize_image_sdxl(
626
  async def upload_image(
627
  request: Request,
628
  file: UploadFile = File(...),
 
 
 
629
  verified: bool = Depends(verify_request)
630
  ):
631
  """
632
  Upload an image and get the uploaded image URL.
633
  Requires Firebase App Check authentication.
634
  """
635
- user_id = None
636
- if hasattr(request, 'state') and hasattr(request.state, 'user'):
637
- user_id = request.state.user.get("uid")
638
-
639
  ip_address = request.client.host if request.client else None
640
 
641
  if not file.content_type or not file.content_type.startswith("image/"):
@@ -644,7 +658,7 @@ async def upload_image(
644
  method="POST",
645
  status_code=400,
646
  error="File must be an image",
647
- user_id=user_id,
648
  ip_address=ip_address
649
  )
650
  raise HTTPException(status_code=400, detail="File must be an image")
@@ -682,7 +696,7 @@ async def upload_image(
682
  filename=file.filename or image_id,
683
  file_size=file_size,
684
  content_type=file.content_type or "image/jpeg",
685
- user_id=user_id,
686
  ip_address=ip_address
687
  )
688
 
@@ -692,9 +706,17 @@ async def upload_image(
692
  status_code=200,
693
  request_data={"filename": file.filename, "content_type": file.content_type},
694
  response_data=response_data,
695
- user_id=user_id,
696
  ip_address=ip_address
697
  )
 
 
 
 
 
 
 
 
698
 
699
  return JSONResponse(response_data)
700
  except Exception as e:
@@ -705,7 +727,7 @@ async def upload_image(
705
  method="POST",
706
  status_code=500,
707
  error=error_msg,
708
- user_id=user_id,
709
  ip_address=ip_address
710
  )
711
  raise HTTPException(status_code=500, detail=f"Error uploading image: {error_msg}")
@@ -719,6 +741,9 @@ async def colorize_api(
719
  negative_prompt: Optional[str] = None,
720
  seed: int = 123,
721
  num_inference_steps: int = 8,
 
 
 
722
  verified: bool = Depends(verify_request)
723
  ):
724
  """
@@ -728,10 +753,9 @@ async def colorize_api(
728
  import time
729
  start_time = time.time()
730
 
731
- user_id = None
732
- if hasattr(request, 'state') and hasattr(request.state, 'user'):
733
- user_id = request.state.user.get("uid")
734
-
735
  ip_address = request.client.host if request.client else None
736
 
737
  if inference_client is None:
@@ -740,7 +764,7 @@ async def colorize_api(
740
  method="POST",
741
  status_code=503,
742
  error="Inference API client not initialized",
743
- user_id=user_id,
744
  ip_address=ip_address
745
  )
746
  raise HTTPException(status_code=503, detail="Inference API client not initialized")
@@ -751,7 +775,7 @@ async def colorize_api(
751
  method="POST",
752
  status_code=400,
753
  error="File must be an image",
754
- user_id=user_id,
755
  ip_address=ip_address
756
  )
757
  raise HTTPException(status_code=400, detail="File must be an image")
@@ -799,7 +823,7 @@ async def colorize_api(
799
  prompt=positive_prompt,
800
  model_type="sdxl",
801
  processing_time=processing_time,
802
- user_id=user_id,
803
  ip_address=ip_address
804
  )
805
 
@@ -815,9 +839,17 @@ async def colorize_api(
815
  "num_inference_steps": num_inference_steps
816
  },
817
  response_data=response_data,
818
- user_id=user_id,
819
  ip_address=ip_address
820
  )
 
 
 
 
 
 
 
 
821
 
822
  return JSONResponse(response_data)
823
  except Exception as e:
@@ -828,7 +860,7 @@ async def colorize_api(
828
  method="POST",
829
  status_code=500,
830
  error=error_msg,
831
- user_id=user_id,
832
  ip_address=ip_address
833
  )
834
  raise HTTPException(status_code=500, detail=f"Error colorizing image: {error_msg}")
 
9
  from pathlib import Path
10
  from typing import Optional, Tuple
11
 
12
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Depends, Request, Body, Form
13
  from fastapi.responses import FileResponse, JSONResponse
14
  from fastapi.middleware.cors import CORSMiddleware
15
  from fastapi.staticfiles import StaticFiles
 
25
  from huggingface_hub import InferenceClient
26
 
27
  from app.config import settings
28
+ from app.database import (
29
+ get_database,
30
+ log_api_call,
31
+ log_image_upload,
32
+ log_colorization,
33
+ log_media_click,
34
+ close_connection,
35
+ )
36
 
37
  # Configure logging
38
  logging.basicConfig(
 
307
  return True
308
 
309
 
310
+ def _resolve_user_id(request: Request, supplied_user_id: Optional[str]) -> Optional[str]:
311
+ """Return supplied user_id if provided, otherwise None (will auto-generate in log_media_click)."""
312
+ return supplied_user_id
313
+
314
+
315
  # ========== Auth Endpoints ==========
316
 
317
  @app.post("/auth/register", response_model=TokenResponse)
 
638
  async def upload_image(
639
  request: Request,
640
  file: UploadFile = File(...),
641
+ user_id: Optional[str] = Form(None),
642
+ category_id: Optional[str] = Form(None),
643
+ categoryId: Optional[str] = Form(None),
644
  verified: bool = Depends(verify_request)
645
  ):
646
  """
647
  Upload an image and get the uploaded image URL.
648
  Requires Firebase App Check authentication.
649
  """
650
+ effective_user_id = _resolve_user_id(request, user_id)
651
+ effective_category_id = category_id or categoryId
652
+
 
653
  ip_address = request.client.host if request.client else None
654
 
655
  if not file.content_type or not file.content_type.startswith("image/"):
 
658
  method="POST",
659
  status_code=400,
660
  error="File must be an image",
661
+ user_id=effective_user_id,
662
  ip_address=ip_address
663
  )
664
  raise HTTPException(status_code=400, detail="File must be an image")
 
696
  filename=file.filename or image_id,
697
  file_size=file_size,
698
  content_type=file.content_type or "image/jpeg",
699
+ user_id=effective_user_id,
700
  ip_address=ip_address
701
  )
702
 
 
706
  status_code=200,
707
  request_data={"filename": file.filename, "content_type": file.content_type},
708
  response_data=response_data,
709
+ user_id=effective_user_id,
710
  ip_address=ip_address
711
  )
712
+
713
+ # Best-effort media click tracking (admin DB)
714
+ log_media_click(
715
+ user_id=effective_user_id,
716
+ category_id=effective_category_id,
717
+ endpoint_path=str(request.url.path),
718
+ default_category_id=settings.DEFAULT_CATEGORY_FALLBACK,
719
+ )
720
 
721
  return JSONResponse(response_data)
722
  except Exception as e:
 
727
  method="POST",
728
  status_code=500,
729
  error=error_msg,
730
+ user_id=effective_user_id,
731
  ip_address=ip_address
732
  )
733
  raise HTTPException(status_code=500, detail=f"Error uploading image: {error_msg}")
 
741
  negative_prompt: Optional[str] = None,
742
  seed: int = 123,
743
  num_inference_steps: int = 8,
744
+ user_id: Optional[str] = Form(None),
745
+ category_id: Optional[str] = Form(None),
746
+ categoryId: Optional[str] = Form(None),
747
  verified: bool = Depends(verify_request)
748
  ):
749
  """
 
753
  import time
754
  start_time = time.time()
755
 
756
+ effective_user_id = _resolve_user_id(request, user_id)
757
+ effective_category_id = category_id or categoryId
758
+
 
759
  ip_address = request.client.host if request.client else None
760
 
761
  if inference_client is None:
 
764
  method="POST",
765
  status_code=503,
766
  error="Inference API client not initialized",
767
+ user_id=effective_user_id,
768
  ip_address=ip_address
769
  )
770
  raise HTTPException(status_code=503, detail="Inference API client not initialized")
 
775
  method="POST",
776
  status_code=400,
777
  error="File must be an image",
778
+ user_id=effective_user_id,
779
  ip_address=ip_address
780
  )
781
  raise HTTPException(status_code=400, detail="File must be an image")
 
823
  prompt=positive_prompt,
824
  model_type="sdxl",
825
  processing_time=processing_time,
826
+ user_id=effective_user_id,
827
  ip_address=ip_address
828
  )
829
 
 
839
  "num_inference_steps": num_inference_steps
840
  },
841
  response_data=response_data,
842
+ user_id=effective_user_id,
843
  ip_address=ip_address
844
  )
845
+
846
+ # Best-effort media click tracking (admin DB)
847
+ log_media_click(
848
+ user_id=effective_user_id,
849
+ category_id=effective_category_id,
850
+ endpoint_path=str(request.url.path),
851
+ default_category_id=settings.DEFAULT_CATEGORY_FALLBACK,
852
+ )
853
 
854
  return JSONResponse(response_data)
855
  except Exception as e:
 
860
  method="POST",
861
  status_code=500,
862
  error=error_msg,
863
+ user_id=effective_user_id,
864
  ip_address=ip_address
865
  )
866
  raise HTTPException(status_code=500, detail=f"Error colorizing image: {error_msg}")
postman_collection.json CHANGED
@@ -51,6 +51,11 @@
51
  "key": "Authorization",
52
  "value": "Bearer {{firebase_token}}",
53
  "type": "text"
 
 
 
 
 
54
  }
55
  ],
56
  "body": {
@@ -61,6 +66,18 @@
61
  "type": "file",
62
  "src": [],
63
  "description": "Image file to colorize"
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
  ]
66
  },
@@ -86,6 +103,11 @@
86
  "key": "Authorization",
87
  "value": "Bearer {{firebase_token}}",
88
  "type": "text"
 
 
 
 
 
89
  }
90
  ],
91
  "body": {
@@ -102,6 +124,18 @@
102
  "value": "vibrant natural colors, high quality photo",
103
  "type": "text",
104
  "description": "Additional descriptive text to enhance the caption"
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
  ]
107
  },
@@ -127,6 +161,11 @@
127
  "key": "Authorization",
128
  "value": "Bearer {{firebase_token}}",
129
  "type": "text"
 
 
 
 
 
130
  }
131
  ],
132
  "body": {
@@ -161,6 +200,18 @@
161
  "value": "8",
162
  "type": "text",
163
  "description": "Number of inference steps"
 
 
 
 
 
 
 
 
 
 
 
 
164
  }
165
  ]
166
  },
 
51
  "key": "Authorization",
52
  "value": "Bearer {{firebase_token}}",
53
  "type": "text"
54
+ },
55
+ {
56
+ "key": "X-Firebase-AppCheck",
57
+ "value": "{{firebase_app_check}}",
58
+ "type": "text"
59
  }
60
  ],
61
  "body": {
 
66
  "type": "file",
67
  "src": [],
68
  "description": "Image file to colorize"
69
+ },
70
+ {
71
+ "key": "user_id",
72
+ "value": "{{user_id}}",
73
+ "type": "text",
74
+ "description": "Optional user id (ObjectId or numeric) for media click logging"
75
+ },
76
+ {
77
+ "key": "category_id",
78
+ "value": "{{category_id}}",
79
+ "type": "text",
80
+ "description": "Optional category id; endpoint default used when omitted"
81
  }
82
  ]
83
  },
 
103
  "key": "Authorization",
104
  "value": "Bearer {{firebase_token}}",
105
  "type": "text"
106
+ },
107
+ {
108
+ "key": "X-Firebase-AppCheck",
109
+ "value": "{{firebase_app_check}}",
110
+ "type": "text"
111
  }
112
  ],
113
  "body": {
 
124
  "value": "vibrant natural colors, high quality photo",
125
  "type": "text",
126
  "description": "Additional descriptive text to enhance the caption"
127
+ },
128
+ {
129
+ "key": "user_id",
130
+ "value": "{{user_id}}",
131
+ "type": "text",
132
+ "description": "Optional user id (ObjectId or numeric) for media click logging"
133
+ },
134
+ {
135
+ "key": "category_id",
136
+ "value": "{{category_id}}",
137
+ "type": "text",
138
+ "description": "Optional category id; endpoint default used when omitted"
139
  }
140
  ]
141
  },
 
161
  "key": "Authorization",
162
  "value": "Bearer {{firebase_token}}",
163
  "type": "text"
164
+ },
165
+ {
166
+ "key": "X-Firebase-AppCheck",
167
+ "value": "{{firebase_app_check}}",
168
+ "type": "text"
169
  }
170
  ],
171
  "body": {
 
200
  "value": "8",
201
  "type": "text",
202
  "description": "Number of inference steps"
203
+ },
204
+ {
205
+ "key": "user_id",
206
+ "value": "{{user_id}}",
207
+ "type": "text",
208
+ "description": "Optional user id (ObjectId or numeric) for media click logging"
209
+ },
210
+ {
211
+ "key": "category_id",
212
+ "value": "{{category_id}}",
213
+ "type": "text",
214
+ "description": "Optional category id; endpoint default used when omitted"
215
  }
216
  ]
217
  },