summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHasanAbbadi <hasanabady55@gmail.com>2022-07-02 11:57:25 +0300
committerHasanAbbadi <hasanabady55@gmail.com>2022-07-02 11:57:25 +0300
commite504a95131aac2c438d9ac3fbf504d90df57d9f8 (patch)
treea3ab70569094dfd7477bebfe33a726ebe144ed67
Init
-rw-r--r--README.md1
-rw-r--r--jaSubs.lua143
-rw-r--r--jaSubs.py982
-rw-r--r--jaSubs_config.py257
4 files changed, 1383 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..89445e5
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# jaSubs
diff --git a/jaSubs.lua b/jaSubs.lua
new file mode 100644
index 0000000..360c7a7
--- /dev/null
+++ b/jaSubs.lua
@@ -0,0 +1,143 @@
+-- v. 2.7
+-- Interactive subtitles for `mpv` for language learners.
+--
+-- default keybinding to start/stop: F5
+-- default keybinding to hide/show: F6
+-- if jaSubs start automatically - mpv won't show notification
+--
+-- dirs in which jaSubs will start automatically; part of path/filename will also work; case insensitive; regexp
+-- autostart_in = {'German', ' ger ', '%.ger%.', 'Deutsch', 'Hebrew'}
+autostart_in = {'Japanese'}
+
+-- for Mac change python3 to python or pythonw
+start_command = 'python3 "%s" "%s" "%s"'
+
+-- recomend to have these in tmpfs, or at least ssd.
+sub_file = '/tmp/mpv_sub'
+mpv_socket = '/tmp/mpv_socket'
+
+keybinding = 'F3'
+keybinding_hide = 'F6'
+
+pyname = '~/.config/mpv/scripts/jaSubs/jaSubs.py'
+
+------------------------------------------------------
+
+debug = false
+-- debug = true
+
+if debug == true then
+ start_command = ''
+ start_command = 'terminator -e \'python3 "%s" "%s" "%s"; sleep 33\''
+end
+
+------------------------------------------------------
+
+function s1()
+ if running == true then
+ s_rm()
+ return
+ end
+
+ running = true
+ mp.msg.warn('Starting jaSubs...')
+ mp.register_event("end-file", s_rm)
+ rnbr = math.random(11111111, 99999999)
+
+ if debug == true then
+ rnbr = ''
+ end
+
+ mpv_socket_2 = mpv_socket .. '_' .. rnbr
+ sub_file_2 = sub_file .. '_' .. rnbr
+
+ -- setting up socket to control mpv
+ mp.set_property("input-ipc-server", mpv_socket_2)
+
+ -- without visible subs won't work
+ sbv = mp.get_property("sub-visibility")
+ mp.set_property("sub-visibility", "yes")
+ mp.set_property("sub-ass-override", "force")
+
+ sub_color1 = mp.get_property("sub-color", "1/1/1/1")
+ sub_color2 = mp.get_property("sub-border-color", "0/0/0/1")
+ sub_color3 = mp.get_property("sub-shadow-color", "0/0/0/1")
+ mp.set_property("sub-color", "0/0/0/0")
+ mp.set_property("sub-border-color", "0/0/0/0")
+ mp.set_property("sub-shadow-color", "0/0/0/0")
+
+ start_command_2 = start_command:format(pyname:gsub('~', os.getenv('HOME')), mpv_socket_2, sub_file_2)
+ os.execute(start_command_2 .. ' &')
+
+ mp.observe_property("sub-text", "string", s2)
+end
+
+function s2(name, value)
+ if type(value) == "string" then
+ file = io.open(sub_file_2, "w")
+ file:write(value)
+ file:close()
+ end
+end
+
+function s_rm()
+ running = false
+ hidden = false
+ mp.msg.warn('Quitting jaSubs...')
+
+ mp.set_property("sub-visibility", sbv)
+ mp.set_property("sub-color", sub_color1)
+ mp.set_property("sub-border-color", sub_color2)
+ --~ mp.set_property("sub-shadow-color", sub_color3)
+
+ os.execute('pkill -f "' .. mpv_socket_2 .. '" &')
+ os.execute('(sleep 3 && rm "' .. mpv_socket_2 .. '") &')
+ os.execute('(sleep 3 && rm "' .. sub_file_2 .. '") &')
+end
+
+function started()
+ if mp.get_property("sub") == 'no' then
+ return true
+ end
+
+ hidden = false
+
+ for kk, pp in pairs(autostart_in) do
+ if mp.get_property("path"):lower():find(pp:lower()) or mp.get_property("working-directory"):lower():find(pp:lower()) then
+ s1()
+ break
+ end
+ end
+end
+
+function s1_1()
+ if running == true then
+ s_rm()
+ mp.command('show-text "Quitting jaSubs..."')
+ else
+ if mp.get_property("sub") == 'no' then
+ mp.command('show-text "Select subtitles before starting jaSubs."')
+ else
+ s1()
+ mp.command('show-text "Starting jaSubs..."')
+ end
+ end
+end
+
+function hide_show()
+ if running == true then
+ if hidden == true then
+ os.execute('rm "' .. mpv_socket_2 .. '_hide" &')
+ mp.osd_message("Showing jaSubs.", .8)
+ hidden = false
+ else
+ os.execute('touch "' .. mpv_socket_2 .. '_hide" &')
+ mp.osd_message("Hiding jaSubs.", .8)
+ hidden = true
+ end
+ end
+end
+
+mp.add_forced_key_binding(keybinding, "start-stop-jaSubs", s1_1)
+mp.add_forced_key_binding(keybinding_hide, "hide-show-jaSubs", hide_show)
+mp.register_event("file-loaded", started)
diff --git a/jaSubs.py b/jaSubs.py
new file mode 100644
index 0000000..b193dda
--- /dev/null
+++ b/jaSubs.py
@@ -0,0 +1,982 @@
+#! /usr/bin/env python
+
+# v. 2.10
+# Interactive subtitles for `mpv` for language learners.
+
+import os, subprocess, sys
+import random, re, time
+import requests
+import threading, queue
+import calendar, math, base64
+import numpy
+import ast
+
+from urllib.parse import quote
+from json import loads
+from json.decoder import JSONDecodeError
+
+import warnings
+from six.moves import urllib
+
+from PyQt5.QtCore import Qt, QThread, QObject, pyqtSignal, pyqtSlot, QSize
+from PyQt5.QtWidgets import QApplication, QFrame, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QWidget
+from PyQt5.QtGui import QPalette, QPaintEvent, QPainter, QPainterPath, QFont, QFontMetrics, QColor, QPen, QBrush
+
+# Import Japanese tokinizer
+from sudachipy import tokenizer
+from sudachipy import dictionary
+tokenizer_obj = dictionary.Dictionary().create()
+
+pth = os.path.expanduser('~/.config/mpv/scripts/')
+os.chdir(pth)
+import jaSubs_config as config
+
+# returns ([[word: reading, translation]..], [morphology = '', gender = ''])
+# jisho.org
+def jisho(word):
+ DOMAIN = 'jisho.org'
+ VERSION = '1'
+
+ base_url = f'https://{DOMAIN}/api/v{VERSION}'
+
+ def get(url, params=None):
+ if params is None:
+ params = {}
+
+ response = requests.get(
+ url,
+ params=params,
+ )
+
+ json = response.json()
+ if response.status_code != 200:
+ raise APIException(response.status_code,
+ response.content.decode())
+
+ try:
+ word = json['data'][0]['japanese'][0]['word'] + ': ' + json['data'][0]['japanese'][0]['reading']
+ reading = json['data'][0]['japanese'][0]['reading']
+ translations = json['data'][0]['senses'][0]['english_definitions']
+ pairs = [[word, '']]
+ for definition in translations:
+ pairs.append(['', definition])
+
+ return pairs, [reading, '']
+ except:
+ return [['No translation Found', ''], ['', '']]
+
+ def search(keyword):
+ url = f'{base_url}/search/words'
+ params = {'keyword': keyword} if keyword else {}
+ return get(url, params=params)
+
+ return search(word)
+
+
+# offline dictionary with word \t translation
+def tab_divided_dict(word):
+ if word in offdict:
+ tr = re.sub('<.*?>', '', offdict[word]) if config.tab_divided_dict_remove_tags_B else offdict[word]
+ tr = tr.replace('\\n', '\n').replace('\\~', '~')
+ return [[tr, '-']], ['', '']
+ else:
+ return [], ['', '']
+
+# Google
+# https://github.com/Saravananslb/py-googletranslation
+class TokenAcquirer:
+ """Google Translate API token generator
+
+ translate.google.com uses a token to authorize the requests. If you are
+ not Google, you do have this token and will have to pay for use.
+ This class is the result of reverse engineering on the obfuscated and
+ minified code used by Google to generate such token.
+
+ The token is based on a seed which is updated once per hour and on the
+ text that will be translated.
+ Both are combined - by some strange math - in order to generate a final
+ token (e.g. 464393.115905) which is used by the API to validate the
+ request.
+
+ This operation will cause an additional request to get an initial
+ token from translate.google.com.
+
+ Example usage:
+ >>> from pygoogletranslation.gauthtoken import TokenAcquirer
+ >>> acquirer = TokenAcquirer()
+ >>> text = 'test'
+ >>> tk = acquirer.do(text)
+ >>> tk
+ 464393.115905
+ """
+
+ def __init__(self, tkk='0', tkk_url='https://translate.google.com/translate_a/element.js', proxies=None):
+
+ if proxies is not None:
+ self.proxies = proxies
+ else:
+ self.proxies = None
+
+ r = requests.get(tkk_url, proxies=self.proxies)
+
+ if r.status_code == 200:
+ re_tkk = re.search("(?<=tkk=\\')[0-9.]{0,}", str(r.content.decode("utf-8")))
+ if re_tkk:
+ self.tkk = re_tkk.group(0)
+ else:
+ self.tkk = '0'
+ else:
+ self.tkk = '0'
+
+
+ def _xr(self, a, b):
+ size_b = len(b)
+ c = 0
+ while c < size_b - 2:
+ d = b[c + 2]
+ d = ord(d[0]) - 87 if 'a' <= d else int(d)
+ d = self.rshift(a, d) if '+' == b[c + 1] else a << d
+ a = a + d & 4294967295 if '+' == b[c] else a ^ d
+
+ c += 3
+ return a
+
+ def acquire(self, text):
+ a = []
+ # Convert text to ints
+ for i in text:
+ val = ord(i)
+ if val < 0x10000:
+ a += [val]
+ else:
+ # Python doesn't natively use Unicode surrogates, so account for those
+ a += [
+ math.floor((val - 0x10000) / 0x400 + 0xD800),
+ math.floor((val - 0x10000) % 0x400 + 0xDC00)
+ ]
+
+ b = self.tkk
+ d = b.split('.')
+ b = int(d[0]) if len(d) > 1 else 0
+
+ # assume e means char code array
+ e = []
+ g = 0
+ size = len(a)
+ while g < size:
+ l = a[g]
+ # just append if l is less than 128(ascii: DEL)
+ if l < 128:
+ e.append(l)
+ # append calculated value if l is less than 2048
+ else:
+ if l < 2048:
+ e.append(l >> 6 | 192)
+ else:
+ # append calculated value if l matches special condition
+ if (l & 64512) == 55296 and g + 1 < size and \
+ a[g + 1] & 64512 == 56320:
+ g += 1
+ l = 65536 + ((l & 1023) << 10) + (a[g] & 1023) # This bracket is important
+ e.append(l >> 18 | 240)
+ e.append(l >> 12 & 63 | 128)
+ else:
+ e.append(l >> 12 | 224)
+ e.append(l >> 6 & 63 | 128)
+ e.append(l & 63 | 128)
+ g += 1
+ a = b
+ for i, value in enumerate(e):
+ a += value
+ a = self._xr(a, '+-a^+6')
+ a = self._xr(a, '+-3^+b+-f')
+ a ^= int(d[1]) if len(d) > 1 else 0
+ if a < 0: # pragma: nocover
+ a = (a & 2147483647) + 2147483648
+ a %= 1000000 # int(1E6)
+ return '{}.{}'.format(a, a ^ b)
+
+ def do(self, text):
+ tk = self.acquire(text)
+ return tk
+
+
+ def rshift(self, val, n):
+ """python port for '>>>'(right shift with padding)
+ """
+ return (val % 0x100000000) >> n
+
+# translate.google.com
+def google(word):
+ word = word.replace('\n', ' ').strip()
+ url = 'https://translate.google.com/translate_a/single?client=t&sl={lang_from}&tl={lang_to}&hl={lang_to}&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&ie=UTF-8&oe=UTF-8&otf=1&pc=1&ssel=3&tsel=3&kc=2&q={word}'.format(lang_from = config.lang_from, lang_to = config.lang_to, word = quote(word))
+
+ pairs = []
+ fname = 'urls/' + url.replace('/', "-")
+ try:
+ if ' ' in word:
+ raise Exception('skip saving')
+
+ p = open(fname).read().split('=====/////-----')
+ try:
+ word_descr = p[1].strip()
+ except:
+ word_descr = ''
+
+ for pi in p[0].strip().split('\n\n'):
+ pi = pi.split('\n')
+ pairs.append([pi[0], pi[1]])
+ except:
+ acquirer = TokenAcquirer()
+ tk = acquirer.do(word)
+
+ url = '{url}&tk={tk}'.format(url = url, tk = tk)
+ p = requests.get(url, headers={'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36'}).text
+ p = loads(p)
+
+ try:
+ pairs.append([p[0][0][0], p[0][0][1]])
+ except:
+ pass
+
+ if p[1] != None:
+ for translations in p[1]:
+ for translation in translations[2]:
+ try:
+ t1 = translation[5] + ' ' + translation[0]
+ except:
+ t1 = translation[0]
+
+ t2 = ', '.join(translation[1])
+
+ if not len(t1):
+ t1 = '-'
+ if not len(t2):
+ t2 = '-'
+
+ pairs.append([t1, t2])
+
+ word_descr = ''
+
+ return pairs, ['', '']
+
+def mpv_pause():
+ os.system('echo \'{ "command": ["set_property", "pause", true] }\' | socat - "' + mpv_socket + '" > /dev/null')
+
+def mpv_resume():
+ os.system('echo \'{ "command": ["set_property", "pause", false] }\' | socat - "' + mpv_socket + '" > /dev/null')
+
+def mpv_pause_status():
+ stdoutdata = subprocess.getoutput('echo \'{ "command": ["get_property", "pause"] }\' | socat - "' + mpv_socket + '"')
+
+ try:
+ return loads(stdoutdata)['data']
+ except:
+ return mpv_pause_status()
+
+def mpv_fullscreen_status():
+ stdoutdata = subprocess.getoutput('echo \'{ "command": ["get_property", "fullscreen"] }\' | socat - "' + mpv_socket + '"')
+
+ try:
+ return loads(stdoutdata)['data']
+ except:
+ return mpv_fullscreen_status()
+
+def mpv_message(message, timeout = 3000):
+ os.system('echo \'{ "command": ["show-text", "' + message + '", "' + str(timeout) + '"] }\' | socat - "' + mpv_socket + '" > /dev/null')
+
+def stripsd2(phrase):
+ return ''.join(e for e in phrase.strip().lower() if e == ' ' or (e.isalnum() and not e.isdigit())).strip()
+
+def r2l(l):
+ l2 = ''
+
+ try:
+ l2 = re.findall('(?!%)\W+$', l)[0][::-1]
+ except:
+ pass
+
+ l2 += re.sub('^\W+|(?!%)\W+$', '', l)
+
+ try:
+ l2 += re.findall('^\W+', l)[0][::-1]
+ except:
+ pass
+
+ return l2
+
+def split_long_lines(line, chunks = 2, max_symbols_per_line = False):
+ if max_symbols_per_line:
+ chunks = 0
+ while 1:
+ chunks += 1
+ new_lines = []
+ for i in range(chunks):
+ new_line = ' '.join(numpy.array_split(line.split(' '), chunks)[i])
+ new_lines.append(new_line)
+
+ if len(max(new_lines, key = len)) <= max_symbols_per_line:
+ return '\n'.join(new_lines)
+ else:
+ new_lines = []
+ for i in range(chunks):
+ new_line = ' '.join(numpy.array_split(line.split(' '), chunks)[i])
+ new_lines.append(new_line)
+
+ return '\n'.join(new_lines)
+
+def dir2(name):
+ print('\n'.join(dir( name )))
+ exit()
+
+class thread_subtitles(QObject):
+ update_subtitles = pyqtSignal(bool, bool)
+
+ @pyqtSlot()
+ def main(self):
+ global subs
+
+ was_hidden = 0
+ inc = 0
+ auto_pause_2_ind = 0
+ last_updated = time.time()
+
+ while 1:
+ time.sleep(config.update_time)
+
+ # hide subs when mpv isn't in focus or in fullscreen
+ if inc * config.update_time > config.focus_checking_time - 0.0001:
+ while 'mpv' not in subprocess.getoutput('xdotool getwindowfocus getwindowname') or (config.hide_when_not_fullscreen_B and not mpv_fullscreen_status()) or (os.path.exists(mpv_socket + '_hide')):
+ if not was_hidden:
+ self.update_subtitles.emit(True, False)
+ was_hidden = 1
+ else:
+ time.sleep(config.focus_checking_time)
+ inc = 0
+ inc += 1
+
+ if was_hidden:
+ was_hidden = 0
+ self.update_subtitles.emit(False, False)
+ continue
+
+ try:
+ tmp_file_subs = open(sub_file).read()
+ except:
+ continue
+
+ if config.extend_subs_duration2max_B and not len(tmp_file_subs):
+ if not config.extend_subs_duration_limit_sec:
+ continue
+ if config.extend_subs_duration_limit_sec > time.time() - last_updated:
+ continue
+
+ last_updated = time.time()
+
+ while tmp_file_subs != subs:
+ if config.auto_pause == 2:
+ if not auto_pause_2_ind and len(re.sub(' +', ' ', stripsd2(subs.replace('\n', ' '))).split(' ')) > config.auto_pause_min_words - 1 and not mpv_pause_status():
+ mpv_pause()
+ auto_pause_2_ind = 1
+
+ if auto_pause_2_ind and mpv_pause_status():
+ break
+
+ auto_pause_2_ind = 0
+
+ subs = tmp_file_subs
+
+ if config.auto_pause == 1:
+ if len(re.sub(' +', ' ', stripsd2(subs.replace('\n', ' '))).split(' ')) > config.auto_pause_min_words - 1:
+ mpv_pause()
+
+ self.update_subtitles.emit(False, False)
+
+ break
+
+class thread_translations(QObject):
+ get_translations = pyqtSignal(str, int, bool)
+
+ @pyqtSlot()
+ def main(self):
+ while 1:
+ to_new_word = False
+
+ try:
+ word, globalX = config.queue_to_translate.get(False)
+ except:
+ time.sleep(config.update_time)
+ continue
+
+ # changing cursor to hourglass during translation
+ QApplication.setOverrideCursor(Qt.WaitCursor)
+
+ threads = []
+ for translation_function_name in config.translation_function_names:
+ threads.append(threading.Thread(target = globals()[translation_function_name], args = (word,)))
+ for x in threads:
+ x.start()
+ while any(thread.is_alive() for thread in threads):
+ if config.queue_to_translate.qsize():
+ to_new_word = True
+ break
+ time.sleep(config.update_time)
+
+ QApplication.restoreOverrideCursor()
+
+ if to_new_word:
+ continue
+
+ if config.block_popup:
+ continue
+
+ self.get_translations.emit(word, globalX, False)
+
+# drawing layer
+# because can't calculate outline with precision
+class drawing_layer(QLabel):
+ def __init__(self, line, subs, parent=None):
+ super().__init__(None)
+ self.line = line
+ self.setStyleSheet(config.style_subs)
+ self.psuedo_line = 0
+
+ def draw_text_n_outline(self, painter: QPainter, x, y, outline_width, outline_blur, text):
+ outline_color = QColor(config.outline_color)
+
+ font = self.font()
+ text_path = QPainterPath()
+ text_path.addText(x, y, font, text)
+
+ # draw blur
+ range_width = range(outline_width, outline_width + outline_blur)
+ # ~range_width = range(outline_width + outline_blur, outline_width, -1)
+
+ for width in range_width:
+ if width == min(range_width):
+ alpha = 200
+ else:
+ alpha = (max(range_width) - width) / max(range_width) * 200
+ alpha = int(alpha)
+
+ blur_color = QColor(outline_color.red(), outline_color.green(), outline_color.blue(), alpha)
+ blur_brush = QBrush(blur_color, Qt.SolidPattern)
+ blur_pen = QPen(blur_brush, width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
+
+ painter.setPen(blur_pen)
+ painter.drawPath(text_path)
+
+ # draw outline
+ outline_color = QColor(outline_color.red(), outline_color.green(), outline_color.blue(), 255)
+ outline_brush = QBrush(outline_color, Qt.SolidPattern)
+ outline_pen = QPen(outline_brush, outline_width, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
+
+ painter.setPen(outline_pen)
+ painter.drawPath(text_path)
+
+ # draw text
+ color = self.palette().color(QPalette.Text)
+ painter.setPen(color)
+ painter.drawText(x, y, text)
+
+ if config.outline_B:
+ def paintEvent(self, evt: QPaintEvent):
+ if not self.psuedo_line:
+ self.psuedo_line = 1
+ return
+
+ x = y = 0
+ y += self.fontMetrics().ascent()
+ painter = QPainter(self)
+
+ self.draw_text_n_outline(
+ painter,
+ x,
+ y + config.outline_top_padding - config.outline_bottom_padding,
+ config.outline_thickness,
+ config.outline_blur,
+ text = self.line
+ )
+
+ def resizeEvent(self, *args):
+ self.setFixedSize(
+ self.fontMetrics().width(self.line),
+ self.fontMetrics().height() +
+ config.outline_bottom_padding +
+ config.outline_top_padding
+ )
+
+ def sizeHint(self):
+ return QSize(
+ self.fontMetrics().width(self.line),
+ self.fontMetrics().height()
+ )
+
+class events_class(QLabel):
+ mouseHover = pyqtSignal(str, int, bool)
+ redraw = pyqtSignal(bool, bool)
+
+ def __init__(self, word, subs, skip = False, parent=None):
+ super().__init__(word)
+ self.setMouseTracking(True)
+ self.word = word
+ self.subs = subs
+ self.skip = skip
+ self.highlight = False
+
+ self.setStyleSheet('background: transparent; color: transparent;')
+
+ def highligting(self, color, underline_width):
+ color = QColor(color)
+ color = QColor(color.red(), color.green(), color.blue(), 200)
+ painter = QPainter(self)
+
+ if config.hover_underline:
+ font_metrics = QFontMetrics(self.font())
+ text_width = font_metrics.width(self.word)
+ text_height = font_metrics.height()
+
+ brush = QBrush(color)
+ pen = QPen(brush, underline_width, Qt.SolidLine, Qt.RoundCap)
+ painter.setPen(pen)
+ if not self.skip:
+ painter.drawLine(0, text_height - underline_width, text_width, text_height - underline_width)
+
+ if config.hover_hightlight:
+ x = y = 0
+ y += self.fontMetrics().ascent()
+
+ painter.setPen(color)
+ painter.drawText(x, y + config.outline_top_padding - config.outline_bottom_padding, self.word)
+
+ if config.outline_B:
+ def paintEvent(self, evt: QPaintEvent):
+ if self.highlight:
+ self.highligting(config.hover_color, config.hover_underline_thickness)
+
+ #####################################################
+
+ def resizeEvent(self, event):
+ text_height = self.fontMetrics().height()
+ text_width = self.fontMetrics().width(self.word)
+
+ self.setFixedSize(text_width, text_height + config.outline_bottom_padding + config.outline_top_padding)
+
+ def enterEvent(self, event):
+ if not self.skip:
+ self.highlight = True
+ self.repaint()
+ config.queue_to_translate.put((self.word, event.globalX()))
+
+ @pyqtSlot()
+ def leaveEvent(self, event):
+ if not self.skip:
+ self.highlight = False
+ self.repaint()
+
+ config.scroll = {}
+ self.mouseHover.emit('', 0, False)
+ QApplication.restoreOverrideCursor()
+
+ def wheel_scrolling(self, event):
+ if event.y() > 0:
+ return 'ScrollUp'
+ if event.y():
+ return 'ScrollDown'
+ if event.x() > 0:
+ return 'ScrollLeft'
+ if event.x():
+ return 'ScrollRight'
+
+ def wheelEvent(self, event):
+ for mouse_action in config.mouse_buttons:
+ if self.wheel_scrolling(event.angleDelta()) == mouse_action[0]:
+ if event.modifiers() == eval('Qt.%s' % mouse_action[1]):
+ exec('self.%s(event)' % mouse_action[2])
+
+ def mousePressEvent(self, event):
+ for mouse_action in config.mouse_buttons:
+ if 'Scroll' not in mouse_action[0]:
+ if event.button() == eval('Qt.%s' % mouse_action[0]):
+ if event.modifiers() == eval('Qt.%s' % mouse_action[1]):
+ exec('self.%s(event)' % mouse_action[2])
+
+ #####################################################
+
+ def f_show_in_browser(self, event):
+ config.avoid_resuming = True
+ os.system(config.show_in_browser.replace('${word}', self.word))
+
+ def f_auto_pause_options(self, event):
+ if config.auto_pause == 2:
+ config.auto_pause = 0
+ else:
+ config.auto_pause += 1
+ mpv_message('auto_pause: %d' % config.auto_pause)
+
+ @pyqtSlot()
+ def f_subs_screen_edge_padding_decrease(self, event):
+ config.subs_screen_edge_padding -= 5
+ mpv_message('subs_screen_edge_padding: %d' % config.subs_screen_edge_padding)
+ self.redraw.emit(False, True)
+
+ @pyqtSlot()
+ def f_subs_screen_edge_padding_increase(self, event):
+ config.subs_screen_edge_padding += 5
+ mpv_message('subs_screen_edge_padding: %d' % config.subs_screen_edge_padding)
+ self.redraw.emit(False, True)
+
+ @pyqtSlot()
+ def f_font_size_decrease(self, event):
+ config.style_subs = re.sub('font-size: (\d+)px;', lambda size: [ 'font-size: %dpx;' % ( int(size.group(1)) - 1 ), mpv_message('font: %s' % size.group(1)) ][0], config.style_subs, flags = re.I)
+ self.redraw.emit(False, True)
+
+ @pyqtSlot()
+ def f_font_size_increase(self, event):
+ config.style_subs = re.sub('font-size: (\d+)px;', lambda size: [ 'font-size: %dpx;' % ( int(size.group(1)) + 1 ), mpv_message('font: %s' % size.group(1)) ][0], config.style_subs, flags = re.I)
+ self.redraw.emit(False, True)
+
+ @pyqtSlot()
+ def f_translation_full_sentence(self, event):
+ self.mouseHover.emit(self.subs , event.globalX(), True)
+
+ def f_auto_pause_min_words_decrease(self, event):
+ config.auto_pause_min_words -= 1
+ mpv_message('auto_pause_min_words: %d' % config.auto_pause_min_words)
+
+ def f_auto_pause_min_words_increase(self, event):
+ config.auto_pause_min_words += 1
+ mpv_message('auto_pause_min_words: %d' % config.auto_pause_min_words)
+
+class main_class(QWidget):
+ def __init__(self):
+ super().__init__()
+
+ self.thread_subs = QThread()
+ self.obj = thread_subtitles()
+ self.obj.update_subtitles.connect(self.render_subtitles)
+ self.obj.moveToThread(self.thread_subs)
+ self.thread_subs.started.connect(self.obj.main)
+ self.thread_subs.start()
+
+ self.thread_translations = QThread()
+ self.obj2 = thread_translations()
+ self.obj2.get_translations.connect(self.render_popup)
+ self.obj2.moveToThread(self.thread_translations)
+ self.thread_translations.started.connect(self.obj2.main)
+ self.thread_translations.start()
+
+ # start the forms
+ self.subtitles_base()
+ self.subtitles_base2()
+ self.popup_base()
+
+ def clearLayout(self, layout):
+ if layout == 'subs':
+ layout = self.subtitles_vbox
+ self.subtitles.hide()
+ elif layout == 'subs2':
+ layout = self.subtitles_vbox2
+ self.subtitles2.hide()
+ elif layout == 'popup':
+ layout = self.popup_vbox
+ self.popup.hide()
+
+ if layout is not None:
+ while layout.count():
+ item = layout.takeAt(0)
+ widget = item.widget()
+
+ if widget is not None:
+ widget.deleteLater()
+ else:
+ self.clearLayout(item.layout())
+
+ def subtitles_base(self):
+ self.subtitles = QFrame()
+ self.subtitles.setAttribute(Qt.WA_TranslucentBackground)
+ self.subtitles.setWindowFlags(Qt.X11BypassWindowManagerHint)
+ self.subtitles.setStyleSheet(config.style_subs)
+
+ self.subtitles_vbox = QVBoxLayout(self.subtitles)
+ self.subtitles_vbox.setSpacing(config.subs_padding_between_lines)
+ self.subtitles_vbox.setContentsMargins(0, 0, 0, 0)
+
+ def subtitles_base2(self):
+ self.subtitles2 = QFrame()
+ self.subtitles2.setAttribute(Qt.WA_TranslucentBackground)
+ self.subtitles2.setWindowFlags(Qt.X11BypassWindowManagerHint)
+ self.subtitles2.setStyleSheet(config.style_subs)
+
+ self.subtitles_vbox2 = QVBoxLayout(self.subtitles2)
+ self.subtitles_vbox2.setSpacing(config.subs_padding_between_lines)
+ self.subtitles_vbox2.setContentsMargins(0, 0, 0, 0)
+
+ if config.pause_during_translation_B:
+ self.subtitles2.enterEvent = lambda event : [mpv_pause(), setattr(config, 'block_popup', False)][0]
+ self.subtitles2.leaveEvent = lambda event : [mpv_resume(), setattr(config, 'block_popup', True)][0] if not config.avoid_resuming else [setattr(config, 'avoid_resuming', False), setattr(config, 'block_popup', True)][0]
+
+ def popup_base(self):
+ self.popup = QFrame()
+ self.popup.setAttribute(Qt.WA_TranslucentBackground)
+ self.popup.setWindowFlags(Qt.X11BypassWindowManagerHint)
+ self.popup.setStyleSheet(config.style_popup)
+
+ self.popup_inner = QFrame()
+ outer_box = QVBoxLayout(self.popup)
+ outer_box.addWidget(self.popup_inner)
+
+ self.popup_vbox = QVBoxLayout(self.popup_inner)
+ self.popup_vbox.setSpacing(0)
+
+ def render_subtitles(self, hide = False, redraw = False):
+ if hide or not len(subs):
+ try:
+ self.subtitles.hide()
+ self.subtitles2.hide()
+ finally:
+ return
+
+ if redraw:
+ self.subtitles.setStyleSheet(config.style_subs)
+ self.subtitles2.setStyleSheet(config.style_subs)
+ else:
+ self.clearLayout('subs')
+ self.clearLayout('subs2')
+
+ if hasattr(self, 'popup'):
+ self.popup.hide()
+
+ # if subtitle consists of one overly long line - split into two
+ if config.split_long_lines_B and len(subs.split('\n')) == 1 and len(subs.split(' ')) > config.split_long_lines_words_min - 1:
+ subs2 = split_long_lines(subs)
+ else:
+ subs2 = subs
+
+ subs2 = re.sub(' +', ' ', subs2).strip()
+
+ ##############################
+
+ for line in subs2.split('\n'):
+ line2 = ' %s ' % line.strip()
+ ll = drawing_layer(line2, subs2)
+
+ hbox = QHBoxLayout()
+ hbox.setContentsMargins(0, 0, 0, 0)
+ hbox.setSpacing(0)
+ hbox.addStretch()
+ hbox.addWidget(ll)
+ hbox.addStretch()
+ self.subtitles_vbox.addLayout(hbox)
+
+ ####################################
+
+ hbox = QHBoxLayout()
+ hbox.setContentsMargins(0, 0, 0, 0)
+ hbox.setSpacing(0)
+ hbox.addStretch()
+
+ line2 += '\00'
+
+ # Japanese Fix
+ mode = tokenizer.Tokenizer.SplitMode.A
+ line2 = [m.surface() for m in tokenizer_obj.tokenize(line2, mode)]
+
+ for smbl in line2:
+ word = smbl
+ if smbl.isalpha():
+ ll = events_class(word, subs2)
+ ll.mouseHover.connect(self.render_popup)
+ ll.redraw.connect(self.render_subtitles)
+
+ hbox.addWidget(ll)
+ else:
+ ll = events_class(smbl, subs2, skip = True)
+ hbox.addWidget(ll)
+
+ hbox.addStretch()
+ self.subtitles_vbox2.addLayout(hbox)
+
+ self.subtitles.adjustSize()
+ self.subtitles2.adjustSize()
+
+ w = self.subtitles.geometry().width()
+ h = self.subtitles.height = self.subtitles.geometry().height()
+
+ x = (config.screen_width/2) - (w/2)
+
+ if config.subs_top_placement_B:
+ y = config.subs_screen_edge_padding
+ else:
+ y = config.screen_height - config.subs_screen_edge_padding - h
+
+ self.subtitles.setGeometry(int(x), int(y), 0, 0)
+ self.subtitles.show()
+
+ self.subtitles2.setGeometry(int(x), int(y), 0, 0)
+ self.subtitles2.show()
+
+ def render_popup(self, text, x_cursor_pos, is_line):
+ if text == '':
+ if hasattr(self, 'popup'):
+ self.popup.hide()
+ return
+
+ self.clearLayout('popup')
+
+ if is_line:
+ QApplication.setOverrideCursor(Qt.WaitCursor)
+
+ line = globals()[config.translation_function_name_full_sentence](text)
+ if config.translation_function_name_full_sentence == 'google':
+ try:
+ line = line[0][0][0].strip()
+ except:
+ line = 'Google translation failed.'
+
+ if config.split_long_lines_B and len(line.split('\n')) == 1 and len(line.split(' ')) > config.split_long_lines_words_min - 1:
+ line = split_long_lines(line)
+
+ ll = QLabel(line)
+ ll.setObjectName("first_line")
+ self.popup_vbox.addWidget(ll)
+ else:
+ word = text
+
+ for translation_function_name_i, translation_function_name in enumerate(config.translation_function_names):
+ pairs, word_descr = globals()[translation_function_name](word)
+
+ if not len(pairs):
+ pairs = [['', '[Not found]']]
+ #return
+
+ # ~pairs = [ [ str(i) + ' ' + pair[0], pair[1] ] for i, pair in enumerate(pairs) ]
+
+ if word in config.scroll:
+ if len(pairs[config.scroll[word]:]) > config.number_of_translations:
+ pairs = pairs[config.scroll[word]:]
+ else:
+ pairs = pairs[-config.number_of_translations:]
+ if len(config.translation_function_names) == 1:
+ config.scroll[word] -= 1
+
+ for i1, pair in enumerate(pairs):
+ if i1 == config.number_of_translations:
+ break
+
+ if config.split_long_lines_in_popup_B:
+ pair[0] = split_long_lines(pair[0], max_symbols_per_line = config.split_long_lines_in_popup_symbols_min)
+ pair[1] = split_long_lines(pair[1], max_symbols_per_line = config.split_long_lines_in_popup_symbols_min)
+
+ if pair[0] == '-':
+ pair[0] = ''
+ if pair[1] == '-':
+ pair[1] = ''
+
+ if pair[0] != '':
+ # to emphasize the exact form of the word
+ # to ignore case on input and match it on output
+ chnks = re.split(word, pair[0], flags = re.I)
+ exct_words = re.findall(word, pair[0], flags = re.I)
+
+ hbox = QHBoxLayout()
+ hbox.setContentsMargins(0, 0, 0, 0)
+
+ for i2, chnk in enumerate(chnks):
+ if len(chnk):
+ ll = QLabel(chnk)
+ ll.setObjectName("first_line")
+ hbox.addWidget(ll)
+ if i2 + 1 < len(chnks):
+ ll = QLabel(exct_words[i2])
+ ll.setObjectName("first_line_emphasize_word")
+ hbox.addWidget(ll)
+
+ # filling the rest of the line with empty bg
+ ll = QLabel()
+ ll.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ hbox.addWidget(ll)
+
+ self.popup_vbox.addLayout(hbox)
+
+ if pair[1] != '':
+ ll = QLabel(pair[1])
+ ll.setObjectName("second_line")
+ self.popup_vbox.addWidget(ll)
+
+ # padding
+ ll = QLabel()
+ ll.setStyleSheet("font-size: 6px;")
+ self.popup_vbox.addWidget(ll)
+
+ if len(word_descr[0]):
+ ll = QLabel(word_descr[0])
+ ll.setProperty("morphology", word_descr[1])
+ ll.setAlignment(Qt.AlignRight)
+ self.popup_vbox.addWidget(ll)
+
+ # delimiter between dictionaries
+ if translation_function_name_i + 1 < len(config.translation_function_names):
+ ll = QLabel()
+ ll.setObjectName("delimiter")
+ self.popup_vbox.addWidget(ll)
+
+ self.popup_inner.adjustSize()
+ self.popup.adjustSize()
+
+ w = self.popup.geometry().width()
+ h = self.popup.geometry().height()
+
+ if w > config.screen_width:
+ w = config.screen_width - 20
+
+ if not is_line:
+ if w < config.screen_width / 3:
+ w = config.screen_width / 3
+
+ if x_cursor_pos == -1:
+ x = (config.screen_width/2) - (w/2)
+ else:
+ x = x_cursor_pos - w/5
+ if x+w > config.screen_width:
+ x = config.screen_width - w
+
+ if config.subs_top_placement_B:
+ y = self.subtitles.height + config.subs_screen_edge_padding
+ else:
+ y = config.screen_height - config.subs_screen_edge_padding - self.subtitles.height - h
+
+ self.popup.setGeometry(int(x), int(y), int(w), 0)
+ self.popup.show()
+
+ QApplication.restoreOverrideCursor()
+
+if __name__ == "__main__":
+ print('[py part] Starting jaSubs ...')
+
+ try:
+ os.mkdir('urls')
+ except:
+ pass
+
+ if 'tab_divided_dict' in config.translation_function_names:
+ offdict = { x.split('\t')[0].strip().lower() : x.split('\t')[1].strip() for x in open(os.path.expanduser(config.tab_divided_dict_fname)).readlines() if '\t' in x }
+
+ mpv_socket = sys.argv[1]
+ sub_file = sys.argv[2]
+ # sub_file = '/tmp/mpv_sub_'
+ # mpv_socket = '/tmp/mpv_socket_'
+
+ subs = ''
+
+ app = QApplication(sys.argv)
+
+ config.avoid_resuming = False
+ config.block_popup = False
+ config.scroll = {}
+ config.queue_to_translate = queue.Queue()
+ config.screen_width = app.primaryScreen().size().width()
+ config.screen_height = app.primaryScreen().size().height()
+
+ form = main_class()
+ app.exec_()
diff --git a/jaSubs_config.py b/jaSubs_config.py
new file mode 100644
index 0000000..fc0ba24
--- /dev/null
+++ b/jaSubs_config.py
@@ -0,0 +1,257 @@
+#! /usr/bin/env python
+
+# v. 2.9
+# Interactive subtitles for `mpv` for language learners.
+
+#########################################
+# all *_B variables are boolean
+######
+
+# make sure selected translation function supports your language and so that codes of your languages are correct
+# for instance Pons doesn't support Hebrew, Google and Reverso do, but their codes are different: 'iw' and 'he' respectively
+# translate from language
+lang_from = 'ja'
+# translate to language
+lang_to = 'en'
+
+# dictionaries to use, one or more
+# or other function's name you might write that will return ([[word, translation]..], [morphology = '', gender = '']) # available:
+# jisho
+# tab_divided_dict - simple offline dictionary with word \t translation per line
+translation_function_names = ['jisho']
+# for automatic switch to Hebrew. Skip if it isn't your language.
+translation_function_names_2 = ['google']
+
+# deepl/google
+translation_function_name_full_sentence = 'google'
+
+# number of translations in popup
+number_of_translations = 4
+# number of translations to save in files for each word; 0 - to save all
+number_of_translations_to_save = 50
+
+# gtts|pons|forvo # gtts is google-text-to-speech
+listen_via = 'gtts'
+
+# path to the offline dictionary
+tab_divided_dict_fname = '~/d/python_shit/mpv/scripts/z.dict'
+# strip <.*?>
+tab_divided_dict_remove_tags_B = True
+
+pause_during_translation_B = True
+# don't hide subtitle when its time is up and keep it on screen until the next line
+extend_subs_duration2max_B = True
+# limit extension duration in seconds; N == 0: do not limit
+extend_subs_duration_limit_sec = 33
+# show jaSubs only in fullscreen
+hide_when_not_fullscreen_B = True
+
+# interval between checking for the next subtitle; in seconds
+update_time = .01
+# interval in seconds between checking if mpv is in focus using `xdotool` and/or in fullscreen; in seconds
+focus_checking_time = .1
+
+# firefox "https://en.wiktionary.org/wiki/${word}"
+show_in_browser = '${BROWSER:-brave} https://translate.google.com/translate_a/single?client=t&sl=ja&tl=en&hl=en&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&ie=UTF-8&oe=UTF-8&otf=1&pc=1&ssel=3&tsel=3&kc=2&q={word}'
+
+# for going through lines step by step
+# skip pausing when subs are less then X words
+auto_pause_min_words = 10
+# 0 - don't pause
+# 1 - pause after subs change
+# 2 - pause before subs change
+# wheel click on jaSubs cycles through options
+auto_pause = 0
+
+#########################################################
+#########################################################
+### LOOKS ###
+#########################################################
+#########################################################
+
+# show subtitles at the top of the screen
+subs_top_placement_B = False
+# distance to the edge; in px
+subs_screen_edge_padding = 25
+subs_padding_between_lines = 0
+
+# when subtitle consists of only one overly long line - splitting into two
+split_long_lines_B = True
+# split when there are more than N words in line
+split_long_lines_words_min = 8
+
+# Qt's line wrapping doesn't work well
+split_long_lines_in_popup_B = True
+# split when there are more than N symbols in given line
+split_long_lines_in_popup_symbols_min = 80
+
+# ~ http://doc.qt.io/qt-5/stylesheet-reference.html
+'''
+/* Examples of css: */
+
+background: transparent; /* fully transparent */
+background: black; /* black background*/
+background: #ffffff; /* white background*/
+background: rgba(0, 0, 0, 20%); /* semi-opaque black 20% */
+background: rgba(0, 0, 0, 50%); /* semi-opaque black 50% */
+background: rgba(0, 0, 0, 80%); /* semi-opaque black 80% */
+background: rgba(255, 255, 255, 40%); /* semi-opaque white 40% */
+background: rgba(44, 44, 44, 90%); /* semi-opaque dark-grey 90% */
+
+/* Font colors: */
+color: white;
+color: #BAC4D6;
+color: rgb(217, 49, 49); /* red */
+color: rgba(217, 49, 49, 70%); /* semi-opaque red 70% */
+
+font-family: "Trebuchet MS";
+font-weight: bold;
+font-size: 33px;
+
+font: bold italic large "Times New Roman" 34px;
+
+ font-family: "FiraGO";
+ font-family: "Trebuchet MS";
+ font-family: "American Typewriter";
+'''
+
+# CSS styles for subtitles
+style_subs = '''
+ /* looks of subtitles */
+ QFrame {
+ background: transparent;
+ color: white;
+ color: #FFF0CD;
+
+ font-family: "American Typewriter";
+ /* font-weight: bold; */
+ font-size: 52px;
+ }
+'''
+
+# CSS styles for translations(popup)
+style_popup = '''
+ /* main */
+ QFrame {
+ background: rgb(44, 44, 44);
+
+ font-family: "Trebuchet MS";
+ font-weight: bold;
+ font-size: 40px;
+ }
+ /* original language */
+ QLabel#first_line {
+ color: #DCDCCC;
+ }
+
+ /* original language - underlining exact word */
+ QLabel#first_line_emphasize_word {
+ color: #DCDCCC;
+ text-decoration: underline;
+ }
+
+ /* translation */
+ QLabel#second_line {
+ color: #8B8F88;
+ }
+
+ /* colorizing morphology */
+ [morphology=""] { color: #CA8200; }
+ [morphology="m"] { color: #5EB0FF; }
+ [morphology="f"] { color: #E34840; }
+ [morphology="nt"] { color: #8BC34A; }
+
+ /* delimiter between dictionaries */
+ QFrame#delimiter {
+ background: #8B8F88;
+ font-size: 4px; /* emulating thickness */
+ }
+'''
+
+# for subtitles to be visible on background with similar color
+# might look ugly with some fonts
+outline_B = True
+outline_color = '#000000'
+# ~ N/2.5 == size in px; here it's 2px of outline + another ~ 3px of blur
+outline_thickness = 5
+outline_blur = 7
+# change if outline is cropped from top/bottom of some letters depending on font
+# in px; can take negative values
+outline_top_padding = -2
+outline_bottom_padding = 2
+
+# highlighting the word under cursor
+hover_color = '#F44336'
+hover_hightlight = True # may look ugly due to only int precision of QFontMetrics
+hover_underline = False
+hover_underline_thickness = 5
+
+#########################################################
+#########################################################
+### reassigning mouse buttons ###
+#########################################################
+#########################################################
+
+# functions' names are self-explanatory
+# ['mouse_event', 'modifier_key', 'self_explanatory_function_name'],
+
+# to one button multiple functions can be assigned
+# by left-click this will open word in browser and save it to file.
+# ['LeftButton', 'NoModifier', 'f_show_in_browser'],
+# ['LeftButton', 'NoModifier', 'f_save_word_to_file'],
+
+# https://doc.qt.io/qt-5/qt.html#MouseButton-enum
+# mouse_event:
+ # LeftButton
+ # RightButton
+ # MiddleButton (wheel-click)
+ # BackButton (Typically present on the 'thumb' side of a mouse with extra buttons. This is NOT the tilt wheel.)
+ # ForwardButton
+
+ # wheel scroll up/down left/right names' are arbitrary and not from Qt
+ # ScrollUp
+ # ScrollDown
+ # ScrollLeft (This is the tilt wheel.)
+ # ScrollRight
+
+# Note: On macOS, the ControlModifier value corresponds to the Command keys on the keyboard.
+# https://doc.qt.io/qt-5/qt.html#KeyboardModifier-enum
+# modifier_key:
+ # NoModifier
+ # ControlModifier
+ # ShiftModifier
+ # AltModifier
+
+# self_explanatory_function_name:
+ # f_show_in_browser
+ # f_auto_pause_options
+ # f_listen
+ # f_scroll_translations_down
+ # f_scroll_translations_up
+ # f_subs_screen_edge_padding_decrease
+ # f_subs_screen_edge_padding_increase
+ # f_font_size_decrease
+ # f_font_size_increase
+ # f_auto_pause_min_words_decrease
+ # f_auto_pause_min_words_increase
+ # f_translation_full_sentence
+ # f_save_word_to_file
+ # f_deepl_translation < obsolete, changed into f_translation_full_sentence
+
+mouse_buttons = [
+ ['LeftButton', 'NoModifier', 'f_show_in_browser'],
+ ['RightButton', 'NoModifier', 'f_translation_full_sentence'],
+ ['MiddleButton', 'NoModifier', 'f_auto_pause_options'],
+
+ ['ScrollDown', 'ControlModifier', 'f_font_size_decrease'],
+ ['ScrollUp', 'ControlModifier', 'f_font_size_increase'],
+
+ # ['ScrollLeft', 'NoModifier', 'f_auto_pause_min_words_decrease'],
+ # ['ScrollRight', 'NoModifier', 'f_auto_pause_min_words_increase'],
+
+ ['ScrollDown', 'ShiftModifier', 'f_subs_screen_edge_padding_decrease'],
+ ['ScrollUp', 'ShiftModifier', 'f_subs_screen_edge_padding_increase'],
+]
+
+# obsolete vars
+hover_underline_width = hover_underline_thickness