media-engine 0.1.0__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.
- cli/clip.py +79 -0
- cli/faces.py +91 -0
- cli/metadata.py +68 -0
- cli/motion.py +77 -0
- cli/objects.py +94 -0
- cli/ocr.py +93 -0
- cli/scenes.py +57 -0
- cli/telemetry.py +65 -0
- cli/transcript.py +76 -0
- media_engine/__init__.py +7 -0
- media_engine/_version.py +34 -0
- media_engine/app.py +80 -0
- media_engine/batch/__init__.py +56 -0
- media_engine/batch/models.py +99 -0
- media_engine/batch/processor.py +1131 -0
- media_engine/batch/queue.py +232 -0
- media_engine/batch/state.py +30 -0
- media_engine/batch/timing.py +321 -0
- media_engine/cli.py +17 -0
- media_engine/config.py +674 -0
- media_engine/extractors/__init__.py +75 -0
- media_engine/extractors/clip.py +401 -0
- media_engine/extractors/faces.py +459 -0
- media_engine/extractors/frame_buffer.py +351 -0
- media_engine/extractors/frames.py +402 -0
- media_engine/extractors/metadata/__init__.py +127 -0
- media_engine/extractors/metadata/apple.py +169 -0
- media_engine/extractors/metadata/arri.py +118 -0
- media_engine/extractors/metadata/avchd.py +208 -0
- media_engine/extractors/metadata/avchd_gps.py +270 -0
- media_engine/extractors/metadata/base.py +688 -0
- media_engine/extractors/metadata/blackmagic.py +139 -0
- media_engine/extractors/metadata/camera_360.py +276 -0
- media_engine/extractors/metadata/canon.py +290 -0
- media_engine/extractors/metadata/dji.py +371 -0
- media_engine/extractors/metadata/dv.py +121 -0
- media_engine/extractors/metadata/ffmpeg.py +76 -0
- media_engine/extractors/metadata/generic.py +119 -0
- media_engine/extractors/metadata/gopro.py +256 -0
- media_engine/extractors/metadata/red.py +305 -0
- media_engine/extractors/metadata/registry.py +114 -0
- media_engine/extractors/metadata/sony.py +442 -0
- media_engine/extractors/metadata/tesla.py +157 -0
- media_engine/extractors/motion.py +765 -0
- media_engine/extractors/objects.py +245 -0
- media_engine/extractors/objects_qwen.py +754 -0
- media_engine/extractors/ocr.py +268 -0
- media_engine/extractors/scenes.py +82 -0
- media_engine/extractors/shot_type.py +217 -0
- media_engine/extractors/telemetry.py +262 -0
- media_engine/extractors/transcribe.py +579 -0
- media_engine/extractors/translate.py +121 -0
- media_engine/extractors/vad.py +263 -0
- media_engine/main.py +68 -0
- media_engine/py.typed +0 -0
- media_engine/routers/__init__.py +15 -0
- media_engine/routers/batch.py +78 -0
- media_engine/routers/health.py +93 -0
- media_engine/routers/models.py +211 -0
- media_engine/routers/settings.py +87 -0
- media_engine/routers/utils.py +135 -0
- media_engine/schemas.py +581 -0
- media_engine/utils/__init__.py +5 -0
- media_engine/utils/logging.py +54 -0
- media_engine/utils/memory.py +49 -0
- media_engine-0.1.0.dist-info/METADATA +276 -0
- media_engine-0.1.0.dist-info/RECORD +70 -0
- media_engine-0.1.0.dist-info/WHEEL +4 -0
- media_engine-0.1.0.dist-info/entry_points.txt +11 -0
- media_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
cli/clip.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract CLIP embeddings from video file."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from media_engine.extractors import (
|
|
11
|
+
analyze_motion,
|
|
12
|
+
decode_frames,
|
|
13
|
+
extract_clip,
|
|
14
|
+
get_adaptive_timestamps,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
parser = argparse.ArgumentParser(description="Extract CLIP embeddings from video")
|
|
20
|
+
parser.add_argument("file", help="Path to video file")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--model",
|
|
23
|
+
type=str,
|
|
24
|
+
default=None,
|
|
25
|
+
help="CLIP model name (e.g., ViT-B-32, ViT-L-14)",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
28
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
29
|
+
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
if args.verbose:
|
|
33
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
34
|
+
else:
|
|
35
|
+
logging.basicConfig(level=logging.WARNING)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
start_time = time.perf_counter()
|
|
39
|
+
|
|
40
|
+
# Run motion analysis to get adaptive timestamps
|
|
41
|
+
motion = analyze_motion(args.file)
|
|
42
|
+
timestamps = get_adaptive_timestamps(motion)
|
|
43
|
+
|
|
44
|
+
# Decode frames once using shared buffer
|
|
45
|
+
frame_buffer = decode_frames(args.file, timestamps=timestamps)
|
|
46
|
+
|
|
47
|
+
# Extract CLIP embeddings using shared frame buffer
|
|
48
|
+
result = extract_clip(
|
|
49
|
+
args.file,
|
|
50
|
+
frame_buffer=frame_buffer,
|
|
51
|
+
model_name=args.model,
|
|
52
|
+
)
|
|
53
|
+
elapsed = time.perf_counter() - start_time
|
|
54
|
+
|
|
55
|
+
if args.json:
|
|
56
|
+
output = result.model_dump()
|
|
57
|
+
output["elapsed_seconds"] = round(elapsed, 2)
|
|
58
|
+
print(json.dumps(output, indent=2, default=str))
|
|
59
|
+
else:
|
|
60
|
+
print(f"File: {args.file}")
|
|
61
|
+
print(f"Model: {result.model}")
|
|
62
|
+
print(f"Segments: {len(result.segments)}")
|
|
63
|
+
if result.segments:
|
|
64
|
+
print(f"Embedding dimensions: {len(result.segments[0].embedding)}")
|
|
65
|
+
print()
|
|
66
|
+
for i, seg in enumerate(result.segments[:10], 1): # Show first 10
|
|
67
|
+
print(f" {i}: {seg.start:.2f}s-{seg.end:.2f}s embedding[{len(seg.embedding)}]")
|
|
68
|
+
if len(result.segments) > 10:
|
|
69
|
+
print(f" ... and {len(result.segments) - 10} more")
|
|
70
|
+
print()
|
|
71
|
+
print(f"Elapsed: {elapsed:.2f}s")
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
cli/faces.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Detect faces in video file."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from media_engine.extractors import (
|
|
11
|
+
analyze_motion,
|
|
12
|
+
decode_frames,
|
|
13
|
+
extract_faces,
|
|
14
|
+
get_adaptive_timestamps,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
parser = argparse.ArgumentParser(description="Detect faces in video file")
|
|
20
|
+
parser.add_argument("file", help="Path to video file")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--sample-fps",
|
|
23
|
+
type=float,
|
|
24
|
+
default=1.0,
|
|
25
|
+
help="Sample rate for face detection (default: 1.0)",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--min-face-size",
|
|
29
|
+
type=int,
|
|
30
|
+
default=80,
|
|
31
|
+
help="Minimum face size in pixels (default: 80)",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--min-confidence",
|
|
35
|
+
type=float,
|
|
36
|
+
default=0.5,
|
|
37
|
+
help="Minimum detection confidence (default: 0.5)",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
40
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
41
|
+
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
if args.verbose:
|
|
45
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
46
|
+
else:
|
|
47
|
+
logging.basicConfig(level=logging.WARNING)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
start_time = time.perf_counter()
|
|
51
|
+
|
|
52
|
+
# Run motion analysis to get adaptive timestamps
|
|
53
|
+
motion = analyze_motion(args.file)
|
|
54
|
+
timestamps = get_adaptive_timestamps(motion)
|
|
55
|
+
|
|
56
|
+
# Decode frames once using shared buffer
|
|
57
|
+
frame_buffer = decode_frames(args.file, timestamps=timestamps)
|
|
58
|
+
|
|
59
|
+
# Extract faces using shared frame buffer
|
|
60
|
+
result = extract_faces(
|
|
61
|
+
args.file,
|
|
62
|
+
frame_buffer=frame_buffer,
|
|
63
|
+
min_face_size=args.min_face_size,
|
|
64
|
+
min_confidence=args.min_confidence,
|
|
65
|
+
)
|
|
66
|
+
elapsed = time.perf_counter() - start_time
|
|
67
|
+
|
|
68
|
+
if args.json:
|
|
69
|
+
output = result.model_dump()
|
|
70
|
+
output["elapsed_seconds"] = round(elapsed, 2)
|
|
71
|
+
print(json.dumps(output, indent=2, default=str))
|
|
72
|
+
else:
|
|
73
|
+
print(f"File: {args.file}")
|
|
74
|
+
print(f"Faces detected: {result.count}")
|
|
75
|
+
print(f"Unique estimate: {result.unique_estimate}")
|
|
76
|
+
print()
|
|
77
|
+
for i, face in enumerate(result.detections[:20], 1): # Show first 20
|
|
78
|
+
bbox = face.bbox
|
|
79
|
+
print(f" {i}: t={face.timestamp:.2f}s " f"box=({bbox.x},{bbox.y},{bbox.width}x{bbox.height}) " f"conf={face.confidence:.2f}")
|
|
80
|
+
if result.count > 20:
|
|
81
|
+
print(f" ... and {result.count - 20} more")
|
|
82
|
+
print()
|
|
83
|
+
print(f"Elapsed: {elapsed:.2f}s")
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
main()
|
cli/metadata.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract metadata from video file."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from media_engine.extractors import extract_metadata
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(description="Extract metadata from video file")
|
|
15
|
+
parser.add_argument("file", help="Path to video file")
|
|
16
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
17
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON (default: human-readable)")
|
|
18
|
+
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
if args.verbose:
|
|
22
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
23
|
+
else:
|
|
24
|
+
logging.basicConfig(level=logging.WARNING)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
start_time = time.perf_counter()
|
|
28
|
+
result = extract_metadata(args.file)
|
|
29
|
+
elapsed = time.perf_counter() - start_time
|
|
30
|
+
|
|
31
|
+
if args.json:
|
|
32
|
+
output = result.model_dump()
|
|
33
|
+
output["elapsed_seconds"] = round(elapsed, 2)
|
|
34
|
+
print(json.dumps(output, indent=2, default=str))
|
|
35
|
+
else:
|
|
36
|
+
print(f"File: {args.file}")
|
|
37
|
+
print(f"Duration: {result.duration}s")
|
|
38
|
+
print(f"Resolution: {result.resolution.width}x{result.resolution.height}")
|
|
39
|
+
print(f"FPS: {result.fps}")
|
|
40
|
+
if result.video_codec:
|
|
41
|
+
print(f"Codec: {result.video_codec.name}")
|
|
42
|
+
if result.device:
|
|
43
|
+
print(f"Device: {result.device.make} {result.device.model}")
|
|
44
|
+
if result.gps:
|
|
45
|
+
print(f"GPS: {result.gps.latitude}, {result.gps.longitude}")
|
|
46
|
+
if result.gps_track:
|
|
47
|
+
track = result.gps_track
|
|
48
|
+
bounds = track.bounds
|
|
49
|
+
if bounds:
|
|
50
|
+
lat_range = bounds["max_lat"] - bounds["min_lat"]
|
|
51
|
+
lon_range = bounds["max_lon"] - bounds["min_lon"]
|
|
52
|
+
print(f"GPS Track: {track.count} points (lat range: {lat_range:.6f}, lon range: {lon_range:.6f})")
|
|
53
|
+
if result.shot_type:
|
|
54
|
+
print(f"Shot type: {result.shot_type.primary} ({result.shot_type.confidence:.2f})")
|
|
55
|
+
if result.keyframes:
|
|
56
|
+
kf = result.keyframes
|
|
57
|
+
interval_type = "fixed GOP" if kf.is_fixed_interval else "irregular (likely cuts)"
|
|
58
|
+
print(f"Keyframes: {kf.count} ({interval_type}, avg {kf.avg_interval}s)")
|
|
59
|
+
print()
|
|
60
|
+
print(f"Elapsed: {elapsed:.2f}s")
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
cli/motion.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Analyze camera motion in video file."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from media_engine.extractors import analyze_motion
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(description="Analyze camera motion in video")
|
|
15
|
+
parser.add_argument("file", help="Path to video file")
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--sample-fps",
|
|
18
|
+
type=float,
|
|
19
|
+
default=2.0,
|
|
20
|
+
help="Sample rate for motion analysis (default: 2.0)",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
23
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
24
|
+
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
if args.verbose:
|
|
28
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
29
|
+
else:
|
|
30
|
+
logging.basicConfig(level=logging.WARNING)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
start_time = time.perf_counter()
|
|
34
|
+
result = analyze_motion(args.file, sample_fps=args.sample_fps)
|
|
35
|
+
elapsed = time.perf_counter() - start_time
|
|
36
|
+
|
|
37
|
+
if args.json:
|
|
38
|
+
data = {
|
|
39
|
+
"duration": result.duration,
|
|
40
|
+
"fps": result.fps,
|
|
41
|
+
"primary_motion": result.primary_motion.value,
|
|
42
|
+
"avg_intensity": float(result.avg_intensity),
|
|
43
|
+
"is_stable": result.is_stable,
|
|
44
|
+
"segments": [
|
|
45
|
+
{
|
|
46
|
+
"start": s.start,
|
|
47
|
+
"end": s.end,
|
|
48
|
+
"motion_type": s.motion_type.value,
|
|
49
|
+
"intensity": float(s.intensity),
|
|
50
|
+
}
|
|
51
|
+
for s in result.segments
|
|
52
|
+
],
|
|
53
|
+
"elapsed_seconds": round(elapsed, 2),
|
|
54
|
+
}
|
|
55
|
+
print(json.dumps(data, indent=2))
|
|
56
|
+
else:
|
|
57
|
+
print(f"File: {args.file}")
|
|
58
|
+
print(f"Duration: {result.duration:.2f}s")
|
|
59
|
+
print(f"Primary motion: {result.primary_motion.value}")
|
|
60
|
+
print(f"Avg intensity: {result.avg_intensity:.2f}")
|
|
61
|
+
print(f"Stable: {result.is_stable}")
|
|
62
|
+
print(f"Segments: {len(result.segments)}")
|
|
63
|
+
print()
|
|
64
|
+
for i, seg in enumerate(result.segments[:10], 1): # Show first 10
|
|
65
|
+
print(f" {i}: {seg.start:.2f}s-{seg.end:.2f}s " f"{seg.motion_type.value} (intensity: {seg.intensity:.2f})")
|
|
66
|
+
if len(result.segments) > 10:
|
|
67
|
+
print(f" ... and {len(result.segments) - 10} more")
|
|
68
|
+
print()
|
|
69
|
+
print(f"Elapsed: {elapsed:.2f}s")
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
cli/objects.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Detect objects in video file."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from media_engine.extractors import (
|
|
11
|
+
analyze_motion,
|
|
12
|
+
decode_frames,
|
|
13
|
+
extract_objects,
|
|
14
|
+
extract_objects_qwen,
|
|
15
|
+
get_adaptive_timestamps,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
parser = argparse.ArgumentParser(description="Detect objects in video file")
|
|
21
|
+
parser.add_argument("file", help="Path to video file")
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--detector",
|
|
24
|
+
type=str,
|
|
25
|
+
default="yolo",
|
|
26
|
+
choices=["yolo", "qwen"],
|
|
27
|
+
help="Object detector to use (default: yolo)",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--sample-fps",
|
|
31
|
+
type=float,
|
|
32
|
+
default=2.0,
|
|
33
|
+
help="Sample rate for YOLO detection (default: 2.0)",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--min-confidence",
|
|
37
|
+
type=float,
|
|
38
|
+
default=0.5,
|
|
39
|
+
help="Minimum detection confidence (default: 0.5)",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
42
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
43
|
+
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
|
|
46
|
+
if args.verbose:
|
|
47
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
48
|
+
else:
|
|
49
|
+
logging.basicConfig(level=logging.WARNING)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
start_time = time.perf_counter()
|
|
53
|
+
if args.detector == "qwen":
|
|
54
|
+
result = extract_objects_qwen(args.file)
|
|
55
|
+
else:
|
|
56
|
+
# Run motion analysis to get adaptive timestamps
|
|
57
|
+
motion = analyze_motion(args.file)
|
|
58
|
+
timestamps = get_adaptive_timestamps(motion)
|
|
59
|
+
|
|
60
|
+
# Decode frames once using shared buffer
|
|
61
|
+
frame_buffer = decode_frames(args.file, timestamps=timestamps)
|
|
62
|
+
|
|
63
|
+
# Extract objects using shared frame buffer
|
|
64
|
+
result = extract_objects(
|
|
65
|
+
args.file,
|
|
66
|
+
frame_buffer=frame_buffer,
|
|
67
|
+
min_confidence=args.min_confidence,
|
|
68
|
+
)
|
|
69
|
+
elapsed = time.perf_counter() - start_time
|
|
70
|
+
|
|
71
|
+
if args.json:
|
|
72
|
+
output = result.model_dump()
|
|
73
|
+
output["elapsed_seconds"] = round(elapsed, 2)
|
|
74
|
+
print(json.dumps(output, indent=2, default=str))
|
|
75
|
+
else:
|
|
76
|
+
print(f"File: {args.file}")
|
|
77
|
+
print(f"Detector: {args.detector}")
|
|
78
|
+
print(f"Detections: {len(result.detections)}")
|
|
79
|
+
print()
|
|
80
|
+
print("Summary:")
|
|
81
|
+
for label, count in sorted(result.summary.items(), key=lambda x: x[1], reverse=True)[:15]:
|
|
82
|
+
print(f" {label}: {count}")
|
|
83
|
+
if len(result.summary) > 15:
|
|
84
|
+
print(f" ... and {len(result.summary) - 15} more types")
|
|
85
|
+
print()
|
|
86
|
+
print(f"Elapsed: {elapsed:.2f}s")
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
main()
|
cli/ocr.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract text (OCR) from video file."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from media_engine.extractors import (
|
|
11
|
+
analyze_motion,
|
|
12
|
+
decode_frames,
|
|
13
|
+
extract_ocr,
|
|
14
|
+
get_adaptive_timestamps,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main():
|
|
19
|
+
parser = argparse.ArgumentParser(description="Extract text (OCR) from video file")
|
|
20
|
+
parser.add_argument("file", help="Path to video file")
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--min-confidence",
|
|
23
|
+
type=float,
|
|
24
|
+
default=0.5,
|
|
25
|
+
help="Minimum detection confidence (default: 0.5)",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--skip-prefilter",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="Skip MSER pre-filter (run OCR on all frames)",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--languages",
|
|
34
|
+
type=str,
|
|
35
|
+
default=None,
|
|
36
|
+
help="OCR languages, comma-separated (e.g., 'en,no,de')",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
39
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
40
|
+
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
if args.verbose:
|
|
44
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
45
|
+
else:
|
|
46
|
+
logging.basicConfig(level=logging.WARNING)
|
|
47
|
+
|
|
48
|
+
languages = None
|
|
49
|
+
if args.languages:
|
|
50
|
+
languages = [lang.strip() for lang in args.languages.split(",")]
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
start_time = time.perf_counter()
|
|
54
|
+
|
|
55
|
+
# Run motion analysis to get adaptive timestamps
|
|
56
|
+
motion = analyze_motion(args.file)
|
|
57
|
+
timestamps = get_adaptive_timestamps(motion)
|
|
58
|
+
|
|
59
|
+
# Decode frames once using shared buffer
|
|
60
|
+
frame_buffer = decode_frames(args.file, timestamps=timestamps)
|
|
61
|
+
|
|
62
|
+
# Extract OCR using shared frame buffer
|
|
63
|
+
result = extract_ocr(
|
|
64
|
+
args.file,
|
|
65
|
+
frame_buffer=frame_buffer,
|
|
66
|
+
min_confidence=args.min_confidence,
|
|
67
|
+
skip_prefilter=args.skip_prefilter,
|
|
68
|
+
languages=languages,
|
|
69
|
+
)
|
|
70
|
+
elapsed = time.perf_counter() - start_time
|
|
71
|
+
|
|
72
|
+
if args.json:
|
|
73
|
+
output = result.model_dump()
|
|
74
|
+
output["elapsed_seconds"] = round(elapsed, 2)
|
|
75
|
+
print(json.dumps(output, indent=2, default=str))
|
|
76
|
+
else:
|
|
77
|
+
print(f"File: {args.file}")
|
|
78
|
+
print(f"Text regions detected: {len(result.detections)}")
|
|
79
|
+
print()
|
|
80
|
+
for i, det in enumerate(result.detections[:20], 1): # Show first 20
|
|
81
|
+
print(f' {i}: t={det.timestamp:.2f}s "{det.text}" (conf={det.confidence:.2f})')
|
|
82
|
+
if len(result.detections) > 20:
|
|
83
|
+
print(f" ... and {len(result.detections) - 20} more")
|
|
84
|
+
print()
|
|
85
|
+
print(f"Elapsed: {elapsed:.2f}s")
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
cli/scenes.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Detect scene boundaries in video file."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from media_engine.extractors import extract_scenes
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(description="Detect scene boundaries in video")
|
|
15
|
+
parser.add_argument("file", help="Path to video file")
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--threshold",
|
|
18
|
+
type=float,
|
|
19
|
+
default=27.0,
|
|
20
|
+
help="Content detection threshold (lower=more sensitive, default: 27.0)",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
23
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
24
|
+
|
|
25
|
+
args = parser.parse_args()
|
|
26
|
+
|
|
27
|
+
if args.verbose:
|
|
28
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
29
|
+
else:
|
|
30
|
+
logging.basicConfig(level=logging.WARNING)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
start_time = time.perf_counter()
|
|
34
|
+
result = extract_scenes(args.file, threshold=args.threshold)
|
|
35
|
+
elapsed = time.perf_counter() - start_time
|
|
36
|
+
|
|
37
|
+
if args.json:
|
|
38
|
+
output = result.model_dump()
|
|
39
|
+
output["elapsed_seconds"] = round(elapsed, 2)
|
|
40
|
+
print(json.dumps(output, indent=2, default=str))
|
|
41
|
+
else:
|
|
42
|
+
print(f"File: {args.file}")
|
|
43
|
+
print(f"Scenes detected: {result.count}")
|
|
44
|
+
print()
|
|
45
|
+
for i, scene in enumerate(result.detections, 1):
|
|
46
|
+
duration = scene.end - scene.start
|
|
47
|
+
print(f" Scene {i}: {scene.start:.2f}s - {scene.end:.2f}s ({duration:.2f}s)")
|
|
48
|
+
print()
|
|
49
|
+
print(f"Elapsed: {elapsed:.2f}s")
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|
cli/telemetry.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract telemetry (GPS/flight data) from video file."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from media_engine.extractors import extract_telemetry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
parser = argparse.ArgumentParser(description="Extract telemetry from video file")
|
|
15
|
+
parser.add_argument("file", help="Path to video file")
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--gpx",
|
|
18
|
+
action="store_true",
|
|
19
|
+
help="Output as GPX format instead of JSON",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
22
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
23
|
+
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
if args.verbose:
|
|
27
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
28
|
+
else:
|
|
29
|
+
logging.basicConfig(level=logging.WARNING)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
start_time = time.perf_counter()
|
|
33
|
+
result = extract_telemetry(args.file)
|
|
34
|
+
elapsed = time.perf_counter() - start_time
|
|
35
|
+
|
|
36
|
+
if result is None:
|
|
37
|
+
print("No telemetry data found", file=sys.stderr)
|
|
38
|
+
sys.exit(0)
|
|
39
|
+
|
|
40
|
+
if args.gpx:
|
|
41
|
+
print(result.to_gpx())
|
|
42
|
+
elif args.json:
|
|
43
|
+
output = result.model_dump()
|
|
44
|
+
output["elapsed_seconds"] = round(elapsed, 2)
|
|
45
|
+
print(json.dumps(output, indent=2, default=str))
|
|
46
|
+
else:
|
|
47
|
+
print(f"File: {args.file}")
|
|
48
|
+
print(f"Source: {result.source}")
|
|
49
|
+
print(f"Points: {len(result.points)}")
|
|
50
|
+
print()
|
|
51
|
+
for i, pt in enumerate(result.points[:10], 1): # Show first 10
|
|
52
|
+
alt = f" alt={pt.altitude:.1f}m" if pt.altitude else ""
|
|
53
|
+
print(f" {i}: ({pt.latitude:.6f}, {pt.longitude:.6f}){alt}")
|
|
54
|
+
if len(result.points) > 10:
|
|
55
|
+
print(f" ... and {len(result.points) - 10} more")
|
|
56
|
+
print()
|
|
57
|
+
print(f"Elapsed: {elapsed:.2f}s")
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|