Skip to content

Commit 2ee0573

Browse files
committed
Fix ffprobe missing (issue #7) + add v0.12.x regression tests
extract_caption.py: - Add _resolve_ffmpeg() and _resolve_ffprobe() mirroring the downloader's pattern. When ffprobe isn't on PATH (common on macOS users who only have imageio-ffmpeg, which doesn't bundle ffprobe), _video_duration now falls back to parsing "Duration: HH:MM:SS.ss" from ffmpeg -i stderr. - imageio-ffmpeg is auto-installed on demand when neither binary is available. test/test_v0_12_fixes.py (new, 50 tests): - Regression tests for issues #4, #5, #6, #7. - ffmpeg/ffprobe resolver coverage (system PATH, imageio-ffmpeg fallback, auto-install, error paths). - _parse_ffmpeg_duration parser across valid/invalid inputs. - Live _video_duration cross-validation against sample video. - Panopto tool-ID resolution via /tabs (strategy A) and /external_tools (strategy B), including cache and malformed-ID handling. - Stream priority (SS > OBJECT > DV > unknown). - HLS detection with query strings (issue #5). - Module-level import invariants. Release: v0.12.9.
1 parent 65bf714 commit 2ee0573

3 files changed

Lines changed: 606 additions & 7 deletions

File tree

electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "auto-note",
3-
"version": "0.12.8",
3+
"version": "0.12.9",
44
"description": "AutoNote — lecture notes generator from Canvas recordings",
55
"homepage": "https://github.com/nodeeeeee/Auto-Note",
66
"author": {

extract_caption.py

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,113 @@ def save_manifest(manifest: dict) -> None:
123123
json.dump(manifest, f, indent=2)
124124

125125

126+
# ── ffmpeg/ffprobe resolution ─────────────────────────────────────────────────
127+
128+
def _resolve_ffmpeg() -> str:
129+
"""Locate an ffmpeg executable.
130+
131+
Order: system PATH → imageio-ffmpeg bundled binary → auto-install
132+
imageio-ffmpeg and retry. Raises RuntimeError if none are available.
133+
Mirrors downloader._resolve_ffmpeg for consistency.
134+
"""
135+
from shutil import which
136+
sys_ff = which("ffmpeg")
137+
if sys_ff:
138+
return sys_ff
139+
140+
def _try_imageio() -> str | None:
141+
try:
142+
import imageio_ffmpeg
143+
return imageio_ffmpeg.get_ffmpeg_exe()
144+
except ImportError:
145+
return None
146+
except Exception as e:
147+
print(f" [warn] imageio-ffmpeg get_ffmpeg_exe failed: {e}")
148+
return None
149+
150+
ff = _try_imageio()
151+
if ff:
152+
return ff
153+
154+
print(" ffmpeg not found locally — installing imageio-ffmpeg fallback…")
155+
try:
156+
subprocess.run(
157+
[sys.executable, "-m", "pip", "install", "--quiet",
158+
"--disable-pip-version-check", "imageio-ffmpeg"],
159+
check=True, timeout=180,
160+
)
161+
except Exception as e:
162+
raise RuntimeError(f"ffmpeg unavailable and auto-install failed: {e}") from e
163+
164+
ff = _try_imageio()
165+
if not ff:
166+
raise RuntimeError("ffmpeg unavailable after imageio-ffmpeg install")
167+
return ff
168+
169+
170+
def _resolve_ffprobe() -> str | None:
171+
"""Locate ffprobe if available on system PATH.
172+
173+
Returns None if ffprobe isn't installed — callers should fall back to
174+
parsing ffmpeg output. imageio-ffmpeg does not bundle ffprobe, so there
175+
is no Python-package fallback for it.
176+
"""
177+
from shutil import which
178+
return which("ffprobe")
179+
180+
126181
# ── OpenAI API helpers ────────────────────────────────────────────────────────
127182

183+
_DURATION_RE = None
184+
185+
186+
def _parse_ffmpeg_duration(stderr: str) -> float | None:
187+
"""Parse 'Duration: HH:MM:SS.ss' from ffmpeg -i stderr output."""
188+
global _DURATION_RE
189+
if _DURATION_RE is None:
190+
import re
191+
_DURATION_RE = re.compile(r"Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)")
192+
m = _DURATION_RE.search(stderr or "")
193+
if not m:
194+
return None
195+
h, mi, s = m.group(1), m.group(2), m.group(3)
196+
return int(h) * 3600 + int(mi) * 60 + float(s)
197+
198+
128199
def _video_duration(video_path: Path) -> float:
129-
"""Return video duration in seconds via ffprobe."""
200+
"""Return video duration in seconds.
201+
202+
Prefers ffprobe (faster, structured output), falls back to parsing
203+
ffmpeg's stderr when ffprobe isn't installed — which is common on
204+
macOS users who get ffmpeg via imageio-ffmpeg but have no separate
205+
ffprobe binary.
206+
"""
207+
ffprobe = _resolve_ffprobe()
208+
if ffprobe:
209+
result = subprocess.run(
210+
[ffprobe, "-v", "quiet", "-print_format", "json",
211+
"-show_format", str(video_path)],
212+
capture_output=True, text=True, check=True,
213+
creationflags=_SUBPROCESS_FLAGS,
214+
)
215+
return float(json.loads(result.stdout)["format"]["duration"])
216+
217+
# Fallback: ffmpeg prints "Duration: HH:MM:SS.ss" to stderr when
218+
# given -i with no output. Exit code is non-zero (no output file)
219+
# so we don't use check=True here.
220+
ff = _resolve_ffmpeg()
130221
result = subprocess.run(
131-
["ffprobe", "-v", "quiet", "-print_format", "json",
132-
"-show_format", str(video_path)],
133-
capture_output=True, text=True, check=True,
222+
[ff, "-hide_banner", "-i", str(video_path)],
223+
capture_output=True, text=True, check=False,
134224
creationflags=_SUBPROCESS_FLAGS,
135225
)
136-
return float(json.loads(result.stdout)["format"]["duration"])
226+
dur = _parse_ffmpeg_duration(result.stderr)
227+
if dur is None:
228+
raise RuntimeError(
229+
f"Could not determine duration of {video_path.name} — "
230+
f"ffmpeg output did not contain a Duration line."
231+
)
232+
return dur
137233

138234

139235
def _extract_audio(video_path: Path, out_path: Path,
@@ -144,7 +240,8 @@ def _extract_audio(video_path: Path, out_path: Path,
144240
When *desc* is given, a tqdm progress bar is shown via ffmpeg-progress-yield.
145241
*total_sec* (or *duration*) is used as the known duration for the bar.
146242
"""
147-
cmd = ["ffmpeg", "-y", "-i", str(video_path)]
243+
ff = _resolve_ffmpeg()
244+
cmd = [ff, "-y", "-i", str(video_path)]
148245
if start > 0:
149246
cmd += ["-ss", str(start)]
150247
if duration is not None:

0 commit comments

Comments
 (0)