nedo-vision-worker-core 0.3.4__tar.gz → 0.3.5__tar.gz
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.
Potentially problematic release.
This version of nedo-vision-worker-core might be problematic. Click here for more details.
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/PKG-INFO +1 -1
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/__init__.py +1 -1
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelineManager.py +13 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelineProcessor.py +3 -2
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelineSyncThread.py +17 -20
- nedo_vision_worker_core-0.3.5/nedo_vision_worker_core/streams/StreamSyncThread.py +107 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/VideoStreamManager.py +76 -20
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/PKG-INFO +1 -1
- nedo_vision_worker_core-0.3.4/nedo_vision_worker_core/streams/StreamSyncThread.py +0 -80
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/MANIFEST.in +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/README.md +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/ai/FrameDrawer.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/ai/ImageDebugger.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/ai/VideoDebugger.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/ai/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/callbacks/DetectionCallbackManager.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/callbacks/DetectionCallbackTypes.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/callbacks/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/cli.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/config/ConfigurationManager.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/config/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/core_service.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/database/DatabaseManager.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/database/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/BaseDetector.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/RFDETRDetector.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/YOLODetector.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/detection_processing/DetectionProcessor.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/detection_processing/HumanDetectionProcessor.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/detection_processing/PPEDetectionProcessor.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/detection/detection_processing/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/doctor.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/inner_corner.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/inner_frame.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/line.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/top_left.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/blue/top_right.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/inner_corner.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/inner_frame.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/line.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/top_left.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/drawing_assets/red/top_right.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/boots-green.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/boots-red.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/gloves-green.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/gloves-red.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/goggles-green.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/goggles-red.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/helmet-green.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/helmet-red.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/mask-red.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/vest-green.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/icons/vest-red.png +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/ai_model.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/auth.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/config.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/dataset_source.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/logs.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/ppe_detection.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/ppe_detection_label.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/restricted_area_violation.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/user.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source_pipeline.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source_pipeline_config.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source_pipeline_debug.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/models/worker_source_pipeline_detection.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/ModelManager.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelineConfigManager.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/PipelinePrepocessor.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/pipeline/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/preprocessing/ImageResizer.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/preprocessing/ImageRoi.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/preprocessing/Preprocessor.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/preprocessing/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/AIModelRepository.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/PPEDetectionRepository.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/RestrictedAreaRepository.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/WorkerSourcePipelineDebugRepository.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/WorkerSourcePipelineDetectionRepository.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/WorkerSourcePipelineRepository.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/WorkerSourceRepository.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/repositories/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/services/SharedVideoStreamServer.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/services/VideoSharingDaemon.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/services/VideoSharingDaemonManager.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/RTMPStreamer.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/SharedVideoDeviceManager.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/VideoStream.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/streams/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/tracker/SFSORT.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/tracker/TrackerManager.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/tracker/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/BoundingBoxMetrics.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/DrawingUtils.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/ModelReadinessChecker.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/PersonAttributeMatcher.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/PersonRestrictedAreaMatcher.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/PlatformDetector.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/TablePrinter.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/util/__init__.py +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/SOURCES.txt +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/dependency_links.txt +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/entry_points.txt +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/requires.txt +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core.egg-info/top_level.txt +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/pyproject.toml +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/requirements.txt +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/setup.cfg +0 -0
- {nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nedo-vision-worker-core
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Nedo Vision Worker Core Library for AI Vision Processing
|
|
5
5
|
Author-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
|
|
6
6
|
Maintainer-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
|
{nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/__init__.py
RENAMED
|
@@ -7,7 +7,7 @@ A library for running AI vision processing and detection in the Nedo Vision plat
|
|
|
7
7
|
from .core_service import CoreService
|
|
8
8
|
from .callbacks import DetectionType, CallbackTrigger, DetectionData, IntervalMetadata
|
|
9
9
|
|
|
10
|
-
__version__ = "0.3.
|
|
10
|
+
__version__ = "0.3.5"
|
|
11
11
|
__all__ = [
|
|
12
12
|
"CoreService",
|
|
13
13
|
"DetectionType",
|
|
@@ -39,6 +39,11 @@ class PipelineManager:
|
|
|
39
39
|
|
|
40
40
|
logging.info(f"🚀 Starting Pipeline processing for pipeline: {pipeline_id} | Source: {worker_source_id} ({pipeline.name})")
|
|
41
41
|
|
|
42
|
+
# Acquire the video stream (starts it if not already running)
|
|
43
|
+
if not self.video_manager.acquire_stream(worker_source_id, pipeline_id):
|
|
44
|
+
logging.error(f"❌ Failed to acquire stream {worker_source_id} for pipeline {pipeline_id}")
|
|
45
|
+
return
|
|
46
|
+
|
|
42
47
|
processor = PipelineProcessor(pipeline, detector, False)
|
|
43
48
|
processor.frame_drawer.location_name = pipeline.location_name
|
|
44
49
|
self.processors[pipeline_id] = processor # Store processor instance
|
|
@@ -79,6 +84,10 @@ class PipelineManager:
|
|
|
79
84
|
self._stopping_pipelines.add(pipeline_id)
|
|
80
85
|
|
|
81
86
|
try:
|
|
87
|
+
# Get worker_source_id before removing metadata
|
|
88
|
+
pipeline = self.pipeline_metadata.get(pipeline_id)
|
|
89
|
+
worker_source_id = pipeline.worker_source_id if pipeline else None
|
|
90
|
+
|
|
82
91
|
# Stop AI processing
|
|
83
92
|
processor = self.processors.pop(pipeline_id, None)
|
|
84
93
|
if processor:
|
|
@@ -92,6 +101,10 @@ class PipelineManager:
|
|
|
92
101
|
# Remove metadata
|
|
93
102
|
self.pipeline_metadata.pop(pipeline_id, None)
|
|
94
103
|
|
|
104
|
+
# Release the video stream (stops it if no more pipelines use it)
|
|
105
|
+
if worker_source_id:
|
|
106
|
+
self.video_manager.release_stream(worker_source_id, pipeline_id)
|
|
107
|
+
|
|
95
108
|
logging.info(f"✅ Pipeline {pipeline_id} stopped successfully.")
|
|
96
109
|
|
|
97
110
|
except Exception as e:
|
|
@@ -459,9 +459,10 @@ class PipelineProcessor:
|
|
|
459
459
|
logging.error(f" Cannot get stream URL for {worker_source_id}")
|
|
460
460
|
return False
|
|
461
461
|
|
|
462
|
-
|
|
462
|
+
# Use internal methods to restart the stream without affecting reference counting
|
|
463
|
+
video_manager._stop_stream(worker_source_id)
|
|
463
464
|
time.sleep(1.0)
|
|
464
|
-
video_manager.
|
|
465
|
+
video_manager._start_stream(worker_source_id, stream_url)
|
|
465
466
|
time.sleep(2.0)
|
|
466
467
|
|
|
467
468
|
if not video_manager.has_stream(worker_source_id):
|
|
@@ -103,8 +103,22 @@ class PipelineSyncThread(threading.Thread):
|
|
|
103
103
|
|
|
104
104
|
for pid in pipeline_ids:
|
|
105
105
|
db_pipeline = db_pipelines[pid]
|
|
106
|
+
|
|
107
|
+
# Check if pipeline should be stopped (status changed to stop/restart in DB)
|
|
108
|
+
if db_pipeline.pipeline_status_code in ['stop', 'restart']:
|
|
109
|
+
if self.pipeline_manager.is_running(pid):
|
|
110
|
+
logging.info(f"⏹️ Stopping pipeline due to status change: {pid}")
|
|
111
|
+
self.pipeline_manager.stop_pipeline(pid)
|
|
112
|
+
continue
|
|
113
|
+
|
|
106
114
|
processor = self.pipeline_manager.processors.get(pid)
|
|
107
115
|
if not processor:
|
|
116
|
+
# Pipeline exists in both sets but processor doesn't exist - shouldn't happen
|
|
117
|
+
# but if it does, try to start it if status is 'run'
|
|
118
|
+
if db_pipeline.pipeline_status_code == 'run':
|
|
119
|
+
logging.warning(f"⚠️ Pipeline {pid} exists locally but has no processor. Restarting...")
|
|
120
|
+
detector = self.model_manager.get_detector(db_pipeline.ai_model_id)
|
|
121
|
+
self.pipeline_manager.start_pipeline(db_pipeline, detector)
|
|
108
122
|
continue
|
|
109
123
|
|
|
110
124
|
local_detector = processor.detector
|
|
@@ -114,29 +128,12 @@ class PipelineSyncThread(threading.Thread):
|
|
|
114
128
|
processor.enable_debug()
|
|
115
129
|
|
|
116
130
|
def update_pipeline(self, pid: str, db_pipeline: object, local_detector: object) -> None:
|
|
117
|
-
"""Updates a single pipeline if necessary."""
|
|
131
|
+
"""Updates a single pipeline if necessary (only called for running pipelines)."""
|
|
118
132
|
processor = self.pipeline_manager.processors.get(pid)
|
|
119
133
|
if not processor:
|
|
120
134
|
return
|
|
121
135
|
|
|
122
|
-
#
|
|
123
|
-
if db_pipeline.pipeline_status_code != processor._pipeline.pipeline_status_code:
|
|
124
|
-
if db_pipeline.pipeline_status_code == 'run':
|
|
125
|
-
logging.info(f"▶️ Resuming pipeline: {pid}")
|
|
126
|
-
self.pipeline_manager.start_pipeline(db_pipeline, self.model_manager.get_detector(db_pipeline.ai_model_id))
|
|
127
|
-
elif db_pipeline.pipeline_status_code in ['stop', 'restart']:
|
|
128
|
-
logging.info(f"⏹️ Stopping pipeline: {pid}")
|
|
129
|
-
self.pipeline_manager.stop_pipeline(pid)
|
|
130
|
-
if db_pipeline.pipeline_status_code == 'restart':
|
|
131
|
-
# This will be picked up by the 'add_new_pipelines' logic in the next cycle
|
|
132
|
-
return
|
|
133
|
-
else:
|
|
134
|
-
processor.update_config(db_pipeline) # Update config for non-running pipelines
|
|
135
|
-
return
|
|
136
|
-
elif db_pipeline.pipeline_status_code != 'run':
|
|
137
|
-
processor.update_config(db_pipeline)
|
|
138
|
-
return
|
|
139
|
-
|
|
136
|
+
# At this point, we know db_pipeline.pipeline_status_code == 'run' (checked in caller)
|
|
140
137
|
# Check for significant changes that require a restart
|
|
141
138
|
db_detector = self.model_manager.get_detector(db_pipeline.ai_model_id)
|
|
142
139
|
|
|
@@ -146,7 +143,7 @@ class PipelineSyncThread(threading.Thread):
|
|
|
146
143
|
local_detector != db_detector
|
|
147
144
|
])
|
|
148
145
|
|
|
149
|
-
if requires_restart
|
|
146
|
+
if requires_restart:
|
|
150
147
|
logging.info(f"🔄 Restarting pipeline due to significant changes: {pid}")
|
|
151
148
|
self.pipeline_manager.stop_pipeline(pid)
|
|
152
149
|
self.pipeline_manager.start_pipeline(db_pipeline, db_detector)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import threading
|
|
5
|
+
from ..database.DatabaseManager import DatabaseManager
|
|
6
|
+
from ..repositories.WorkerSourceRepository import WorkerSourceRepository
|
|
7
|
+
from .VideoStreamManager import VideoStreamManager
|
|
8
|
+
|
|
9
|
+
class StreamSyncThread(threading.Thread):
|
|
10
|
+
"""Thread responsible for synchronizing video streams from the database in real-time."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, manager: VideoStreamManager, polling_interval=5):
|
|
13
|
+
super().__init__() # Set as a daemon so it stops with the main process
|
|
14
|
+
|
|
15
|
+
self.source_file_path = DatabaseManager.STORAGE_PATHS["files"] / "source_files"
|
|
16
|
+
|
|
17
|
+
self.manager = manager
|
|
18
|
+
self.polling_interval = polling_interval
|
|
19
|
+
self.worker_source_repo = WorkerSourceRepository()
|
|
20
|
+
self.running = True
|
|
21
|
+
|
|
22
|
+
def _get_source_file_path(self, file):
|
|
23
|
+
"""Returns the file path for a given source file."""
|
|
24
|
+
return self.source_file_path / os.path.basename(file)
|
|
25
|
+
|
|
26
|
+
def run(self):
|
|
27
|
+
"""Continuously updates the VideoStreamManager with database changes."""
|
|
28
|
+
while self.running:
|
|
29
|
+
try:
|
|
30
|
+
sources = self.worker_source_repo.get_worker_sources()
|
|
31
|
+
db_sources = {
|
|
32
|
+
source.id:
|
|
33
|
+
source.url if source.type_code == "live"
|
|
34
|
+
else source.url if source.type_code == "direct"
|
|
35
|
+
else self._get_source_file_path(source.file_path)
|
|
36
|
+
for source in sources
|
|
37
|
+
} # Store latest sources
|
|
38
|
+
|
|
39
|
+
# Get both active streams and pending streams
|
|
40
|
+
active_stream_ids = set(self.manager.get_active_stream_ids())
|
|
41
|
+
with self.manager._lock:
|
|
42
|
+
pending_stream_ids = set(self.manager.pending_streams.keys())
|
|
43
|
+
registered_stream_ids = active_stream_ids | pending_stream_ids
|
|
44
|
+
|
|
45
|
+
# **1️⃣ Register new streams (lazy loading - don't start them yet)**
|
|
46
|
+
for source_id, url in db_sources.items():
|
|
47
|
+
if source_id not in registered_stream_ids:
|
|
48
|
+
logging.info(f"🟢 Registering new stream: {source_id} ({url})")
|
|
49
|
+
self.manager.register_stream(source_id, url)
|
|
50
|
+
|
|
51
|
+
# **2️⃣ Unregister deleted or disconnected streams**
|
|
52
|
+
for stream_id in registered_stream_ids:
|
|
53
|
+
if stream_id not in db_sources:
|
|
54
|
+
logging.info(f"🔴 Unregistering stream: {stream_id}")
|
|
55
|
+
self.manager.unregister_stream(stream_id)
|
|
56
|
+
|
|
57
|
+
# Refresh registered streams
|
|
58
|
+
with self.manager._lock:
|
|
59
|
+
pending_stream_ids = set(self.manager.pending_streams.keys())
|
|
60
|
+
registered_stream_ids = active_stream_ids | pending_stream_ids
|
|
61
|
+
|
|
62
|
+
# **3️⃣ Update streams if URL has changed**
|
|
63
|
+
for source_id, url in db_sources.items():
|
|
64
|
+
if source_id in registered_stream_ids:
|
|
65
|
+
# Check if it's an active stream or pending stream
|
|
66
|
+
with self.manager._lock:
|
|
67
|
+
is_pending = source_id in self.manager.pending_streams
|
|
68
|
+
if is_pending:
|
|
69
|
+
existing_url = self.manager.pending_streams.get(source_id)
|
|
70
|
+
else:
|
|
71
|
+
existing_url = None
|
|
72
|
+
|
|
73
|
+
if existing_url is None:
|
|
74
|
+
# It's an active stream, get URL from stream manager
|
|
75
|
+
existing_url = self.manager.get_stream_url(source_id)
|
|
76
|
+
|
|
77
|
+
# Only update if URL actually changed
|
|
78
|
+
if existing_url != url:
|
|
79
|
+
if is_pending:
|
|
80
|
+
# It's pending, just update the URL
|
|
81
|
+
with self.manager._lock:
|
|
82
|
+
self.manager.pending_streams[source_id] = url
|
|
83
|
+
logging.info(f"🟡 Updated pending stream {source_id} URL")
|
|
84
|
+
else:
|
|
85
|
+
# It's active, need to restart it
|
|
86
|
+
logging.info(f"🟡 Updating active stream {source_id}: New URL {url}")
|
|
87
|
+
# Unregister and re-register with new URL
|
|
88
|
+
self.manager.unregister_stream(source_id)
|
|
89
|
+
# Add a small delay for device cleanup
|
|
90
|
+
if self._is_direct_device(url) or self._is_direct_device(existing_url):
|
|
91
|
+
time.sleep(0.5) # Allow device to be properly released
|
|
92
|
+
self.manager.register_stream(source_id, url)
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logging.error(f"⚠️ Error syncing streams from database: {e}")
|
|
96
|
+
|
|
97
|
+
time.sleep(self.polling_interval) # Poll every X seconds
|
|
98
|
+
|
|
99
|
+
def _is_direct_device(self, url) -> bool:
|
|
100
|
+
"""Check if URL represents a direct video device."""
|
|
101
|
+
if isinstance(url, str):
|
|
102
|
+
return url.isdigit() or url.startswith('/dev/video')
|
|
103
|
+
return isinstance(url, int)
|
|
104
|
+
|
|
105
|
+
def stop(self):
|
|
106
|
+
"""Stops the synchronization thread."""
|
|
107
|
+
self.running = False
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import time
|
|
3
3
|
import threading
|
|
4
|
-
import cv2
|
|
5
4
|
from typing import Any, Dict, Optional
|
|
6
5
|
|
|
7
6
|
from .VideoStream import VideoStream
|
|
@@ -18,6 +17,11 @@ class VideoStreamManager:
|
|
|
18
17
|
self.direct_device_streams: Dict[Any, Dict[str, Any]] = {}
|
|
19
18
|
# Per-direct-device locks: {worker_source_id: threading.Lock}
|
|
20
19
|
self.direct_device_locks: Dict[Any, threading.Lock] = {}
|
|
20
|
+
|
|
21
|
+
# Reference counting for lazy loading: {worker_source_id: set of pipeline_ids}
|
|
22
|
+
self.stream_references: Dict[Any, set] = {}
|
|
23
|
+
# Store URLs for streams that aren't started yet: {worker_source_id: url}
|
|
24
|
+
self.pending_streams: Dict[Any, str] = {}
|
|
21
25
|
|
|
22
26
|
self.shared_device_manager = SharedVideoDeviceManager()
|
|
23
27
|
|
|
@@ -38,16 +42,71 @@ class VideoStreamManager:
|
|
|
38
42
|
# -----------------------
|
|
39
43
|
# Public API
|
|
40
44
|
# -----------------------
|
|
41
|
-
def
|
|
42
|
-
"""
|
|
45
|
+
def register_stream(self, worker_source_id, url):
|
|
46
|
+
"""Register a stream URL without starting it (lazy loading)."""
|
|
43
47
|
with self._lock:
|
|
48
|
+
if worker_source_id not in self.pending_streams:
|
|
49
|
+
self.pending_streams[worker_source_id] = url
|
|
50
|
+
logging.debug(f"📝 Registered stream {worker_source_id} for lazy loading")
|
|
51
|
+
|
|
52
|
+
def unregister_stream(self, worker_source_id):
|
|
53
|
+
"""Unregister a stream that's no longer available in the database."""
|
|
54
|
+
with self._lock:
|
|
55
|
+
# If it's pending, remove it
|
|
56
|
+
if worker_source_id in self.pending_streams:
|
|
57
|
+
del self.pending_streams[worker_source_id]
|
|
58
|
+
logging.debug(f"🗑️ Unregistered pending stream {worker_source_id}")
|
|
59
|
+
|
|
60
|
+
# If it's active and has no references, remove it
|
|
61
|
+
if worker_source_id in self.stream_references:
|
|
62
|
+
if len(self.stream_references[worker_source_id]) == 0:
|
|
63
|
+
self._stop_stream(worker_source_id)
|
|
64
|
+
|
|
65
|
+
def acquire_stream(self, worker_source_id, pipeline_id):
|
|
66
|
+
"""Request access to a stream for a pipeline. Starts the stream if not already running."""
|
|
67
|
+
with self._lock:
|
|
68
|
+
# Initialize reference set if needed
|
|
69
|
+
if worker_source_id not in self.stream_references:
|
|
70
|
+
self.stream_references[worker_source_id] = set()
|
|
71
|
+
|
|
72
|
+
# Add pipeline reference
|
|
73
|
+
self.stream_references[worker_source_id].add(pipeline_id)
|
|
74
|
+
logging.info(f"🔗 Pipeline {pipeline_id} acquired stream {worker_source_id} (refs: {len(self.stream_references[worker_source_id])})")
|
|
75
|
+
|
|
76
|
+
# If stream is already running, we're done
|
|
44
77
|
if worker_source_id in self.streams or worker_source_id in self.direct_device_streams:
|
|
45
|
-
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
# Get URL from pending streams
|
|
81
|
+
url = self.pending_streams.get(worker_source_id)
|
|
82
|
+
if not url:
|
|
83
|
+
logging.error(f"❌ Cannot acquire stream {worker_source_id}: URL not registered")
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
# Start the stream (outside lock to avoid blocking)
|
|
87
|
+
return self._start_stream(worker_source_id, url)
|
|
88
|
+
|
|
89
|
+
def release_stream(self, worker_source_id, pipeline_id):
|
|
90
|
+
"""Release a stream reference from a pipeline. Stops the stream if no more references."""
|
|
91
|
+
with self._lock:
|
|
92
|
+
if worker_source_id not in self.stream_references:
|
|
46
93
|
return
|
|
47
|
-
|
|
94
|
+
|
|
95
|
+
# Remove pipeline reference
|
|
96
|
+
self.stream_references[worker_source_id].discard(pipeline_id)
|
|
97
|
+
ref_count = len(self.stream_references[worker_source_id])
|
|
98
|
+
logging.info(f"🔓 Pipeline {pipeline_id} released stream {worker_source_id} (refs: {ref_count})")
|
|
99
|
+
|
|
100
|
+
# If no more references, stop the stream
|
|
101
|
+
if ref_count == 0:
|
|
102
|
+
logging.info(f"💤 Stream {worker_source_id} has no more references, stopping...")
|
|
103
|
+
self._stop_stream(worker_source_id)
|
|
104
|
+
|
|
105
|
+
def _start_stream(self, worker_source_id, url):
|
|
106
|
+
"""Internal method to actually start a stream."""
|
|
48
107
|
if self._is_direct_device(url):
|
|
49
108
|
self._add_direct_device_stream(worker_source_id, url)
|
|
50
|
-
return
|
|
109
|
+
return True
|
|
51
110
|
|
|
52
111
|
# Regular stream
|
|
53
112
|
stream = VideoStream(url)
|
|
@@ -55,18 +114,19 @@ class VideoStreamManager:
|
|
|
55
114
|
stream.start() # start thread
|
|
56
115
|
with self._lock:
|
|
57
116
|
self.streams[worker_source_id] = stream
|
|
58
|
-
logging.info("✅
|
|
117
|
+
logging.info("✅ Started video stream: %s", worker_source_id)
|
|
118
|
+
return True
|
|
59
119
|
except Exception as e:
|
|
60
120
|
logging.error("❌ Failed to start regular stream %s: %s", worker_source_id, e)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
-
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def _stop_stream(self, worker_source_id):
|
|
124
|
+
"""Internal method to stop a stream."""
|
|
67
125
|
# Direct device?
|
|
68
126
|
with self._lock:
|
|
69
127
|
is_direct = worker_source_id in self.direct_device_streams
|
|
128
|
+
# Clean up references
|
|
129
|
+
self.stream_references.pop(worker_source_id, None)
|
|
70
130
|
|
|
71
131
|
if is_direct:
|
|
72
132
|
self._remove_direct_device_stream(worker_source_id)
|
|
@@ -77,20 +137,16 @@ class VideoStreamManager:
|
|
|
77
137
|
stream = self.streams.pop(worker_source_id, None)
|
|
78
138
|
|
|
79
139
|
if stream is None:
|
|
80
|
-
logging.warning("⚠️ Stream %s not found in manager.", worker_source_id)
|
|
81
140
|
return
|
|
82
141
|
|
|
83
|
-
logging.info("🛑
|
|
142
|
+
logging.info("🛑 Stopping video stream: %s", worker_source_id)
|
|
84
143
|
try:
|
|
85
|
-
# Expectation: VideoStream.stop() should signal and join internally.
|
|
86
144
|
stream.stop()
|
|
87
145
|
except Exception as e:
|
|
88
146
|
logging.error("❌ Error stopping stream %s: %s", worker_source_id, e)
|
|
89
147
|
finally:
|
|
90
148
|
stream = None
|
|
91
149
|
|
|
92
|
-
logging.info("✅ Stream %s removed successfully.", worker_source_id)
|
|
93
|
-
|
|
94
150
|
def start_all(self):
|
|
95
151
|
"""Starts all regular streams that are not alive. (Direct devices are publisher-driven.)"""
|
|
96
152
|
logging.info("🔄 Starting all video streams...")
|
|
@@ -114,13 +170,13 @@ class VideoStreamManager:
|
|
|
114
170
|
|
|
115
171
|
for wid in regular_ids:
|
|
116
172
|
try:
|
|
117
|
-
self.
|
|
173
|
+
self._stop_stream(wid)
|
|
118
174
|
except Exception as e:
|
|
119
175
|
logging.error("Error stopping regular stream %s: %s", wid, e)
|
|
120
176
|
|
|
121
177
|
for wid in direct_ids:
|
|
122
178
|
try:
|
|
123
|
-
self.
|
|
179
|
+
self._stop_stream(wid)
|
|
124
180
|
except Exception as e:
|
|
125
181
|
logging.error("Error stopping direct device stream %s: %s", wid, e)
|
|
126
182
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nedo-vision-worker-core
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Nedo Vision Worker Core Library for AI Vision Processing
|
|
5
5
|
Author-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
|
|
6
6
|
Maintainer-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os
|
|
3
|
-
import time
|
|
4
|
-
import threading
|
|
5
|
-
from ..database.DatabaseManager import DatabaseManager
|
|
6
|
-
from ..repositories.WorkerSourceRepository import WorkerSourceRepository
|
|
7
|
-
from .VideoStreamManager import VideoStreamManager
|
|
8
|
-
|
|
9
|
-
class StreamSyncThread(threading.Thread):
|
|
10
|
-
"""Thread responsible for synchronizing video streams from the database in real-time."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, manager: VideoStreamManager, polling_interval=5):
|
|
13
|
-
super().__init__() # Set as a daemon so it stops with the main process
|
|
14
|
-
|
|
15
|
-
self.source_file_path = DatabaseManager.STORAGE_PATHS["files"] / "source_files"
|
|
16
|
-
|
|
17
|
-
self.manager = manager
|
|
18
|
-
self.polling_interval = polling_interval
|
|
19
|
-
self.worker_source_repo = WorkerSourceRepository()
|
|
20
|
-
self.running = True
|
|
21
|
-
|
|
22
|
-
def _get_source_file_path(self, file):
|
|
23
|
-
"""Returns the file path for a given source file."""
|
|
24
|
-
return self.source_file_path / os.path.basename(file)
|
|
25
|
-
|
|
26
|
-
def run(self):
|
|
27
|
-
"""Continuously updates the VideoStreamManager with database changes."""
|
|
28
|
-
while self.running:
|
|
29
|
-
try:
|
|
30
|
-
sources = self.worker_source_repo.get_worker_sources()
|
|
31
|
-
db_sources = {
|
|
32
|
-
source.id: (
|
|
33
|
-
source.url if source.type_code == "live"
|
|
34
|
-
else source.url if source.type_code == "direct"
|
|
35
|
-
else self._get_source_file_path(source.file_path),
|
|
36
|
-
source.status_code
|
|
37
|
-
) for source in sources
|
|
38
|
-
} # Store latest sources
|
|
39
|
-
active_stream_ids = set(self.manager.get_active_stream_ids())
|
|
40
|
-
|
|
41
|
-
# **1️⃣ Add new streams**
|
|
42
|
-
for source_id, (url, status_code) in db_sources.items():
|
|
43
|
-
if source_id not in active_stream_ids and status_code == "connected":
|
|
44
|
-
logging.info(f"🟢 Adding new stream: {source_id} ({url})")
|
|
45
|
-
self.manager.add_stream(source_id, url)
|
|
46
|
-
|
|
47
|
-
# **2️⃣ Remove deleted streams**
|
|
48
|
-
for stream_id in active_stream_ids:
|
|
49
|
-
if stream_id not in db_sources or db_sources[stream_id][1] != "connected":
|
|
50
|
-
logging.info(f"🔴 Removing deleted stream: {stream_id}")
|
|
51
|
-
self.manager.remove_stream(stream_id)
|
|
52
|
-
|
|
53
|
-
active_stream_ids = set(self.manager.get_active_stream_ids())
|
|
54
|
-
|
|
55
|
-
# **3️⃣ Update streams if URL has changed**
|
|
56
|
-
for source_id, (url, status_code) in db_sources.items():
|
|
57
|
-
if source_id in active_stream_ids:
|
|
58
|
-
existing_url = self.manager.get_stream_url(source_id)
|
|
59
|
-
if existing_url != url:
|
|
60
|
-
logging.info(f"🟡 Updating stream {source_id}: New URL {url}")
|
|
61
|
-
self.manager.remove_stream(source_id)
|
|
62
|
-
# Add a small delay for device cleanup
|
|
63
|
-
if self._is_direct_device(url) or self._is_direct_device(existing_url):
|
|
64
|
-
time.sleep(0.5) # Allow device to be properly released
|
|
65
|
-
self.manager.add_stream(source_id, url)
|
|
66
|
-
|
|
67
|
-
except Exception as e:
|
|
68
|
-
logging.error(f"⚠️ Error syncing streams from database: {e}")
|
|
69
|
-
|
|
70
|
-
time.sleep(self.polling_interval) # Poll every X seconds
|
|
71
|
-
|
|
72
|
-
def _is_direct_device(self, url) -> bool:
|
|
73
|
-
"""Check if URL represents a direct video device."""
|
|
74
|
-
if isinstance(url, str):
|
|
75
|
-
return url.isdigit() or url.startswith('/dev/video')
|
|
76
|
-
return isinstance(url, int)
|
|
77
|
-
|
|
78
|
-
def stop(self):
|
|
79
|
-
"""Stops the synchronization thread."""
|
|
80
|
-
self.running = False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nedo_vision_worker_core-0.3.4 → nedo_vision_worker_core-0.3.5}/nedo_vision_worker_core/doctor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|