PayPerTranscript 0.3.0__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/PKG-INFO +3 -1
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/PayPerTranscript.egg-info/PKG-INFO +3 -1
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/PayPerTranscript.egg-info/SOURCES.txt +14 -1
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/PayPerTranscript.egg-info/requires.txt +1 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/README.md +1 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/__init__.py +1 -1
- paypertranscript-0.3.2/paypertranscript/core/atomic_io.py +29 -0
- paypertranscript-0.3.2/paypertranscript/core/clipboard_manager.py +263 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/config.py +38 -16
- paypertranscript-0.3.2/paypertranscript/core/context_detector.py +250 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/hotkey.py +50 -26
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/recorder.py +9 -8
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/session_logger.py +4 -2
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/text_inserter.py +49 -40
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/updater.py +7 -5
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/pipeline/transcription.py +71 -8
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/providers/groq_provider.py +2 -2
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/app.py +42 -6
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/main_window.py +9 -5
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/pages/home_page.py +2 -2
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/pages/settings_page.py +9 -2
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/tray.py +3 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/pyproject.toml +2 -1
- paypertranscript-0.3.2/tests/test_atomic_and_session_logger.py +104 -0
- paypertranscript-0.3.2/tests/test_clipboard_integration.py +73 -0
- paypertranscript-0.3.2/tests/test_clipboard_manager_formats.py +60 -0
- paypertranscript-0.3.2/tests/test_groq_provider_logging.py +42 -0
- paypertranscript-0.3.2/tests/test_home_page_config_key.py +94 -0
- paypertranscript-0.3.2/tests/test_hotkey_settings_and_timer.py +112 -0
- paypertranscript-0.3.2/tests/test_pipeline_privacy_and_transcripts.py +89 -0
- paypertranscript-0.3.2/tests/test_recorder_thread_safety.py +39 -0
- paypertranscript-0.3.2/tests/test_release_script.py +48 -0
- paypertranscript-0.3.2/tests/test_updater_version.py +14 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/LICENSE +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/PayPerTranscript.egg-info/dependency_links.txt +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/PayPerTranscript.egg-info/entry_points.txt +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/PayPerTranscript.egg-info/top_level.txt +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/__main__.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/icons/app.ico +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/icons/app.png +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/icons/app_big.png +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/icons/arrow_down.svg +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/icons/tray.png +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/icons/tray_green.png +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/icons/tray_orange.png +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/sounds/start.wav +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/sounds/stop.wav +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/assets/styles/dark.qss +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/__init__.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/audio_manager.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/cost_tracker.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/logging.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/paths.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/core/window_detector.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/pipeline/__init__.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/providers/__init__.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/providers/base.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/__init__.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/animated.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/constants.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/overlay.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/pages/__init__.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/pages/statistics_page.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/pages/window_mapping_page.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/pages/word_list_page.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/setup_wizard.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/sidebar.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/paypertranscript/ui/widgets.py +0 -0
- {paypertranscript-0.3.0 → paypertranscript-0.3.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PayPerTranscript
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Open-Source Voice-to-Text mit Pay-per-Use Pricing
|
|
5
5
|
Author: PayPerTranscript Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,6 +22,7 @@ Requires-Dist: pyperclip
|
|
|
22
22
|
Requires-Dist: pyautogui
|
|
23
23
|
Requires-Dist: keyring
|
|
24
24
|
Requires-Dist: soundfile
|
|
25
|
+
Requires-Dist: packaging
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: build; extra == "dev"
|
|
27
28
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -65,6 +66,7 @@ Kommerzielle Voice-to-Text Dienste kosten **$12-15/Monat** - egal ob du sie 5 Mi
|
|
|
65
66
|
- **Hold-to-Record**: `Ctrl+Win` halten - sprechen - loslassen - fertig
|
|
66
67
|
- **Blitzschnell**: 30 Sekunden Audio = 0.14 Sekunden Transkription (via Groq Whisper)
|
|
67
68
|
- **Smart Formatting**: WhatsApp bekommt lockere Texte, Outlook professionelle E-Mails
|
|
69
|
+
- **Kontext-Erkennung**: Markierten Text im aktiven Fenster erkennen - das LLM nutzt ihn für korrekte Schreibweisen und Bezüge
|
|
68
70
|
- **Wortliste**: Eigene Namen und Fachbegriffe werden immer korrekt geschrieben
|
|
69
71
|
|
|
70
72
|
### 📊 Transparenz & Kontrolle
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PayPerTranscript
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Open-Source Voice-to-Text mit Pay-per-Use Pricing
|
|
5
5
|
Author: PayPerTranscript Contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,6 +22,7 @@ Requires-Dist: pyperclip
|
|
|
22
22
|
Requires-Dist: pyautogui
|
|
23
23
|
Requires-Dist: keyring
|
|
24
24
|
Requires-Dist: soundfile
|
|
25
|
+
Requires-Dist: packaging
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: build; extra == "dev"
|
|
27
28
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -65,6 +66,7 @@ Kommerzielle Voice-to-Text Dienste kosten **$12-15/Monat** - egal ob du sie 5 Mi
|
|
|
65
66
|
- **Hold-to-Record**: `Ctrl+Win` halten - sprechen - loslassen - fertig
|
|
66
67
|
- **Blitzschnell**: 30 Sekunden Audio = 0.14 Sekunden Transkription (via Groq Whisper)
|
|
67
68
|
- **Smart Formatting**: WhatsApp bekommt lockere Texte, Outlook professionelle E-Mails
|
|
69
|
+
- **Kontext-Erkennung**: Markierten Text im aktiven Fenster erkennen - das LLM nutzt ihn für korrekte Schreibweisen und Bezüge
|
|
68
70
|
- **Wortliste**: Eigene Namen und Fachbegriffe werden immer korrekt geschrieben
|
|
69
71
|
|
|
70
72
|
### 📊 Transparenz & Kontrolle
|
|
@@ -20,8 +20,11 @@ paypertranscript/assets/sounds/start.wav
|
|
|
20
20
|
paypertranscript/assets/sounds/stop.wav
|
|
21
21
|
paypertranscript/assets/styles/dark.qss
|
|
22
22
|
paypertranscript/core/__init__.py
|
|
23
|
+
paypertranscript/core/atomic_io.py
|
|
23
24
|
paypertranscript/core/audio_manager.py
|
|
25
|
+
paypertranscript/core/clipboard_manager.py
|
|
24
26
|
paypertranscript/core/config.py
|
|
27
|
+
paypertranscript/core/context_detector.py
|
|
25
28
|
paypertranscript/core/cost_tracker.py
|
|
26
29
|
paypertranscript/core/hotkey.py
|
|
27
30
|
paypertranscript/core/logging.py
|
|
@@ -51,4 +54,14 @@ paypertranscript/ui/pages/home_page.py
|
|
|
51
54
|
paypertranscript/ui/pages/settings_page.py
|
|
52
55
|
paypertranscript/ui/pages/statistics_page.py
|
|
53
56
|
paypertranscript/ui/pages/window_mapping_page.py
|
|
54
|
-
paypertranscript/ui/pages/word_list_page.py
|
|
57
|
+
paypertranscript/ui/pages/word_list_page.py
|
|
58
|
+
tests/test_atomic_and_session_logger.py
|
|
59
|
+
tests/test_clipboard_integration.py
|
|
60
|
+
tests/test_clipboard_manager_formats.py
|
|
61
|
+
tests/test_groq_provider_logging.py
|
|
62
|
+
tests/test_home_page_config_key.py
|
|
63
|
+
tests/test_hotkey_settings_and_timer.py
|
|
64
|
+
tests/test_pipeline_privacy_and_transcripts.py
|
|
65
|
+
tests/test_recorder_thread_safety.py
|
|
66
|
+
tests/test_release_script.py
|
|
67
|
+
tests/test_updater_version.py
|
|
@@ -36,6 +36,7 @@ Kommerzielle Voice-to-Text Dienste kosten **$12-15/Monat** - egal ob du sie 5 Mi
|
|
|
36
36
|
- **Hold-to-Record**: `Ctrl+Win` halten - sprechen - loslassen - fertig
|
|
37
37
|
- **Blitzschnell**: 30 Sekunden Audio = 0.14 Sekunden Transkription (via Groq Whisper)
|
|
38
38
|
- **Smart Formatting**: WhatsApp bekommt lockere Texte, Outlook professionelle E-Mails
|
|
39
|
+
- **Kontext-Erkennung**: Markierten Text im aktiven Fenster erkennen - das LLM nutzt ihn für korrekte Schreibweisen und Bezüge
|
|
39
40
|
- **Wortliste**: Eigene Namen und Fachbegriffe werden immer korrekt geschrieben
|
|
40
41
|
|
|
41
42
|
### 📊 Transparenz & Kontrolle
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Atomic file writing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def atomic_write_text(path: Path, content: str, encoding: str = "utf-8") -> None:
|
|
11
|
+
"""Atomically write text content to a file."""
|
|
12
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
14
|
+
prefix=f".{path.name}.",
|
|
15
|
+
suffix=".tmp",
|
|
16
|
+
dir=str(path.parent),
|
|
17
|
+
)
|
|
18
|
+
try:
|
|
19
|
+
with os.fdopen(fd, "w", encoding=encoding, newline="") as tmp_file:
|
|
20
|
+
tmp_file.write(content)
|
|
21
|
+
tmp_file.flush()
|
|
22
|
+
os.fsync(tmp_file.fileno())
|
|
23
|
+
os.replace(tmp_name, path)
|
|
24
|
+
except Exception:
|
|
25
|
+
try:
|
|
26
|
+
os.unlink(tmp_name)
|
|
27
|
+
except OSError:
|
|
28
|
+
pass
|
|
29
|
+
raise
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Windows clipboard snapshot/restore helpers.
|
|
2
|
+
|
|
3
|
+
Designed for low-latency clipboard preservation during context detection and paste
|
|
4
|
+
operations. The implementation preserves clipboard formats best-effort by copying
|
|
5
|
+
raw global-memory payloads for each available format.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ctypes
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Final
|
|
15
|
+
|
|
16
|
+
from paypertranscript.core.logging import get_logger
|
|
17
|
+
|
|
18
|
+
log = get_logger("core.clipboard_manager")
|
|
19
|
+
|
|
20
|
+
if ctypes.sizeof(ctypes.c_void_p) == 8:
|
|
21
|
+
_SIZE_T = ctypes.c_uint64
|
|
22
|
+
else:
|
|
23
|
+
_SIZE_T = ctypes.c_uint32
|
|
24
|
+
|
|
25
|
+
_user32 = ctypes.WinDLL("user32", use_last_error=True)
|
|
26
|
+
_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
27
|
+
|
|
28
|
+
_user32.OpenClipboard.argtypes = [ctypes.c_void_p]
|
|
29
|
+
_user32.OpenClipboard.restype = ctypes.c_int
|
|
30
|
+
_user32.CloseClipboard.argtypes = []
|
|
31
|
+
_user32.CloseClipboard.restype = ctypes.c_int
|
|
32
|
+
_user32.EmptyClipboard.argtypes = []
|
|
33
|
+
_user32.EmptyClipboard.restype = ctypes.c_int
|
|
34
|
+
_user32.EnumClipboardFormats.argtypes = [ctypes.c_uint]
|
|
35
|
+
_user32.EnumClipboardFormats.restype = ctypes.c_uint
|
|
36
|
+
_user32.GetClipboardData.argtypes = [ctypes.c_uint]
|
|
37
|
+
_user32.GetClipboardData.restype = ctypes.c_void_p
|
|
38
|
+
_user32.SetClipboardData.argtypes = [ctypes.c_uint, ctypes.c_void_p]
|
|
39
|
+
_user32.SetClipboardData.restype = ctypes.c_void_p
|
|
40
|
+
_user32.IsClipboardFormatAvailable.argtypes = [ctypes.c_uint]
|
|
41
|
+
_user32.IsClipboardFormatAvailable.restype = ctypes.c_int
|
|
42
|
+
_user32.GetClipboardSequenceNumber.argtypes = []
|
|
43
|
+
_user32.GetClipboardSequenceNumber.restype = ctypes.c_uint
|
|
44
|
+
|
|
45
|
+
_kernel32.GlobalAlloc.argtypes = [ctypes.c_uint, _SIZE_T]
|
|
46
|
+
_kernel32.GlobalAlloc.restype = ctypes.c_void_p
|
|
47
|
+
_kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
|
|
48
|
+
_kernel32.GlobalLock.restype = ctypes.c_void_p
|
|
49
|
+
_kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]
|
|
50
|
+
_kernel32.GlobalUnlock.restype = ctypes.c_int
|
|
51
|
+
_kernel32.GlobalSize.argtypes = [ctypes.c_void_p]
|
|
52
|
+
_kernel32.GlobalSize.restype = _SIZE_T
|
|
53
|
+
_kernel32.GlobalFree.argtypes = [ctypes.c_void_p]
|
|
54
|
+
_kernel32.GlobalFree.restype = ctypes.c_void_p
|
|
55
|
+
|
|
56
|
+
_GMEM_MOVEABLE: Final[int] = 0x0002
|
|
57
|
+
_CF_UNICODETEXT: Final[int] = 13
|
|
58
|
+
|
|
59
|
+
_OPEN_RETRIES: Final[int] = 5
|
|
60
|
+
_OPEN_DELAY_SECONDS: Final[float] = 0.002
|
|
61
|
+
|
|
62
|
+
_lock = threading.RLock()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class ClipboardFormatPayload:
|
|
67
|
+
"""Raw bytes for a single clipboard format."""
|
|
68
|
+
|
|
69
|
+
format_id: int
|
|
70
|
+
data: bytes
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class ClipboardSnapshot:
|
|
75
|
+
"""Clipboard snapshot containing copied format payloads."""
|
|
76
|
+
|
|
77
|
+
sequence_number: int
|
|
78
|
+
formats: tuple[ClipboardFormatPayload, ...]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_EMPTY_SNAPSHOT = ClipboardSnapshot(sequence_number=0, formats=())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _open_clipboard() -> bool:
|
|
85
|
+
for attempt in range(_OPEN_RETRIES):
|
|
86
|
+
if _user32.OpenClipboard(None):
|
|
87
|
+
return True
|
|
88
|
+
if attempt < _OPEN_RETRIES - 1:
|
|
89
|
+
time.sleep(_OPEN_DELAY_SECONDS)
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _copy_hglobal(handle: ctypes.c_void_p) -> bytes | None:
|
|
94
|
+
size = int(_kernel32.GlobalSize(handle))
|
|
95
|
+
if size < 0:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
ptr = _kernel32.GlobalLock(handle)
|
|
99
|
+
if not ptr:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
if size == 0:
|
|
104
|
+
return b""
|
|
105
|
+
raw = (ctypes.c_ubyte * size).from_address(ptr)
|
|
106
|
+
return bytes(raw)
|
|
107
|
+
finally:
|
|
108
|
+
_kernel32.GlobalUnlock(handle)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _alloc_hglobal(data: bytes) -> ctypes.c_void_p:
|
|
112
|
+
size = max(len(data), 1)
|
|
113
|
+
handle = _kernel32.GlobalAlloc(_GMEM_MOVEABLE, size)
|
|
114
|
+
if not handle:
|
|
115
|
+
return ctypes.c_void_p()
|
|
116
|
+
|
|
117
|
+
ptr = _kernel32.GlobalLock(handle)
|
|
118
|
+
if not ptr:
|
|
119
|
+
_kernel32.GlobalFree(handle)
|
|
120
|
+
return ctypes.c_void_p()
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
if data:
|
|
124
|
+
ctypes.memmove(ptr, data, len(data))
|
|
125
|
+
if size > len(data):
|
|
126
|
+
ctypes.memset(ptr + len(data), 0, size - len(data))
|
|
127
|
+
finally:
|
|
128
|
+
_kernel32.GlobalUnlock(handle)
|
|
129
|
+
|
|
130
|
+
return handle
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def snapshot_clipboard() -> ClipboardSnapshot:
|
|
134
|
+
"""Capture the current clipboard in a best-effort, format-preserving way."""
|
|
135
|
+
start = time.perf_counter()
|
|
136
|
+
with _lock:
|
|
137
|
+
if not _open_clipboard():
|
|
138
|
+
log.debug("Clipboard snapshot skipped: OpenClipboard failed")
|
|
139
|
+
return _EMPTY_SNAPSHOT
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
sequence_number = int(_user32.GetClipboardSequenceNumber())
|
|
143
|
+
payloads: list[ClipboardFormatPayload] = []
|
|
144
|
+
|
|
145
|
+
fmt = 0
|
|
146
|
+
while True:
|
|
147
|
+
fmt = int(_user32.EnumClipboardFormats(fmt))
|
|
148
|
+
if fmt == 0:
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
handle = _user32.GetClipboardData(fmt)
|
|
152
|
+
if not handle:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
copied = _copy_hglobal(handle)
|
|
156
|
+
if copied is None:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
payloads.append(ClipboardFormatPayload(format_id=fmt, data=copied))
|
|
160
|
+
|
|
161
|
+
snapshot = ClipboardSnapshot(
|
|
162
|
+
sequence_number=sequence_number,
|
|
163
|
+
formats=tuple(payloads),
|
|
164
|
+
)
|
|
165
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
166
|
+
log.debug(
|
|
167
|
+
"Clipboard snapshot: %d formats in %.2fms",
|
|
168
|
+
len(snapshot.formats),
|
|
169
|
+
elapsed_ms,
|
|
170
|
+
)
|
|
171
|
+
return snapshot
|
|
172
|
+
finally:
|
|
173
|
+
_user32.CloseClipboard()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def restore_clipboard(snapshot: ClipboardSnapshot) -> bool:
|
|
177
|
+
"""Restore a previously captured clipboard snapshot."""
|
|
178
|
+
start = time.perf_counter()
|
|
179
|
+
with _lock:
|
|
180
|
+
if not _open_clipboard():
|
|
181
|
+
log.warning("Clipboard restore failed: OpenClipboard failed")
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
if not _user32.EmptyClipboard():
|
|
186
|
+
log.warning("Clipboard restore failed: EmptyClipboard failed")
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
restored_count = 0
|
|
190
|
+
for payload in snapshot.formats:
|
|
191
|
+
handle = _alloc_hglobal(payload.data)
|
|
192
|
+
if not handle:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
result = _user32.SetClipboardData(payload.format_id, handle)
|
|
196
|
+
if not result:
|
|
197
|
+
_kernel32.GlobalFree(handle)
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
restored_count += 1
|
|
201
|
+
|
|
202
|
+
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
203
|
+
log.debug(
|
|
204
|
+
"Clipboard restore: %d/%d formats in %.2fms",
|
|
205
|
+
restored_count,
|
|
206
|
+
len(snapshot.formats),
|
|
207
|
+
elapsed_ms,
|
|
208
|
+
)
|
|
209
|
+
return restored_count == len(snapshot.formats)
|
|
210
|
+
finally:
|
|
211
|
+
_user32.CloseClipboard()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def get_clipboard_text() -> str:
|
|
215
|
+
"""Read UTF-16 text from clipboard if available."""
|
|
216
|
+
with _lock:
|
|
217
|
+
if not _open_clipboard():
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
if not _user32.IsClipboardFormatAvailable(_CF_UNICODETEXT):
|
|
222
|
+
return ""
|
|
223
|
+
|
|
224
|
+
handle = _user32.GetClipboardData(_CF_UNICODETEXT)
|
|
225
|
+
if not handle:
|
|
226
|
+
return ""
|
|
227
|
+
|
|
228
|
+
ptr = _kernel32.GlobalLock(handle)
|
|
229
|
+
if not ptr:
|
|
230
|
+
return ""
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
return ctypes.wstring_at(ptr) or ""
|
|
234
|
+
finally:
|
|
235
|
+
_kernel32.GlobalUnlock(handle)
|
|
236
|
+
finally:
|
|
237
|
+
_user32.CloseClipboard()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def set_clipboard_text(text: str) -> bool:
|
|
241
|
+
"""Replace clipboard contents with UTF-16 text."""
|
|
242
|
+
utf16 = text.encode("utf-16-le") + b"\x00\x00"
|
|
243
|
+
|
|
244
|
+
with _lock:
|
|
245
|
+
if not _open_clipboard():
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
if not _user32.EmptyClipboard():
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
handle = _alloc_hglobal(utf16)
|
|
253
|
+
if not handle:
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
result = _user32.SetClipboardData(_CF_UNICODETEXT, handle)
|
|
257
|
+
if not result:
|
|
258
|
+
_kernel32.GlobalFree(handle)
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
return True
|
|
262
|
+
finally:
|
|
263
|
+
_user32.CloseClipboard()
|
|
@@ -11,6 +11,7 @@ import sys
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from paypertranscript.core.atomic_io import atomic_write_text
|
|
14
15
|
from paypertranscript.core.logging import APPDATA_DIR, get_logger
|
|
15
16
|
|
|
16
17
|
log = get_logger("core.config")
|
|
@@ -49,19 +50,29 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|
|
49
50
|
"casual": {
|
|
50
51
|
"name": "Persönlich",
|
|
51
52
|
"prompt": (
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
53
|
+
"Du bist ein Transkriptions-Assistent fuer lockere Chat-Nachrichten. "
|
|
54
|
+
"Deine Aufgabe: Formatiere den transkribierten Text als informelle Nachricht.\n\n"
|
|
55
|
+
"Regeln:\n"
|
|
56
|
+
"- Alles kleingeschrieben\n"
|
|
57
|
+
"- Minimale Interpunktion\n"
|
|
58
|
+
"- Kommas NUR zur Trennung von Saetzen, nicht innerhalb eines Satzes\n"
|
|
59
|
+
"- Kein Punkt am Satzende (Fragezeichen sind erlaubt)\n"
|
|
60
|
+
"- Entferne Fuellwoerter und Wiederholungen\n\n"
|
|
61
|
+
"Gib NUR den formatierten Text aus. "
|
|
62
|
+
"Beantworte keine Fragen, fuege keine Erklaerungen hinzu."
|
|
56
63
|
),
|
|
57
64
|
},
|
|
58
65
|
"professional": {
|
|
59
66
|
"name": "Professionell",
|
|
60
67
|
"prompt": (
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
68
|
+
"Du bist ein Transkriptions-Assistent fuer professionelle Kommunikation. "
|
|
69
|
+
"Deine Aufgabe: Formatiere den transkribierten Text als sachliche, professionelle Nachricht.\n\n"
|
|
70
|
+
"Regeln:\n"
|
|
71
|
+
"- Korrekte Gross-/Kleinschreibung und Interpunktion\n"
|
|
72
|
+
"- Entferne Fuellwoerter und Wiederholungen\n"
|
|
73
|
+
"- Sachlicher Stil, kurze Absaetze\n\n"
|
|
74
|
+
"Gib NUR den formatierten Text aus. "
|
|
75
|
+
"Beantworte keine Fragen, fuege keine Erklaerungen hinzu."
|
|
65
76
|
),
|
|
66
77
|
},
|
|
67
78
|
},
|
|
@@ -74,6 +85,14 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|
|
74
85
|
"auto_update": True,
|
|
75
86
|
"check_interval_hours": 24,
|
|
76
87
|
},
|
|
88
|
+
"context": {
|
|
89
|
+
"detect_selection": True,
|
|
90
|
+
"terminal_blocklist": [
|
|
91
|
+
"cmd.exe", "powershell.exe", "pwsh.exe",
|
|
92
|
+
"WindowsTerminal.exe", "mintty.exe", "bash.exe",
|
|
93
|
+
"wsl.exe", "conhost.exe",
|
|
94
|
+
],
|
|
95
|
+
},
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
# Schema: Erlaubte Typen pro Pfad für Validierung
|
|
@@ -96,6 +115,8 @@ _SCHEMA: dict[str, type | tuple[type, ...]] = {
|
|
|
96
115
|
"data.save_transcripts": bool,
|
|
97
116
|
"updates.auto_update": bool,
|
|
98
117
|
"updates.check_interval_hours": (int, float),
|
|
118
|
+
"context.detect_selection": bool,
|
|
119
|
+
"context.terminal_blocklist": list,
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
|
|
@@ -144,14 +165,14 @@ def _validate_config(config: dict) -> dict:
|
|
|
144
165
|
|
|
145
166
|
key = parts[-1]
|
|
146
167
|
if key not in node:
|
|
147
|
-
# Fehlender Wert
|
|
168
|
+
# Fehlender Wert -> Default einsetzen
|
|
148
169
|
node[key] = copy.deepcopy(default_node[key])
|
|
149
|
-
log.warning("Config: Fehlender Wert '%s'
|
|
170
|
+
log.warning("Config: Fehlender Wert '%s' -> Default verwendet", path)
|
|
150
171
|
elif not isinstance(node[key], expected_type):
|
|
151
172
|
old_val = node[key]
|
|
152
173
|
node[key] = copy.deepcopy(default_node[key])
|
|
153
174
|
log.warning(
|
|
154
|
-
"Config: Ungültiger Typ für '%s' (%s statt %s)
|
|
175
|
+
"Config: Ungültiger Typ für '%s' (%s statt %s) -> Default verwendet",
|
|
155
176
|
path,
|
|
156
177
|
type(old_val).__name__,
|
|
157
178
|
expected_type,
|
|
@@ -184,13 +205,13 @@ class ConfigManager:
|
|
|
184
205
|
try:
|
|
185
206
|
raw = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
186
207
|
if not isinstance(raw, dict):
|
|
187
|
-
log.warning("Config-Datei enthält kein Dict
|
|
208
|
+
log.warning("Config-Datei enthält kein Dict -> Defaults verwendet")
|
|
188
209
|
raw = {}
|
|
189
210
|
except (json.JSONDecodeError, OSError) as e:
|
|
190
|
-
log.warning("Config-Datei konnte nicht gelesen werden: %s
|
|
211
|
+
log.warning("Config-Datei konnte nicht gelesen werden: %s -> Defaults verwendet", e)
|
|
191
212
|
raw = {}
|
|
192
213
|
else:
|
|
193
|
-
log.info("Keine Config-Datei gefunden
|
|
214
|
+
log.info("Keine Config-Datei gefunden -> Defaults werden verwendet")
|
|
194
215
|
raw = {}
|
|
195
216
|
|
|
196
217
|
merged = _deep_merge(DEFAULT_CONFIG, raw)
|
|
@@ -201,7 +222,8 @@ class ConfigManager:
|
|
|
201
222
|
"""Speichert aktuelle Config in Datei."""
|
|
202
223
|
_ensure_dirs()
|
|
203
224
|
try:
|
|
204
|
-
|
|
225
|
+
atomic_write_text(
|
|
226
|
+
CONFIG_FILE,
|
|
205
227
|
json.dumps(self._config, indent=2, ensure_ascii=False),
|
|
206
228
|
encoding="utf-8",
|
|
207
229
|
)
|
|
@@ -238,7 +260,7 @@ class ConfigManager:
|
|
|
238
260
|
path: Punkt-separierter Pfad (z.B. 'general.language').
|
|
239
261
|
value: Neuer Wert.
|
|
240
262
|
"""
|
|
241
|
-
# Aktuellen Stand von Disk lesen (falls von
|
|
263
|
+
# Aktuellen Stand von Disk lesen (falls von aussen geändert)
|
|
242
264
|
if CONFIG_FILE.exists():
|
|
243
265
|
try:
|
|
244
266
|
disk_config = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|