Skip to content

Commit 4cde800

Browse files
satraclaude
andauthored
Fix docs review: ffmpeg guard, cleanup env, GPU column (#481)
* Fix docs review: ffmpeg guard, remove environment, GPU column - video/input_output.py: proper ffmpeg guard with _FFMPEG_AVAILABLE flag and ModuleNotFoundError (was silently None) - docs-preview.yaml: remove deployment environment (needs manual approval), use PR comment with create-or-update-comment instead - tutorials/README.md: GPU column corrected for transcription and speech representations tutorials Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Restore deployment environment for docs preview Use a single 'docs-preview' environment (pre-created via API with no protection rules) instead of per-PR environments. The single environment only needs to exist once and all PRs reuse it with different URLs. Removed PR comment approach — the deployment URL shows in the checks UI as "View deployment". Also removed cleanup comment posting — the deployment status in the checks UI handles this naturally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Revert ffmpeg-python from video extras, improve error message ffmpeg-python is a CLI wrapper (separate from torchcodec's shared libs). It's not a core video dependency — keep it as an optional install with a clear error message pointing to both the Python package and system FFmpeg requirement. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Replace ffmpeg-python CLI with PyAV for video audio extraction extract_audios_from_local_videos now uses av (PyAV), which is already a video dependency, instead of ffmpeg-python (CLI wrapper). PyAV wraps FFmpeg shared libraries directly — no CLI binary needed. Eliminates the ffmpeg-python dependency entirely. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5e8b76b commit 4cde800

3 files changed

Lines changed: 48 additions & 32 deletions

File tree

.github/workflows/docs-preview.yaml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
if: github.event.action != 'closed'
1515
runs-on: ubuntu-latest
1616
environment:
17-
name: docs-preview-pr-${{ github.event.pull_request.number }}
17+
name: docs-preview
1818
url: https://sensein.group/senselab/pr-${{ github.event.pull_request.number }}/
1919
steps:
2020
- uses: actions/checkout@v5
@@ -60,8 +60,3 @@ jobs:
6060
git commit -m "Remove docs preview for PR #${PR_NUM}"
6161
git push origin docs
6262
fi
63-
- name: Delete environment
64-
uses: strumwolf/delete-deployment-environment@v3
65-
with:
66-
token: ${{ secrets.GITHUB_TOKEN }}
67-
environment: docs-preview-pr-${{ github.event.pull_request.number }}

src/senselab/video/tasks/input_output.py

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
from pathlib import Path
66
from typing import Any, Dict, List, Union
77

8-
try:
9-
import ffmpeg
10-
except ImportError:
11-
ffmpeg = None # type: ignore[assignment]
8+
import av
9+
import numpy as np
10+
import soundfile as sf
1211

13-
# import shutil
1412
from senselab.utils.data_structures import from_strings_to_files, get_common_directory
1513
from senselab.utils.tasks.input_output import read_files_from_disk
1614

@@ -20,40 +18,63 @@ def extract_audios_from_local_videos(
2018
audio_format: str = "wav",
2119
acodec: str = "pcm_s16le",
2220
) -> Dict[str, Any]:
23-
"""Read files from disk and create a Hugging Face `Dataset` object."""
21+
"""Extract audio tracks from video files and return as a dataset.
2422
25-
def _extract_audio_from_local_video(video_path: Path, output_audio_path: str, format: str, acodec: str) -> None:
26-
"""Extract audio from a video file."""
27-
try:
28-
# Input stream configuration
29-
input_stream = ffmpeg.input(video_path)
23+
Uses PyAV (av) to decode the audio stream from each video file,
24+
then writes the raw audio to disk in the requested format.
3025
31-
# Audio extraction configuration
32-
audio_stream = input_stream.audio.output(output_audio_path, format=format, acodec=acodec)
26+
Args:
27+
files: Path(s) to video files.
28+
audio_format: Output audio format (default: wav).
29+
acodec: Audio codec hint (default: pcm_s16le). Used to select
30+
output sample format (16-bit signed int for pcm_s16le).
3331
34-
# Execute ffmpeg command
35-
ffmpeg.run(audio_stream, overwrite_output=True)
32+
Returns:
33+
A dataset dict of the extracted audio files.
34+
"""
3635

37-
except ffmpeg.Error as e:
38-
print("An error occurred while extracting audio:", str(e))
36+
def _extract_audio_from_local_video(video_path: Path, output_audio_path: str, fmt: str, codec: str) -> None:
37+
"""Extract audio from a video file using PyAV."""
38+
container = av.open(str(video_path))
39+
audio_stream = container.streams.audio[0]
40+
sample_rate = audio_stream.rate or 16000
41+
42+
frames = []
43+
for frame in container.decode(audio=0):
44+
arr = frame.to_ndarray()
45+
if arr.ndim > 1:
46+
arr = arr.mean(axis=0) # downmix to mono
47+
frames.append(arr)
48+
container.close()
49+
50+
if not frames:
51+
return
52+
53+
audio_data = np.concatenate(frames)
54+
55+
# Match codec hint to sample format
56+
if "s16" in codec:
57+
audio_data = (
58+
(audio_data * 32767).clip(-32768, 32767).astype(np.int16)
59+
if audio_data.dtype != np.int16
60+
else audio_data
61+
)
62+
63+
sf.write(output_audio_path, audio_data, sample_rate, format=fmt.upper())
3964

4065
if isinstance(files, str):
4166
files = [files]
4267
formatted_files = from_strings_to_files(files)
4368
common_path = get_common_directory(files)
4469

45-
# Create a temporary directory to hold the audio files
4670
temp_dir = tempfile.mkdtemp()
4771

4872
audio_files_paths = []
4973
for file in formatted_files:
5074
base_file_name = os.path.splitext(str(file.filepath).replace(common_path, ""))[0]
5175
output_audio_path = os.path.join(temp_dir, f"{base_file_name}.{audio_format}")
52-
_extract_audio_from_local_video(file.filepath, output_audio_path, format=audio_format, acodec=acodec)
76+
os.makedirs(os.path.dirname(output_audio_path), exist_ok=True)
77+
_extract_audio_from_local_video(file.filepath, output_audio_path, fmt=audio_format, codec=acodec)
5378
audio_files_paths.append(output_audio_path)
5479

55-
audio_dataset = read_files_from_disk(audio_files_paths)
56-
57-
# Clean up the temporary non empty directory
58-
# shutil.rmtree(temp_dir)
59-
return audio_dataset
80+
return read_files_from_disk(audio_files_paths)

tutorials/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ if not GPU_AVAILABLE:
7676
| voice_activity_detection | Optional | Yes* | Detect speech segments |
7777
| voice_cloning | Yes | No | Voice conversion |
7878
| audio_recording_and_acoustic_analysis | No | No | Recording and acoustic feature analysis |
79-
| transcription_and_phonemic_analysis | No | No | Transcription and phoneme-level analysis |
80-
| speech_representations_lab | Optional | No | Speech representation learning |
79+
| transcription_and_phonemic_analysis | Yes | No | Transcription and phoneme-level analysis |
80+
| speech_representations_lab | Yes | No | Acoustic vs articulatory speech representations |
8181

8282
*Requires accepting pyannote model terms on HuggingFace
8383

0 commit comments

Comments
 (0)