aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md13
-rw-r--r--config.py24
-rw-r--r--gui.py114
-rw-r--r--pyproject.toml1
-rw-r--r--server.py190
-rw-r--r--uv.lock107
6 files changed, 410 insertions, 39 deletions
diff --git a/README.md b/README.md
index 9216fd0..5c7803e 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,25 @@
# auto-live-tl
-A basic LOCAL translation backend that listens to an audio sink via PCM and runs translation via faster-whisper
+A basic LOCAL translation backend that listens to an audio sink via PCM and runs translation via faster-whisper. Also supports the option to use `qwen2.5-7b-instruct` to format/clean-up subtitles based on sliding window context.
Translations and trascriptions are transformers based, inaccuracies and hallucinations will occur.
+
+# Setup
> It's highly recommended that you run this with a GPU, running with CPU is possible but inference will be very slow outside of using tiny models (which compromise accuracy)
>
> For this, you will need to install a Nvidia CUDA 12 toolkit. I am running with [CUDA Toolkit 12.9](https://developer.nvidia.com/cuda-12-9-0-download-archive)
+```
+uv sync
+uv run server.py
+```
+A GUI is available for configuration
+
`server.py` serves a backend for translating incoming audio data. It expects some other client to hit the `/events` endpoint to fetch the translated data.
+
+# Clients:
+
`youtube-subtitle.user.js` is one such example client that can fetch data from this endpoint and render it beneath a YouTube video. You can install it as a userscript.
<img width="1210" height="109" alt="image" src="https://github.com/user-attachments/assets/2bffde45-bc61-4d63-b779-b7a8cd183bc0" />
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..1101085
--- /dev/null
+++ b/config.py
@@ -0,0 +1,24 @@
+_SYSTEM_PROMPT: str = (
+ "You are a live-stream subtitle deduplicator and sentence completer.\n"
+ "The speech-to-text engine uses a ROLLING AUDIO WINDOW, so every new "
+ "raw input re-transcribes the recent past verbatim. Most of the raw "
+ "input is old text already shown to the viewer.\n\n"
+ "ALREADY SHOWN lists every subtitle line already displayed.\n\n"
+ "YOUR JOB:\n"
+ "Extract only the genuinely NEW spoken content from the raw input, "
+ "while ensuring the output forms clean, complete, natural sentences.\n\n"
+ "STRICT RULES:\n"
+ " 1. NEVER repeat text that is already fully covered by ALREADY SHOWN.\n"
+ " 2. Prefer returning COMPLETE SENTENCES instead of cut-off fragments.\n"
+ " If the new content starts mid-sentence, use the rolling context "
+ " from the raw input to complete the full sentence naturally.\n"
+ " 3. Do NOT paraphrase, summarize, or invent meaning — preserve the "
+ " speaker's original wording as closely as possible.\n"
+ " 4. You may use overlapping words from the raw input only when needed "
+ " to reconstruct a full readable sentence, but avoid unnecessary repetition.\n"
+ " 5. Fix punctuation, capitalization, and obvious transcript artifacts "
+ " (like duplicated partial words) for readability.\n"
+ " 6. If the entire raw input is already covered by ALREADY SHOWN, "
+ " output an empty string and nothing else.\n"
+ " 7. Output ONLY the final subtitle text. No labels, no explanations."
+)
diff --git a/gui.py b/gui.py
index aece888..7425567 100644
--- a/gui.py
+++ b/gui.py
@@ -20,16 +20,21 @@ def select_settings(
return settings.get(key, default_settings.get(key, fallback))
root = tk.Tk()
- root.title("Audio & Whisper Settings")
+ root.title("Settings")
root.resizable(False, False)
- frame = ttk.Frame(root, padding=10)
- frame.pack(fill="both", expand=True)
- frame.columnconfigure(1, weight=1)
+ notebook = ttk.Notebook(root)
+ notebook.pack(fill="both", expand=True, padx=10, pady=(10, 0))
- def add_row(row: int, label_text: str, widget: tk.Widget) -> None:
- label = ttk.Label(frame, text=label_text)
- label.grid(row=row, column=0, sticky="w", pady=4)
+ # ------------------------------------------------------------------ #
+ # Tab 1 – Whisper #
+ # ------------------------------------------------------------------ #
+ whisper_tab = ttk.Frame(notebook, padding=10)
+ whisper_tab.columnconfigure(1, weight=1)
+ notebook.add(whisper_tab, text="Whisper")
+
+ def add_row(parent: ttk.Frame, row: int, label_text: str, widget: tk.Widget) -> None:
+ ttk.Label(parent, text=label_text).grid(row=row, column=0, sticky="w", pady=4, padx=(0, 12))
widget.grid(row=row, column=1, sticky="ew", pady=4)
device_options = [
@@ -37,70 +42,92 @@ def select_settings(
for idx, dev in input_devices
]
device_names = [dev["name"] for _idx, dev in input_devices]
- device_combo = ttk.Combobox(frame, values=device_options, state="readonly", width=60)
-
+ device_combo = ttk.Combobox(whisper_tab, values=device_options, state="readonly", width=60)
default_device_name = get_value("audio_device_name", "")
if default_device_name in device_names:
device_combo.current(device_names.index(default_device_name))
else:
device_combo.current(0)
- add_row(0, "Audio input device:", device_combo)
+ add_row(whisper_tab, 0, "Audio input device:", device_combo)
model_var = tk.StringVar(value=get_value("model_name", "medium"))
- model_combo = ttk.Combobox(frame, values=list(model_choices), textvariable=model_var)
+ model_combo = ttk.Combobox(whisper_tab, values=list(model_choices), textvariable=model_var)
model_combo.set(model_var.get())
- add_row(1, "Model:", model_combo)
+ add_row(whisper_tab, 1, "Model:", model_combo)
device_type_var = tk.StringVar(value=get_value("device", "cpu"))
device_type_combo = ttk.Combobox(
- frame, values=list(device_choices), textvariable=device_type_var, state="readonly"
+ whisper_tab, values=list(device_choices), textvariable=device_type_var, state="readonly"
)
device_type_combo.set(device_type_var.get())
- add_row(2, "Compute device:", device_type_combo)
+ add_row(whisper_tab, 2, "Compute device:", device_type_combo)
compute_type_var = tk.StringVar(value=get_value("compute_type", "int8"))
- compute_type_combo = ttk.Combobox(
- frame, values=list(compute_choices), textvariable=compute_type_var
- )
+ compute_type_combo = ttk.Combobox(whisper_tab, values=list(compute_choices), textvariable=compute_type_var)
compute_type_combo.set(compute_type_var.get())
- add_row(3, "Compute type:", compute_type_combo)
+ add_row(whisper_tab, 3, "Compute type:", compute_type_combo)
task_var = tk.StringVar(value=get_value("task", "translate"))
- task_combo = ttk.Combobox(frame, values=list(task_choices), textvariable=task_var, state="readonly")
+ task_combo = ttk.Combobox(whisper_tab, values=list(task_choices), textvariable=task_var, state="readonly")
task_combo.set(task_var.get())
- add_row(4, "Task:", task_combo)
+ add_row(whisper_tab, 4, "Task:", task_combo)
beam_size_var = tk.StringVar(value=str(get_value("beam_size", 3)))
- beam_entry = ttk.Entry(frame, textvariable=beam_size_var, width=10)
- add_row(5, "Beam size:", beam_entry)
+ add_row(whisper_tab, 5, "Beam size:", ttk.Entry(whisper_tab, textvariable=beam_size_var, width=10))
language_var = tk.StringVar(value=get_value("language", ""))
- language_entry = ttk.Entry(frame, textvariable=language_var)
- add_row(6, "Language (optional):", language_entry)
+ add_row(whisper_tab, 6, "Language (optional):", ttk.Entry(whisper_tab, textvariable=language_var))
context_seconds_var = tk.StringVar(value=str(get_value("context_seconds", 10)))
- context_entry = ttk.Entry(frame, textvariable=context_seconds_var, width=10)
- add_row(7, "Context seconds:", context_entry)
+ add_row(whisper_tab, 7, "Context seconds:", ttk.Entry(whisper_tab, textvariable=context_seconds_var, width=10))
update_interval_var = tk.StringVar(value=str(get_value("update_interval_seconds", 2)))
- update_interval_entry = ttk.Entry(frame, textvariable=update_interval_var, width=10)
- add_row(8, "Update interval (s):", update_interval_entry)
+ add_row(whisper_tab, 8, "Update interval (s):", ttk.Entry(whisper_tab, textvariable=update_interval_var, width=10))
+
+ # ------------------------------------------------------------------ #
+ # Tab 2 – Ollama #
+ # ------------------------------------------------------------------ #
+ ollama_tab = ttk.Frame(notebook, padding=10)
+ ollama_tab.columnconfigure(1, weight=1)
+ notebook.add(ollama_tab, text="Ollama")
+
+ use_ollama_cleanup_var = tk.BooleanVar(value=get_value("use_ollama_cleanup", True))
+ add_row(ollama_tab, 0, "LLM subtitle cleanup:", ttk.Checkbutton(ollama_tab, variable=use_ollama_cleanup_var))
+
+ ollama_device_var = tk.StringVar(value=get_value("ollama_device", "CPU"))
+ ollama_device_combo = ttk.Combobox(
+ ollama_tab, values=["CPU", "GPU"], textvariable=ollama_device_var, state="readonly", width=10
+ )
+ ollama_device_combo.set(ollama_device_var.get())
+ add_row(ollama_tab, 1, "Ollama compute:", ollama_device_combo)
- button_frame = ttk.Frame(root, padding=(10, 0, 10, 10))
+ ollama_context_var = tk.StringVar(value=str(get_value("ollama_context_window", 6)))
+ add_row(ollama_tab, 2, "Context window (segments):", ttk.Entry(ollama_tab, textvariable=ollama_context_var, width=10))
+
+ ollama_batch_var = tk.StringVar(value=str(get_value("ollama_raw_batch_size", 3)))
+ add_row(ollama_tab, 3, "Batch size (lines per LLM call):", ttk.Entry(ollama_tab, textvariable=ollama_batch_var, width=10))
+
+ # ------------------------------------------------------------------ #
+ # Buttons #
+ # ------------------------------------------------------------------ #
+ button_frame = ttk.Frame(root, padding=(10, 6, 10, 10))
button_frame.pack(fill="x")
selected_settings: Dict[str, Any] = {}
def on_ok() -> None:
nonlocal selected_settings
+
selection = device_combo.current()
if selection < 0:
messagebox.showwarning("Select a device", "Please select an audio input device.")
return
+
model_name = model_var.get().strip()
if not model_name:
messagebox.showwarning("Model required", "Please select or enter a model name.")
return
+
try:
beam_size = int(beam_size_var.get().strip())
if beam_size <= 0:
@@ -108,6 +135,7 @@ def select_settings(
except ValueError:
messagebox.showwarning("Invalid beam size", "Beam size must be a positive integer.")
return
+
try:
context_seconds = float(context_seconds_var.get().strip())
if context_seconds <= 0:
@@ -115,15 +143,31 @@ def select_settings(
except ValueError:
messagebox.showwarning("Invalid context seconds", "Context seconds must be a positive number.")
return
+
try:
update_interval_seconds = float(update_interval_var.get().strip())
if update_interval_seconds <= 0:
raise ValueError
except ValueError:
- messagebox.showwarning(
- "Invalid update interval", "Update interval must be a positive number."
- )
+ messagebox.showwarning("Invalid update interval", "Update interval must be a positive number.")
+ return
+
+ try:
+ ollama_context_window = int(ollama_context_var.get().strip())
+ if ollama_context_window <= 0:
+ raise ValueError
+ except ValueError:
+ messagebox.showwarning("Invalid context window", "Context window must be a positive integer.")
return
+
+ try:
+ ollama_raw_batch_size = int(ollama_batch_var.get().strip())
+ if ollama_raw_batch_size <= 0:
+ raise ValueError
+ except ValueError:
+ messagebox.showwarning("Invalid batch size", "Batch size must be a positive integer.")
+ return
+
selected_settings = {
"audio_device_name": device_names[selection],
"model_name": model_name,
@@ -134,6 +178,10 @@ def select_settings(
"language": language_var.get().strip(),
"context_seconds": context_seconds,
"update_interval_seconds": update_interval_seconds,
+ "use_ollama_cleanup": use_ollama_cleanup_var.get(),
+ "ollama_device": ollama_device_var.get(),
+ "ollama_context_window": ollama_context_window,
+ "ollama_raw_batch_size": ollama_raw_batch_size,
}
root.quit()
@@ -185,4 +233,4 @@ def prompt_input_sample_rate(device_index: int, common_rates: Iterable[int] | No
parent=root,
)
finally:
- root.destroy() \ No newline at end of file
+ root.destroy()
diff --git a/pyproject.toml b/pyproject.toml
index 37c0cd5..0f28a93 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,5 +8,6 @@ dependencies = [
"faster-whisper>=1.2.1",
"flask>=3.1.3",
"flask-cors>=6.0.2",
+ "ollama>=0.6.1",
"sounddevice>=0.5.5",
]
diff --git a/server.py b/server.py
index 35916b7..6831495 100644
--- a/server.py
+++ b/server.py
@@ -3,14 +3,20 @@ import threading
import json
import queue
import os
-from typing import Any, Dict, Optional, Set, List, Iterator
+from collections import Counter, deque
+import re
+from typing import Any, Deque, Dict, Optional, Set, List, Iterator
from flask import Flask
from flask_cors import CORS
+import ollama as _ollama
+from ollama import chat
+from ollama import ChatResponse
import numpy as np
import sounddevice as sd
from faster_whisper import WhisperModel
from gui import select_settings, prompt_input_sample_rate
from routes import register_routes
+from config import _SYSTEM_PROMPT
TARGET_SAMPLE_RATE: int = 16000
CAPTURE_SAMPLE_RATE: int = 0
@@ -20,6 +26,12 @@ PROCESS_INTERVAL_SECONDS: float = 2
SSE_EVENT_SUBTITLE: str = "subtitle"
SSE_KEEPALIVE_SECONDS: int = 15
+USE_OLLAMA_CLEANUP: bool = True
+OLLAMA_MODEL: str = "qwen2.5:7b-instruct"
+OLLAMA_CONTEXT_WINDOW: int = 6 # number of recent cleaned segments kept as context
+OLLAMA_OPTIONS: Dict[str, Any] = {"num_gpu": 1}
+RAW_BATCH_SIZE: int = 2 # accumulate this many raw Whisper lines before calling the LLM
+
SETTINGS_PATH: str = os.path.join(os.path.dirname(__file__), "settings.json")
DEFAULT_SETTINGS: Dict[str, Any] = {
@@ -32,6 +44,10 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
"language": "",
"context_seconds": 10,
"update_interval_seconds": 2,
+ "use_ollama_cleanup": True,
+ "ollama_device": "GPU",
+ "ollama_context_window": 5,
+ "ollama_raw_batch_size": 2,
}
MODEL_CHOICES: List[str] = ["tiny", "base", "small", "medium", "large-v2", "large-v3", "distil-large-v3"]
@@ -54,6 +70,12 @@ SERVER_PORT: int = 5000
app: Flask = Flask(__name__)
CORS(app)
+# OLLAMA stuff
+llm_input_queue: queue.Queue = queue.Queue(maxsize=1)
+subtitle_context: Deque[str] = deque(maxlen=OLLAMA_CONTEXT_WINDOW) # sliding window context
+subtitle_context_lock: threading.Lock = threading.Lock()
+_raw_batch: List[str] = []
+_raw_batch_lock: threading.Lock = threading.Lock()
def resample_audio(audio_np: np.ndarray, src_rate: int, dst_rate: int) -> np.ndarray:
if src_rate == dst_rate:
@@ -90,18 +112,163 @@ def save_settings(settings: Dict[str, Any]) -> None:
except OSError as exc:
print(f"Failed to save settings: {exc}")
+def cleanup_subtitle_with_ollama(raw_text: str, context: List[str]) -> Optional[str]:
+ if context:
+ context_block = "\n".join(f"- {seg}" for seg in context)
+ else:
+ context_block = "(none yet)"
+
+ user_message = (
+ f"ALREADY SHOWN:\n{context_block}\n\n"
+ "RAW INPUT (multiple consecutive transcriptions of the same rolling window — "
+ f"deduplicate and extract only the genuinely new spoken content as one subtitle):\n{raw_text}"
+ )
+
+ try:
+ response: ChatResponse = chat(
+ model=OLLAMA_MODEL,
+ messages=[
+ {"role": "system", "content": _SYSTEM_PROMPT},
+ {"role": "user", "content": user_message},
+ ],
+ options=OLLAMA_OPTIONS,
+ )
+ return response.message.content.strip()
+ except Exception as exc:
+ print(f"⚠️ OLLAMA cleanup error: {exc}")
+ return None
+
+
+def ensure_ollama_ready() -> None:
+ try:
+ local = _ollama.list()
+ except Exception as exc:
+ raise RuntimeError(
+ f"Cannot reach Ollama — is the server running? ({exc})"
+ ) from exc
+ model_names: List[str] = [m.model for m in local.models]
+ if not any(name.startswith(OLLAMA_MODEL) for name in model_names):
+ print(f" '{OLLAMA_MODEL}' not found locally — pulling (this may take a while) ...")
+ try:
+ _ollama.pull(OLLAMA_MODEL)
+ print(" Pull complete.")
+ except Exception as exc:
+ raise RuntimeError(f"Failed to pull model '{OLLAMA_MODEL}': {exc}") from exc
+ else:
+ print(f" Model found locally.")
+ print(" Warming up model, almost done ...")
+ try:
+ chat(
+ model=OLLAMA_MODEL,
+ messages=[{"role": "user", "content": "Ready?"}],
+ options=OLLAMA_OPTIONS,
+ )
+ print(" ✅ Ollama is ready.")
+ except Exception as exc:
+ raise RuntimeError(f"Ollama warm-up failed: {exc}") from exc
+
+_LLM_EMPTY_SENTINELS: frozenset = frozenset({
+ "empty string", "empty", "(empty)", "[empty]",
+ "(empty string)", "[empty string]", "(none)", "none", "n/a",
+})
+
+
+def normalize_llm_output(text: str) -> str:
+ if text.strip().lower().rstrip(".") in _LLM_EMPTY_SENTINELS:
+ return ""
+ return text
+
+
+def is_hallucination(text: str) -> bool:
+ words = text.split()
+ if not words:
+ return False
+ max_expected = int(BUFFER_SECONDS * 4.5)
+ if len(words) > max_expected:
+ print(f"🔴 Hallucination (too long: {len(words)} words > {max_expected}): {text[:60]!r}")
+ return True
+ clean = [re.sub(r"[^\w']+", "", w).lower() for w in words]
+ clean = [w for w in clean if w]
+ for n in (2, 3):
+ if len(clean) < n * 3:
+ continue
+ ngrams = [" ".join(clean[i : i + n]) for i in range(len(clean) - n + 1)]
+ top, count = Counter(ngrams).most_common(1)[0]
+ if count >= 3:
+ print(f"🔴 Hallucination (\'{top}\' x{count}): {text[:60]!r}")
+ return True
+ top, count = Counter(clean).most_common(1)[0]
+ if count >= 4 and count / len(clean) > 0.40:
+ print(f"🔴 Hallucination (\'{top}\' x{count}, {count/len(clean):.0%}): {text[:60]!r}")
+ return True
+ return False
+
+
+def llm_processing_loop() -> None:
+ print(f"LLM cleanup thread started (model={OLLAMA_MODEL})")
+ while True:
+ try:
+ raw_text: str = llm_input_queue.get(timeout=1)
+ except queue.Empty:
+ continue
+
+ with subtitle_context_lock:
+ context = list(subtitle_context)
+
+ cleaned: Optional[str] = cleanup_subtitle_with_ollama(raw_text, context)
+
+ if cleaned is None:
+ cleaned = raw_text
+ else:
+ cleaned = normalize_llm_output(cleaned)
+
+ if cleaned:
+ with subtitle_context_lock:
+ subtitle_context.append(cleaned)
+ print(f"🔵 (cleaned) {cleaned}")
+ broadcast_subtitle(cleaned)
+ else:
+ print("🟡 (LLM: no new content)")
+
def run_whisper(audio_np: np.ndarray) -> str:
transcribe_kwargs: Dict[str, Any] = {"task": WHISPER_TASK, "beam_size": WHISPER_BEAM_SIZE}
if WHISPER_LANGUAGE:
transcribe_kwargs["language"] = WHISPER_LANGUAGE
- # model is expected to be initialized in main()
assert model is not None, "Whisper model is not initialized"
segments, _info = model.transcribe(audio_np, **transcribe_kwargs)
text = " ".join(seg.text for seg in segments).strip()
- if text:
- print("🟢", text)
+ if not text:
+ return text
+
+ print(f"🟢 (raw) {text}")
+
+ if is_hallucination(text):
+ return text
+
+ if USE_OLLAMA_CLEANUP:
+ with _raw_batch_lock:
+ _raw_batch.append(text)
+ if len(_raw_batch) >= RAW_BATCH_SIZE:
+ batch_text = "\n".join(_raw_batch)
+ _raw_batch.clear()
+ else:
+ batch_text = None
+ if batch_text is not None:
+ try:
+ llm_input_queue.put_nowait(batch_text)
+ except queue.Full:
+ try:
+ llm_input_queue.get_nowait()
+ except queue.Empty:
+ pass
+ try:
+ llm_input_queue.put_nowait(batch_text)
+ except queue.Full:
+ pass
+ else:
broadcast_subtitle(text)
+
return text
@@ -225,7 +392,8 @@ def select_input_sample_rate(device_index: int, preferred_rate: int) -> int:
def main() -> None:
global CAPTURE_SAMPLE_RATE, MAX_SAMPLES, model, WHISPER_TASK, WHISPER_BEAM_SIZE, WHISPER_LANGUAGE
- global BUFFER_SECONDS, PROCESS_INTERVAL_SECONDS
+ global BUFFER_SECONDS, PROCESS_INTERVAL_SECONDS, USE_OLLAMA_CLEANUP
+ global OLLAMA_CONTEXT_WINDOW, RAW_BATCH_SIZE, subtitle_context
start_subtitle_server()
settings: Dict[str, Any] = load_settings()
@@ -242,6 +410,16 @@ def main() -> None:
)
save_settings(settings)
+ USE_OLLAMA_CLEANUP = bool(settings.get("use_ollama_cleanup", True))
+ OLLAMA_OPTIONS["num_gpu"] = 0 if settings.get("ollama_device", "CPU").upper() == "CPU" else 1
+ OLLAMA_CONTEXT_WINDOW = int(settings.get("ollama_context_window", 6))
+ subtitle_context = deque(maxlen=OLLAMA_CONTEXT_WINDOW)
+ RAW_BATCH_SIZE = int(settings.get("ollama_raw_batch_size", 3))
+ if USE_OLLAMA_CLEANUP:
+ ensure_ollama_ready()
+ llm_thread = threading.Thread(target=llm_processing_loop, daemon=True)
+ llm_thread.start()
+
device_name: str = settings.get("audio_device_name", "")
matched_index: Optional[int] = None
for idx, dev in enumerate(devices):
@@ -277,6 +455,8 @@ def main() -> None:
print(f"Model: {model_name} | task={WHISPER_TASK} | beam_size={WHISPER_BEAM_SIZE}")
print(f"Compute: device={whisper_device} | compute_type={compute_type}")
print(f"Capture sample rate: {CAPTURE_SAMPLE_RATE} Hz (resampling to {TARGET_SAMPLE_RATE} Hz)")
+ print(f"Ollama cleanup: {'enabled' if USE_OLLAMA_CLEANUP else 'disabled'} (model={OLLAMA_MODEL})")
+
processing_thread = threading.Thread(target=processing_loop, daemon=True)
processing_thread.start()
with sd.InputStream(
diff --git a/uv.lock b/uv.lock
index 37d8686..b882fe0 100644
--- a/uv.lock
+++ b/uv.lock
@@ -12,6 +12,15 @@ wheels = [
]
[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
name = "anyio"
version = "4.13.0"
source = { registry = "https://pypi.org/simple" }
@@ -31,6 +40,7 @@ dependencies = [
{ name = "faster-whisper" },
{ name = "flask" },
{ name = "flask-cors" },
+ { name = "ollama" },
{ name = "sounddevice" },
]
@@ -39,6 +49,7 @@ requires-dist = [
{ name = "faster-whisper", specifier = ">=1.2.1" },
{ name = "flask", specifier = ">=3.1.3" },
{ name = "flask-cors", specifier = ">=6.0.2" },
+ { name = "ollama", specifier = ">=0.6.1" },
{ name = "sounddevice", specifier = ">=0.5.5" },
]
@@ -501,6 +512,19 @@ wheels = [
]
[[package]]
+name = "ollama"
+version = "0.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" },
+]
+
+[[package]]
name = "onnxruntime"
version = "1.24.4"
source = { registry = "https://pypi.org/simple" }
@@ -562,6 +586,77 @@ wheels = [
]
[[package]]
+name = "pydantic"
+version = "2.13.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" },
+ { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" },
+ { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" },
+ { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" },
+ { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" },
+ { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" },
+ { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" },
+ { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" },
+ { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" },
+ { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" },
+ { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" },
+ { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" },
+ { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" },
+ { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" },
+ { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" },
+]
+
+[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
@@ -728,6 +823,18 @@ wheels = [
]
[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
name = "werkzeug"
version = "3.1.8"
source = { registry = "https://pypi.org/simple" }
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage