sciveo 0.1.66__tar.gz → 0.1.68__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.
- {sciveo-0.1.66 → sciveo-0.1.68}/PKG-INFO +1 -1
- sciveo-0.1.68/sciveo/media/capture/motion_detection.py +139 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/capture/nvr.py +19 -7
- sciveo-0.1.68/sciveo/media/capture/readers.py +122 -0
- sciveo-0.1.68/sciveo/tools/totp.py +80 -0
- sciveo-0.1.68/sciveo/version.py +2 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo.egg-info/PKG-INFO +1 -1
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo.egg-info/SOURCES.txt +5 -1
- sciveo-0.1.68/test/test_totp.py +41 -0
- sciveo-0.1.66/sciveo/version.py +0 -2
- {sciveo-0.1.66 → sciveo-0.1.68}/README.md +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/api/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/api/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/api/predictors.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/api/server.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/api/upload.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/cli.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/common/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/common/configuration.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/common/model.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/common/optimizers.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/common/sampling.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/content/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/content/dataset.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/content/experiment.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/content/project.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/content/runner.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/capture/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/capture/cam.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/encoders/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/encoders/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/encoders/normalizer.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/nlp/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/nlp/search.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/time_series/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/time_series/dataset.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/time_series/predictor.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/time_series/trainer.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/ml/time_series/window_generator.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/job_daemon.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/layouts/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/layouts/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/pipeline.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/postprocessors/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/postprocessors/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/postprocessors/default.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/audio/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/audio/audio.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/audio/audio_extractor_process.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/aws.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/file/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/file/archive.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/album.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/album_in_image.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/depth_esimation.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/embeddings.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/filters.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/generators.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/histogram.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/mask.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/object_detection.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/resize.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/segmentation.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/image/watermark.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/media_info.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/nlp/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/nlp/address.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/qr.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/sci/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/sci/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/sci/dataset.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/sci/time_series/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/sci/time_series/predictor.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/sci/time_series/trainer.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/tpu_base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/video/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/video/generators.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/video/motion_detection.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/video/resize.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/video/video_album.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/video/video_frames.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/video/video_resample.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/queues.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/server.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/web/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/web/server.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/tools/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/tools/video_interactive.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/dataset/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/dataset/object_detection.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/evaluation/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/evaluation/markdown.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/evaluation/object_detection.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/images/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/images/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/images/description.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/images/embeddings.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/images/object_detection.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/images/tools.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/images/transformers.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/nlp/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/nlp/embeddings.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/nlp/tokenizers/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/nlp/tokenizers/bpe.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/video/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/ml/video/description.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/monitoring/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/monitoring/monitor.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/monitoring/start.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/monitoring/watchdog/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/monitoring/watchdog/base.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/monitoring/watchdog/process.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/network/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/network/camera.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/network/sniffer.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/network/tools.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/array.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/aws/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/aws/priority_queue.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/aws/s3.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/common.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/complexity.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/compress.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/configuration.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/crypto.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/daemon.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/draw/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/draw/contours.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/formating.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/hardware.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/http.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/logger.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/os.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/queue.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/random.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/remote.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/simple_counter.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/synchronized.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/tools/timers.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/web/__init__.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo/web/common.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo.egg-info/dependency_links.txt +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo.egg-info/entry_points.txt +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo.egg-info/requires.txt +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/sciveo.egg-info/top_level.txt +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/setup.cfg +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/setup.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_complexity.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_compress.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_configuration.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_crypto.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_eval_markdown.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_ml_datasets.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_monitoring.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_runner.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_sampling.py +0 -0
- {sciveo-0.1.66 → sciveo-0.1.68}/test/test_tokenizers.py +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Stanislav Georgiev, Softel Labs
|
|
3
|
+
#
|
|
4
|
+
# This is a proprietary file and may not be copied,
|
|
5
|
+
# distributed, or modified without express permission
|
|
6
|
+
# from the owner. For licensing inquiries, please
|
|
7
|
+
# contact s.georgiev@softel.bg.
|
|
8
|
+
#
|
|
9
|
+
# 2025
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import cv2
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from sciveo.tools.logger import *
|
|
19
|
+
from sciveo.tools.daemon import DaemonBase
|
|
20
|
+
from sciveo.media.capture.readers import VideoReaderFFMPEG
|
|
21
|
+
from sciveo.tools.queue import TouchedFilePathQueue
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MotionDetectorGrid:
|
|
25
|
+
def __init__(self, grid):
|
|
26
|
+
self.grid = np.array(grid)
|
|
27
|
+
self.grid_mask = None
|
|
28
|
+
self.zero = False
|
|
29
|
+
|
|
30
|
+
def init_grid(self, frame):
|
|
31
|
+
self.grid_mask = np.zeros_like(frame, dtype=np.float32)
|
|
32
|
+
|
|
33
|
+
for i in range(0, self.grid_mask.shape[0]):
|
|
34
|
+
for j in range(0, self.grid_mask.shape[1]):
|
|
35
|
+
gi = int(self.grid.shape[0] * i / self.grid_mask.shape[0])
|
|
36
|
+
gj = int(self.grid.shape[1] * j / self.grid_mask.shape[1])
|
|
37
|
+
|
|
38
|
+
self.grid_mask[i][j] = self.grid[gi][gj]
|
|
39
|
+
|
|
40
|
+
self.zero = self.grid_mask.sum() <= 0.0
|
|
41
|
+
|
|
42
|
+
def apply(self, frame):
|
|
43
|
+
if self.grid_mask is None:
|
|
44
|
+
self.init_grid(frame)
|
|
45
|
+
result = (frame * self.grid_mask).astype(np.uint8)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class VideoMotionDetector:
|
|
50
|
+
def __init__(self, source_id, configuration):
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.source_id = source_id
|
|
53
|
+
self.configuration = configuration
|
|
54
|
+
self.count_motion_hold = 0
|
|
55
|
+
self.detection_grid = MotionDetectorGrid(self.configuration["grid"])
|
|
56
|
+
|
|
57
|
+
self.init()
|
|
58
|
+
|
|
59
|
+
def init(self):
|
|
60
|
+
self.fgbg = cv2.createBackgroundSubtractorMOG2(history=50, varThreshold=20, detectShadows=False)
|
|
61
|
+
|
|
62
|
+
def apply(self, frame):
|
|
63
|
+
md_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
64
|
+
md_frame = cv2.blur(src=md_frame, ksize=(10,10))
|
|
65
|
+
md_frame = self.fgbg.apply(md_frame)
|
|
66
|
+
md_frame = self.detection_grid.apply(md_frame)
|
|
67
|
+
return md_frame
|
|
68
|
+
|
|
69
|
+
def check_motion(self, fgmask):
|
|
70
|
+
k_motion_level = 0.5 + 0.5 * min(self.configuration["threshold_motion_hold"], self.count_motion_hold) / self.configuration["threshold_motion_hold"]
|
|
71
|
+
current_threshold = self.configuration["threshold_motion"] * k_motion_level
|
|
72
|
+
motion_level = np.mean(fgmask)
|
|
73
|
+
|
|
74
|
+
if motion_level > current_threshold:
|
|
75
|
+
self.count_motion_hold += 1
|
|
76
|
+
if self.count_motion_hold >= self.configuration["threshold_motion_hold"]:
|
|
77
|
+
debug(self.source_id, "motion detected", motion_level, "threshold", current_threshold)
|
|
78
|
+
return True
|
|
79
|
+
else:
|
|
80
|
+
self.count_motion_hold = 0
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def __call__(self, list_frames):
|
|
84
|
+
self.count_motion_hold = 0
|
|
85
|
+
|
|
86
|
+
for frame in list_frames:
|
|
87
|
+
try:
|
|
88
|
+
if self.detection_grid.zero:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
fgmask = self.apply(frame)
|
|
92
|
+
|
|
93
|
+
if self.check_motion(fgmask):
|
|
94
|
+
return True
|
|
95
|
+
except Exception as e:
|
|
96
|
+
exception(e)
|
|
97
|
+
time.sleep(3)
|
|
98
|
+
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MotionDetectorDaemon(DaemonBase):
|
|
103
|
+
def __init__(self, configuration):
|
|
104
|
+
super().__init__()
|
|
105
|
+
self.configuration = configuration
|
|
106
|
+
self.queue = TouchedFilePathQueue(self.configuration["src"], period=3, touched_timeout=3)
|
|
107
|
+
self.motion_detectors = {}
|
|
108
|
+
|
|
109
|
+
def on_motion(self, source_id, file_name, file_path, configuration):
|
|
110
|
+
debug("MOTION", source_id, file_name, configuration)
|
|
111
|
+
if "dst" in self.configuration:
|
|
112
|
+
dst_file_path = os.path.join(self.configuration["dst"], file_name)
|
|
113
|
+
shutil.copy(file_path, dst_file_path)
|
|
114
|
+
debug("CP", file_path, "=>", dst_file_path)
|
|
115
|
+
|
|
116
|
+
def run(self):
|
|
117
|
+
while(self.is_running):
|
|
118
|
+
try:
|
|
119
|
+
file_name, file_path = self.queue.pop()
|
|
120
|
+
debug("pop", file_name, file_path)
|
|
121
|
+
|
|
122
|
+
file_name_split = file_name.split("___")
|
|
123
|
+
if len(file_name_split) >= 3:
|
|
124
|
+
source_id = file_name_split[0]
|
|
125
|
+
|
|
126
|
+
if source_id in self.configuration:
|
|
127
|
+
configuration = self.configuration.get(source_id, { "threshold_motion": 5, "threshold_motion_hold": 8, "grid": [[0.0]] })
|
|
128
|
+
md = self.motion_detectors.get(source_id, VideoMotionDetector(source_id, configuration))
|
|
129
|
+
motion = md(VideoReaderFFMPEG.read(file_path, resolution=360, RGB=False, gpu_id=0))
|
|
130
|
+
|
|
131
|
+
if motion:
|
|
132
|
+
self.on_motion(source_id, file_name, file_path, configuration)
|
|
133
|
+
|
|
134
|
+
os.remove(file_path)
|
|
135
|
+
debug("RM", file_path)
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
exception(e)
|
|
139
|
+
time.sleep(5)
|
|
@@ -22,6 +22,7 @@ from sciveo.tools.logger import *
|
|
|
22
22
|
from sciveo.tools.daemon import DaemonBase
|
|
23
23
|
from sciveo.tools.queue import TouchedFilePathQueue
|
|
24
24
|
from sciveo.tools.simple_counter import RunCounter
|
|
25
|
+
from sciveo.media.capture.motion_detection import MotionDetectorDaemon
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class VideoCameraCaptureDaemon(DaemonBase):
|
|
@@ -117,7 +118,7 @@ class VideoRecorder:
|
|
|
117
118
|
def __init__(self, path_configuration):
|
|
118
119
|
with open(path_configuration, 'r') as fp:
|
|
119
120
|
self.configuration = json.load(fp)
|
|
120
|
-
self.
|
|
121
|
+
self.daemons = []
|
|
121
122
|
|
|
122
123
|
for cam_id, cam_config in self.configuration["cam"].items():
|
|
123
124
|
cam = VideoCameraCaptureDaemon(
|
|
@@ -126,14 +127,17 @@ class VideoRecorder:
|
|
|
126
127
|
self.configuration.get("max_video_len", 60),
|
|
127
128
|
self.configuration.get("transport", "tcp")
|
|
128
129
|
)
|
|
129
|
-
self.
|
|
130
|
+
self.daemons.append(cam)
|
|
131
|
+
|
|
132
|
+
if "motion" in self.configuration:
|
|
133
|
+
self.daemons.append(MotionDetectorDaemon(configuration=self.configuration["motion"]))
|
|
130
134
|
|
|
131
135
|
self.queue = TouchedFilePathQueue(self.configuration["path"]["tmp"], period=5, touched_timeout=5)
|
|
132
136
|
self.cleaner_timer = RunCounter(1000, self.clean_old_videos)
|
|
133
137
|
|
|
134
138
|
def start(self):
|
|
135
|
-
for
|
|
136
|
-
|
|
139
|
+
for d in self.daemons:
|
|
140
|
+
d.start()
|
|
137
141
|
|
|
138
142
|
time.sleep(10)
|
|
139
143
|
|
|
@@ -171,9 +175,17 @@ class VideoRecorder:
|
|
|
171
175
|
video_base_path = os.path.join(self.configuration["path"]["video"], cam_id, video_date)
|
|
172
176
|
video_file_path = os.path.join(video_base_path, video_file_name)
|
|
173
177
|
|
|
174
|
-
debug("MV", file_path, "=>", video_file_path)
|
|
175
178
|
os.makedirs(video_base_path, exist_ok=True)
|
|
176
|
-
shutil.
|
|
179
|
+
shutil.copy(file_path, video_file_path)
|
|
180
|
+
debug("CP", file_path, "=>", video_file_path)
|
|
181
|
+
|
|
182
|
+
if "motion" in self.configuration:
|
|
183
|
+
motion_file_path = os.path.join(self.configuration["motion"]["src"], f"{cam_id}___{video_date}___{video_file_name}")
|
|
184
|
+
shutil.move(file_path, motion_file_path)
|
|
185
|
+
debug("MV", file_path, "=>", motion_file_path)
|
|
186
|
+
else:
|
|
187
|
+
os.remove(file_path)
|
|
188
|
+
debug("RM", file_path)
|
|
177
189
|
|
|
178
190
|
def clean_old_videos(self):
|
|
179
191
|
try:
|
|
@@ -182,7 +194,7 @@ class VideoRecorder:
|
|
|
182
194
|
debug("cmd", cmd)
|
|
183
195
|
os.system(cmd)
|
|
184
196
|
except Exception as e:
|
|
185
|
-
|
|
197
|
+
exception(e, cmd)
|
|
186
198
|
|
|
187
199
|
|
|
188
200
|
if __name__ == '__main__':
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Stanislav Georgiev, Softel Labs
|
|
3
|
+
#
|
|
4
|
+
# This is a proprietary file and may not be copied,
|
|
5
|
+
# distributed, or modified without express permission
|
|
6
|
+
# from the owner. For licensing inquiries, please
|
|
7
|
+
# contact s.georgiev@softel.bg.
|
|
8
|
+
#
|
|
9
|
+
# 2025
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import select
|
|
14
|
+
import json
|
|
15
|
+
import subprocess
|
|
16
|
+
import ffmpeg
|
|
17
|
+
import json
|
|
18
|
+
import numpy as np
|
|
19
|
+
from sciveo.tools.logger import *
|
|
20
|
+
from sciveo.tools.timers import Timer
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VideoReaderFFMPEG:
|
|
24
|
+
def __init__(self, path, resolution=720, RGB=False, gpu_id=-1) -> None:
|
|
25
|
+
self.path = path
|
|
26
|
+
self.resolution = resolution
|
|
27
|
+
self.RGB = RGB
|
|
28
|
+
self.gpu_id = gpu_id
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def get_dim(w, h, resolution):
|
|
32
|
+
if resolution <= 0:
|
|
33
|
+
return w, h
|
|
34
|
+
|
|
35
|
+
if h > resolution:
|
|
36
|
+
ratio = resolution / h
|
|
37
|
+
else:
|
|
38
|
+
ratio = 1.0
|
|
39
|
+
width = int(w * ratio)
|
|
40
|
+
height = int(h * ratio)
|
|
41
|
+
width += width % 2
|
|
42
|
+
height += height % 2
|
|
43
|
+
return width, height
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def read(file_path, resolution=720, RGB=False, gpu_id=-1, timeout=30):
|
|
47
|
+
timer_video_read = Timer()
|
|
48
|
+
frames = []
|
|
49
|
+
|
|
50
|
+
probe = subprocess.check_output([
|
|
51
|
+
"ffprobe", "-v", "error",
|
|
52
|
+
"-select_streams", "v:0",
|
|
53
|
+
"-show_entries", "stream=width,height,codec_name",
|
|
54
|
+
"-of", "json", file_path
|
|
55
|
+
])
|
|
56
|
+
stream_data = json.loads(probe)["streams"][0]
|
|
57
|
+
|
|
58
|
+
in_w, in_h = int(stream_data["width"]), int(stream_data["height"])
|
|
59
|
+
out_w, out_h = VideoReaderFFMPEG.get_dim(in_w, in_h, resolution)
|
|
60
|
+
frame_size = out_w * out_h * 3
|
|
61
|
+
|
|
62
|
+
gpu_nvdec_id = int(os.environ.get("GPU_NVDEC_H264_ID", gpu_id))
|
|
63
|
+
|
|
64
|
+
codec = stream_data["codec_name"]
|
|
65
|
+
if codec in ["h264", "avc1"]:
|
|
66
|
+
decoder = "h264_cuvid"
|
|
67
|
+
elif codec in ["hevc", "h265", "hvc1"]:
|
|
68
|
+
decoder = "hevc_cuvid"
|
|
69
|
+
gpu_nvdec_id = int(os.environ.get("GPU_NVDEC_H265_ID", gpu_nvdec_id))
|
|
70
|
+
|
|
71
|
+
HW = "CPU"
|
|
72
|
+
pix_fmt = "rgb24" if RGB else "bgr24"
|
|
73
|
+
|
|
74
|
+
if min(gpu_id, gpu_nvdec_id) < 0:
|
|
75
|
+
HW = "CPU"
|
|
76
|
+
ffmpeg_input = ffmpeg.input(file_path)
|
|
77
|
+
else:
|
|
78
|
+
HW = "GPU"
|
|
79
|
+
ffmpeg_input = ffmpeg.input(file_path, hwaccel='cuda', hwaccel_device=gpu_nvdec_id, c=decoder)
|
|
80
|
+
|
|
81
|
+
if in_w != out_w or in_h != out_h:
|
|
82
|
+
ffmpeg_input = ffmpeg_input.filter('scale', out_w, out_h)
|
|
83
|
+
|
|
84
|
+
process = (
|
|
85
|
+
ffmpeg_input
|
|
86
|
+
.output('pipe:', format='rawvideo', pix_fmt=pix_fmt, vsync='vfr')
|
|
87
|
+
.global_args('-v', 'quiet')
|
|
88
|
+
.run_async(pipe_stdout=True, pipe_stderr=subprocess.DEVNULL)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
fd = process.stdout.fileno()
|
|
92
|
+
|
|
93
|
+
while True:
|
|
94
|
+
ready, _, _ = select.select([fd], [], [], timeout)
|
|
95
|
+
if not ready:
|
|
96
|
+
warning("VideoReaderFFMPEG::read", file_path, f"[{HW}][{gpu_id}] not ready FAIL")
|
|
97
|
+
process.kill()
|
|
98
|
+
process.wait()
|
|
99
|
+
frames = []
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
in_bytes = process.stdout.read(frame_size)
|
|
103
|
+
if not in_bytes:
|
|
104
|
+
break
|
|
105
|
+
frame = np.frombuffer(in_bytes, np.uint8).reshape([out_h, out_w, 3]).copy()
|
|
106
|
+
frames.append(frame)
|
|
107
|
+
|
|
108
|
+
process.wait()
|
|
109
|
+
|
|
110
|
+
if gpu_id >= 0 and len(frames) == 0:
|
|
111
|
+
warning("VideoReaderFFMPEG::read", file_path, f"[{HW}][{gpu_id}] reading not successful, fallback to CPU")
|
|
112
|
+
return VideoReaderFFMPEG.read(file_path, resolution, RGB, gpu_id=-1)
|
|
113
|
+
|
|
114
|
+
elapsed = timer_video_read.stop()
|
|
115
|
+
FPS = round(len(frames) / elapsed, 2)
|
|
116
|
+
debug("VideoReaderFFMPEG::read", file_path, f"[{HW}] video reading {len(frames)} frames, codec[{codec}] decoder[{decoder}] pix_fmt[{pix_fmt}] [{out_w}x{out_h}], elapsed {round(elapsed, 1)} FPS {FPS}")
|
|
117
|
+
return frames
|
|
118
|
+
|
|
119
|
+
def __call__(self):
|
|
120
|
+
return VideoReaderFFMPEG.read(self.path, self.resolution, RGB=self.RGB, gpu_id=self.gpu_id)
|
|
121
|
+
|
|
122
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Pavlin Georgiev, Softel Labs
|
|
3
|
+
#
|
|
4
|
+
# This is a proprietary file and may not be copied,
|
|
5
|
+
# distributed, or modified without express permission
|
|
6
|
+
# from the owner. For licensing inquiries, please
|
|
7
|
+
# contact pavlin@softel.bg.
|
|
8
|
+
#
|
|
9
|
+
# 2025
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import hmac
|
|
15
|
+
import base64
|
|
16
|
+
import struct
|
|
17
|
+
import hashlib
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
from sciveo.tools.logger import *
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthTOTP:
|
|
24
|
+
def __init__(self, secret: str, interval: int = 30, digits: int = 6, algo=hashlib.sha1):
|
|
25
|
+
"""
|
|
26
|
+
secret: the private key
|
|
27
|
+
interval: time step in seconds (default 30)
|
|
28
|
+
digits: number of digits in code (default 6)
|
|
29
|
+
algo: hash function (default SHA1, per RFC 6238)
|
|
30
|
+
"""
|
|
31
|
+
self.secret = secret
|
|
32
|
+
self.interval = interval
|
|
33
|
+
self.digits = digits
|
|
34
|
+
self.algo = algo
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def new_private_key(length: int = 20) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Generate a new Base32 secret key.
|
|
40
|
+
"""
|
|
41
|
+
raw = os.urandom(length)
|
|
42
|
+
return base64.b32encode(raw).decode("utf-8").replace("=", "")
|
|
43
|
+
|
|
44
|
+
def _generate_otp(self, for_time: int) -> int:
|
|
45
|
+
"""
|
|
46
|
+
Internal: generate OTP for given time counter.
|
|
47
|
+
"""
|
|
48
|
+
pad_len = (8 - len(self.secret) % 8) % 8
|
|
49
|
+
secret_padded = self.secret + ("=" * pad_len)
|
|
50
|
+
key = base64.b32decode(secret_padded, casefold=True)
|
|
51
|
+
|
|
52
|
+
counter = struct.pack(">Q", for_time)
|
|
53
|
+
hmac_digest = hmac.new(key, counter, self.algo).digest()
|
|
54
|
+
|
|
55
|
+
offset = hmac_digest[-1] & 0x0F
|
|
56
|
+
code = struct.unpack(">I", hmac_digest[offset:offset+4])[0] & 0x7FFFFFFF
|
|
57
|
+
return code % (10 ** self.digits)
|
|
58
|
+
|
|
59
|
+
def next_token(self, for_time: int = None) -> str:
|
|
60
|
+
"""
|
|
61
|
+
Return the TOTP token for current (or given) time.
|
|
62
|
+
"""
|
|
63
|
+
if for_time is None:
|
|
64
|
+
for_time = int(time.time())
|
|
65
|
+
counter = for_time // self.interval
|
|
66
|
+
return str(self._generate_otp(counter)).zfill(self.digits)
|
|
67
|
+
|
|
68
|
+
def verify_token(self, token: str, for_time: int = None, window: int = 1) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Verify a token, allowing ±window intervals (to handle clock skew).
|
|
71
|
+
"""
|
|
72
|
+
if for_time is None:
|
|
73
|
+
for_time = int(time.time())
|
|
74
|
+
counter = for_time // self.interval
|
|
75
|
+
for delta in range(-window, window + 1):
|
|
76
|
+
candidate = str(self._generate_otp(counter + delta)).zfill(self.digits)
|
|
77
|
+
# debug("verify", token, "delta", delta, "candidate", candidate)
|
|
78
|
+
if hmac.compare_digest(candidate, token):
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
@@ -27,7 +27,9 @@ sciveo/content/runner.py
|
|
|
27
27
|
sciveo/media/__init__.py
|
|
28
28
|
sciveo/media/capture/__init__.py
|
|
29
29
|
sciveo/media/capture/cam.py
|
|
30
|
+
sciveo/media/capture/motion_detection.py
|
|
30
31
|
sciveo/media/capture/nvr.py
|
|
32
|
+
sciveo/media/capture/readers.py
|
|
31
33
|
sciveo/media/ml/__init__.py
|
|
32
34
|
sciveo/media/ml/base.py
|
|
33
35
|
sciveo/media/ml/encoders/__init__.py
|
|
@@ -143,6 +145,7 @@ sciveo/tools/remote.py
|
|
|
143
145
|
sciveo/tools/simple_counter.py
|
|
144
146
|
sciveo/tools/synchronized.py
|
|
145
147
|
sciveo/tools/timers.py
|
|
148
|
+
sciveo/tools/totp.py
|
|
146
149
|
sciveo/tools/aws/__init__.py
|
|
147
150
|
sciveo/tools/aws/priority_queue.py
|
|
148
151
|
sciveo/tools/aws/s3.py
|
|
@@ -159,4 +162,5 @@ test/test_ml_datasets.py
|
|
|
159
162
|
test/test_monitoring.py
|
|
160
163
|
test/test_runner.py
|
|
161
164
|
test/test_sampling.py
|
|
162
|
-
test/test_tokenizers.py
|
|
165
|
+
test/test_tokenizers.py
|
|
166
|
+
test/test_totp.py
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Pavlin Georgiev, Softel Labs
|
|
3
|
+
#
|
|
4
|
+
# This is a proprietary file and may not be copied,
|
|
5
|
+
# distributed, or modified without express permission
|
|
6
|
+
# from the owner. For licensing inquiries, please
|
|
7
|
+
# contact pavlin@softel.bg.
|
|
8
|
+
#
|
|
9
|
+
# 2025
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
from sciveo.tools.totp import *
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestTOTP(unittest.TestCase):
|
|
19
|
+
@classmethod
|
|
20
|
+
def setUpClass(cls):
|
|
21
|
+
cls.secret = AuthTOTP.new_private_key(length=32)
|
|
22
|
+
cls.auth = AuthTOTP(cls.secret, digits=8)
|
|
23
|
+
|
|
24
|
+
def test_pass(self):
|
|
25
|
+
token = self.auth.next_token()
|
|
26
|
+
self.assertTrue(self.auth.verify_token(token=token))
|
|
27
|
+
|
|
28
|
+
def test_expiry_time_fail(self):
|
|
29
|
+
auth = AuthTOTP(self.secret, interval=3, digits=8)
|
|
30
|
+
token = auth.next_token()
|
|
31
|
+
self.assertTrue(auth.verify_token(token=token))
|
|
32
|
+
time.sleep(2)
|
|
33
|
+
self.assertTrue(auth.verify_token(token=token))
|
|
34
|
+
time.sleep(2)
|
|
35
|
+
self.assertTrue(auth.verify_token(token=token))
|
|
36
|
+
time.sleep(2)
|
|
37
|
+
self.assertFalse(auth.verify_token(token=token))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if __name__ == '__main__':
|
|
41
|
+
unittest.main()
|
sciveo-0.1.66/sciveo/version.py
DELETED
|
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
|
{sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/audio/audio_extractor_process.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
|
{sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/sci/time_series/__init__.py
RENAMED
|
File without changes
|
{sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/sci/time_series/predictor.py
RENAMED
|
File without changes
|
{sciveo-0.1.66 → sciveo-0.1.68}/sciveo/media/pipelines/processors/sci/time_series/trainer.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|