More changes
This commit is contained in:
@@ -20,7 +20,7 @@ Additionally:
|
|||||||
- You can switch between OCR providers pressing their corresponding keyboard key inside the terminal window (refer to the list of keys in the providers list below)
|
- You can switch between OCR providers pressing their corresponding keyboard key inside the terminal window (refer to the list of keys in the providers list below)
|
||||||
- You can start the script paused with the `-p` option or with a specific provider with the `-e` option (refer to `owocr -h` for the list)
|
- You can start the script paused with the `-p` option or with a specific provider with the `-e` option (refer to `owocr -h` for the list)
|
||||||
- You can specify keyboard combos in the config file to pause/unpause and switch the OCR provider from anywhere (refer to the config file or `owocr -h`)
|
- You can specify keyboard combos in the config file to pause/unpause and switch the OCR provider from anywhere (refer to the config file or `owocr -h`)
|
||||||
- You can auto pause the script after a successful text recognition with the `-a=seconds` option if you're not using screen capture. 0 (the default) disables it.
|
- You can auto pause the script after a successful text recognition with the `-a=seconds` option. 0 (the default) disables it.
|
||||||
- You can enable notifications in the config file or with `-n` to show the text with a native OS notification if you're not using screen capture with automatic screenshots. **Important for macOS users:** if you use Python from brew, you need to enter this command in your terminal before the first notification: `codesign -f -s - $(brew --cellar python)/3.*/Frameworks/Python.framework` (works on Ventura/Sonoma). Older macOS versions might require Python to be installed from the [official website](https://www.python.org/downloads/). Nothing can be done about this unfortunately.
|
- You can enable notifications in the config file or with `-n` to show the text with a native OS notification if you're not using screen capture with automatic screenshots. **Important for macOS users:** if you use Python from brew, you need to enter this command in your terminal before the first notification: `codesign -f -s - $(brew --cellar python)/3.*/Frameworks/Python.framework` (works on Ventura/Sonoma). Older macOS versions might require Python to be installed from the [official website](https://www.python.org/downloads/). Nothing can be done about this unfortunately.
|
||||||
- Optionally, you can speed up the online providers by installing fpng-py: `pip install owocr[faster-png]` (requires setting up a developer environment on most operating systems/Python versions)
|
- Optionally, you can speed up the online providers by installing fpng-py: `pip install owocr[faster-png]` (requires setting up a developer environment on most operating systems/Python versions)
|
||||||
- A config file (which will be automatically created in `user directory/.config/owocr_config.ini`, on Windows `user directory` is the `C:\Users\yourusername` folder) can be used to configure the script, as an example to limit providers (to reduce clutter/memory usage) as well as specifying provider settings such as api keys etc. A sample config file is also provided [here](https://raw.githubusercontent.com/AuroraWright/owocr/master/owocr_config.ini)
|
- A config file (which will be automatically created in `user directory/.config/owocr_config.ini`, on Windows `user directory` is the `C:\Users\yourusername` folder) can be used to configure the script, as an example to limit providers (to reduce clutter/memory usage) as well as specifying provider settings such as api keys etc. A sample config file is also provided [here](https://raw.githubusercontent.com/AuroraWright/owocr/master/owocr_config.ini)
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ parser.add_argument('-d', '--delete_images', type=str2bool, nargs='?', const=Tru
|
|||||||
parser.add_argument('-n', '--notifications', type=str2bool, nargs='?', const=True, default=argparse.SUPPRESS,
|
parser.add_argument('-n', '--notifications', type=str2bool, nargs='?', const=True, default=argparse.SUPPRESS,
|
||||||
help='Show an operating system notification with the detected text. Will be ignored when reading with screen capture and periodic screenshots.')
|
help='Show an operating system notification with the detected text. Will be ignored when reading with screen capture and periodic screenshots.')
|
||||||
parser.add_argument('-a', '--auto_pause', type=float, default=argparse.SUPPRESS,
|
parser.add_argument('-a', '--auto_pause', type=float, default=argparse.SUPPRESS,
|
||||||
help='Automatically pause the program after the specified amount of seconds since the last successful text recognition. Will be ignored when reading with screen capture. 0 to disable.')
|
help='Automatically pause the program after the specified amount of seconds since the last successful text recognition. 0 to disable.')
|
||||||
parser.add_argument('-cp', '--combo_pause', type=str, default=argparse.SUPPRESS,
|
parser.add_argument('-cp', '--combo_pause', type=str, default=argparse.SUPPRESS,
|
||||||
help='Combo to wait on for pausing the program. As an example: "<ctrl>+<shift>+p". The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key')
|
help='Combo to wait on for pausing the program. As an example: "<ctrl>+<shift>+p". The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key')
|
||||||
parser.add_argument('-cs', '--combo_engine_switch', type=str, default=argparse.SUPPRESS,
|
parser.add_argument('-cs', '--combo_engine_switch', type=str, default=argparse.SUPPRESS,
|
||||||
help='Combo to wait on for switching the OCR engine. As an example: "<ctrl>+<shift>+a". To be used with combo_pause. The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key')
|
help='Combo to wait on for switching the OCR engine. As an example: "<ctrl>+<shift>+a". The list of keys can be found here: https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key')
|
||||||
parser.add_argument('-sa', '--screen_capture_area', type=str, default=argparse.SUPPRESS,
|
parser.add_argument('-sa', '--screen_capture_area', type=str, default=argparse.SUPPRESS,
|
||||||
help='Area to target when reading with screen capture. Can be either empty (automatic selector), a set of coordinates (x,y,width,height), "screen_N" (captures a whole screen, where N is the screen number starting from 1) or a window name (the first matching window title will be used).')
|
help='Area to target when reading with screen capture. Can be either empty (automatic selector), a set of coordinates (x,y,width,height), "screen_N" (captures a whole screen, where N is the screen number starting from 1) or a window name (the first matching window title will be used).')
|
||||||
parser.add_argument('-swa', '--screen_capture_window_area', type=str, default=argparse.SUPPRESS,
|
parser.add_argument('-swa', '--screen_capture_window_area', type=str, default=argparse.SUPPRESS,
|
||||||
|
|||||||
219
owocr/run.py
219
owocr/run.py
@@ -27,7 +27,7 @@ from desktop_notifier import DesktopNotifierSync, Urgency
|
|||||||
|
|
||||||
from .ocr import *
|
from .ocr import *
|
||||||
from .config import config
|
from .config import config
|
||||||
from .screen_coordinate_picker import get_screen_selection
|
from .screen_coordinate_picker import get_screen_selection, terminate_selector_if_running
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import win32gui
|
import win32gui
|
||||||
@@ -100,7 +100,7 @@ class ClipboardThread(threading.Thread):
|
|||||||
def process_message(self, hwnd: int, msg: int, wparam: int, lparam: int):
|
def process_message(self, hwnd: int, msg: int, wparam: int, lparam: int):
|
||||||
WM_CLIPBOARDUPDATE = 0x031D
|
WM_CLIPBOARDUPDATE = 0x031D
|
||||||
timestamp = time.time()
|
timestamp = time.time()
|
||||||
if msg == WM_CLIPBOARDUPDATE and timestamp - self.last_update > 1 and not paused:
|
if msg == WM_CLIPBOARDUPDATE and timestamp - self.last_update > 1 and not paused.is_set():
|
||||||
self.last_update = timestamp
|
self.last_update = timestamp
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -144,8 +144,8 @@ class ClipboardThread(threading.Thread):
|
|||||||
process_clipboard = False
|
process_clipboard = False
|
||||||
img = None
|
img = None
|
||||||
|
|
||||||
while not terminated:
|
while not terminated.is_set():
|
||||||
if paused:
|
if paused.is_set():
|
||||||
sleep_time = 0.5
|
sleep_time = 0.5
|
||||||
process_clipboard = False
|
process_clipboard = False
|
||||||
else:
|
else:
|
||||||
@@ -173,7 +173,7 @@ class ClipboardThread(threading.Thread):
|
|||||||
|
|
||||||
process_clipboard = True
|
process_clipboard = True
|
||||||
|
|
||||||
if not terminated:
|
if not terminated.is_set():
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
|
||||||
@@ -194,8 +194,8 @@ class DirectoryWatcher(threading.Thread):
|
|||||||
if path.suffix.lower() in self.allowed_extensions:
|
if path.suffix.lower() in self.allowed_extensions:
|
||||||
old_paths.add(self.get_path_key(path))
|
old_paths.add(self.get_path_key(path))
|
||||||
|
|
||||||
while not terminated:
|
while not terminated.is_set():
|
||||||
if paused:
|
if paused.is_set():
|
||||||
sleep_time = 0.5
|
sleep_time = 0.5
|
||||||
else:
|
else:
|
||||||
sleep_time = self.delay_secs
|
sleep_time = self.delay_secs
|
||||||
@@ -205,10 +205,10 @@ class DirectoryWatcher(threading.Thread):
|
|||||||
if path_key not in old_paths:
|
if path_key not in old_paths:
|
||||||
old_paths.add(path_key)
|
old_paths.add(path_key)
|
||||||
|
|
||||||
if not paused:
|
if not paused.is_set():
|
||||||
image_queue.put((path, False))
|
image_queue.put((path, False))
|
||||||
|
|
||||||
if not terminated:
|
if not terminated.is_set():
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ class WebsocketServerThread(threading.Thread):
|
|||||||
self.clients.add(websocket)
|
self.clients.add(websocket)
|
||||||
try:
|
try:
|
||||||
async for message in websocket:
|
async for message in websocket:
|
||||||
if self.read and not paused:
|
if self.read and not paused.is_set():
|
||||||
image_queue.put((message, False))
|
image_queue.put((message, False))
|
||||||
try:
|
try:
|
||||||
await websocket.send('True')
|
await websocket.send('True')
|
||||||
@@ -282,7 +282,7 @@ class RequestHandler(socketserver.BaseRequestHandler):
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not paused:
|
if not paused.is_set():
|
||||||
image_queue.put((img, False))
|
image_queue.put((img, False))
|
||||||
conn.sendall(b'True')
|
conn.sendall(b'True')
|
||||||
else:
|
else:
|
||||||
@@ -789,7 +789,8 @@ class ScreenshotThread(threading.Thread):
|
|||||||
elif screen_capture_area.startswith('screen_'):
|
elif screen_capture_area.startswith('screen_'):
|
||||||
parts = screen_capture_area.split('_')
|
parts = screen_capture_area.split('_')
|
||||||
if len(parts) != 2 or not parts[1].isdigit():
|
if len(parts) != 2 or not parts[1].isdigit():
|
||||||
raise ValueError('Invalid screen_capture_area')
|
logger.error('Invalid screen_capture_area')
|
||||||
|
sys.exit(1)
|
||||||
screen_capture_monitor = int(parts[1])
|
screen_capture_monitor = int(parts[1])
|
||||||
self.screencapture_mode = 1
|
self.screencapture_mode = 1
|
||||||
elif len(screen_capture_area.split(',')) == 4:
|
elif len(screen_capture_area.split(',')) == 4:
|
||||||
@@ -806,7 +807,8 @@ class ScreenshotThread(threading.Thread):
|
|||||||
if self.screencapture_mode == 1:
|
if self.screencapture_mode == 1:
|
||||||
mon = self.sct.monitors
|
mon = self.sct.monitors
|
||||||
if len(mon) <= screen_capture_monitor:
|
if len(mon) <= screen_capture_monitor:
|
||||||
raise ValueError('Invalid monitor number in screen_capture_area')
|
logger.error('Invalid monitor number in screen_capture_area')
|
||||||
|
sys.exit(1)
|
||||||
coord_left = mon[screen_capture_monitor]['left']
|
coord_left = mon[screen_capture_monitor]['left']
|
||||||
coord_top = mon[screen_capture_monitor]['top']
|
coord_top = mon[screen_capture_monitor]['top']
|
||||||
coord_width = mon[screen_capture_monitor]['width']
|
coord_width = mon[screen_capture_monitor]['width']
|
||||||
@@ -850,7 +852,8 @@ class ScreenshotThread(threading.Thread):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not window_index:
|
if not window_index:
|
||||||
raise ValueError(area_invalid_error)
|
logger.error(area_invalid_error)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
self.window_id = window_ids[window_index]
|
self.window_id = window_ids[window_index]
|
||||||
window_title = window_titles[window_index]
|
window_title = window_titles[window_index]
|
||||||
@@ -863,7 +866,8 @@ class ScreenshotThread(threading.Thread):
|
|||||||
self.window_handle, window_title = self.get_windows_window_handle(screen_capture_area)
|
self.window_handle, window_title = self.get_windows_window_handle(screen_capture_area)
|
||||||
|
|
||||||
if not self.window_handle:
|
if not self.window_handle:
|
||||||
raise ValueError(area_invalid_error)
|
logger.error(area_invalid_error)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
||||||
|
|
||||||
@@ -871,7 +875,8 @@ class ScreenshotThread(threading.Thread):
|
|||||||
self.windows_window_tracker_instance.start()
|
self.windows_window_tracker_instance.start()
|
||||||
logger.info(f'Selected window: {window_title}')
|
logger.info(f'Selected window: {window_title}')
|
||||||
else:
|
else:
|
||||||
raise ValueError('Window capture is only currently supported on Windows and macOS')
|
logger.error('Window capture is only currently supported on Windows and macOS')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
screen_capture_window_area = config.get_general('screen_capture_window_area')
|
screen_capture_window_area = config.get_general('screen_capture_window_area')
|
||||||
if screen_capture_window_area != 'window':
|
if screen_capture_window_area != 'window':
|
||||||
@@ -882,7 +887,8 @@ class ScreenshotThread(threading.Thread):
|
|||||||
elif screen_capture_window_area == '':
|
elif screen_capture_window_area == '':
|
||||||
self.launch_coordinate_picker(False, False)
|
self.launch_coordinate_picker(False, False)
|
||||||
else:
|
else:
|
||||||
raise ValueError('"screen_capture_window_area" must be empty, "window" for the whole window, or a valid set of coordinates')
|
logger.error('"screen_capture_window_area" must be empty, "window" for the whole window, or a valid set of coordinates')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def get_windows_window_handle(self, window_title):
|
def get_windows_window_handle(self, window_title):
|
||||||
def callback(hwnd, window_title_part):
|
def callback(hwnd, window_title_part):
|
||||||
@@ -906,7 +912,7 @@ class ScreenshotThread(threading.Thread):
|
|||||||
|
|
||||||
def windows_window_tracker(self):
|
def windows_window_tracker(self):
|
||||||
found = True
|
found = True
|
||||||
while not terminated:
|
while not terminated.is_set():
|
||||||
found = win32gui.IsWindow(self.window_handle)
|
found = win32gui.IsWindow(self.window_handle)
|
||||||
if not found:
|
if not found:
|
||||||
break
|
break
|
||||||
@@ -914,7 +920,7 @@ class ScreenshotThread(threading.Thread):
|
|||||||
self.screencapture_window_active = self.window_handle == win32gui.GetForegroundWindow()
|
self.screencapture_window_active = self.window_handle == win32gui.GetForegroundWindow()
|
||||||
else:
|
else:
|
||||||
self.screencapture_window_visible = not win32gui.IsIconic(self.window_handle)
|
self.screencapture_window_visible = not win32gui.IsIconic(self.window_handle)
|
||||||
time.sleep(0.2)
|
time.sleep(0.5)
|
||||||
if not found:
|
if not found:
|
||||||
on_window_closed(False)
|
on_window_closed(False)
|
||||||
|
|
||||||
@@ -965,7 +971,7 @@ class ScreenshotThread(threading.Thread):
|
|||||||
|
|
||||||
def macos_window_tracker(self):
|
def macos_window_tracker(self):
|
||||||
found = True
|
found = True
|
||||||
while found and not terminated:
|
while found and not terminated.is_set():
|
||||||
found = False
|
found = False
|
||||||
is_active = False
|
is_active = False
|
||||||
with objc.autorelease_pool():
|
with objc.autorelease_pool():
|
||||||
@@ -985,7 +991,7 @@ class ScreenshotThread(threading.Thread):
|
|||||||
found = True
|
found = True
|
||||||
if found:
|
if found:
|
||||||
self.screencapture_window_active = is_active
|
self.screencapture_window_active = is_active
|
||||||
time.sleep(0.2)
|
time.sleep(0.5)
|
||||||
if not found:
|
if not found:
|
||||||
on_window_closed(False)
|
on_window_closed(False)
|
||||||
|
|
||||||
@@ -1074,7 +1080,8 @@ class ScreenshotThread(threading.Thread):
|
|||||||
screen_selection = get_screen_selection(None, self.coordinate_selector_combo_enabled)
|
screen_selection = get_screen_selection(None, self.coordinate_selector_combo_enabled)
|
||||||
if not screen_selection:
|
if not screen_selection:
|
||||||
if on_init:
|
if on_init:
|
||||||
raise ValueError('Picker window was closed or an error occurred')
|
logger.error('Picker window was closed or an error occurred')
|
||||||
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
logger.warning('Picker window was closed or an error occurred, leaving settings unchanged')
|
logger.warning('Picker window was closed or an error occurred, leaving settings unchanged')
|
||||||
return
|
return
|
||||||
@@ -1111,8 +1118,8 @@ class ScreenshotThread(threading.Thread):
|
|||||||
def run(self):
|
def run(self):
|
||||||
if self.screencapture_mode != 2:
|
if self.screencapture_mode != 2:
|
||||||
self.sct = mss.mss()
|
self.sct = mss.mss()
|
||||||
while not terminated:
|
while not terminated.is_set():
|
||||||
if not screenshot_event.wait(timeout=0.1):
|
if not screenshot_event.wait(timeout=0.5):
|
||||||
if coordinate_selector_event.is_set():
|
if coordinate_selector_event.is_set():
|
||||||
self.launch_coordinate_picker(False, False)
|
self.launch_coordinate_picker(False, False)
|
||||||
coordinate_selector_event.clear()
|
coordinate_selector_event.clear()
|
||||||
@@ -1133,33 +1140,49 @@ class ScreenshotThread(threading.Thread):
|
|||||||
|
|
||||||
|
|
||||||
class AutopauseTimer:
|
class AutopauseTimer:
|
||||||
def __init__(self, timeout):
|
def __init__(self):
|
||||||
self.timeout = timeout
|
self.timeout = config.get_general('auto_pause')
|
||||||
self.timer_thread = None
|
|
||||||
self.running = False
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
self.stop()
|
|
||||||
self.running = True
|
|
||||||
self.timer_thread = threading.Thread(target=self._countdown)
|
self.timer_thread = threading.Thread(target=self._countdown)
|
||||||
|
self.running = True
|
||||||
|
self.countdown_active = threading.Event()
|
||||||
|
self.allow_auto_pause = threading.Event()
|
||||||
|
self.seconds_remaining = 0
|
||||||
|
self.lock = threading.Lock()
|
||||||
self.timer_thread.start()
|
self.timer_thread.start()
|
||||||
|
|
||||||
|
def start_timer(self):
|
||||||
|
with self.lock:
|
||||||
|
self.seconds_remaining = self.timeout
|
||||||
|
self.allow_auto_pause.set()
|
||||||
|
self.countdown_active.set()
|
||||||
|
|
||||||
|
def stop_timer(self):
|
||||||
|
self.countdown_active.clear()
|
||||||
|
self.allow_auto_pause.set()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self.running and self.timer_thread and self.timer_thread.is_alive():
|
|
||||||
self.running = False
|
self.running = False
|
||||||
|
self.allow_auto_pause.set()
|
||||||
|
self.countdown_active.set()
|
||||||
|
if self.timer_thread.is_alive():
|
||||||
self.timer_thread.join()
|
self.timer_thread.join()
|
||||||
|
|
||||||
def _countdown(self):
|
def _countdown(self):
|
||||||
seconds = self.timeout
|
while self.running:
|
||||||
while seconds > 0 and self.running and not terminated:
|
self.countdown_active.wait()
|
||||||
|
if not self.running:
|
||||||
|
break
|
||||||
|
|
||||||
|
while self.running and self.countdown_active.is_set() and self.seconds_remaining > 0:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
seconds -= 1
|
with self.lock:
|
||||||
if self.running:
|
self.seconds_remaining -= 1
|
||||||
self.running = False
|
|
||||||
if not (paused or terminated):
|
self.allow_auto_pause.wait()
|
||||||
|
|
||||||
|
if self.running and self.countdown_active.is_set() and self.seconds_remaining == 0:
|
||||||
|
self.countdown_active.clear()
|
||||||
|
if not (paused.is_set() or terminated.is_set()):
|
||||||
pause_handler(True)
|
pause_handler(True)
|
||||||
|
|
||||||
|
|
||||||
@@ -1170,9 +1193,6 @@ class SecondPassThread:
|
|||||||
self.ocr_thread = None
|
self.ocr_thread = None
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self.ocr_thread is None or not self.ocr_thread.is_alive():
|
if self.ocr_thread is None or not self.ocr_thread.is_alive():
|
||||||
self.running = True
|
self.running = True
|
||||||
@@ -1189,9 +1209,9 @@ class SecondPassThread:
|
|||||||
self.output_queue.get()
|
self.output_queue.get()
|
||||||
|
|
||||||
def _process_ocr(self):
|
def _process_ocr(self):
|
||||||
while self.running and not terminated:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
img, engine_instance, recovered_lines_count = self.input_queue.get(timeout=0.1)
|
img, engine_instance, recovered_lines_count = self.input_queue.get(timeout=0.5)
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
res, result_data = engine_instance(img)
|
res, result_data = engine_instance(img)
|
||||||
@@ -1237,10 +1257,8 @@ class OutputResult:
|
|||||||
lines.append(self.filtering._get_line_text(l))
|
lines.append(self.filtering._get_line_text(l))
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def __call__(self, img_or_path, filter_text, notify):
|
def __call__(self, img_or_path, filter_text, auto_pause, notify):
|
||||||
if auto_pause_handler and not filter_text:
|
engine_index_local = engine_index
|
||||||
auto_pause_handler.stop()
|
|
||||||
|
|
||||||
output_format = config.get_general('output_format')
|
output_format = config.get_general('output_format')
|
||||||
engine_color = config.get_general('engine_color')
|
engine_color = config.get_general('engine_color')
|
||||||
engine_instance = engine_instances[engine_index]
|
engine_instance = engine_instances[engine_index]
|
||||||
@@ -1248,7 +1266,7 @@ class OutputResult:
|
|||||||
result_data = None
|
result_data = None
|
||||||
|
|
||||||
if filter_text and self.screen_capture_periodic:
|
if filter_text and self.screen_capture_periodic:
|
||||||
if engine_index_2 != -1 and engine_index_2 != engine_index and engine_instance.threading_support:
|
if engine_index_2 != -1 and engine_index_2 != engine_index_local and engine_instance.threading_support:
|
||||||
two_pass_processing_active = True
|
two_pass_processing_active = True
|
||||||
engine_instance_2 = engine_instances[engine_index_2]
|
engine_instance_2 = engine_instances[engine_index_2]
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
@@ -1278,6 +1296,9 @@ class OutputResult:
|
|||||||
else:
|
else:
|
||||||
self.second_pass_thread.stop()
|
self.second_pass_thread.stop()
|
||||||
|
|
||||||
|
if auto_pause_handler and auto_pause:
|
||||||
|
auto_pause_handler.allow_auto_pause.clear()
|
||||||
|
|
||||||
if not result_data:
|
if not result_data:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
res, result_data = engine_instance(img_or_path)
|
res, result_data = engine_instance(img_or_path)
|
||||||
@@ -1287,6 +1308,8 @@ class OutputResult:
|
|||||||
recovered_lines_count = 0
|
recovered_lines_count = 0
|
||||||
|
|
||||||
if not res:
|
if not res:
|
||||||
|
if auto_pause_handler and auto_pause:
|
||||||
|
auto_pause_handler.stop_timer()
|
||||||
logger.opt(colors=True).warning(f'<{engine_color}>{engine_name}</{engine_color}> reported an error after {processing_time:0.03f}s: {result_data}')
|
logger.opt(colors=True).warning(f'<{engine_color}>{engine_name}</{engine_color}> reported an error after {processing_time:0.03f}s: {result_data}')
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1310,7 +1333,9 @@ class OutputResult:
|
|||||||
if result_data_text != None:
|
if result_data_text != None:
|
||||||
if filter_text:
|
if filter_text:
|
||||||
text_to_process = self.filtering._find_changed_lines_text(result_data_text, result_data, two_pass_processing_active, recovered_lines_count)
|
text_to_process = self.filtering._find_changed_lines_text(result_data_text, result_data, two_pass_processing_active, recovered_lines_count)
|
||||||
if self.screen_capture_periodic and len(text_to_process) == 0:
|
if self.screen_capture_periodic and not text_to_process:
|
||||||
|
if auto_pause_handler and auto_pause:
|
||||||
|
auto_pause_handler.allow_auto_pause.set()
|
||||||
return
|
return
|
||||||
output_string = self._post_process(text_to_process, True)
|
output_string = self._post_process(text_to_process, True)
|
||||||
else:
|
else:
|
||||||
@@ -1341,8 +1366,11 @@ class OutputResult:
|
|||||||
with Path(write_to).open('a', encoding='utf-8') as f:
|
with Path(write_to).open('a', encoding='utf-8') as f:
|
||||||
f.write(output_string + '\n')
|
f.write(output_string + '\n')
|
||||||
|
|
||||||
if auto_pause_handler and not paused and not filter_text:
|
if auto_pause_handler and auto_pause:
|
||||||
auto_pause_handler.start()
|
if not paused.is_set():
|
||||||
|
auto_pause_handler.start_timer()
|
||||||
|
else:
|
||||||
|
auto_pause_handler.stop_timer()
|
||||||
|
|
||||||
|
|
||||||
def get_notification_urgency():
|
def get_notification_urgency():
|
||||||
@@ -1353,14 +1381,14 @@ def get_notification_urgency():
|
|||||||
|
|
||||||
def pause_handler(is_combo=True):
|
def pause_handler(is_combo=True):
|
||||||
global paused
|
global paused
|
||||||
message = 'Unpaused!' if paused else 'Paused!'
|
message = 'Unpaused!' if paused.is_set() else 'Paused!'
|
||||||
|
|
||||||
if auto_pause_handler:
|
if auto_pause_handler:
|
||||||
auto_pause_handler.stop()
|
auto_pause_handler.stop_timer()
|
||||||
if is_combo:
|
if is_combo:
|
||||||
notifier.send(title='owocr', message=message, urgency=get_notification_urgency())
|
notifier.send(title='owocr', message=message, urgency=get_notification_urgency())
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
paused = not paused
|
paused.clear() if paused.is_set() else paused.set()
|
||||||
|
|
||||||
|
|
||||||
def engine_change_handler(user_input='s', is_combo=True):
|
def engine_change_handler(user_input='s', is_combo=True):
|
||||||
@@ -1386,11 +1414,11 @@ def user_input_thread_run():
|
|||||||
def _terminate_handler():
|
def _terminate_handler():
|
||||||
global terminated
|
global terminated
|
||||||
logger.info('Terminated!')
|
logger.info('Terminated!')
|
||||||
terminated = True
|
terminated.set()
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
import msvcrt
|
import msvcrt
|
||||||
while not terminated:
|
while not terminated.is_set():
|
||||||
if coordinate_selector_event.is_set():
|
if coordinate_selector_event.is_set():
|
||||||
while coordinate_selector_event.is_set():
|
while coordinate_selector_event.is_set():
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@@ -1407,19 +1435,19 @@ def user_input_thread_run():
|
|||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
time.sleep(0.1)
|
time.sleep(0.2)
|
||||||
else:
|
else:
|
||||||
import tty, termios, select
|
import tty, termios, select
|
||||||
fd = sys.stdin.fileno()
|
fd = sys.stdin.fileno()
|
||||||
old_settings = termios.tcgetattr(fd)
|
old_settings = termios.tcgetattr(fd)
|
||||||
try:
|
try:
|
||||||
tty.setcbreak(fd)
|
tty.setcbreak(fd)
|
||||||
while not terminated:
|
while not terminated.is_set():
|
||||||
if coordinate_selector_event.is_set():
|
if coordinate_selector_event.is_set():
|
||||||
while coordinate_selector_event.is_set():
|
while coordinate_selector_event.is_set():
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
tty.setcbreak(fd)
|
tty.setcbreak(fd)
|
||||||
rlist, _, _ = select.select([sys.stdin], [], [], 0.1)
|
rlist, _, _ = select.select([sys.stdin], [], [], 0.2)
|
||||||
if rlist:
|
if rlist:
|
||||||
user_input = sys.stdin.read(1)
|
user_input = sys.stdin.read(1)
|
||||||
if user_input.lower() in 'tq':
|
if user_input.lower() in 'tq':
|
||||||
@@ -1435,14 +1463,14 @@ def user_input_thread_run():
|
|||||||
def signal_handler(sig, frame):
|
def signal_handler(sig, frame):
|
||||||
global terminated
|
global terminated
|
||||||
logger.info('Terminated!')
|
logger.info('Terminated!')
|
||||||
terminated = True
|
terminated.set()
|
||||||
|
|
||||||
|
|
||||||
def on_window_closed(alive):
|
def on_window_closed(alive):
|
||||||
global terminated
|
global terminated
|
||||||
if not (alive or terminated):
|
if not (alive or terminated):
|
||||||
logger.info('Window closed or error occurred, terminated!')
|
logger.info('Window closed or error occurred, terminated!')
|
||||||
terminated = True
|
terminated.set()
|
||||||
|
|
||||||
|
|
||||||
def on_screenshot_combo():
|
def on_screenshot_combo():
|
||||||
@@ -1518,8 +1546,10 @@ def run():
|
|||||||
read_from_path = None
|
read_from_path = None
|
||||||
read_from_readable = []
|
read_from_readable = []
|
||||||
write_to = config.get_general('write_to')
|
write_to = config.get_general('write_to')
|
||||||
terminated = False
|
terminated = threading.Event()
|
||||||
paused = config.get_general('pause_at_startup')
|
paused = threading.Event()
|
||||||
|
if config.get_general('pause_at_startup'):
|
||||||
|
paused.set()
|
||||||
auto_pause = config.get_general('auto_pause')
|
auto_pause = config.get_general('auto_pause')
|
||||||
output_format = config.get_general('output_format')
|
output_format = config.get_general('output_format')
|
||||||
clipboard_thread = None
|
clipboard_thread = None
|
||||||
@@ -1544,10 +1574,7 @@ def run():
|
|||||||
if combo_pause != '':
|
if combo_pause != '':
|
||||||
key_combos[combo_pause] = pause_handler
|
key_combos[combo_pause] = pause_handler
|
||||||
if combo_engine_switch != '':
|
if combo_engine_switch != '':
|
||||||
if combo_pause != '':
|
|
||||||
key_combos[combo_engine_switch] = engine_change_handler
|
key_combos[combo_engine_switch] = engine_change_handler
|
||||||
else:
|
|
||||||
raise ValueError('combo_pause must also be specified')
|
|
||||||
|
|
||||||
if 'websocket' in (read_from, read_from_secondary) or write_to == 'websocket':
|
if 'websocket' in (read_from, read_from_secondary) or write_to == 'websocket':
|
||||||
websocket_server_thread = WebsocketServerThread('websocket' in (read_from, read_from_secondary))
|
websocket_server_thread = WebsocketServerThread('websocket' in (read_from, read_from_secondary))
|
||||||
@@ -1568,7 +1595,8 @@ def run():
|
|||||||
periodic_screenshot_queue = queue.Queue()
|
periodic_screenshot_queue = queue.Queue()
|
||||||
screen_capture_periodic = True
|
screen_capture_periodic = True
|
||||||
if not (screen_capture_on_combo or screen_capture_periodic):
|
if not (screen_capture_on_combo or screen_capture_periodic):
|
||||||
raise ValueError('screen_capture_delay_secs or screen_capture_combo need to be valid values')
|
logger.error('screen_capture_delay_secs or screen_capture_combo need to be valid values')
|
||||||
|
sys.exit(1)
|
||||||
screenshot_event = threading.Event()
|
screenshot_event = threading.Event()
|
||||||
screenshot_thread = ScreenshotThread()
|
screenshot_thread = ScreenshotThread()
|
||||||
screenshot_thread.start()
|
screenshot_thread.start()
|
||||||
@@ -1577,7 +1605,8 @@ def run():
|
|||||||
read_from_readable.append('websocket')
|
read_from_readable.append('websocket')
|
||||||
if 'unixsocket' in (read_from, read_from_secondary):
|
if 'unixsocket' in (read_from, read_from_secondary):
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
raise ValueError('"unixsocket" is not currently supported on Windows')
|
logger.error('"unixsocket" is not currently supported on Windows')
|
||||||
|
sys.exit(1)
|
||||||
socket_path = Path('/tmp/owocr.sock')
|
socket_path = Path('/tmp/owocr.sock')
|
||||||
if socket_path.exists():
|
if socket_path.exists():
|
||||||
socket_path.unlink()
|
socket_path.unlink()
|
||||||
@@ -1591,11 +1620,13 @@ def run():
|
|||||||
read_from_readable.append('clipboard')
|
read_from_readable.append('clipboard')
|
||||||
if any(i and i not in non_path_inputs for i in (read_from, read_from_secondary)):
|
if any(i and i not in non_path_inputs for i in (read_from, read_from_secondary)):
|
||||||
if all(i and i not in non_path_inputs for i in (read_from, read_from_secondary)):
|
if all(i and i not in non_path_inputs for i in (read_from, read_from_secondary)):
|
||||||
raise ValueError("read_from and read_from_secondary can't both be directory paths")
|
logger.error("read_from and read_from_secondary can't both be directory paths")
|
||||||
|
sys.exit(1)
|
||||||
delete_images = config.get_general('delete_images')
|
delete_images = config.get_general('delete_images')
|
||||||
read_from_path = Path(read_from) if read_from not in non_path_inputs else Path(read_from_secondary)
|
read_from_path = Path(read_from) if read_from not in non_path_inputs else Path(read_from_secondary)
|
||||||
if not read_from_path.is_dir():
|
if not read_from_path.is_dir():
|
||||||
raise ValueError('read_from and read_from_secondary must be either "websocket", "unixsocket", "clipboard", "screencapture", or a path to a directory')
|
logger.error('read_from and read_from_secondary must be either "websocket", "unixsocket", "clipboard", "screencapture", or a path to a directory')
|
||||||
|
sys.exit(1)
|
||||||
directory_watcher_thread = DirectoryWatcher(read_from_path)
|
directory_watcher_thread = DirectoryWatcher(read_from_path)
|
||||||
directory_watcher_thread.start()
|
directory_watcher_thread.start()
|
||||||
read_from_readable.append(f'directory {read_from_path}')
|
read_from_readable.append(f'directory {read_from_path}')
|
||||||
@@ -1610,64 +1641,71 @@ def run():
|
|||||||
write_to_readable = write_to
|
write_to_readable = write_to
|
||||||
else:
|
else:
|
||||||
if Path(write_to).suffix.lower() != '.txt':
|
if Path(write_to).suffix.lower() != '.txt':
|
||||||
raise ValueError('write_to must be either "websocket", "clipboard" or a path to a text file')
|
logger.error('write_to must be either "websocket", "clipboard" or a path to a text file')
|
||||||
|
sys.exit(1)
|
||||||
write_to_readable = f'file {write_to}'
|
write_to_readable = f'file {write_to}'
|
||||||
|
|
||||||
process_queue = (any(i in ('clipboard', 'websocket', 'unixsocket') for i in (read_from, read_from_secondary)) or read_from_path or screen_capture_on_combo)
|
process_queue = (any(i in ('clipboard', 'websocket', 'unixsocket') for i in (read_from, read_from_secondary)) or read_from_path or screen_capture_on_combo)
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
if (not screen_capture_periodic) and auto_pause != 0:
|
if auto_pause != 0:
|
||||||
auto_pause_handler = AutopauseTimer(auto_pause)
|
auto_pause_handler = AutopauseTimer()
|
||||||
user_input_thread = threading.Thread(target=user_input_thread_run, daemon=True)
|
user_input_thread = threading.Thread(target=user_input_thread_run, daemon=True)
|
||||||
user_input_thread.start()
|
user_input_thread.start()
|
||||||
|
|
||||||
# if json is selected check if engine is compatible
|
|
||||||
if output_format == 'json' and not engine_instances[engine_index].coordinate_support:
|
if output_format == 'json' and not engine_instances[engine_index].coordinate_support:
|
||||||
supported_engines = (engine.name for engine in engine_instances if engine.coordinate_support)
|
supported_engines = (engine.name for engine in engine_instances if engine.coordinate_support)
|
||||||
logger.error(f"The selected engine '{engine_instances[engine_index].name}' does not support coordinate output.")
|
logger.error(f"The selected engine '{engine_instances[engine_index].name}' does not support coordinate output.")
|
||||||
logger.error(f"Please choose one of: {', '.join(supported_engines)}")
|
logger.error(f"Please choose one of: {', '.join(supported_engines)}.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
logger.opt(colors=True).info(f"Reading from {' and '.join(read_from_readable)}, writing to {write_to_readable} using <{engine_color}>{engine_instances[engine_index].readable_name}</{engine_color}>{' (paused)' if paused else ''}")
|
logger.opt(colors=True).info(f"Reading from {' and '.join(read_from_readable)}, writing to {write_to_readable} using <{engine_color}>{engine_instances[engine_index].readable_name}</{engine_color}>{' (paused)' if paused.is_set() else ''}")
|
||||||
|
|
||||||
while not terminated:
|
while not terminated.is_set():
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
img = None
|
img = None
|
||||||
filter_text = False
|
filter_text = False
|
||||||
|
auto_pause = True
|
||||||
|
notify = False
|
||||||
|
|
||||||
if process_queue:
|
if process_queue:
|
||||||
try:
|
try:
|
||||||
img, filter_text = image_queue.get(timeout=0.1)
|
img, is_screen_capture = image_queue.get_nowait()
|
||||||
if screen_capture_periodic:
|
if not screen_capture_periodic and is_screen_capture:
|
||||||
filter_text = False
|
filter_text = True
|
||||||
|
if is_screen_capture:
|
||||||
|
auto_pause = False
|
||||||
notify = True
|
notify = True
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if (not img) and screen_capture_periodic:
|
if (not img) and screen_capture_periodic:
|
||||||
if (not paused) and screenshot_thread.screencapture_window_active and screenshot_thread.screencapture_window_visible and (time.time() - last_screenshot_time) > screen_capture_delay_secs:
|
if (not paused.is_set()) and screenshot_thread.screencapture_window_active and screenshot_thread.screencapture_window_visible and (time.time() - last_screenshot_time) > screen_capture_delay_secs:
|
||||||
screenshot_event.set()
|
screenshot_event.set()
|
||||||
try:
|
try:
|
||||||
img = periodic_screenshot_queue.get(timeout=0.1)
|
img = periodic_screenshot_queue.get_nowait()
|
||||||
filter_text = True
|
filter_text = True
|
||||||
notify = False
|
|
||||||
last_screenshot_time = time.time()
|
last_screenshot_time = time.time()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if img == 0:
|
if img == 0:
|
||||||
on_window_closed(False)
|
on_window_closed(False)
|
||||||
terminated = True
|
terminated.set()
|
||||||
break
|
break
|
||||||
elif img:
|
elif img:
|
||||||
output_result(img, filter_text, notify)
|
output_result(img, filter_text, auto_pause, notify)
|
||||||
if isinstance(img, Path):
|
if isinstance(img, Path):
|
||||||
if delete_images:
|
if delete_images:
|
||||||
Path.unlink(img)
|
Path.unlink(img)
|
||||||
|
|
||||||
elapsed_time = time.time() - start_time
|
elapsed_time = time.time() - start_time
|
||||||
if (not terminated) and elapsed_time < 0.1:
|
if (not terminated.is_set()) and elapsed_time < 0.1:
|
||||||
time.sleep(0.1 - elapsed_time)
|
time.sleep(0.1 - elapsed_time)
|
||||||
|
|
||||||
|
user_input_thread.join()
|
||||||
|
auto_pause_handler.stop()
|
||||||
|
output_result.second_pass_thread.stop()
|
||||||
|
terminate_selector_if_running()
|
||||||
if websocket_server_thread:
|
if websocket_server_thread:
|
||||||
websocket_server_thread.stop_server()
|
websocket_server_thread.stop_server()
|
||||||
websocket_server_thread.join()
|
websocket_server_thread.join()
|
||||||
@@ -1684,4 +1722,3 @@ def run():
|
|||||||
screenshot_thread.join()
|
screenshot_thread.join()
|
||||||
if key_combo_listener:
|
if key_combo_listener:
|
||||||
key_combo_listener.stop()
|
key_combo_listener.stop()
|
||||||
user_input_thread.join()
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import multiprocessing
|
import multiprocessing
|
||||||
import queue
|
import queue
|
||||||
import mss
|
import mss
|
||||||
|
from loguru import logger
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import sys
|
import sys
|
||||||
try:
|
try:
|
||||||
@@ -170,7 +171,8 @@ def get_screen_selection(pil_image, permanent_process):
|
|||||||
global selector_process, result_queue, command_queue
|
global selector_process, result_queue, command_queue
|
||||||
|
|
||||||
if not selector_available:
|
if not selector_available:
|
||||||
raise ValueError('tkinter or PIL with tkinter support are not installed, unable to open picker')
|
logger.error('tkinter or PIL with tkinter support are not installed, unable to open picker')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if selector_process is None or not selector_process.is_alive():
|
if selector_process is None or not selector_process.is_alive():
|
||||||
result_queue = multiprocessing.Queue()
|
result_queue = multiprocessing.Queue()
|
||||||
@@ -188,6 +190,10 @@ def get_screen_selection(pil_image, permanent_process):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
if not permanent_process:
|
if not permanent_process:
|
||||||
|
terminate_selector_if_running()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def terminate_selector_if_running():
|
||||||
|
if selector_process and selector_process.is_alive():
|
||||||
command_queue.put(False)
|
command_queue.put(False)
|
||||||
selector_process.join()
|
selector_process.join()
|
||||||
return result
|
|
||||||
|
|||||||
@@ -23,8 +23,7 @@
|
|||||||
;pause_at_startup = False
|
;pause_at_startup = False
|
||||||
|
|
||||||
;Automatically pause the program after the specified amount of seconds since
|
;Automatically pause the program after the specified amount of seconds since
|
||||||
;the last successful text recognition. Will be ignored when reading with screen
|
;the last successful text recognition. 0 to disable.
|
||||||
;capture. 0 to disable.
|
|
||||||
;auto_pause = 0
|
;auto_pause = 0
|
||||||
|
|
||||||
;Delete image files after processing when reading from a directory.
|
;Delete image files after processing when reading from a directory.
|
||||||
@@ -52,8 +51,7 @@
|
|||||||
;combo_pause =
|
;combo_pause =
|
||||||
|
|
||||||
;Combo to wait on for switching the OCR engine. As an example:
|
;Combo to wait on for switching the OCR engine. As an example:
|
||||||
;"<ctrl>+<shift>+a". To be used with combo_pause. The list of keys can be found
|
;"<ctrl>+<shift>+a". The list of keys can be found here:
|
||||||
;here:
|
|
||||||
;https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key
|
;https://pynput.readthedocs.io/en/latest/keyboard.html#pynput.keyboard.Key
|
||||||
;combo_engine_switch =
|
;combo_engine_switch =
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user