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.
- subtitld/__init__.py +19 -0
- subtitld/__main__.py +240 -0
- subtitld/ftfy/char_classes.dat +0 -0
- subtitld/graphics/Montserrat-Black.ttf +0 -0
- subtitld/graphics/Montserrat-BlackItalic.ttf +0 -0
- subtitld/graphics/Montserrat-Bold.ttf +0 -0
- subtitld/graphics/Montserrat-BoldItalic.ttf +0 -0
- subtitld/graphics/Montserrat-ExtraBold.ttf +0 -0
- subtitld/graphics/Montserrat-ExtraBoldItalic.ttf +0 -0
- subtitld/graphics/Montserrat-ExtraLight.ttf +0 -0
- subtitld/graphics/Montserrat-ExtraLightItalic.ttf +0 -0
- subtitld/graphics/Montserrat-Italic.ttf +0 -0
- subtitld/graphics/Montserrat-Light.ttf +0 -0
- subtitld/graphics/Montserrat-LightItalic.ttf +0 -0
- subtitld/graphics/Montserrat-Medium.ttf +0 -0
- subtitld/graphics/Montserrat-MediumItalic.ttf +0 -0
- subtitld/graphics/Montserrat-Regular.ttf +0 -0
- subtitld/graphics/Montserrat-SemiBold.ttf +0 -0
- subtitld/graphics/Montserrat-SemiBoldItalic.ttf +0 -0
- subtitld/graphics/Montserrat-Thin.ttf +0 -0
- subtitld/graphics/Montserrat-ThinItalic.ttf +0 -0
- subtitld/graphics/Ubuntu-B.ttf +0 -0
- subtitld/graphics/Ubuntu-BI.ttf +0 -0
- subtitld/graphics/Ubuntu-C.ttf +0 -0
- subtitld/graphics/Ubuntu-L.ttf +0 -0
- subtitld/graphics/Ubuntu-LI.ttf +0 -0
- subtitld/graphics/Ubuntu-M.ttf +0 -0
- subtitld/graphics/Ubuntu-MI.ttf +0 -0
- subtitld/graphics/Ubuntu-R.ttf +0 -0
- subtitld/graphics/Ubuntu-RI.ttf +0 -0
- subtitld/graphics/Ubuntu-Th.ttf +0 -0
- subtitld/graphics/UbuntuMono-B.ttf +0 -0
- subtitld/graphics/UbuntuMono-BI.ttf +0 -0
- subtitld/graphics/UbuntuMono-R.ttf +0 -0
- subtitld/graphics/UbuntuMono-RI.ttf +0 -0
- subtitld/graphics/add_and_play.svg +4 -0
- subtitld/graphics/add_from_last.svg +4 -0
- subtitld/graphics/add_from_last_checked.svg +4 -0
- subtitld/graphics/add_subtitle_icon.svg +4 -0
- subtitld/graphics/add_subtitle_to_next_start.svg +4 -0
- subtitld/graphics/add_subtitle_to_next_start_checked.svg +4 -0
- subtitld/graphics/background_watermark.svg +9 -0
- subtitld/graphics/character_count_icon.svg +1 -0
- subtitld/graphics/checkbox_checked.svg +8 -0
- subtitld/graphics/checkbox_checked_hover.svg +8 -0
- subtitld/graphics/checkbox_disabled.svg +4 -0
- subtitld/graphics/checkbox_unchecked.svg +5 -0
- subtitld/graphics/checkbox_unchecked_hover.svg +5 -0
- subtitld/graphics/combobox_down_arrow.svg +4 -0
- subtitld/graphics/cps_icon.svg +1 -0
- subtitld/graphics/dialog_close_icon.svg +4 -0
- subtitld/graphics/dialog_close_icon_hover.svg +4 -0
- subtitld/graphics/dubbing_container_collapse_icon.svg +8 -0
- subtitld/graphics/dubbing_container_expand_icon.svg +4 -0
- subtitld/graphics/file.svg +4 -0
- subtitld/graphics/gap_add_icon.svg +4 -0
- subtitld/graphics/gap_remove_icon.svg +4 -0
- subtitld/graphics/grid_frames_icon.svg +4 -0
- subtitld/graphics/grid_frames_icon_checked.svg +4 -0
- subtitld/graphics/grid_frames_icon_disabled.svg +4 -0
- subtitld/graphics/grid_scenes_icon.svg +4 -0
- subtitld/graphics/grid_scenes_icon_checked.svg +4 -0
- subtitld/graphics/grid_scenes_icon_disabled.svg +4 -0
- subtitld/graphics/grid_seconds_icon.svg +4 -0
- subtitld/graphics/grid_seconds_icon_checked.svg +4 -0
- subtitld/graphics/grid_seconds_icon_disabled.svg +4 -0
- subtitld/graphics/last_end_to_current_position_icon.svg +4 -0
- subtitld/graphics/last_start_last_end_to_current_position_button.svg +9 -0
- subtitld/graphics/last_start_to_current_position_icon.svg +4 -0
- subtitld/graphics/left_panel_autosave.svg +4 -0
- subtitld/graphics/left_panel_dubbing.svg +4 -0
- subtitld/graphics/left_panel_export.svg +4 -0
- subtitld/graphics/left_panel_global.svg +4 -0
- subtitld/graphics/left_panel_import.svg +4 -0
- subtitld/graphics/left_panel_interface.svg +4 -0
- subtitld/graphics/left_panel_keyboard.svg +4 -0
- subtitld/graphics/left_panel_metadata.svg +4 -0
- subtitld/graphics/left_panel_navigation_background.svg +11 -0
- subtitld/graphics/left_panel_qualitycheck.svg +4 -0
- subtitld/graphics/left_panel_speakers.svg +4 -0
- subtitld/graphics/left_panel_subtitles.svg +1 -0
- subtitld/graphics/left_panel_transcription.svg +4 -0
- subtitld/graphics/left_panel_translation.svg +4 -0
- subtitld/graphics/merge_back_selected_subtitle_icon.svg +4 -0
- subtitld/graphics/merge_next_selected_subtitle_icon.svg +4 -0
- subtitld/graphics/move_backward_subtitle.svg +4 -0
- subtitld/graphics/move_end_back_subtitle.svg +4 -0
- subtitld/graphics/move_end_forward_subtitle.svg +4 -0
- subtitld/graphics/move_forward_subtitle.svg +4 -0
- subtitld/graphics/move_start_back_subtitle.svg +4 -0
- subtitld/graphics/move_start_forward_subtitle.svg +4 -0
- subtitld/graphics/music_voice_separation_music_icon.svg +4 -0
- subtitld/graphics/music_voice_separation_music_icon_disabled.svg +4 -0
- subtitld/graphics/music_voice_separation_voice_icon.svg +4 -0
- subtitld/graphics/music_voice_separation_voice_icon_disabled.svg +4 -0
- subtitld/graphics/new_line_icon.svg +1 -0
- subtitld/graphics/next_end_to_current_position_icon.svg +4 -0
- subtitld/graphics/next_start_next_end_to_current_position_button.svg +4 -0
- subtitld/graphics/next_start_to_current_position_icon.svg +4 -0
- subtitld/graphics/pause_icon.svg +12 -0
- subtitld/graphics/play_from_last_start_icon.svg +5 -0
- subtitld/graphics/play_from_next_start_icon.svg +7 -0
- subtitld/graphics/play_icon.svg +12 -0
- subtitld/graphics/play_icon_hover.svg +12 -0
- subtitld/graphics/play_icon_pressed.svg +12 -0
- subtitld/graphics/playback_repeat.svg +4 -0
- subtitld/graphics/playback_speed.svg +6 -0
- subtitld/graphics/playbackspeed_border.svg +5 -0
- subtitld/graphics/playbackspeed_border_checked.svg +5 -0
- subtitld/graphics/playbackspeed_border_hover.svg +5 -0
- subtitld/graphics/playercontrols_button_first.svg +5 -0
- subtitld/graphics/playercontrols_button_first_hover.svg +6 -0
- subtitld/graphics/playercontrols_button_first_pressed.svg +6 -0
- subtitld/graphics/playercontrols_button_last.svg +6 -0
- subtitld/graphics/playercontrols_button_last_hover.svg +6 -0
- subtitld/graphics/playercontrols_button_last_pressed.svg +6 -0
- subtitld/graphics/playercontrols_widget_center_bottom_line.svg +13 -0
- subtitld/graphics/playercontrols_widget_center_top_line.svg +17 -0
- subtitld/graphics/plus_icon.svg +4 -0
- subtitld/graphics/record_icon.svg +8 -0
- subtitld/graphics/record_icon_pressed.svg +5 -0
- subtitld/graphics/remove_selected_subtitle_icon.svg +6 -0
- subtitld/graphics/repeat_playback_border.svg +5 -0
- subtitld/graphics/repeat_playback_border_checked.svg +5 -0
- subtitld/graphics/repeat_playback_border_hover.svg +5 -0
- subtitld/graphics/save.svg +4 -0
- subtitld/graphics/send_text_to_last_subtitle_and_slice_button.svg +8 -0
- subtitld/graphics/send_text_to_last_subtitle_icon.svg +8 -0
- subtitld/graphics/send_text_to_next_subtitle_and_slice_button.svg +8 -0
- subtitld/graphics/send_text_to_next_subtitle_icon.svg +8 -0
- subtitld/graphics/slice_selected_subtitle_icon.svg +4 -0
- subtitld/graphics/snap_grid_icon.svg +4 -0
- subtitld/graphics/snap_grid_icon_checked.svg +4 -0
- subtitld/graphics/snap_grid_icon_disabled.svg +4 -0
- subtitld/graphics/snap_limits_icon.svg +4 -0
- subtitld/graphics/snap_limits_icon_checked.svg +4 -0
- subtitld/graphics/snap_limits_icon_disabled.svg +4 -0
- subtitld/graphics/snap_move_next.svg +4 -0
- subtitld/graphics/snap_move_next_checked.svg +4 -0
- subtitld/graphics/snap_move_next_disabled.svg +4 -0
- subtitld/graphics/snap_moving_icon.svg +4 -0
- subtitld/graphics/snap_moving_icon_checked.svg +4 -0
- subtitld/graphics/snap_moving_icon_disabled.svg +4 -0
- subtitld/graphics/speaker_list_item_change_color_icon.svg +4 -0
- subtitld/graphics/speaker_list_item_change_color_icon_hover.svg +4 -0
- subtitld/graphics/speaker_list_item_change_image_icon.svg +4 -0
- subtitld/graphics/speaker_list_item_change_image_icon_hover.svg +4 -0
- subtitld/graphics/speaker_list_item_export_icon.svg +4 -0
- subtitld/graphics/speaker_list_item_export_icon_hover.svg +4 -0
- subtitld/graphics/speaker_list_item_remove_icon.svg +4 -0
- subtitld/graphics/speaker_list_item_remove_icon_hover.svg +4 -0
- subtitld/graphics/speaker_list_item_rename_icon.svg +4 -0
- subtitld/graphics/speaker_list_item_rename_icon_hover.svg +4 -0
- subtitld/graphics/spinbox_arrow_down.svg +4 -0
- subtitld/graphics/spinbox_arrow_up.svg +4 -0
- subtitld/graphics/start_screen_top_shadow.svg +23 -0
- subtitld/graphics/stop_icon.svg +6 -0
- subtitld/graphics/stop_icon_hover.svg +6 -0
- subtitld/graphics/stop_icon_pressed.svg +6 -0
- subtitld/graphics/stylesheet.qss +1838 -0
- subtitld/graphics/subtitld.ico +0 -0
- subtitld/graphics/subtitld.png +0 -0
- subtitld/graphics/subtitles_panel_simplelist_properties_duration_timing_lock_closed_icon.svg +4 -0
- subtitld/graphics/subtitles_panel_simplelist_properties_duration_timing_lock_open_icon.svg +4 -0
- subtitld/graphics/subtitles_panel_simplelist_properties_duration_timing_lock_open_icon_hover.svg +4 -0
- subtitld/graphics/timeline_bottom_controls_background.svg +25 -0
- subtitld/graphics/timeline_cursor_back_frame.svg +4 -0
- subtitld/graphics/timeline_cursor_back_frame_hover.svg +4 -0
- subtitld/graphics/timeline_cursor_next_frame.svg +4 -0
- subtitld/graphics/timeline_cursor_next_frame_hover.svg +4 -0
- subtitld/graphics/timeline_show_speaker_color.svg +5 -0
- subtitld/graphics/timeline_show_speaker_color_checked.svg +5 -0
- subtitld/graphics/timeline_show_speaker_in_tracks.svg +4 -0
- subtitld/graphics/timeline_show_speaker_in_tracks_checked.svg +4 -0
- subtitld/graphics/timelinescrolling_follow_icon.svg +4 -0
- subtitld/graphics/timelinescrolling_follow_icon_checked.svg +4 -0
- subtitld/graphics/timelinescrolling_none_icon.svg +6 -0
- subtitld/graphics/timelinescrolling_none_icon_checked.svg +6 -0
- subtitld/graphics/timelinescrolling_page_icon.svg +4 -0
- subtitld/graphics/timelinescrolling_page_icon_checked.svg +4 -0
- subtitld/graphics/titleBar_left_container_border.svg +113 -0
- subtitld/graphics/unknown_speaker.svg +5 -0
- subtitld/graphics/vosk_transcription_download_model_icon.svg +4 -0
- subtitld/graphics/vosk_transcription_update_model_icon.svg +4 -0
- subtitld/graphics/word_count_icon.svg +1 -0
- subtitld/graphics/wpm_icon.svg +1 -0
- subtitld/graphics/zoom_in_icon.svg +4 -0
- subtitld/graphics/zoom_in_icon_disabled.svg +4 -0
- subtitld/graphics/zoom_out_icon.svg +4 -0
- subtitld/graphics/zoom_out_icon_disabled.svg +4 -0
- subtitld/interface/__init__.py +0 -0
- subtitld/interface/actionmanager.py +18 -0
- subtitld/interface/bottom_panel.py +43 -0
- subtitld/interface/left_panel.py +144 -0
- subtitld/interface/left_panel_autosave.py +149 -0
- subtitld/interface/left_panel_dubbing.py +469 -0
- subtitld/interface/left_panel_export.py +755 -0
- subtitld/interface/left_panel_global.py +358 -0
- subtitld/interface/left_panel_import.py +82 -0
- subtitld/interface/left_panel_interface.py +616 -0
- subtitld/interface/left_panel_keyboard.py +261 -0
- subtitld/interface/left_panel_metadata.py +74 -0
- subtitld/interface/left_panel_qualitycheck.py +262 -0
- subtitld/interface/left_panel_speakers.py +737 -0
- subtitld/interface/left_panel_subtitleslist.py +791 -0
- subtitld/interface/left_panel_transcription.py +763 -0
- subtitld/interface/left_panel_translation.py +294 -0
- subtitld/interface/playercontrols.py +2348 -0
- subtitld/interface/preview_panel.py +407 -0
- subtitld/interface/productionscreen.py +65 -0
- subtitld/interface/startscreen.py +273 -0
- subtitld/interface/timeline.py +1308 -0
- subtitld/interface/top_bar.py +329 -0
- subtitld/interface/translation.py +68 -0
- subtitld/interface/utils.py +286 -0
- subtitld/locale/en_US.json +301 -0
- subtitld/locale/pt_BR.json +269 -0
- subtitld/modules/__init__.py +0 -0
- subtitld/modules/audioengine.py +606 -0
- subtitld/modules/bounce.py +229 -0
- subtitld/modules/config.py +84 -0
- subtitld/modules/file_io.py +870 -0
- subtitld/modules/history.py +31 -0
- subtitld/modules/quality_check.py +53 -0
- subtitld/modules/session.py +270 -0
- subtitld/modules/shortcuts.py +84 -0
- subtitld/modules/subtitles.py +413 -0
- subtitld/modules/timecode.py +496 -0
- subtitld/modules/usf.py +366 -0
- subtitld/modules/utils.py +79 -0
- subtitld/modules/waveform.py +53 -0
- subtitld-0.0.0.dev0.dist-info/METADATA +97 -0
- subtitld-0.0.0.dev0.dist-info/RECORD +237 -0
- subtitld-0.0.0.dev0.dist-info/WHEEL +5 -0
- subtitld-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- subtitld-0.0.0.dev0.dist-info/licenses/LICENSE +674 -0
- subtitld-0.0.0.dev0.dist-info/top_level.txt +1 -0
subtitld/modules/usf.py
ADDED
|
@@ -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
|