More changes

This commit is contained in:
AuroraWright
2025-10-14 05:15:43 +02:00
parent 95f699cf07
commit a5b807b44e
5 changed files with 146 additions and 105 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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()

View File

@@ -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

View File

@@ -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 =