PayPerTranscript 0.2.7__tar.gz → 0.2.9__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.2.7 → paypertranscript-0.2.9}/PKG-INFO +1 -1
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/PKG-INFO +1 -1
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/SOURCES.txt +2 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/__init__.py +1 -1
- paypertranscript-0.2.9/paypertranscript/assets/icons/tray_green.png +0 -0
- paypertranscript-0.2.9/paypertranscript/assets/icons/tray_orange.png +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/config.py +14 -1
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/cost_tracker.py +18 -7
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/pipeline/transcription.py +1 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/providers/groq_provider.py +15 -4
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/app.py +1 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/constants.py +9 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/main_window.py +64 -16
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/home_page.py +2 -2
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/settings_page.py +70 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/window_mapping_page.py +65 -39
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/word_list_page.py +29 -20
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/tray.py +23 -68
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/pyproject.toml +1 -1
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/LICENSE +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/dependency_links.txt +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/entry_points.txt +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/requires.txt +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/top_level.txt +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/README.md +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/__main__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/assets/icons/app.ico +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/assets/icons/app.png +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/assets/icons/app_big.png +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/assets/icons/arrow_down.svg +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/assets/icons/tray.png +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/assets/sounds/start.wav +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/assets/sounds/stop.wav +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/assets/styles/dark.qss +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/audio_manager.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/hotkey.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/logging.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/paths.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/recorder.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/session_logger.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/text_inserter.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/updater.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/core/window_detector.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/pipeline/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/providers/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/providers/base.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/animated.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/overlay.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/__init__.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/statistics_page.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/setup_wizard.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/sidebar.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/widgets.py +0 -0
- {paypertranscript-0.2.7 → paypertranscript-0.2.9}/setup.cfg +0 -0
|
@@ -14,6 +14,8 @@ paypertranscript/assets/icons/app.png
|
|
|
14
14
|
paypertranscript/assets/icons/app_big.png
|
|
15
15
|
paypertranscript/assets/icons/arrow_down.svg
|
|
16
16
|
paypertranscript/assets/icons/tray.png
|
|
17
|
+
paypertranscript/assets/icons/tray_green.png
|
|
18
|
+
paypertranscript/assets/icons/tray_orange.png
|
|
17
19
|
paypertranscript/assets/sounds/start.wav
|
|
18
20
|
paypertranscript/assets/sounds/stop.wav
|
|
19
21
|
paypertranscript/assets/styles/dark.qss
|
|
Binary file
|
|
Binary file
|
|
@@ -33,6 +33,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|
|
33
33
|
"provider": "groq",
|
|
34
34
|
"stt_model": "whisper-large-v3-turbo",
|
|
35
35
|
"llm_model": "openai/gpt-oss-20b",
|
|
36
|
+
"llm_temperature": 1.0,
|
|
36
37
|
},
|
|
37
38
|
"words": {
|
|
38
39
|
"misspelled_words": [],
|
|
@@ -87,6 +88,7 @@ _SCHEMA: dict[str, type | tuple[type, ...]] = {
|
|
|
87
88
|
"api.provider": str,
|
|
88
89
|
"api.stt_model": str,
|
|
89
90
|
"api.llm_model": str,
|
|
91
|
+
"api.llm_temperature": (int, float),
|
|
90
92
|
"words.misspelled_words": list,
|
|
91
93
|
"formatting.window_mappings": dict,
|
|
92
94
|
"formatting.categories": dict,
|
|
@@ -97,11 +99,22 @@ _SCHEMA: dict[str, type | tuple[type, ...]] = {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
|
|
102
|
+
# Keys whose dict values represent user data collections (not config structure).
|
|
103
|
+
# These are replaced entirely by the user's saved value, not recursively merged,
|
|
104
|
+
# so that deletions of default entries persist across restarts.
|
|
105
|
+
_REPLACE_KEYS = frozenset({"window_mappings", "categories"})
|
|
106
|
+
|
|
107
|
+
|
|
100
108
|
def _deep_merge(base: dict, override: dict) -> dict:
|
|
101
109
|
"""Merge override in base (rekursiv). Gibt neues Dict zurück."""
|
|
102
110
|
result = copy.deepcopy(base)
|
|
103
111
|
for key, value in override.items():
|
|
104
|
-
if
|
|
112
|
+
if (
|
|
113
|
+
key in result
|
|
114
|
+
and isinstance(result[key], dict)
|
|
115
|
+
and isinstance(value, dict)
|
|
116
|
+
and key not in _REPLACE_KEYS
|
|
117
|
+
):
|
|
105
118
|
result[key] = _deep_merge(result[key], value)
|
|
106
119
|
else:
|
|
107
120
|
result[key] = copy.deepcopy(value)
|
|
@@ -6,12 +6,18 @@ Keine I/O, keine Seiteneffekte - einfach testbar.
|
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
|
|
9
|
-
# STT
|
|
9
|
+
# STT API-Preise (Stand: 2026-02)
|
|
10
10
|
STT_PRICE_PER_HOUR_USD = 0.04
|
|
11
11
|
STT_MIN_BILLED_SECONDS = 10 # API-seitiges Minimum-Billing
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# LLM-Preise pro Modell: (Input USD/M Tokens, Output USD/M Tokens)
|
|
14
|
+
LLM_PRICES: dict[str, tuple[float, float]] = {
|
|
15
|
+
"openai/gpt-oss-20b": (0.075, 0.30),
|
|
16
|
+
"openai/gpt-oss-120b": (0.15, 0.60),
|
|
17
|
+
"moonshotai/kimi-k2-instruct-0905": (1.00, 3.00),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_DEFAULT_LLM_PRICES = (0.075, 0.30) # Fallback
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
@dataclass(frozen=True)
|
|
@@ -41,19 +47,22 @@ def calculate_stt_cost(audio_duration_seconds: float) -> tuple[float, float]:
|
|
|
41
47
|
return billed, cost
|
|
42
48
|
|
|
43
49
|
|
|
44
|
-
def calculate_llm_cost(
|
|
50
|
+
def calculate_llm_cost(
|
|
51
|
+
input_tokens: int, output_tokens: int, model: str = "",
|
|
52
|
+
) -> float:
|
|
45
53
|
"""Berechnet LLM-Kosten.
|
|
46
54
|
|
|
47
55
|
Args:
|
|
48
56
|
input_tokens: Anzahl Input-Tokens.
|
|
49
57
|
output_tokens: Anzahl Output-Tokens.
|
|
58
|
+
model: LLM-Modellname fuer modellspezifische Preise.
|
|
50
59
|
|
|
51
60
|
Returns:
|
|
52
61
|
Kosten in USD.
|
|
53
62
|
"""
|
|
63
|
+
input_price, output_price = LLM_PRICES.get(model, _DEFAULT_LLM_PRICES)
|
|
54
64
|
return (
|
|
55
|
-
input_tokens *
|
|
56
|
-
+ output_tokens * LLM_OUTPUT_PRICE_PER_M_TOKENS
|
|
65
|
+
input_tokens * input_price + output_tokens * output_price
|
|
57
66
|
) / 1_000_000
|
|
58
67
|
|
|
59
68
|
|
|
@@ -61,6 +70,7 @@ def calculate_total_cost(
|
|
|
61
70
|
audio_duration_seconds: float,
|
|
62
71
|
llm_input_tokens: int = 0,
|
|
63
72
|
llm_output_tokens: int = 0,
|
|
73
|
+
llm_model: str = "",
|
|
64
74
|
) -> CostResult:
|
|
65
75
|
"""Berechnet Gesamtkosten einer Transkription.
|
|
66
76
|
|
|
@@ -68,12 +78,13 @@ def calculate_total_cost(
|
|
|
68
78
|
audio_duration_seconds: Audio-Dauer in Sekunden.
|
|
69
79
|
llm_input_tokens: LLM Input-Tokens (0 wenn kein LLM).
|
|
70
80
|
llm_output_tokens: LLM Output-Tokens (0 wenn kein LLM).
|
|
81
|
+
llm_model: LLM-Modellname fuer modellspezifische Preise.
|
|
71
82
|
|
|
72
83
|
Returns:
|
|
73
84
|
CostResult mit allen Kosten-Details.
|
|
74
85
|
"""
|
|
75
86
|
billed, stt_cost = calculate_stt_cost(audio_duration_seconds)
|
|
76
|
-
llm_cost = calculate_llm_cost(llm_input_tokens, llm_output_tokens)
|
|
87
|
+
llm_cost = calculate_llm_cost(llm_input_tokens, llm_output_tokens, llm_model)
|
|
77
88
|
return CostResult(
|
|
78
89
|
audio_duration_seconds=audio_duration_seconds,
|
|
79
90
|
billed_seconds=billed,
|
{paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/providers/groq_provider.py
RENAMED
|
@@ -87,14 +87,16 @@ class GroqLLMProvider(AbstractLLMProvider):
|
|
|
87
87
|
self,
|
|
88
88
|
api_key: str | None = None,
|
|
89
89
|
model: str = "openai/gpt-oss-20b",
|
|
90
|
+
temperature: float | None = None,
|
|
90
91
|
) -> None:
|
|
91
92
|
self._model = model
|
|
93
|
+
self._temperature = temperature
|
|
92
94
|
self._last_usage: dict[str, int] | None = None
|
|
93
95
|
try:
|
|
94
96
|
self._client = groq.Groq(api_key=api_key)
|
|
95
97
|
except groq.GroqError as e:
|
|
96
98
|
raise ProviderError(f"Groq-Client konnte nicht erstellt werden: {e}") from e
|
|
97
|
-
log.info("GroqLLMProvider initialisiert (Modell: %s)", self._model)
|
|
99
|
+
log.info("GroqLLMProvider initialisiert (Modell: %s, Temperature: %s)", self._model, self._temperature)
|
|
98
100
|
|
|
99
101
|
@property
|
|
100
102
|
def last_usage(self) -> dict[str, int] | None:
|
|
@@ -106,17 +108,25 @@ class GroqLLMProvider(AbstractLLMProvider):
|
|
|
106
108
|
) -> list[dict[str, str]]:
|
|
107
109
|
return [
|
|
108
110
|
{"role": "system", "content": system_prompt},
|
|
109
|
-
{"role": "user", "content": text},
|
|
111
|
+
{"role": "user", "content": f"<transcript>{text}</transcript>"},
|
|
110
112
|
]
|
|
111
113
|
|
|
114
|
+
def _completion_kwargs(self) -> dict:
|
|
115
|
+
"""Baut gemeinsame kwargs für chat.completions.create."""
|
|
116
|
+
kwargs: dict = {}
|
|
117
|
+
if self._temperature is not None:
|
|
118
|
+
kwargs["temperature"] = self._temperature
|
|
119
|
+
return kwargs
|
|
120
|
+
|
|
112
121
|
def format_text(self, system_prompt: str, text: str) -> str:
|
|
113
|
-
log.info("LLM-Anfrage (non-streaming, Modell: %s)", self._model)
|
|
122
|
+
log.info("LLM-Anfrage (non-streaming, Modell: %s, Temperature: %s)", self._model, self._temperature)
|
|
114
123
|
self._last_usage = None
|
|
115
124
|
try:
|
|
116
125
|
response = self._client.chat.completions.create(
|
|
117
126
|
model=self._model,
|
|
118
127
|
messages=self._build_messages(system_prompt, text),
|
|
119
128
|
stream=False,
|
|
129
|
+
**self._completion_kwargs(),
|
|
120
130
|
)
|
|
121
131
|
except groq.AuthenticationError as e:
|
|
122
132
|
raise ProviderError(f"API-Key ungültig: {e}") from e
|
|
@@ -142,13 +152,14 @@ class GroqLLMProvider(AbstractLLMProvider):
|
|
|
142
152
|
return result
|
|
143
153
|
|
|
144
154
|
def format_text_stream(self, system_prompt: str, text: str) -> Iterator[str]:
|
|
145
|
-
log.info("LLM-Anfrage (streaming, Modell: %s)", self._model)
|
|
155
|
+
log.info("LLM-Anfrage (streaming, Modell: %s, Temperature: %s)", self._model, self._temperature)
|
|
146
156
|
self._last_usage = None
|
|
147
157
|
try:
|
|
148
158
|
stream = self._client.chat.completions.create(
|
|
149
159
|
model=self._model,
|
|
150
160
|
messages=self._build_messages(system_prompt, text),
|
|
151
161
|
stream=True,
|
|
162
|
+
**self._completion_kwargs(),
|
|
152
163
|
)
|
|
153
164
|
except groq.AuthenticationError as e:
|
|
154
165
|
raise ProviderError(f"API-Key ungültig: {e}") from e
|
|
@@ -46,4 +46,13 @@ STT_MODELS: list[str] = [
|
|
|
46
46
|
# LLM-Modelle
|
|
47
47
|
LLM_MODELS: list[str] = [
|
|
48
48
|
"openai/gpt-oss-20b",
|
|
49
|
+
"openai/gpt-oss-120b",
|
|
50
|
+
"moonshotai/kimi-k2-instruct-0905",
|
|
49
51
|
]
|
|
52
|
+
|
|
53
|
+
# LLM-Modell-Metadaten: (Standard-Temperature, Empfohlene Temperature)
|
|
54
|
+
LLM_MODEL_DEFAULTS: dict[str, tuple[float, float]] = {
|
|
55
|
+
"openai/gpt-oss-20b": (1.0, 0.6),
|
|
56
|
+
"openai/gpt-oss-120b": (1.0, 0.6),
|
|
57
|
+
"moonshotai/kimi-k2-instruct-0905": (0.6, 0.4),
|
|
58
|
+
}
|
|
@@ -16,10 +16,6 @@ from paypertranscript.core.logging import get_logger
|
|
|
16
16
|
from paypertranscript.core.paths import get_icons_dir
|
|
17
17
|
from paypertranscript.core.session_logger import SessionLogger
|
|
18
18
|
from paypertranscript.ui.pages.home_page import HomePage
|
|
19
|
-
from paypertranscript.ui.pages.settings_page import SettingsPage
|
|
20
|
-
from paypertranscript.ui.pages.statistics_page import StatisticsPage
|
|
21
|
-
from paypertranscript.ui.pages.window_mapping_page import WindowMappingPage
|
|
22
|
-
from paypertranscript.ui.pages.word_list_page import WordListPage
|
|
23
19
|
from paypertranscript.ui.sidebar import Sidebar
|
|
24
20
|
|
|
25
21
|
log = get_logger("ui.main_window")
|
|
@@ -86,29 +82,81 @@ class MainWindow(QDialog):
|
|
|
86
82
|
self._stack = QStackedWidget()
|
|
87
83
|
self._stack.setStyleSheet("QStackedWidget { background-color: #121218; }")
|
|
88
84
|
|
|
89
|
-
#
|
|
90
|
-
self.
|
|
91
|
-
self.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
# Lazy Page Loading: Nur HomePage sofort erstellen, Rest on-demand.
|
|
86
|
+
self._pages: dict[int, QWidget] = {}
|
|
87
|
+
self._page_factories: dict[int, Callable[[], QWidget]] = {
|
|
88
|
+
1: lambda: self._create_statistics_page(session_logger),
|
|
89
|
+
2: lambda: self._create_settings_page(config, hotkey_listener),
|
|
90
|
+
3: lambda: self._create_word_list_page(config),
|
|
91
|
+
4: lambda: self._create_window_mapping_page(config),
|
|
92
|
+
}
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
self.
|
|
99
|
-
self._stack.addWidget(
|
|
100
|
-
|
|
94
|
+
# HomePage (Index 0) sofort — wird bei jedem Oeffnen angezeigt
|
|
95
|
+
home = HomePage(config, session_logger, get_last_transcription)
|
|
96
|
+
self._pages[0] = home
|
|
97
|
+
self._stack.addWidget(home) # 0
|
|
98
|
+
|
|
99
|
+
# Placeholders fuer restliche Seiten
|
|
100
|
+
for _ in range(1, 5):
|
|
101
|
+
self._stack.addWidget(QWidget())
|
|
101
102
|
|
|
102
103
|
layout.addWidget(self._stack, 1)
|
|
103
104
|
|
|
105
|
+
# -- Lazy Page Factories --
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _create_statistics_page(session_logger: SessionLogger | None) -> QWidget:
|
|
109
|
+
from paypertranscript.ui.pages.statistics_page import StatisticsPage
|
|
110
|
+
|
|
111
|
+
return StatisticsPage(session_logger) if session_logger else QWidget()
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def _create_settings_page(
|
|
115
|
+
config: ConfigManager, hotkey_listener: HotkeyListener | None
|
|
116
|
+
) -> QWidget:
|
|
117
|
+
from paypertranscript.ui.pages.settings_page import SettingsPage
|
|
118
|
+
|
|
119
|
+
return SettingsPage(config, hotkey_listener)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _create_word_list_page(config: ConfigManager) -> QWidget:
|
|
123
|
+
from paypertranscript.ui.pages.word_list_page import WordListPage
|
|
124
|
+
|
|
125
|
+
return WordListPage(config)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _create_window_mapping_page(config: ConfigManager) -> QWidget:
|
|
129
|
+
from paypertranscript.ui.pages.window_mapping_page import WindowMappingPage
|
|
130
|
+
|
|
131
|
+
return WindowMappingPage(config)
|
|
132
|
+
|
|
133
|
+
def _ensure_page(self, index: int) -> None:
|
|
134
|
+
"""Erstellt die Seite beim ersten Zugriff (lazy loading)."""
|
|
135
|
+
if index in self._pages:
|
|
136
|
+
return
|
|
137
|
+
factory = self._page_factories.pop(index, None)
|
|
138
|
+
if factory is None:
|
|
139
|
+
return
|
|
140
|
+
page = factory()
|
|
141
|
+
self._pages[index] = page
|
|
142
|
+
old = self._stack.widget(index)
|
|
143
|
+
self._stack.removeWidget(old)
|
|
144
|
+
self._stack.insertWidget(index, page)
|
|
145
|
+
old.deleteLater()
|
|
146
|
+
log.debug("Seite %d lazy erstellt: %s", index, type(page).__name__)
|
|
147
|
+
|
|
148
|
+
# -- Navigation --
|
|
149
|
+
|
|
104
150
|
def _on_page_changed(self, index: int) -> None:
|
|
105
|
-
"""Wechselt zur angeforderten Seite."""
|
|
151
|
+
"""Wechselt zur angeforderten Seite (lazy creation bei Erstaufruf)."""
|
|
152
|
+
self._ensure_page(index)
|
|
106
153
|
self._stack.setCurrentIndex(index)
|
|
107
154
|
self._sidebar.set_active(index)
|
|
108
155
|
|
|
109
156
|
def navigate_to(self, page_index: int) -> None:
|
|
110
157
|
"""Navigiert zu einer bestimmten Seite und zeigt das Fenster."""
|
|
111
158
|
if 0 <= page_index < self._stack.count():
|
|
159
|
+
self._ensure_page(page_index)
|
|
112
160
|
self._stack.setCurrentIndex(page_index)
|
|
113
161
|
self._sidebar.set_active(page_index)
|
|
114
162
|
self.show()
|
|
@@ -209,8 +209,8 @@ class HomePage(QWidget):
|
|
|
209
209
|
self._hotkey_value.setText(" · ".join(parts))
|
|
210
210
|
|
|
211
211
|
# -- Modelle --
|
|
212
|
-
stt_model = self._config.get("
|
|
213
|
-
llm_model = self._config.get("
|
|
212
|
+
stt_model = self._config.get("api.stt_model", "whisper-large-v3-turbo")
|
|
213
|
+
llm_model = self._config.get("api.llm_model", "openai/gpt-oss-20b")
|
|
214
214
|
self._model_value.setText(f"STT: {stt_model} · LLM: {llm_model}")
|
|
215
215
|
|
|
216
216
|
# -- Sprache + API-Status --
|
{paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/settings_page.py
RENAMED
|
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import (
|
|
|
18
18
|
QPushButton,
|
|
19
19
|
QRadioButton,
|
|
20
20
|
QScrollArea,
|
|
21
|
+
QSlider,
|
|
21
22
|
QSpinBox,
|
|
22
23
|
QVBoxLayout,
|
|
23
24
|
QWidget,
|
|
@@ -29,6 +30,7 @@ from paypertranscript.core.logging import get_logger
|
|
|
29
30
|
from paypertranscript.ui.constants import (
|
|
30
31
|
HOLD_PRESETS,
|
|
31
32
|
LANGUAGES,
|
|
33
|
+
LLM_MODEL_DEFAULTS,
|
|
32
34
|
LLM_MODELS,
|
|
33
35
|
STT_MODELS,
|
|
34
36
|
TOGGLE_PRESETS,
|
|
@@ -221,6 +223,34 @@ class SettingsPage(QWidget):
|
|
|
221
223
|
self._llm_combo.currentTextChanged.connect(self._on_llm_model_changed)
|
|
222
224
|
api_layout.addWidget(self._llm_combo)
|
|
223
225
|
|
|
226
|
+
api_layout.addSpacing(8)
|
|
227
|
+
|
|
228
|
+
# Temperature
|
|
229
|
+
temp_header = QHBoxLayout()
|
|
230
|
+
temp_label = QLabel("LLM Temperature:")
|
|
231
|
+
temp_header.addWidget(temp_label)
|
|
232
|
+
temp_header.addStretch()
|
|
233
|
+
self._temp_value_label = QLabel("Standard")
|
|
234
|
+
self._temp_value_label.setProperty("subheading", True)
|
|
235
|
+
temp_header.addWidget(self._temp_value_label)
|
|
236
|
+
api_layout.addLayout(temp_header)
|
|
237
|
+
|
|
238
|
+
self._temp_slider = QSlider(Qt.Orientation.Horizontal)
|
|
239
|
+
self._temp_slider.setRange(0, 20) # 0..20 → 0.0..2.0 in 0.1-Schritten
|
|
240
|
+
self._temp_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
241
|
+
self._temp_slider.setTickInterval(2)
|
|
242
|
+
self._temp_slider.setSingleStep(1)
|
|
243
|
+
self._temp_slider.setPageStep(2)
|
|
244
|
+
self._temp_slider.valueChanged.connect(self._on_temperature_changed)
|
|
245
|
+
api_layout.addWidget(self._temp_slider)
|
|
246
|
+
|
|
247
|
+
self._temp_hint = QLabel("")
|
|
248
|
+
self._temp_hint.setProperty("subheading", True)
|
|
249
|
+
self._temp_hint.setWordWrap(True)
|
|
250
|
+
api_layout.addWidget(self._temp_hint)
|
|
251
|
+
|
|
252
|
+
api_layout.addSpacing(4)
|
|
253
|
+
|
|
224
254
|
hint = QLabel("Modell-\u00c4nderungen werden nach Neustart wirksam.")
|
|
225
255
|
hint.setProperty("subheading", True)
|
|
226
256
|
api_layout.addWidget(hint)
|
|
@@ -337,6 +367,17 @@ class SettingsPage(QWidget):
|
|
|
337
367
|
if idx >= 0:
|
|
338
368
|
self._llm_combo.setCurrentIndex(idx)
|
|
339
369
|
|
|
370
|
+
llm_temp = self._config.get("api.llm_temperature", 1.0)
|
|
371
|
+
# Fallback fuer alte Configs mit None
|
|
372
|
+
if llm_temp is None:
|
|
373
|
+
model = self._llm_combo.currentText()
|
|
374
|
+
llm_temp = LLM_MODEL_DEFAULTS.get(model, (1.0, 1.0))[0]
|
|
375
|
+
slider_val = round(float(llm_temp) * 10)
|
|
376
|
+
self._temp_slider.setValue(max(0, min(20, slider_val)))
|
|
377
|
+
self._temp_value_label.setText(f"{llm_temp:.1f}")
|
|
378
|
+
|
|
379
|
+
self._update_temp_hint()
|
|
380
|
+
|
|
340
381
|
self._retention_spin.setValue(self._config.get("data.audio_retention_hours", 24))
|
|
341
382
|
self._chk_transcripts.setChecked(self._config.get("data.save_transcripts", False))
|
|
342
383
|
|
|
@@ -485,6 +526,35 @@ class SettingsPage(QWidget):
|
|
|
485
526
|
if self._updating:
|
|
486
527
|
return
|
|
487
528
|
self._config.set("api.llm_model", text)
|
|
529
|
+
# Slider auf den Standard-Wert des neuen Modells setzen
|
|
530
|
+
default_temp = LLM_MODEL_DEFAULTS.get(text, (1.0, 1.0))[0]
|
|
531
|
+
self._updating = True
|
|
532
|
+
slider_val = round(default_temp * 10)
|
|
533
|
+
self._temp_slider.setValue(max(0, min(20, slider_val)))
|
|
534
|
+
self._temp_value_label.setText(f"{default_temp:.1f}")
|
|
535
|
+
self._updating = False
|
|
536
|
+
self._config.set("api.llm_temperature", default_temp)
|
|
537
|
+
self._update_temp_hint()
|
|
538
|
+
|
|
539
|
+
def _on_temperature_changed(self, value: int) -> None:
|
|
540
|
+
if self._updating:
|
|
541
|
+
return
|
|
542
|
+
temp = value / 10.0
|
|
543
|
+
self._temp_value_label.setText(f"{temp:.1f}")
|
|
544
|
+
self._config.set("api.llm_temperature", temp)
|
|
545
|
+
|
|
546
|
+
def _update_temp_hint(self) -> None:
|
|
547
|
+
"""Zeigt modellspezifischen Temperature-Hinweis."""
|
|
548
|
+
model = self._llm_combo.currentText()
|
|
549
|
+
meta = LLM_MODEL_DEFAULTS.get(model)
|
|
550
|
+
if meta:
|
|
551
|
+
default_temp, recommended_temp = meta
|
|
552
|
+
self._temp_hint.setText(
|
|
553
|
+
f"Standard: {default_temp:.1f} | Empfohlen: {recommended_temp:.1f}"
|
|
554
|
+
)
|
|
555
|
+
self._temp_hint.setVisible(True)
|
|
556
|
+
else:
|
|
557
|
+
self._temp_hint.setVisible(False)
|
|
488
558
|
|
|
489
559
|
# -- Callbacks: Daten & Updates --
|
|
490
560
|
|
{paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/window_mapping_page.py
RENAMED
|
@@ -166,30 +166,18 @@ class _MappingItemDelegate(QStyledItemDelegate):
|
|
|
166
166
|
option: QStyleOptionViewItem,
|
|
167
167
|
index: QModelIndex,
|
|
168
168
|
) -> bool:
|
|
169
|
-
|
|
169
|
+
# Click handling is done via viewport event filter in WindowMappingPage
|
|
170
|
+
# (editorEvent does not receive MouseButtonRelease with default edit triggers)
|
|
171
|
+
if event.type() == QEvent.Type.MouseMove:
|
|
170
172
|
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if over_icon:
|
|
180
|
-
view.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
181
|
-
else:
|
|
182
|
-
view.unsetCursor()
|
|
183
|
-
return False
|
|
184
|
-
|
|
185
|
-
# MouseButtonRelease
|
|
186
|
-
if x >= delete_left:
|
|
187
|
-
self._on_delete(index.row())
|
|
188
|
-
return True
|
|
189
|
-
if x >= edit_left:
|
|
190
|
-
self._on_edit(index.row())
|
|
191
|
-
return True
|
|
192
|
-
|
|
173
|
+
over_icon = mouse.position().x() >= option.rect.right() - self._ICONS_TOTAL
|
|
174
|
+
view = option.widget
|
|
175
|
+
if view is not None:
|
|
176
|
+
if over_icon:
|
|
177
|
+
view.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
178
|
+
else:
|
|
179
|
+
view.unsetCursor()
|
|
180
|
+
return False
|
|
193
181
|
return super().editorEvent(event, model, option, index)
|
|
194
182
|
|
|
195
183
|
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
|
@@ -263,24 +251,17 @@ class _CategoryItemDelegate(QStyledItemDelegate):
|
|
|
263
251
|
option: QStyleOptionViewItem,
|
|
264
252
|
index: QModelIndex,
|
|
265
253
|
) -> bool:
|
|
266
|
-
|
|
254
|
+
# Click handling is done via viewport event filter in WindowMappingPage
|
|
255
|
+
if event.type() == QEvent.Type.MouseMove:
|
|
267
256
|
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
268
257
|
over_icon = mouse.position().x() >= option.rect.right() - self._ICON_SIZE
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
view.unsetCursor()
|
|
277
|
-
return False
|
|
278
|
-
|
|
279
|
-
# MouseButtonRelease
|
|
280
|
-
if over_icon:
|
|
281
|
-
self._on_delete(index.row())
|
|
282
|
-
return True
|
|
283
|
-
|
|
258
|
+
view = option.widget
|
|
259
|
+
if view is not None:
|
|
260
|
+
if over_icon:
|
|
261
|
+
view.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
262
|
+
else:
|
|
263
|
+
view.unsetCursor()
|
|
264
|
+
return False
|
|
284
265
|
return super().editorEvent(event, model, option, index)
|
|
285
266
|
|
|
286
267
|
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
|
@@ -321,6 +302,11 @@ class WindowMappingPage(QWidget):
|
|
|
321
302
|
self._tabs.addTab(self._create_categories_tab(), "Kategorien")
|
|
322
303
|
layout.addWidget(self._tabs, 1)
|
|
323
304
|
|
|
305
|
+
# Viewport event filters for reliable icon click handling
|
|
306
|
+
# (editorEvent does not receive MouseButtonRelease with default edit triggers)
|
|
307
|
+
self._mapping_list.viewport().installEventFilter(self)
|
|
308
|
+
self._cat_list.viewport().installEventFilter(self)
|
|
309
|
+
|
|
324
310
|
# -- Tab: Zuordnungen --
|
|
325
311
|
|
|
326
312
|
def _create_mappings_tab(self) -> QWidget:
|
|
@@ -438,6 +424,46 @@ class WindowMappingPage(QWidget):
|
|
|
438
424
|
|
|
439
425
|
return tab
|
|
440
426
|
|
|
427
|
+
# -- Viewport event filter: icon clicks --
|
|
428
|
+
|
|
429
|
+
def eventFilter(self, watched: object, event: QEvent) -> bool:
|
|
430
|
+
"""Intercept mouse clicks on delegate icons (edit/delete)."""
|
|
431
|
+
if event.type() == QEvent.Type.MouseButtonRelease:
|
|
432
|
+
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
433
|
+
if watched is self._mapping_list.viewport():
|
|
434
|
+
return self._handle_mapping_icon_click(mouse)
|
|
435
|
+
if watched is self._cat_list.viewport():
|
|
436
|
+
return self._handle_category_icon_click(mouse)
|
|
437
|
+
return super().eventFilter(watched, event)
|
|
438
|
+
|
|
439
|
+
def _handle_mapping_icon_click(self, event: QMouseEvent) -> bool:
|
|
440
|
+
pos = event.position().toPoint()
|
|
441
|
+
index = self._mapping_list.indexAt(pos)
|
|
442
|
+
if not index.isValid():
|
|
443
|
+
return False
|
|
444
|
+
rect = self._mapping_list.visualRect(index)
|
|
445
|
+
x = event.position().x()
|
|
446
|
+
delete_left = rect.right() - _MappingItemDelegate._ICON_SIZE
|
|
447
|
+
edit_left = rect.right() - _MappingItemDelegate._ICONS_TOTAL
|
|
448
|
+
if x >= delete_left:
|
|
449
|
+
self._remove_mapping_at(index.row())
|
|
450
|
+
return True
|
|
451
|
+
if x >= edit_left:
|
|
452
|
+
self._edit_mapping_at(index.row())
|
|
453
|
+
return True
|
|
454
|
+
return False
|
|
455
|
+
|
|
456
|
+
def _handle_category_icon_click(self, event: QMouseEvent) -> bool:
|
|
457
|
+
pos = event.position().toPoint()
|
|
458
|
+
index = self._cat_list.indexAt(pos)
|
|
459
|
+
if not index.isValid():
|
|
460
|
+
return False
|
|
461
|
+
rect = self._cat_list.visualRect(index)
|
|
462
|
+
if event.position().x() >= rect.right() - _CategoryItemDelegate._ICON_SIZE:
|
|
463
|
+
self._delete_category_at(index.row())
|
|
464
|
+
return True
|
|
465
|
+
return False
|
|
466
|
+
|
|
441
467
|
# -- Daten laden --
|
|
442
468
|
|
|
443
469
|
def _reload_all(self) -> None:
|
{paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/word_list_page.py
RENAMED
|
@@ -110,27 +110,17 @@ class _WordItemDelegate(QStyledItemDelegate):
|
|
|
110
110
|
option: QStyleOptionViewItem,
|
|
111
111
|
index: QModelIndex,
|
|
112
112
|
) -> bool:
|
|
113
|
-
"""
|
|
114
|
-
if event.type()
|
|
113
|
+
"""Cursor change on hover; click handling via viewport event filter."""
|
|
114
|
+
if event.type() == QEvent.Type.MouseMove:
|
|
115
115
|
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
else:
|
|
125
|
-
view.unsetCursor()
|
|
126
|
-
return False
|
|
127
|
-
|
|
128
|
-
# MouseButtonRelease
|
|
129
|
-
if over_icon:
|
|
130
|
-
view = option.widget
|
|
131
|
-
if view is not None:
|
|
132
|
-
view.edit(index)
|
|
133
|
-
return True
|
|
116
|
+
over_icon = mouse.position().x() >= option.rect.right() - self._EDIT_WIDTH
|
|
117
|
+
view = option.widget
|
|
118
|
+
if view is not None:
|
|
119
|
+
if over_icon:
|
|
120
|
+
view.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
121
|
+
else:
|
|
122
|
+
view.unsetCursor()
|
|
123
|
+
return False
|
|
134
124
|
return super().editorEvent(event, model, option, index)
|
|
135
125
|
|
|
136
126
|
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
|
@@ -200,6 +190,9 @@ class WordListPage(QWidget):
|
|
|
200
190
|
self._list.itemChanged.connect(self._on_word_edited)
|
|
201
191
|
layout.addWidget(self._list, 1)
|
|
202
192
|
|
|
193
|
+
# Viewport event filter for reliable edit icon click handling
|
|
194
|
+
self._list.viewport().installEventFilter(self)
|
|
195
|
+
|
|
203
196
|
# Entfernen + Token-Counter
|
|
204
197
|
action_row = QHBoxLayout()
|
|
205
198
|
btn_remove = QPushButton("Entfernen")
|
|
@@ -213,6 +206,22 @@ class WordListPage(QWidget):
|
|
|
213
206
|
action_row.addWidget(self._counter_label)
|
|
214
207
|
layout.addLayout(action_row)
|
|
215
208
|
|
|
209
|
+
def eventFilter(self, watched: object, event: QEvent) -> bool:
|
|
210
|
+
"""Intercept mouse clicks on the edit (pencil) icon."""
|
|
211
|
+
if (
|
|
212
|
+
event.type() == QEvent.Type.MouseButtonRelease
|
|
213
|
+
and watched is self._list.viewport()
|
|
214
|
+
):
|
|
215
|
+
mouse: QMouseEvent = event # type: ignore[assignment]
|
|
216
|
+
pos = mouse.position().toPoint()
|
|
217
|
+
index = self._list.indexAt(pos)
|
|
218
|
+
if index.isValid():
|
|
219
|
+
rect = self._list.visualRect(index)
|
|
220
|
+
if mouse.position().x() >= rect.right() - _WordItemDelegate._EDIT_WIDTH:
|
|
221
|
+
self._list.edit(index)
|
|
222
|
+
return True
|
|
223
|
+
return super().eventFilter(watched, event)
|
|
224
|
+
|
|
216
225
|
def _load_words(self) -> None:
|
|
217
226
|
self._updating = True
|
|
218
227
|
words = self._config.get("words.misspelled_words", [])
|
|
@@ -5,8 +5,8 @@ QSystemTrayIcon mit Kontextmenue und Icon-Zustandswechsel.
|
|
|
5
5
|
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
|
|
8
|
-
from PySide6.QtCore import
|
|
9
|
-
from PySide6.QtGui import
|
|
8
|
+
from PySide6.QtCore import Qt, Slot
|
|
9
|
+
from PySide6.QtGui import QIcon, QPixmap
|
|
10
10
|
from PySide6.QtWidgets import (
|
|
11
11
|
QHBoxLayout,
|
|
12
12
|
QLabel,
|
|
@@ -27,84 +27,39 @@ from paypertranscript.core.session_logger import SessionLogger
|
|
|
27
27
|
|
|
28
28
|
log = get_logger("ui.tray")
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
# Icon-Dateinamen fuer die drei Zustaende
|
|
31
|
+
_ICON_FILES: dict[str, str] = {
|
|
32
|
+
"idle": "tray.png",
|
|
33
|
+
"recording": "tray_orange.png",
|
|
34
|
+
"processing": "tray_green.png",
|
|
35
|
+
}
|
|
33
36
|
|
|
34
|
-
# Dot-Groesse relativ zum Icon
|
|
35
|
-
_DOT_RADIUS_RATIO = 0.30 # 30% der Icon-Groesse (~60% Durchmesser)
|
|
36
|
-
_DOT_BORDER_RATIO = 0.04 # Dunkler Rand um den Dot
|
|
37
37
|
|
|
38
|
+
def _load_icon(filename: str, size: int = 64) -> QIcon:
|
|
39
|
+
"""Laedt ein Tray-Icon aus den Assets.
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
Sucht nach tray.png in den Assets. Fallback: weisser Kreis.
|
|
41
|
+
Args:
|
|
42
|
+
filename: Dateiname im icons-Ordner.
|
|
43
|
+
size: Zielgroesse in Pixeln.
|
|
43
44
|
"""
|
|
44
|
-
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
if not
|
|
48
|
-
|
|
45
|
+
path = get_icons_dir() / filename
|
|
46
|
+
if path.exists():
|
|
47
|
+
pixmap = QPixmap(str(path))
|
|
48
|
+
if not pixmap.isNull():
|
|
49
|
+
scaled = pixmap.scaled(
|
|
49
50
|
size, size,
|
|
50
51
|
Qt.AspectRatioMode.KeepAspectRatio,
|
|
51
52
|
Qt.TransformationMode.SmoothTransformation,
|
|
52
53
|
)
|
|
54
|
+
return QIcon(scaled)
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
pixmap.fill(QColor(0, 0, 0, 0))
|
|
57
|
-
painter = QPainter(pixmap)
|
|
58
|
-
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
59
|
-
painter.setBrush(QColor("#ffffff"))
|
|
60
|
-
painter.setPen(QColor(0, 0, 0, 0))
|
|
61
|
-
painter.drawEllipse(4, 4, size - 8, size - 8)
|
|
62
|
-
painter.end()
|
|
63
|
-
return pixmap
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _create_icon_with_dot(base: QPixmap, dot_color: str | None = None) -> QIcon:
|
|
67
|
-
"""Erstellt ein Tray-Icon aus dem Basis-Logo, optional mit Status-Dot.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
base: Basis-Pixmap (Logo).
|
|
71
|
-
dot_color: Hex-Farbe fuer den Status-Dot. None = kein Dot (idle).
|
|
72
|
-
"""
|
|
73
|
-
pixmap = base.copy()
|
|
74
|
-
|
|
75
|
-
if dot_color is not None:
|
|
76
|
-
size = pixmap.width()
|
|
77
|
-
dot_r = size * _DOT_RADIUS_RATIO
|
|
78
|
-
border_w = max(1.0, size * _DOT_BORDER_RATIO)
|
|
79
|
-
|
|
80
|
-
# Position: unten rechts mit kleinem Abstand zum Rand
|
|
81
|
-
cx = size - dot_r - border_w - 1
|
|
82
|
-
cy = size - dot_r - border_w - 1
|
|
83
|
-
|
|
84
|
-
painter = QPainter(pixmap)
|
|
85
|
-
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
86
|
-
|
|
87
|
-
# Dunkler Rand (hebt den Dot vom Logo ab)
|
|
88
|
-
painter.setPen(QPen(QColor(18, 18, 24, 200), border_w))
|
|
89
|
-
painter.setBrush(QColor(dot_color))
|
|
90
|
-
painter.drawEllipse(QRectF(cx - dot_r, cy - dot_r, dot_r * 2, dot_r * 2))
|
|
91
|
-
|
|
92
|
-
painter.end()
|
|
93
|
-
|
|
94
|
-
return QIcon(pixmap)
|
|
56
|
+
log.warning("Tray-Icon nicht gefunden: %s", path)
|
|
57
|
+
return QIcon()
|
|
95
58
|
|
|
96
59
|
|
|
97
60
|
def create_tray_icons() -> dict[str, QIcon]:
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
Idle: Logo pur. Recording: Logo + Emerald-Dot. Processing: Logo + Grau-Dot.
|
|
101
|
-
"""
|
|
102
|
-
base = _load_base_pixmap(64)
|
|
103
|
-
return {
|
|
104
|
-
"idle": _create_icon_with_dot(base),
|
|
105
|
-
"recording": _create_icon_with_dot(base, _DOT_RECORDING),
|
|
106
|
-
"processing": _create_icon_with_dot(base, _DOT_PROCESSING),
|
|
107
|
-
}
|
|
61
|
+
"""Laedt alle Tray-Icons (idle, recording, processing) aus PNG-Dateien."""
|
|
62
|
+
return {state: _load_icon(fname) for state, fname in _ICON_FILES.items()}
|
|
108
63
|
|
|
109
64
|
|
|
110
65
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/assets/icons/arrow_down.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{paypertranscript-0.2.7 → paypertranscript-0.2.9}/paypertranscript/ui/pages/statistics_page.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|