PayPerTranscript 0.2.8__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.
Files changed (56) hide show
  1. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/PKG-INFO +1 -1
  2. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/PKG-INFO +1 -1
  3. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/__init__.py +1 -1
  4. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/config.py +2 -0
  5. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/cost_tracker.py +18 -7
  6. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/pipeline/transcription.py +1 -0
  7. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/providers/groq_provider.py +15 -4
  8. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/app.py +1 -0
  9. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/constants.py +9 -0
  10. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/pages/home_page.py +2 -2
  11. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/pages/settings_page.py +70 -0
  12. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/pyproject.toml +1 -1
  13. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/LICENSE +0 -0
  14. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/SOURCES.txt +0 -0
  15. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/dependency_links.txt +0 -0
  16. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/entry_points.txt +0 -0
  17. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/requires.txt +0 -0
  18. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/PayPerTranscript.egg-info/top_level.txt +0 -0
  19. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/README.md +0 -0
  20. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/__main__.py +0 -0
  21. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/icons/app.ico +0 -0
  22. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/icons/app.png +0 -0
  23. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/icons/app_big.png +0 -0
  24. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/icons/arrow_down.svg +0 -0
  25. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/icons/tray.png +0 -0
  26. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/icons/tray_green.png +0 -0
  27. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/icons/tray_orange.png +0 -0
  28. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/sounds/start.wav +0 -0
  29. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/sounds/stop.wav +0 -0
  30. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/assets/styles/dark.qss +0 -0
  31. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/__init__.py +0 -0
  32. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/audio_manager.py +0 -0
  33. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/hotkey.py +0 -0
  34. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/logging.py +0 -0
  35. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/paths.py +0 -0
  36. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/recorder.py +0 -0
  37. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/session_logger.py +0 -0
  38. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/text_inserter.py +0 -0
  39. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/updater.py +0 -0
  40. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/core/window_detector.py +0 -0
  41. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/pipeline/__init__.py +0 -0
  42. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/providers/__init__.py +0 -0
  43. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/providers/base.py +0 -0
  44. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/__init__.py +0 -0
  45. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/animated.py +0 -0
  46. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/main_window.py +0 -0
  47. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/overlay.py +0 -0
  48. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/pages/__init__.py +0 -0
  49. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/pages/statistics_page.py +0 -0
  50. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/pages/window_mapping_page.py +0 -0
  51. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/pages/word_list_page.py +0 -0
  52. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/setup_wizard.py +0 -0
  53. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/sidebar.py +0 -0
  54. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/tray.py +0 -0
  55. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/paypertranscript/ui/widgets.py +0 -0
  56. {paypertranscript-0.2.8 → paypertranscript-0.2.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PayPerTranscript
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: Open-Source Voice-to-Text mit Pay-per-Use Pricing
5
5
  Author: PayPerTranscript Contributors
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PayPerTranscript
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: Open-Source Voice-to-Text mit Pay-per-Use Pricing
5
5
  Author: PayPerTranscript Contributors
6
6
  License-Expression: MIT
@@ -1,3 +1,3 @@
1
1
  """PayPerTranscript - Voice-to-Text mit Pay-per-Use Pricing."""
2
2
 
3
- __version__ = "0.2.8"
3
+ __version__ = "0.2.9"
@@ -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,
@@ -6,12 +6,18 @@ Keine I/O, keine Seiteneffekte - einfach testbar.
6
6
 
7
7
  from dataclasses import dataclass
8
8
 
9
- # STT/LLM API-Preise (Stand: 2026-02)
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
- LLM_INPUT_PRICE_PER_M_TOKENS = 0.075 # USD per million input tokens
14
- LLM_OUTPUT_PRICE_PER_M_TOKENS = 0.30 # USD per million output tokens
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(input_tokens: int, output_tokens: int) -> float:
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 * LLM_INPUT_PRICE_PER_M_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,
@@ -165,6 +165,7 @@ class TranscriptionPipeline:
165
165
  audio_duration_seconds=audio_duration,
166
166
  llm_input_tokens=llm_input_tokens,
167
167
  llm_output_tokens=llm_output_tokens,
168
+ llm_model=self._config.get("api.llm_model", ""),
168
169
  )
169
170
 
170
171
  session_data = {
@@ -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
@@ -241,6 +241,7 @@ class PayPerTranscriptApp:
241
241
  config.get("api.provider", "groq"),
242
242
  model=config.get("api.llm_model", "openai/gpt-oss-20b"),
243
243
  api_key=api_key,
244
+ temperature=config.get("api.llm_temperature"),
244
245
  )
245
246
  except ProviderError as e:
246
247
  log.warning(
@@ -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
+ }
@@ -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("stt.model", "whisper-large-v3-turbo")
213
- llm_model = self._config.get("llm.model", "openai/gpt-oss-20b")
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 --
@@ -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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "PayPerTranscript"
7
- version = "0.2.8"
7
+ version = "0.2.9"
8
8
  description = "Open-Source Voice-to-Text mit Pay-per-Use Pricing"
9
9
  license = "MIT"
10
10
  requires-python = ">=3.12"