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,366 @@
1
+ """USF (Universal Subtitle Format) reader/writer."""
2
+
3
+
4
+ import base64
5
+ import os
6
+
7
+ from bs4 import BeautifulSoup, NavigableString
8
+ import subtitld.modules.timecode as timecode
9
+
10
+
11
+ USF_BASE_MARKUP = '''<!-- USF file generated by Subtitld -->
12
+ <USFSubtitles version="1.0">
13
+ <metadata></metadata>
14
+ <styles></styles>
15
+ </USFSubtitles>'''
16
+
17
+
18
+ def _extract_inline_text(tag):
19
+ """Return the text of a tag, converting <br/> to \\n and flattening inline formatting."""
20
+ parts = []
21
+ for child in tag.children:
22
+ if isinstance(child, NavigableString):
23
+ parts.append(str(child))
24
+ continue
25
+ if child.name == 'br':
26
+ parts.append('\n')
27
+ continue
28
+ parts.append(_extract_inline_text(child))
29
+ return ''.join(parts).strip()
30
+
31
+
32
+ def _parse_numeric(value, caster):
33
+ try:
34
+ return caster(value)
35
+ except (TypeError, ValueError):
36
+ return None
37
+
38
+
39
+ class USFReader():
40
+ """USFReader class"""
41
+ def __init__(self, *args, **kwargs):
42
+ super(USFReader, self).__init__(*args, **kwargs)
43
+ self.metadata = {}
44
+ self.language = None
45
+ self.speakers = {} # name -> {'color': '#RRGGBB'}
46
+ self.has_embedded_images = False
47
+ self.has_embedded_audio_clips = False
48
+
49
+ def detect(self, content):
50
+ """Function to return if this is a USF file"""
51
+ return '<usfsubtitles' in content.lower()
52
+
53
+ def read(self, content):
54
+ """Parse USF content and return a list of subtitle dicts.
55
+
56
+ Populates self.metadata, self.language and self.speakers as a side
57
+ effect so the caller can reflect title/author/date/language and the
58
+ per-speaker colors back into session state.
59
+ """
60
+ self.metadata = {}
61
+ self.language = None
62
+ self.speakers = {}
63
+ self.has_embedded_images = False
64
+ self.has_embedded_audio_clips = False
65
+
66
+ soup = BeautifulSoup(content, features="xml")
67
+
68
+ meta = soup.find('metadata')
69
+ if meta:
70
+ for key in ('title', 'date'):
71
+ tag = meta.find(key)
72
+ if tag and tag.text:
73
+ self.metadata[key] = tag.text.strip()
74
+ lang = meta.find('language')
75
+ if lang and lang.attrs.get('code'):
76
+ self.language = lang.attrs['code']
77
+ author = meta.find('author')
78
+ if author:
79
+ name = author.find('name')
80
+ if name and name.text:
81
+ self.metadata['author'] = name.text.strip()
82
+
83
+ for style in soup.select('styles > style'):
84
+ name = style.attrs.get('name')
85
+ if not name or name == 'Default':
86
+ continue
87
+ entry = {}
88
+ fontstyle = style.find('fontstyle')
89
+ if fontstyle and fontstyle.attrs.get('color'):
90
+ entry['color'] = fontstyle.attrs['color']
91
+ avatar = style.find('avatar')
92
+ if avatar and avatar.attrs.get('encoding') == 'base64' and avatar.text:
93
+ try:
94
+ entry['image_bytes'] = base64.b64decode(avatar.text.strip())
95
+ self.has_embedded_images = True
96
+ except Exception:
97
+ pass
98
+ dub_elem = style.find('dubbing')
99
+ if dub_elem:
100
+ dub_data = {}
101
+ for key in ('engine', 'voice'):
102
+ if key in dub_elem.attrs:
103
+ dub_data[key] = dub_elem.attrs[key]
104
+ for key in ('rate', 'pitch'):
105
+ if key in dub_elem.attrs:
106
+ parsed = _parse_numeric(dub_elem.attrs[key], int)
107
+ if parsed is not None:
108
+ dub_data[key] = parsed
109
+ if dub_data:
110
+ entry['dubbing'] = dub_data
111
+ if entry:
112
+ self.speakers[name] = entry
113
+
114
+ blocks = soup.find_all('subtitles')
115
+ if not blocks:
116
+ return []
117
+
118
+ primary_block = blocks[0]
119
+ primary_lang = self._block_language(primary_block)
120
+ if primary_lang and not self.language:
121
+ self.language = primary_lang
122
+
123
+ subtitles_list = []
124
+ for element in primary_block.find_all('subtitle', recursive=False):
125
+ entry = self._parse_subtitle(element)
126
+ if entry:
127
+ subtitles_list.append(entry)
128
+
129
+ for block in blocks[1:]:
130
+ lang = self._block_language(block)
131
+ if not lang:
132
+ continue
133
+ tr_subs = block.find_all('subtitle', recursive=False)
134
+ for primary_sub, tr_sub in zip(subtitles_list, tr_subs):
135
+ text_tag = tr_sub.find('text') or tr_sub.find('karaoke')
136
+ if text_tag is None:
137
+ continue
138
+ primary_sub.setdefault('translations', {})[lang] = _extract_inline_text(text_tag)
139
+
140
+ return subtitles_list
141
+
142
+ def _block_language(self, block):
143
+ lang = block.find('language')
144
+ if lang and lang.attrs.get('code'):
145
+ return lang.attrs['code']
146
+ return None
147
+
148
+ def _parse_subtitle(self, element):
149
+ if 'start' not in element.attrs or 'stop' not in element.attrs:
150
+ return None
151
+
152
+ subtitle = {
153
+ 'start': timecode.Timecode('1000', element.attrs['start']).float,
154
+ 'end': timecode.Timecode('1000', element.attrs['stop']).float,
155
+ }
156
+
157
+ text_tag = element.find('text') or element.find('karaoke')
158
+ subtitle['text'] = _extract_inline_text(text_tag) if text_tag else ''
159
+ subtitle['speaker'] = (text_tag.attrs.get('speaker') if text_tag else None) or 'A'
160
+
161
+ dubs = []
162
+ for dub_elem in element.find_all('dubbing', recursive=False):
163
+ dub = {}
164
+ for key, value in dub_elem.attrs.items():
165
+ dub[key] = value
166
+ for key in ('start', 'end'):
167
+ if key in dub:
168
+ parsed = _parse_numeric(dub[key], float)
169
+ if parsed is not None:
170
+ dub[key] = parsed
171
+ for key in ('rate', 'pitch'):
172
+ if key in dub:
173
+ parsed = _parse_numeric(dub[key], int)
174
+ if parsed is not None:
175
+ dub[key] = parsed
176
+ data_tag = dub_elem.find('data')
177
+ if data_tag and data_tag.attrs.get('encoding') == 'base64' and data_tag.text:
178
+ try:
179
+ dub['_embedded_bytes'] = base64.b64decode(data_tag.text.strip())
180
+ dub['_embedded_format'] = data_tag.attrs.get('format', 'wav')
181
+ self.has_embedded_audio_clips = True
182
+ except Exception:
183
+ pass
184
+ dubs.append(dub)
185
+ if dubs:
186
+ subtitle['dubbing'] = dubs
187
+
188
+ return subtitle
189
+
190
+
191
+ class USFWriter():
192
+ """USFWriter class"""
193
+ def __init__(self, *args, **kwargs):
194
+ super(USFWriter, self).__init__(*args, **kwargs)
195
+
196
+ def write(self, subtitles, speakers=None, language=None, metadata=None,
197
+ embed_audio_clips=False):
198
+ """Serialize subtitles to USF XML.
199
+
200
+ Parameters:
201
+ subtitles: list of subtitle dicts (from session.SUBTITLE['segments']).
202
+ speakers: dict name -> {'color': '#RRGGBB', 'image_bytes': bytes, ...}.
203
+ Emits one <style> per speaker; writes `speaker` + `style` on
204
+ <text>. If a speaker has `image_bytes`, embeds it inside the
205
+ <style> as <avatar encoding="base64" format="png">…</avatar>.
206
+ language: ISO language code for the primary <subtitles> block.
207
+ metadata: dict with optional 'title', 'author', 'date' keys.
208
+ embed_audio_clips: if True, reads each dub's WAV file from disk and
209
+ embeds base64 bytes as <data encoding="base64"> inside the
210
+ <dubbing> element.
211
+ """
212
+ speakers = speakers or {}
213
+ metadata = metadata or {}
214
+
215
+ usf = BeautifulSoup(USF_BASE_MARKUP, 'lxml-xml')
216
+
217
+ meta_tag = usf.find('metadata')
218
+ if 'title' in metadata:
219
+ title = usf.new_tag('title')
220
+ title.string = str(metadata['title'])
221
+ meta_tag.append(title)
222
+ author = usf.new_tag('author')
223
+ name = usf.new_tag('name')
224
+ name.string = str(metadata.get('author', 'Subtitld'))
225
+ author.append(name)
226
+ meta_tag.append(author)
227
+ if language:
228
+ lang_tag = usf.new_tag('language')
229
+ lang_tag['code'] = language
230
+ meta_tag.append(lang_tag)
231
+ if 'date' in metadata:
232
+ date = usf.new_tag('date')
233
+ date.string = str(metadata['date'])
234
+ meta_tag.append(date)
235
+
236
+ styles_tag = usf.find('styles')
237
+
238
+ default_style = usf.new_tag('style')
239
+ default_style['name'] = 'Default'
240
+ default_fontstyle = usf.new_tag('fontstyle', color='#EEEEFF', face='Arial', size='24', family='Arial')
241
+ default_fontstyle['outline-color'] = '#5555FF'
242
+ default_style.append(default_fontstyle)
243
+ default_position = usf.new_tag('position', alignment='BottomCenter')
244
+ default_position['relative-to'] = 'Window'
245
+ default_position['vertical-margin'] = '10%'
246
+ default_style.append(default_position)
247
+ styles_tag.append(default_style)
248
+
249
+ for speaker_name, speaker_data in speakers.items():
250
+ if speaker_name == 'Default':
251
+ continue
252
+ style = usf.new_tag('style')
253
+ style['name'] = speaker_name
254
+ color = speaker_data.get('color') if isinstance(speaker_data, dict) else None
255
+ if color:
256
+ fontstyle = usf.new_tag('fontstyle')
257
+ fontstyle['color'] = color
258
+ style.append(fontstyle)
259
+ image_bytes = speaker_data.get('image_bytes') if isinstance(speaker_data, dict) else None
260
+ if image_bytes:
261
+ avatar = usf.new_tag('avatar')
262
+ avatar['encoding'] = 'base64'
263
+ avatar['format'] = 'png'
264
+ avatar.string = base64.b64encode(image_bytes).decode('ascii')
265
+ style.append(avatar)
266
+ dubbing_settings = speaker_data.get('dubbing') if isinstance(speaker_data, dict) else None
267
+ if isinstance(dubbing_settings, dict):
268
+ dub_tag = usf.new_tag('dubbing')
269
+ for key in ('engine', 'voice'):
270
+ val = dubbing_settings.get(key)
271
+ if val:
272
+ dub_tag[key] = str(val)
273
+ for key in ('rate', 'pitch'):
274
+ val = dubbing_settings.get(key)
275
+ if val is not None:
276
+ dub_tag[key] = str(val)
277
+ if dub_tag.attrs:
278
+ style.append(dub_tag)
279
+ styles_tag.append(style)
280
+
281
+ primary = usf.new_tag('subtitles')
282
+ if language:
283
+ lang_tag = usf.new_tag('language')
284
+ lang_tag['code'] = language
285
+ primary.append(lang_tag)
286
+ usf.find('USFSubtitles').append(primary)
287
+
288
+ for subtitle in subtitles:
289
+ primary.append(self._build_subtitle(usf, subtitle, speakers, embed_audio_clips=embed_audio_clips))
290
+
291
+ translation_languages = []
292
+ for subtitle in subtitles:
293
+ for lang in subtitle.get('translations', {}):
294
+ if lang and lang not in translation_languages and lang != language:
295
+ translation_languages.append(lang)
296
+
297
+ for tr_lang in translation_languages:
298
+ block = usf.new_tag('subtitles')
299
+ lang_tag = usf.new_tag('language')
300
+ lang_tag['code'] = tr_lang
301
+ block.append(lang_tag)
302
+ for subtitle in subtitles:
303
+ block.append(self._build_subtitle(
304
+ usf,
305
+ subtitle,
306
+ speakers,
307
+ text_override=subtitle.get('translations', {}).get(tr_lang, ''),
308
+ include_dubs=False,
309
+ ))
310
+ usf.find('USFSubtitles').append(block)
311
+
312
+ return str(usf)
313
+
314
+ def _build_subtitle(self, usf, subtitle, speakers, text_override=None, include_dubs=True, embed_audio_clips=False):
315
+ new_sub = usf.new_tag(
316
+ 'subtitle',
317
+ start=str(timecode.Timecode('1000', start_seconds=subtitle['start'], fractional=True)),
318
+ stop=str(timecode.Timecode('1000', start_seconds=subtitle['end'], fractional=True)),
319
+ )
320
+
321
+ text_tag = usf.new_tag('text')
322
+ speaker = subtitle.get('speaker', 'A')
323
+ if speaker:
324
+ text_tag['speaker'] = speaker
325
+ if speaker in speakers:
326
+ text_tag['style'] = speaker
327
+
328
+ text_value = subtitle.get('text', '') if text_override is None else text_override
329
+ self._append_text_with_breaks(usf, text_tag, text_value)
330
+ new_sub.append(text_tag)
331
+
332
+ if include_dubs:
333
+ for dub in subtitle.get('dubbing', []):
334
+ dub_tag = usf.new_tag('dubbing')
335
+ for key in ('engine', 'path', 'uid'):
336
+ if key in dub and dub[key] is not None:
337
+ dub_tag[key] = str(dub[key])
338
+ for key in ('start', 'end', 'rate', 'pitch'):
339
+ if key in dub and dub[key] is not None:
340
+ dub_tag[key] = str(dub[key])
341
+ if embed_audio_clips:
342
+ path = dub.get('path')
343
+ if path and os.path.isfile(path):
344
+ try:
345
+ with open(path, 'rb') as f:
346
+ raw = f.read()
347
+ data_tag = usf.new_tag('data')
348
+ data_tag['encoding'] = 'base64'
349
+ data_tag['format'] = os.path.splitext(path)[1].lstrip('.').lower() or 'wav'
350
+ data_tag.string = base64.b64encode(raw).decode('ascii')
351
+ dub_tag.append(data_tag)
352
+ except Exception:
353
+ pass
354
+ new_sub.append(dub_tag)
355
+
356
+ return new_sub
357
+
358
+ def _append_text_with_breaks(self, usf, text_tag, value):
359
+ if not value:
360
+ return
361
+ lines = str(value).split('\n')
362
+ for idx, line in enumerate(lines):
363
+ if idx:
364
+ text_tag.append(usf.new_tag('br'))
365
+ if line:
366
+ text_tag.append(NavigableString(line))
@@ -0,0 +1,79 @@
1
+ import os
2
+ import hashlib
3
+
4
+ from subtitld.modules import session
5
+
6
+
7
+ def is_float(element: str) -> bool:
8
+ try:
9
+ float(element)
10
+ return True
11
+ except ValueError:
12
+ return False
13
+
14
+
15
+ def get_timeline_time_str(seconds, ms=False):
16
+ """Function to return timecode from seconds"""
17
+ secs = int(seconds % 60)
18
+ mins = int((seconds / 60) % 60)
19
+ hrs = int((seconds / 60) / 60)
20
+ mss = int(round(float('0.' + str(seconds).split('.', 1)[-1]), 3) * 1000)
21
+
22
+ if ms:
23
+ if hrs:
24
+ return "{hh:02d}:{mm:02d}:{ss:02d}.{mss:03d}".format(hh=hrs, mm=mins, ss=secs, mss=mss)
25
+ elif mins:
26
+ return "{mm:02d}:{ss:02d}.{mss:03d}".format(mm=mins, ss=secs, mss=mss)
27
+ else:
28
+ return "{ss:02d}.{mss:03d}".format(ss=secs, mss=mss)
29
+ else:
30
+ if hrs:
31
+ return "{hh:02d}:{mm:02d}:{ss:02d}".format(hh=hrs, mm=mins, ss=secs)
32
+ elif mins:
33
+ return "{mm:02d}:{ss:02d}".format(mm=mins, ss=secs)
34
+ else:
35
+ return "{ss:02d}".format(ss=secs)
36
+
37
+
38
+ def convert_ffmpeg_timecode_to_seconds(timecode):
39
+ """Function to convert ffmpeg timecode to seconds"""
40
+ if timecode:
41
+ final_value = float(timecode.split(':')[-1])
42
+ if timecode.count(':') > 2:
43
+ final_value += int(timecode.split(':')[-2]) * 60.0
44
+ if timecode.count(':') > 3:
45
+ final_value += int(timecode.split(':')[-3]) * 3600.0
46
+ if timecode.count(':') > 4:
47
+ final_value += int(timecode.split(':')[-4]) * 3600.0
48
+ return final_value
49
+ else:
50
+ return False
51
+
52
+
53
+ def get_subtitle_format(subtitle_filepath):
54
+ subtitle_format = False
55
+ if subtitle_filepath:
56
+ for formt in session.LIST_OF_SUPPORTED_SUBTITLE_EXTENSIONS:
57
+ for ext in session.LIST_OF_SUPPORTED_SUBTITLE_EXTENSIONS[formt]['extensions']:
58
+ if subtitle_filepath.endswith(ext):
59
+ subtitle_format = formt
60
+ return subtitle_format
61
+
62
+
63
+ def get_format_from_extension(extension):
64
+ for format in session.LIST_OF_SUPPORTED_SUBTITLE_EXTENSIONS:
65
+ for ext in session.LIST_OF_SUPPORTED_SUBTITLE_EXTENSIONS[format]['extensions']:
66
+ if extension == ext:
67
+ return format
68
+ return 'USF'
69
+
70
+
71
+ def get_cache_key(filepath):
72
+ """Generate a cache key based on file path, size and modification time."""
73
+ try:
74
+ stat = os.stat(filepath)
75
+ file_info = f"{filepath}|{stat.st_size}|{stat.st_mtime}"
76
+ hash_key = hashlib.md5(file_info.encode()).hexdigest()
77
+ return hash_key
78
+ except Exception:
79
+ return None
@@ -0,0 +1,53 @@
1
+ """Waveform module
2
+
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ import json
8
+
9
+ from subtitld.modules.session import STARTUPINFO, FFMPEG_EXECUTABLE, PATH_TEMP, FFPROBE_EXECUTABLE
10
+
11
+
12
+ def ffmpeg_extract_subtitle(filepath, index):
13
+ """Function to extract subtitle from video using ffmpeg"""
14
+ command = [
15
+ FFMPEG_EXECUTABLE,
16
+ '-i',
17
+ filepath,
18
+ '-map',
19
+ '0:' + str(index),
20
+ os.path.join(PATH_TEMP, 'subtitle.vtt')]
21
+ subprocess.Popen(
22
+ command,
23
+ stdout=subprocess.PIPE,
24
+ stderr=subprocess.DEVNULL,
25
+ startupinfo=STARTUPINFO
26
+ ).wait()
27
+ return os.path.join(PATH_TEMP, 'subtitle.vtt')
28
+
29
+
30
+ def ffmpeg_load_metadata(filepath):
31
+ """Function to read video's metadata"""
32
+ command = [
33
+ FFPROBE_EXECUTABLE,
34
+ '-v',
35
+ 'quiet',
36
+ '-print_format',
37
+ 'json',
38
+ '-show_format',
39
+ '-show_streams',
40
+ filepath]
41
+ proc = subprocess.Popen(
42
+ command,
43
+ stdout=subprocess.PIPE,
44
+ stderr=subprocess.DEVNULL,
45
+ startupinfo=STARTUPINFO
46
+ )
47
+ json_file = False
48
+ with proc.stdout as stdout:
49
+ json_file = json.loads(stdout.read())
50
+
51
+ return json_file
52
+
53
+
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: subtitld
3
+ Version: 0.0.0.dev0
4
+ Summary: Subtitld
5
+ Author-email: Jonatã Bolzan Loss <subtitld@subtitld.org>
6
+ License: GPL3
7
+ Project-URL: Homepage, https://subtitld.org
8
+ Project-URL: Issues, https://gitlab.com/jonata/subtitld/issues
9
+ Keywords: subtitld
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: X11 Applications :: Qt
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
+ Classifier: Natural Language :: English
15
+ Classifier: Operating System :: POSIX
16
+ Classifier: Topic :: Multimedia
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: PySide6==6.10.0
22
+ Requires-Dist: PySideSix-Frameless-Window
23
+ Requires-Dist: mediapipe==0.10.21
24
+ Requires-Dist: opencv-python
25
+ Requires-Dist: python-i18n
26
+ Requires-Dist: python-docx
27
+ Requires-Dist: pycaption
28
+ Requires-Dist: chardet==6.0.0
29
+ Requires-Dist: pysubs2
30
+ Requires-Dist: translate
31
+ Requires-Dist: autohex
32
+ Requires-Dist: platformdirs
33
+ Requires-Dist: psutil
34
+ Requires-Dist: vosk
35
+ Requires-Dist: assemblyai
36
+ Requires-Dist: deep_translator
37
+ Requires-Dist: soundfile
38
+ Requires-Dist: edge-tts
39
+ Provides-Extra: transcribe
40
+ Requires-Dist: vosk; extra == "transcribe"
41
+ Requires-Dist: assemblyai; extra == "transcribe"
42
+ Provides-Extra: translate
43
+ Requires-Dist: translate; extra == "translate"
44
+ Requires-Dist: deep_translator; extra == "translate"
45
+ Provides-Extra: vision
46
+ Requires-Dist: mediapipe==0.10.21; extra == "vision"
47
+ Requires-Dist: opencv-python; extra == "vision"
48
+ Provides-Extra: dub
49
+ Requires-Dist: edge-tts; extra == "dub"
50
+ Provides-Extra: all
51
+ Requires-Dist: vosk; extra == "all"
52
+ Requires-Dist: assemblyai; extra == "all"
53
+ Requires-Dist: translate; extra == "all"
54
+ Requires-Dist: deep_translator; extra == "all"
55
+ Requires-Dist: mediapipe==0.10.21; extra == "all"
56
+ Requires-Dist: opencv-python; extra == "all"
57
+ Requires-Dist: edge-tts; extra == "all"
58
+ Dynamic: license-file
59
+
60
+ # Subtitld
61
+
62
+ Subtitld is a software to create, edit and transcribe subtitles.
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install subtitld
68
+ ```
69
+
70
+ ## Requirements
71
+
72
+ Subtitld uses `ffmpeg` for internal processes.
73
+
74
+ On Ubuntu/Debian:
75
+ ```bash
76
+ sudo apt install ffmpeg
77
+ ```
78
+
79
+ ## Usage
80
+
81
+ ```bash
82
+ subtitld
83
+ ```
84
+
85
+ Or with a file:
86
+ ```bash
87
+ subtitld video.mp4
88
+ ```
89
+
90
+ ## License
91
+
92
+ GPL3 - see LICENSE file for details
93
+
94
+ ## Links
95
+
96
+ - Homepage: https://subtitld.org
97
+ - Issues: https://gitlab.com/jonata/subtitld/issues