import sys import httpx import os import argparse from datetime import datetime from dotenv import load_dotenv from deepgram import DeepgramClient, PrerecordedOptions from deepgram_captions import DeepgramConverter, srt from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.audio.io.AudioFileClip import AudioFileClip import deepl import re load_dotenv() def cleanup_srt_punctuation(srt_content): # Split the SRT content into blocks blocks = re.split(r'\n\s*\n', srt_content.strip()) parsed_blocks = [] for block in blocks: lines = block.split('\n') if len(lines) >= 2: index = lines[0] timecode = lines[1] text = "\n".join(lines[2:]) if len(lines) > 2 else "" parsed_blocks.append({ "index": index, "timecode": timecode, "text": text }) # Rule 1: Clean up spaces before punctuation within each block for block in parsed_blocks: if "text" in block: block["text"] = re.sub(r'\s+([.,!?~:;。、])', r'\1', block["text"]) # Rule 2 & 3: Handle leading punctuation and punctuation-only blocks for i in range(len(parsed_blocks)): block = parsed_blocks[i] if "text" not in block: continue text = block["text"].strip() # Check if the block is only punctuation if text and all(c in ".,!?~:;。、" or c.isspace() for c in text): for j in range(i - 1, -1, -1): prev_block = parsed_blocks[j] if "text" in prev_block and prev_block["text"].strip(): prev_block["text"] = prev_block["text"].rstrip() + " " + text prev_block["text"] = re.sub(r'\s+([.,!?~:;。、])', r'\1', prev_block["text"]) break block["text"] = "" continue # Check if the block starts with leading punctuation (e.g. ", text") match = re.match(r'^([.,!?~:;。、\s]+)(.*)', block["text"]) if match: lead_punct = match.group(1).strip() remaining_text = match.group(2) if lead_punct: for j in range(i - 1, -1, -1): prev_block = parsed_blocks[j] if "text" in prev_block and prev_block["text"].strip(): prev_block["text"] = prev_block["text"].rstrip() + " " + lead_punct prev_block["text"] = re.sub(r'\s+([.,!?~:;。、])', r'\1', prev_block["text"]) break block["text"] = remaining_text # Reconstruct and re-index the SRT string, filtering out empty blocks reconstructed = [] entry = 1 for block in parsed_blocks: text = block["text"].strip() if text: reconstructed.append(f"{entry}\n{block['timecode']}\n{text}") entry += 1 return "\n\n".join(reconstructed) + "\n" def translate_srt_content(srt_content, deepl_api_key, target_lang): import deepl # Split the SRT content into blocks blocks = re.split(r'\n\s*\n', srt_content.strip()) parsed_blocks = [] text_list = [] for block in blocks: lines = block.split('\n') if len(lines) >= 2: index = lines[0] timecode = lines[1] text = "\n".join(lines[2:]) if len(lines) > 2 else "" # Extract speaker tag if any (e.g. "[speaker 0] Hello" or "[Speaker 1]") tag = "" clean_text = text match = re.match(r'^(\[speaker \d+\]\s*)(.*)', text, re.IGNORECASE) if match: tag = match.group(1) clean_text = match.group(2) parsed_blocks.append({ "index": index, "timecode": timecode, "tag": tag, "clean_text": clean_text }) if clean_text.strip(): text_list.append(clean_text) else: parsed_blocks.append({ "raw": block }) # Translate clean texts using DeepL text translation translator = deepl.Translator(deepl_api_key) translated_texts = [] # Chunk text requests to avoid hitting DeepL payload size limits chunk_size = 50 for i in range(0, len(text_list), chunk_size): chunk = text_list[i:i + chunk_size] try: results = translator.translate_text(chunk, target_lang=target_lang) translated_texts.extend([r.text for r in results]) except Exception as e: print(f"Error translating chunk: {e}") translated_texts.extend(chunk) # Reassemble the parsed blocks text_idx = 0 reconstructed = [] entry = 1 for block in parsed_blocks: if "raw" in block: reconstructed.append(block["raw"]) else: clean_text = block["clean_text"] tag = block["tag"] if clean_text.strip(): translated_text = translated_texts[text_idx] if text_idx < len(translated_texts) else clean_text text_idx += 1 full_text = tag + translated_text else: full_text = tag + clean_text # Filter out empty blocks after translation and re-index sequentially stripped_text = full_text.strip() if stripped_text: reconstructed.append(f"{entry}\n{block['timecode']}\n{stripped_text}") entry += 1 return "\n\n".join(reconstructed) + "\n" def main(): parser = argparse.ArgumentParser(description="Transcribe video/audio to SRT subtitles using Deepgram.") parser.add_argument("filepath", type=str, help="Path to the audio or video file to transcribe.") parser.add_argument("-m", "--model", type=str, default="nova-3", help="Deepgram model to use (default: %(default)s).") parser.add_argument("-l", "--language", type=str, default=None, help="BCP-47 language tag (e.g. 'en', 'es', 'fr'), or 'auto'/'detect' to enable automatic language detection.") parser.add_argument("--no-diarize", dest="diarize", action="store_false", help="Disable speaker diarization.") parser.add_argument("-t", "--translate-to", type=str, default=None, help="Translate the generated subtitles to this BCP-47 language tag (e.g. 'ko', 'en', 'ja') using DeepL.") parser.set_defaults(diarize=True) args = parser.parse_args() filepath = args.filepath # Resolve filepath. If it doesn't exist directly but exists in 'media/', use it from there. if not os.path.exists(filepath): media_fallback = os.path.join("media", filepath) if os.path.exists(media_fallback): filepath = media_fallback if not os.path.exists(filepath): print(f"Error: File '{filepath}' not found.") print("Please check the path or place the file in the 'media' directory.") return _, ext = os.path.splitext(filepath.lower()) if ext == '.srt': if not args.translate_to: print("Error: When passing an .srt file, you must specify a target language using -t or --translate-to.") return deepl_api_key = os.getenv("DEEPL_API_KEY") or os.getenv("DEEPL_AUTH_KEY") if not deepl_api_key: print("Error: DEEPL_API_KEY or DEEPL_AUTH_KEY environment variable is not set.") print("Please set it in your environment or add it to your .env file to use translation.") return try: target_lang = args.translate_to.upper() if target_lang == "EN": target_lang = "EN-US" elif target_lang == "PT": target_lang = "PT-BR" base, _ = os.path.splitext(filepath) translated_srt_path = f"{base}.{args.translate_to.lower()}.srt" print(f"Translating {filepath} to {args.translate_to} using DeepL...") with open(filepath, "r", encoding="utf-8") as f: original_content = f.read() translated_content = translate_srt_content(original_content, deepl_api_key, target_lang) cleaned_content = cleanup_srt_punctuation(translated_content) with open(translated_srt_path, "w", encoding="utf-8") as f: f.write(cleaned_content) print(f"Successfully translated subtitles. Saved to: {translated_srt_path}") except Exception as translate_err: print(f"An error occurred during translation: {translate_err}") return api_key = os.getenv("DEEPGRAM_API_KEY") if not api_key: print("Error: DEEPGRAM_API_KEY environment variable is not set.") print("Please set it in your environment or add it to a .env file in the project directory.") return try: deepgram = DeepgramClient(api_key) is_audio = ext in {'.mp3', '.wav', '.m4a', '.flac', '.ogg', '.aac', '.wma', '.opus', '.webm', '.m4b', '.mp4a', '.aiff', '.aif', '.mp2'} audio_filepath = filepath should_remove_audio = False if not is_audio: audio_filepath = f"{filepath}-audio.mp3" should_remove_audio = False audio_exists = False if os.path.exists(audio_filepath) and os.path.getsize(audio_filepath) > 0: try: with VideoFileClip(filepath) as video_clip: video_duration = video_clip.duration with AudioFileClip(audio_filepath) as audio_clip: audio_duration = audio_clip.duration if abs(video_duration - audio_duration) < 1.0: audio_exists = True print(f"Found existing audio file '{audio_filepath}' with matching duration. Skipping extraction.") except Exception as check_err: print(f"Could not verify existing audio file: {check_err}. Re-extracting...") if not audio_exists: try: with VideoFileClip(filepath) as video_clip: audio_clip = video_clip.audio audio_clip.write_audiofile(audio_filepath) except Exception as e: print(f"An error occurred extracting audio from video: {e}") return with open(audio_filepath, "rb") as file: buffer_data = file.read() payload = {"buffer": buffer_data} options_dict = { "model": args.model, "smart_format": True, "utterances": True, "punctuate": True, "diarize": args.diarize, } if args.language: if args.language.lower() in {"auto", "detect"}: options_dict["detect_language"] = True else: options_dict["language"] = args.language options = PrerecordedOptions(**options_dict) print("Making request to deepgram") before = datetime.now() response = deepgram.listen.rest.v("1").transcribe_file( payload, options, timeout=httpx.Timeout(30000.0, connect=10.0) ) after = datetime.now() print("Got response from deepgram") print(response.to_json(indent=4)) # Check if the transcription contains words to avoid IndexError on silent audio files has_words = False try: if hasattr(response, 'results') and response.results: if response.results.channels and response.results.channels[0].alternatives: if response.results.channels[0].alternatives[0].words: has_words = True except Exception: pass if not has_words: print("No speech or words detected in the audio file. Generating empty subtitle file.") captions = "" else: transcription = DeepgramConverter(response) captions = srt(transcription) original_srt_path = f"{filepath}-captions.srt" cleaned_captions = cleanup_srt_punctuation(captions) with open(original_srt_path, "a", encoding="utf-8") as f: f.write(cleaned_captions) if args.translate_to: print(f"Translating subtitles to {args.translate_to} using DeepL...") deepl_api_key = os.getenv("DEEPL_API_KEY") or os.getenv("DEEPL_AUTH_KEY") if not deepl_api_key: print("Error: DEEPL_API_KEY or DEEPL_AUTH_KEY environment variable is not set.") print("Please set it in your environment or add it to your .env file to use translation.") else: try: target_lang = args.translate_to.upper() # DeepL-specific target language code overrides if target_lang == "EN": target_lang = "EN-US" elif target_lang == "PT": target_lang = "PT-BR" translated_srt_path = f"{filepath}-captions.{args.translate_to.lower()}.srt" # Translate and post-process translated_content = translate_srt_content(cleaned_captions, deepl_api_key, target_lang) cleaned_content = cleanup_srt_punctuation(translated_content) with open(translated_srt_path, "w", encoding="utf-8") as f: f.write(cleaned_content) print(f"Successfully translated subtitles. Saved to: {translated_srt_path}") except Exception as translate_err: print(f"An error occurred during translation: {translate_err}") if should_remove_audio: os.remove(audio_filepath) except Exception as e: print(f"Exception: {e}") if __name__ == "__main__": main()