Proiect descărcare + transcriere curs ATM (trading, Bogdan Jinga)
Pipeline audio-only (fără video pe disc) prin ffmpeg direct din URL CloudFront, transcriere cu faster-whisper (CPU), sumarizări structurate cu diagrame SVG la scară reală (STYLE.md documentează toate cerințele de format).
This commit is contained in:
113
extract_audio.py
Normal file
113
extract_audio.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
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 (for piloting)")
|
||||
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 mod["name"] not in CORE_MODULES:
|
||||
continue
|
||||
if args.only and mod["name"] != args.only:
|
||||
continue
|
||||
|
||||
mod_dir = out_dir / f"{mod_idx:02d}_{slugify(mod['name'])}"
|
||||
log.info(f"Module: {mod['name']} ({len(mod['lectures'])} lectures)")
|
||||
|
||||
for lec_idx, lec in enumerate(mod["lectures"], 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()
|
||||
Reference in New Issue
Block a user