├── .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 |
--------------------------------------------------------------------------------