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,870 @@
1
+ import os
2
+ import copy
3
+ import hashlib
4
+ import shutil
5
+ import zipfile
6
+ from docx import Document
7
+ import json
8
+ import pycaption
9
+ from pycaption.exceptions import CaptionReadSyntaxError, CaptionReadNoCaptions
10
+ import chardet
11
+ import pysubs2
12
+ import datetime
13
+
14
+ from PySide6.QtWidgets import QFileDialog
15
+ from PySide6.QtCore import QThread, Signal, QByteArray, QBuffer, QIODevice
16
+ from PySide6.QtGui import QImage
17
+
18
+ from subtitld.modules import timecode
19
+ from subtitld.modules import session
20
+ from subtitld.modules import waveform
21
+ from subtitld.modules import usf
22
+ from subtitld.modules import utils
23
+
24
+
25
+ def _safe_asset_name(name):
26
+ safe = ''.join(c if c.isalnum() or c in '-_.' else '_' for c in str(name))
27
+ return safe or 'unnamed'
28
+
29
+
30
+ def _usfx_extract_dir(usfx_path):
31
+ project_hash = hashlib.md5(os.path.abspath(usfx_path).encode('utf-8')).hexdigest()
32
+ path = os.path.join(session.PATH_SUBTITLD_USER_CACHE, 'usfx', project_hash)
33
+ os.makedirs(path, exist_ok=True)
34
+ return path
35
+
36
+
37
+ USFX_OPTION_DEFAULTS = {
38
+ 'include_speaker_images': True,
39
+ 'include_waveform_cache': False,
40
+ 'include_original_audio': False,
41
+ 'include_processed_audio': False,
42
+ 'include_original_video': False,
43
+ }
44
+
45
+
46
+ def _get_usfx_options():
47
+ """Merge stored defaults with fallback values."""
48
+ stored = {}
49
+ if isinstance(session.CONFIG, dict):
50
+ stored = session.CONFIG.get('default_values', {}).get('usfx_options') or {}
51
+ return {**USFX_OPTION_DEFAULTS, **stored}
52
+
53
+ from subtitld.interface.translation import _
54
+
55
+
56
+ class ThreadGenerateHashOfVideo(QThread):
57
+ """Thread to extract time positions of scenes"""
58
+ response = Signal(list)
59
+ filepath = ''
60
+
61
+ def run(self):
62
+ if self.filepath and os.path.isfile(self.filepath):
63
+ md5_object = hashlib.md5()
64
+ block_size = 128 * md5_object.block_size
65
+ file_object = open(self.filepath, 'rb')
66
+ chunk = file_object.read(block_size)
67
+ while chunk:
68
+ md5_object.update(chunk)
69
+ chunk = file_object.read(block_size)
70
+
71
+ md5_hash = md5_object.hexdigest()
72
+
73
+ self.response.emit([self.filepath, md5_hash])
74
+
75
+
76
+ def process_subtitles_file(subtitle_file=False, subtitle_format='SRT'):
77
+ """Definition to process subtitle file. It returns a dict with the subtitles."""
78
+ segments_list = []
79
+
80
+ if subtitle_file and os.path.isfile(subtitle_file):
81
+ if subtitle_file.lower().endswith(('.srt')):
82
+ enc = chardet.detect(open(subtitle_file, 'rb').read())['encoding']
83
+ with open(subtitle_file, mode='rb') as srt_file:
84
+ srt_content = srt_file.read().decode(enc, 'ignore')
85
+
86
+ if ' -> ' in srt_content:
87
+ srt_content = srt_content.replace(' -> ', ' --> ')
88
+
89
+ srt_reader = pycaption.SRTReader().read(srt_content)
90
+ languages = srt_reader.get_languages()
91
+ language = languages[0]
92
+ captions = srt_reader.get_captions(language)
93
+ for caption in captions:
94
+ segments_list.append({
95
+ 'start': caption.start / 1000000,
96
+ 'end': caption.end / 1000000,
97
+ 'text': caption.get_text()
98
+ })
99
+
100
+ elif subtitle_file.lower().endswith(('.vtt', '.webvtt')):
101
+ subtitle_format = 'VTT'
102
+ with open(subtitle_file, encoding='utf-8') as vtt_file:
103
+ try:
104
+ vtt_reader = pycaption.WebVTTReader().read(vtt_file.read())
105
+ languages = vtt_reader.get_languages()
106
+ language = languages[0]
107
+ captions = vtt_reader.get_captions(language)
108
+ for caption in captions:
109
+ segments_list.append({
110
+ 'start': caption.start / 1000000,
111
+ 'end': caption.end / 1000000,
112
+ 'text': caption.get_text()
113
+ })
114
+ except CaptionReadSyntaxError:
115
+ with open(subtitle_file, encoding='utf-8') as fileobj:
116
+ subfile = pysubs2.SSAFile.from_string(fileobj.read())
117
+
118
+ for event in subfile.events:
119
+ segments_list.append({
120
+ 'start': event.start / 1000.0,
121
+ 'end': (event.start / 1000.0) + (event.duration / 1000.0),
122
+ 'text': event.text
123
+ })
124
+ except CaptionReadNoCaptions:
125
+ pass
126
+
127
+ # error_message = QMessageBox()
128
+ # error_message.setWindowTitle('There is a problem with this file and can not be opened.')
129
+
130
+ elif subtitle_file.lower().endswith(('.ttml', '.dfxp', '.xml', '.itt')):
131
+ if '<tt ' in open(subtitle_file).read():
132
+ subtitle_format = 'TTML'
133
+ else:
134
+ subtitle_format = 'DFXP'
135
+
136
+ with open(subtitle_file, encoding='utf-8') as dfxp_file:
137
+ dfxp_reader = pycaption.DFXPReader().read(dfxp_file.read())
138
+ languages = dfxp_reader.get_languages()
139
+ language = languages[0]
140
+ captions = dfxp_reader.get_captions(language)
141
+ for caption in captions:
142
+ segments_list.append({
143
+ 'start': caption.start / 1000000,
144
+ 'end': caption.end / 1000000,
145
+ 'text': caption.get_text()
146
+ })
147
+
148
+ elif subtitle_file.lower().endswith(('.smi', '.sami')):
149
+ subtitle_format = 'SAMI'
150
+ with open(subtitle_file, encoding='utf-8') as sami_file:
151
+ sami_reader = pycaption.SAMIReader().read(sami_file.read())
152
+ languages = sami_reader.get_languages()
153
+ language = languages[0]
154
+ captions = sami_reader.get_captions(language)
155
+ for caption in captions:
156
+ segments_list.append({
157
+ 'start': caption.start / 1000000,
158
+ 'end': caption.end / 1000000,
159
+ 'text': caption.get_text()
160
+ })
161
+
162
+ # elif subtitle_file.lower().endswith(('.sbv')):
163
+ # subtitle_format = 'SBV'
164
+ # with open(subtitle_file, encoding='utf-8') as sbv_file:
165
+ # from captionstransformer.sbv import Reader
166
+ # captions = Reader(sbv_file).read()
167
+ # for caption in captions:
168
+ # segments_list.append([(caption.start - datetime.datetime(1900, 1, 1)).total_seconds(), caption.duration.total_seconds(), caption.text.strip()])
169
+
170
+ elif subtitle_file.lower().endswith(('.ass', '.ssa', '.sub')):
171
+ if subtitle_file.lower().endswith(('.ass', '.ssa')):
172
+ subtitle_format = 'ASS'
173
+ with open(subtitle_file, encoding='utf-8') as fileobj:
174
+ subfile = pysubs2.SSAFile.from_string(fileobj.read())
175
+ elif subtitle_file.lower().endswith(('.sub')):
176
+ subtitle_format = 'SUB'
177
+ enc = chardet.detect(open(subtitle_file, 'rb').read())['encoding']
178
+ subfile = pysubs2.SSAFile.from_string(open(subtitle_file, mode='rb').read().decode(enc, 'ignore'))
179
+
180
+ for event in subfile.events:
181
+ segments_list.append({
182
+ 'start': event.start / 1000.0,
183
+ 'end': (event.start / 1000.0) + (event.duration / 1000.0),
184
+ 'text': event.plaintext
185
+ })
186
+
187
+ elif subtitle_file.lower().endswith(('.scc')):
188
+ subtitle_format = 'SCC'
189
+
190
+ with open(subtitle_file, encoding='utf-8') as scc_file:
191
+ scc_reader = pycaption.SCCReader().read(scc_file.read())
192
+ languages = scc_reader.get_languages()
193
+ language = languages[0]
194
+ captions = scc_reader.get_captions(language)
195
+ for caption in captions:
196
+ segments_list.append({
197
+ 'start': caption.start / 1000000,
198
+ 'end': caption.end / 1000000,
199
+ 'text': caption.get_text()
200
+ })
201
+
202
+ elif subtitle_file.lower().endswith(('.usf')):
203
+ subtitle_format = 'USF'
204
+ reader = usf.USFReader()
205
+ segments_list = reader.read(open(subtitle_file).read())
206
+ for speaker_name, speaker_data in reader.speakers.items():
207
+ existing = session.SPEAKERS.get(speaker_name, {})
208
+ if 'color' in speaker_data:
209
+ existing['color'] = speaker_data['color']
210
+ if isinstance(speaker_data.get('dubbing'), dict):
211
+ existing['dubbing'] = speaker_data['dubbing']
212
+ image_bytes = speaker_data.get('image_bytes')
213
+ if image_bytes:
214
+ qimg = QImage()
215
+ if qimg.loadFromData(image_bytes):
216
+ existing['image'] = qimg
217
+ session.SPEAKERS[speaker_name] = existing
218
+
219
+ if not isinstance(session.FORMAT, dict):
220
+ session.FORMAT = {}
221
+ session.FORMAT['format'] = 'USF'
222
+ session.FORMAT['options'] = {
223
+ 'embed_speaker_images': reader.has_embedded_images,
224
+ 'embed_audio_clips': reader.has_embedded_audio_clips,
225
+ }
226
+
227
+ dub_cache_dir = os.path.join(session.PATH_SUBTITLD_USER_CACHE, 'waveform')
228
+ os.makedirs(dub_cache_dir, exist_ok=True)
229
+ for segment in segments_list:
230
+ for dub in segment.get('dubbing', []) or []:
231
+ raw = dub.pop('_embedded_bytes', None)
232
+ fmt = dub.pop('_embedded_format', 'wav')
233
+ if raw and dub.get('uid'):
234
+ target = os.path.join(dub_cache_dir, f'{dub["uid"]}.{fmt}')
235
+ if not os.path.exists(target):
236
+ try:
237
+ with open(target, 'wb') as wf:
238
+ wf.write(raw)
239
+ except Exception:
240
+ pass
241
+ dub['path'] = target
242
+
243
+ elif subtitle_file.lower().endswith(('.usfx')):
244
+ subtitle_format = 'USFX'
245
+ extract_dir = _usfx_extract_dir(subtitle_file)
246
+
247
+ with zipfile.ZipFile(subtitle_file, 'r') as zf:
248
+ zf.extractall(extract_dir)
249
+
250
+ inner_usf = os.path.join(extract_dir, 'subtitles.usf')
251
+ if not os.path.exists(inner_usf):
252
+ for fname in os.listdir(extract_dir):
253
+ if fname.lower().endswith('.usf'):
254
+ inner_usf = os.path.join(extract_dir, fname)
255
+ break
256
+
257
+ reader = usf.USFReader()
258
+ with open(inner_usf, encoding='utf-8') as fh:
259
+ segments_list = reader.read(fh.read())
260
+
261
+ speakers_dir = os.path.join(extract_dir, 'assets', 'speakers')
262
+ for speaker_name, speaker_data in reader.speakers.items():
263
+ existing = session.SPEAKERS.get(speaker_name, {})
264
+ if 'color' in speaker_data:
265
+ existing['color'] = speaker_data['color']
266
+ if isinstance(speaker_data.get('dubbing'), dict):
267
+ existing['dubbing'] = speaker_data['dubbing']
268
+ image_bytes = speaker_data.get('image_bytes')
269
+ if image_bytes:
270
+ qimg = QImage()
271
+ if qimg.loadFromData(image_bytes):
272
+ existing['image'] = qimg
273
+ else:
274
+ safe_name = _safe_asset_name(speaker_name)
275
+ for ext in ('png', 'jpg', 'jpeg'):
276
+ candidate = os.path.join(speakers_dir, f'{safe_name}.{ext}')
277
+ if os.path.exists(candidate):
278
+ qimg = QImage()
279
+ if qimg.load(candidate):
280
+ existing['image'] = qimg
281
+ break
282
+ session.SPEAKERS[speaker_name] = existing
283
+
284
+ for segment in segments_list:
285
+ for dub in segment.get('dubbing', []) or []:
286
+ path = dub.get('path', '')
287
+ if path and not os.path.isabs(path):
288
+ dub['path'] = os.path.join(extract_dir, path)
289
+
290
+ # If the bundle ships the original video, adopt it as the project's video
291
+ # (this lets a USFX opened on another machine find its source).
292
+ bundled_video_dir = os.path.join(extract_dir, 'assets', 'video')
293
+ if os.path.isdir(bundled_video_dir):
294
+ for fname in sorted(os.listdir(bundled_video_dir)):
295
+ candidate = os.path.join(bundled_video_dir, fname)
296
+ if os.path.isfile(candidate):
297
+ if not isinstance(session.VIDEO, dict):
298
+ session.VIDEO = {}
299
+ session.VIDEO['filepath'] = candidate
300
+ break
301
+
302
+ # Copy bundled caches to the cache directories, keyed against the current video.
303
+ current_video = session.VIDEO.get('filepath') if isinstance(session.VIDEO, dict) else None
304
+ current_key = utils.get_cache_key(current_video) if current_video else None
305
+ if current_key:
306
+ wf_bundled = os.path.join(extract_dir, 'assets', 'waveform.npy')
307
+ if os.path.isfile(wf_bundled):
308
+ wf_dir = os.path.join(session.PATH_SUBTITLD_USER_CACHE, 'waveform')
309
+ os.makedirs(wf_dir, exist_ok=True)
310
+ wf_target = os.path.join(wf_dir, f'{current_key}_waveform.npy')
311
+ if not os.path.exists(wf_target):
312
+ try:
313
+ shutil.copy(wf_bundled, wf_target)
314
+ except Exception:
315
+ pass
316
+
317
+ for kind in ('original', 'vocals', 'background'):
318
+ bundled = os.path.join(extract_dir, 'assets', 'audio', f'{kind}.flac')
319
+ if os.path.isfile(bundled):
320
+ target = os.path.join(session.PATH_SUBTITLD_DATA_AUDIOSEPARATION, f'{current_key}_{kind}.flac')
321
+ if not os.path.exists(target):
322
+ try:
323
+ shutil.copy(bundled, target)
324
+ except Exception:
325
+ pass
326
+
327
+ if not isinstance(session.FORMAT, dict):
328
+ session.FORMAT = {}
329
+ session.FORMAT['format'] = 'USFX'
330
+ session.FORMAT['options'] = {}
331
+
332
+ elif subtitle_file.lower().endswith(('.json')):
333
+ subtitle_format = 'JSON'
334
+
335
+ with open(subtitle_file, encoding='utf-8') as json_file:
336
+ source_content = json.load(json_file)
337
+
338
+ for segment in source_content['segments']:
339
+ segments_list.append({
340
+ 'start': segment['start'],
341
+ 'end': segment['end'],
342
+ 'text': segment['text'],
343
+ 'speaker': segment.get('speaker', 'A'),
344
+ 'translations': segment.get('translations', {})
345
+ })
346
+ if not 'config' in session.FORMAT:
347
+ session.FORMAT['config'] = {
348
+ 'standard': 'Whisper'
349
+ }
350
+
351
+ if not 'format' in session.FORMAT:
352
+ session.FORMAT['format'] = subtitle_format
353
+
354
+ return segments_list, subtitle_format
355
+
356
+
357
+ def process_video_file(video_file=False):
358
+ """Function to process video file. It returns a dict with video properties."""
359
+ video_metadata = {}
360
+ json_result = waveform.ffmpeg_load_metadata(video_file)
361
+ video_metadata['audio'] = False
362
+ video_metadata['audio_is_present'] = False
363
+ video_metadata['waveform'] = {}
364
+ video_metadata['duration'] = float(json_result.get('format', {}).get('duration', '0.01'))
365
+ for stream in json_result.get('streams', []):
366
+ if stream.get('codec_type', '') == 'video' and not stream.get('codec_name', 'png') in ['png', 'mjpeg']:
367
+ video_metadata['width'] = int(stream.get('width', 640))
368
+ video_metadata['height'] = int(stream.get('height', 480))
369
+ video_metadata['framerate'] = int(stream.get('r_frame_rate', '1/30').split('/', 1)[0]) / int(stream.get('r_frame_rate', '1/30').split('/', 1)[-1])
370
+ elif stream.get('codec_type', '') in ['subtitle'] and not video_metadata.get('subtitles', False):
371
+ video_metadata['subtitles'] = waveform.ffmpeg_extract_subtitle(video_file, stream.get('index', 2))
372
+ # TODO: select what language if multiple embedded subtitles
373
+ elif stream.get('codec_type', '') in ['audio']:
374
+ video_metadata['audio_is_present'] = True
375
+ video_metadata['filepath'] = video_file
376
+ video_metadata['scenes'] = []
377
+
378
+ return video_metadata
379
+
380
+
381
+ def import_file(filename=False, subtitle_format=False): # , fit_to_length=False, length=.01, distribute_fixed_duration=False):
382
+ """Function to import file into the subtitle project."""
383
+ session.SUBTITLE['segments'] = []
384
+ if filename:
385
+ if filename.lower().endswith(('.txt')):
386
+ subtitle_format = 'TXT'
387
+ with open(filename) as txt_file:
388
+ # clean("some input",
389
+ # fix_unicode=True, # fix various unicode errors
390
+ # to_ascii=True, # transliterate to closest ASCII representation
391
+ # lower=True, # lowercase text
392
+ # no_line_breaks=False, # fully strip line breaks as opposed to only normalizing them
393
+ # no_urls=False, # replace all URLs with a special token
394
+ # no_emails=False, # replace all email addresses with a special token
395
+ # no_phone_numbers=False, # replace all phone numbers with a special token
396
+ # no_numbers=False, # replace all numbers with a special token
397
+ # no_digits=False, # replace all digits with a special token
398
+ # no_currency_symbols=False, # replace all currency symbols with a special token
399
+ # no_punct=False, # fully remove punctuation
400
+ # replace_with_url="<URL>",
401
+ # replace_with_email="<EMAIL>",
402
+ # replace_with_phone_number="<PHONE>",
403
+ # replace_with_number="<NUMBER>",
404
+ # replace_with_digit="0",
405
+ # replace_with_currency_symbol="<CUR>",
406
+ # lang="en" # set to 'de' for German special handling
407
+ # )
408
+
409
+ # txt_content = clean(txt_file.read())
410
+ txt_content = txt_file.read()
411
+ pos = 0.0
412
+ for phrase in txt_content.split('. '):
413
+ session.SUBTITLE['segments'].append({
414
+ 'start': pos,
415
+ 'end': pos + 5.0,
416
+ 'text': phrase + '.'
417
+ })
418
+ pos += 5.0
419
+
420
+ elif filename.lower().endswith(('.srt')):
421
+ subtitle_format = 'SRT'
422
+ session.SUBTITLE['segments'] += process_subtitles_file(subtitle_file=filename, subtitle_format=subtitle_format)[0]
423
+
424
+ elif filename.lower().endswith(('.docx')):
425
+ subtitle_format = 'DOCX'
426
+ txt_content = ''
427
+
428
+ doc = Document(filename)
429
+ for par in doc.paragraphs:
430
+ txt_content += par.text
431
+
432
+ pos = 0.0
433
+ for phrase in txt_content.split('. '):
434
+ session.SUBTITLE['segments'].append({
435
+ 'start': pos,
436
+ 'end': pos + 5.0,
437
+ 'text': phrase + '.'
438
+ })
439
+ pos += 5.0
440
+
441
+ session.SUBTITLE['segments'] += process_subtitles_file(subtitle_file=filename, subtitle_format=subtitle_format)[0]
442
+
443
+ return session.SUBTITLE['segments'], subtitle_format
444
+
445
+
446
+ def export_file(filename=False, export_format='TXT', options=False):
447
+ """Function to export file. A filepath and a subtitle dict is given."""
448
+ if session.SUBTITLE['segments'] and filename:
449
+ if export_format in ['.txt']:
450
+ final_txt = ''
451
+ for sub in session.SUBTITLE['segments']:
452
+ final_txt += sub[2].replace('\n', ' ') + ' '
453
+ if options:
454
+ if options.get('new_line', False):
455
+ final_txt = final_txt.replace('. ', '.\n')
456
+ final_txt = final_txt.replace('! ', '!\n')
457
+ final_txt = final_txt.replace('? ', '?\n')
458
+ with open(filename, mode='w', encoding='utf-8') as txt_file:
459
+ txt_file.write(final_txt)
460
+ elif export_format in ['.kdenlive']:
461
+ final_xml = '''<?xml version='1.0' encoding='utf-8'?><mlt LC_NUMERIC="C" producer="main_bin" version="6.26.1" root="/home/jonata"><profile frame_rate_num="25" sample_aspect_num="1" display_aspect_den="9" colorspace="709" progressive="1" description="HD 1080p 25 fps" display_aspect_num="16" frame_rate_den="1" width="1920" height="1080" sample_aspect_den="1"/>'''
462
+
463
+ i = 0
464
+ for sub in session.SUBTITLE['segments']:
465
+ final_xml += '''<producer id="producer{i}" in="{zerotime}" out="{out}">
466
+ <property name="length">{length}</property>
467
+ <property name="eof">pause</property>
468
+ <property name="resource"/>
469
+ <property name="progressive">1</property>
470
+ <property name="aspect_ratio">1</property>
471
+ <property name="seekable">1</property>
472
+ <property name="mlt_service">kdenlivetitle</property>
473
+ <property name="kdenlive:duration">125</property>
474
+ <property name="kdenlive:clipname">{clipname}</property>
475
+ <property name="xmldata">&lt;kdenlivetitle duration="125" LC_NUMERIC="C" width="1920" height="1080" out="124"> &lt;item type="QGraphicsTextItem" z-index="0"> &lt;position x="784" y="910"> &lt;transform>1,0,0,0,1,0,0,0,1&lt;/transform> &lt;/position>
476
+ &lt;content shadow="0;#64000000;3;3;3" font-underline="0" box-height="62" font-outline-color="0,0,0,255" font="Ubuntu" letter-spacing="0" font-pixel-size="54" font-italic="0" typewriter="0;2;1;0;0" alignment="1" font-weight="50" font-outline="0"
477
+ box-width="351.719" font-color="255,255,255,255">{content}&lt;/content> &lt;/item> &lt;startviewport rect="0,0,1920,1080"/> &lt;endviewport rect="0,0,1920,1080"/> &lt;background color="0,0,0,0"/> &lt;/kdenlivetitle>
478
+ </property>
479
+ <property name="kdenlive:folderid">-1</property>
480
+ <property name="kdenlive:id">{id}</property>
481
+ <property name="force_reload">0</property>
482
+ <property name="meta.media.width">1920</property>
483
+ <property name="meta.media.height">1080</property>
484
+ </producer>'''.format(i=i, id=i + 2, length=int(sub[1] * 25), clipname='Subtitle {i}'.format(i=i), content=sub[2], zerotime=str(timecode.Timecode('1000', start_seconds=0.001, fractional=True)), out=str(timecode.Timecode('1000', start_seconds=sub[1], fractional=True)))
485
+ i += 1
486
+
487
+ final_xml += '''<playlist id="main_bin">
488
+ <property name="kdenlive:docproperties.activeTrack">2</property>
489
+ <property name="kdenlive:docproperties.audioChannels">2</property>
490
+ <property name="kdenlive:docproperties.audioTarget">-1</property>
491
+ <property name="kdenlive:docproperties.disablepreview">0</property>
492
+ <property name="kdenlive:docproperties.documentid">1621801540856</property>
493
+ <property name="kdenlive:docproperties.enableTimelineZone">0</property>
494
+ <property name="kdenlive:docproperties.enableexternalproxy">0</property>
495
+ <property name="kdenlive:docproperties.enableproxy">0</property>
496
+ <property name="kdenlive:docproperties.externalproxyparams">../Sub;;S03.MP4;../Clip;;.MXF</property>
497
+ <property name="kdenlive:docproperties.generateimageproxy">0</property>
498
+ <property name="kdenlive:docproperties.generateproxy">0</property>
499
+ <property name="kdenlive:docproperties.groups">[ ]
500
+ </property>
501
+ <property name="kdenlive:docproperties.kdenliveversion">21.04.0</property>
502
+ <property name="kdenlive:docproperties.position">372</property>
503
+ <property name="kdenlive:docproperties.previewextension"/>
504
+ <property name="kdenlive:docproperties.previewparameters"/>
505
+ <property name="kdenlive:docproperties.profile">atsc_1080p_25</property>
506
+ <property name="kdenlive:docproperties.proxyextension"/>
507
+ <property name="kdenlive:docproperties.proxyimageminsize">2000</property>
508
+ <property name="kdenlive:docproperties.proxyimagesize">800</property>
509
+ <property name="kdenlive:docproperties.proxyminsize">1000</property>
510
+ <property name="kdenlive:docproperties.proxyparams"/>
511
+ <property name="kdenlive:docproperties.scrollPos">0</property>
512
+ <property name="kdenlive:docproperties.seekOffset">30000</property>
513
+ <property name="kdenlive:docproperties.version">1</property>
514
+ <property name="kdenlive:docproperties.verticalzoom">1</property>
515
+ <property name="kdenlive:docproperties.videoTarget">-1</property>
516
+ <property name="kdenlive:docproperties.zonein">0</property>
517
+ <property name="kdenlive:docproperties.zoneout">75</property>
518
+ <property name="kdenlive:docproperties.zoom">8</property>
519
+ <property name="kdenlive:expandedFolders"/>
520
+ <property name="kdenlive:documentnotes"/>
521
+ <property name="xml_retain">1</property>\n'''
522
+
523
+ i = 0
524
+ for sub in session.SUBTITLE['segments']:
525
+ final_xml += '''<entry producer="producer{i}" in="{zerotime}" out="{out}"/>\n'''.format(i=i, zerotime=str(timecode.Timecode('1000', start_seconds=0.001, fractional=True)), out=str(timecode.Timecode('1000', start_seconds=sub[1], fractional=True)))
526
+ i += 1
527
+
528
+ final_xml += '''</playlist>
529
+ <producer id="black_track" in="00:00:00.000" out="00:20:12.120">
530
+ <property name="length">2147483647</property>
531
+ <property name="eof">continue</property>
532
+ <property name="resource">black</property>
533
+ <property name="aspect_ratio">1</property>
534
+ <property name="mlt_service">color</property>
535
+ <property name="mlt_image_format">rgb24a</property>
536
+ <property name="set.test_audio">0</property>
537
+ </producer>
538
+ <playlist id="playlist0">
539
+ <property name="kdenlive:audio_track">1</property>
540
+ </playlist>
541
+ <playlist id="playlist1"/>
542
+ <tractor id="tractor0" in="00:00:00.000">
543
+ <property name="kdenlive:audio_track">1</property>
544
+ <property name="kdenlive:trackheight">69</property>
545
+ <property name="kdenlive:timeline_active">1</property>
546
+ <property name="kdenlive:collapsed">0</property>
547
+ <property name="kdenlive:thumbs_format"/>
548
+ <property name="kdenlive:audio_rec"/>
549
+ <track hide="video" producer="playlist0"/>
550
+ <track hide="video" producer="playlist1"/>
551
+ </tractor>
552
+ <playlist id="playlist2">
553
+ <property name="kdenlive:audio_track">1</property>
554
+ </playlist>
555
+ <playlist id="playlist3"/>
556
+ <tractor id="tractor1" in="00:00:00.000">
557
+ <property name="kdenlive:audio_track">1</property>
558
+ <property name="kdenlive:trackheight">69</property>
559
+ <property name="kdenlive:timeline_active">1</property>
560
+ <property name="kdenlive:collapsed">0</property>
561
+ <property name="kdenlive:thumbs_format"/>
562
+ <property name="kdenlive:audio_rec"/>
563
+ <track hide="video" producer="playlist2"/>
564
+ <track hide="video" producer="playlist3"/>
565
+ </tractor>
566
+ <playlist id="playlist4"/>
567
+ <playlist id="playlist5"/>
568
+ <tractor id="tractor2" in="00:00:00.000" out="00:00:12.080">
569
+ <property name="kdenlive:trackheight">69</property>
570
+ <property name="kdenlive:timeline_active">1</property>
571
+ <property name="kdenlive:collapsed">0</property>
572
+ <property name="kdenlive:thumbs_format"/>
573
+ <property name="kdenlive:audio_rec"/>
574
+ <track hide="audio" producer="playlist4"/>
575
+ <track producer="playlist5"/>
576
+ </tractor>
577
+ <playlist id="playlist6">'''
578
+ i = 0
579
+ last_intime = 0
580
+ for sub in session.SUBTITLE['segments']:
581
+ last_intime = sub['start'] - last_intime
582
+ if last_intime:
583
+ final_xml += '''
584
+ <blank length="{intime}"/>'''.format(intime=str(timecode.Timecode('1000', start_seconds=last_intime, fractional=True)))
585
+ final_xml += '''
586
+ <entry producer="producer{i}" in="{zerotime}" out="{out}">
587
+ <property name="kdenlive:id">{id}</property>
588
+ </entry>'''.format(i=i, id=i + 2, zerotime=str(timecode.Timecode('1000', start_seconds=0.001, fractional=True)), out=str(timecode.Timecode('1000', start_seconds=sub[1], fractional=True)))
589
+ last_intime = sub['end']
590
+ i += 1
591
+
592
+ final_xml += '''
593
+ </playlist>
594
+ <playlist id="playlist7"/>
595
+ <tractor id="tractor3" in="00:00:00.000">
596
+ <property name="kdenlive:trackheight">69</property>
597
+ <property name="kdenlive:timeline_active">1</property>
598
+ <property name="kdenlive:collapsed">0</property>
599
+ <property name="kdenlive:thumbs_format"/>
600
+ <property name="kdenlive:audio_rec"/>
601
+ <track hide="audio" producer="playlist6"/>
602
+ <track producer="playlist7"/>
603
+ </tractor>
604
+ <tractor id="tractor4" global_feed="1" in="00:00:00.000" out="00:20:12.120">
605
+ <track producer="black_track"/>
606
+ <track producer="tractor0"/>
607
+ <track producer="tractor1"/>
608
+ <track producer="tractor2"/>
609
+ <track producer="tractor3"/>
610
+ </tractor>
611
+ </mlt>'''
612
+
613
+ with open(filename, mode='w', encoding='utf-8') as txt_file:
614
+ txt_file.write(final_xml)
615
+
616
+
617
+ def save_file(final_file, subtitle_format='USFX', language='en'):
618
+ """Function to save the subtitle project. A subtitles dict and the format is given."""
619
+ if session.SUBTITLE['segments']:
620
+ # if not final_file.lower().endswith('.' + format.lower()):
621
+ # final_file += '.' + format.lower()
622
+
623
+ if not 'format' in session.FORMAT:
624
+ session.FORMAT['format'] = subtitle_format
625
+
626
+ if subtitle_format in ['SRT', 'DFXP', 'TTML', 'SAMI', 'SCC', 'VTT']:
627
+ captions = pycaption.CaptionList()
628
+ for sub in session.SUBTITLE['segments']:
629
+ # skip extra blank lines
630
+ nodes = [pycaption.CaptionNode.create_text(sub['text'])]
631
+ caption = pycaption.Caption(start=sub['start'] * 1000000, end=(sub['end']) * 1000000, nodes=nodes)
632
+ captions.append(caption)
633
+ caption_set = pycaption.CaptionSet({language: captions})
634
+
635
+ if subtitle_format == 'SRT':
636
+ open(final_file, mode='w', encoding='utf-8').write(pycaption.SRTWriter().write(caption_set))
637
+ elif subtitle_format in ['DFXP', 'TTML']:
638
+ open(final_file, mode='w', encoding='utf-8').write(pycaption.DFXPWriter().write(caption_set))
639
+ elif subtitle_format == 'SAMI':
640
+ open(final_file, mode='w', encoding='utf-8').write(pycaption.SAMIWriter().write(caption_set))
641
+ elif subtitle_format == 'SCC':
642
+ open(final_file, mode='w', encoding='utf-8').write(pycaption.SCCWriter().write(caption_set))
643
+ elif subtitle_format == 'VTT':
644
+ open(final_file, mode='w', encoding='utf-8').write(pycaption.WebVTTWriter().write(caption_set))
645
+
646
+ elif subtitle_format in ['ASS', 'SBV', 'XML', 'SUB']:
647
+ if subtitle_format in ['ASS', 'SUB']:
648
+ assfile = pysubs2.SSAFile()
649
+ index = 0
650
+ for sub in reversed(sorted(session.SUBTITLE['segments'])):
651
+ assfile.insert(
652
+ index,
653
+ pysubs2.SSAEvent(
654
+ start=int(sub['start'] * 1000),
655
+ end=int(sub['end'] * 1000),
656
+ text=sub['text'].replace('\n', ' ')
657
+ )
658
+ )
659
+ if subtitle_format == 'SUB':
660
+ assfile.save(final_file, subtitle_format='microdvd')
661
+ else:
662
+ assfile.save(final_file)
663
+ # else:
664
+ # if subtitle_format == 'SBV':
665
+ # from captionstransformer.sbv import Writer
666
+ # elif subtitle_format == 'XML':
667
+ # from captionstransformer.transcript import Writer
668
+ # writer = Writer(open(final_file, mode='w', encoding='utf-8'))
669
+ # captions = []
670
+ # for cap in session.SUBTITLE['segments']:
671
+ # caption = captionstransformer.core.Caption()
672
+ # caption.start = captionstransformer.core.get_date(second=int(cap[0] // 1), millisecond=int((cap[0] % 1) * 1000))
673
+ # caption.duration = captionstransformer.core.get_date(second=int(cap[1] // 1), millisecond=int((cap[1] % 1) * 1000)) - captionstransformer.core.get_date()
674
+ # caption.text = cap[2]
675
+ # captions.append(caption)
676
+ # writer.set_captions(captions)
677
+ # writer.write()
678
+
679
+ elif subtitle_format in ['JSON']:
680
+ if session.FORMAT.get('options', {}).get('standard', 'Whisper') == 'Whisper':
681
+ open(final_file, mode='w', encoding='utf-8').write(json.dumps(session.SUBTITLE, indent=4))
682
+ elif session.FORMAT['options'].get('standard', 'Whisper') == 'AD':
683
+ new_json_dict = {
684
+ 'metadata': {
685
+ 'framerate': session.VIDEO.get('framerate', 25),
686
+ 'video_name': os.path.basename(session.VIDEO.get('filepath', ''))
687
+ },
688
+ 'description_cues': [],
689
+ }
690
+ for i, segment in enumerate(session.SUBTITLE['segments']):
691
+ new_json_dict['description_cues'].append({
692
+ 'cue_number': i,
693
+ 'text': segment['text'],
694
+ 'start_frame': int(segment['start'] * session.VIDEO.get('framerate', 25)),
695
+ 'end_frame': int(segment['end'] * session.VIDEO.get('framerate', 25)),
696
+ 'start_time_smpte': str(timecode.Timecode(session.VIDEO.get('framerate', 25), start_seconds=segment['start'], fractional=False)),
697
+ 'end_time_smpte': str(timecode.Timecode(session.VIDEO.get('framerate', 25), start_seconds=segment['end'], fractional=False)),
698
+ 'start_time': str(timecode.Timecode(session.VIDEO.get('framerate', 25), start_seconds=segment['start'], fractional=True)),
699
+ 'end_time': str(timecode.Timecode(session.VIDEO.get('framerate', 25), start_seconds=segment['end'], fractional=True)),
700
+ 'speaker': segment.get('speaker', 'A')
701
+ })
702
+
703
+ open(final_file, mode='w', encoding='utf-8').write(json.dumps(new_json_dict, indent=4))
704
+
705
+
706
+ elif subtitle_format in ['USF']:
707
+ options = session.FORMAT.get('options', {}) if isinstance(session.FORMAT, dict) else {}
708
+ embed_speaker_images = bool(options.get('embed_speaker_images', False))
709
+ embed_audio_clips = bool(options.get('embed_audio_clips', False))
710
+
711
+ writer_speakers = {}
712
+ for name, data in session.SPEAKERS.items():
713
+ if not isinstance(data, dict):
714
+ continue
715
+ entry = {}
716
+ if data.get('color'):
717
+ entry['color'] = data['color']
718
+ if isinstance(data.get('dubbing'), dict):
719
+ entry['dubbing'] = data['dubbing']
720
+ if embed_speaker_images and isinstance(data.get('image'), QImage):
721
+ buf = QByteArray()
722
+ qbuf = QBuffer(buf)
723
+ qbuf.open(QIODevice.WriteOnly)
724
+ if data['image'].save(qbuf, 'PNG'):
725
+ entry['image_bytes'] = bytes(buf)
726
+ qbuf.close()
727
+ if entry:
728
+ writer_speakers[name] = entry
729
+
730
+ open(final_file, mode='w', encoding='utf-8').write(usf.USFWriter().write(
731
+ session.SUBTITLE['segments'],
732
+ speakers=writer_speakers,
733
+ language=language,
734
+ embed_audio_clips=embed_audio_clips,
735
+ ))
736
+
737
+ elif subtitle_format in ['USFX']:
738
+ usfx_options = _get_usfx_options()
739
+
740
+ writer_speakers = {}
741
+ speaker_image_bytes = {}
742
+ for name, data in session.SPEAKERS.items():
743
+ if not isinstance(data, dict):
744
+ continue
745
+ entry = {}
746
+ if data.get('color'):
747
+ entry['color'] = data['color']
748
+ if isinstance(data.get('dubbing'), dict):
749
+ entry['dubbing'] = data['dubbing']
750
+ if usfx_options['include_speaker_images'] and isinstance(data.get('image'), QImage):
751
+ buf = QByteArray()
752
+ qbuf = QBuffer(buf)
753
+ qbuf.open(QIODevice.WriteOnly)
754
+ if data['image'].save(qbuf, 'PNG'):
755
+ speaker_image_bytes[name] = bytes(buf)
756
+ qbuf.close()
757
+ if entry:
758
+ writer_speakers[name] = entry
759
+
760
+ segments_copy = copy.deepcopy(session.SUBTITLE['segments'])
761
+ dub_files_to_include = {}
762
+ for segment in segments_copy:
763
+ for dub in segment.get('dubbing', []) or []:
764
+ source_path = dub.get('path')
765
+ uid = dub.get('uid')
766
+ if source_path and uid and os.path.isfile(source_path):
767
+ ext = (os.path.splitext(source_path)[1].lstrip('.').lower() or 'wav')
768
+ arcname = f'assets/dubs/{_safe_asset_name(uid)}.{ext}'
769
+ dub_files_to_include[source_path] = arcname
770
+ dub['path'] = arcname
771
+
772
+ # Collect optional bundled assets (video + separation caches + waveform cache).
773
+ video_path = session.VIDEO.get('filepath', '') if isinstance(session.VIDEO, dict) else ''
774
+ video_cache_key = utils.get_cache_key(video_path) if video_path else None
775
+ extra_files_to_include = {} # source path -> arcname
776
+
777
+ if usfx_options['include_original_video'] and video_path and os.path.isfile(video_path):
778
+ extra_files_to_include[video_path] = f'assets/video/{_safe_asset_name(os.path.basename(video_path))}'
779
+
780
+ if video_cache_key:
781
+ if usfx_options['include_waveform_cache']:
782
+ wf_source = os.path.join(session.PATH_SUBTITLD_USER_CACHE, 'waveform', f'{video_cache_key}_waveform.npy')
783
+ if os.path.isfile(wf_source):
784
+ extra_files_to_include[wf_source] = 'assets/waveform.npy'
785
+ if usfx_options['include_original_audio']:
786
+ src = os.path.join(session.PATH_SUBTITLD_DATA_AUDIOSEPARATION, f'{video_cache_key}_original.flac')
787
+ if os.path.isfile(src):
788
+ extra_files_to_include[src] = 'assets/audio/original.flac'
789
+ if usfx_options['include_processed_audio']:
790
+ for kind in ('vocals', 'background'):
791
+ src = os.path.join(session.PATH_SUBTITLD_DATA_AUDIOSEPARATION, f'{video_cache_key}_{kind}.flac')
792
+ if os.path.isfile(src):
793
+ extra_files_to_include[src] = f'assets/audio/{kind}.flac'
794
+
795
+ xml_content = usf.USFWriter().write(
796
+ segments_copy,
797
+ speakers=writer_speakers,
798
+ language=language,
799
+ embed_audio_clips=False,
800
+ )
801
+
802
+ def _mime_for(path_in_zip):
803
+ ext = path_in_zip.rsplit('.', 1)[-1].lower()
804
+ return {
805
+ 'usf': 'application/x-usf+xml',
806
+ 'png': 'image/png',
807
+ 'wav': 'audio/wav',
808
+ 'flac': 'audio/flac',
809
+ 'mp3': 'audio/mpeg',
810
+ 'mp4': 'video/mp4',
811
+ 'mkv': 'video/x-matroska',
812
+ 'mov': 'video/quicktime',
813
+ 'webm': 'video/webm',
814
+ 'ogv': 'video/ogg',
815
+ 'npy': 'application/octet-stream',
816
+ }.get(ext, 'application/octet-stream')
817
+
818
+ manifest_entries = [('subtitles.usf', 'application/x-usf+xml')]
819
+ for name in speaker_image_bytes:
820
+ manifest_entries.append((f'assets/speakers/{_safe_asset_name(name)}.png', 'image/png'))
821
+ for arc in dub_files_to_include.values():
822
+ manifest_entries.append((arc, _mime_for(arc)))
823
+ for arc in extra_files_to_include.values():
824
+ manifest_entries.append((arc, _mime_for(arc)))
825
+
826
+ manifest_lines = ['<?xml version="1.0" encoding="UTF-8"?>',
827
+ '<manifest version="1.0">',
828
+ ' <format>USFX</format>',
829
+ ' <generator>Subtitld</generator>']
830
+ for path, mime in manifest_entries:
831
+ manifest_lines.append(f' <entry path="{path}" type="{mime}"/>')
832
+ manifest_lines.append('</manifest>')
833
+ manifest_xml = '\n'.join(manifest_lines)
834
+
835
+ tmp_path = final_file + '.tmp'
836
+ try:
837
+ with zipfile.ZipFile(tmp_path, 'w', zipfile.ZIP_DEFLATED) as zf:
838
+ zf.writestr('subtitles.usf', xml_content)
839
+ zf.writestr('manifest.xml', manifest_xml)
840
+ for source, arc in dub_files_to_include.items():
841
+ zf.write(source, arc)
842
+ for name, img_bytes in speaker_image_bytes.items():
843
+ zf.writestr(f'assets/speakers/{_safe_asset_name(name)}.png', img_bytes)
844
+ for source, arc in extra_files_to_include.items():
845
+ zf.write(source, arc)
846
+ os.replace(tmp_path, final_file)
847
+ except Exception:
848
+ if os.path.exists(tmp_path):
849
+ try:
850
+ os.unlink(tmp_path)
851
+ except Exception:
852
+ pass
853
+ raise
854
+
855
+
856
+ def autosave_backup_timer_timeout():
857
+ if session.SUBTITLE:
858
+ if not 'filepath' in session.SUBTITLE:
859
+ filename = os.path.basename(session.VIDEO['filepath']).rsplit('.', 1)[0]
860
+ else:
861
+ filename = os.path.basename(session.SUBTITLE['filepath']).rsplit('.', 1)[0]
862
+ if not filename:
863
+ filename = os.path.basename(session.VIDEO['filepath']).rsplit('.', 1)[0]
864
+ save_file(os.path.join(session.PATH_SUBTITLD_DATA_BACKUP, filename + '_' + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '.usfx'), 'USFX', session.CONFIG.get('selected_language', 'en'))
865
+
866
+
867
+ def autosave_original_timer_timeout():
868
+ if session.SUBTITLE and session.SUBTITLE.get('filepath', '').lower().endswith('.usfx'):
869
+ save_file(session.SUBTITLE['filepath'], 'USFX', session.CONFIG['selected_language'])
870
+