distant-frames 0.1.0__tar.gz → 0.2.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: distant-frames
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Smart video frame extraction tool
5
5
  Project-URL: Homepage, https://github.com/yubraaj11/distant-frames
6
6
  Project-URL: Repository, https://github.com/yubraaj11/distant-frames
@@ -18,6 +18,7 @@ Classifier: Topic :: Multimedia :: Video
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Requires-Python: >=3.12
20
20
  Requires-Dist: opencv-python>=4.10.0
21
+ Requires-Dist: typer>=0.9.0
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
  # Distant Frames
@@ -0,0 +1,83 @@
1
+ # Release Notes
2
+
3
+ ## Version 0.2.0 (Upcoming)
4
+
5
+ ### 🎨 Enhanced CLI Interface
6
+
7
+ We've completely redesigned the command-line interface using **Typer** for a significantly improved user experience!
8
+
9
+ #### ✨ New Features
10
+
11
+ - **Rich Terminal Output**: Beautiful, formatted help text with tables and color-coded sections
12
+ - **Short Option Flags**: Added convenient shortcuts:
13
+ - `-o` for `--output`
14
+ - `-t` for `--threshold`
15
+ - **Automatic Validation**: Built-in input validation with helpful error messages
16
+ - File existence checking before processing
17
+ - Threshold range validation (0.0-1.0)
18
+ - Readable file verification
19
+
20
+ #### 🔧 Improvements
21
+
22
+ - **Better Help Messages**: Clear, comprehensive help text with detailed parameter descriptions
23
+ - **Type Safety**: Full type hints for all CLI parameters with automatic validation
24
+ - **Enhanced Error Handling**: User-friendly error messages with suggestions and proper formatting
25
+ - **Cleaner Code**: More maintainable and declarative CLI implementation
26
+
27
+ #### 📦 Dependencies
28
+
29
+ - Added `typer>=0.9.0` for improved CLI functionality
30
+
31
+ #### 🎯 Usage Examples
32
+
33
+ **Basic usage:**
34
+ ```bash
35
+ distant-frames video.mp4
36
+ ```
37
+
38
+ **With custom output directory:**
39
+ ```bash
40
+ distant-frames video.mp4 -o my_frames
41
+ ```
42
+
43
+ **With custom threshold:**
44
+ ```bash
45
+ distant-frames video.mp4 -t 0.8
46
+ ```
47
+
48
+ **Combined options:**
49
+ ```bash
50
+ distant-frames video.mp4 -o output_frames -t 0.7
51
+ ```
52
+
53
+ **View help:**
54
+ ```bash
55
+ distant-frames --help
56
+ ```
57
+
58
+ #### 🐛 Bug Fixes
59
+
60
+ - Improved error messages when video file is not found
61
+ - Better validation for threshold parameter values
62
+
63
+ #### ⚠️ Breaking Changes
64
+
65
+ None - the CLI interface remains backward compatible with existing usage patterns.
66
+
67
+ ---
68
+
69
+ ## Version 0.1.2
70
+
71
+ ### Features
72
+
73
+ - Smart frame extraction with similarity-based deduplication
74
+ - Fallback mechanism for gradual scene changes
75
+ - Verbose logging for debugging
76
+ - HSV-based histogram comparison for robust similarity detection
77
+
78
+ ### Core Functionality
79
+
80
+ - Extract frames at 1-second intervals
81
+ - Skip similar frames based on configurable threshold
82
+ - Automatic output directory creation
83
+ - Detailed frame-by-frame logging
@@ -0,0 +1,49 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from typing_extensions import Annotated
4
+ from distant_frames.core import extract_frames
5
+
6
+ app = typer.Typer(
7
+ help="Smart video frame extraction tool with similarity-based deduplication.",
8
+ add_completion=False,
9
+ )
10
+
11
+ @app.command()
12
+ def main(
13
+ video_path: Annotated[
14
+ Path,
15
+ typer.Argument(
16
+ help="Path to the input video file",
17
+ exists=True,
18
+ file_okay=True,
19
+ dir_okay=False,
20
+ readable=True,
21
+ )
22
+ ],
23
+ output: Annotated[
24
+ str,
25
+ typer.Option(
26
+ "--output", "-o",
27
+ help="Output directory for extracted frames"
28
+ )
29
+ ] = "extracted_frames",
30
+ threshold: Annotated[
31
+ float,
32
+ typer.Option(
33
+ "--threshold", "-t",
34
+ min=0.0,
35
+ max=1.0,
36
+ help="Similarity threshold (0.0-1.0). Higher values mean stricter deduplication (fewer frames saved)."
37
+ )
38
+ ] = 0.65,
39
+ ):
40
+ """
41
+ Extract distinct frames from a video file based on visual similarity.
42
+
43
+ The tool samples the video at 1-second intervals and compares consecutive frames.
44
+ Frames that are too similar to previously saved frames are automatically skipped.
45
+ """
46
+ extract_frames(str(video_path), output, threshold)
47
+
48
+ if __name__ == "__main__":
49
+ app()
@@ -32,7 +32,7 @@ def calculate_similarity(frame1, frame2):
32
32
  similarity = cv2.compareHist(hist1, hist2, cv2.HISTCMP_CORREL)
33
33
  return similarity
34
34
 
35
- def extract_frames(video_path, output_folder, threshold=0.9):
35
+ def extract_frames(video_path, output_folder, threshold=0.65):
36
36
  """Extracts distinct frames from a video file based on visual similarity.
37
37
 
38
38
  The function samples the video at 1-second intervals. It compares the current
@@ -50,7 +50,7 @@ def extract_frames(video_path, output_folder, threshold=0.9):
50
50
  threshold (float, optional): Similarity threshold (0.0 to 1.0).
51
51
  Frames with similarity higher than this value regarding the last
52
52
  saved frame will be dropped. Higher values mean stricter
53
- deduplication (fewer frames saved). Defaults to 0.9.
53
+ deduplication (fewer frames saved). Defaults to 0.65.
54
54
 
55
55
  Returns:
56
56
  None
@@ -80,8 +80,9 @@ def extract_frames(video_path, output_folder, threshold=0.9):
80
80
  current_frame_idx = 0
81
81
  saved_count = 0
82
82
  last_saved_frame = None
83
- prev_saved_frame = None
84
- force_fallback_to_prev = False
83
+ last_saved_timestamp = None
84
+ skip_reference_frame = None
85
+ skip_reference_timestamp = None
85
86
 
86
87
  while True:
87
88
  # Set position to the next second
@@ -98,35 +99,44 @@ def extract_frames(video_path, output_folder, threshold=0.9):
98
99
  if last_saved_frame is None:
99
100
  should_save = True
100
101
  similarity = 0.0 # No previous frame
102
+ print(f"[{timestamp:.1f}s] First frame → SAVE")
101
103
  else:
102
104
  # Determine reference frame
103
- reference_frame = last_saved_frame
104
- if force_fallback_to_prev and prev_saved_frame is not None:
105
- reference_frame = prev_saved_frame
106
- # We used fallback, so reset the flag for the next check
107
- # (unless we skip again, which re-sets it below)
108
- force_fallback_to_prev = False
105
+ # If we have a skip reference (from previous skip), use that
106
+ # Otherwise use the last saved frame
107
+ if skip_reference_frame is not None:
108
+ reference_frame = skip_reference_frame
109
+ ref_timestamp = skip_reference_timestamp
110
+ ref_label = "skip_ref"
111
+ else:
112
+ reference_frame = last_saved_frame
113
+ ref_timestamp = last_saved_timestamp
114
+ ref_label = "last"
109
115
 
110
116
  similarity = calculate_similarity(reference_frame, frame)
117
+
111
118
  if similarity < threshold:
112
119
  should_save = True
120
+ print(f"[{timestamp:.1f}s] vs {ref_label}@{ref_timestamp:.1f}s | sim={similarity:.3f} → SAVE")
121
+ # Clear skip reference when we save
122
+ skip_reference_frame = None
123
+ skip_reference_timestamp = None
113
124
  else:
114
- print(f"Skipping frame at {timestamp:.2f}s (Similarity: {similarity:.4f})")
115
- force_fallback_to_prev = True
125
+ print(f"[{timestamp:.1f}s] vs {ref_label}@{ref_timestamp:.1f}s | sim={similarity:.3f} → SKIP")
126
+ # When we skip, set the skip reference to the last saved frame
127
+ # so next comparison uses this same reference
128
+ skip_reference_frame = last_saved_frame
129
+ skip_reference_timestamp = last_saved_timestamp
116
130
 
117
131
  if should_save:
118
132
  output_filename = os.path.join(output_folder, f"frame_{timestamp:.2f}.jpg")
119
133
  cv2.imwrite(output_filename, frame)
120
134
 
121
- # Update history
122
- prev_saved_frame = last_saved_frame
135
+ # Update last saved frame
123
136
  last_saved_frame = frame
124
-
125
- # Reset fallback flag because we just saved a new distinct frame
126
- force_fallback_to_prev = False
137
+ last_saved_timestamp = timestamp
127
138
 
128
139
  saved_count += 1
129
- print(f"Saved {output_filename} (Similarity: {similarity:.4f})")
130
140
 
131
141
  current_frame_idx += frame_interval
132
142
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "distant-frames"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Smart video frame extraction tool"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -20,6 +20,7 @@ classifiers = [
20
20
  ]
21
21
  dependencies = [
22
22
  "opencv-python>=4.10.0",
23
+ "typer>=0.9.0",
23
24
  ]
24
25
 
25
26
  [project.urls]
@@ -28,7 +29,7 @@ Repository = "https://github.com/yubraaj11/distant-frames"
28
29
  Issues = "https://github.com/yubraaj11/distant-frames/issues"
29
30
 
30
31
  [project.scripts]
31
- distant-frames = "distant_frames.cli:main"
32
+ distant-frames = "distant_frames.cli:app"
32
33
 
33
34
  [build-system]
34
35
  requires = ["hatchling"]
@@ -2,16 +2,62 @@ version = 1
2
2
  revision = 3
3
3
  requires-python = ">=3.12"
4
4
 
5
+ [[package]]
6
+ name = "click"
7
+ version = "8.3.1"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "colorama", marker = "sys_platform == 'win32'" },
11
+ ]
12
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
13
+ wheels = [
14
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
15
+ ]
16
+
17
+ [[package]]
18
+ name = "colorama"
19
+ version = "0.4.6"
20
+ source = { registry = "https://pypi.org/simple" }
21
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
24
+ ]
25
+
5
26
  [[package]]
6
27
  name = "distant-frames"
7
- version = "0.1.0"
28
+ version = "0.2.0"
8
29
  source = { editable = "." }
9
30
  dependencies = [
10
31
  { name = "opencv-python" },
32
+ { name = "typer" },
11
33
  ]
12
34
 
13
35
  [package.metadata]
14
- requires-dist = [{ name = "opencv-python", specifier = ">=4.10.0" }]
36
+ requires-dist = [
37
+ { name = "opencv-python", specifier = ">=4.10.0" },
38
+ { name = "typer", specifier = ">=0.9.0" },
39
+ ]
40
+
41
+ [[package]]
42
+ name = "markdown-it-py"
43
+ version = "4.0.0"
44
+ source = { registry = "https://pypi.org/simple" }
45
+ dependencies = [
46
+ { name = "mdurl" },
47
+ ]
48
+ sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
51
+ ]
52
+
53
+ [[package]]
54
+ name = "mdurl"
55
+ version = "0.1.2"
56
+ source = { registry = "https://pypi.org/simple" }
57
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
58
+ wheels = [
59
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
60
+ ]
15
61
 
16
62
  [[package]]
17
63
  name = "numpy"
@@ -67,3 +113,58 @@ wheels = [
67
113
  { url = "https://files.pythonhosted.org/packages/02/96/213fea371d3cb2f1d537612a105792aa0a6659fb2665b22cad709a75bd94/opencv_python-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:ff554d3f725b39878ac6a2e1fa232ec509c36130927afc18a1719ebf4fbf4357", size = 30284131, upload-time = "2025-07-07T09:14:08.819Z" },
68
114
  { url = "https://files.pythonhosted.org/packages/fa/80/eb88edc2e2b11cd2dd2e56f1c80b5784d11d6e6b7f04a1145df64df40065/opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2", size = 39000307, upload-time = "2025-07-07T09:14:16.641Z" },
69
115
  ]
116
+
117
+ [[package]]
118
+ name = "pygments"
119
+ version = "2.19.2"
120
+ source = { registry = "https://pypi.org/simple" }
121
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
122
+ wheels = [
123
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
124
+ ]
125
+
126
+ [[package]]
127
+ name = "rich"
128
+ version = "14.2.0"
129
+ source = { registry = "https://pypi.org/simple" }
130
+ dependencies = [
131
+ { name = "markdown-it-py" },
132
+ { name = "pygments" },
133
+ ]
134
+ sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
135
+ wheels = [
136
+ { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
137
+ ]
138
+
139
+ [[package]]
140
+ name = "shellingham"
141
+ version = "1.5.4"
142
+ source = { registry = "https://pypi.org/simple" }
143
+ sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
144
+ wheels = [
145
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
146
+ ]
147
+
148
+ [[package]]
149
+ name = "typer"
150
+ version = "0.20.1"
151
+ source = { registry = "https://pypi.org/simple" }
152
+ dependencies = [
153
+ { name = "click" },
154
+ { name = "rich" },
155
+ { name = "shellingham" },
156
+ { name = "typing-extensions" },
157
+ ]
158
+ sdist = { url = "https://files.pythonhosted.org/packages/6d/c1/933d30fd7a123ed981e2a1eedafceab63cb379db0402e438a13bc51bbb15/typer-0.20.1.tar.gz", hash = "sha256:68585eb1b01203689c4199bc440d6be616f0851e9f0eb41e4a778845c5a0fd5b", size = 105968, upload-time = "2025-12-19T16:48:56.302Z" }
159
+ wheels = [
160
+ { url = "https://files.pythonhosted.org/packages/c8/52/1f2df7e7d1be3d65ddc2936d820d4a3d9777a54f4204f5ca46b8513eff77/typer-0.20.1-py3-none-any.whl", hash = "sha256:4b3bde918a67c8e03d861aa02deca90a95bbac572e71b1b9be56ff49affdb5a8", size = 47381, upload-time = "2025-12-19T16:48:53.679Z" },
161
+ ]
162
+
163
+ [[package]]
164
+ name = "typing-extensions"
165
+ version = "4.15.0"
166
+ source = { registry = "https://pypi.org/simple" }
167
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
168
+ wheels = [
169
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
170
+ ]
@@ -1,9 +0,0 @@
1
- import sys
2
- import os
3
- print(f"CWD: {os.getcwd()}")
4
- sys.path.insert(0, os.path.abspath("src"))
5
- try:
6
- import distant_frames
7
- print(f"Imported from: {distant_frames.__file__}")
8
- except ImportError as e:
9
- print(f"Import failed: {e}")
@@ -1,15 +0,0 @@
1
- import argparse
2
- from distant_frames.core import extract_frames
3
-
4
- def main():
5
- parser = argparse.ArgumentParser(description="Extract frames from video with similarity check.")
6
- parser.add_argument("video_path", help="Path to the input video file")
7
- parser.add_argument("--output", default="extracted_frames", help="Output directory for frames")
8
- parser.add_argument("--threshold", type=float, default=0.65, help="Similarity threshold (0-1). Higher value means stricter similarity check (more frames dropped).")
9
-
10
- args = parser.parse_args()
11
-
12
- extract_frames(args.video_path, args.output, args.threshold)
13
-
14
- if __name__ == "__main__":
15
- main()
File without changes
File without changes