NeuralFalcon commited on
Commit
6350c17
·
verified ·
1 Parent(s): 2103a11

Create subtitle.py

Browse files
Files changed (1) hide show
  1. subtitle.py +579 -0
subtitle.py ADDED
@@ -0,0 +1,579 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ # ==============================================================================
4
+ # --- 1. IMPORTS
5
+ # ==============================================================================
6
+
7
+ import os
8
+ import re
9
+ import gc
10
+ import uuid
11
+ import math
12
+ import shutil
13
+ import string
14
+ import requests
15
+ import urllib.request
16
+ import urllib.error
17
+
18
+ import torch
19
+ import pysrt
20
+ from tqdm.auto import tqdm
21
+ from faster_whisper import WhisperModel
22
+
23
+
24
+ # ==============================================================================
25
+ # --- 2. CONSTANTS & CONFIGURATION
26
+ # ==============================================================================
27
+
28
+ # Folder paths for storing generated files and temporary audio
29
+ SUBTITLE_FOLDER = "./generated_subtitle"
30
+ TEMP_FOLDER = "./subtitle_audio"
31
+
32
+ # Mapping of language names to their ISO 639-1 codes
33
+ LANGUAGE_CODE = {
34
+ 'Akan': 'aka', 'Albanian': 'sq', 'Amharic': 'am', 'Arabic': 'ar', 'Armenian': 'hy',
35
+ 'Assamese': 'as', 'Azerbaijani': 'az', 'Basque': 'eu', 'Bashkir': 'ba', 'Bengali': 'bn',
36
+ 'Bosnian': 'bs', 'Bulgarian': 'bg', 'Burmese': 'my', 'Catalan': 'ca', 'Chinese': 'zh',
37
+ 'Croatian': 'hr', 'Czech': 'cs', 'Danish': 'da', 'Dutch': 'nl', 'English': 'en',
38
+ 'Estonian': 'et', 'Faroese': 'fo', 'Finnish': 'fi', 'French': 'fr', 'Galician': 'gl',
39
+ 'Georgian': 'ka', 'German': 'de', 'Greek': 'el', 'Gujarati': 'gu', 'Haitian Creole': 'ht',
40
+ 'Hausa': 'ha', 'Hebrew': 'he', 'Hindi': 'hi', 'Hungarian': 'hu', 'Icelandic': 'is',
41
+ 'Indonesian': 'id', 'Italian': 'it', 'Japanese': 'ja', 'Kannada': 'kn', 'Kazakh': 'kk',
42
+ 'Korean': 'ko', 'Kurdish': 'ckb', 'Kyrgyz': 'ky', 'Lao': 'lo', 'Lithuanian': 'lt',
43
+ 'Luxembourgish': 'lb', 'Macedonian': 'mk', 'Malay': 'ms', 'Malayalam': 'ml', 'Maltese': 'mt',
44
+ 'Maori': 'mi', 'Marathi': 'mr', 'Mongolian': 'mn', 'Nepali': 'ne', 'Norwegian': 'no',
45
+ 'Norwegian Nynorsk': 'nn', 'Pashto': 'ps', 'Persian': 'fa', 'Polish': 'pl', 'Portuguese': 'pt',
46
+ 'Punjabi': 'pa', 'Romanian': 'ro', 'Russian': 'ru', 'Serbian': 'sr', 'Sinhala': 'si',
47
+ 'Slovak': 'sk', 'Slovenian': 'sl', 'Somali': 'so', 'Spanish': 'es', 'Sundanese': 'su',
48
+ 'Swahili': 'sw', 'Swedish': 'sv', 'Tamil': 'ta', 'Telugu': 'te', 'Thai': 'th',
49
+ 'Turkish': 'tr', 'Ukrainian': 'uk', 'Urdu': 'ur', 'Uzbek': 'uz', 'Vietnamese': 'vi',
50
+ 'Welsh': 'cy', 'Yiddish': 'yi', 'Yoruba': 'yo', 'Zulu': 'zu'
51
+ }
52
+
53
+
54
+ # ==============================================================================
55
+ # --- 3. FILE & MODEL DOWNLOADING UTILITIES
56
+ # ==============================================================================
57
+
58
+ def download_file(url, download_file_path, redownload=False):
59
+ """Download a single file with urllib and a tqdm progress bar."""
60
+ base_path = os.path.dirname(download_file_path)
61
+ os.makedirs(base_path, exist_ok=True)
62
+
63
+ if os.path.exists(download_file_path):
64
+ if redownload:
65
+ os.remove(download_file_path)
66
+ tqdm.write(f"♻️ Redownloading: {os.path.basename(download_file_path)}")
67
+ elif os.path.getsize(download_file_path) > 0:
68
+ tqdm.write(f"✔️ Skipped (already exists): {os.path.basename(download_file_path)}")
69
+ return True
70
+
71
+ try:
72
+ request = urllib.request.urlopen(url)
73
+ total = int(request.headers.get('Content-Length', 0))
74
+ except urllib.error.URLError as e:
75
+ print(f"❌ Error: Unable to open URL: {url}")
76
+ print(f"Reason: {e.reason}")
77
+ return False
78
+
79
+ with tqdm(total=total, desc=os.path.basename(download_file_path), unit='B', unit_scale=True, unit_divisor=1024) as progress:
80
+ try:
81
+ urllib.request.urlretrieve(
82
+ url,
83
+ download_file_path,
84
+ reporthook=lambda count, block_size, total_size: progress.update(block_size)
85
+ )
86
+ except urllib.error.URLError as e:
87
+ print(f"❌ Error: Failed to download {url}")
88
+ print(f"Reason: {e.reason}")
89
+ return False
90
+
91
+ tqdm.write(f"⬇️ Downloaded: {os.path.basename(download_file_path)}")
92
+ return True
93
+
94
+
95
+ def download_model(repo_id, download_folder="./", redownload=False):
96
+ """
97
+ Downloads all files from a Hugging Face repository using the public API,
98
+ avoiding the need for a Hugging Face token for public models.
99
+ """
100
+ if not download_folder.strip():
101
+ download_folder = "."
102
+
103
+ api_url = f"https://huggingface.co/api/models/{repo_id}"
104
+ model_name = repo_id.split('/')[-1]
105
+ download_dir = os.path.abspath(f"{download_folder.rstrip('/')}/{model_name}")
106
+ os.makedirs(download_dir, exist_ok=True)
107
+
108
+ print(f"📂 Download directory: {download_dir}")
109
+
110
+ try:
111
+ response = requests.get(api_url)
112
+ response.raise_for_status()
113
+ except requests.exceptions.RequestException as e:
114
+ print(f"❌ Error fetching repo info: {e}")
115
+ return None
116
+
117
+ data = response.json()
118
+ files_to_download = [f["rfilename"] for f in data.get("siblings", [])]
119
+
120
+ if not files_to_download:
121
+ print(f"⚠️ No files found in repo '{repo_id}'.")
122
+ return None
123
+
124
+ print(f"📦 Found {len(files_to_download)} files in repo '{repo_id}'. Checking cache...")
125
+
126
+ for file in tqdm(files_to_download, desc="Processing files", unit="file"):
127
+ file_url = f"https://huggingface.co/{repo_id}/resolve/main/{file}"
128
+ file_path = os.path.join(download_dir, file)
129
+ download_file(file_url, file_path, redownload=redownload)
130
+
131
+ return download_dir
132
+
133
+
134
+ # ==============================================================================
135
+ # --- 4. CORE TRANSCRIPTION & PROCESSING LOGIC
136
+ # ==============================================================================
137
+
138
+ def get_language_name(code):
139
+ """Retrieves the full language name from its code."""
140
+ for name, value in LANGUAGE_CODE.items():
141
+ if value == code:
142
+ return name
143
+ return None
144
+
145
+ def clean_file_name(file_path):
146
+ """Generates a clean, unique file name to avoid path issues."""
147
+ dir_name = os.path.dirname(file_path)
148
+ base_name, extension = os.path.splitext(os.path.basename(file_path))
149
+
150
+ cleaned_base = re.sub(r'[^a-zA-Z\d]+', '_', base_name)
151
+ cleaned_base = re.sub(r'_+', '_', cleaned_base).strip('_')
152
+ random_uuid = uuid.uuid4().hex[:6]
153
+
154
+ return os.path.join(dir_name, f"{cleaned_base}_{random_uuid}{extension}")
155
+
156
+ def format_segments(segments):
157
+ """Formats the raw segments from Whisper into structured lists."""
158
+ sentence_timestamp = []
159
+ words_timestamp = []
160
+ speech_to_text = ""
161
+
162
+ for i in segments:
163
+ text = i.text.strip()
164
+ sentence_id = len(sentence_timestamp)
165
+ sentence_timestamp.append({
166
+ "id": sentence_id,
167
+ "text": text,
168
+ "start": i.start,
169
+ "end": i.end,
170
+ "words": []
171
+ })
172
+ speech_to_text += text + " "
173
+
174
+ for word in i.words:
175
+ word_data = {
176
+ "word": word.word.strip(),
177
+ "start": word.start,
178
+ "end": word.end
179
+ }
180
+ sentence_timestamp[sentence_id]["words"].append(word_data)
181
+ words_timestamp.append(word_data)
182
+
183
+ return sentence_timestamp, words_timestamp, speech_to_text.strip()
184
+
185
+ # def get_audio_file(uploaded_file):
186
+ # """Copies the uploaded media file to a temporary location for processing."""
187
+ # temp_path = os.path.join(TEMP_FOLDER, os.path.basename(uploaded_file))
188
+ # cleaned_path = clean_file_name(temp_path)
189
+ # shutil.copy(uploaded_file, cleaned_path)
190
+ # return cleaned_path
191
+
192
+ whisper_model=None
193
+
194
+ def load_whisper_model(model_name="deepdml/faster-whisper-large-v3-turbo-ct2"):
195
+ global whisper_model
196
+ if whisper_model is None:
197
+ device = "cuda" if torch.cuda.is_available() else "cpu"
198
+ compute_type = "float16" if torch.cuda.is_available() else "int8"
199
+ try:
200
+ whisper_model = WhisperModel(
201
+ model_name,
202
+ device=device,
203
+ compute_type=compute_type,
204
+ )
205
+ except Exception as e:
206
+ model_dir = download_model(
207
+ "deepdml/faster-whisper-large-v3-turbo-ct2",
208
+ download_folder="./",
209
+ redownload=False)
210
+ whisper_model = WhisperModel(
211
+ model_dir,
212
+ device=device,
213
+ compute_type=compute_type)
214
+ return whisper_model
215
+
216
+
217
+
218
+
219
+ def whisper_subtitle(uploaded_file, source_language):
220
+ """
221
+ Main transcription function. Loads the model, transcribes the audio,
222
+ and generates subtitle files.
223
+ """
224
+
225
+ model = load_whisper_model()
226
+
227
+ # 2. Process audio file
228
+ audio_file_path = uploaded_file
229
+
230
+ # 3. Transcribe
231
+ detected_language = source_language
232
+ lang_code = LANGUAGE_CODE.get(source_language)
233
+
234
+ #fallback to auto if language not found
235
+ if source_language == "Auto" or lang_code is None:
236
+ segments, info = model.transcribe(audio_file_path, word_timestamps=True)
237
+ detected_lang_code = info.language
238
+ detected_language = get_language_name(detected_lang_code)
239
+ else:
240
+ segments, _ = model.transcribe(
241
+ audio_file_path,
242
+ word_timestamps=True,
243
+ language=lang_code
244
+ )
245
+
246
+ sentence_timestamps, word_timestamps, transcript_text = format_segments(segments)
247
+
248
+ # 4. Cleanup
249
+ del model
250
+ gc.collect()
251
+ if torch.cuda.is_available():
252
+ torch.cuda.empty_cache()
253
+
254
+ # 5. Prepare output file paths
255
+ base_filename = os.path.splitext(os.path.basename(uploaded_file))[0][:30]
256
+ srt_base = f"{SUBTITLE_FOLDER}/{base_filename}_{detected_language}.srt"
257
+ clean_srt_path = clean_file_name(srt_base)
258
+ txt_path = clean_srt_path.replace(".srt", ".txt")
259
+ word_srt_path = clean_srt_path.replace(".srt", "_word_level.srt")
260
+ custom_srt_path = clean_srt_path.replace(".srt", "_Multiline.srt")
261
+ shorts_srt_path = clean_srt_path.replace(".srt", "_shorts.srt")
262
+
263
+ # 6. Generate all subtitle files
264
+ generate_srt_from_sentences(sentence_timestamps, srt_path=clean_srt_path)
265
+ word_level_srt(word_timestamps, srt_path=word_srt_path)
266
+ shorts_json = write_sentence_srt(
267
+ word_timestamps, output_file=shorts_srt_path, max_lines=1,
268
+ max_duration_s=2.0, max_chars_per_line=17
269
+ )
270
+ sentence_json = write_sentence_srt(
271
+ word_timestamps, output_file=custom_srt_path, max_lines=2,
272
+ max_duration_s=7.0, max_chars_per_line=38
273
+ )
274
+
275
+ with open(txt_path, 'w', encoding='utf-8') as f:
276
+ f.write(transcript_text)
277
+
278
+ return (
279
+ clean_srt_path, custom_srt_path, word_srt_path, shorts_srt_path,
280
+ txt_path, transcript_text, sentence_json, shorts_json, detected_language
281
+ )
282
+
283
+
284
+
285
+ # ==============================================================================
286
+ # --- 5. SUBTITLE GENERATION & FORMATTING
287
+ # ==============================================================================
288
+
289
+ def convert_time_to_srt_format(seconds):
290
+ """Converts seconds to the standard SRT time format (HH:MM:SS,ms)."""
291
+ hours = int(seconds // 3600)
292
+ minutes = int((seconds % 3600) // 60)
293
+ secs = int(seconds % 60)
294
+ milliseconds = round((seconds - int(seconds)) * 1000)
295
+
296
+ if milliseconds == 1000:
297
+ milliseconds = 0
298
+ secs += 1
299
+ if secs == 60:
300
+ secs, minutes = 0, minutes + 1
301
+ if minutes == 60:
302
+ minutes, hours = 0, hours + 1
303
+
304
+ return f"{hours:02}:{minutes:02}:{secs:02},{milliseconds:03}"
305
+
306
+ def split_line_by_char_limit(text, max_chars_per_line=38):
307
+ """Splits a string into multiple lines based on a character limit."""
308
+ words = text.split()
309
+ lines = []
310
+ current_line = ""
311
+ for word in words:
312
+ if not current_line:
313
+ current_line = word
314
+ elif len(current_line + " " + word) <= max_chars_per_line:
315
+ current_line += " " + word
316
+ else:
317
+ lines.append(current_line)
318
+ current_line = word
319
+ if current_line:
320
+ lines.append(current_line)
321
+ return lines
322
+
323
+ def merge_punctuation_glitches(subtitles):
324
+ """Cleans up punctuation artifacts at the boundaries of subtitle entries."""
325
+ if not subtitles:
326
+ return []
327
+
328
+ cleaned = [subtitles[0]]
329
+ for i in range(1, len(subtitles)):
330
+ prev = cleaned[-1]
331
+ curr = subtitles[i]
332
+
333
+ prev_text = prev["text"].rstrip()
334
+ curr_text = curr["text"].lstrip()
335
+
336
+ match = re.match(r'^([,.:;!?]+)(\s*)(.+)', curr_text)
337
+ if match:
338
+ punct, _, rest = match.groups()
339
+ if not prev_text.endswith(tuple(punct)):
340
+ prev["text"] = prev_text + punct
341
+ curr_text = rest.strip()
342
+
343
+ unwanted_chars = ['"', '“', '”', ';', ':']
344
+ for ch in unwanted_chars:
345
+ curr_text = curr_text.replace(ch, '')
346
+ curr_text = curr_text.strip()
347
+
348
+ if not curr_text or re.fullmatch(r'[.,!?]+', curr_text):
349
+ prev["end"] = curr["end"]
350
+ continue
351
+
352
+ curr["text"] = curr_text
353
+ prev["text"] = prev["text"].replace('"', '').replace('“', '').replace('”', '')
354
+ cleaned.append(curr)
355
+
356
+ return cleaned
357
+
358
+ import json
359
+ def write_sentence_srt(
360
+ word_level_timestamps, output_file="subtitles_professional.srt", max_lines=2,
361
+ max_duration_s=7.0, max_chars_per_line=38, hard_pause_threshold=0.5,
362
+ merge_pause_threshold=0.4
363
+ ):
364
+ """Creates professional-grade SRT files and a corresponding timestamp.json file."""
365
+ if not word_level_timestamps:
366
+ return
367
+
368
+ # Phase 1: Generate draft subtitles based on timing and length rules
369
+ draft_subtitles = []
370
+ i = 0
371
+ while i < len(word_level_timestamps):
372
+ start_time = word_level_timestamps[i]["start"]
373
+
374
+ # We'll now store the full word objects, not just the text
375
+ current_word_objects = []
376
+
377
+ j = i
378
+ while j < len(word_level_timestamps):
379
+ entry = word_level_timestamps[j]
380
+
381
+ # Create potential text from the word objects
382
+ potential_words = [w["word"] for w in current_word_objects] + [entry["word"]]
383
+ potential_text = " ".join(potential_words)
384
+
385
+ if len(split_line_by_char_limit(potential_text, max_chars_per_line)) > max_lines: break
386
+ if (entry["end"] - start_time) > max_duration_s and current_word_objects: break
387
+
388
+ if j > i:
389
+ prev_entry = word_level_timestamps[j-1]
390
+ pause = entry["start"] - prev_entry["end"]
391
+ if pause >= hard_pause_threshold: break
392
+ if prev_entry["word"].endswith(('.','!','?')): break
393
+
394
+ # Append the full word object
395
+ current_word_objects.append(entry)
396
+ j += 1
397
+
398
+ if not current_word_objects:
399
+ current_word_objects.append(word_level_timestamps[i])
400
+ j = i + 1
401
+
402
+ text = " ".join([w["word"] for w in current_word_objects])
403
+ end_time = word_level_timestamps[j - 1]["end"]
404
+
405
+ # Include the list of word objects in our draft subtitle
406
+ draft_subtitles.append({
407
+ "start": start_time,
408
+ "end": end_time,
409
+ "text": text,
410
+ "words": current_word_objects
411
+ })
412
+ i = j
413
+
414
+ # Phase 2: Post-process to merge single-word "orphan" subtitles
415
+ if not draft_subtitles: return
416
+ final_subtitles = [draft_subtitles[0]]
417
+ for k in range(1, len(draft_subtitles)):
418
+ prev_sub = final_subtitles[-1]
419
+ current_sub = draft_subtitles[k]
420
+ is_orphan = len(current_sub["text"].split()) == 1
421
+ pause_from_prev = current_sub["start"] - prev_sub["end"]
422
+
423
+ if is_orphan and pause_from_prev < merge_pause_threshold:
424
+ merged_text = prev_sub["text"] + " " + current_sub["text"]
425
+ if len(split_line_by_char_limit(merged_text, max_chars_per_line)) <= max_lines:
426
+ prev_sub["text"] = merged_text
427
+ prev_sub["end"] = current_sub["end"]
428
+
429
+ # Merge the word-level data as well
430
+ prev_sub["words"].extend(current_sub["words"])
431
+ continue
432
+
433
+ final_subtitles.append(current_sub)
434
+
435
+ final_subtitles = merge_punctuation_glitches(final_subtitles)
436
+ # print(final_subtitles)
437
+ # ==============================================================================
438
+ # NEW CODE BLOCK: Generate JSON data and write files
439
+ # ==============================================================================
440
+
441
+ # This dictionary will hold the data for our JSON file
442
+ timestamps_data = {}
443
+
444
+ # Phase 3: Write the final SRT file (and prepare JSON data)
445
+ with open(output_file, "w", encoding="utf-8") as f:
446
+ for idx, sub in enumerate(final_subtitles, start=1):
447
+ # --- SRT Writing (Unchanged) ---
448
+ text = sub["text"].replace(" ,", ",").replace(" .", ".")
449
+ formatted_lines = split_line_by_char_limit(text, max_chars_per_line)
450
+ start_time_str = convert_time_to_srt_format(sub['start'])
451
+ end_time_str = convert_time_to_srt_format(sub['end'])
452
+
453
+ f.write(f"{idx}\n")
454
+ f.write(f"{start_time_str} --> {end_time_str}\n")
455
+ f.write("\n".join(formatted_lines) + "\n\n")
456
+
457
+ # --- JSON Data Population (New) ---
458
+ # Create the list of word dictionaries for the current subtitle
459
+ word_data = []
460
+ for word_obj in sub["words"]:
461
+ word_data.append({
462
+ "word": word_obj["word"],
463
+ "start": convert_time_to_srt_format(word_obj["start"]),
464
+ "end": convert_time_to_srt_format(word_obj["end"])
465
+ })
466
+
467
+ # Add the complete entry to our main dictionary
468
+ timestamps_data[str(idx)] = {
469
+ "text": "\n".join(formatted_lines),
470
+ "start": start_time_str,
471
+ "end": end_time_str,
472
+ "words": word_data
473
+ }
474
+
475
+ # Write the collected data to the JSON file
476
+ json_output_file = output_file.replace(".srt",".json")
477
+ with open(json_output_file, "w", encoding="utf-8") as f_json:
478
+ json.dump(timestamps_data, f_json, indent=4, ensure_ascii=False)
479
+
480
+ # print(f"Successfully generated SRT file: {output_file}")
481
+ # print(f"Successfully generated JSON file: {json_output_file}")
482
+ return json_output_file
483
+
484
+ def write_subtitles_to_file(subtitles, filename="subtitles.srt"):
485
+ """Writes a dictionary of subtitles to a standard SRT file."""
486
+ with open(filename, 'w', encoding='utf-8') as f:
487
+ for id, entry in subtitles.items():
488
+ if entry['start'] is None or entry['end'] is None:
489
+ print(f"Skipping subtitle ID {id} due to missing timestamps.")
490
+ continue
491
+ start_time = convert_time_to_srt_format(entry['start'])
492
+ end_time = convert_time_to_srt_format(entry['end'])
493
+ f.write(f"{id}\n")
494
+ f.write(f"{start_time} --> {end_time}\n")
495
+ f.write(f"{entry['text']}\n\n")
496
+
497
+ def word_level_srt(words_timestamp, srt_path="word_level_subtitle.srt", shorts=False):
498
+ """Generates an SRT file with one word per subtitle entry."""
499
+ punctuation = re.compile(r'[.,!?;:"\–—_~^+*|]')
500
+ with open(srt_path, 'w', encoding='utf-8') as srt_file:
501
+ for i, word_info in enumerate(words_timestamp, start=1):
502
+ start = convert_time_to_srt_format(word_info['start'])
503
+ end = convert_time_to_srt_format(word_info['end'])
504
+ word = re.sub(punctuation, '', word_info['word'])
505
+ if word.strip().lower() == 'i': word = "I"
506
+ if not shorts: word = word.replace("-", "")
507
+ srt_file.write(f"{i}\n{start} --> {end}\n{word}\n\n")
508
+
509
+ def generate_srt_from_sentences(sentence_timestamp, srt_path="default_subtitle.srt"):
510
+ """Generates a standard SRT file from sentence-level timestamps."""
511
+ with open(srt_path, 'w', encoding='utf-8') as srt_file:
512
+ for index, sentence in enumerate(sentence_timestamp, start=1):
513
+ start = convert_time_to_srt_format(sentence['start'])
514
+ end = convert_time_to_srt_format(sentence['end'])
515
+ srt_file.write(f"{index}\n{start} --> {end}\n{sentence['text']}\n\n")
516
+
517
+
518
+
519
+
520
+ # ==============================================================================
521
+ # --- 7. MAIN ORCHESTRATOR FUNCTION
522
+ # ==============================================================================
523
+
524
+ def subtitle_maker(media_file, source_lang):
525
+ """
526
+ The main entry point to generate and optionally translate subtitles.
527
+
528
+ Args:
529
+ media_file (str): Path to the input media file.
530
+ source_lang (str): The source language ('Automatic' for detection).
531
+ target_lang (str): The target language for translation.
532
+
533
+ Returns:
534
+ A tuple containing paths to all generated files and the transcript text.
535
+ """
536
+
537
+ try:
538
+ (
539
+ default_srt, custom_srt, word_srt, shorts_srt,
540
+ txt_path, transcript, sentence_json,word_json,detected_lang
541
+ ) = whisper_subtitle(media_file, source_lang)
542
+ except Exception as e:
543
+ print(f"❌ An error occurred during transcription: {e}")
544
+ return (None, None, None, None, None, None,None,None, f"Error: {e}")
545
+
546
+
547
+ return (
548
+ default_srt, custom_srt, word_srt,
549
+ shorts_srt, txt_path,sentence_json,word_json, transcript,detected_lang
550
+ )
551
+
552
+
553
+ # ==============================================================================
554
+ # --- 8. INITIALIZATION
555
+ # ==============================================================================
556
+ os.makedirs(SUBTITLE_FOLDER, exist_ok=True)
557
+ os.makedirs(TEMP_FOLDER, exist_ok=True)
558
+
559
+
560
+ # from subtitle import subtitle_maker
561
+ # media_file = "/content/output.mp3"
562
+ # source_lang = "Auto" #"English"
563
+
564
+ # default_srt, custom_srt, word_srt,shorts_srt, txt_path,sentence_json,word_json, transcript,detected_lang= subtitle_maker(
565
+ # media_file, source_lang
566
+ # )
567
+
568
+
569
+ # default_srt -> Original subtitles generated directly by Whisper-Large-V3-Turbo-CT2
570
+ # custom_srt -> Modified version of default subtitles with shorter segments
571
+ # (better readability for horizontal videos, Maximum 38 characters per segment. )
572
+ # word_srt -> Word-level timestamps (useful for creating YouTube Shorts/Reels)
573
+ # shorts_srt -> Optimized subtitles for vertical videos (displays 3–4 words at a time , Maximum 17 characters per segment.)
574
+ # txt_path -> Full transcript as plain text (useful for video summarization or for asking questions about the video or audio data with other LLM tools)
575
+ # sentence_json,word_json --> To Generate .ass file later
576
+ # transcript -> Transcript text directly returned by the function, if you just need the transcript
577
+ # detected_lang -> Detected Lang
578
+ # All functionality is contained in a single file, making it portable
579
+ # and reusable across multiple projects for different purposes.