extract_audio.py primeste --skip N, ca transele urmatoare sa nu reia lectiile deja luate: slice + enumerate de la skip+1, numerotarea fisierelor ramane cea globala. Audio-ul (862 MB) sters dupa verificare fisier cu fisier - fiecare mp3 avea transcriere completa, iar singurul fara era duplicatul confirmat _duplicate/02_08092026 (fingerprint 1.000 fata de 01_10092026). PROGRESS.md: sectiune noua 0bis cu ce ruleaza acum. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtSTyTmt6AbCL9j5ajEDm1
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""
|
|
Extract audio-only tracks for the core-curriculum lectures of the ATM course.
|
|
|
|
No video ever touches disk: ffmpeg reads the CloudFront MP4 URL directly over
|
|
HTTP and writes only the audio track (mono, 16kHz -> what whisper wants
|
|
anyway) to disk. Manifest-free; resumable by checking file existence.
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
from dotenv import load_dotenv
|
|
|
|
from download import login, get_course_id, get_menu, get_lecture_video_url, slugify
|
|
|
|
# First-phase scope: core curriculum only, not the 147 "ANALIZĂ ZILNICĂ"
|
|
# recaps / Q&A / live-room recordings (confirmed with user 2026-09-11).
|
|
CORE_MODULES = {
|
|
"MODUL 1", "MODUL 2", "MODUL 3", "MODUL 4", "MODUL 5", "MODUL 6",
|
|
"CATALIZATORI", "ANALIZĂ FUNDAMENTALĂ",
|
|
"MASTER ATM 1", "MASTER ATM 2", "MASTER ATM 3", "MASTER ATM 4",
|
|
}
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def extract_audio(url: str, dest: Path) -> bool:
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = dest.with_suffix(dest.suffix + ".tmp")
|
|
cmd = [
|
|
"ffmpeg", "-y", "-i", url,
|
|
"-vn", "-acodec", "libmp3lame", "-ar", "16000", "-ac", "1", "-b:a", "64k",
|
|
"-f", "mp3", str(tmp),
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
|
|
if result.returncode != 0 or not tmp.exists():
|
|
log.error(f" ffmpeg failed: {result.stderr[-1500:]}")
|
|
return False
|
|
tmp.rename(dest)
|
|
return True
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(description="Extract audio for ATM core-curriculum lectures")
|
|
p.add_argument("--out", default="audio", help="Output directory")
|
|
p.add_argument("--only", default=None, help="Limit to one module name (bypasses CORE_MODULES)")
|
|
p.add_argument("--lectures", type=int, default=None, help="With --only: stop after lecture N of that module")
|
|
p.add_argument("--skip", type=int, default=0, help="Skip the first N lectures (transa urmatoare)")
|
|
args = p.parse_args()
|
|
|
|
load_dotenv()
|
|
email = os.getenv("ATM_EMAIL", "")
|
|
password = os.getenv("ATM_PASSWORD", "")
|
|
if not email or not password:
|
|
log.error("Set ATM_EMAIL and ATM_PASSWORD in .env")
|
|
sys.exit(1)
|
|
|
|
out_dir = Path(args.out)
|
|
session = requests.Session()
|
|
session.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"})
|
|
|
|
log.info("Logging in...")
|
|
login(session, email, password)
|
|
course_id = get_course_id(session)
|
|
modules = get_menu(session, course_id)
|
|
|
|
done = skipped = failed = 0
|
|
for mod_idx, mod in enumerate(modules, 1):
|
|
if args.only:
|
|
if mod["name"] != args.only:
|
|
continue
|
|
elif mod["name"] not in CORE_MODULES:
|
|
continue
|
|
|
|
mod_dir = out_dir / f"{mod_idx:02d}_{slugify(mod['name'])}"
|
|
log.info(f"Module: {mod['name']} ({len(mod['lectures'])} lectures)")
|
|
|
|
lectures = mod["lectures"][args.skip : args.lectures]
|
|
for lec_idx, lec in enumerate(lectures, args.skip + 1):
|
|
stem = f"{lec_idx:02d}_{slugify(lec['name'])}"
|
|
dest = mod_dir / f"{stem}.mp3"
|
|
if dest.exists() and dest.stat().st_size > 10_000:
|
|
log.info(f" Skipping (exists): {dest.name}")
|
|
skipped += 1
|
|
continue
|
|
|
|
try:
|
|
video_url = get_lecture_video_url(session, lec["id"])
|
|
except requests.exceptions.HTTPError as e:
|
|
if e.response is not None and e.response.status_code == 403:
|
|
log.info(f" Skipping (not included in access): {lec['name']}")
|
|
continue
|
|
raise
|
|
if not video_url:
|
|
log.info(f" No video (text/quiz): {lec['name']}")
|
|
continue
|
|
|
|
log.info(f" Extracting: {lec['name']}")
|
|
if extract_audio(video_url, dest):
|
|
done += 1
|
|
else:
|
|
failed += 1
|
|
|
|
log.info("=" * 60)
|
|
log.info(f"Extracted {done}, skipped {skipped}, failed {failed}.")
|
|
if failed:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|