Skip to content

Commit b3f72b3

Browse files
MrMe0wscursoragent
andcommitted
Release v0.3.0: перевод с экрана по клавише Ё.
Добавлены оверлей выделения, Windows OCR, модалка рядом с выделением и обновлены README и release notes. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a73416f commit b3f72b3

14 files changed

Lines changed: 580 additions & 28 deletions

File tree

‎README.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
<img src="photo/modal.png" alt="Пример работы" width="420" style="max-width: 100%; height: auto;">
88
</p>
99

10-
**MeowsL** — фоновый переводчик для Windows. Работает из системного трея: скопировал текст, нажал Ctrl+c+c — получил перевод в компактном popup-окне.
10+
**MeowsL** — фоновый переводчик для Windows из системного трея. **Ctrl+C+C** — перевод из буфера; **Ё** — выдели текст на экране рамкой. Результат в компактном popup-окне.
1111

1212
## Возможности
1313

1414
- **Ctrl+C+C** — открыть перевод текста из буфера обмена
15+
- **Ё** — выделить область на экране и перевести текст с картинки
1516
- **Ctrl+Alt+T** — запасной хоткей
1617
- Автоопределение языка: русский ↔ английский
1718
- Редактирование исходного текста с debounce-переводом
@@ -47,13 +48,23 @@ py -3 main.py
4748
3. Можно править верхнее поле — перевод обновится автоматически.
4849
4. Закройте окно через `Esc`, клик мимо окна или **×** — приложение останется в трее.
4950

51+
### Перевод с экрана (картинки, PDF, игры)
52+
53+
1. Нажмите **Ё** (физическая клавиша слева от `1`).
54+
2. Зажмите левую кнопку мыши и обведите нужный текст рамкой.
55+
3. Отпустите кнопку — MeowsL распознает текст и покажет перевод.
56+
4. `Esc` во время выделения — отмена.
57+
58+
> Для распознавания используется встроенный Windows OCR. Убедитесь, что в системе установлены OCR-пакеты нужных языков (обычно EN и RU уже есть). Проверка в PowerShell: `Get-WindowsCapability -Online -Name "Language.OCR*"`.
59+
5060

5161
## Стек
5262

5363
- Python 3.10+
5464
- PyQt6 — интерфейс
5565
- `deep_translator` — Google Translate API
5666
- `keyboard` — глобальные хоткеи Windows
67+
- `winocr` + `Pillow` — захват и распознавание текста с экрана (Windows OCR)
5768
- PyInstaller — сборка в exe
5869

5970
## Структура

‎RELEASE_NOTES.md‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
1-
# Релиз v0.2.1
1+
# Релиз v0.3.0
22

33
## Вариант для GitHub Releases
44

55
## 🚀 Новое
66

7-
- **Автозапуск с Windows** — галочка «Запускать с Windows» в меню трея; состояние сохраняется в настройках и синхронизируется с реестром (`HKCU\...\Run`) при старте приложения
7+
- **Перевод с экрана по клавише Ё** — нажмите физическую клавишу "Ё", обведите текст рамкой мышью и получите перевод в модалке
8+
- **Оверлей выделения** — затемнение экрана, пунктирная рамка, отмена по `Esc`
9+
- **Модалка рядом с выделением** — окно перевода открывается под, над или сбоку от выбранной области (с учётом границ экрана)
810

911
## ✨ Улучшения
1012

11-
- В **README** добавлено описание автозапуска; уточнена формулировка про необходимость прав администратора для глобальных хоткеев
13+
- Хоткей **Ё** привязан к физической клавише (scan code 41) и работает независимо от раскладки
14+
- Распознавание текста с экрана через встроенный **Windows OCR** (английский пакет)
15+
1216

1317
## 🧱 Технические изменения
1418

15-
- Модуль `platform/autostart.py`: запись в реестр, синхронизация пути к exe при обновлении
16-
- Установщик Inno Setup: при деинсталляции удаляется запись автозапуска MeowsL из реестра
19+
- Новые модули: `screen_capture_overlay`, `ocr`, `ocr_runner`
20+
- Зависимости: `winocr`, `Pillow`
21+
- Обновлены `build/meowsl.spec` (сборка WinRT/OCR), `README.md`, `requirements.txt`
1722

1823
---
1924

2025
## Вариант для Telegram
2126

22-
0.2.1 https://github.com/MrMe0ws/MeowsL/releases/tag/0.2.1
27+
0.3.0 https://github.com/MrMe0ws/MeowsL/releases/tag/0.3.0
2328

2429
🚀 Новое
25-
• Автозапуск с Windows — галочка «Запускать с Windows» в меню трея
30+
• Перевод с экрана по клавише Ё — выделите текст рамкой и получите перевод в модалке
31+
• Оверлей выделения с затемнением экрана; отмена по Esc
32+
• Модалка открывается рядом с выделенной областью
2633

2734
✨ Улучшения
28-
• README: описание автозапуска и уточнение про хоткеи
35+
• Хоткей Ё работает на любой раскладке клавиатуры
36+
• Распознавание текста через Windows OCR
2937

3038
🧱 Технические изменения
31-
• Синхронизация автозапуска с реестром при старте; очистка записи при удалении через установщик
39+
• winocr и Pillow; обновлены сборка exe и README

‎build/installer.iss‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
; Скачать Inno Setup: https://jrsoftware.org/isinfo.php
33

44
#define MyAppName "MeowsL"
5-
#define MyAppVersion "0.2.0"
5+
#define MyAppVersion "0.3.0"
66
#define MyAppPublisher "MeowsLate"
77
#define MyAppExeName "MeowsL.exe"
88

‎build/meowsl.spec‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
11
# -*- mode: python ; coding: utf-8 -*-
22
from pathlib import Path
33

4+
from PyInstaller.utils.hooks import collect_all, collect_submodules
5+
46
# SPECPATH = папка build; корень проекта — на уровень выше
57
ROOT = Path(SPECPATH).parent
68

9+
winocr_datas, winocr_binaries, winocr_hiddenimports = collect_all("winocr")
10+
winrt_hiddenimports = collect_submodules("winrt")
11+
712
a = Analysis(
813
[str(ROOT / "main.py")],
914
pathex=[str(ROOT / "src")],
10-
binaries=[],
15+
binaries=winocr_binaries,
1116
datas=[
1217
(str(ROOT / "photo" / "logo.png"), "photo"),
1318
(str(ROOT / "photo" / "logo-icon.png"), "photo"),
19+
*winocr_datas,
1420
],
1521
hiddenimports=[
1622
"PyQt6.QtCore",
1723
"PyQt6.QtGui",
1824
"PyQt6.QtWidgets",
1925
"deep_translator",
2026
"keyboard",
27+
"winocr",
28+
"PIL",
29+
"PIL.Image",
30+
"winrt.windows.media.ocr",
31+
"winrt.windows.globalization",
32+
"winrt.windows.graphics.imaging",
33+
"winrt.windows.storage.streams",
34+
"winrt.windows.foundation",
35+
"winrt.windows.foundation.collections",
36+
*winocr_hiddenimports,
37+
*winrt_hiddenimports,
2138
],
2239
hookspath=[],
2340
hooksconfig={},

‎pyproject.toml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "translate-meows"
7-
version = "0.2.0"
7+
version = "0.3.0"
88
description = "Глобальный переводчик по хоткею для Windows"
99
readme = "README.md"
1010
requires-python = ">=3.10"
1111
dependencies = [
1212
"PyQt6>=6.6.0",
1313
"deep-translator>=1.11.4",
1414
"keyboard>=0.13.5",
15+
"winocr>=0.0.15",
16+
"Pillow>=10.0.0",
1517
]
1618

1719
[project.scripts]

‎requirements.txt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
PyQt6>=6.6.0
22
deep-translator>=1.11.4
33
keyboard>=0.13.5
4+
winocr>=0.0.15
5+
Pillow>=10.0.0

‎src/translate_meows/app.py‎

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,27 @@
22

33
import sys
44

5-
from PyQt6.QtCore import QObject, Qt, pyqtSignal
6-
from PyQt6.QtGui import QGuiApplication
5+
from PyQt6.QtCore import QObject, Qt, QRect, pyqtSignal
6+
from PyQt6.QtGui import QGuiApplication, QImage
77
from PyQt6.QtWidgets import QApplication, QMessageBox, QSystemTrayIcon
88

99
from translate_meows.config import APP_NAME
1010
from translate_meows.platform import autostart
1111
from translate_meows.platform.hotkey import HotkeyListener
1212
from translate_meows.platform.tray import TrayManager
13+
from translate_meows.services.ocr_runner import OcrRunner
1314
from translate_meows.ui.icons import app_icon
1415
from translate_meows.ui.popup_window import TranslationPopup
16+
from translate_meows.ui.screen_capture_overlay import ScreenCaptureOverlay
17+
18+
OCR_EMPTY_MESSAGE = "Текст не распознан"
1519

1620

1721
class HotkeyBridge(QObject):
1822
"""Мост между потоком keyboard и главным потоком Qt."""
1923

2024
triggered = pyqtSignal()
25+
screen_capture_triggered = pyqtSignal()
2126

2227

2328
class TranslatorApp:
@@ -31,12 +36,27 @@ def __init__(self) -> None:
3136
self._app.setStyle("Fusion")
3237

3338
self._popup = TranslationPopup()
39+
self._overlay = ScreenCaptureOverlay()
40+
self._ocr_runner = OcrRunner()
41+
self._ocr_request_id = 0
42+
self._capture_anchor: QRect | None = None
43+
3444
self._bridge = HotkeyBridge()
3545
self._bridge.triggered.connect(
3646
self._on_hotkey_triggered, Qt.ConnectionType.QueuedConnection
3747
)
48+
self._bridge.screen_capture_triggered.connect(
49+
self._on_screen_capture_triggered, Qt.ConnectionType.QueuedConnection
50+
)
3851

39-
self._hotkey = HotkeyListener(on_trigger=self._bridge.triggered.emit)
52+
self._overlay.region_captured.connect(self._on_region_captured)
53+
self._ocr_runner.finished.connect(self._on_ocr_finished)
54+
self._ocr_runner.error.connect(self._on_ocr_error)
55+
56+
self._hotkey = HotkeyListener(
57+
on_trigger=self._bridge.triggered.emit,
58+
on_screen_capture=self._bridge.screen_capture_triggered.emit,
59+
)
4060
self._tray = TrayManager(
4161
on_show=self._on_hotkey_triggered,
4262
on_quit=self._app.quit,
@@ -59,13 +79,47 @@ def run(self) -> int:
5979

6080
def _on_about_to_quit(self) -> None:
6181
self._hotkey.stop()
82+
self._overlay.dismiss()
83+
self._ocr_request_id += 1
84+
self._ocr_runner.shutdown(timeout_ms=15000)
6285
self._popup.shutdown()
6386

6487
def _on_hotkey_triggered(self) -> None:
6588
clipboard = QGuiApplication.clipboard()
6689
text = clipboard.text() if clipboard else ""
6790
self._popup.show_with_text(text)
6891

92+
def _on_screen_capture_triggered(self) -> None:
93+
if self._overlay.isVisible():
94+
return
95+
self._overlay.show_overlay()
96+
97+
def _on_region_captured(self, image: QImage, selection: QRect) -> None:
98+
self._capture_anchor = selection
99+
self._ocr_request_id += 1
100+
self._ocr_runner.submit(image, self._ocr_request_id)
101+
102+
def _on_ocr_finished(self, text: str, request_id: int) -> None:
103+
if request_id != self._ocr_request_id:
104+
return
105+
anchor = self._capture_anchor
106+
self._capture_anchor = None
107+
if text.strip():
108+
self._popup.show_with_text(text, anchor_rect=anchor)
109+
return
110+
self._popup.show_with_text(
111+
OCR_EMPTY_MESSAGE, auto_translate=False, anchor_rect=anchor
112+
)
113+
114+
def _on_ocr_error(self, message: str, request_id: int) -> None:
115+
if request_id != self._ocr_request_id:
116+
return
117+
anchor = self._capture_anchor
118+
self._capture_anchor = None
119+
self._popup.show_with_text(
120+
message, auto_translate=False, anchor_rect=anchor
121+
)
122+
69123

70124
def run() -> int:
71125
return TranslatorApp().run()

‎src/translate_meows/config.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
APP_NAME = "Translate Meows"
44
APP_DISPLAY_NAME = "MeowsL"
5-
APP_VERSION = "0.2.2"
5+
APP_VERSION = "0.3.0"
66

77
# Перевод
88
DEBOUNCE_MS = 600
@@ -11,6 +11,12 @@
1111
# Глобальные хоткеи
1212
DOUBLE_CTRL_C_INTERVAL_MS = 500
1313
FALLBACK_HOTKEY = "ctrl+alt+t"
14+
# Физическая клавиша слева от «1» (Ё / `) — scan code 41 на стандартной раскладке
15+
SCREEN_CAPTURE_SCAN_CODE = 41
16+
17+
# Захват области экрана
18+
MIN_SELECTION_SIZE = 10
19+
POPUP_ANCHOR_MARGIN = 12
1420

1521
# Окно
1622
POPUP_WIDTH = 420

‎src/translate_meows/platform/hotkey.py‎

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,77 @@
66

77
import keyboard
88

9-
from translate_meows.config import DOUBLE_CTRL_C_INTERVAL_MS, FALLBACK_HOTKEY
9+
from translate_meows.config import (
10+
DOUBLE_CTRL_C_INTERVAL_MS,
11+
FALLBACK_HOTKEY,
12+
SCREEN_CAPTURE_SCAN_CODE,
13+
)
14+
15+
16+
def _register_scan_code_hotkey(
17+
scan_code: int,
18+
callback: Callable[[], None],
19+
*,
20+
suppress: bool = True,
21+
) -> Callable[[], None]:
22+
"""
23+
Привязка к физической клавише по scan code.
24+
Нужна для клавиши Ё, т.к. имя символа зависит от раскладки.
25+
"""
26+
from keyboard import KEY_DOWN, _listener as listener
27+
28+
def hook(event) -> bool:
29+
if event.event_type == KEY_DOWN:
30+
callback()
31+
if suppress:
32+
return False
33+
return True
34+
35+
listener.start_if_necessary()
36+
store = listener.blocking_keys if suppress else listener.nonblocking_keys
37+
store[scan_code].append(hook)
38+
39+
def remove() -> None:
40+
if hook in store[scan_code]:
41+
store[scan_code].remove(hook)
42+
43+
return remove
1044

1145

1246
class HotkeyListener:
1347
"""
1448
Слушает глобальные хоткеи:
15-
- Ctrl+C+C (двойное нажатие за 500 мс) — основной
49+
- Ctrl+C+C (двойное нажатие за 500 мс) — перевод из буфера
1650
- Ctrl+Alt+T — запасной
51+
- Ё (физ. клавиша scan 41) — выделение области экрана
1752
"""
1853

19-
def __init__(self, on_trigger: Callable[[], None]) -> None:
54+
def __init__(
55+
self,
56+
on_trigger: Callable[[], None],
57+
on_screen_capture: Callable[[], None],
58+
) -> None:
2059
self._on_trigger = on_trigger
60+
self._on_screen_capture = on_screen_capture
2161
self._last_ctrl_c = 0.0
2262
self._interval = DOUBLE_CTRL_C_INTERVAL_MS / 1000.0
2363
self._lock = Lock()
2464
self._ctrl_c_handle = None
2565
self._fallback_handle = None
66+
self._screen_capture_remove: Optional[Callable[[], None]] = None
2667

2768
def start(self) -> None:
28-
# add_hotkey надёжнее raw hook на Windows
2969
self._ctrl_c_handle = keyboard.add_hotkey(
3070
"ctrl+c", self._on_ctrl_c, suppress=False, trigger_on_release=False
3171
)
3272
self._fallback_handle = keyboard.add_hotkey(
3373
FALLBACK_HOTKEY, self._on_trigger, suppress=False
3474
)
75+
self._screen_capture_remove = _register_scan_code_hotkey(
76+
SCREEN_CAPTURE_SCAN_CODE,
77+
self._on_screen_capture,
78+
suppress=True,
79+
)
3580

3681
def stop(self) -> None:
3782
if self._ctrl_c_handle is not None:
@@ -40,6 +85,9 @@ def stop(self) -> None:
4085
if self._fallback_handle is not None:
4186
keyboard.remove_hotkey(self._fallback_handle)
4287
self._fallback_handle = None
88+
if self._screen_capture_remove is not None:
89+
self._screen_capture_remove()
90+
self._screen_capture_remove = None
4391

4492
def _on_ctrl_c(self) -> None:
4593
now = time.monotonic()

0 commit comments

Comments
 (0)