Spaces:
Sleeping
Sleeping
Download transcriber.py from heebin/YoutubeScript: direct link, hf CLI and curl.
- Browser
- Download file 19.6 kB
-
https://huggingface.co/spaces/heebin/YoutubeScript/resolve/97fa2d2624c19d91f458b508d823c9341d66ad4e/transcriber.py
- Command line
-
hf download hf://spaces/heebin/YoutubeScript@97fa2d2624c19d91f458b508d823c9341d66ad4e/transcriber.py
-
curl -L -o transcriber.py https://huggingface.co/spaces/heebin/YoutubeScript/resolve/97fa2d2624c19d91f458b508d823c9341d66ad4e/transcriber.py
19.6 kB
| """YouTube ์์์์ ์์ฑ์ ์ถ์ถํ๊ณ ํ ์คํธ๋ก ๋ณํํ๋ ํต์ฌ ๋ชจ๋.""" | |
| from __future__ import annotations | |
| import os | |
| import re | |
| import shutil | |
| import tempfile | |
| from typing import Callable, List, Optional | |
| from pathlib import Path | |
| from pytubefix import YouTube | |
| from pydub import AudioSegment | |
| def download_audio( | |
| url: str, | |
| output_dir: Optional[str] = None, | |
| on_progress: Optional[Callable] = None, | |
| ) -> dict: | |
| """YouTube URL์์ ์ค๋์ค๋ฅผ ๋ค์ด๋ก๋ํ๋ค.""" | |
| if output_dir is None: | |
| output_dir = tempfile.mkdtemp() | |
| def _notify(percent: int, detail: str): | |
| if on_progress: | |
| on_progress({"step": "download", "percent": percent, "detail": detail}) | |
| _notify(0, "์์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ค...") | |
| # pytubefix ๋ค์ด๋ก๋ ์งํ ์ฝ๋ฐฑ | |
| def _download_cb(stream, chunk, bytes_remaining): | |
| total = stream.filesize | |
| downloaded = total - bytes_remaining | |
| pct = min(int(downloaded / total * 80), 80) # 0~80%: ๋ค์ด๋ก๋ | |
| dl_mb = downloaded / (1024 * 1024) | |
| total_mb = total / (1024 * 1024) | |
| _notify(pct, f"๋ค์ด๋ก๋ ์ค... {dl_mb:.1f}MB / {total_mb:.1f}MB") | |
| yt = YouTube(url, on_progress_callback=_download_cb) | |
| title = yt.title | |
| duration = yt.length | |
| _notify(5, f"'{title}' ์ค๋์ค ์คํธ๋ฆผ ์ ํ ์ค...") | |
| audio_stream = yt.streams.filter(only_audio=True).order_by("abr").desc().first() | |
| if not audio_stream: | |
| raise RuntimeError("์ค๋์ค ์คํธ๋ฆผ์ ์ฐพ์ ์ ์์ต๋๋ค.") | |
| # ๋ค์ด๋ก๋ (m4a/webm) | |
| downloaded = audio_stream.download(output_path=output_dir) | |
| # mp3๋ก ๋ณํ (80~100%) | |
| _notify(85, "MP3๋ก ๋ณํ ์ค...") | |
| mp3_path = os.path.join(output_dir, Path(downloaded).stem + ".mp3") | |
| audio = AudioSegment.from_file(downloaded) | |
| audio.export(mp3_path, format="mp3", bitrate="192k") | |
| if downloaded != mp3_path and os.path.exists(downloaded): | |
| os.remove(downloaded) | |
| file_mb = os.path.getsize(mp3_path) / (1024 * 1024) | |
| _notify(100, f"๋ค์ด๋ก๋ ์๋ฃ ({file_mb:.1f}MB, {duration}์ด)") | |
| return {"audio_path": mp3_path, "title": title, "duration": duration} | |
| def download_video( | |
| url: str, | |
| output_dir: Optional[str] = None, | |
| on_progress: Optional[Callable] = None, | |
| max_resolution: int = 720, | |
| ) -> str: | |
| """ํคํ๋ ์ ์ถ์ถ์ ์ํด YouTube ์์์ ๋ค์ด๋ก๋ํ๋ค (720p ์ดํ).""" | |
| if output_dir is None: | |
| output_dir = tempfile.mkdtemp() | |
| def _notify(percent: int, detail: str): | |
| if on_progress: | |
| on_progress({"step": "download", "percent": percent, "detail": detail}) | |
| _notify(50, "ํคํ๋ ์์ฉ ์์ ๋ค์ด๋ก๋ ์ค...") | |
| def _download_cb(stream, chunk, bytes_remaining): | |
| total = stream.filesize | |
| downloaded = total - bytes_remaining | |
| pct = min(50 + int(downloaded / total * 45), 95) | |
| dl_mb = downloaded / (1024 * 1024) | |
| total_mb = total / (1024 * 1024) | |
| _notify(pct, f"์์ ๋ค์ด๋ก๋ ์ค... {dl_mb:.1f}MB / {total_mb:.1f}MB") | |
| yt = YouTube(url, on_progress_callback=_download_cb) | |
| # progressive ์คํธ๋ฆผ (์ค๋์ค+๋น๋์ค ํฉ๋ณธ, ์์ ํ์ผ) | |
| stream = ( | |
| yt.streams.filter(progressive=True, file_extension="mp4") | |
| .filter(res=f"{max_resolution}p") | |
| .first() | |
| ) | |
| if not stream: | |
| # ํด์๋ ์ ํ ์์ด ๊ฐ์ฅ ๋ฎ์ progressive ์๋ | |
| stream = ( | |
| yt.streams.filter(progressive=True, file_extension="mp4") | |
| .order_by("resolution") | |
| .first() | |
| ) | |
| if not stream: | |
| raise RuntimeError("์์ ์คํธ๋ฆผ์ ์ฐพ์ ์ ์์ต๋๋ค.") | |
| video_path = stream.download(output_path=output_dir, filename_prefix="video_") | |
| _notify(98, "์์ ๋ค์ด๋ก๋ ์๋ฃ") | |
| return video_path | |
| def extract_keyframes( | |
| video_path: str, | |
| output_dir: Optional[str] = None, | |
| method: str = "scene", | |
| interval_seconds: int = 30, | |
| max_frames: int = 50, | |
| on_progress: Optional[Callable] = None, | |
| ) -> List[dict]: | |
| """์์์์ ํคํ๋ ์์ ์ถ์ถํ๋ค (OpenCV). | |
| method="scene": ํ์คํ ๊ทธ๋จ ๋น๊ต๋ก ์ฅ๋ฉด ์ ํ ๊ฐ์ง. | |
| method="interval": N์ด ๊ฐ๊ฒฉ์ผ๋ก ์ถ์ถ. | |
| ๋ฐํ: [{"path": str, "timestamp": "MM:SS"}, ...] | |
| """ | |
| import cv2 | |
| def _notify(percent: int, detail: str): | |
| if on_progress: | |
| on_progress({"step": "keyframe_extract", "percent": percent, "detail": detail}) | |
| if output_dir is None: | |
| output_dir = tempfile.mkdtemp() | |
| os.makedirs(output_dir, exist_ok=True) | |
| cap = cv2.VideoCapture(video_path) | |
| if not cap.isOpened(): | |
| raise RuntimeError(f"์์ ํ์ผ์ ์ด ์ ์์ต๋๋ค: {video_path}") | |
| fps = cap.get(cv2.CAP_PROP_FPS) or 30.0 | |
| total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) | |
| total_seconds = total_frames / fps | |
| _notify(5, f"์์ ๋ถ์ ์์ ({total_seconds:.0f}์ด, {fps:.0f}fps)") | |
| keyframes = [] | |
| prev_hist = None | |
| frame_idx = 0 | |
| threshold = 0.6 # ์ฅ๋ฉด ์ ํ ๊ฐ์ง ์๊ณ๊ฐ | |
| while True: | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| current_seconds = frame_idx / fps | |
| pct = min(int(5 + (frame_idx / total_frames) * 85), 90) | |
| should_save = False | |
| if method == "scene": | |
| # HSV ํ์คํ ๊ทธ๋จ ๋น๊ต๋ก ์ฅ๋ฉด ์ ํ ๊ฐ์ง | |
| hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) | |
| hist = cv2.calcHist([hsv], [0, 1], None, [50, 60], [0, 180, 0, 256]) | |
| cv2.normalize(hist, hist, 0, 1, cv2.NORM_MINMAX) | |
| if prev_hist is not None: | |
| score = cv2.compareHist(prev_hist, hist, cv2.HISTCMP_CORREL) | |
| if score < threshold: | |
| should_save = True | |
| else: | |
| # ์ฒซ ํ๋ ์์ ํญ์ ์ ์ฅ | |
| should_save = True | |
| prev_hist = hist | |
| else: | |
| # interval ๋ชจ๋: N์ด ๊ฐ๊ฒฉ | |
| if frame_idx == 0 or (frame_idx % int(fps * interval_seconds) == 0): | |
| should_save = True | |
| if should_save and len(keyframes) < max_frames: | |
| mins = int(current_seconds // 60) | |
| secs = int(current_seconds % 60) | |
| timestamp = f"{mins:02d}:{secs:02d}" | |
| frame_path = os.path.join(output_dir, f"keyframe_{len(keyframes) + 1:03d}.jpg") | |
| cv2.imwrite(frame_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 85]) | |
| keyframes.append({"path": frame_path, "timestamp": timestamp}) | |
| if len(keyframes) % 5 == 0: | |
| _notify(pct, f"{len(keyframes)}๊ฐ ํคํ๋ ์ ์ถ์ถ๋จ ({timestamp})") | |
| if len(keyframes) >= max_frames: | |
| break | |
| frame_idx += 1 | |
| cap.release() | |
| _notify(100, f"ํคํ๋ ์ ์ถ์ถ ์๋ฃ: {len(keyframes)}๊ฐ") | |
| return keyframes | |
| def _split_audio(audio_path: str, max_size_mb: int = 24) -> List[str]: | |
| """์ค๋์ค ํ์ผ์ Whisper API ์ ํ(25MB)์ ๋ง๊ฒ ๋ถํ ํ๋ค.""" | |
| file_size = os.path.getsize(audio_path) | |
| max_bytes = max_size_mb * 1024 * 1024 | |
| if file_size <= max_bytes: | |
| return [audio_path] | |
| audio = AudioSegment.from_mp3(audio_path) | |
| total_ms = len(audio) | |
| num_chunks = (file_size // max_bytes) + 1 | |
| chunk_ms = total_ms // num_chunks | |
| chunks = [] | |
| base = Path(audio_path) | |
| for i in range(num_chunks): | |
| start = i * chunk_ms | |
| end = min((i + 1) * chunk_ms, total_ms) | |
| chunk = audio[start:end] | |
| chunk_path = str(base.parent / f"{base.stem}_part{i}{base.suffix}") | |
| chunk.export(chunk_path, format="mp3") | |
| chunks.append(chunk_path) | |
| return chunks | |
| def transcribe_api( | |
| audio_path: str, | |
| api_key: str, | |
| on_progress: Optional[Callable] = None, | |
| ) -> dict: | |
| """OpenAI Whisper API๋ก ์์ฑ์ ํ ์คํธ๋ก ๋ณํํ๋ค.""" | |
| from openai import OpenAI | |
| def _notify(percent: int, detail: str): | |
| if on_progress: | |
| on_progress({"step": "transcribe", "percent": percent, "detail": detail}) | |
| _notify(0, "์ค๋์ค ํ์ผ ๋ถ์ ์ค...") | |
| client = OpenAI(api_key=api_key) | |
| chunks = _split_audio(audio_path) | |
| total_chunks = len(chunks) | |
| all_segments = [] | |
| full_text_parts = [] | |
| detected_language = None | |
| time_offset = 0.0 | |
| if total_chunks > 1: | |
| _notify(5, f"ํ์ผ์ด ์ปค์ {total_chunks}๊ฐ๋ก ๋ถํ ํ์ฌ ์ฒ๋ฆฌํฉ๋๋ค") | |
| for i, chunk_path in enumerate(chunks): | |
| chunk_pct_start = int(i / total_chunks * 90) | |
| chunk_pct_end = int((i + 1) / total_chunks * 90) | |
| if total_chunks > 1: | |
| _notify(chunk_pct_start, f"์ฒญํฌ {i + 1}/{total_chunks} Whisper API ์ ์ก ์ค...") | |
| else: | |
| _notify(10, "Whisper API๋ก ์ ์ก ์ค...") | |
| with open(chunk_path, "rb") as f: | |
| response = client.audio.transcriptions.create( | |
| model="whisper-1", | |
| file=f, | |
| response_format="verbose_json", | |
| timestamp_granularities=["segment"], | |
| ) | |
| if detected_language is None: | |
| detected_language = response.language | |
| full_text_parts.append(response.text) | |
| if total_chunks > 1: | |
| _notify(chunk_pct_end, f"์ฒญํฌ {i + 1}/{total_chunks} ์๋ฃ") | |
| else: | |
| _notify(85, "์๋ต ์ฒ๋ฆฌ ์ค...") | |
| if response.segments: | |
| for seg in response.segments: | |
| _get = (lambda k: seg[k]) if isinstance(seg, dict) else (lambda k: getattr(seg, k)) | |
| all_segments.append( | |
| { | |
| "start": _get("start") + time_offset, | |
| "end": _get("end") + time_offset, | |
| "text": _get("text"), | |
| } | |
| ) | |
| if chunks[-1] != chunk_path: | |
| audio = AudioSegment.from_mp3(chunk_path) | |
| time_offset += len(audio) / 1000.0 | |
| # ๋ถํ ๋ ์์ ํ์ผ ์ ๋ฆฌ | |
| for chunk_path in chunks: | |
| if chunk_path != audio_path and os.path.exists(chunk_path): | |
| os.remove(chunk_path) | |
| _notify(100, f"๋ณํ ์๋ฃ (์ธ์ด: {detected_language}, ์ธ๊ทธ๋จผํธ: {len(all_segments)}๊ฐ)") | |
| return { | |
| "text": " ".join(full_text_parts), | |
| "segments": all_segments, | |
| "language": detected_language or "unknown", | |
| } | |
| def transcribe_local( | |
| audio_path: str, | |
| model_size: str = "base", | |
| on_progress: Optional[Callable] = None, | |
| ) -> dict: | |
| """๋ก์ปฌ Whisper ๋ชจ๋ธ๋ก ์์ฑ์ ํ ์คํธ๋ก ๋ณํํ๋ค.""" | |
| import whisper | |
| def _notify(percent: int, detail: str): | |
| if on_progress: | |
| on_progress({"step": "transcribe", "percent": percent, "detail": detail}) | |
| _notify(0, f"Whisper {model_size} ๋ชจ๋ธ ๋ก๋ฉ ์ค...") | |
| model = whisper.load_model(model_size) | |
| _notify(20, "์์ฑ ๋ถ์ ์ค... (์์ ๊ธธ์ด์ ๋ฐ๋ผ ์๊ฐ์ด ๊ฑธ๋ฆฝ๋๋ค)") | |
| result = model.transcribe(audio_path) | |
| _notify(90, "๊ฒฐ๊ณผ ์ ๋ฆฌ ์ค...") | |
| segments = [] | |
| for seg in result.get("segments", []): | |
| segments.append( | |
| {"start": seg["start"], "end": seg["end"], "text": seg["text"]} | |
| ) | |
| lang = result.get("language", "unknown") | |
| _notify(100, f"๋ณํ ์๋ฃ (์ธ์ด: {lang}, ์ธ๊ทธ๋จผํธ: {len(segments)}๊ฐ)") | |
| return { | |
| "text": result["text"], | |
| "segments": segments, | |
| "language": lang, | |
| } | |
| def _format_srt_time(seconds: float) -> str: | |
| """์ด๋ฅผ SRT ํ์์คํฌํ ํ์์ผ๋ก ๋ณํํ๋ค.""" | |
| hours = int(seconds // 3600) | |
| minutes = int((seconds % 3600) // 60) | |
| secs = int(seconds % 60) | |
| millis = int((seconds % 1) * 1000) | |
| return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}" | |
| def save_txt(result: dict, output_path: str) -> str: | |
| """ํ ์คํธ ํ์ผ๋ก ์ ์ฅํ๋ค.""" | |
| with open(output_path, "w", encoding="utf-8") as f: | |
| f.write(result["text"].strip()) | |
| return output_path | |
| def save_srt(result: dict, output_path: str) -> str: | |
| """SRT ์๋ง ํ์ผ๋ก ์ ์ฅํ๋ค.""" | |
| with open(output_path, "w", encoding="utf-8") as f: | |
| for i, seg in enumerate(result["segments"], 1): | |
| f.write(f"{i}\n") | |
| f.write( | |
| f"{_format_srt_time(seg['start'])} --> {_format_srt_time(seg['end'])}\n" | |
| ) | |
| f.write(f"{seg['text'].strip()}\n\n") | |
| return output_path | |
| def _sanitize_filename(title: str) -> str: | |
| """ํ์ผ๋ช ์ ์ฌ์ฉํ ์ ์๋ ๋ฌธ์๋ฅผ ์ ๊ฑฐํ๋ค.""" | |
| sanitized = re.sub(r'[<>:"/\\|?*]', "", title) | |
| sanitized = sanitized.strip(". ") | |
| return sanitized[:100] if sanitized else "untitled" | |
| def process_video( | |
| url: str, | |
| mode: str = "api", | |
| api_key: Optional[str] = None, | |
| model_size: str = "base", | |
| output_dir: str = "./output", | |
| formats: Optional[List[str]] = None, | |
| on_progress: Optional[Callable] = None, | |
| # LLM ์ต์ (์ ์ญ ๊ธฐ๋ณธ๊ฐ, ํ์ ํธํ) | |
| md_llm: Optional[str] = None, | |
| md_api_key: Optional[str] = None, | |
| md_ollama_model: str = "llama3.2", | |
| # ๋ฒ์ญ ์ต์ | |
| translate_lang: Optional[str] = None, | |
| # ๋จ๊ณ๋ณ LLM ์ค๋ฒ๋ผ์ด๋ | |
| format_llm: Optional[str] = None, | |
| format_api_key: Optional[str] = None, | |
| translate_llm: Optional[str] = None, | |
| translate_api_key: Optional[str] = None, | |
| keyframe_llm: Optional[str] = None, | |
| keyframe_api_key: Optional[str] = None, | |
| # ํคํ๋ ์ ์ต์ | |
| enable_keyframes: bool = False, | |
| keyframe_method: str = "scene", | |
| keyframe_interval: int = 30, | |
| keyframe_max_frames: int = 50, | |
| ) -> dict: | |
| """์ ์ฒด ํ์ดํ๋ผ์ธ: ๋ค์ด๋ก๋ โ (ํคํ๋ ์) โ ๋ณํ โ (AI ์ ๋ฆฌ) โ (๋ฒ์ญ) โ ์ ์ฅ.""" | |
| from formatter import make_llm_config, format_as_markdown, translate_text, analyze_keyframes | |
| if formats is None: | |
| formats = ["txt", "srt"] | |
| os.makedirs(output_dir, exist_ok=True) | |
| def progress(data): | |
| if on_progress: | |
| on_progress(data) | |
| # ๋จ๊ณ๋ณ LLM ์ค์ ๋น๋ | |
| llm_cfg = make_llm_config( | |
| global_llm=md_llm, global_api_key=md_api_key, | |
| global_ollama_model=md_ollama_model, | |
| format_llm=format_llm, format_api_key=format_api_key, | |
| translate_llm=translate_llm, translate_api_key=translate_api_key, | |
| keyframe_llm=keyframe_llm, keyframe_api_key=keyframe_api_key, | |
| ) | |
| # 1. ์ค๋์ค ๋ค์ด๋ก๋ | |
| audio_info = download_audio(url, on_progress=on_progress) | |
| audio_path = audio_info["audio_path"] | |
| title = audio_info["title"] | |
| video_path = None | |
| keyframe_dir = None | |
| try: | |
| # 2. ํคํ๋ ์์ฉ ์์ ๋ค์ด๋ก๋ (์ ํ ์) | |
| if enable_keyframes: | |
| video_path = download_video(url, on_progress=on_progress) | |
| # 3. ํคํ๋ ์ ์ถ์ถ (์ ํ ์) | |
| keyframe_descriptions = None | |
| if enable_keyframes and video_path: | |
| keyframe_dir = tempfile.mkdtemp() | |
| keyframes = extract_keyframes( | |
| video_path, output_dir=keyframe_dir, | |
| method=keyframe_method, | |
| interval_seconds=keyframe_interval, | |
| max_frames=keyframe_max_frames, | |
| on_progress=on_progress, | |
| ) | |
| # 4. ํคํ๋ ์ Vision LLM ๋ถ์ | |
| if keyframes: | |
| kf_cfg = llm_cfg["keyframe"] | |
| keyframe_descriptions = analyze_keyframes( | |
| keyframe_paths=keyframes, | |
| llm_provider=kf_cfg["llm"], | |
| api_key=kf_cfg["api_key"], | |
| on_progress=on_progress, | |
| ) | |
| # 5. ์์ฑ โ ํ ์คํธ ๋ณํ | |
| if mode == "api": | |
| if not api_key: | |
| raise ValueError("API ๋ชจ๋์์๋ OpenAI API ํค๊ฐ ํ์ํฉ๋๋ค.") | |
| result = transcribe_api(audio_path, api_key, on_progress=on_progress) | |
| else: | |
| result = transcribe_local(audio_path, model_size, on_progress=on_progress) | |
| # 6. AI ๋งํฌ๋ค์ด ์ ๋ฆฌ (MD ์ ํ ์) | |
| md_content = None | |
| fmt_cfg = llm_cfg["format"] | |
| if "md" in formats and fmt_cfg["llm"]: | |
| md_content = format_as_markdown( | |
| text=result["text"], | |
| title=title, | |
| llm_provider=fmt_cfg["llm"], | |
| api_key=fmt_cfg["api_key"], | |
| ollama_model=fmt_cfg["ollama_model"], | |
| on_progress=on_progress, | |
| keyframe_descriptions=keyframe_descriptions, | |
| ) | |
| # 7. ๋ฒ์ญ (์ ํ ์) | |
| translated_txt = None | |
| translated_md = None | |
| tr_cfg = llm_cfg["translate"] | |
| if translate_lang and tr_cfg["llm"]: | |
| translated_txt = translate_text( | |
| text=result["text"], | |
| target_lang=translate_lang, | |
| llm_provider=tr_cfg["llm"], | |
| api_key=tr_cfg["api_key"], | |
| ollama_model=tr_cfg["ollama_model"], | |
| on_progress=on_progress, | |
| ) | |
| if md_content: | |
| translated_md = translate_text( | |
| text=md_content, | |
| target_lang=translate_lang, | |
| llm_provider=tr_cfg["llm"], | |
| api_key=tr_cfg["api_key"], | |
| ollama_model=tr_cfg["ollama_model"], | |
| on_progress=on_progress, | |
| ) | |
| # 8. ํ์ผ ์ ์ฅ | |
| progress({"step": "save", "percent": 0, "detail": "ํ์ผ ์ ์ฅ ์ค..."}) | |
| safe_title = _sanitize_filename(title) | |
| saved_files = {} | |
| if "txt" in formats: | |
| txt_path = os.path.join(output_dir, f"{safe_title}.txt") | |
| save_txt(result, txt_path) | |
| saved_files["txt"] = txt_path | |
| if "srt" in formats: | |
| srt_path = os.path.join(output_dir, f"{safe_title}.srt") | |
| save_srt(result, srt_path) | |
| saved_files["srt"] = srt_path | |
| if "md" in formats and md_content: | |
| md_path = os.path.join(output_dir, f"{safe_title}.md") | |
| with open(md_path, "w", encoding="utf-8") as f: | |
| f.write(md_content) | |
| saved_files["md"] = md_path | |
| # ๋ฒ์ญ ํ์ผ ์ ์ฅ | |
| if translated_txt: | |
| lang_suffix = translate_lang.replace("-", "").lower() | |
| tr_txt_path = os.path.join(output_dir, f"{safe_title}_{lang_suffix}.txt") | |
| with open(tr_txt_path, "w", encoding="utf-8") as f: | |
| f.write(translated_txt) | |
| saved_files[f"txt_{lang_suffix}"] = tr_txt_path | |
| if translated_md: | |
| lang_suffix = translate_lang.replace("-", "").lower() | |
| tr_md_path = os.path.join(output_dir, f"{safe_title}_{lang_suffix}.md") | |
| with open(tr_md_path, "w", encoding="utf-8") as f: | |
| f.write(translated_md) | |
| saved_files[f"md_{lang_suffix}"] = tr_md_path | |
| fmt_str = ", ".join(f.upper() for f in saved_files.keys()) | |
| progress({"step": "save", "percent": 100, "detail": f"{fmt_str} ํ์ผ ์ ์ฅ ์๋ฃ"}) | |
| return { | |
| "title": title, | |
| "language": result["language"], | |
| "text": result["text"], | |
| "segments": result["segments"], | |
| "files": saved_files, | |
| } | |
| finally: | |
| # ์์ ํ์ผ ์ ๋ฆฌ | |
| if os.path.exists(audio_path): | |
| os.remove(audio_path) | |
| if video_path and os.path.exists(video_path): | |
| os.remove(video_path) | |
| if keyframe_dir and os.path.exists(keyframe_dir): | |
| shutil.rmtree(keyframe_dir, ignore_errors=True) | |