├── .gitignore ├── LICENSE ├── README.md ├── controller.py ├── img ├── icon.ico ├── icon.png ├── mac_preferences.png ├── mac_screen.png ├── mac_screen_nolist.png ├── mac_whisper.png ├── mac_whisper_old.png └── window_screen_white.png ├── mainview.py ├── model.py ├── preferences ├── player_preferences.json ├── video_preferences.json └── vlc_args.txt ├── requirements.txt ├── srt ├── test.mp454057.srt └── test_sub.mkv54056.srt ├── styles ├── btnback_style.css ├── btnforward_style.css ├── btnplaystop_play_style.css ├── btnplaystop_stop_style.css ├── loadbar_style.css └── speed_slider.css ├── test_torch.py ├── timestamps.json ├── views.py └── whispermodel.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # testing material 132 | .test 133 | test/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Luruu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RViewer 2 | RViewer is a VLC media player that can generate subtitle using OpenAI Whisper Model. It's a personal project that I developed because I wanted to create a video player that was comfortable to use for many hours a day for video lessons (or in general to analyze video). 3 | In particular, I used WhisperX. 4 | 5 | https://github.com/Luruu/RViewer/assets/31493347/c1a90980-2d77-4563-afaa-652628b690c8 6 | 7 | ## prerequisites: 8 | - python3 9 | - VLC program 10 | - [Optional] (to generate subtitles): ffmpeg program 11 | 12 | 13 | ## libraries: 14 | - python-vlc, pyside6 15 | - [Optional] (to generate subtitles): ffmpeg-python, whisperx, torch 16 | 17 | 18 | ## hardware requirements (to generate subtitles): 19 | - Windows/MacOS/Linux 20 | - at least 2GB free space for libraries/models 21 | - at least 1GB VRAM for OpenAI Tiny model, 3GB VRAM for Small model 22 | 23 | ## setup 24 | 1. Open a terminal and clone the repo: `gh repo clone Luruu/RViewer` (or download it manually) 25 | 2. Move to RViewer folder: `cd RViewer` 26 | 3. Create and activate a virtual environment: 27 | - Linux/MacOS: `python3 -m venv env` and `source env/bin/activate` 28 | - Windows: `py -m venv env` and `.\env\Scripts\activate` 29 | 4. Install libraries: `pip install pyside6 python-vlc git+https://github.com/m-bain/whisperx.git` 30 | 31 | note: you must have the VLC program installed to be able to view the videos (and open RViewer) and the ffmpeg program to be able to use whisper. 32 | 33 | ## tips 34 | - (Subtitle): use CUDA if you have a NVIDIA GPU: it will be faster. Check if torch.cuda is available with: `test_torch.py`. 35 | This helped me to activate it: https://github.com/pytorch/pytorch/issues/30664#issuecomment-757431613 36 | - change content of CSS files if you want to change object styles 37 | - change `vlc_args.txt` in preferences folder if you want to change the behavior of vlc (for example, to change subtitle font, size and so on) 38 | 39 | ## solutions 40 | - [MacOS]: if you get error `SSL: CERTIFICATE_VERIFY_FAILED` try to install `certifi` or upgrade it. 41 | 42 | ## possible future updates 43 | 1. add code documentation 44 | 2. handle playlists 45 | 3. handle youtube videos 46 | 4. Generate audio transcript from subtitles 47 | 5. vocal commands 48 | 6. Stream video 49 | 50 | ## credits 51 | Program Icon created by Azland Studio - Flaticon 52 | 53 | ## Outputs 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /controller.py: -------------------------------------------------------------------------------- 1 | ''' 2 | controller 3 | ''' 4 | 5 | 6 | from views import PlayerView 7 | 8 | from mainview import MainView, AddItemDialog 9 | 10 | from model import PlayerModel, VideoModel 11 | 12 | 13 | import sys 14 | import time 15 | import os 16 | from PySide6.QtCore import * 17 | from PySide6.QtGui import * 18 | from PySide6.QtWidgets import * 19 | 20 | class Controller(): 21 | 22 | 23 | def __init__(self): 24 | 25 | self.program_name = "RV" 26 | 27 | self.program_path = self.get_original_path() 28 | 29 | self.check_video_path() 30 | 31 | self.sem_player = QSemaphore(0) 32 | self.w_player = PlayerView(self) 33 | self.w_player.start() 34 | 35 | 36 | self.sem_player.acquire(1) 37 | self.m_player = PlayerModel(self.program_path) 38 | self.window = MainView(self.program_name, self.program_path, self, self.m_player.player_preferences) 39 | self.window.whisper_window.combobox1.setCurrentText(self.m_player.player_preferences["whisper_language"]) 40 | self.window.whisper_window.combobox2.setCurrentText(self.m_player.player_preferences["whisper_model"]) 41 | self.window.setEnabled(False) 42 | self.anchorVLCtoWindow(self.w_player.get_istance_vlc_player(), self.window.videoframe.winId()) 43 | 44 | 45 | self.sem = QSemaphore(0) 46 | self.play_pause() 47 | self.sem.acquire(1) 48 | self.m_video = VideoModel(self.m_player.player_preferences,self.program_path) 49 | 50 | 51 | 52 | self.thread = ThreadTimer(self) 53 | 54 | 55 | 56 | 57 | self.w_player.parse_media() 58 | self.window.show() 59 | 60 | self.m_video.get_videoinfo_byvideo(self.w_player) 61 | self.m_video.set_namevideofile() 62 | 63 | 64 | 65 | self.initialize_gui() 66 | 67 | 68 | if sys.platform == "darwin": 69 | self.thread.update_gui.connect(self.update_gui) 70 | 71 | self.whisper = None 72 | self.window.whisper_window.name_video = self.m_video.name_video 73 | self.set_view_connections() 74 | self.thread.start() 75 | self.window.setEnabled(True) 76 | 77 | 78 | sys.exit(self.window.app.exec()) 79 | 80 | 81 | def get_original_path(self): 82 | # path of main .py or .exe when converted with pyinstaller 83 | if getattr(sys, 'frozen', False): 84 | script_path = os.path.dirname(sys.executable) 85 | else: 86 | script_path = os.path.dirname( 87 | os.path.abspath(sys.modules['__main__'].__file__) 88 | ) 89 | return script_path 90 | 91 | def get_MEI_path(self): 92 | # path of your data in same folder of main .py or added using --add-data 93 | if getattr(sys, 'frozen', False): 94 | data_folder_path = sys._MEIPASS 95 | else: 96 | data_folder_path = os.path.dirname( 97 | os.path.abspath(sys.modules['__main__'].__file__) 98 | ) 99 | return data_folder_path 100 | 101 | def check_video_path(self): 102 | if len(sys.argv) > 1: # if argv has an input 103 | if not os.path.exists(sys.argv[1]): 104 | print("RV ERROR: video file does not exists.") 105 | sys.exit() 106 | else: 107 | print("RV ERROR: choose a video to open RV program.") 108 | sys.exit() 109 | 110 | def play(self): 111 | self.w_player.play() 112 | self.w_player.is_paused = False 113 | self.window.btnPlayPause.setText("||") 114 | self.window.btnPlayPause.setShortcut(self.m_player.player_preferences["playpause_shortkey"]) 115 | self.window.btnPlayPause.setStyleSheet(self.window.stop_style) 116 | if self.sem.available() == 0: 117 | self.sem.release(1) 118 | 119 | def pause(self): 120 | if self.w_player.is_playing(): 121 | self.w_player.pause() 122 | self.w_player.is_paused = True 123 | self.window.btnPlayPause.setText(">") 124 | self.window.btnPlayPause.setStyleSheet(self.window.play_style) 125 | self.window.btnPlayPause.setShortcut(self.m_player.player_preferences["playpause_shortkey"]) 126 | 127 | def play_pause(self): 128 | if self.w_player.is_playing(): 129 | if self.sem.available() >= 1: 130 | self.sem.acquire(1) 131 | self.pause() 132 | else: 133 | if self.sem.available() == 0: 134 | self.sem.release(1) 135 | self.play() 136 | 137 | def changeSpeedVideo(self): 138 | # for example: value on trackbar for 1.0x is 5, so 5/5 = 1. For 2.0x is 10, so 10/5 = 2 and so on. 139 | new_speed = self.window.speed_slider.value() / 5 140 | self.window.label_speed.setText(" " + str(new_speed) + "x") 141 | self.w_player.set_rate(new_speed) 142 | 143 | 144 | def __check_track_video_preference(self): 145 | # use video speed instead player speed 146 | if self.m_player.player_preferences["track_video"]: 147 | self.window.speed_slider.setValue(self.m_video.video_preferences["track_value"]) 148 | else: 149 | self.window.speed_slider.setValue(self.m_player.player_preferences["track_value"]) 150 | self.changeSpeedVideo() 151 | 152 | def __check_pick_up_where_you_left_off_preference(self): 153 | # if pick up where you left off is true, load last position 154 | if self.m_player.player_preferences["pick_up_where_you_left_off"]: 155 | self.w_player.set_time(self.m_video.video_preferences["load_pos"]) 156 | 157 | 158 | def __check_volume_preference(self): 159 | # self.w_player.set_volume(self.m_video.video_preferences["volume_value"]) 160 | self.window.volume_slider.setValue(self.m_video.video_preferences["volume_value"]) 161 | 162 | def __check_if_audiotrack_exist(self): 163 | if self.w_player.get_audio_count() <= 0: 164 | self.window.btnSubtitle.setEnabled(False) 165 | self.window.btnSubtitle.setText("no audio track found") 166 | return False 167 | else: 168 | return True 169 | 170 | 171 | def load_subtitles_into_combobox(self): 172 | self.window.whisper_window.combobox0.clear() 173 | 174 | list_subs_all = self.w_player.get_sub_descriptions() 175 | list_subs_names = [sub[1].decode("utf-8") for sub in list_subs_all] 176 | 177 | self.window.whisper_window.combobox0.addItems(list_subs_names) 178 | 179 | def select_subtitle_into_combobox(self,sel_to_sub): 180 | self.window.whisper_window.combobox0.setCurrentText(sel_to_sub) 181 | 182 | def set_subtitle_by_combo(self): 183 | sub_selected = self.window.whisper_window.combobox0.currentText() 184 | sub = self.find_sub_in_player(sub_selected) 185 | if sub != None: 186 | self.w_player.set_sub(sub[0]) 187 | else: 188 | print("[INFO]: cannot set_subtitle_by_combo (note: this may appear initially due to the initialization of combobox)") 189 | 190 | def find_sub_in_player(self,sub_to_find): 191 | for sub in self.w_player.get_sub_descriptions(): 192 | if sub[1].decode("utf-8") == sub_to_find: 193 | return sub 194 | return None 195 | 196 | def find_sub_name_in_player_by_int(self,int): 197 | for sub in self.w_player.get_sub_descriptions(): 198 | if sub[0] == int: 199 | return sub[1].decode("utf-8") 200 | return None 201 | 202 | 203 | def set_subtitle_and_load_into_combobox(self): 204 | path_str = os.path.join(self.program_path, 'srt', "{}.srt".format(self.m_video.name_video)) 205 | if os.path.exists(path_str): 206 | self.w_player.set_subtitle(path_str) 207 | time.sleep(1.2) # w_player needs time to load path_str into video 208 | self.load_subtitles_into_combobox() 209 | self.window.whisper_window.combobox0.setCurrentIndex( self.window.whisper_window.combobox0.count()-1) # last index is the new file subtitle 210 | 211 | def __check_show_subtitle_if_available_preference(self): 212 | #check if video contains audiotracks 213 | if not self.__check_if_audiotrack_exist(): 214 | return 215 | 216 | path_str = os.path.join(self.program_path, 'srt', "{}.srt".format(self.m_video.name_video)) 217 | if os.path.exists(path_str): 218 | self.w_player.set_subtitle(path_str) 219 | 220 | 221 | if self.w_player.get_sub_count() >= 2: # note: first value is -1 that means no subtitles [Note: path_str will increase sub_counter after this moment!] 222 | if self.m_player.player_preferences["show_subtitle_if_available"]: # if user want show subtitles 223 | sub_selected = self.m_video.video_preferences["selected_sub_title"] 224 | sub = self.find_sub_in_player(sub_selected) 225 | if sub != None: 226 | self.w_player.set_sub(sub[0]) 227 | else: 228 | print("unexpected error: check!") 229 | else: #user does not want show subtitles, so if file contains subtitle, program select the first element "disable". 230 | self.w_player.hide_subtitle() 231 | 232 | else: 233 | print("no subtitles found inside video file") 234 | 235 | self.thread_sub = ThreadWaitForSubs(self) 236 | if sys.platform == "darwin": 237 | self.thread_sub.check_hide_sub.connect(self.check_hide_sub) 238 | self.thread_sub.start() 239 | 240 | def load_list_timestamps(self): 241 | self.m_video.load_videotimestamps() 242 | 243 | for key, value in self.m_video.timestamps.items(): 244 | self.window.listwidget.addItem(key) 245 | 246 | 247 | def _check_show_timestamps(self): 248 | 249 | if self.m_player.player_preferences["show_time_stamp"]: 250 | self.window.btnShowTimestamps.setText("hide timestamps") 251 | self.window.listframe.setVisible(True) 252 | else: 253 | self.window.btnShowTimestamps.setText("show timestamps") 254 | self.window.listframe.setVisible(False) 255 | 256 | 257 | def initialize_gui(self): 258 | 259 | self.m_video.load_videopreferences() 260 | 261 | self.load_list_timestamps() 262 | 263 | self._check_show_timestamps() 264 | 265 | self.__check_track_video_preference() 266 | 267 | self.__check_pick_up_where_you_left_off_preference() 268 | 269 | self.__check_volume_preference() 270 | 271 | self.load_subtitles_into_combobox() 272 | self.__check_show_subtitle_if_available_preference() 273 | 274 | 275 | 276 | def show_subtitle_form(self): 277 | actual_sub = self.w_player.get_sub() 278 | 279 | if self.window.whisper_window.isHidden(): 280 | self.window.show_whisper_window() 281 | self.load_subtitles_into_combobox() 282 | 283 | self.select_subtitle_into_combobox(self.find_sub_name_in_player_by_int(actual_sub)) 284 | else: 285 | self.window.whisper_window.activateWindow() 286 | 287 | def handle_stderr(self): 288 | data = self.whisper.readAllStandardError() 289 | stderr = bytes(data).decode("utf8") 290 | # Extract progress if it is in the data. 291 | self.window.whisper_window.textedit.append(stderr) 292 | 293 | def handle_stdout(self): 294 | data = self.whisper.readAllStandardOutput() 295 | stdout = bytes(data).decode("utf8") 296 | self.window.whisper_window.textedit.append(stdout) 297 | 298 | def handle_state(self, state): 299 | states = { 300 | QProcess.NotRunning: 'Not running', 301 | QProcess.Starting: 'Starting', 302 | QProcess.Running: 'Running', 303 | } 304 | state_name = states[state] 305 | self.window.whisper_window.textedit.append(f"State changed: {state_name}") 306 | 307 | if state_name == "Not running": 308 | self.window.whisper_window.setEnabled(True) 309 | self.window.setEnabled(True) 310 | self.process_finished() #if is not correctly finished, the condition "if os.path.exists(srt_file_name):" in process_finished() will be False. 311 | self.whisper = None 312 | 313 | def process_finished(self): 314 | # if process is completed, it will have created the srt file. 315 | srt_file_name = os.path.join('srt', "{}.srt".format(self.m_video.name_video)) 316 | if os.path.exists(srt_file_name): 317 | self.window.whisper_window.textedit.append("Process completed! :)") 318 | self.set_subtitle_and_load_into_combobox() 319 | else: # if process is interrupted, it will not have created the srt file. 320 | self.window.whisper_window.textedit.append("Process interrupted! :(") 321 | 322 | 323 | 324 | def do_subtitles(self): 325 | self.pause() # pause video 326 | if self.whisper is None: 327 | self.window.whisper_window.setEnabled(False) 328 | self.window.setEnabled(False) 329 | self.whisper = QProcess() 330 | self.whisper.readyReadStandardOutput.connect(self.handle_stdout) 331 | self.whisper.readyReadStandardError.connect(self.handle_stderr) 332 | self.whisper.stateChanged.connect(self.handle_state) 333 | self.whisper.finished.connect(self.process_finished) 334 | 335 | 336 | # os.environ['VIRTUAL_ENV'] 337 | if sys.platform == "win32": 338 | python = os.path.join("env", "Scripts", "python.exe") 339 | else: 340 | python = os.path.join("env", "bin", "python") 341 | 342 | 343 | file_path = os.path.join(self.get_MEI_path(),"whispermodel.py") 344 | self.whisper.start(python, [file_path, self.program_path , self.m_video.name_video, sys.argv[1], self.window.whisper_window.get_language_selected(),self.window.whisper_window.combobox2.currentText()]) 345 | 346 | 347 | def whisper_view_close(self): 348 | if self.whisper is not None: 349 | self.whisper.close() 350 | self.window.setEnabled(True) 351 | 352 | 353 | def set_subtitles_by_file(self, srt): 354 | return self.w_player.set_subtitle(srt) == 1 355 | 356 | def update_gui(self): 357 | if self.m_video.video_info["Artist"] is None: 358 | new_title = "{} - {} [{}]".format(self.program_name, self.m_video.video_info['Title'], self.m_video.video_info["Duration_hh_mm_ss"]) 359 | else: 360 | new_title = "{} - {} by {} [{}]".format(self.program_name, self.m_video.video_info['Title'], self.m_video.video_info["Artist"], self.m_video.video_info["Duration_hh_mm_ss"]) 361 | 362 | self.window.setWindowTitle(new_title) 363 | self.window.loadbar.setMaximum(self.m_video.video_info["Duration"]) 364 | self.m_video.video_info["Position"] = self.w_player.get_time() 365 | self.window.loadbar.setValue(int(self.m_video.video_info["Position"])) 366 | self.window.labelposition.setText(self.m_video.convert_ms_to_hmmss(self.m_video.video_info["Position"])) 367 | self.window.labelduration.setText(self.m_video.convert_ms_to_hmmss(self.m_video.video_info["Duration"] - self.m_video.video_info["Position"])) 368 | 369 | 370 | ''' This slot is only used to handle mouse clicks.''' 371 | def slider_clicked(self): 372 | if self.window.loadbar.mouse_pressed: 373 | self.window.loadbar.mouse_pressed = False 374 | self.pause() 375 | self.w_player.set_time(self.window.loadbar.value()) 376 | self.update_gui() 377 | self.play() 378 | 379 | def goback_and_update_gui(self): 380 | self.w_player.go_back(self.m_video.convert_seconds_to_ms(self.m_player.player_preferences["back_value"])) 381 | self.update_gui() 382 | 383 | def goforward_and_update_gui(self): 384 | self.w_player.go_forward(self.m_video.convert_seconds_to_ms(self.m_player.player_preferences["forward_value"])) 385 | self.update_gui() 386 | 387 | def slider_released_behavior(self): 388 | self.w_player.set_time(self.window.loadbar.value()) 389 | time.sleep(0.2) 390 | self.play() 391 | 392 | def update_time_to_timestamp(self): 393 | time_ms = self.m_video.timestamps[self.window.listwidget.currentItem().text()] 394 | self.w_player.set_time(time_ms) 395 | time.sleep(0.2) 396 | self.play() 397 | 398 | def show_hide_timestamps(self): 399 | if self.window.listframe.isVisible(): 400 | self.window.btnShowTimestamps.setText("show timestamps") 401 | self.window.listframe.setVisible(False) 402 | else: 403 | self.window.btnShowTimestamps.setText("hide timestamps") 404 | self.window.listframe.setVisible(True) 405 | 406 | def addTimeStamp(self): 407 | dlgAdd = AddItemDialog() 408 | if dlgAdd.exec(): 409 | input = dlgAdd.text1.text() 410 | time_ms = self.w_player.get_time() 411 | timestamp = self.m_video.convert_ms_to_hmmss(time_ms) 412 | title = "[{}] {}".format(timestamp,input) 413 | self.window.listwidget.addItem(title) 414 | self.m_video.add_timestamp(title,time_ms) 415 | 416 | def removeTimeStamp(self): 417 | item_to_remove = self.window.listwidget.currentItem() 418 | if item_to_remove == None: 419 | return 420 | 421 | title = self.window.listwidget.currentItem().text() 422 | self.window.listwidget.takeItem(self.window.listwidget.currentRow()) 423 | self.m_video.delete_timestamp(title) 424 | 425 | 426 | 427 | def set_view_connections(self): 428 | self.window.btnBack.clicked.connect(self.goback_and_update_gui) 429 | self.window.btnPlayPause.clicked.connect(self.play_pause) 430 | self.window.btnForward.clicked.connect(self.goforward_and_update_gui) 431 | self.window.btnpreferences.clicked.connect(self.window.show_preference_window) 432 | self.window.btnSubtitle.clicked.connect(self.show_subtitle_form) 433 | self.window.btnShowTimestamps.clicked.connect(self.show_hide_timestamps) 434 | self.window.speed_slider.valueChanged.connect(self.changeSpeedVideo) 435 | 436 | self.window.listwidget.itemClicked.connect(self.update_time_to_timestamp) 437 | 438 | self.window.btnAdd.clicked.connect(self.addTimeStamp) 439 | self.window.btnRemove.clicked.connect(self.removeTimeStamp) 440 | 441 | self.window.loadbar.sliderPressed.connect(self.pause) 442 | self.window.loadbar.sliderReleased.connect(self.slider_released_behavior) 443 | self.window.loadbar.valueChanged.connect(self.slider_clicked) 444 | self.window.volume_slider.valueChanged.connect(lambda: self.w_player.set_volume(self.window.volume_slider.value())) 445 | 446 | self.window.tool_bar2.orientationChanged.connect(self.window.set_loadbar2orientation) 447 | 448 | self.window.whisper_window.createbutton.clicked.connect(self.do_subtitles) 449 | self.window.whisper_window.combobox0.currentTextChanged.connect(self.set_subtitle_by_combo) 450 | 451 | def anchorVLCtoWindow(self, player, id): 452 | if sys.platform.startswith('linux'): # for Linux using the X Server 453 | player.set_xwindow(id) 454 | elif sys.platform == "win32": # for Windows 455 | player.set_hwnd(id) 456 | elif sys.platform == "darwin": # for MacOS 457 | player.set_nsobject(id) 458 | else: 459 | print("ERROR: this software does not work with", sys.platform) 460 | sys.exit() 461 | 462 | def close_program(self, event, track_pos,load_pos): 463 | print("closing..") 464 | self.window.preference_window.close() 465 | self.window.whisper_window.close() 466 | self.thread.terminate() 467 | 468 | 469 | combo_text = self.window.whisper_window.combobox0.currentText() 470 | if combo_text == '': 471 | sel_to_save = self.m_video.video_preferences["selected_sub_title"] 472 | else: 473 | sel_to_save = combo_text 474 | 475 | self.m_video.save_video_preferences(track_pos=track_pos, load_pos=load_pos, vol= self.w_player.get_volume(), sel_sub=sel_to_save) 476 | geometry = self.window.geometry() 477 | self.m_player.save_player_preferences(x=geometry.x(), y=geometry.y(), dim=geometry.width(), hei=geometry.height(), 478 | whisper_len=self.window.whisper_window.combobox1.currentText(), 479 | whisper_model=self.window.whisper_window.combobox2.currentText(), 480 | time_stamp=self.window.listframe.isVisible()) 481 | 482 | self.w_player.vlc_istance.release() 483 | 484 | 485 | def check_hide_sub(self): 486 | if self.m_video.video_preferences["selected_sub_title"] == 'Disable' or self.m_video.video_preferences["selected_sub_title"] == '': 487 | self.w_player.hide_subtitle() 488 | 489 | #this thread is used to check if hide subtitle (this can be done only after tot ms)) 490 | class ThreadWaitForSubs(QThread): 491 | check_hide_sub = Signal() 492 | def __init__(self,controller): 493 | QThread.__init__(self) 494 | self.controller = controller 495 | 496 | 497 | def run(self): 498 | QThread.sleep(1.2) 499 | if sys.platform == "darwin": 500 | self.check_hide_sub.emit() 501 | else: 502 | self.controller.check_hide_sub() 503 | 504 | # to avoid freezes, I use this QThread as a timer 505 | class ThreadTimer(QThread): 506 | update_gui = Signal() 507 | def __init__(self,controller): 508 | QThread.__init__(self) 509 | self.controller = controller 510 | 511 | def run(self): 512 | while not self.isInterruptionRequested(): 513 | if self.controller.sem.available() == 0 and self.controller.w_player.is_paused: 514 | self.controller.sem.acquire(1) 515 | QThread.sleep(1) 516 | 517 | # MacOS has problem if a external thread updates UI objects 518 | if sys.platform == "darwin": 519 | self.update_gui.emit() 520 | else: 521 | self.controller.update_gui() 522 | 523 | if not self.controller.w_player.is_playing(): # if is paused or stopped 524 | self.controller.window.btnPlayPause.setText(">") 525 | if not self.controller.w_player.is_paused: # if is stopped 526 | self.controller.w_player.stop() 527 | if self.controller.m_player.player_preferences["loop_video"]: 528 | self.controller.window.btnPlayPause.setEnabled(False) 529 | QThread.sleep(3) 530 | self.controller.play() 531 | self.controller.window.btnPlayPause.setEnabled(True) 532 | 533 | 534 | 535 | if __name__ == '__main__': 536 | c = Controller() -------------------------------------------------------------------------------- /img/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lurub/RViewer/92245d5330738d4dbd8e5fdc55ab284bebe7fb20/img/icon.ico -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lurub/RViewer/92245d5330738d4dbd8e5fdc55ab284bebe7fb20/img/icon.png -------------------------------------------------------------------------------- /img/mac_preferences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lurub/RViewer/92245d5330738d4dbd8e5fdc55ab284bebe7fb20/img/mac_preferences.png -------------------------------------------------------------------------------- /img/mac_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lurub/RViewer/92245d5330738d4dbd8e5fdc55ab284bebe7fb20/img/mac_screen.png -------------------------------------------------------------------------------- /img/mac_screen_nolist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lurub/RViewer/92245d5330738d4dbd8e5fdc55ab284bebe7fb20/img/mac_screen_nolist.png -------------------------------------------------------------------------------- /img/mac_whisper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lurub/RViewer/92245d5330738d4dbd8e5fdc55ab284bebe7fb20/img/mac_whisper.png -------------------------------------------------------------------------------- /img/mac_whisper_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lurub/RViewer/92245d5330738d4dbd8e5fdc55ab284bebe7fb20/img/mac_whisper_old.png -------------------------------------------------------------------------------- /img/window_screen_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lurub/RViewer/92245d5330738d4dbd8e5fdc55ab284bebe7fb20/img/window_screen_white.png -------------------------------------------------------------------------------- /mainview.py: -------------------------------------------------------------------------------- 1 | ''' 2 | view 3 | ''' 4 | 5 | from PySide6.QtCore import * 6 | from PySide6.QtGui import * 7 | from PySide6.QtWidgets import * 8 | 9 | import sys 10 | import os 11 | 12 | from views import PreferencesView, WhisperView 13 | 14 | 15 | class MainView(QMainWindow): 16 | 17 | def file_to_str(self, file_path): 18 | with open(file_path, 'r') as f: 19 | return f.read() 20 | 21 | 22 | def __init__(self, name_program, path_program, controller, player_user_preferences): 23 | 24 | self.controller = controller # used in close_event() 25 | 26 | dir_css_name = "styles" 27 | 28 | css_paths = { 29 | "loadbar" : os.path.join(path_program, dir_css_name, "loadbar_style.css"), 30 | "sliderbar" : os.path.join(path_program, dir_css_name, "speed_slider.css"), 31 | "play" : os.path.join(path_program, dir_css_name, "btnplaystop_play_style.css"), 32 | "stop" : os.path.join(path_program, dir_css_name, "btnplaystop_stop_style.css"), 33 | "back" : os.path.join(path_program, dir_css_name, "btnback_style.css"), 34 | "forward" : os.path.join(path_program, dir_css_name, "btnforward_style.css") 35 | } 36 | 37 | 38 | self.loadbar_style = self.file_to_str(css_paths["loadbar"]) 39 | self.speedslider_style = self.file_to_str(css_paths["sliderbar"]) 40 | self.play_style = self.file_to_str(css_paths["play"]) 41 | self.stop_style = self.file_to_str(css_paths["stop"]) 42 | self.back_style = self.file_to_str(css_paths["back"]) 43 | self.forward_style = self.file_to_str(css_paths["forward"]) 44 | 45 | 46 | #if user platform is windows and he want special dark mode.. 47 | if sys.platform == "win32" and player_user_preferences["windows_dark_mode"]: # for Windows 48 | sys.argv += ['-platform', 'windows:darkmode=2'] 49 | 50 | self.app = QApplication(sys.argv) 51 | self.app.setApplicationName(name_program) 52 | self.app.setApplicationVersion("1.0") 53 | path_icon = os.path.join(path_program, 'img', "icon.png") 54 | 55 | self.app.setWindowIcon(QIcon(path_icon)) 56 | 57 | self.app.setStyle('Fusion') 58 | super().__init__() 59 | self.preference_window = PreferencesView(controller=self.controller) 60 | self.whisper_window = WhisperView(controller=self.controller) 61 | available_geometry = self.screen().availableGeometry() 62 | 63 | # if is the first time that user open RViewer.. 64 | if player_user_preferences["x"] == 0: 65 | self.resize(available_geometry.width() / 3, 66 | available_geometry.height() / 2.5) 67 | else: 68 | self.setGeometry(player_user_preferences["x"], player_user_preferences["y"], player_user_preferences["dim"], player_user_preferences["hei"]) 69 | 70 | 71 | self.setWindowTitle(name_program) 72 | 73 | self.set_frames() 74 | 75 | self.set_widgets(player_user_preferences) 76 | 77 | self.add_widgets() 78 | 79 | def show_preference_window(self): 80 | self.preference_window.show() 81 | 82 | def show_whisper_window(self): 83 | self.whisper_window.show() 84 | 85 | def set_frames(self): 86 | main_layout = QHBoxLayout() 87 | main_layout.setContentsMargins(0,0,0,0) 88 | main_layout.setSpacing(0) 89 | 90 | self.videoframe = QFrame(self) 91 | #self.videoframe.setContentsMargins(0,0,0,0) 92 | 93 | self.list_layout = QVBoxLayout() 94 | self.list_layout.setContentsMargins(0,0,0,0) 95 | self.list_layout.setSpacing(0) 96 | 97 | self.listframe = QFrame() 98 | self.listframe.setAutoFillBackground(True) 99 | self.listframe.setMaximumWidth(200) 100 | self.listframe.setLayout(self.list_layout) 101 | 102 | self.addremove_layout = QHBoxLayout() 103 | self.addremove_layout.setContentsMargins(0,0,0,0) 104 | self.addremove_layout.setSpacing(0) 105 | 106 | self.addremoveframe = QFrame() 107 | self.addremoveframe.setAutoFillBackground(True) 108 | self.addremoveframe.setMaximumWidth(200) 109 | self.addremoveframe.setLayout(self.addremove_layout) 110 | 111 | 112 | main_layout.addWidget(self.videoframe) 113 | main_layout.addWidget(self.listframe) 114 | 115 | 116 | central_widget = QWidget() 117 | 118 | central_widget.setLayout(main_layout) 119 | central_widget.setAutoFillBackground(True) 120 | self.setCentralWidget(central_widget) 121 | 122 | 123 | def set_widgets(self, player_user_preferences): 124 | 125 | self.labelposition = QLabel(self) 126 | self.labelposition.setText("00:00:00") 127 | self.labelposition.setAlignment(Qt.AlignCenter) 128 | 129 | 130 | self.tool_bar = QToolBar() 131 | self.tool_bar2 = QToolBar() 132 | self.tool_bar3 = QToolBar() 133 | 134 | self.tool_bar.setMovable(False) 135 | 136 | # toolbar shown on second toolbar 137 | self.addToolBar(Qt.BottomToolBarArea, self.tool_bar) 138 | self.addToolBarBreak(Qt.BottomToolBarArea) 139 | self.addToolBar(Qt.BottomToolBarArea, self.tool_bar2) 140 | self.addToolBar(Qt.TopToolBarArea, self.tool_bar3) 141 | self.tool_bar3.setAllowedAreas(Qt.BottomToolBarArea | Qt.TopToolBarArea) 142 | 143 | self.btnBack = QPushButton(self) 144 | self.btnBack.setStyleSheet(self.back_style) 145 | self.btnBack.setFixedSize(70,28) 146 | self.btnBack.setText("-{}".format(player_user_preferences["back_value"])) 147 | self.btnBack.setShortcut(player_user_preferences["back_shortkey"]) 148 | 149 | 150 | self.btnPlayPause = QPushButton(self) 151 | self.btnPlayPause.setStyleSheet(self.play_style) 152 | self.btnPlayPause.setFixedSize(105,35) 153 | self.btnPlayPause.setText("||") 154 | self.btnPlayPause.setShortcut(player_user_preferences["playpause_shortkey"]) 155 | 156 | 157 | 158 | self.btnForward = QPushButton(self) 159 | self.btnForward.setStyleSheet( self.forward_style) 160 | self.btnForward.setFixedSize(70,28) 161 | self.btnForward.setText("+{}".format(player_user_preferences["forward_value"])) 162 | self.btnForward.setShortcut(player_user_preferences["forward_shortkey"]) 163 | 164 | 165 | 166 | self.loadbar = SliderClicker() 167 | self.loadbar.setOrientation(Qt.Horizontal) 168 | self.loadbar.setMinimum(0) 169 | self.loadbar.setMaximum(1) 170 | self.loadbar.setSingleStep(1) 171 | self.loadbar.setStyleSheet(self.loadbar_style) 172 | 173 | 174 | self.speed_slider = SliderClicker() 175 | self.speed_slider.setOrientation(Qt.Horizontal) 176 | self.speed_slider.setMinimum(1) 177 | self.speed_slider.setMaximum(10) 178 | available_width = self.screen().availableGeometry().width() 179 | self.speed_slider.setFixedWidth(available_width / 12) 180 | self.speed_slider.setValue(player_user_preferences["track_value"]) #default playback value of a video (5/5 = 1.0x) 181 | self.speed_slider.setTickInterval(1) 182 | self.speed_slider.setTickPosition(SliderClicker.TicksBelow) 183 | self.speed_slider.setToolTip("speed video") 184 | self.speed_slider.setStyleSheet(self.speedslider_style) 185 | 186 | self.volume_slider = SliderClicker() 187 | self.volume_slider.setOrientation(Qt.Vertical) 188 | self.volume_slider.setMinimum(1) 189 | self.volume_slider.setMaximum(125) 190 | 191 | available_width = self.screen().availableGeometry().width() 192 | self.volume_slider.setFixedHeight(22) 193 | self.volume_slider.setValue(100) 194 | self.volume_slider.setTickInterval(10) 195 | self.volume_slider.setTickPosition(SliderClicker.TicksLeft) 196 | 197 | 198 | 199 | self.label_speed = QLabel(self) 200 | 201 | self.label_speed.setAlignment(Qt.AlignCenter) 202 | 203 | self.labelduration = QLabel(self) 204 | self.labelduration.setText("00:00:00") 205 | self.labelduration.setAlignment(Qt.AlignCenter) 206 | 207 | 208 | self.spacer1 = QWidget() 209 | self.spacer1.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 210 | 211 | self.spacer2 = QWidget() 212 | self.spacer2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 213 | 214 | 215 | self.spacer3 = QWidget() 216 | self.spacer3.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 217 | 218 | self.spacer4 = QWidget() 219 | self.spacer4.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 220 | 221 | 222 | self.btnpreferences = QPushButton(self) 223 | self.btnpreferences.setText("preferences") 224 | 225 | self.btnShowTimestamps = QPushButton(self) 226 | self.btnShowTimestamps.setText("hide timestamps") 227 | 228 | self.btnSubtitle = QPushButton(self) 229 | self.btnSubtitle.setText("subtitles") 230 | 231 | 232 | self.btnAdd = QPushButton() 233 | self.btnAdd.setText("add") 234 | 235 | self.btnRemove = QPushButton() 236 | self.btnRemove.setText("remove") 237 | 238 | self.listwidget = QListWidget() 239 | 240 | 241 | 242 | def add_widgets(self): 243 | 244 | self.tool_bar.addWidget(self.spacer1) 245 | self.tool_bar.addWidget(self.btnBack) 246 | self.tool_bar.addWidget(self.btnPlayPause) 247 | self.tool_bar.addWidget(self.btnForward) 248 | self.tool_bar.addWidget(self.spacer2) 249 | 250 | 251 | self.tool_bar2.addWidget(self.labelposition) 252 | self.tool_bar2.addWidget(self.loadbar) 253 | self.tool_bar2.addWidget(self.labelduration) 254 | 255 | 256 | self.tool_bar3.addWidget(self.speed_slider) 257 | self.tool_bar3.addWidget(self.label_speed) 258 | self.tool_bar3.addWidget(self.btnSubtitle) 259 | self.tool_bar3.addWidget(self.btnShowTimestamps) 260 | self.tool_bar3.addWidget(self.btnpreferences) 261 | self.tool_bar3.addWidget(self.spacer3) 262 | self.tool_bar3.addWidget(QLabel("volume")) 263 | self.tool_bar3.addWidget(self.volume_slider) 264 | 265 | 266 | self.list_layout.addWidget(self.listwidget) 267 | 268 | self.list_layout.addWidget(self.addremoveframe) 269 | 270 | self.addremove_layout.addWidget(self.btnAdd) 271 | self.addremove_layout.addWidget(self.btnRemove) 272 | 273 | 274 | def set_loadbar2orientation(self): 275 | if self.loadbar.orientation() == Qt.Horizontal: 276 | self.loadbar.setOrientation(Qt.Vertical) 277 | else: 278 | self.loadbar.setOrientation(Qt.Horizontal) 279 | 280 | 281 | 282 | def closeEvent(self, event): 283 | self.controller.close_program(event, track_pos=self.speed_slider.value(), load_pos=self.loadbar.value()) 284 | 285 | 286 | 287 | class AddItemDialog(QDialog): 288 | def __init__(self): 289 | super().__init__() 290 | 291 | self.setWindowTitle("RV") 292 | 293 | QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel 294 | 295 | self.buttonBox = QDialogButtonBox(QBtn) 296 | self.buttonBox.accepted.connect(self.accept) 297 | self.buttonBox.rejected.connect(self.reject) 298 | 299 | self.text1 = QLineEdit() 300 | 301 | self.layout = QVBoxLayout() 302 | message = QLabel("Insert Title timestamp") 303 | self.layout.addWidget(message) 304 | self.layout.addWidget(self.text1) 305 | self.layout.addWidget(self.buttonBox) 306 | self.setLayout(self.layout) 307 | 308 | 309 | 310 | class SliderClicker(QSlider): 311 | 312 | ''' ---------------- WHY SLIDERCICKER CLASS IS USED? 313 | this class is useful for handling the "mousepressEvent" event which is not normally supported by QSlider. 314 | ''' 315 | def __init__(self): 316 | super().__init__() 317 | self.mouse_pressed = False # this boolean variable can be used for checking if mouse is pressed. 318 | 319 | 320 | def mousePressEvent(self, event): 321 | super(SliderClicker, self).mousePressEvent(event) 322 | if event.button() == Qt.LeftButton: 323 | self.mouse_pressed = True 324 | val = self.pixelPosToRangeValue(event.pos()) 325 | self.setValue(val) 326 | 327 | 328 | 329 | def pixelPosToRangeValue(self, pos): 330 | opt = QStyleOptionSlider() 331 | self.initStyleOption(opt) 332 | gr = self.style().subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderGroove, self) 333 | sr = self.style().subControlRect(QStyle.CC_Slider, opt, QStyle.SC_SliderHandle, self) 334 | 335 | if self.orientation() == Qt.Horizontal: 336 | sliderLength = sr.width() 337 | sliderMin = gr.x() 338 | sliderMax = gr.right() - sliderLength + 1 339 | else: 340 | sliderLength = sr.height() 341 | sliderMin = gr.y() 342 | sliderMax = gr.bottom() - sliderLength + 1 343 | pr = pos - sr.center() + sr.topLeft() 344 | p = pr.x() if self.orientation() == Qt.Horizontal else pr.y() 345 | return QStyle.sliderValueFromPosition(self.minimum(), self.maximum(), p - sliderMin, 346 | sliderMax - sliderMin, opt.upsideDown) 347 | 348 | -------------------------------------------------------------------------------- /model.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Model 3 | ''' 4 | 5 | import datetime 6 | import vlc 7 | import json 8 | import sys 9 | import os.path 10 | 11 | def _save_in_file(filename, dict): 12 | with open(filename, 'w', encoding='utf-8') as f: 13 | json.dump(dict, f) 14 | 15 | 16 | class VideoModel(): 17 | def __init__(self, player_preferences, path_program): 18 | 19 | self.timestamps = {} 20 | self.file_timestamps = os.path.join(path_program, "timestamps.json") 21 | 22 | 23 | self.default_video_preferences = { 24 | "track_value" : player_preferences["track_value"], 25 | "load_pos": 0, 26 | "selected_sub_title": 1, # I have to change this value when I know if it is a number or an object. 27 | "volume_value" : 100 # I have to change this value when I know range. 28 | } 29 | 30 | self.video_preferences = {} 31 | 32 | self.file_video_preferences = os.path.join(path_program, "preferences", "video_preferences.json") 33 | self.name_video = "" # this is the name into json file 34 | 35 | self.video_info = {} # Title, Artist, Duration, Rate, etc. 36 | 37 | # define video name into json and srt 38 | def set_namevideofile(self): 39 | self.name_video = self.video_info["Title"] + str(self.video_info["Duration"]) 40 | 41 | 42 | def _load_by_file(self,filename,videoname): 43 | if not os.path.isfile(filename): 44 | return False 45 | 46 | with open(filename, 'r') as z: 47 | json_file = json.load(z) 48 | if videoname in json_file: 49 | return json_file[videoname] 50 | else: # video is not in json file 51 | return False 52 | 53 | 54 | def _load_video_preferences_by_file(self): 55 | self.video_preferences = self._load_by_file(self.file_video_preferences,self.name_video) 56 | return self.video_preferences != False 57 | 58 | def _load_timestamps_by_file(self): 59 | self.timestamps = self._load_by_file(self.file_timestamps,self.name_video) 60 | return self.timestamps != False 61 | 62 | 63 | def load_videotimestamps(self): 64 | if not os.path.isfile(self.file_timestamps): 65 | video_dict = {self.name_video: {}} # in {} i'll have a dict of titles and timestamps 66 | _save_in_file(self.file_timestamps, video_dict) 67 | 68 | video_timestamps_exist = self._load_timestamps_by_file() 69 | if not video_timestamps_exist: 70 | self.timestamps = {} 71 | 72 | def delete_timestamp(self,title): 73 | self.timestamps.pop(title) 74 | self.save_timestamps() 75 | 76 | 77 | def load_videopreferences(self): 78 | #if file does not exists I have to create it and to set default values for a single video 79 | if not os.path.isfile(self.file_video_preferences): 80 | video_dict = {self.name_video: {}} 81 | video_dict[self.name_video] = self.default_video_preferences 82 | _save_in_file(self.file_video_preferences, video_dict) 83 | 84 | # now file exists, so I can read user video preferences (and if not exist video preferences, I use default video preferences ) 85 | video_preferences_exist = self._load_video_preferences_by_file() 86 | if not video_preferences_exist: 87 | self.video_preferences = self.default_video_preferences 88 | 89 | 90 | def add_timestamp(self,title,timestamp): 91 | self.timestamps[title] = timestamp 92 | self.save_timestamps() 93 | 94 | 95 | def save_timestamps(self): 96 | self._append_in_json(self.timestamps, self.file_timestamps) 97 | 98 | 99 | def save_video_preferences(self,track_pos,load_pos, vol, sel_sub): 100 | self.video_preferences = { 101 | "track_value" : track_pos, 102 | "load_pos": load_pos, 103 | "selected_sub_title": sel_sub, 104 | "volume_value" : vol 105 | } 106 | self._append_in_json(self.video_preferences, self.file_video_preferences) 107 | 108 | def _append_in_json(self, subset, file_name): #add a subset (I mean a {"namevideo1": number1, "namevideo2": number2, etc. }) into a json 109 | 110 | # read all json file because I need all json to modify a single value of a key. 111 | with open(file_name, 'r') as z: 112 | self.file_json = json.load(z) 113 | 114 | self.file_json[self.name_video] = subset 115 | _save_in_file(file_name, self.file_json) 116 | 117 | 118 | def get_videoinfo_byvideo(self, w_player): 119 | for key, value in vlc.Meta._enum_names_.items(): 120 | self.video_info[value] = w_player.get_video_property(vlc.Meta(key)) 121 | 122 | self.video_info["Subs"] = { "Count": w_player.get_sub_count(), 123 | "available" : w_player.get_sub(), 124 | "descriptions": w_player.get_sub_descriptions()} 125 | self.video_info["Rate"] = w_player.get_rate() 126 | self.video_info["Duration"] = w_player.get_duration() 127 | self.video_info["Duration_ss"] = w_player.get_duration() / 1000 128 | self.video_info["Duration_hh_mm_ss"] = self.convert_ms_to_hmmss(w_player.get_duration()) 129 | 130 | def convert_seconds_to_ms(self, seconds): 131 | return int(seconds) * 1000 132 | 133 | def convert_ms_to_hmmss(self, ms): 134 | return str(datetime.timedelta(seconds=int(ms/1000))) 135 | 136 | def convert_hmmss_to_ms(self,time_str): 137 | h, m, s = time_str.split(':') 138 | return str((int(h) * 3600 + int(m) * 60 + int(s)) * 1000) 139 | 140 | 141 | 142 | class PlayerModel(): 143 | def __init__(self, path_program): 144 | self.default_player_preferences = { 145 | "back_value" : 10, 146 | "forward_value" : 30, 147 | "track_value": 5, 148 | "loop_video": True, 149 | "pick_up_where_you_left_off": True, 150 | "track_video" : True, 151 | "show_subtitle_if_available" : True, 152 | "back_shortkey" : "Ctrl+D", 153 | "playpause_shortkey" : "Space", 154 | "forward_shortkey" : "Ctrl+G", 155 | "windows_dark_mode": sys.platform == "win32", 156 | "whisper_language": "english", 157 | "whisper_model": "base", 158 | "x" : 0, 159 | "y": 0, 160 | "dim": 0, 161 | "hei": 0, 162 | "show_time_stamp": True 163 | } 164 | 165 | self.player_preferences = {} 166 | 167 | self.file_player_preferences = os.path.join(path_program, "preferences", "player_preferences.json") 168 | 169 | 170 | 171 | if not os.path.isfile(self.file_player_preferences): #If player file does not exists 172 | self.save_preferences_in_file(self.file_player_preferences, self.default_player_preferences) #create a file with default values 173 | 174 | with open(self.file_player_preferences, 'r') as f: 175 | self.player_preferences = json.load(f) #update player preferences values with player_preferences.json 176 | 177 | def save_player_preferences(self,back=None,forward=None,track_pos=None,loop=None,pick=None,save=None,show=None,x=None,y=None,dim=None,hei=None, back_short=None, plpau_short=None, forwd_short=None,darkmodewin=None, whisper_len=None, whisper_model=None, time_stamp=None): 178 | if back is not None: 179 | self.player_preferences["back_value"] = back 180 | if forward is not None: 181 | self.player_preferences["forward_value"] = forward 182 | if track_pos is not None: 183 | self.player_preferences["track_value"] = track_pos 184 | if loop is not None: 185 | self.player_preferences["loop_video"] = loop 186 | if pick is not None: 187 | self.player_preferences["pick_up_where_you_left_off"] = pick 188 | if save is not None: 189 | self.player_preferences["track_video"] = save 190 | if show is not None: 191 | self.player_preferences["show_subtitle_if_available"] = show 192 | if x is not None: 193 | self.player_preferences["x"] = x 194 | if y is not None: 195 | self.player_preferences["y"] = y 196 | if dim is not None: 197 | self.player_preferences["dim"] = dim 198 | if hei is not None: 199 | self.player_preferences["hei"] = hei 200 | 201 | if back_short is not None: 202 | self.player_preferences["back_shortkey"] = back_short 203 | if plpau_short is not None: 204 | self.player_preferences["playpause_shortkey"] = plpau_short 205 | if forwd_short is not None: 206 | self.player_preferences["forward_shortkey"] = forwd_short 207 | 208 | if darkmodewin is not None: 209 | self.player_preferences["windows_dark_mode"] = darkmodewin 210 | 211 | if whisper_len is not None: 212 | self.player_preferences["whisper_language"] = whisper_len 213 | 214 | if whisper_model is not None: 215 | self.player_preferences["whisper_model"] = whisper_model 216 | 217 | if time_stamp is not None: 218 | self.player_preferences["show_time_stamp"] = time_stamp 219 | 220 | # note: all key-values are salved into file. i.e: if back is None, self.player_preferences["back_value"] value is saved into file. If it is not none, back value instead is saved into file! 221 | _save_in_file(self.file_player_preferences, self.player_preferences) 222 | 223 | 224 | -------------------------------------------------------------------------------- /preferences/player_preferences.json: -------------------------------------------------------------------------------- 1 | {"back_value": 10, "forward_value": 30, "track_value": 5, "loop_video": true, "pick_up_where_you_left_off": true, "track_video": true, "show_subtitle_if_available": true, "back_shortkey": "Ctrl+D", "playpause_shortkey": "Space", "forward_shortkey": "Ctrl+G", "windows_dark_mode": true, "whisper_language": "english", "whisper_model": "tiny", "x": 600, "y": 225, "dim": 967, "hei": 690, "show_time_stamp": true} -------------------------------------------------------------------------------- /preferences/video_preferences.json: -------------------------------------------------------------------------------- 1 | {"sample.mp41722619": {"track_value": 5, "load_pos": 2902, "selected_sub_title": "Disable", "volume_value": 100}, "video.mp41022040": {"track_value": 5, "load_pos": 28406, "selected_sub_title": "Disable", "volume_value": 100}, "video_test.mp4158639": {"track_value": 5, "load_pos": 72770, "selected_sub_title": 1, "volume_value": 100}, "test.mp454057": {"track_value": 5, "load_pos": 17973, "selected_sub_title": "Track 1 - [mp454057]", "volume_value": 100}, "test_sub.mkv54056": {"track_value": 5, "load_pos": 17616, "selected_sub_title": "Track 3 - [mkv54056]", "volume_value": 100}} -------------------------------------------------------------------------------- /preferences/vlc_args.txt: -------------------------------------------------------------------------------- 1 | --nofreetype-bold --freetype-rel-fontsize=15 --freetype-color=16777215 --freetype-shadow-opacity=255 --freetype-shadow-color=0 --freetype-background-color=16711935 --freetype-background-opacity=1 --freetype-outline-color=0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.4 2 | aiosignal==1.3.1 3 | alembic==1.11.1 4 | altgraph==0.17.3 5 | ansicon==1.89.0 6 | antlr4-python3-runtime==4.9.3 7 | anyio==3.7.0 8 | appdirs==1.4.4 9 | arrow==1.2.3 10 | asteroid-filterbanks==0.4.0 11 | async-timeout==4.0.2 12 | attrs==23.1.0 13 | audioread==3.0.0 14 | av==10.0.0 15 | beautifulsoup4==4.12.2 16 | blessed==1.20.0 17 | certifi==2023.5.7 18 | cffi==1.15.1 19 | charset-normalizer==3.1.0 20 | click==8.1.3 21 | cmaes==0.9.1 22 | colorama==0.4.6 23 | coloredlogs==15.0.1 24 | colorlog==6.7.0 25 | contourpy==1.0.7 26 | croniter==1.3.15 27 | ctranslate2==3.15.1 28 | cycler==0.11.0 29 | dateutils==0.6.12 30 | decorator==5.1.1 31 | deepdiff==6.3.0 32 | docopt==0.6.2 33 | einops==0.6.1 34 | exceptiongroup==1.1.1 35 | fastapi==0.88.0 36 | faster-whisper==0.6.0 37 | ffmpeg-python==0.2.0 38 | filelock==3.12.1 39 | flatbuffers==23.5.26 40 | fonttools==4.40.0 41 | frozenlist==1.3.3 42 | fsspec==2023.6.0 43 | future==0.18.3 44 | greenlet==2.0.2 45 | h11==0.14.0 46 | huggingface-hub==0.15.1 47 | humanfriendly==10.0 48 | HyperPyYAML==1.2.1 49 | idna==3.4 50 | inquirer==3.1.3 51 | itsdangerous==2.1.2 52 | Jinja2==3.1.2 53 | jinxed==1.2.0 54 | joblib==1.2.0 55 | julius==0.2.7 56 | kiwisolver==1.4.4 57 | lazy_loader==0.2 58 | librosa==0.10.0.post2 59 | lightning==2.0.3 60 | lightning-cloud==0.5.36 61 | lightning-utilities==0.8.0 62 | llvmlite==0.40.1rc1 63 | Mako==1.2.4 64 | markdown-it-py==3.0.0 65 | MarkupSafe==2.1.3 66 | matplotlib==3.7.1 67 | mdurl==0.1.2 68 | mpmath==1.3.0 69 | msgpack==1.0.5 70 | multidict==6.0.4 71 | networkx==3.1 72 | nltk==3.8.1 73 | numba==0.57.0 74 | numpy==1.24.3 75 | omegaconf==2.3.0 76 | onnxruntime==1.15.0 77 | optuna==3.2.0 78 | ordered-set==4.1.0 79 | packaging==23.1 80 | pandas==2.0.2 81 | pefile==2023.2.7 82 | Pillow==9.5.0 83 | pooch==1.6.0 84 | primePy==1.3 85 | protobuf==3.20.3 86 | psutil==5.9.5 87 | pyannote.audio==2.1.1 88 | pyannote.core==5.0.0 89 | pyannote.database==5.0.1 90 | pyannote.metrics==3.2.1 91 | pyannote.pipeline==2.3 92 | pycparser==2.21 93 | pydantic==1.10.9 94 | Pygments==2.15.1 95 | pyinstaller==5.12.0 96 | pyinstaller-hooks-contrib==2023.3 97 | PyJWT==2.7.0 98 | pyparsing==3.0.9 99 | pyreadline3==3.4.1 100 | PySide6==6.5.1.1 101 | PySide6-Addons==6.5.1.1 102 | PySide6-Essentials==6.5.1.1 103 | python-dateutil==2.8.2 104 | python-editor==1.0.4 105 | python-multipart==0.0.6 106 | python-vlc==3.0.18122 107 | pytorch-lightning==2.0.3 108 | pytorch-metric-learning==2.1.2 109 | pytz==2023.3 110 | pywin32-ctypes==0.2.0 111 | PyYAML==6.0 112 | readchar==4.0.5 113 | regex==2023.6.3 114 | requests==2.31.0 115 | rich==13.4.2 116 | ruamel.yaml==0.17.28 117 | ruamel.yaml.clib==0.2.7 118 | safetensors==0.3.1 119 | scikit-learn==1.2.2 120 | scipy==1.10.1 121 | semver==3.0.0 122 | sentencepiece==0.1.99 123 | shellingham==1.5.0.post1 124 | shiboken6==6.5.1.1 125 | six==1.16.0 126 | sniffio==1.3.0 127 | sortedcontainers==2.4.0 128 | soundfile==0.12.1 129 | soupsieve==2.4.1 130 | soxr==0.3.5 131 | speechbrain==0.5.14 132 | SQLAlchemy==2.0.16 133 | starlette==0.22.0 134 | starsessions==1.3.0 135 | sympy==1.12 136 | tabulate==0.9.0 137 | tensorboardX==2.6 138 | threadpoolctl==3.1.0 139 | tokenizers==0.13.3 140 | torch==2.0.1 141 | torch-audiomentations==0.11.0 142 | torch-pitch-shift==1.2.4 143 | torchaudio==2.0.2 144 | torchmetrics==0.11.4 145 | tqdm==4.65.0 146 | traitlets==5.9.0 147 | transformers==4.30.1 148 | typer==0.9.0 149 | typing_extensions==4.6.3 150 | tzdata==2023.3 151 | urllib3==2.0.3 152 | uvicorn==0.22.0 153 | wcwidth==0.2.6 154 | websocket-client==1.5.3 155 | websockets==11.0.3 156 | whisperx==3.1.1 157 | yarl==1.9.2 -------------------------------------------------------------------------------- /srt/test.mp454057.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 0:00:16.951 --> 0:00:17.018 3 | you 4 | 5 | 2 6 | 0:00:53.603 --> 0:00:53.806 7 | Thank you. 8 | 9 | -------------------------------------------------------------------------------- /srt/test_sub.mkv54056.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 0:00:16.951 --> 0:00:17.018 3 | you 4 | 5 | 2 6 | 0:00:53.603 --> 0:00:53.806 7 | Thank you. 8 | 9 | -------------------------------------------------------------------------------- /styles/btnback_style.css: -------------------------------------------------------------------------------- 1 | QPushButton {background-color: silver; color: black;} -------------------------------------------------------------------------------- /styles/btnforward_style.css: -------------------------------------------------------------------------------- 1 | QPushButton {background-color: silver; color: black;} -------------------------------------------------------------------------------- /styles/btnplaystop_play_style.css: -------------------------------------------------------------------------------- 1 | QPushButton {background-color: green; color: white;} 2 | -------------------------------------------------------------------------------- /styles/btnplaystop_stop_style.css: -------------------------------------------------------------------------------- 1 | QPushButton {background-color: #981c12; color: white;} 2 | -------------------------------------------------------------------------------- /styles/loadbar_style.css: -------------------------------------------------------------------------------- 1 | QSlider::groove:horizontal { 2 | border: 1px solid #bbb; 3 | background: blue; 4 | height: 10px; 5 | border-radius: 4px; 6 | } 7 | 8 | QSlider::sub-page:horizontal { 9 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 10 | stop: 0 #66e, stop: 1 #df0000); 11 | background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, 12 | stop: 0 #df0000, stop: 1 #4e0303); 13 | height: 10px; 14 | border-radius: 4px; 15 | } 16 | 17 | QSlider::add-page:horizontal { 18 | background: #353739; 19 | height: 10px; 20 | border-radius: 4px; 21 | } 22 | 23 | QSlider::handle:horizontal { 24 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, 25 | stop:0 #eee, stop:1 #ccc); 26 | width: 13px; 27 | margin-top: -2px; 28 | margin-bottom: -2px; 29 | border-radius: 4px; 30 | } 31 | 32 | QSlider::handle:horizontal:hover { 33 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, 34 | stop:0 #fff, stop:1 #ddd); 35 | border: 1px solid #444; 36 | border-radius: 4px; 37 | } 38 | 39 | 40 | 41 | QSlider::groove:vertical { 42 | border: 1px solid #bbb; 43 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 44 | stop: 0 #4e0303, stop: 1 #df0000); 45 | border-radius: 2px; 46 | } 47 | 48 | QSlider::handle:vertical { 49 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, 50 | stop:0 #ccc, stop:1 #eee); 51 | height: 10px; 52 | margin-left: -15px; 53 | margin-right: -15px; 54 | border-radius: 15px; 55 | } 56 | 57 | QSlider::handle:vertical:hover { 58 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, 59 | stop:0 #ddd, stop:1 #fff); 60 | border: 1px solid #444; 61 | border-radius: 4px; 62 | } 63 | 64 | QSlider::sub-page:vertical { 65 | background: #353739; 66 | height: 2px; 67 | border-radius: 4px; 68 | } 69 | 70 | QSlider::sub-page:vertical { 71 | background: #353739; 72 | height: 2px; 73 | border-radius: 4px; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /styles/speed_slider.css: -------------------------------------------------------------------------------- 1 | QSlider::groove:horizontal{ 2 | border: 1px solid #000000; 3 | background: #434446 ; 4 | height: 7px; 5 | border-radius: 3px; 6 | } 7 | QSlider::sub-page:horizontal { 8 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 9 | stop: 0 #df0000, stop: 1 #df0000); 10 | 11 | border: 1px solid #777; 12 | height: 10px; 13 | border-radius: 4px; 14 | } 15 | QSlider::add-page:horizontal { 16 | background: #0d8a45ab; 17 | border: 1px solid #000000; 18 | height: 10px; 19 | border-radius: 4px; 20 | } 21 | QSlider::handle:horizontal{ 22 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, 23 | stop:0 #fff, stop:1 #df0000); 24 | border: 1px solid #777; 25 | width: 5px; 26 | margin-top: -4px; 27 | margin-bottom: -4px; 28 | border-radius: 2px; 29 | } 30 | 31 | 32 | 33 | /*Verticales*/ 34 | QSlider::groove:vertical{ 35 | border: 1px solid #110303; 36 | background: white; 37 | width:7px; 38 | height: 55px; 39 | border-radius: 3px; 40 | } 41 | QSlider::add-page:vertical { 42 | background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 43 | stop: 0 #ABC7EC, stop: 1 #df0000); 44 | border: 1px solid #154A98; 45 | width: 10px; 46 | border-radius: 4px; 47 | } 48 | QSlider::sub-page:vertical { 49 | background: #ffffff; 50 | border: 1px solid #353739; 51 | width: 7px; 52 | border-radius: 4px; 53 | } 54 | QSlider::handle:vertical{ 55 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, 56 | stop:0 #fff, stop:1 #ABC7EC); 57 | border: 1px solid #353739; 58 | height: 5px; 59 | margin-left: -4px; 60 | margin-right: -4px; 61 | border-radius: 2px; 62 | } 63 | /*Horizontales y Verticales*/ 64 | QSlider::handle:horizontal:hover, 65 | QSlider::handle:vertical:hover{ 66 | border: 1px solid hsl(0, 5%, 33%); 67 | border-radius: 3px; 68 | } -------------------------------------------------------------------------------- /test_torch.py: -------------------------------------------------------------------------------- 1 | import torch 2 | print(torch.cuda.is_available()) -------------------------------------------------------------------------------- /timestamps.json: -------------------------------------------------------------------------------- 1 | {"sample.mp41722619": {"[0:03:51] 5": 231829}, "test_sub.mkv54056": {"[0:00:15] ISTANTE PRIMA DEL YOU": 15856}} -------------------------------------------------------------------------------- /views.py: -------------------------------------------------------------------------------- 1 | ''' 2 | view 3 | ''' 4 | 5 | import time 6 | from PySide6.QtCore import * 7 | from PySide6.QtGui import * 8 | from PySide6.QtWidgets import * 9 | 10 | import vlc 11 | import sys 12 | import shutil 13 | import os 14 | 15 | class PlayerView(QThread): 16 | 17 | def __init__(self, controller): 18 | QThread.__init__(self) 19 | self.vlc_istance = None 20 | self.vlc_player = None 21 | self.media = None 22 | self.controller = controller 23 | self.name_file_arg = "vlc_args.txt" 24 | self.file_arg_path = os.path.join(self.controller.program_path,'preferences', "{}".format(self.name_file_arg)) 25 | 26 | def run(self): 27 | self.video_path = sys.argv[1] 28 | args = self.load_args_by_file() 29 | self.vlc_istance = vlc.Instance(args) # "--verbose -1" 30 | self.vlc_player = self.vlc_istance.media_player_new() 31 | self.media = self.vlc_istance.media_new(sys.argv[1]) 32 | self.vlc_player.set_media(self.media) 33 | self.is_paused = False 34 | self.controller.sem_player.release(1) 35 | 36 | def load_args_by_file(self): 37 | with open(self.file_arg_path, 'r') as file: 38 | return file.read() 39 | 40 | 41 | 42 | def get_media(self): 43 | return self.media 44 | 45 | def get_video_property(self, e_meta): 46 | return self.media.get_meta(e_meta) 47 | 48 | 49 | def play(self): 50 | return self.vlc_player.play() 51 | 52 | def pause(self): 53 | return self.vlc_player.pause() 54 | 55 | def stop(self): 56 | return self.vlc_player.stop() 57 | 58 | def is_playing(self): 59 | return self.vlc_player.is_playing() 60 | 61 | def get_state(self): 62 | return self.vlc_player.get_state() 63 | 64 | 65 | def parse_media(self): 66 | return self.media.parse() 67 | 68 | 69 | def go_back(self, ms): 70 | self.set_time(max(self.get_time() - ms, 0)) 71 | 72 | def go_forward(self, ms): 73 | new_t = self.get_time() + ms 74 | if new_t < self.get_duration(): 75 | self.set_time(new_t) 76 | 77 | 78 | def get_duration(self): 79 | return self.vlc_player.get_length() 80 | 81 | def get_time(self): 82 | return self.vlc_player.get_time() 83 | 84 | def set_time(self, i_time): 85 | return self.vlc_player.set_time(i_time) 86 | 87 | def get_position(self): 88 | return self.vlc_player.get_position() 89 | 90 | def set_position(self, f_pos): 91 | return self.vlc_player.set_position(f_pos) 92 | 93 | def get_rate(self): 94 | return self.vlc_player.get_rate() 95 | 96 | def set_rate(self, rate): 97 | return self.vlc_player.set_rate(rate) 98 | 99 | 100 | def get_sub_count(self): 101 | return self.vlc_player.video_get_spu_count() 102 | 103 | def get_sub(self): 104 | return self.vlc_player.video_get_spu() 105 | 106 | def set_sub(self, i_spu): 107 | return self.vlc_player.video_set_spu(i_spu) 108 | 109 | def get_sub_descriptions(self): 110 | return self.vlc_player.video_get_spu_description() 111 | 112 | def get_sub_delay(self): 113 | return self.vlc_player.video_get_spu_delay() 114 | 115 | def set_sub_delay(self, delay): 116 | return self.vlc_player.video_set_spu_delay(delay) 117 | 118 | def set_subtitle(self, subtitle_path): 119 | return self.vlc_player.video_set_subtitle_file(subtitle_path) 120 | 121 | def hide_subtitle(self): 122 | self.set_sub(self.get_sub_descriptions()[0][0]) 123 | 124 | def get_audio_count(self): 125 | return self.vlc_player.audio_get_track_count() 126 | 127 | def get_audio_description(self): 128 | return self.vlc_player.audio_get_track_description() 129 | 130 | def get_istance_vlc_player(self): 131 | return self.vlc_player 132 | 133 | 134 | def set_volume(self, volume): 135 | return self.vlc_player.audio_set_volume(volume) 136 | 137 | def get_volume(self): 138 | return self.vlc_player.audio_get_volume() 139 | 140 | 141 | 142 | 143 | class PreferencesView(QDialog): 144 | def __init__(self, controller): 145 | super(PreferencesView, self).__init__() 146 | self.controller = controller 147 | self.player_preferences = self.controller.m_player.player_preferences 148 | self.nameprogram = self.controller.program_name 149 | self.setWindowTitle(self.nameprogram + " preferences") 150 | 151 | 152 | self.setFixedSize(245, 480) 153 | self.set_widgets() 154 | self.add_widgets() 155 | self.track_bar_conversion = 5 156 | 157 | 158 | def showEvent(self, event): 159 | self.spinbox1.setValue(int(self.player_preferences["back_value"])) 160 | self.text1.setText(self.player_preferences["back_shortkey"]) 161 | self.spinbox2.setValue(int(self.player_preferences["forward_value"])) 162 | self.text2.setText(self.player_preferences["forward_shortkey"]) 163 | self.text3.setText(self.player_preferences["playpause_shortkey"]) 164 | self.spinbox3.setValue(float(self.player_preferences["track_value"] / self.track_bar_conversion)) 165 | 166 | self.checkbox1.setChecked(self.player_preferences["loop_video"]) 167 | self.checkbox2.setChecked(self.player_preferences["pick_up_where_you_left_off"]) 168 | self.checkbox3.setChecked(self.player_preferences["track_video"]) 169 | self.checkbox4.setChecked(self.player_preferences["show_subtitle_if_available"]) 170 | self.checkbox5.setChecked(self.player_preferences["windows_dark_mode"]) 171 | 172 | 173 | def unsaved_changes(self): 174 | changes_list = [] 175 | changes_list.insert(0,self.player_preferences["back_value"] != self.spinbox1.value()) 176 | changes_list.insert(1,self.player_preferences["forward_value"] != self.spinbox2.value()) 177 | changes_list.insert(2,self.player_preferences["track_value"] != int(self.spinbox3.value() * self.track_bar_conversion)) 178 | changes_list.insert(3,self.player_preferences["loop_video"] != self.checkbox1.isChecked()) 179 | changes_list.insert(4,self.player_preferences["pick_up_where_you_left_off"] != self.checkbox2.isChecked()) 180 | changes_list.insert(5,self.player_preferences["track_video"] != self.checkbox3.isChecked()) 181 | changes_list.insert(6,self.player_preferences["show_subtitle_if_available"] != self.checkbox4.isChecked()) 182 | changes_list.insert(7,self.player_preferences["windows_dark_mode"] != self.checkbox5.isChecked()) 183 | 184 | changes_list.insert(8,self.player_preferences["back_shortkey"] != self.text1.text()) 185 | changes_list.insert(9,self.player_preferences["forward_shortkey"] != self.text2.text()) 186 | changes_list.insert(10,self.player_preferences["playpause_shortkey"] != self.text3.text()) 187 | 188 | return True in changes_list 189 | 190 | 191 | def changes_applied(self): 192 | dlg = QMessageBox(self) 193 | dlg.setWindowTitle(self.nameprogram + " changes applied.") 194 | dlg.setText("Changes applied. please reopen the video player.") 195 | dlg.setStandardButtons(QMessageBox.Ok) 196 | dlg.setIcon(QMessageBox.Information) 197 | button = dlg.exec_() 198 | 199 | def accept(self): 200 | if self.unsaved_changes(): 201 | track_bar_conversion = 5 202 | self.controller.m_player.save_player_preferences(back=self.spinbox1.value(),forward=self.spinbox2.value(),track_pos=int(self.spinbox3.value() * track_bar_conversion), 203 | loop=self.checkbox1.isChecked(),pick=self.checkbox2.isChecked(), save=self.checkbox3.isChecked(),show=self.checkbox4.isChecked(), 204 | back_short=self.text1.text(), forwd_short=self.text2.text(), plpau_short=self.text3.text(), darkmodewin=self.checkbox5.isChecked()) 205 | self.changes_applied() 206 | super().accept() 207 | 208 | 209 | def reject(self): 210 | if self.unsaved_changes(): 211 | dlg = QMessageBox(self) 212 | dlg.setWindowTitle(self.nameprogram + " unsaved changes") 213 | dlg.setText("There are values that have not been saved. Do you want to save the changes?") 214 | dlg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) 215 | dlg.setIcon(QMessageBox.Question) 216 | button = dlg.exec_() 217 | if button == QMessageBox.Yes: 218 | self.accept() 219 | 220 | super().reject() 221 | 222 | 223 | def restore(self): 224 | dlg = QMessageBox(self) 225 | dlg.setWindowTitle(self.nameprogram + " restore default values") 226 | dlg.setText("Are you sure to restore default values?") 227 | dlg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) 228 | dlg.setIcon(QMessageBox.Question) 229 | button = dlg.exec_() 230 | if button == QMessageBox.Yes: 231 | self.controller.m_player.player_preferences = self.controller.m_player.default_player_preferences 232 | self.close() 233 | 234 | def set_widgets(self): 235 | self.layoutt = QFormLayout() 236 | 237 | self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) 238 | self.button_box.accepted.connect(self.accept) 239 | self.button_box.rejected.connect(self.reject) 240 | 241 | self.restorebutton = QPushButton() 242 | self.restorebutton.setText("restore") 243 | self.restorebutton.setStyleSheet('QPushButton {background-color: red}') 244 | self.restorebutton.clicked.connect(self.restore) 245 | 246 | self.spinbox1 = QSpinBox() 247 | self.spinbox1.setMinimum(1) 248 | 249 | self.spinbox2 = QSpinBox() 250 | self.spinbox2.setMinimum(1) 251 | 252 | self.spinbox3 = QDoubleSpinBox() 253 | self.spinbox3.setMaximum(2) 254 | self.spinbox3.setMinimum(0.2) 255 | self.spinbox3.setSingleStep(0.2) 256 | self.spinbox3.setDecimals(1) 257 | 258 | self.spinbox3.lineEdit().setReadOnly(True) # edit disabled but arrows enabled 259 | 260 | self.checkbox1 = QCheckBox("loop video") 261 | self.checkbox2 = QCheckBox("pick up where you left off") 262 | self.checkbox3 = QCheckBox("use video speed instead player speed") 263 | self.checkbox4 = QCheckBox("show subtitle (if available) at startup") 264 | self.checkbox5 = QCheckBox("special Dark Mode (Windows OS only)") 265 | if sys.platform == "win32": # for Windows 266 | self.checkbox5.setEnabled(True) 267 | else: 268 | self.checkbox5.setEnabled(False) 269 | 270 | self.text1 = QLineEdit() 271 | self.text2 = QLineEdit() 272 | self.text3 = QLineEdit() 273 | 274 | self.labelspeed1 = QLabel("The player speed value is used for") 275 | self.labelspeed2 = QLabel("videos that you have never played.\n") 276 | self.labelspeed1.setStyleSheet("QLabel {color: #4c4c4c;}") 277 | self.labelspeed2.setStyleSheet("QLabel {color: #4c4c4c;}") 278 | 279 | 280 | def add_widgets(self): 281 | self.layoutt.setSpacing(10) 282 | self.layoutt.addRow(self.restorebutton) 283 | self.layoutt.addRow(QLabel("back value:"), self.spinbox1) 284 | self.layoutt.addRow(QLabel("back short key:") , self.text1) 285 | self.layoutt.addRow(QLabel("forward value:"), self.spinbox2) 286 | self.layoutt.addRow(QLabel("forward short key:") , self.text2) 287 | self.layoutt.addRow(QLabel("play/pause short key:") , self.text3) 288 | self.layoutt.addRow(QLabel("player speed value:"),self.spinbox3) 289 | self.layoutt.addRow(self.labelspeed1) 290 | self.layoutt.addRow(self.labelspeed2) 291 | self.layoutt.addRow(self.checkbox1) 292 | self.layoutt.addRow(self.checkbox2) 293 | self.layoutt.addRow(self.checkbox3) 294 | self.layoutt.addRow(self.checkbox4) 295 | self.layoutt.addRow(self.checkbox5) 296 | self.layoutt.addRow(self.button_box) 297 | 298 | self.setLayout(self.layoutt) 299 | 300 | 301 | 302 | 303 | 304 | 305 | class WhisperView(QDialog): 306 | def __init__(self, controller): 307 | super(WhisperView, self).__init__() 308 | self.controller = controller 309 | self.nameprogram = self.controller.program_name 310 | self.name_video = "" 311 | self.setWindowTitle(self.nameprogram + " subtitles") 312 | 313 | self.models = ["tiny", "base", "small"] 314 | self.languages = { "en": "english", "zh": "chinese", "de": "german", "es": "spanish", "ru": "russian", "ko": "korean", "fr": "french", "ja": "japanese", "pt": "portuguese", "tr": "turkish", "pl": "polish", "ca": "catalan", "nl": "dutch", "ar": "arabic", "sv": "swedish", "it": "italian", "id": "indonesian", "hi": "hindi", "fi": "finnish", "vi": "vietnamese", "he": "hebrew", "uk": "ukrainian", "el": "greek", "ms": "malay", "cs": "czech", "ro": "romanian", "da": "danish", "hu": "hungarian", "ta": "tamil", "no": "norwegian", "th": "thai", "ur": "urdu", "hr": "croatian", "bg": "bulgarian", "lt": "lithuanian", "la": "latin", "mi": "maori", "ml": "malayalam", "cy": "welsh", "sk": "slovak", "te": "telugu", "fa": "persian", "lv": "latvian", "bn": "bengali", "sr": "serbian", "az": "azerbaijani", "sl": "slovenian", "kn": "kannada", "et": "estonian", "mk": "macedonian", "br": "breton", "eu": "basque", "is": "icelandic", "hy": "armenian", "ne": "nepali", "mn": "mongolian", "bs": "bosnian", "kk": "kazakh", "sq": "albanian", "sw": "swahili", "gl": "galician", "mr": "marathi", "pa": "punjabi", "si": "sinhala", "km": "khmer", "sn": "shona", "yo": "yoruba", "so": "somali", "af": "afrikaans", "oc": "occitan", "ka": "georgian", "be": "belarusian", "tg": "tajik", "sd": "sindhi", "gu": "gujarati", "am": "amharic", "yi": "yiddish", "lo": "lao", "uz": "uzbek", "fo": "faroese", "ht": "haitian creole", "ps": "pashto", "tk": "turkmen", "nn": "nynorsk", "mt": "maltese", "sa": "sanskrit", "lb": "luxembourgish", "my": "myanmar", "bo": "tibetan", "tl": "tagalog", "mg": "malagasy", "as": "assamese", "tt": "tatar", "haw": "hawaiian", "ln": "lingala", "ha": "hausa", "ba": "bashkir", "jw": "javanese", "su": "sundanese",} 315 | 316 | self.setFixedSize(255, 320) 317 | self.set_widgets() 318 | self.add_widgets() 319 | 320 | 321 | 322 | def closeEvent(self, event): 323 | self.controller.whisper_view_close() 324 | 325 | def import_and_load_subs(self): 326 | self.import_file() 327 | self.controller.set_subtitle_and_load_into_combobox() 328 | 329 | 330 | def import_file(self): 331 | dlg = QFileDialog() 332 | dlg.setFileMode(QFileDialog.AnyFile) 333 | dlg.setNameFilter("Text files (*.aqt *.cvd *.dks *.jss *.sub *.ttxt *.mpl *.txt *.pjs *.psb *.rt *.smi *.ssf *.srt *.ssa *.svcd *.usf*.idx)") 334 | filenames = "" 335 | 336 | if dlg.exec_(): 337 | filenames = dlg.selectedFiles() 338 | new_name = os.path.join(self.controller.program_path,'srt', "{}.srt".format(self.name_video)) 339 | 340 | try: 341 | shutil.copyfile(filenames[0],new_name) 342 | except shutil.SameFileError: 343 | pass 344 | 345 | 346 | 347 | def get_language_selected(self): 348 | return list(self.languages.keys())[list(self.languages.values()).index(self.combobox1.currentText())] 349 | 350 | def set_widgets(self): 351 | self.layoutt = QFormLayout() 352 | 353 | self.label0 = QLabel("Select Subtitle") 354 | 355 | self.combobox0 = QComboBox() 356 | 357 | self.label1 = QLabel("CREATE SUBTITLES WITH WHISPERX") 358 | self.label1.setAlignment(Qt.AlignCenter) 359 | 360 | 361 | self.importbutton = QPushButton() 362 | self.importbutton.setText("Import subtitles from existing file") 363 | self.importbutton.clicked.connect(self.import_and_load_subs) 364 | 365 | 366 | self.combobox1 = QComboBox() 367 | self.combobox1.addItems(sorted(self.languages.values())) 368 | 369 | self.combobox2 = QComboBox() 370 | self.combobox2.addItems(self.models) 371 | 372 | self.createbutton = QPushButton() 373 | self.createbutton.setText("create subtitles") 374 | 375 | 376 | self.textedit = QTextEdit() 377 | # self.textedit.setVisible(False) 378 | self.textedit.setReadOnly(True) 379 | self.textedit.setFixedHeight(80) 380 | 381 | def add_widgets(self): 382 | self.layoutt.setSpacing(10) 383 | 384 | self.layoutt.addRow(self.importbutton) 385 | self.layoutt.addRow(self.label0,self.combobox0) 386 | self.layoutt.addRow(QLabel("")) 387 | self.layoutt.addRow(self.label1) 388 | self.layoutt.addRow(QLabel("select Audio Language:"), self.combobox1) 389 | self.layoutt.addRow(QLabel("select Whisper Model:"), self.combobox2) 390 | self.layoutt.addRow(self.textedit) 391 | self.layoutt.addRow(self.createbutton) 392 | 393 | self.setLayout(self.layoutt) -------------------------------------------------------------------------------- /whispermodel.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Model 3 | ''' 4 | import time 5 | import os.path 6 | import datetime 7 | import sys 8 | import whisperx 9 | import torch 10 | class Whisper(): 11 | 12 | def __init__(self, program_path, name_video, path_video, lang_sub, model_selected): 13 | self.name_video = name_video 14 | self.path_video = path_video 15 | self.program_path = program_path 16 | 17 | self.model = None 18 | self.lang_sub = lang_sub 19 | self.model_selected = model_selected 20 | self.batch_size = 4 # reduce if low on GPU mem 21 | self.compute_type = "int8" # change to "int8" if low on GPU mem (may reduce accuracy) 22 | 23 | 24 | def run(self): 25 | 26 | print("starting WhisperX..") 27 | 28 | DEVICE = "cuda" if torch.cuda.is_available() else "cpu" 29 | 30 | print("{}: Loading Whisper {} Model on\n '{}' [torch.cuda {}]".format(time.strftime("%H:%M:%S", time.localtime()), self.model_selected.upper(), "GPU" if DEVICE == "cuda" else "CPU", "available" if DEVICE == "cuda" else "NOT available")) 31 | 32 | 33 | if self.model is None: 34 | self.model = whisperx.load_model(self.model_selected, language=self.lang_sub, device=DEVICE, compute_type=self.compute_type) 35 | 36 | audio = whisperx.load_audio(self.path_video) 37 | 38 | print("{}: WhisperX 1. -> TRASCRIPTION started...".format(time.strftime("%H:%M:%S", time.localtime()))) 39 | 40 | # 1. Transcribe with original whisper (batched) 41 | 42 | result = self.model.transcribe(audio, language=self.lang_sub, batch_size=self.batch_size) 43 | 44 | print("{}: TRANSCRIBE OPERATION COMPLETED!".format(time.strftime("%H:%M:%S", time.localtime()))) 45 | 46 | print("{}: WhisperX 2. -> ALIGNMENT started...".format(time.strftime("%H:%M:%S", time.localtime()))) 47 | 48 | # 2. Align whisper output 49 | model_a, metadata = whisperx.load_align_model(language_code="en", device=DEVICE) 50 | 51 | result = whisperx.align(result["segments"], model_a, metadata, audio, DEVICE, return_char_alignments=False) 52 | 53 | print("{}: ALIGNMENT OPERATION COMPLETED!".format(time.strftime("%H:%M:%S", time.localtime()))) 54 | 55 | srt_file_name = os.path.join(self.program_path,'srt', "{}.srt".format(self.name_video)) 56 | 57 | self.create_srt(srt_file_name, result) 58 | print("{}: srt file Subtitles created correctly!".format(time.strftime("%H:%M:%S", time.localtime()))) 59 | 60 | 61 | def create_srt(self,srt_file_name, result): 62 | self.str_out = "" 63 | i=0 64 | for key in result["segments"]: 65 | i += 1 66 | self.str_out += "{}\n{} --> {}\n{}\n\n".format(str(i), self.format_td(key["start"]), self.format_td(key["end"]), key["text"]) 67 | 68 | with open(srt_file_name, 'w', encoding="utf-8") as f: 69 | f.write(self.str_out) 70 | 71 | def format_td(self, seconds, digits=3): 72 | isec, fsec = divmod(round(seconds*10**digits), 10**digits) 73 | return ("{}.{:0%d.0f}" % digits).format(datetime.timedelta(seconds=isec), fsec) 74 | 75 | 76 | if __name__ == '__main__': 77 | if len(sys.argv) > 0: 78 | Whisper(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]).run() 79 | else: 80 | print("error: no arguments") 81 | --------------------------------------------------------------------------------