subtitld 0.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. subtitld/__init__.py +19 -0
  2. subtitld/__main__.py +240 -0
  3. subtitld/ftfy/char_classes.dat +0 -0
  4. subtitld/graphics/Montserrat-Black.ttf +0 -0
  5. subtitld/graphics/Montserrat-BlackItalic.ttf +0 -0
  6. subtitld/graphics/Montserrat-Bold.ttf +0 -0
  7. subtitld/graphics/Montserrat-BoldItalic.ttf +0 -0
  8. subtitld/graphics/Montserrat-ExtraBold.ttf +0 -0
  9. subtitld/graphics/Montserrat-ExtraBoldItalic.ttf +0 -0
  10. subtitld/graphics/Montserrat-ExtraLight.ttf +0 -0
  11. subtitld/graphics/Montserrat-ExtraLightItalic.ttf +0 -0
  12. subtitld/graphics/Montserrat-Italic.ttf +0 -0
  13. subtitld/graphics/Montserrat-Light.ttf +0 -0
  14. subtitld/graphics/Montserrat-LightItalic.ttf +0 -0
  15. subtitld/graphics/Montserrat-Medium.ttf +0 -0
  16. subtitld/graphics/Montserrat-MediumItalic.ttf +0 -0
  17. subtitld/graphics/Montserrat-Regular.ttf +0 -0
  18. subtitld/graphics/Montserrat-SemiBold.ttf +0 -0
  19. subtitld/graphics/Montserrat-SemiBoldItalic.ttf +0 -0
  20. subtitld/graphics/Montserrat-Thin.ttf +0 -0
  21. subtitld/graphics/Montserrat-ThinItalic.ttf +0 -0
  22. subtitld/graphics/Ubuntu-B.ttf +0 -0
  23. subtitld/graphics/Ubuntu-BI.ttf +0 -0
  24. subtitld/graphics/Ubuntu-C.ttf +0 -0
  25. subtitld/graphics/Ubuntu-L.ttf +0 -0
  26. subtitld/graphics/Ubuntu-LI.ttf +0 -0
  27. subtitld/graphics/Ubuntu-M.ttf +0 -0
  28. subtitld/graphics/Ubuntu-MI.ttf +0 -0
  29. subtitld/graphics/Ubuntu-R.ttf +0 -0
  30. subtitld/graphics/Ubuntu-RI.ttf +0 -0
  31. subtitld/graphics/Ubuntu-Th.ttf +0 -0
  32. subtitld/graphics/UbuntuMono-B.ttf +0 -0
  33. subtitld/graphics/UbuntuMono-BI.ttf +0 -0
  34. subtitld/graphics/UbuntuMono-R.ttf +0 -0
  35. subtitld/graphics/UbuntuMono-RI.ttf +0 -0
  36. subtitld/graphics/add_and_play.svg +4 -0
  37. subtitld/graphics/add_from_last.svg +4 -0
  38. subtitld/graphics/add_from_last_checked.svg +4 -0
  39. subtitld/graphics/add_subtitle_icon.svg +4 -0
  40. subtitld/graphics/add_subtitle_to_next_start.svg +4 -0
  41. subtitld/graphics/add_subtitle_to_next_start_checked.svg +4 -0
  42. subtitld/graphics/background_watermark.svg +9 -0
  43. subtitld/graphics/character_count_icon.svg +1 -0
  44. subtitld/graphics/checkbox_checked.svg +8 -0
  45. subtitld/graphics/checkbox_checked_hover.svg +8 -0
  46. subtitld/graphics/checkbox_disabled.svg +4 -0
  47. subtitld/graphics/checkbox_unchecked.svg +5 -0
  48. subtitld/graphics/checkbox_unchecked_hover.svg +5 -0
  49. subtitld/graphics/combobox_down_arrow.svg +4 -0
  50. subtitld/graphics/cps_icon.svg +1 -0
  51. subtitld/graphics/dialog_close_icon.svg +4 -0
  52. subtitld/graphics/dialog_close_icon_hover.svg +4 -0
  53. subtitld/graphics/dubbing_container_collapse_icon.svg +8 -0
  54. subtitld/graphics/dubbing_container_expand_icon.svg +4 -0
  55. subtitld/graphics/file.svg +4 -0
  56. subtitld/graphics/gap_add_icon.svg +4 -0
  57. subtitld/graphics/gap_remove_icon.svg +4 -0
  58. subtitld/graphics/grid_frames_icon.svg +4 -0
  59. subtitld/graphics/grid_frames_icon_checked.svg +4 -0
  60. subtitld/graphics/grid_frames_icon_disabled.svg +4 -0
  61. subtitld/graphics/grid_scenes_icon.svg +4 -0
  62. subtitld/graphics/grid_scenes_icon_checked.svg +4 -0
  63. subtitld/graphics/grid_scenes_icon_disabled.svg +4 -0
  64. subtitld/graphics/grid_seconds_icon.svg +4 -0
  65. subtitld/graphics/grid_seconds_icon_checked.svg +4 -0
  66. subtitld/graphics/grid_seconds_icon_disabled.svg +4 -0
  67. subtitld/graphics/last_end_to_current_position_icon.svg +4 -0
  68. subtitld/graphics/last_start_last_end_to_current_position_button.svg +9 -0
  69. subtitld/graphics/last_start_to_current_position_icon.svg +4 -0
  70. subtitld/graphics/left_panel_autosave.svg +4 -0
  71. subtitld/graphics/left_panel_dubbing.svg +4 -0
  72. subtitld/graphics/left_panel_export.svg +4 -0
  73. subtitld/graphics/left_panel_global.svg +4 -0
  74. subtitld/graphics/left_panel_import.svg +4 -0
  75. subtitld/graphics/left_panel_interface.svg +4 -0
  76. subtitld/graphics/left_panel_keyboard.svg +4 -0
  77. subtitld/graphics/left_panel_metadata.svg +4 -0
  78. subtitld/graphics/left_panel_navigation_background.svg +11 -0
  79. subtitld/graphics/left_panel_qualitycheck.svg +4 -0
  80. subtitld/graphics/left_panel_speakers.svg +4 -0
  81. subtitld/graphics/left_panel_subtitles.svg +1 -0
  82. subtitld/graphics/left_panel_transcription.svg +4 -0
  83. subtitld/graphics/left_panel_translation.svg +4 -0
  84. subtitld/graphics/merge_back_selected_subtitle_icon.svg +4 -0
  85. subtitld/graphics/merge_next_selected_subtitle_icon.svg +4 -0
  86. subtitld/graphics/move_backward_subtitle.svg +4 -0
  87. subtitld/graphics/move_end_back_subtitle.svg +4 -0
  88. subtitld/graphics/move_end_forward_subtitle.svg +4 -0
  89. subtitld/graphics/move_forward_subtitle.svg +4 -0
  90. subtitld/graphics/move_start_back_subtitle.svg +4 -0
  91. subtitld/graphics/move_start_forward_subtitle.svg +4 -0
  92. subtitld/graphics/music_voice_separation_music_icon.svg +4 -0
  93. subtitld/graphics/music_voice_separation_music_icon_disabled.svg +4 -0
  94. subtitld/graphics/music_voice_separation_voice_icon.svg +4 -0
  95. subtitld/graphics/music_voice_separation_voice_icon_disabled.svg +4 -0
  96. subtitld/graphics/new_line_icon.svg +1 -0
  97. subtitld/graphics/next_end_to_current_position_icon.svg +4 -0
  98. subtitld/graphics/next_start_next_end_to_current_position_button.svg +4 -0
  99. subtitld/graphics/next_start_to_current_position_icon.svg +4 -0
  100. subtitld/graphics/pause_icon.svg +12 -0
  101. subtitld/graphics/play_from_last_start_icon.svg +5 -0
  102. subtitld/graphics/play_from_next_start_icon.svg +7 -0
  103. subtitld/graphics/play_icon.svg +12 -0
  104. subtitld/graphics/play_icon_hover.svg +12 -0
  105. subtitld/graphics/play_icon_pressed.svg +12 -0
  106. subtitld/graphics/playback_repeat.svg +4 -0
  107. subtitld/graphics/playback_speed.svg +6 -0
  108. subtitld/graphics/playbackspeed_border.svg +5 -0
  109. subtitld/graphics/playbackspeed_border_checked.svg +5 -0
  110. subtitld/graphics/playbackspeed_border_hover.svg +5 -0
  111. subtitld/graphics/playercontrols_button_first.svg +5 -0
  112. subtitld/graphics/playercontrols_button_first_hover.svg +6 -0
  113. subtitld/graphics/playercontrols_button_first_pressed.svg +6 -0
  114. subtitld/graphics/playercontrols_button_last.svg +6 -0
  115. subtitld/graphics/playercontrols_button_last_hover.svg +6 -0
  116. subtitld/graphics/playercontrols_button_last_pressed.svg +6 -0
  117. subtitld/graphics/playercontrols_widget_center_bottom_line.svg +13 -0
  118. subtitld/graphics/playercontrols_widget_center_top_line.svg +17 -0
  119. subtitld/graphics/plus_icon.svg +4 -0
  120. subtitld/graphics/record_icon.svg +8 -0
  121. subtitld/graphics/record_icon_pressed.svg +5 -0
  122. subtitld/graphics/remove_selected_subtitle_icon.svg +6 -0
  123. subtitld/graphics/repeat_playback_border.svg +5 -0
  124. subtitld/graphics/repeat_playback_border_checked.svg +5 -0
  125. subtitld/graphics/repeat_playback_border_hover.svg +5 -0
  126. subtitld/graphics/save.svg +4 -0
  127. subtitld/graphics/send_text_to_last_subtitle_and_slice_button.svg +8 -0
  128. subtitld/graphics/send_text_to_last_subtitle_icon.svg +8 -0
  129. subtitld/graphics/send_text_to_next_subtitle_and_slice_button.svg +8 -0
  130. subtitld/graphics/send_text_to_next_subtitle_icon.svg +8 -0
  131. subtitld/graphics/slice_selected_subtitle_icon.svg +4 -0
  132. subtitld/graphics/snap_grid_icon.svg +4 -0
  133. subtitld/graphics/snap_grid_icon_checked.svg +4 -0
  134. subtitld/graphics/snap_grid_icon_disabled.svg +4 -0
  135. subtitld/graphics/snap_limits_icon.svg +4 -0
  136. subtitld/graphics/snap_limits_icon_checked.svg +4 -0
  137. subtitld/graphics/snap_limits_icon_disabled.svg +4 -0
  138. subtitld/graphics/snap_move_next.svg +4 -0
  139. subtitld/graphics/snap_move_next_checked.svg +4 -0
  140. subtitld/graphics/snap_move_next_disabled.svg +4 -0
  141. subtitld/graphics/snap_moving_icon.svg +4 -0
  142. subtitld/graphics/snap_moving_icon_checked.svg +4 -0
  143. subtitld/graphics/snap_moving_icon_disabled.svg +4 -0
  144. subtitld/graphics/speaker_list_item_change_color_icon.svg +4 -0
  145. subtitld/graphics/speaker_list_item_change_color_icon_hover.svg +4 -0
  146. subtitld/graphics/speaker_list_item_change_image_icon.svg +4 -0
  147. subtitld/graphics/speaker_list_item_change_image_icon_hover.svg +4 -0
  148. subtitld/graphics/speaker_list_item_export_icon.svg +4 -0
  149. subtitld/graphics/speaker_list_item_export_icon_hover.svg +4 -0
  150. subtitld/graphics/speaker_list_item_remove_icon.svg +4 -0
  151. subtitld/graphics/speaker_list_item_remove_icon_hover.svg +4 -0
  152. subtitld/graphics/speaker_list_item_rename_icon.svg +4 -0
  153. subtitld/graphics/speaker_list_item_rename_icon_hover.svg +4 -0
  154. subtitld/graphics/spinbox_arrow_down.svg +4 -0
  155. subtitld/graphics/spinbox_arrow_up.svg +4 -0
  156. subtitld/graphics/start_screen_top_shadow.svg +23 -0
  157. subtitld/graphics/stop_icon.svg +6 -0
  158. subtitld/graphics/stop_icon_hover.svg +6 -0
  159. subtitld/graphics/stop_icon_pressed.svg +6 -0
  160. subtitld/graphics/stylesheet.qss +1838 -0
  161. subtitld/graphics/subtitld.ico +0 -0
  162. subtitld/graphics/subtitld.png +0 -0
  163. subtitld/graphics/subtitles_panel_simplelist_properties_duration_timing_lock_closed_icon.svg +4 -0
  164. subtitld/graphics/subtitles_panel_simplelist_properties_duration_timing_lock_open_icon.svg +4 -0
  165. subtitld/graphics/subtitles_panel_simplelist_properties_duration_timing_lock_open_icon_hover.svg +4 -0
  166. subtitld/graphics/timeline_bottom_controls_background.svg +25 -0
  167. subtitld/graphics/timeline_cursor_back_frame.svg +4 -0
  168. subtitld/graphics/timeline_cursor_back_frame_hover.svg +4 -0
  169. subtitld/graphics/timeline_cursor_next_frame.svg +4 -0
  170. subtitld/graphics/timeline_cursor_next_frame_hover.svg +4 -0
  171. subtitld/graphics/timeline_show_speaker_color.svg +5 -0
  172. subtitld/graphics/timeline_show_speaker_color_checked.svg +5 -0
  173. subtitld/graphics/timeline_show_speaker_in_tracks.svg +4 -0
  174. subtitld/graphics/timeline_show_speaker_in_tracks_checked.svg +4 -0
  175. subtitld/graphics/timelinescrolling_follow_icon.svg +4 -0
  176. subtitld/graphics/timelinescrolling_follow_icon_checked.svg +4 -0
  177. subtitld/graphics/timelinescrolling_none_icon.svg +6 -0
  178. subtitld/graphics/timelinescrolling_none_icon_checked.svg +6 -0
  179. subtitld/graphics/timelinescrolling_page_icon.svg +4 -0
  180. subtitld/graphics/timelinescrolling_page_icon_checked.svg +4 -0
  181. subtitld/graphics/titleBar_left_container_border.svg +113 -0
  182. subtitld/graphics/unknown_speaker.svg +5 -0
  183. subtitld/graphics/vosk_transcription_download_model_icon.svg +4 -0
  184. subtitld/graphics/vosk_transcription_update_model_icon.svg +4 -0
  185. subtitld/graphics/word_count_icon.svg +1 -0
  186. subtitld/graphics/wpm_icon.svg +1 -0
  187. subtitld/graphics/zoom_in_icon.svg +4 -0
  188. subtitld/graphics/zoom_in_icon_disabled.svg +4 -0
  189. subtitld/graphics/zoom_out_icon.svg +4 -0
  190. subtitld/graphics/zoom_out_icon_disabled.svg +4 -0
  191. subtitld/interface/__init__.py +0 -0
  192. subtitld/interface/actionmanager.py +18 -0
  193. subtitld/interface/bottom_panel.py +43 -0
  194. subtitld/interface/left_panel.py +144 -0
  195. subtitld/interface/left_panel_autosave.py +149 -0
  196. subtitld/interface/left_panel_dubbing.py +469 -0
  197. subtitld/interface/left_panel_export.py +755 -0
  198. subtitld/interface/left_panel_global.py +358 -0
  199. subtitld/interface/left_panel_import.py +82 -0
  200. subtitld/interface/left_panel_interface.py +616 -0
  201. subtitld/interface/left_panel_keyboard.py +261 -0
  202. subtitld/interface/left_panel_metadata.py +74 -0
  203. subtitld/interface/left_panel_qualitycheck.py +262 -0
  204. subtitld/interface/left_panel_speakers.py +737 -0
  205. subtitld/interface/left_panel_subtitleslist.py +791 -0
  206. subtitld/interface/left_panel_transcription.py +763 -0
  207. subtitld/interface/left_panel_translation.py +294 -0
  208. subtitld/interface/playercontrols.py +2348 -0
  209. subtitld/interface/preview_panel.py +407 -0
  210. subtitld/interface/productionscreen.py +65 -0
  211. subtitld/interface/startscreen.py +273 -0
  212. subtitld/interface/timeline.py +1308 -0
  213. subtitld/interface/top_bar.py +329 -0
  214. subtitld/interface/translation.py +68 -0
  215. subtitld/interface/utils.py +286 -0
  216. subtitld/locale/en_US.json +301 -0
  217. subtitld/locale/pt_BR.json +269 -0
  218. subtitld/modules/__init__.py +0 -0
  219. subtitld/modules/audioengine.py +606 -0
  220. subtitld/modules/bounce.py +229 -0
  221. subtitld/modules/config.py +84 -0
  222. subtitld/modules/file_io.py +870 -0
  223. subtitld/modules/history.py +31 -0
  224. subtitld/modules/quality_check.py +53 -0
  225. subtitld/modules/session.py +270 -0
  226. subtitld/modules/shortcuts.py +84 -0
  227. subtitld/modules/subtitles.py +413 -0
  228. subtitld/modules/timecode.py +496 -0
  229. subtitld/modules/usf.py +366 -0
  230. subtitld/modules/utils.py +79 -0
  231. subtitld/modules/waveform.py +53 -0
  232. subtitld-0.0.0.dev0.dist-info/METADATA +97 -0
  233. subtitld-0.0.0.dev0.dist-info/RECORD +237 -0
  234. subtitld-0.0.0.dev0.dist-info/WHEEL +5 -0
  235. subtitld-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  236. subtitld-0.0.0.dev0.dist-info/licenses/LICENSE +674 -0
  237. subtitld-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,229 @@
1
+ """Bounce / render dub speeches to audio or video files."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+
8
+ import numpy as np
9
+ import soundfile as sf
10
+
11
+ from subtitld.modules import session
12
+ from subtitld.modules import utils
13
+
14
+
15
+ def _safe_piece(name):
16
+ return ''.join(c if c.isalnum() or c in '-_.' else '_' for c in str(name)) or 'x'
17
+
18
+
19
+ def _separation_path(kind):
20
+ """Path to the cached <kind>.flac (`background` or `vocals`) for the current video."""
21
+ video_path = session.VIDEO.get('filepath') if isinstance(session.VIDEO, dict) else None
22
+ if not video_path:
23
+ return None
24
+ key = utils.get_cache_key(video_path)
25
+ if not key:
26
+ return None
27
+ path = os.path.join(session.PATH_SUBTITLD_DATA_AUDIOSEPARATION, f'{key}_{kind}.flac')
28
+ return path if os.path.exists(path) else None
29
+
30
+
31
+ def _background_audio_path():
32
+ return _separation_path('background')
33
+
34
+
35
+ def _vocals_audio_path():
36
+ return _separation_path('vocals')
37
+
38
+
39
+ def _load_audio(path, samplerate):
40
+ """Load audio file, resample to `samplerate`, return (frames, 2) float32."""
41
+ data, sr = sf.read(path, dtype='float32', always_2d=True)
42
+ if data.shape[1] == 1:
43
+ data = np.tile(data, (1, 2))
44
+ if sr != samplerate:
45
+ # Linear resample — cheap and sufficient for dub mixing.
46
+ ratio = samplerate / sr
47
+ out_len = int(round(data.shape[0] * ratio))
48
+ src_idx = np.arange(out_len, dtype=np.float64) / ratio
49
+ i0 = np.floor(src_idx).astype(np.int64)
50
+ i1 = np.minimum(i0 + 1, data.shape[0] - 1)
51
+ frac = (src_idx - i0).astype(np.float32)
52
+ data = (1.0 - frac[:, None]) * data[i0] + frac[:, None] * data[i1]
53
+ return data.astype(np.float32, copy=False)
54
+
55
+
56
+ def _write_audio(path, buffer, samplerate, audio_format):
57
+ """Write `buffer` (frames, 2) to `path`.
58
+
59
+ WAV / FLAC → direct soundfile write.
60
+ MP3 → ffmpeg libmp3lame.
61
+ """
62
+ audio_format = audio_format.upper()
63
+ if audio_format in ('WAV', 'FLAC'):
64
+ sf.write(path, buffer, samplerate, subtype='PCM_16' if audio_format == 'WAV' else 'PCM_16')
65
+ return
66
+
67
+ if audio_format == 'MP3':
68
+ with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tf:
69
+ tmp_wav = tf.name
70
+ try:
71
+ sf.write(tmp_wav, buffer, samplerate, subtype='PCM_16')
72
+ cmd = [
73
+ session.FFMPEG_EXECUTABLE,
74
+ '-hide_banner', '-loglevel', 'error',
75
+ '-y', '-i', tmp_wav,
76
+ '-codec:a', 'libmp3lame', '-qscale:a', '2',
77
+ path,
78
+ ]
79
+ subprocess.run(cmd, startupinfo=session.STARTUPINFO, check=True)
80
+ finally:
81
+ try:
82
+ os.unlink(tmp_wav)
83
+ except Exception:
84
+ pass
85
+ return
86
+
87
+ raise ValueError(f'Unsupported audio format: {audio_format}')
88
+
89
+
90
+ def _mux_mp4(video_path, audio_buffer, samplerate, output_path):
91
+ """Copy the video stream from `video_path`, replace audio with AAC-encoded
92
+ version of `audio_buffer`. Original audio/subtitle/data streams are dropped."""
93
+ with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tf:
94
+ tmp_wav = tf.name
95
+ try:
96
+ sf.write(tmp_wav, audio_buffer, samplerate, subtype='PCM_16')
97
+ cmd = [
98
+ session.FFMPEG_EXECUTABLE,
99
+ '-hide_banner', '-loglevel', 'error',
100
+ '-y',
101
+ '-an', # drop audio from the video input entirely
102
+ '-i', video_path,
103
+ '-i', tmp_wav,
104
+ '-map', '0:v:0', # only video stream from input 0
105
+ '-map', '1:a:0', # only audio from rendered WAV
106
+ '-sn', '-dn', # no subtitle, no data streams
107
+ '-c:v', 'copy',
108
+ '-c:a', 'aac', '-b:a', '192k',
109
+ '-shortest',
110
+ output_path,
111
+ ]
112
+ subprocess.run(cmd, startupinfo=session.STARTUPINFO, check=True)
113
+ finally:
114
+ try:
115
+ os.unlink(tmp_wav)
116
+ except Exception:
117
+ pass
118
+
119
+
120
+ def bounce(output_path, audio_format, mode, audio_engine=None,
121
+ include_background=False):
122
+ """Bounce dubs to disk.
123
+
124
+ mode: 'mixdown' | 'stems' | 'clips'
125
+ audio_format: 'WAV' | 'FLAC' | 'MP3' | 'MP4'
126
+ """
127
+ audio_format = audio_format.upper()
128
+ samplerate = audio_engine.samplerate if audio_engine else 48000
129
+
130
+ if mode == 'clips':
131
+ folder = os.path.splitext(output_path)[0] + '_clips'
132
+ os.makedirs(folder, exist_ok=True)
133
+ for segment in session.SUBTITLE.get('segments', []):
134
+ dubs = segment.get('dubbing') or []
135
+ if not dubs:
136
+ continue
137
+ src = dubs[0].get('path')
138
+ if not src or not os.path.isfile(src):
139
+ continue
140
+ base = f'{segment["start"]:.3f}_{_safe_piece(segment.get("speaker", "A"))}.wav'
141
+ shutil.copy(src, os.path.join(folder, base))
142
+ return
143
+
144
+ if audio_engine is None:
145
+ raise ValueError('audio_engine is required for non-clip bounce modes')
146
+
147
+ duration = float(session.VIDEO.get('duration', 0.0))
148
+ if duration <= 0.0:
149
+ # Fall back to the latest dub endpoint
150
+ for segment in session.SUBTITLE.get('segments', []):
151
+ for dub in segment.get('dubbing') or []:
152
+ try:
153
+ duration = max(duration, float(dub.get('start', 0.0)) + float(dub.get('end', 0.0)) - float(dub.get('start', 0.0)))
154
+ except Exception:
155
+ pass
156
+
157
+ bg_buffer = None
158
+ if include_background:
159
+ bg_path = _background_audio_path()
160
+ if bg_path:
161
+ bg_buffer = _load_audio(bg_path, samplerate)
162
+ print(f'[bounce] background source: {bg_path} (frames={len(bg_buffer)}, peak={float(np.max(np.abs(bg_buffer))):.3f})')
163
+ else:
164
+ print('[bounce] include_background requested but no _background.flac found in cache')
165
+ else:
166
+ print('[bounce] background NOT included (checkbox unchecked)')
167
+
168
+ def _apply_background(buffer):
169
+ if bg_buffer is None:
170
+ return buffer
171
+ length = max(len(buffer), len(bg_buffer))
172
+ padded = np.zeros((length, 2), dtype=np.float32)
173
+ padded[:len(buffer)] += buffer
174
+ padded[:len(bg_buffer)] += bg_buffer
175
+ np.clip(padded, -1.0, 1.0, out=padded)
176
+ return padded
177
+
178
+ dub_tracks = list((getattr(audio_engine, 'speaker_tracks', {}) or {}).values())
179
+
180
+ if mode in ('background_only', 'vocals_only'):
181
+ src = _background_audio_path() if mode == 'background_only' else _vocals_audio_path()
182
+ if not src:
183
+ raise ValueError(f'No cached audio available for mode {mode}')
184
+ buffer = _load_audio(src, samplerate)
185
+ if audio_format == 'MP4':
186
+ video = session.VIDEO.get('filepath')
187
+ if not video:
188
+ raise ValueError('No video loaded for MP4 mux')
189
+ _mux_mp4(video, buffer, samplerate, output_path)
190
+ else:
191
+ _write_audio(output_path, buffer, samplerate, audio_format)
192
+ return
193
+
194
+ if mode == 'mixdown':
195
+ buffer = audio_engine.render_buffer(start=0.0, end=duration, samplerate=samplerate, tracks=dub_tracks)
196
+ buffer = _apply_background(buffer)
197
+ if audio_format == 'MP4':
198
+ video = session.VIDEO.get('filepath')
199
+ if not video:
200
+ raise ValueError('No video loaded for MP4 mux')
201
+ _mux_mp4(video, buffer, samplerate, output_path)
202
+ else:
203
+ _write_audio(output_path, buffer, samplerate, audio_format)
204
+ return
205
+
206
+ if mode == 'stems':
207
+ stem, ext = os.path.splitext(output_path)
208
+ stem_tracks = getattr(audio_engine, 'speaker_tracks', {}) or {}
209
+ if not stem_tracks:
210
+ # No per-speaker tracks — just write a single mixdown under the stem name.
211
+ buffer = audio_engine.render_buffer(start=0.0, end=duration, samplerate=samplerate, tracks=dub_tracks)
212
+ buffer = _apply_background(buffer)
213
+ _write_audio(output_path, buffer, samplerate, audio_format)
214
+ return
215
+ for speaker_name, track in stem_tracks.items():
216
+ buffer = audio_engine.render_buffer(start=0.0, end=duration, samplerate=samplerate, tracks=[track])
217
+ if include_background:
218
+ buffer = _apply_background(buffer)
219
+ speaker_path = f'{stem}-{_safe_piece(speaker_name)}{ext}'
220
+ if audio_format == 'MP4':
221
+ video = session.VIDEO.get('filepath')
222
+ if not video:
223
+ raise ValueError('No video loaded for MP4 mux')
224
+ _mux_mp4(video, buffer, samplerate, speaker_path)
225
+ else:
226
+ _write_audio(speaker_path, buffer, samplerate, audio_format)
227
+ return
228
+
229
+ raise ValueError(f'Unknown bounce mode: {mode}')
@@ -0,0 +1,84 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from subtitld.modules.session import PATH_SUBTITLD_USER_CONFIG_FILE
5
+
6
+
7
+ class Config(dict):
8
+ def __init__(self, filepath=PATH_SUBTITLD_USER_CONFIG_FILE):
9
+ self.filepath = Path(filepath)
10
+
11
+ if self.filepath.exists():
12
+ try:
13
+ with open(self.filepath, 'r', encoding='utf-8') as f:
14
+ data = json.load(f)
15
+ except json.JSONDecodeError:
16
+ data = json.loads('{}')
17
+ else:
18
+ data = json.loads('{}')
19
+
20
+ self.load_defaults()
21
+
22
+ super().__init__(data)
23
+
24
+ def __getitem__(self, key):
25
+ return super().get(key, False) # returns False if not found
26
+
27
+ def save(self):
28
+ with open(self.filepath, 'w', encoding='utf-8') as f:
29
+ json.dump(self, f, indent=4)
30
+
31
+ def load_defaults(self):
32
+ self.setdefault('timeline_zoom', 100.0)
33
+ self.setdefault('playback_speed', 1.0)
34
+ self.setdefault('repeat_activated', False)
35
+ self.setdefault('playback_repeat_duration', 10.0)
36
+ self.setdefault('playback_repeat_times', 3)
37
+ self.setdefault('timeline', {})
38
+ self.setdefault('interface_splitters', {})
39
+ self.setdefault('shortcuts', {})
40
+ self.setdefault('default_new_subtitle_duration', 10.0)
41
+ self.setdefault('new_subtitle_start_from_last', False)
42
+ self.setdefault('new_subtitle_and_play', False)
43
+ self.setdefault('new_subtitle_to_next_start', False)
44
+ self.setdefault('quality_check', {})
45
+ self.setdefault('default_values', {})
46
+ self.setdefault('videoplayer', {})
47
+ self.setdefault('export', {})
48
+ self.setdefault('autosave', {})
49
+ self.setdefault('transcription', {})
50
+ self.setdefault('translation', {})
51
+ self.setdefault('dubbing', {})
52
+
53
+ @staticmethod
54
+ def get_valid_keys():
55
+ return {
56
+ 'timeline_zoom',
57
+ 'playback_speed',
58
+ 'repeat_activated',
59
+ 'playback_repeat_duration',
60
+ 'playback_repeat_times',
61
+ 'timeline',
62
+ 'interface_splitters',
63
+ 'shortcuts',
64
+ 'default_new_subtitle_duration',
65
+ 'new_subtitle_start_from_last',
66
+ 'new_subtitle_and_play',
67
+ 'new_subtitle_to_next_start',
68
+ 'quality_check',
69
+ 'default_values',
70
+ 'videoplayer',
71
+ 'export',
72
+ 'autosave',
73
+ 'transcription',
74
+ 'translation',
75
+ 'dubbing',
76
+ }
77
+
78
+ @staticmethod
79
+ def validate_config_keys(config_data):
80
+ valid_keys = Config.get_valid_keys()
81
+ invalid_keys = set(config_data.keys()) - valid_keys
82
+ return len(invalid_keys) == 0, invalid_keys
83
+
84
+