no auth mode

This commit is contained in:
phact 2025-09-02 10:50:20 -04:00
parent 7b2c16ca21
commit 1b14d6c8e5
14 changed files with 303 additions and 143 deletions

View file

@ -41,7 +41,7 @@ interface Connection {
}
function KnowledgeSourcesPage() {
const { isAuthenticated } = useAuth()
const { isAuthenticated, isNoAuthMode } = useAuth()
const { addTask, tasks } = useTask()
const searchParams = useSearchParams()
@ -351,48 +351,66 @@ function KnowledgeSourcesPage() {
<h2 className="text-2xl font-semibold tracking-tight mb-2">Cloud Connectors</h2>
</div>
{/* Sync Settings */}
<div className="flex items-center justify-between py-4">
<div>
<h3 className="text-lg font-medium">Sync Settings</h3>
<p className="text-sm text-muted-foreground">Configure how many files to sync when manually triggering a sync</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="syncAllFiles"
checked={syncAllFiles}
onCheckedChange={(checked) => {
setSyncAllFiles(!!checked)
if (checked) {
setMaxFiles(0)
} else {
setMaxFiles(10)
}
}}
/>
<Label htmlFor="syncAllFiles" className="font-medium whitespace-nowrap">
Sync all files
{/* Conditional Sync Settings or No-Auth Message */}
{isNoAuthMode ? (
<Card className="border-yellow-500/50 bg-yellow-500/5">
<CardHeader>
<CardTitle className="text-lg text-yellow-600">Cloud connectors are only available with auth mode enabled</CardTitle>
<CardDescription className="text-sm">
Please provide the following environment variables and restart:
</CardDescription>
</CardHeader>
<CardContent>
<div className="bg-muted rounded-md p-4 font-mono text-sm">
<div className="text-muted-foreground mb-2"># make here https://console.cloud.google.com/apis/credentials</div>
<div>GOOGLE_OAUTH_CLIENT_ID=</div>
<div>GOOGLE_OAUTH_CLIENT_SECRET=</div>
</div>
</CardContent>
</Card>
) : (
<div className="flex items-center justify-between py-4">
<div>
<h3 className="text-lg font-medium">Sync Settings</h3>
<p className="text-sm text-muted-foreground">Configure how many files to sync when manually triggering a sync</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="syncAllFiles"
checked={syncAllFiles}
onCheckedChange={(checked) => {
setSyncAllFiles(!!checked)
if (checked) {
setMaxFiles(0)
} else {
setMaxFiles(10)
}
}}
/>
<Label htmlFor="syncAllFiles" className="font-medium whitespace-nowrap">
Sync all files
</Label>
</div>
<Label htmlFor="maxFiles" className="font-medium whitespace-nowrap">
Max files per sync:
</Label>
</div>
<Label htmlFor="maxFiles" className="font-medium whitespace-nowrap">
Max files per sync:
</Label>
<div className="relative">
<Input
id="maxFiles"
type="number"
value={syncAllFiles ? 0 : maxFiles}
onChange={(e) => setMaxFiles(parseInt(e.target.value) || 10)}
disabled={syncAllFiles}
className="w-16 min-w-16 max-w-16 flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
min="1"
max="100"
title={syncAllFiles ? "Disabled when 'Sync all files' is checked" : "Leave blank or set to 0 for unlimited"}
/>
<div className="relative">
<Input
id="maxFiles"
type="number"
value={syncAllFiles ? 0 : maxFiles}
onChange={(e) => setMaxFiles(parseInt(e.target.value) || 10)}
disabled={syncAllFiles}
className="w-16 min-w-16 max-w-16 flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
min="1"
max="100"
title={syncAllFiles ? "Disabled when 'Sync all files' is checked" : "Leave blank or set to 0 for unlimited"}
/>
</div>
</div>
</div>
</div>
)}
{/* Connectors Grid */}
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
@ -468,33 +486,6 @@ function KnowledgeSourcesPage() {
))}
</div>
{/* Coming Soon Section */}
<Card className="border-dashed">
<CardHeader>
<CardTitle className="text-lg text-muted-foreground">Coming Soon</CardTitle>
<CardDescription>
Additional connectors are in development
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 opacity-50">
<div className="flex items-center gap-3 p-3 rounded-lg border border-dashed">
<div className="w-8 h-8 bg-blue-600 rounded flex items-center justify-center text-white font-bold leading-none">D</div>
<div>
<div className="font-medium">Dropbox</div>
<div className="text-sm text-muted-foreground">File storage</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 rounded-lg border border-dashed">
<div className="w-8 h-8 bg-orange-600 rounded flex items-center justify-center text-white font-bold leading-none">B</div>
<div>
<div className="font-medium">Box</div>
<div className="text-sm text-muted-foreground">Enterprise file sharing</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
)

View file

@ -14,8 +14,8 @@ async def chat_endpoint(request: Request, chat_service, session_manager):
user = request.state.user
user_id = user.user_id
# Get JWT token from request cookie
jwt_token = request.cookies.get("auth_token")
# Get JWT token from auth middleware
jwt_token = request.state.jwt_token
if not prompt:
return JSONResponse({"error": "Prompt is required"}, status_code=400)
@ -57,8 +57,8 @@ async def langflow_endpoint(request: Request, chat_service, session_manager):
user = request.state.user
user_id = user.user_id
# Get JWT token from request cookie
jwt_token = request.cookies.get("auth_token")
# Get JWT token from auth middleware
jwt_token = request.state.jwt_token
if not prompt:
return JSONResponse({"error": "Prompt is required"}, status_code=400)

View file

@ -20,7 +20,7 @@ async def connector_sync(request: Request, connector_service, session_manager):
print(f"[DEBUG] Starting connector sync for connector_type={connector_type}, max_files={max_files}")
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
print(f"[DEBUG] User: {user.user_id}")
# Get all active connections for this connector type and user

View file

@ -18,7 +18,7 @@ async def create_knowledge_filter(request: Request, knowledge_filter_service, se
return JSONResponse({"error": "Query data is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
# Create knowledge filter document
filter_id = str(uuid.uuid4())
@ -54,7 +54,7 @@ async def search_knowledge_filters(request: Request, knowledge_filter_service, s
limit = payload.get("limit", 20)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
result = await knowledge_filter_service.search_knowledge_filters(query, user_id=user.user_id, jwt_token=jwt_token, limit=limit)
@ -75,7 +75,7 @@ async def get_knowledge_filter(request: Request, knowledge_filter_service, sessi
return JSONResponse({"error": "Knowledge filter ID is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
result = await knowledge_filter_service.get_knowledge_filter(filter_id, user_id=user.user_id, jwt_token=jwt_token)
@ -100,7 +100,7 @@ async def update_knowledge_filter(request: Request, knowledge_filter_service, se
payload = await request.json()
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
# First, get the existing knowledge filter
existing_result = await knowledge_filter_service.get_knowledge_filter(filter_id, user_id=user.user_id, jwt_token=jwt_token)
@ -147,7 +147,7 @@ async def delete_knowledge_filter(request: Request, knowledge_filter_service, se
return JSONResponse({"error": "Knowledge filter ID is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
result = await knowledge_filter_service.delete_knowledge_filter(filter_id, user_id=user.user_id, jwt_token=jwt_token)
@ -171,7 +171,7 @@ async def subscribe_to_knowledge_filter(request: Request, knowledge_filter_servi
payload = await request.json()
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
# Get the knowledge filter to validate it exists and get its details
filter_result = await knowledge_filter_service.get_knowledge_filter(filter_id, user_id=user.user_id, jwt_token=jwt_token)
@ -227,7 +227,7 @@ async def list_knowledge_filter_subscriptions(request: Request, knowledge_filter
return JSONResponse({"error": "Knowledge filter ID is required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
result = await knowledge_filter_service.get_filter_subscriptions(filter_id, user_id=user.user_id, jwt_token=jwt_token)
@ -251,7 +251,7 @@ async def cancel_knowledge_filter_subscription(request: Request, knowledge_filte
return JSONResponse({"error": "Knowledge filter ID and subscription ID are required"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
# Get subscription details to find the monitor ID
subscriptions_result = await knowledge_filter_service.get_filter_subscriptions(filter_id, user_id=user.user_id, jwt_token=jwt_token)

View file

@ -14,8 +14,10 @@ async def search(request: Request, search_service, session_manager):
score_threshold = payload.get("scoreThreshold", 0) # Optional score threshold, defaults to 0
user = request.state.user
# Extract JWT token from cookie for OpenSearch OIDC auth
jwt_token = request.cookies.get("auth_token")
# Extract JWT token from auth middleware
jwt_token = request.state.jwt_token
print(f"[DEBUG] search API: user={user}, user_id={user.user_id if user else None}, jwt_token={'None' if jwt_token is None else 'present'}")
result = await search_service.search(query, user_id=user.user_id, jwt_token=jwt_token, filters=filters, limit=limit, score_threshold=score_threshold)
return JSONResponse(result, status_code=200)

View file

@ -10,7 +10,7 @@ async def upload(request: Request, document_service, session_manager):
form = await request.form()
upload_file = form["file"]
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
result = await document_service.process_upload_file(upload_file, owner_user_id=user.user_id, jwt_token=jwt_token, owner_name=user.name, owner_email=user.email)
return JSONResponse(result, status_code=201) # Created
@ -36,7 +36,7 @@ async def upload_path(request: Request, task_service, session_manager):
return JSONResponse({"error": "No files found in directory"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
task_id = await task_service.create_upload_task(user.user_id, file_paths, jwt_token=jwt_token, owner_name=user.name, owner_email=user.email)
return JSONResponse({
@ -55,8 +55,8 @@ async def upload_context(request: Request, document_service, chat_service, sessi
previous_response_id = form.get("previous_response_id")
endpoint = form.get("endpoint", "langflow")
# Get JWT token from request cookie for authentication
jwt_token = request.cookies.get("auth_token")
# Get JWT token from auth middleware
jwt_token = request.state.jwt_token
# Get user info from request state (set by auth middleware)
user = request.state.user
@ -122,7 +122,7 @@ async def upload_bucket(request: Request, task_service, session_manager):
return JSONResponse({"error": "No files found in bucket"}, status_code=400)
user = request.state.user
jwt_token = request.cookies.get("auth_token")
jwt_token = request.state.jwt_token
from models.processors import S3FileProcessor

View file

@ -2,10 +2,15 @@ from starlette.requests import Request
from starlette.responses import JSONResponse
from typing import Optional
from session_manager import User
from config.settings import is_no_auth_mode
def get_current_user(request: Request, session_manager) -> Optional[User]:
"""Extract current user from request cookies"""
# In no-auth mode, ignore cookies entirely
if is_no_auth_mode():
return None
auth_token = request.cookies.get("auth_token")
if not auth_token:
return None
@ -17,6 +22,25 @@ def require_auth(session_manager):
"""Decorator to require authentication for endpoints"""
def decorator(handler):
async def wrapper(request: Request):
# In no-auth mode, bypass authentication entirely
if is_no_auth_mode():
print(f"[DEBUG] No-auth mode: Creating anonymous user")
# Create an anonymous user object so endpoints don't break
from session_manager import User
from datetime import datetime
request.state.user = User(
user_id="anonymous",
email="anonymous@localhost",
name="Anonymous User",
picture=None,
provider="none",
created_at=datetime.now(),
last_login=datetime.now()
)
request.state.jwt_token = None # No JWT in no-auth mode
print(f"[DEBUG] Set user_id=anonymous, jwt_token=None")
return await handler(request)
user = get_current_user(request, session_manager)
if not user:
return JSONResponse(
@ -24,8 +48,9 @@ def require_auth(session_manager):
status_code=401
)
# Add user to request state so handlers can access it
# Add user and JWT token to request state so handlers can access them
request.state.user = user
request.state.jwt_token = None if is_no_auth_mode() else request.cookies.get("auth_token")
return await handler(request)
return wrapper
@ -36,8 +61,25 @@ def optional_auth(session_manager):
"""Decorator to optionally extract user for endpoints"""
def decorator(handler):
async def wrapper(request: Request):
user = get_current_user(request, session_manager)
request.state.user = user # Can be None
# In no-auth mode, create anonymous user
if is_no_auth_mode():
# Create an anonymous user object so endpoints don't break
from session_manager import User
from datetime import datetime
request.state.user = User(
user_id="anonymous",
email="anonymous@localhost",
name="Anonymous User",
picture=None,
provider="none",
created_at=datetime.now(),
last_login=datetime.now()
)
request.state.jwt_token = None # No JWT in no-auth mode
else:
user = get_current_user(request, session_manager)
request.state.user = user # Can be None
request.state.jwt_token = None if is_no_auth_mode() else (request.cookies.get("auth_token") if user else None)
return await handler(request)
return wrapper

View file

@ -30,6 +30,12 @@ SESSION_SECRET = os.getenv("SESSION_SECRET", "your-secret-key-change-in-producti
GOOGLE_OAUTH_CLIENT_ID = os.getenv("GOOGLE_OAUTH_CLIENT_ID")
GOOGLE_OAUTH_CLIENT_SECRET = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
def is_no_auth_mode():
"""Check if we're running in no-auth mode (OAuth credentials missing)"""
result = not (GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET)
print(f"[DEBUG] is_no_auth_mode() = {result}, CLIENT_ID={GOOGLE_OAUTH_CLIENT_ID is not None}, CLIENT_SECRET={GOOGLE_OAUTH_CLIENT_SECRET is not None}")
return result
# Webhook configuration - must be set to enable webhooks
WEBHOOK_BASE_URL = os.getenv("WEBHOOK_BASE_URL") # No default - must be explicitly configured

View file

@ -192,12 +192,17 @@ async def initialize_services():
# Load persisted connector connections at startup so webhooks and syncs
# can resolve existing subscriptions immediately after server boot
try:
await connector_service.initialize()
loaded_count = len(connector_service.connection_manager.connections)
print(f"[CONNECTORS] Loaded {loaded_count} persisted connection(s) on startup")
except Exception as e:
print(f"[WARNING] Failed to load persisted connections on startup: {e}")
# Skip in no-auth mode since connectors require OAuth
from config.settings import is_no_auth_mode
if not is_no_auth_mode():
try:
await connector_service.initialize()
loaded_count = len(connector_service.connection_manager.connections)
print(f"[CONNECTORS] Loaded {loaded_count} persisted connection(s) on startup")
except Exception as e:
print(f"[WARNING] Failed to load persisted connections on startup: {e}")
else:
print(f"[CONNECTORS] Skipping connection loading in no-auth mode")
return {
'document_service': document_service,

View file

@ -6,7 +6,7 @@ import aiofiles
from datetime import datetime, timedelta
from typing import Optional
from config.settings import WEBHOOK_BASE_URL
from config.settings import WEBHOOK_BASE_URL, is_no_auth_mode
from session_manager import SessionManager
from connectors.google_drive.oauth import GoogleDriveOAuth
from connectors.onedrive.oauth import OneDriveOAuth
@ -24,6 +24,13 @@ class AuthService:
async def init_oauth(self, connector_type: str, purpose: str, connection_name: str,
redirect_uri: str, user_id: str = None) -> dict:
"""Initialize OAuth flow for authentication or data source connection"""
# Check if we're in no-auth mode
if is_no_auth_mode():
if purpose == "app_auth":
raise ValueError("OAuth credentials not configured. Please add GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET environment variables to enable authentication.")
else:
raise ValueError("OAuth credentials not configured. Data source connections require OAuth setup.")
# Validate connector_type based on purpose
if purpose == "app_auth" and connector_type != "google_drive":
raise ValueError("Only Google login supported for app authentication")
@ -253,6 +260,14 @@ class AuthService:
async def get_user_info(self, request) -> Optional[dict]:
"""Get current user information from request"""
# In no-auth mode, return a consistent response
if is_no_auth_mode():
return {
"authenticated": False,
"user": None,
"no_auth_mode": True
}
user = getattr(request.state, 'user', None)
if user:

View file

@ -152,13 +152,18 @@ class DocumentService:
"page": chunk["page"],
"text": chunk["text"],
"chunk_embedding": vect,
"owner": owner_user_id,
"owner_name": owner_name,
"owner_email": owner_email,
"file_size": file_size,
"connector_type": connector_type,
"indexed_time": datetime.datetime.now().isoformat()
}
# Only set owner fields if owner_user_id is provided (for no-auth mode support)
if owner_user_id is not None:
chunk_doc["owner"] = owner_user_id
if owner_name is not None:
chunk_doc["owner_name"] = owner_name
if owner_email is not None:
chunk_doc["owner_email"] = owner_email
chunk_id = f"{file_hash}_{i}"
try:
await opensearch_client.index(index=INDEX_NAME, id=chunk_id, body=chunk_doc)
@ -288,13 +293,18 @@ class DocumentService:
"page": chunk["page"],
"text": chunk["text"],
"chunk_embedding": vect,
"owner": owner_user_id,
"owner_name": owner_name,
"owner_email": owner_email,
"file_size": file_size,
"connector_type": connector_type,
"indexed_time": datetime.datetime.now().isoformat()
}
# Only set owner fields if owner_user_id is provided (for no-auth mode support)
if owner_user_id is not None:
chunk_doc["owner"] = owner_user_id
if owner_name is not None:
chunk_doc["owner_name"] = owner_name
if owner_email is not None:
chunk_doc["owner_email"] = owner_email
chunk_id = f"{slim_doc['id']}_{i}"
try:
await opensearch_client.index(index=INDEX_NAME, id=chunk_id, body=chunk_doc)

View file

@ -132,11 +132,13 @@ class SearchService:
search_body["min_score"] = score_threshold
# Authentication required - DLS will handle document filtering automatically
print(f"[DEBUG] search_service: user_id={user_id}, jwt_token={'None' if jwt_token is None else 'present'}")
if not user_id:
print(f"[DEBUG] search_service: user_id is None/empty, returning auth error")
return {"results": [], "error": "Authentication required"}
# Get user's OpenSearch client with JWT for OIDC auth
opensearch_client = clients.create_user_opensearch_client(jwt_token)
# Get user's OpenSearch client with JWT for OIDC auth through session manager
opensearch_client = self.session_manager.get_user_opensearch_client(user_id, jwt_token)
try:
results = await opensearch_client.search(index=INDEX_NAME, body=search_body)
@ -173,7 +175,8 @@ class SearchService:
async def search(self, query: str, user_id: str = None, jwt_token: str = None, filters: Dict[str, Any] = None, limit: int = 10, score_threshold: float = 0) -> Dict[str, Any]:
"""Public search method for API endpoints"""
# Set auth context if provided (for direct API calls)
if user_id and jwt_token:
from config.settings import is_no_auth_mode
if user_id and (jwt_token or is_no_auth_mode()):
from auth_context import set_auth_context
set_auth_context(user_id, jwt_token)

View file

@ -160,9 +160,37 @@ class SessionManager:
def get_user_opensearch_client(self, user_id: str, jwt_token: str):
"""Get or create OpenSearch client for user with their JWT"""
from config.settings import is_no_auth_mode
print(f"[DEBUG] get_user_opensearch_client: user_id={user_id}, jwt_token={'None' if jwt_token is None else 'present'}, no_auth_mode={is_no_auth_mode()}")
# In no-auth mode, create anonymous JWT for OpenSearch DLS
if is_no_auth_mode() and jwt_token is None:
if not hasattr(self, '_anonymous_jwt'):
# Create anonymous JWT token for OpenSearch OIDC
print(f"[DEBUG] Creating anonymous JWT...")
self._anonymous_jwt = self._create_anonymous_jwt()
print(f"[DEBUG] Anonymous JWT created: {self._anonymous_jwt[:50]}...")
jwt_token = self._anonymous_jwt
print(f"[DEBUG] Using anonymous JWT for OpenSearch")
# Check if we have a cached client for this user
if user_id not in self.user_opensearch_clients:
from config.settings import clients
self.user_opensearch_clients[user_id] = clients.create_user_opensearch_client(jwt_token)
return self.user_opensearch_clients[user_id]
return self.user_opensearch_clients[user_id]
def _create_anonymous_jwt(self) -> str:
"""Create JWT token for anonymous user in no-auth mode"""
anonymous_user = User(
user_id="anonymous",
email="anonymous@localhost",
name="Anonymous User",
picture=None,
provider="none",
created_at=datetime.now(),
last_login=datetime.now()
)
return self.create_jwt_token(anonymous_user)

130
uv.lock generated
View file

@ -4,7 +4,8 @@ requires-python = ">=3.13"
resolution-markers = [
"sys_platform == 'darwin'",
"platform_machine == 'aarch64' and sys_platform == 'linux'",
"(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
"platform_machine == 'x86_64' and sys_platform == 'linux'",
"(platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
]
[[package]]
@ -372,8 +373,10 @@ dependencies = [
{ name = "pydantic" },
{ name = "rtree" },
{ name = "safetensors", extra = ["torch"] },
{ name = "torch" },
{ name = "torchvision" },
{ name = "torch", version = "2.7.1+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "torchvision", version = "0.22.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "torchvision", version = "0.23.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "tqdm" },
{ name = "transformers" },
]
@ -417,8 +420,10 @@ dependencies = [
{ name = "scikit-image" },
{ name = "scipy" },
{ name = "shapely" },
{ name = "torch" },
{ name = "torchvision" },
{ name = "torch", version = "2.7.1+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "torchvision", version = "0.22.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "torchvision", version = "0.23.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/84/4a2cab0e6adde6a85e7ba543862e5fc0250c51f3ac721a078a55cdcff250/easyocr-1.7.2-py3-none-any.whl", hash = "sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c", size = 2870178, upload-time = "2024-09-24T11:34:43.554Z" },
@ -1202,7 +1207,7 @@ name = "nvidia-cudnn-cu12"
version = "9.7.1.26"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/dc/dc825c4b1c83b538e207e34f48f86063c88deaa35d46c651c7c181364ba2/nvidia_cudnn_cu12-9.7.1.26-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:6d011159a158f3cfc47bf851aea79e31bcff60d530b70ef70474c84cac484d07", size = 726851421, upload-time = "2025-02-06T22:18:29.812Z" },
@ -1213,7 +1218,7 @@ name = "nvidia-cufft-cu12"
version = "11.3.3.41"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/26/b53c493c38dccb1f1a42e1a21dc12cba2a77fbe36c652f7726d9ec4aba28/nvidia_cufft_cu12-11.3.3.41-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:da650080ab79fcdf7a4b06aa1b460e99860646b176a43f6208099bdc17836b6a", size = 193118795, upload-time = "2025-01-23T17:56:30.536Z" },
@ -1240,9 +1245,9 @@ name = "nvidia-cusolver-cu12"
version = "11.7.2.55"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/08/953675873a136d96bb12f93b49ba045d1107bc94d2551c52b12fa6c7dec3/nvidia_cusolver_cu12-11.7.2.55-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4d1354102f1e922cee9db51920dba9e2559877cf6ff5ad03a00d853adafb191b", size = 260373342, upload-time = "2025-01-23T17:58:56.406Z" },
@ -1253,7 +1258,7 @@ name = "nvidia-cusparse-cu12"
version = "12.5.7.53"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/ab/31e8149c66213b846c082a3b41b1365b831f41191f9f40c6ddbc8a7d550e/nvidia_cusparse_cu12-12.5.7.53-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c1b61eb8c85257ea07e9354606b26397612627fdcd327bfd91ccf6155e7c86d", size = 292064180, upload-time = "2025-01-23T18:00:23.233Z" },
@ -1386,7 +1391,8 @@ dependencies = [
{ name = "pyjwt" },
{ name = "python-multipart" },
{ name = "starlette" },
{ name = "torch" },
{ name = "torch", version = "2.7.1+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "uvicorn" },
]
@ -1407,7 +1413,8 @@ requires-dist = [
{ name = "pyjwt", specifier = ">=2.8.0" },
{ name = "python-multipart", specifier = ">=0.0.20" },
{ name = "starlette", specifier = ">=0.47.1" },
{ name = "torch", specifier = ">=2.7.1", index = "https://download.pytorch.org/whl/cu128" },
{ name = "torch", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'", specifier = ">=2.7.1" },
{ name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=2.7.1", index = "https://download.pytorch.org/whl/cu128" },
{ name = "uvicorn", specifier = ">=0.35.0" },
]
@ -2094,7 +2101,8 @@ wheels = [
[package.optional-dependencies]
torch = [
{ name = "numpy" },
{ name = "torch" },
{ name = "torch", version = "2.7.1+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
]
[[package]]
@ -2339,11 +2347,14 @@ wheels = [
name = "torch"
version = "2.7.1+cu128"
source = { registry = "https://download.pytorch.org/whl/cu128" }
resolution-markers = [
"platform_machine == 'x86_64' and sys_platform == 'linux'",
]
dependencies = [
{ name = "filelock" },
{ name = "fsspec" },
{ name = "jinja2" },
{ name = "networkx" },
{ name = "filelock", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "fsspec", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "jinja2", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "networkx", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
@ -2358,38 +2369,85 @@ dependencies = [
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "setuptools" },
{ name = "sympy" },
{ name = "triton", marker = "sys_platform == 'linux'" },
{ name = "typing-extensions" },
{ name = "setuptools", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "sympy", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "typing-extensions", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
wheels = [
{ url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:d56d29a6ad7758ba5173cc2b0c51c93e126e2b0a918e874101dc66545283967f" },
{ url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9560425f9ea1af1791507e8ca70d5b9ecf62fed7ca226a95fcd58d0eb2cca78f" },
{ url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:500ad5b670483f62d4052e41948a3fb19e8c8de65b99f8d418d879cbb15a82d6" },
{ url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f112465fdf42eb1297c6dddda1a8b7f411914428b704e1b8a47870c52e290909" },
{ url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c355db49c218ada70321d5c5c9bb3077312738b99113c8f3723ef596b554a7b9" },
{ url = "https://download.pytorch.org/whl/cu128/torch-2.7.1%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:e27e5f7e74179fb5d814a0412e5026e4b50c9e0081e9050bc4c28c992a276eb1" },
]
[[package]]
name = "torch"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"sys_platform == 'darwin'",
"platform_machine == 'aarch64' and sys_platform == 'linux'",
"(platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
{ name = "filelock", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "fsspec", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "jinja2", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "networkx", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "setuptools", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "sympy", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "typing-extensions", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/4e/469ced5a0603245d6a19a556e9053300033f9c5baccf43a3d25ba73e189e/torch-2.8.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2b2f96814e0345f5a5aed9bf9734efa913678ed19caf6dc2cddb7930672d6128", size = 101936856, upload-time = "2025-08-06T14:54:01.526Z" },
{ url = "https://files.pythonhosted.org/packages/16/82/3948e54c01b2109238357c6f86242e6ecbf0c63a1af46906772902f82057/torch-2.8.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:65616ca8ec6f43245e1f5f296603e33923f4c30f93d65e103d9e50c25b35150b", size = 887922844, upload-time = "2025-08-06T14:55:50.78Z" },
{ url = "https://files.pythonhosted.org/packages/e3/54/941ea0a860f2717d86a811adf0c2cd01b3983bdd460d0803053c4e0b8649/torch-2.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:659df54119ae03e83a800addc125856effda88b016dfc54d9f65215c3975be16", size = 241330968, upload-time = "2025-08-06T14:54:45.293Z" },
{ url = "https://files.pythonhosted.org/packages/de/69/8b7b13bba430f5e21d77708b616f767683629fc4f8037564a177d20f90ed/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:1a62a1ec4b0498930e2543535cf70b1bef8c777713de7ceb84cd79115f553767", size = 73915128, upload-time = "2025-08-06T14:54:34.769Z" },
{ url = "https://files.pythonhosted.org/packages/15/0e/8a800e093b7f7430dbaefa80075aee9158ec22e4c4fc3c1a66e4fb96cb4f/torch-2.8.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:83c13411a26fac3d101fe8035a6b0476ae606deb8688e904e796a3534c197def", size = 102020139, upload-time = "2025-08-06T14:54:39.047Z" },
{ url = "https://files.pythonhosted.org/packages/4a/15/5e488ca0bc6162c86a33b58642bc577c84ded17c7b72d97e49b5833e2d73/torch-2.8.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8f0a9d617a66509ded240add3754e462430a6c1fc5589f86c17b433dd808f97a", size = 887990692, upload-time = "2025-08-06T14:56:18.286Z" },
{ url = "https://files.pythonhosted.org/packages/b4/a8/6a04e4b54472fc5dba7ca2341ab219e529f3c07b6941059fbf18dccac31f/torch-2.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a7242b86f42be98ac674b88a4988643b9bc6145437ec8f048fea23f72feb5eca", size = 241603453, upload-time = "2025-08-06T14:55:22.945Z" },
{ url = "https://files.pythonhosted.org/packages/04/6e/650bb7f28f771af0cb791b02348db8b7f5f64f40f6829ee82aa6ce99aabe/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:7b677e17f5a3e69fdef7eb3b9da72622f8d322692930297e4ccb52fefc6c8211", size = 73632395, upload-time = "2025-08-06T14:55:28.645Z" },
]
[[package]]
name = "torchvision"
version = "0.22.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"platform_machine == 'x86_64' and sys_platform == 'linux'",
]
dependencies = [
{ name = "numpy" },
{ name = "pillow" },
{ name = "torch" },
{ name = "numpy", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "pillow", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "torch", version = "2.7.1+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/30/fecdd09fb973e963da68207fe9f3d03ec6f39a935516dc2a98397bf495c6/torchvision-0.22.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c3ae3319624c43cc8127020f46c14aa878406781f0899bb6283ae474afeafbf", size = 1947818, upload-time = "2025-06-04T17:42:51.954Z" },
{ url = "https://files.pythonhosted.org/packages/55/f4/b45f6cd92fa0acfac5e31b8e9258232f25bcdb0709a604e8b8a39d76e411/torchvision-0.22.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:4a614a6a408d2ed74208d0ea6c28a2fbb68290e9a7df206c5fef3f0b6865d307", size = 2471597, upload-time = "2025-06-04T17:42:48.838Z" },
{ url = "https://files.pythonhosted.org/packages/8d/b0/3cffd6a285b5ffee3fe4a31caff49e350c98c5963854474d1c4f7a51dea5/torchvision-0.22.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7ee682be589bb1a002b7704f06b8ec0b89e4b9068f48e79307d2c6e937a9fdf4", size = 7485894, upload-time = "2025-06-04T17:43:01.371Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1d/0ede596fedc2080d18108149921278b59f220fbb398f29619495337b0f86/torchvision-0.22.1-cp313-cp313-win_amd64.whl", hash = "sha256:2566cafcfa47ecfdbeed04bab8cef1307c8d4ef75046f7624b9e55f384880dfe", size = 1708020, upload-time = "2025-06-04T17:43:06.085Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ca/e9a06bd61ee8e04fb4962a3fb524fe6ee4051662db07840b702a9f339b24/torchvision-0.22.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:043d9e35ed69c2e586aff6eb9e2887382e7863707115668ac9d140da58f42cba", size = 2137623, upload-time = "2025-06-04T17:43:05.028Z" },
{ url = "https://files.pythonhosted.org/packages/ab/c8/2ebe90f18e7ffa2120f5c3eab62aa86923185f78d2d051a455ea91461608/torchvision-0.22.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:27142bcc8a984227a6dcf560985e83f52b82a7d3f5fe9051af586a2ccc46ef26", size = 2476561, upload-time = "2025-06-04T17:42:59.691Z" },
{ url = "https://files.pythonhosted.org/packages/94/8b/04c6b15f8c29b39f0679589753091cec8b192ab296d4fdaf9055544c4ec9/torchvision-0.22.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ef46e065502f7300ad6abc98554131c35dc4c837b978d91306658f1a65c00baa", size = 7658543, upload-time = "2025-06-04T17:42:46.064Z" },
{ url = "https://files.pythonhosted.org/packages/ab/c0/131628e6d42682b0502c63fd7f647b8b5ca4bd94088f6c85ca7225db8ac4/torchvision-0.22.1-cp313-cp313t-win_amd64.whl", hash = "sha256:7414eeacfb941fa21acddcd725f1617da5630ec822e498660a4b864d7d998075", size = 1629892, upload-time = "2025-06-04T17:42:57.156Z" },
]
[[package]]
name = "torchvision"
version = "0.23.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"sys_platform == 'darwin'",
"platform_machine == 'aarch64' and sys_platform == 'linux'",
"(platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
]
dependencies = [
{ name = "numpy", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "pillow", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
{ name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/37/45a5b9407a7900f71d61b2b2f62db4b7c632debca397f205fdcacb502780/torchvision-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1c37e325e09a184b730c3ef51424f383ec5745378dc0eca244520aca29722600", size = 1856886, upload-time = "2025-08-06T14:58:05.491Z" },
{ url = "https://files.pythonhosted.org/packages/ac/da/a06c60fc84fc849377cf035d3b3e9a1c896d52dbad493b963c0f1cdd74d0/torchvision-0.23.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2f7fd6c15f3697e80627b77934f77705f3bc0e98278b989b2655de01f6903e1d", size = 2353112, upload-time = "2025-08-06T14:58:26.265Z" },
{ url = "https://files.pythonhosted.org/packages/a0/27/5ce65ba5c9d3b7d2ccdd79892ab86a2f87ac2ca6638f04bb0280321f1a9c/torchvision-0.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a76fafe113b2977be3a21bf78f115438c1f88631d7a87203acb3dd6ae55889e6", size = 8627658, upload-time = "2025-08-06T14:58:15.999Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e4/028a27b60aa578a2fa99d9d7334ff1871bb17008693ea055a2fdee96da0d/torchvision-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:07d069cb29691ff566e3b7f11f20d91044f079e1dbdc9d72e0655899a9b06938", size = 1600749, upload-time = "2025-08-06T14:58:10.719Z" },
{ url = "https://files.pythonhosted.org/packages/05/35/72f91ad9ac7c19a849dedf083d347dc1123f0adeb401f53974f84f1d04c8/torchvision-0.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:2df618e1143805a7673aaf82cb5720dd9112d4e771983156aaf2ffff692eebf9", size = 2047192, upload-time = "2025-08-06T14:58:11.813Z" },
{ url = "https://files.pythonhosted.org/packages/1d/9d/406cea60a9eb9882145bcd62a184ee61e823e8e1d550cdc3c3ea866a9445/torchvision-0.23.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2a3299d2b1d5a7aed2d3b6ffb69c672ca8830671967eb1cee1497bacd82fe47b", size = 2359295, upload-time = "2025-08-06T14:58:17.469Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f4/34662f71a70fa1e59de99772142f22257ca750de05ccb400b8d2e3809c1d/torchvision-0.23.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:76bc4c0b63d5114aa81281390f8472a12a6a35ce9906e67ea6044e5af4cab60c", size = 8800474, upload-time = "2025-08-06T14:58:22.53Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f5/b5a2d841a8d228b5dbda6d524704408e19e7ca6b7bb0f24490e081da1fa1/torchvision-0.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b9e2dabf0da9c8aa9ea241afb63a8f3e98489e706b22ac3f30416a1be377153b", size = 1527667, upload-time = "2025-08-06T14:58:14.446Z" },
]
[[package]]
@ -2430,7 +2488,7 @@ name = "triton"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "setuptools", marker = "sys_platform != 'darwin'" },
{ name = "setuptools", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/1f/dfb531f90a2d367d914adfee771babbd3f1a5b26c3f5fbc458dee21daa78/triton-3.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b89d846b5a4198317fec27a5d3a609ea96b6d557ff44b56c23176546023c4240", size = 155673035, upload-time = "2025-05-29T23:40:02.468Z" },