Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ video_sampler hash "https://www.youtube.com/watch?v=GbpP3Sxp-1U&list=PLFezMcAw96
--hash-size 3 --buffer-size 20 --ytdlp --keywords "cat,dog,another keyword,test keyword"
```

- segment based on audio transcription

```bash
video_sampler hash some_video.mp4 ./frames/ \
--extractor "audio_keyword" --keywords "alert,bell,voice"
```

The videos are never directly downloaded, only streamed, so you can use it to sample videos from the internet without downloading them first.

##### Extra YT-DLP options
Expand Down
11 changes: 11 additions & 0 deletions tests/test_audio_extractor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sys

import pytest

from video_sampler.language.keyword_capture import AudioKeywordExtractor


def test_audio_extractor_requires_whisper(monkeypatch):
monkeypatch.setitem(sys.modules, "whisper", None)
with pytest.raises(ImportError):
Comment thread
LemurPwned marked this conversation as resolved.
AudioKeywordExtractor(["test"])
6 changes: 6 additions & 0 deletions video_sampler/gating.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@


def create_model(model_name: str):
if "open_clip" not in globals() or "torch" not in globals():
raise ImportError(
"open_clip and torch are required for the clip gate. "
"Install them to use this feature."
)
Comment thread
LemurPwned marked this conversation as resolved.
Outdated

model, _, preprocess = open_clip.create_model_and_transforms(
model_name, pretrained="laion2b_s34b_b79k"
)
Expand Down
3 changes: 3 additions & 0 deletions video_sampler/language/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .keyword_capture import AudioKeywordExtractor, KeywordExtractor

__all__ = ["KeywordExtractor", "AudioKeywordExtractor"]
42 changes: 30 additions & 12 deletions video_sampler/language/keyword_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,7 @@ def __init__(self, keywords: list[str]) -> None:
def generate_segments(
self, subtitle_list: list[tuple[tuple[int, int], str]]
) -> Iterable[subtitle_line]:
"""
Captures keyword segments from a list of subtitles.

Args:
subtitle_list (list[tuple[tuple[int, int], str]]): List of subtitles in the format
(start_time, end_time, content).

Yields:
subtitle_line: A named tuple representing a keyword segment in the format
(start_time, end_time, lemma, content).

"""
"""Capture segments containing the configured keywords."""
for (start_time, end_time), content in subtitle_list:
doc = self.nlp(content.lower())
for lemma in doc:
Expand All @@ -105,8 +94,37 @@ def generate_segments(
break


class AudioKeywordExtractor(KeywordExtractor):
"""Extract keywords from an audio file by transcribing it first."""

def __init__(self, keywords: list[str], model: str = "base") -> None:
super().__init__(keywords)
try:
import whisper
except ImportError as e: # pragma: no cover - optional dependency
raise ImportError(
"To use this feature install whisper by 'pip install openai-whisper'"
) from e
self.whisper = whisper.load_model(model)
Comment thread
LemurPwned marked this conversation as resolved.

def transcribe(self, audio_path: str) -> list[tuple[tuple[int, int], str]]:
result = self.whisper.transcribe(audio_path)
subtitle_list: list[tuple[tuple[int, int], str]] = []
for seg in result.get("segments", []):
subtitle_list.append(
((int(seg["start"] * 1000), int(seg["end"] * 1000)), seg["text"])
)
Comment on lines +113 to +116

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Trim whitespace from Whisper segment text

Call .strip() on seg['text'] to remove extra whitespace and prevent matching issues.

Suggested change
for seg in result.get("segments", []):
subtitle_list.append(
((int(seg["start"] * 1000), int(seg["end"] * 1000)), seg["text"])
)
for seg in result.get("segments", []):
subtitle_list.append(
((int(seg["start"] * 1000), int(seg["end"] * 1000)), seg["text"].strip())
)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not change the indents

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point—let’s keep the existing indent structure and just call .strip() on the text. For example, if your current code looks like this:

for seg in result.get("segments", []):
    subtitle_list.append(
        ((int(seg["start"] * 1000), int(seg["end"] * 1000)),
         seg["text"])
    )

you can change it to:

for seg in result.get("segments", []):
    subtitle_list.append(
        ((int(seg["start"] * 1000), int(seg["end"] * 1000)),
         seg["text"].strip())
    )

This way you only trim the whitespace and don’t alter the indentation levels. Let me know if that aligns with your style rules!

Comment on lines +112 to +116

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Convert for loop into list comprehension (list-comprehension)

Suggested change
subtitle_list: list[tuple[tuple[int, int], str]] = []
for seg in result.get("segments", []):
subtitle_list.append(
((int(seg["start"] * 1000), int(seg["end"] * 1000)), seg["text"])
)
subtitle_list: list[tuple[tuple[int, int], str]] = [
((int(seg["start"] * 1000), int(seg["end"] * 1000)), seg["text"])
for seg in result.get("segments", [])
]

return subtitle_list

def generate_segments_from_audio(self, audio_path: str) -> Iterable[subtitle_line]:
subtitle_list = self.transcribe(audio_path)
yield from super().generate_segments(subtitle_list)


def create_extractor(config: dict):
if config["type"] == "keyword":
return KeywordExtractor(**config["args"])
if config["type"] == "audio_keyword":
return AudioKeywordExtractor(**config["args"])

raise NotImplementedError(f"{config['type']} not implemented yet")