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,229 @@
|
|
|
1
|
+
"""Bounce / render dub speeches to audio or video files."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import soundfile as sf
|
|
10
|
+
|
|
11
|
+
from subtitld.modules import session
|
|
12
|
+
from subtitld.modules import utils
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _safe_piece(name):
|
|
16
|
+
return ''.join(c if c.isalnum() or c in '-_.' else '_' for c in str(name)) or 'x'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _separation_path(kind):
|
|
20
|
+
"""Path to the cached <kind>.flac (`background` or `vocals`) for the current video."""
|
|
21
|
+
video_path = session.VIDEO.get('filepath') if isinstance(session.VIDEO, dict) else None
|
|
22
|
+
if not video_path:
|
|
23
|
+
return None
|
|
24
|
+
key = utils.get_cache_key(video_path)
|
|
25
|
+
if not key:
|
|
26
|
+
return None
|
|
27
|
+
path = os.path.join(session.PATH_SUBTITLD_DATA_AUDIOSEPARATION, f'{key}_{kind}.flac')
|
|
28
|
+
return path if os.path.exists(path) else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _background_audio_path():
|
|
32
|
+
return _separation_path('background')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _vocals_audio_path():
|
|
36
|
+
return _separation_path('vocals')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _load_audio(path, samplerate):
|
|
40
|
+
"""Load audio file, resample to `samplerate`, return (frames, 2) float32."""
|
|
41
|
+
data, sr = sf.read(path, dtype='float32', always_2d=True)
|
|
42
|
+
if data.shape[1] == 1:
|
|
43
|
+
data = np.tile(data, (1, 2))
|
|
44
|
+
if sr != samplerate:
|
|
45
|
+
# Linear resample — cheap and sufficient for dub mixing.
|
|
46
|
+
ratio = samplerate / sr
|
|
47
|
+
out_len = int(round(data.shape[0] * ratio))
|
|
48
|
+
src_idx = np.arange(out_len, dtype=np.float64) / ratio
|
|
49
|
+
i0 = np.floor(src_idx).astype(np.int64)
|
|
50
|
+
i1 = np.minimum(i0 + 1, data.shape[0] - 1)
|
|
51
|
+
frac = (src_idx - i0).astype(np.float32)
|
|
52
|
+
data = (1.0 - frac[:, None]) * data[i0] + frac[:, None] * data[i1]
|
|
53
|
+
return data.astype(np.float32, copy=False)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _write_audio(path, buffer, samplerate, audio_format):
|
|
57
|
+
"""Write `buffer` (frames, 2) to `path`.
|
|
58
|
+
|
|
59
|
+
WAV / FLAC → direct soundfile write.
|
|
60
|
+
MP3 → ffmpeg libmp3lame.
|
|
61
|
+
"""
|
|
62
|
+
audio_format = audio_format.upper()
|
|
63
|
+
if audio_format in ('WAV', 'FLAC'):
|
|
64
|
+
sf.write(path, buffer, samplerate, subtype='PCM_16' if audio_format == 'WAV' else 'PCM_16')
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if audio_format == 'MP3':
|
|
68
|
+
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tf:
|
|
69
|
+
tmp_wav = tf.name
|
|
70
|
+
try:
|
|
71
|
+
sf.write(tmp_wav, buffer, samplerate, subtype='PCM_16')
|
|
72
|
+
cmd = [
|
|
73
|
+
session.FFMPEG_EXECUTABLE,
|
|
74
|
+
'-hide_banner', '-loglevel', 'error',
|
|
75
|
+
'-y', '-i', tmp_wav,
|
|
76
|
+
'-codec:a', 'libmp3lame', '-qscale:a', '2',
|
|
77
|
+
path,
|
|
78
|
+
]
|
|
79
|
+
subprocess.run(cmd, startupinfo=session.STARTUPINFO, check=True)
|
|
80
|
+
finally:
|
|
81
|
+
try:
|
|
82
|
+
os.unlink(tmp_wav)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
raise ValueError(f'Unsupported audio format: {audio_format}')
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _mux_mp4(video_path, audio_buffer, samplerate, output_path):
|
|
91
|
+
"""Copy the video stream from `video_path`, replace audio with AAC-encoded
|
|
92
|
+
version of `audio_buffer`. Original audio/subtitle/data streams are dropped."""
|
|
93
|
+
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tf:
|
|
94
|
+
tmp_wav = tf.name
|
|
95
|
+
try:
|
|
96
|
+
sf.write(tmp_wav, audio_buffer, samplerate, subtype='PCM_16')
|
|
97
|
+
cmd = [
|
|
98
|
+
session.FFMPEG_EXECUTABLE,
|
|
99
|
+
'-hide_banner', '-loglevel', 'error',
|
|
100
|
+
'-y',
|
|
101
|
+
'-an', # drop audio from the video input entirely
|
|
102
|
+
'-i', video_path,
|
|
103
|
+
'-i', tmp_wav,
|
|
104
|
+
'-map', '0:v:0', # only video stream from input 0
|
|
105
|
+
'-map', '1:a:0', # only audio from rendered WAV
|
|
106
|
+
'-sn', '-dn', # no subtitle, no data streams
|
|
107
|
+
'-c:v', 'copy',
|
|
108
|
+
'-c:a', 'aac', '-b:a', '192k',
|
|
109
|
+
'-shortest',
|
|
110
|
+
output_path,
|
|
111
|
+
]
|
|
112
|
+
subprocess.run(cmd, startupinfo=session.STARTUPINFO, check=True)
|
|
113
|
+
finally:
|
|
114
|
+
try:
|
|
115
|
+
os.unlink(tmp_wav)
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def bounce(output_path, audio_format, mode, audio_engine=None,
|
|
121
|
+
include_background=False):
|
|
122
|
+
"""Bounce dubs to disk.
|
|
123
|
+
|
|
124
|
+
mode: 'mixdown' | 'stems' | 'clips'
|
|
125
|
+
audio_format: 'WAV' | 'FLAC' | 'MP3' | 'MP4'
|
|
126
|
+
"""
|
|
127
|
+
audio_format = audio_format.upper()
|
|
128
|
+
samplerate = audio_engine.samplerate if audio_engine else 48000
|
|
129
|
+
|
|
130
|
+
if mode == 'clips':
|
|
131
|
+
folder = os.path.splitext(output_path)[0] + '_clips'
|
|
132
|
+
os.makedirs(folder, exist_ok=True)
|
|
133
|
+
for segment in session.SUBTITLE.get('segments', []):
|
|
134
|
+
dubs = segment.get('dubbing') or []
|
|
135
|
+
if not dubs:
|
|
136
|
+
continue
|
|
137
|
+
src = dubs[0].get('path')
|
|
138
|
+
if not src or not os.path.isfile(src):
|
|
139
|
+
continue
|
|
140
|
+
base = f'{segment["start"]:.3f}_{_safe_piece(segment.get("speaker", "A"))}.wav'
|
|
141
|
+
shutil.copy(src, os.path.join(folder, base))
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
if audio_engine is None:
|
|
145
|
+
raise ValueError('audio_engine is required for non-clip bounce modes')
|
|
146
|
+
|
|
147
|
+
duration = float(session.VIDEO.get('duration', 0.0))
|
|
148
|
+
if duration <= 0.0:
|
|
149
|
+
# Fall back to the latest dub endpoint
|
|
150
|
+
for segment in session.SUBTITLE.get('segments', []):
|
|
151
|
+
for dub in segment.get('dubbing') or []:
|
|
152
|
+
try:
|
|
153
|
+
duration = max(duration, float(dub.get('start', 0.0)) + float(dub.get('end', 0.0)) - float(dub.get('start', 0.0)))
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
bg_buffer = None
|
|
158
|
+
if include_background:
|
|
159
|
+
bg_path = _background_audio_path()
|
|
160
|
+
if bg_path:
|
|
161
|
+
bg_buffer = _load_audio(bg_path, samplerate)
|
|
162
|
+
print(f'[bounce] background source: {bg_path} (frames={len(bg_buffer)}, peak={float(np.max(np.abs(bg_buffer))):.3f})')
|
|
163
|
+
else:
|
|
164
|
+
print('[bounce] include_background requested but no _background.flac found in cache')
|
|
165
|
+
else:
|
|
166
|
+
print('[bounce] background NOT included (checkbox unchecked)')
|
|
167
|
+
|
|
168
|
+
def _apply_background(buffer):
|
|
169
|
+
if bg_buffer is None:
|
|
170
|
+
return buffer
|
|
171
|
+
length = max(len(buffer), len(bg_buffer))
|
|
172
|
+
padded = np.zeros((length, 2), dtype=np.float32)
|
|
173
|
+
padded[:len(buffer)] += buffer
|
|
174
|
+
padded[:len(bg_buffer)] += bg_buffer
|
|
175
|
+
np.clip(padded, -1.0, 1.0, out=padded)
|
|
176
|
+
return padded
|
|
177
|
+
|
|
178
|
+
dub_tracks = list((getattr(audio_engine, 'speaker_tracks', {}) or {}).values())
|
|
179
|
+
|
|
180
|
+
if mode in ('background_only', 'vocals_only'):
|
|
181
|
+
src = _background_audio_path() if mode == 'background_only' else _vocals_audio_path()
|
|
182
|
+
if not src:
|
|
183
|
+
raise ValueError(f'No cached audio available for mode {mode}')
|
|
184
|
+
buffer = _load_audio(src, samplerate)
|
|
185
|
+
if audio_format == 'MP4':
|
|
186
|
+
video = session.VIDEO.get('filepath')
|
|
187
|
+
if not video:
|
|
188
|
+
raise ValueError('No video loaded for MP4 mux')
|
|
189
|
+
_mux_mp4(video, buffer, samplerate, output_path)
|
|
190
|
+
else:
|
|
191
|
+
_write_audio(output_path, buffer, samplerate, audio_format)
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
if mode == 'mixdown':
|
|
195
|
+
buffer = audio_engine.render_buffer(start=0.0, end=duration, samplerate=samplerate, tracks=dub_tracks)
|
|
196
|
+
buffer = _apply_background(buffer)
|
|
197
|
+
if audio_format == 'MP4':
|
|
198
|
+
video = session.VIDEO.get('filepath')
|
|
199
|
+
if not video:
|
|
200
|
+
raise ValueError('No video loaded for MP4 mux')
|
|
201
|
+
_mux_mp4(video, buffer, samplerate, output_path)
|
|
202
|
+
else:
|
|
203
|
+
_write_audio(output_path, buffer, samplerate, audio_format)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if mode == 'stems':
|
|
207
|
+
stem, ext = os.path.splitext(output_path)
|
|
208
|
+
stem_tracks = getattr(audio_engine, 'speaker_tracks', {}) or {}
|
|
209
|
+
if not stem_tracks:
|
|
210
|
+
# No per-speaker tracks — just write a single mixdown under the stem name.
|
|
211
|
+
buffer = audio_engine.render_buffer(start=0.0, end=duration, samplerate=samplerate, tracks=dub_tracks)
|
|
212
|
+
buffer = _apply_background(buffer)
|
|
213
|
+
_write_audio(output_path, buffer, samplerate, audio_format)
|
|
214
|
+
return
|
|
215
|
+
for speaker_name, track in stem_tracks.items():
|
|
216
|
+
buffer = audio_engine.render_buffer(start=0.0, end=duration, samplerate=samplerate, tracks=[track])
|
|
217
|
+
if include_background:
|
|
218
|
+
buffer = _apply_background(buffer)
|
|
219
|
+
speaker_path = f'{stem}-{_safe_piece(speaker_name)}{ext}'
|
|
220
|
+
if audio_format == 'MP4':
|
|
221
|
+
video = session.VIDEO.get('filepath')
|
|
222
|
+
if not video:
|
|
223
|
+
raise ValueError('No video loaded for MP4 mux')
|
|
224
|
+
_mux_mp4(video, buffer, samplerate, speaker_path)
|
|
225
|
+
else:
|
|
226
|
+
_write_audio(speaker_path, buffer, samplerate, audio_format)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
raise ValueError(f'Unknown bounce mode: {mode}')
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from subtitld.modules.session import PATH_SUBTITLD_USER_CONFIG_FILE
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Config(dict):
|
|
8
|
+
def __init__(self, filepath=PATH_SUBTITLD_USER_CONFIG_FILE):
|
|
9
|
+
self.filepath = Path(filepath)
|
|
10
|
+
|
|
11
|
+
if self.filepath.exists():
|
|
12
|
+
try:
|
|
13
|
+
with open(self.filepath, 'r', encoding='utf-8') as f:
|
|
14
|
+
data = json.load(f)
|
|
15
|
+
except json.JSONDecodeError:
|
|
16
|
+
data = json.loads('{}')
|
|
17
|
+
else:
|
|
18
|
+
data = json.loads('{}')
|
|
19
|
+
|
|
20
|
+
self.load_defaults()
|
|
21
|
+
|
|
22
|
+
super().__init__(data)
|
|
23
|
+
|
|
24
|
+
def __getitem__(self, key):
|
|
25
|
+
return super().get(key, False) # returns False if not found
|
|
26
|
+
|
|
27
|
+
def save(self):
|
|
28
|
+
with open(self.filepath, 'w', encoding='utf-8') as f:
|
|
29
|
+
json.dump(self, f, indent=4)
|
|
30
|
+
|
|
31
|
+
def load_defaults(self):
|
|
32
|
+
self.setdefault('timeline_zoom', 100.0)
|
|
33
|
+
self.setdefault('playback_speed', 1.0)
|
|
34
|
+
self.setdefault('repeat_activated', False)
|
|
35
|
+
self.setdefault('playback_repeat_duration', 10.0)
|
|
36
|
+
self.setdefault('playback_repeat_times', 3)
|
|
37
|
+
self.setdefault('timeline', {})
|
|
38
|
+
self.setdefault('interface_splitters', {})
|
|
39
|
+
self.setdefault('shortcuts', {})
|
|
40
|
+
self.setdefault('default_new_subtitle_duration', 10.0)
|
|
41
|
+
self.setdefault('new_subtitle_start_from_last', False)
|
|
42
|
+
self.setdefault('new_subtitle_and_play', False)
|
|
43
|
+
self.setdefault('new_subtitle_to_next_start', False)
|
|
44
|
+
self.setdefault('quality_check', {})
|
|
45
|
+
self.setdefault('default_values', {})
|
|
46
|
+
self.setdefault('videoplayer', {})
|
|
47
|
+
self.setdefault('export', {})
|
|
48
|
+
self.setdefault('autosave', {})
|
|
49
|
+
self.setdefault('transcription', {})
|
|
50
|
+
self.setdefault('translation', {})
|
|
51
|
+
self.setdefault('dubbing', {})
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def get_valid_keys():
|
|
55
|
+
return {
|
|
56
|
+
'timeline_zoom',
|
|
57
|
+
'playback_speed',
|
|
58
|
+
'repeat_activated',
|
|
59
|
+
'playback_repeat_duration',
|
|
60
|
+
'playback_repeat_times',
|
|
61
|
+
'timeline',
|
|
62
|
+
'interface_splitters',
|
|
63
|
+
'shortcuts',
|
|
64
|
+
'default_new_subtitle_duration',
|
|
65
|
+
'new_subtitle_start_from_last',
|
|
66
|
+
'new_subtitle_and_play',
|
|
67
|
+
'new_subtitle_to_next_start',
|
|
68
|
+
'quality_check',
|
|
69
|
+
'default_values',
|
|
70
|
+
'videoplayer',
|
|
71
|
+
'export',
|
|
72
|
+
'autosave',
|
|
73
|
+
'transcription',
|
|
74
|
+
'translation',
|
|
75
|
+
'dubbing',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def validate_config_keys(config_data):
|
|
80
|
+
valid_keys = Config.get_valid_keys()
|
|
81
|
+
invalid_keys = set(config_data.keys()) - valid_keys
|
|
82
|
+
return len(invalid_keys) == 0, invalid_keys
|
|
83
|
+
|
|
84
|
+
|