isa-model 0.4.0__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- isa_model/client.py +466 -43
- isa_model/core/cache/redis_cache.py +12 -3
- isa_model/core/config/config_manager.py +230 -3
- isa_model/core/config.py +90 -0
- isa_model/core/database/direct_db_client.py +114 -0
- isa_model/core/database/migration_manager.py +563 -0
- isa_model/core/database/migrations.py +21 -1
- isa_model/core/database/supabase_client.py +154 -19
- isa_model/core/dependencies.py +316 -0
- isa_model/core/discovery/__init__.py +19 -0
- isa_model/core/discovery/consul_discovery.py +190 -0
- isa_model/core/logging/__init__.py +54 -0
- isa_model/core/logging/influx_logger.py +523 -0
- isa_model/core/logging/loki_logger.py +160 -0
- isa_model/core/models/__init__.py +27 -18
- isa_model/core/models/config_models.py +625 -0
- isa_model/core/models/deployment_billing_tracker.py +430 -0
- isa_model/core/models/model_manager.py +40 -17
- isa_model/core/models/model_metadata.py +690 -0
- isa_model/core/models/model_repo.py +174 -18
- isa_model/core/models/system_models.py +857 -0
- isa_model/core/repositories/__init__.py +9 -0
- isa_model/core/repositories/config_repository.py +912 -0
- isa_model/core/services/intelligent_model_selector.py +399 -21
- isa_model/core/storage/hf_storage.py +1 -1
- isa_model/core/types.py +1 -0
- isa_model/deployment/__init__.py +5 -48
- isa_model/deployment/core/__init__.py +2 -31
- isa_model/deployment/core/deployment_manager.py +1278 -370
- isa_model/deployment/local/__init__.py +31 -0
- isa_model/deployment/local/config.py +248 -0
- isa_model/deployment/local/gpu_gateway.py +607 -0
- isa_model/deployment/local/health_checker.py +428 -0
- isa_model/deployment/local/provider.py +586 -0
- isa_model/deployment/local/tensorrt_service.py +621 -0
- isa_model/deployment/local/transformers_service.py +644 -0
- isa_model/deployment/local/vllm_service.py +527 -0
- isa_model/deployment/modal/__init__.py +8 -0
- isa_model/deployment/modal/config.py +136 -0
- isa_model/deployment/{services/auto_hf_modal_deployer.py → modal/deployer.py} +1 -1
- isa_model/deployment/modal/services/__init__.py +3 -0
- isa_model/deployment/modal/services/audio/__init__.py +1 -0
- isa_model/deployment/modal/services/embedding/__init__.py +1 -0
- isa_model/deployment/modal/services/llm/__init__.py +1 -0
- isa_model/deployment/modal/services/llm/isa_llm_service.py +424 -0
- isa_model/deployment/modal/services/video/__init__.py +1 -0
- isa_model/deployment/modal/services/vision/__init__.py +1 -0
- isa_model/deployment/models/org-org-acme-corp-tenant-a-service-llm-20250825-225822/tenant-a-service_modal_service.py +48 -0
- isa_model/deployment/models/org-test-org-123-prefix-test-service-llm-20250825-225822/prefix-test-service_modal_service.py +48 -0
- isa_model/deployment/models/test-llm-service-llm-20250825-204442/test-llm-service_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-212906/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/models/test-monitoring-gpt2-llm-20250825-213009/test-monitoring-gpt2_modal_service.py +48 -0
- isa_model/deployment/storage/__init__.py +5 -0
- isa_model/deployment/storage/deployment_repository.py +824 -0
- isa_model/deployment/triton/__init__.py +10 -0
- isa_model/deployment/triton/config.py +196 -0
- isa_model/deployment/triton/configs/__init__.py +1 -0
- isa_model/deployment/triton/provider.py +512 -0
- isa_model/deployment/triton/scripts/__init__.py +1 -0
- isa_model/deployment/triton/templates/__init__.py +1 -0
- isa_model/inference/__init__.py +47 -1
- isa_model/inference/ai_factory.py +137 -10
- isa_model/inference/legacy_services/__init__.py +21 -0
- isa_model/inference/legacy_services/model_evaluation.py +637 -0
- isa_model/inference/legacy_services/model_service.py +573 -0
- isa_model/inference/legacy_services/model_serving.py +717 -0
- isa_model/inference/legacy_services/model_training.py +561 -0
- isa_model/inference/models/__init__.py +21 -0
- isa_model/inference/models/inference_config.py +551 -0
- isa_model/inference/models/inference_record.py +675 -0
- isa_model/inference/models/performance_models.py +714 -0
- isa_model/inference/repositories/__init__.py +9 -0
- isa_model/inference/repositories/inference_repository.py +828 -0
- isa_model/inference/services/audio/base_stt_service.py +184 -11
- isa_model/inference/services/audio/openai_stt_service.py +22 -6
- isa_model/inference/services/custom_model_manager.py +277 -0
- isa_model/inference/services/embedding/ollama_embed_service.py +15 -3
- isa_model/inference/services/embedding/resilient_embed_service.py +285 -0
- isa_model/inference/services/llm/__init__.py +10 -2
- isa_model/inference/services/llm/base_llm_service.py +335 -24
- isa_model/inference/services/llm/cerebras_llm_service.py +628 -0
- isa_model/inference/services/llm/helpers/llm_adapter.py +9 -4
- isa_model/inference/services/llm/helpers/llm_prompts.py +342 -0
- isa_model/inference/services/llm/helpers/llm_utils.py +321 -23
- isa_model/inference/services/llm/huggingface_llm_service.py +581 -0
- isa_model/inference/services/llm/local_llm_service.py +747 -0
- isa_model/inference/services/llm/ollama_llm_service.py +9 -2
- isa_model/inference/services/llm/openai_llm_service.py +33 -16
- isa_model/inference/services/llm/yyds_llm_service.py +8 -2
- isa_model/inference/services/vision/__init__.py +22 -1
- isa_model/inference/services/vision/blip_vision_service.py +359 -0
- isa_model/inference/services/vision/helpers/image_utils.py +8 -5
- isa_model/inference/services/vision/isa_vision_service.py +65 -4
- isa_model/inference/services/vision/openai_vision_service.py +19 -10
- isa_model/inference/services/vision/vgg16_vision_service.py +257 -0
- isa_model/serving/api/cache_manager.py +245 -0
- isa_model/serving/api/dependencies/__init__.py +1 -0
- isa_model/serving/api/dependencies/auth.py +194 -0
- isa_model/serving/api/dependencies/database.py +139 -0
- isa_model/serving/api/error_handlers.py +284 -0
- isa_model/serving/api/fastapi_server.py +172 -22
- isa_model/serving/api/middleware/auth.py +8 -2
- isa_model/serving/api/middleware/security.py +23 -33
- isa_model/serving/api/middleware/tenant_context.py +414 -0
- isa_model/serving/api/routes/analytics.py +4 -1
- isa_model/serving/api/routes/config.py +645 -0
- isa_model/serving/api/routes/deployment_billing.py +315 -0
- isa_model/serving/api/routes/deployments.py +138 -2
- isa_model/serving/api/routes/gpu_gateway.py +440 -0
- isa_model/serving/api/routes/health.py +32 -12
- isa_model/serving/api/routes/inference_monitoring.py +486 -0
- isa_model/serving/api/routes/local_deployments.py +448 -0
- isa_model/serving/api/routes/tenants.py +575 -0
- isa_model/serving/api/routes/unified.py +680 -18
- isa_model/serving/api/routes/webhooks.py +479 -0
- isa_model/serving/api/startup.py +68 -54
- isa_model/utils/gpu_utils.py +311 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.3.dist-info}/METADATA +66 -24
- isa_model-0.4.3.dist-info/RECORD +193 -0
- isa_model/core/storage/minio_storage.py +0 -0
- isa_model/deployment/cloud/__init__.py +0 -9
- isa_model/deployment/cloud/modal/__init__.py +0 -10
- isa_model/deployment/core/deployment_config.py +0 -356
- isa_model/deployment/core/isa_deployment_service.py +0 -401
- isa_model/deployment/gpu_int8_ds8/app/server.py +0 -66
- isa_model/deployment/gpu_int8_ds8/scripts/test_client.py +0 -43
- isa_model/deployment/gpu_int8_ds8/scripts/test_client_os.py +0 -35
- isa_model/deployment/runtime/deployed_service.py +0 -338
- isa_model/deployment/services/__init__.py +0 -9
- isa_model/deployment/services/auto_deploy_vision_service.py +0 -538
- isa_model/deployment/services/model_service.py +0 -332
- isa_model/deployment/services/service_monitor.py +0 -356
- isa_model/deployment/services/service_registry.py +0 -527
- isa_model/eval/__init__.py +0 -92
- isa_model/eval/benchmarks/__init__.py +0 -27
- isa_model/eval/benchmarks/multimodal_datasets.py +0 -460
- isa_model/eval/benchmarks.py +0 -701
- isa_model/eval/config/__init__.py +0 -10
- isa_model/eval/config/evaluation_config.py +0 -108
- isa_model/eval/evaluators/__init__.py +0 -24
- isa_model/eval/evaluators/audio_evaluator.py +0 -727
- isa_model/eval/evaluators/base_evaluator.py +0 -503
- isa_model/eval/evaluators/embedding_evaluator.py +0 -742
- isa_model/eval/evaluators/llm_evaluator.py +0 -472
- isa_model/eval/evaluators/vision_evaluator.py +0 -564
- isa_model/eval/example_evaluation.py +0 -395
- isa_model/eval/factory.py +0 -798
- isa_model/eval/infrastructure/__init__.py +0 -24
- isa_model/eval/infrastructure/experiment_tracker.py +0 -466
- isa_model/eval/isa_benchmarks.py +0 -700
- isa_model/eval/isa_integration.py +0 -582
- isa_model/eval/metrics.py +0 -951
- isa_model/eval/tests/unit/test_basic.py +0 -396
- isa_model/serving/api/routes/evaluations.py +0 -579
- isa_model/training/__init__.py +0 -168
- isa_model/training/annotation/annotation_schema.py +0 -47
- isa_model/training/annotation/processors/annotation_processor.py +0 -126
- isa_model/training/annotation/storage/dataset_manager.py +0 -131
- isa_model/training/annotation/storage/dataset_schema.py +0 -44
- isa_model/training/annotation/tests/test_annotation_flow.py +0 -109
- isa_model/training/annotation/tests/test_minio copy.py +0 -113
- isa_model/training/annotation/tests/test_minio_upload.py +0 -43
- isa_model/training/annotation/views/annotation_controller.py +0 -158
- isa_model/training/cloud/__init__.py +0 -22
- isa_model/training/cloud/job_orchestrator.py +0 -402
- isa_model/training/cloud/runpod_trainer.py +0 -454
- isa_model/training/cloud/storage_manager.py +0 -482
- isa_model/training/core/__init__.py +0 -26
- isa_model/training/core/config.py +0 -181
- isa_model/training/core/dataset.py +0 -222
- isa_model/training/core/trainer.py +0 -720
- isa_model/training/core/utils.py +0 -213
- isa_model/training/examples/intelligent_training_example.py +0 -281
- isa_model/training/factory.py +0 -424
- isa_model/training/intelligent/__init__.py +0 -25
- isa_model/training/intelligent/decision_engine.py +0 -643
- isa_model/training/intelligent/intelligent_factory.py +0 -888
- isa_model/training/intelligent/knowledge_base.py +0 -751
- isa_model/training/intelligent/resource_optimizer.py +0 -839
- isa_model/training/intelligent/task_classifier.py +0 -576
- isa_model/training/storage/__init__.py +0 -24
- isa_model/training/storage/core_integration.py +0 -439
- isa_model/training/storage/training_repository.py +0 -552
- isa_model/training/storage/training_storage.py +0 -628
- isa_model-0.4.0.dist-info/RECORD +0 -182
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_chatTTS_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_fish_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_openvoice_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/audio}/isa_audio_service_v2.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/embedding}/isa_embed_rerank_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/video}/isa_video_hunyuan_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ocr_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_qwen25_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_table_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service.py +0 -0
- /isa_model/deployment/{cloud/modal → modal/services/vision}/isa_vision_ui_service_optimized.py +0 -0
- /isa_model/deployment/{services → modal/services/vision}/simple_auto_deploy_vision_service.py +0 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.3.dist-info}/WHEEL +0 -0
- {isa_model-0.4.0.dist-info → isa_model-0.4.3.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,7 @@ import logging
|
|
9
9
|
import base64
|
10
10
|
import io
|
11
11
|
import time
|
12
|
+
import asyncio
|
12
13
|
from typing import Dict, Any, List, Union, Optional, BinaryIO
|
13
14
|
from PIL import Image
|
14
15
|
|
@@ -36,7 +37,7 @@ class ISAVisionService(BaseVisionService):
|
|
36
37
|
def __init__(self,
|
37
38
|
modal_app_id: str = "ap-VlHUQoiPUdy9cgrHSfG7Fk",
|
38
39
|
modal_app_name: str = "isa-vision-ui-optimized",
|
39
|
-
timeout: int =
|
40
|
+
timeout: int = 60):
|
40
41
|
"""
|
41
42
|
初始化ISA Vision服务
|
42
43
|
|
@@ -77,6 +78,31 @@ class ISAVisionService(BaseVisionService):
|
|
77
78
|
self.request_count = 0
|
78
79
|
self.total_cost = 0.0
|
79
80
|
|
81
|
+
# 性能优化 - 预热连接(延迟初始化)
|
82
|
+
self._connection_warmed = False
|
83
|
+
|
84
|
+
# 简单缓存机制(可选)
|
85
|
+
self._result_cache = {}
|
86
|
+
self._cache_max_size = 100
|
87
|
+
|
88
|
+
async def _warm_connection(self):
|
89
|
+
"""预热Modal连接,减少首次调用延迟"""
|
90
|
+
if self._connection_warmed or not self.modal_app:
|
91
|
+
return
|
92
|
+
|
93
|
+
try:
|
94
|
+
logger.info("Warming up Modal connection...")
|
95
|
+
# 尝试获取服务状态来预热连接
|
96
|
+
if hasattr(self.modal_app, 'list_functions'):
|
97
|
+
await asyncio.wait_for(
|
98
|
+
asyncio.to_thread(self.modal_app.list_functions),
|
99
|
+
timeout=10
|
100
|
+
)
|
101
|
+
self._connection_warmed = True
|
102
|
+
logger.info("✅ Modal connection warmed up")
|
103
|
+
except Exception as e:
|
104
|
+
logger.warning(f"Failed to warm up connection: {e}")
|
105
|
+
|
80
106
|
async def analyze_image(
|
81
107
|
self,
|
82
108
|
image: Union[str, BinaryIO],
|
@@ -154,6 +180,9 @@ class ISAVisionService(BaseVisionService):
|
|
154
180
|
'error': 'Modal app or service not available'
|
155
181
|
}
|
156
182
|
|
183
|
+
# 预热连接以减少延迟
|
184
|
+
await self._warm_connection()
|
185
|
+
|
157
186
|
# 准备图像数据
|
158
187
|
image_b64 = await self._prepare_image_base64(image)
|
159
188
|
|
@@ -208,11 +237,22 @@ class ISAVisionService(BaseVisionService):
|
|
208
237
|
|
209
238
|
# 创建实例并调用优化方法(快速模式,无字幕)
|
210
239
|
instance = OptimizedUIDetectionService()
|
211
|
-
|
240
|
+
# 使用超时控制Modal调用
|
241
|
+
result = await asyncio.wait_for(
|
242
|
+
instance.detect_ui_elements_fast.remote(image_b64, enable_captions=False),
|
243
|
+
timeout=self.timeout
|
244
|
+
)
|
212
245
|
|
213
246
|
logger.info("✅ Modal SDK call successful")
|
214
247
|
return result
|
215
248
|
|
249
|
+
except asyncio.TimeoutError:
|
250
|
+
logger.error(f"Modal SDK call timed out after {self.timeout} seconds")
|
251
|
+
return {
|
252
|
+
'success': False,
|
253
|
+
'error': f'Modal service timeout after {self.timeout} seconds',
|
254
|
+
'timeout': True
|
255
|
+
}
|
216
256
|
except Exception as e:
|
217
257
|
logger.error(f"Modal SDK call failed: {e}")
|
218
258
|
return {
|
@@ -316,11 +356,22 @@ class ISAVisionService(BaseVisionService):
|
|
316
356
|
|
317
357
|
# 创建实例并调用方法
|
318
358
|
instance = SuryaOCRService()
|
319
|
-
|
359
|
+
# 使用超时控制OCR调用
|
360
|
+
result = await asyncio.wait_for(
|
361
|
+
instance.extract_text.remote(image_b64, languages),
|
362
|
+
timeout=self.timeout
|
363
|
+
)
|
320
364
|
|
321
365
|
logger.info("✅ OCR service call successful")
|
322
366
|
return result
|
323
367
|
|
368
|
+
except asyncio.TimeoutError:
|
369
|
+
logger.error(f"OCR service call timed out after {self.timeout} seconds")
|
370
|
+
return {
|
371
|
+
'success': False,
|
372
|
+
'error': f'OCR service timeout after {self.timeout} seconds',
|
373
|
+
'timeout': True
|
374
|
+
}
|
324
375
|
except Exception as e:
|
325
376
|
logger.error(f"OCR service call failed: {e}")
|
326
377
|
return {
|
@@ -499,7 +550,7 @@ class ISAVisionService(BaseVisionService):
|
|
499
550
|
"""准备base64编码的图像"""
|
500
551
|
if isinstance(image, str):
|
501
552
|
# Check if it's already base64 encoded
|
502
|
-
if image.startswith('data:image') or len(image) > 1000:
|
553
|
+
if image.startswith('data:image') or (not image.startswith('http') and len(image) > 1000):
|
503
554
|
# Likely already base64
|
504
555
|
if image.startswith('data:image'):
|
505
556
|
# Extract base64 part
|
@@ -507,6 +558,16 @@ class ISAVisionService(BaseVisionService):
|
|
507
558
|
else:
|
508
559
|
# Assume it's pure base64
|
509
560
|
return image
|
561
|
+
elif image.startswith('http://') or image.startswith('https://'):
|
562
|
+
# URL - download the image
|
563
|
+
import aiohttp
|
564
|
+
async with aiohttp.ClientSession() as session:
|
565
|
+
async with session.get(image) as response:
|
566
|
+
if response.status == 200:
|
567
|
+
image_data = await response.read()
|
568
|
+
return base64.b64encode(image_data).decode('utf-8')
|
569
|
+
else:
|
570
|
+
raise ValueError(f"Failed to download image from URL: {response.status}")
|
510
571
|
else:
|
511
572
|
# File path
|
512
573
|
with open(image, 'rb') as f:
|
@@ -92,12 +92,21 @@ class OpenAIVisionService(BaseVisionService, VisionPromptMixin):
|
|
92
92
|
}
|
93
93
|
]
|
94
94
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
temperature
|
100
|
-
|
95
|
+
# Use max_completion_tokens for newer models like gpt-4o-mini
|
96
|
+
completion_params = {
|
97
|
+
"model": self.model_name,
|
98
|
+
"messages": messages, # type: ignore
|
99
|
+
"temperature": self.temperature
|
100
|
+
}
|
101
|
+
|
102
|
+
# Check if model uses new parameter name
|
103
|
+
# All newer models (gpt-4o, gpt-4.1, o1, etc.) use max_completion_tokens
|
104
|
+
if any(prefix in self.model_name for prefix in ["gpt-4o", "gpt-4.1", "o1"]):
|
105
|
+
completion_params["max_completion_tokens"] = max_tokens
|
106
|
+
else:
|
107
|
+
completion_params["max_tokens"] = max_tokens
|
108
|
+
|
109
|
+
response = await self._client.chat.completions.create(**completion_params) # type: ignore
|
101
110
|
|
102
111
|
# Track usage for billing
|
103
112
|
if response.usage:
|
@@ -162,7 +171,7 @@ class OpenAIVisionService(BaseVisionService, VisionPromptMixin):
|
|
162
171
|
图像描述 - 使用专门提示词
|
163
172
|
"""
|
164
173
|
prompt = self.get_task_prompt("describe", detail_level=detail_level)
|
165
|
-
return await self.analyze_image(image, prompt)
|
174
|
+
return await self.analyze_image(image, prompt, max_tokens=1000)
|
166
175
|
|
167
176
|
async def extract_text(self, image: Union[str, BinaryIO]) -> Dict[str, Any]:
|
168
177
|
"""
|
@@ -170,7 +179,7 @@ class OpenAIVisionService(BaseVisionService, VisionPromptMixin):
|
|
170
179
|
"""
|
171
180
|
prompt = self.get_task_prompt("extract_text")
|
172
181
|
|
173
|
-
return await self.analyze_image(image, prompt)
|
182
|
+
return await self.analyze_image(image, prompt, max_tokens=1000)
|
174
183
|
|
175
184
|
async def detect_objects(
|
176
185
|
self,
|
@@ -182,7 +191,7 @@ class OpenAIVisionService(BaseVisionService, VisionPromptMixin):
|
|
182
191
|
"""
|
183
192
|
prompt = self.get_task_prompt("detect_objects", confidence_threshold=confidence_threshold)
|
184
193
|
|
185
|
-
return await self.analyze_image(image, prompt)
|
194
|
+
return await self.analyze_image(image, prompt, max_tokens=1000)
|
186
195
|
|
187
196
|
async def detect_ui_elements(
|
188
197
|
self,
|
@@ -195,7 +204,7 @@ class OpenAIVisionService(BaseVisionService, VisionPromptMixin):
|
|
195
204
|
"""
|
196
205
|
prompt = self.get_task_prompt("detect_ui_elements", element_types=element_types, confidence_threshold=confidence_threshold)
|
197
206
|
|
198
|
-
return await self.analyze_image(image, prompt)
|
207
|
+
return await self.analyze_image(image, prompt, max_tokens=1000)
|
199
208
|
|
200
209
|
async def detect_document_elements(
|
201
210
|
self,
|
@@ -0,0 +1,257 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
VGG16 Vision Service
|
4
|
+
Computer vision service using VGG16 for image classification
|
5
|
+
Based on the aircraft damage detection notebook implementation
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import numpy as np
|
10
|
+
from typing import Dict, List, Any, Optional, Union, BinaryIO
|
11
|
+
import logging
|
12
|
+
from PIL import Image
|
13
|
+
import io
|
14
|
+
|
15
|
+
from .base_vision_service import BaseVisionService
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
def _lazy_import_vgg16_deps():
|
20
|
+
"""Lazy import VGG16 dependencies"""
|
21
|
+
try:
|
22
|
+
import tensorflow as tf
|
23
|
+
from tensorflow.keras.applications import VGG16
|
24
|
+
from tensorflow.keras.layers import Dense, Dropout, Flatten
|
25
|
+
from tensorflow.keras.models import Sequential, Model
|
26
|
+
from tensorflow.keras.optimizers import Adam
|
27
|
+
from tensorflow.keras.preprocessing.image import ImageDataGenerator
|
28
|
+
|
29
|
+
return {
|
30
|
+
'tf': tf,
|
31
|
+
'VGG16': VGG16,
|
32
|
+
'Dense': Dense,
|
33
|
+
'Dropout': Dropout,
|
34
|
+
'Flatten': Flatten,
|
35
|
+
'Sequential': Sequential,
|
36
|
+
'Model': Model,
|
37
|
+
'Adam': Adam,
|
38
|
+
'ImageDataGenerator': ImageDataGenerator,
|
39
|
+
'available': True
|
40
|
+
}
|
41
|
+
except ImportError as e:
|
42
|
+
logger.warning(f"VGG16 dependencies not available: {e}")
|
43
|
+
return {'available': False}
|
44
|
+
|
45
|
+
class VGG16VisionService(BaseVisionService):
|
46
|
+
"""
|
47
|
+
VGG16-based vision service for image classification
|
48
|
+
Provides an alternative implementation to VLM-based classification
|
49
|
+
"""
|
50
|
+
|
51
|
+
def __init__(self, model_path: Optional[str] = None, class_names: Optional[List[str]] = None):
|
52
|
+
"""
|
53
|
+
Initialize VGG16 vision service
|
54
|
+
|
55
|
+
Args:
|
56
|
+
model_path: Path to trained VGG16 model
|
57
|
+
class_names: List of class names for classification
|
58
|
+
"""
|
59
|
+
super().__init__()
|
60
|
+
|
61
|
+
self.model_path = model_path
|
62
|
+
self.class_names = class_names or ["class_0", "class_1"]
|
63
|
+
self.model = None
|
64
|
+
self.input_shape = (224, 224, 3)
|
65
|
+
|
66
|
+
# Lazy load dependencies
|
67
|
+
self.vgg16_components = _lazy_import_vgg16_deps()
|
68
|
+
|
69
|
+
if not self.vgg16_components['available']:
|
70
|
+
raise ImportError("TensorFlow and VGG16 dependencies are required")
|
71
|
+
|
72
|
+
# Load model if path provided
|
73
|
+
if model_path and os.path.exists(model_path):
|
74
|
+
self._load_model(model_path)
|
75
|
+
|
76
|
+
def _load_model(self, model_path: str):
|
77
|
+
"""Load trained VGG16 model"""
|
78
|
+
try:
|
79
|
+
tf = self.vgg16_components['tf']
|
80
|
+
self.model = tf.keras.models.load_model(model_path)
|
81
|
+
logger.info(f"VGG16 model loaded from {model_path}")
|
82
|
+
except Exception as e:
|
83
|
+
logger.error(f"Error loading VGG16 model: {e}")
|
84
|
+
raise
|
85
|
+
|
86
|
+
def _preprocess_image(self, image: Union[str, BinaryIO]) -> np.ndarray:
|
87
|
+
"""
|
88
|
+
Preprocess image for VGG16 input
|
89
|
+
|
90
|
+
Args:
|
91
|
+
image: Image path or binary data
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Preprocessed image array
|
95
|
+
"""
|
96
|
+
try:
|
97
|
+
# Handle different image input types
|
98
|
+
if isinstance(image, str):
|
99
|
+
# File path
|
100
|
+
pil_image = Image.open(image).convert('RGB')
|
101
|
+
elif hasattr(image, 'read'):
|
102
|
+
# Binary IO
|
103
|
+
image_data = image.read()
|
104
|
+
pil_image = Image.open(io.BytesIO(image_data)).convert('RGB')
|
105
|
+
else:
|
106
|
+
raise ValueError("Unsupported image format")
|
107
|
+
|
108
|
+
# Resize to VGG16 input size
|
109
|
+
pil_image = pil_image.resize((self.input_shape[0], self.input_shape[1]))
|
110
|
+
|
111
|
+
# Convert to array and normalize
|
112
|
+
image_array = np.array(pil_image) / 255.0
|
113
|
+
|
114
|
+
# Add batch dimension
|
115
|
+
image_batch = np.expand_dims(image_array, axis=0)
|
116
|
+
|
117
|
+
return image_batch, image_array
|
118
|
+
|
119
|
+
except Exception as e:
|
120
|
+
logger.error(f"Error preprocessing image: {e}")
|
121
|
+
raise
|
122
|
+
|
123
|
+
async def classify_image(self,
|
124
|
+
image: Union[str, BinaryIO],
|
125
|
+
categories: Optional[List[str]] = None) -> Dict[str, Any]:
|
126
|
+
"""
|
127
|
+
Classify image using trained VGG16 model
|
128
|
+
|
129
|
+
Args:
|
130
|
+
image: Image path or binary data
|
131
|
+
categories: Optional list of categories (uses model's classes if None)
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
Classification results
|
135
|
+
"""
|
136
|
+
try:
|
137
|
+
if self.model is None:
|
138
|
+
return {
|
139
|
+
"error": "No trained model available. Please load a model first.",
|
140
|
+
"service": "VGG16VisionService"
|
141
|
+
}
|
142
|
+
|
143
|
+
# Preprocess image
|
144
|
+
image_batch, image_array = self._preprocess_image(image)
|
145
|
+
|
146
|
+
# Make prediction
|
147
|
+
predictions = self.model.predict(image_batch, verbose=0)
|
148
|
+
|
149
|
+
# Use provided categories or default class names
|
150
|
+
class_names = categories or self.class_names
|
151
|
+
|
152
|
+
# Process predictions based on model output
|
153
|
+
if len(predictions[0]) == 1: # Binary classification
|
154
|
+
predicted_class_idx = int(predictions[0] > 0.5)
|
155
|
+
confidence = float(predictions[0][0]) if predicted_class_idx == 1 else float(1 - predictions[0][0])
|
156
|
+
|
157
|
+
# Create probability distribution
|
158
|
+
probabilities = {
|
159
|
+
class_names[0]: float(1 - predictions[0][0]),
|
160
|
+
class_names[1]: float(predictions[0][0])
|
161
|
+
}
|
162
|
+
else: # Multiclass classification
|
163
|
+
predicted_class_idx = np.argmax(predictions[0])
|
164
|
+
confidence = float(predictions[0][predicted_class_idx])
|
165
|
+
|
166
|
+
# Create probability distribution
|
167
|
+
probabilities = {
|
168
|
+
class_names[i]: float(predictions[0][i])
|
169
|
+
for i in range(min(len(class_names), len(predictions[0])))
|
170
|
+
}
|
171
|
+
|
172
|
+
predicted_class = class_names[predicted_class_idx] if predicted_class_idx < len(class_names) else f"class_{predicted_class_idx}"
|
173
|
+
|
174
|
+
return {
|
175
|
+
"task": "classify",
|
176
|
+
"service": "VGG16VisionService",
|
177
|
+
"predicted_class": predicted_class,
|
178
|
+
"confidence": confidence,
|
179
|
+
"probabilities": probabilities,
|
180
|
+
"model_type": "VGG16",
|
181
|
+
"success": True
|
182
|
+
}
|
183
|
+
|
184
|
+
except Exception as e:
|
185
|
+
logger.error(f"Error classifying image: {e}")
|
186
|
+
return {
|
187
|
+
"error": str(e),
|
188
|
+
"service": "VGG16VisionService",
|
189
|
+
"success": False
|
190
|
+
}
|
191
|
+
|
192
|
+
async def analyze_image(self,
|
193
|
+
image: Union[str, BinaryIO],
|
194
|
+
prompt: Optional[str] = None,
|
195
|
+
max_tokens: int = 1000) -> Dict[str, Any]:
|
196
|
+
"""
|
197
|
+
Analyze image using VGG16 classification
|
198
|
+
|
199
|
+
Args:
|
200
|
+
image: Image path or binary data
|
201
|
+
prompt: Optional prompt (used to guide interpretation)
|
202
|
+
max_tokens: Not used for classification
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Analysis results
|
206
|
+
"""
|
207
|
+
# For VGG16, analysis is essentially classification
|
208
|
+
classification_result = await self.classify_image(image)
|
209
|
+
|
210
|
+
if classification_result.get("success"):
|
211
|
+
# Create analysis text based on classification
|
212
|
+
predicted_class = classification_result["predicted_class"]
|
213
|
+
confidence = classification_result["confidence"]
|
214
|
+
|
215
|
+
analysis_text = f"The image has been classified as '{predicted_class}' with {confidence:.2%} confidence."
|
216
|
+
|
217
|
+
if prompt:
|
218
|
+
analysis_text += f" Analysis context: {prompt}"
|
219
|
+
|
220
|
+
return {
|
221
|
+
"task": "analyze",
|
222
|
+
"service": "VGG16VisionService",
|
223
|
+
"text": analysis_text,
|
224
|
+
"confidence": confidence,
|
225
|
+
"classification": classification_result,
|
226
|
+
"success": True
|
227
|
+
}
|
228
|
+
else:
|
229
|
+
return classification_result
|
230
|
+
|
231
|
+
def set_class_names(self, class_names: List[str]):
|
232
|
+
"""Set class names for classification"""
|
233
|
+
self.class_names = class_names
|
234
|
+
|
235
|
+
def load_trained_model(self, model_path: str, class_names: Optional[List[str]] = None):
|
236
|
+
"""
|
237
|
+
Load a trained VGG16 model
|
238
|
+
|
239
|
+
Args:
|
240
|
+
model_path: Path to the trained model
|
241
|
+
class_names: Optional class names
|
242
|
+
"""
|
243
|
+
self._load_model(model_path)
|
244
|
+
if class_names:
|
245
|
+
self.set_class_names(class_names)
|
246
|
+
|
247
|
+
def get_service_info(self) -> Dict[str, Any]:
|
248
|
+
"""Get service information"""
|
249
|
+
return {
|
250
|
+
"service_name": "VGG16VisionService",
|
251
|
+
"model_type": "VGG16",
|
252
|
+
"capabilities": ["classify", "analyze"],
|
253
|
+
"model_loaded": self.model is not None,
|
254
|
+
"input_shape": self.input_shape,
|
255
|
+
"class_names": self.class_names,
|
256
|
+
"dependencies_available": self.vgg16_components['available']
|
257
|
+
}
|
@@ -0,0 +1,245 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
|
4
|
+
"""
|
5
|
+
Simple Cache Manager for ISA Model API
|
6
|
+
Provides in-memory caching to improve API performance
|
7
|
+
"""
|
8
|
+
|
9
|
+
import time
|
10
|
+
import logging
|
11
|
+
from typing import Dict, Any, Optional, Callable
|
12
|
+
from dataclasses import dataclass
|
13
|
+
from threading import RLock
|
14
|
+
import asyncio
|
15
|
+
import hashlib
|
16
|
+
import json
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class CacheEntry:
|
22
|
+
"""Cache entry with data and metadata"""
|
23
|
+
data: Any
|
24
|
+
created_at: float
|
25
|
+
ttl: float
|
26
|
+
access_count: int = 0
|
27
|
+
last_accessed: float = None
|
28
|
+
|
29
|
+
def is_expired(self) -> bool:
|
30
|
+
"""Check if cache entry is expired"""
|
31
|
+
return time.time() - self.created_at > self.ttl
|
32
|
+
|
33
|
+
def access(self) -> Any:
|
34
|
+
"""Mark as accessed and return data"""
|
35
|
+
self.access_count += 1
|
36
|
+
self.last_accessed = time.time()
|
37
|
+
return self.data
|
38
|
+
|
39
|
+
class APICache:
|
40
|
+
"""
|
41
|
+
Simple in-memory cache for API responses
|
42
|
+
Thread-safe with automatic expiration
|
43
|
+
"""
|
44
|
+
|
45
|
+
def __init__(self, default_ttl: float = 300.0, max_size: int = 1000):
|
46
|
+
self.default_ttl = default_ttl # 5 minutes default
|
47
|
+
self.max_size = max_size
|
48
|
+
self._cache: Dict[str, CacheEntry] = {}
|
49
|
+
self._lock = RLock()
|
50
|
+
self._stats = {
|
51
|
+
"hits": 0,
|
52
|
+
"misses": 0,
|
53
|
+
"evictions": 0,
|
54
|
+
"total_requests": 0
|
55
|
+
}
|
56
|
+
|
57
|
+
def _generate_key(self, *args, **kwargs) -> str:
|
58
|
+
"""Generate cache key from arguments"""
|
59
|
+
# Create a stable key from arguments
|
60
|
+
key_data = {
|
61
|
+
"args": args,
|
62
|
+
"kwargs": sorted(kwargs.items()) if kwargs else {}
|
63
|
+
}
|
64
|
+
key_string = json.dumps(key_data, sort_keys=True, default=str)
|
65
|
+
return hashlib.md5(key_string.encode()).hexdigest()
|
66
|
+
|
67
|
+
def _cleanup_expired(self):
|
68
|
+
"""Remove expired entries"""
|
69
|
+
with self._lock:
|
70
|
+
current_time = time.time()
|
71
|
+
expired_keys = [
|
72
|
+
key for key, entry in self._cache.items()
|
73
|
+
if entry.is_expired()
|
74
|
+
]
|
75
|
+
|
76
|
+
for key in expired_keys:
|
77
|
+
del self._cache[key]
|
78
|
+
|
79
|
+
if expired_keys:
|
80
|
+
logger.debug(f"Cleaned up {len(expired_keys)} expired cache entries")
|
81
|
+
|
82
|
+
def _evict_lru(self):
|
83
|
+
"""Evict least recently used entries when cache is full"""
|
84
|
+
with self._lock:
|
85
|
+
if len(self._cache) >= self.max_size:
|
86
|
+
# Sort by last_accessed time (LRU)
|
87
|
+
sorted_entries = sorted(
|
88
|
+
self._cache.items(),
|
89
|
+
key=lambda x: x[1].last_accessed or x[1].created_at
|
90
|
+
)
|
91
|
+
|
92
|
+
# Remove oldest 20% of entries
|
93
|
+
num_to_remove = max(1, len(sorted_entries) // 5)
|
94
|
+
for key, _ in sorted_entries[:num_to_remove]:
|
95
|
+
del self._cache[key]
|
96
|
+
self._stats["evictions"] += 1
|
97
|
+
|
98
|
+
logger.debug(f"Evicted {num_to_remove} LRU cache entries")
|
99
|
+
|
100
|
+
def get(self, key: str) -> Optional[Any]:
|
101
|
+
"""Get cached value by key"""
|
102
|
+
with self._lock:
|
103
|
+
self._stats["total_requests"] += 1
|
104
|
+
|
105
|
+
if key in self._cache:
|
106
|
+
entry = self._cache[key]
|
107
|
+
if not entry.is_expired():
|
108
|
+
self._stats["hits"] += 1
|
109
|
+
return entry.access()
|
110
|
+
else:
|
111
|
+
# Remove expired entry
|
112
|
+
del self._cache[key]
|
113
|
+
|
114
|
+
self._stats["misses"] += 1
|
115
|
+
return None
|
116
|
+
|
117
|
+
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
118
|
+
"""Set cached value with optional TTL"""
|
119
|
+
with self._lock:
|
120
|
+
# Cleanup and eviction
|
121
|
+
self._cleanup_expired()
|
122
|
+
self._evict_lru()
|
123
|
+
|
124
|
+
entry = CacheEntry(
|
125
|
+
data=value,
|
126
|
+
created_at=time.time(),
|
127
|
+
ttl=ttl or self.default_ttl,
|
128
|
+
last_accessed=time.time()
|
129
|
+
)
|
130
|
+
|
131
|
+
self._cache[key] = entry
|
132
|
+
|
133
|
+
def delete(self, key: str) -> bool:
|
134
|
+
"""Delete cached value"""
|
135
|
+
with self._lock:
|
136
|
+
if key in self._cache:
|
137
|
+
del self._cache[key]
|
138
|
+
return True
|
139
|
+
return False
|
140
|
+
|
141
|
+
def clear(self) -> None:
|
142
|
+
"""Clear all cached values"""
|
143
|
+
with self._lock:
|
144
|
+
self._cache.clear()
|
145
|
+
logger.info("Cache cleared")
|
146
|
+
|
147
|
+
def get_stats(self) -> Dict[str, Any]:
|
148
|
+
"""Get cache statistics"""
|
149
|
+
with self._lock:
|
150
|
+
hit_rate = (
|
151
|
+
self._stats["hits"] / self._stats["total_requests"]
|
152
|
+
if self._stats["total_requests"] > 0 else 0
|
153
|
+
)
|
154
|
+
|
155
|
+
return {
|
156
|
+
"cache_size": len(self._cache),
|
157
|
+
"max_size": self.max_size,
|
158
|
+
"default_ttl": self.default_ttl,
|
159
|
+
"hit_rate": round(hit_rate * 100, 2),
|
160
|
+
**self._stats
|
161
|
+
}
|
162
|
+
|
163
|
+
# Decorator for caching function results
|
164
|
+
def cached(ttl: float = 300.0, cache_key_func: Optional[Callable] = None):
|
165
|
+
"""
|
166
|
+
Decorator to cache function results
|
167
|
+
|
168
|
+
Args:
|
169
|
+
ttl: Time to live in seconds
|
170
|
+
cache_key_func: Custom function to generate cache key
|
171
|
+
"""
|
172
|
+
def decorator(func):
|
173
|
+
async def async_wrapper(*args, **kwargs):
|
174
|
+
# Generate cache key
|
175
|
+
if cache_key_func:
|
176
|
+
cache_key = cache_key_func(*args, **kwargs)
|
177
|
+
else:
|
178
|
+
cache_key = api_cache._generate_key(func.__name__, *args, **kwargs)
|
179
|
+
|
180
|
+
# Try to get from cache
|
181
|
+
cached_result = api_cache.get(cache_key)
|
182
|
+
if cached_result is not None:
|
183
|
+
logger.debug(f"Cache hit for {func.__name__}")
|
184
|
+
return cached_result
|
185
|
+
|
186
|
+
# Execute function and cache result
|
187
|
+
try:
|
188
|
+
result = await func(*args, **kwargs)
|
189
|
+
api_cache.set(cache_key, result, ttl)
|
190
|
+
logger.debug(f"Cached result for {func.__name__}")
|
191
|
+
return result
|
192
|
+
except Exception as e:
|
193
|
+
logger.error(f"Function {func.__name__} failed: {e}")
|
194
|
+
raise
|
195
|
+
|
196
|
+
def sync_wrapper(*args, **kwargs):
|
197
|
+
# Generate cache key
|
198
|
+
if cache_key_func:
|
199
|
+
cache_key = cache_key_func(*args, **kwargs)
|
200
|
+
else:
|
201
|
+
cache_key = api_cache._generate_key(func.__name__, *args, **kwargs)
|
202
|
+
|
203
|
+
# Try to get from cache
|
204
|
+
cached_result = api_cache.get(cache_key)
|
205
|
+
if cached_result is not None:
|
206
|
+
logger.debug(f"Cache hit for {func.__name__}")
|
207
|
+
return cached_result
|
208
|
+
|
209
|
+
# Execute function and cache result
|
210
|
+
try:
|
211
|
+
result = func(*args, **kwargs)
|
212
|
+
api_cache.set(cache_key, result, ttl)
|
213
|
+
logger.debug(f"Cached result for {func.__name__}")
|
214
|
+
return result
|
215
|
+
except Exception as e:
|
216
|
+
logger.error(f"Function {func.__name__} failed: {e}")
|
217
|
+
raise
|
218
|
+
|
219
|
+
# Return appropriate wrapper based on function type
|
220
|
+
if asyncio.iscoroutinefunction(func):
|
221
|
+
return async_wrapper
|
222
|
+
else:
|
223
|
+
return sync_wrapper
|
224
|
+
|
225
|
+
return decorator
|
226
|
+
|
227
|
+
# Global cache instance
|
228
|
+
api_cache = APICache(default_ttl=300.0, max_size=1000)
|
229
|
+
|
230
|
+
def get_api_cache() -> APICache:
|
231
|
+
"""Get the global API cache instance"""
|
232
|
+
return api_cache
|
233
|
+
|
234
|
+
# Cache key generators for common patterns
|
235
|
+
def model_list_cache_key(service_type=None):
|
236
|
+
"""Generate cache key for model list API"""
|
237
|
+
return f"models_list_{service_type or 'all'}"
|
238
|
+
|
239
|
+
def provider_list_cache_key():
|
240
|
+
"""Generate cache key for provider list API"""
|
241
|
+
return "providers_list"
|
242
|
+
|
243
|
+
def custom_models_cache_key(model_type=None, provider=None):
|
244
|
+
"""Generate cache key for custom models API"""
|
245
|
+
return f"custom_models_{model_type or 'all'}_{provider or 'all'}"
|