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
|
@@ -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"><kdenlivetitle duration="125" LC_NUMERIC="C" width="1920" height="1080" out="124"> <item type="QGraphicsTextItem" z-index="0"> <position x="784" y="910"> <transform>1,0,0,0,1,0,0,0,1</transform> </position>
|
|
476
|
+
<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}</content> </item> <startviewport rect="0,0,1920,1080"/> <endviewport rect="0,0,1920,1080"/> <background color="0,0,0,0"/> </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
|
+
|