From e7a3aa6123b42cd2e42b2eabb3f20295285ca743 Mon Sep 17 00:00:00 2001 From: James Juniper <217263268+jjuniper-dev@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:37:29 -0400 Subject: [PATCH 1/5] voxcpm phone assistant bridge --- .gitignore | 1 + README.md | 16 +- app.py | 10 +- lora_ft_webui.py | 10 +- pyproject.toml | 2 +- src/voxcpm/phone_assistant.py | 1004 +++++++++++++++++++++++++++++++++ tests/test_phone_assistant.py | 37 ++ uv.lock | 454 --------------- 8 files changed, 1076 insertions(+), 458 deletions(-) create mode 100644 src/voxcpm/phone_assistant.py create mode 100644 tests/test_phone_assistant.py diff --git a/.gitignore b/.gitignore index f7fa9812..25971f25 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ voxcpm.egg-info .DS_Store ./pretrained_models/ app_local.py +.voxcpm-phone-profiles/ diff --git a/README.md b/README.md index 0b31c85e..e15fb062 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,20 @@ python app.py --device auto Supported values are `auto`, `cpu`, `mps`, `cuda`, and `cuda:N`. On Apple Silicon Macs, `auto` uses MPS when available. +### Phone Assistant Bridge + +For a mobile-first assistant flow with voice input, webhook-backed replies, and VoxCPM voice cloning: + +```bash +voxcpm-assistant --port 8809 +``` + +Use a clean reference sample plus an exact transcript for the highest-quality cloned voice. If you connect a webhook, the app POSTs the user message and conversation history and expects a reply in plain text or in a `reply` field. +If you want phone microphone dictation, install the optional ASR dependency separately with `pip install funasr`. +You can also save a named voice enrollment once and reuse it later from the app's profile panel. +By default, the assistant loads the saved `reddit-female` profile if it exists. +To wire in your PCA backend automatically, set `PCA_BACKEND_URL` plus optional `PCA_BACKEND_TOKEN`, `PCA_ASSISTANT_CONTEXT`, and `PCA_BACKEND_MODE=auto|openai|custom` before launching. + ### 🚢 Production Deployment (Nano-vLLM) For high-throughput serving, use **[Nano-vLLM-VoxCPM](https://github.com/a710128/nanovllm-voxcpm)** — a dedicated inference engine built on Nano-vLLM with concurrent request support and an async API. @@ -627,4 +641,4 @@ VoxCPM model weights and code are open-sourced under the [Apache-2.0](LICENSE) l ## ⭐ Star History -[Star History Chart](https://star-history.com/#OpenBMB/VoxCPM&Date) \ No newline at end of file +[Star History Chart](https://star-history.com/#OpenBMB/VoxCPM&Date) diff --git a/app.py b/app.py index 95eac945..dd889c73 100644 --- a/app.py +++ b/app.py @@ -5,9 +5,13 @@ import numpy as np import gradio as gr from typing import Optional, Tuple -from funasr import AutoModel from pathlib import Path +try: + from funasr import AutoModel +except ImportError: # Optional dependency for ASR-assisted prompt transcription + AutoModel = None + os.environ["TOKENIZERS_PARALLELISM"] = "false" import voxcpm @@ -245,6 +249,10 @@ def get_or_load_voxcpm(self) -> voxcpm.VoxCPM: return self.voxcpm_model def get_or_load_asr_model(self) -> AutoModel: + if AutoModel is None: + raise RuntimeError( + "funasr is not installed. Install the optional ASR dependency to enable prompt transcription." + ) if self.asr_model is not None: return self.asr_model logger.info( diff --git a/lora_ft_webui.py b/lora_ft_webui.py index e4a68228..6bd03283 100644 --- a/lora_ft_webui.py +++ b/lora_ft_webui.py @@ -22,7 +22,11 @@ from voxcpm.core import VoxCPM from voxcpm.model.voxcpm import LoRAConfig import numpy as np -from funasr import AutoModel + +try: + from funasr import AutoModel +except ImportError: # Optional dependency for ASR-assisted prompt transcription + AutoModel = None # --- Localization --- LANG_DICT = { @@ -121,6 +125,10 @@ def detect_sample_rate(pretrained_path: str) -> Optional[int]: def get_or_load_asr_model(): global asr_model + if AutoModel is None: + raise RuntimeError( + "funasr is not installed. Install the optional ASR dependency to enable prompt transcription." + ) if asr_model is None: print("Loading ASR model (SenseVoiceSmall)...", file=sys.stderr) device = "cuda:0" if torch.cuda.is_available() else "cpu" diff --git a/pyproject.toml b/pyproject.toml index 45ae2ee3..b532da3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,6 @@ dependencies = [ "soundfile", "librosa", "matplotlib", - "funasr", "spaces", "argbind", "safetensors" @@ -62,6 +61,7 @@ dev = [ [project.scripts] voxcpm = "voxcpm.cli:main" +voxcpm-assistant = "voxcpm.phone_assistant:main" [project.urls] Homepage = "https://github.com/OpenBMB/VoxCPM" diff --git a/src/voxcpm/phone_assistant.py b/src/voxcpm/phone_assistant.py new file mode 100644 index 00000000..29856538 --- /dev/null +++ b/src/voxcpm/phone_assistant.py @@ -0,0 +1,1004 @@ +from __future__ import annotations + +import argparse +import json +import logging +import os +import sys +import shutil +from pathlib import Path +from typing import Any, Optional + +import gradio as gr +import numpy as np +import requests + +try: + import torch +except ImportError: # pragma: no cover - torch is required by the project + torch = None + +try: + from funasr import AutoModel +except ImportError: # Optional dependency for speech-to-text input / transcripts + AutoModel = None + +from voxcpm.core import VoxCPM +from voxcpm.model.utils import resolve_runtime_device + +os.environ["TOKENIZERS_PARALLELISM"] = "false" + +logger = logging.getLogger(__name__) +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], +) + +DEFAULT_MODEL_ID = "openbmb/VoxCPM2" +DEFAULT_ASR_MODEL_ID = "iic/SenseVoiceSmall" +PROFILE_DIR = Path.cwd() / ".voxcpm-phone-profiles" +DEFAULT_BACKEND_URL = os.environ.get("PCA_BACKEND_URL", "").strip() +DEFAULT_BACKEND_TOKEN = os.environ.get("PCA_BACKEND_TOKEN", "").strip() +DEFAULT_BACKEND_CONTEXT = os.environ.get("PCA_ASSISTANT_CONTEXT", "").strip() +DEFAULT_BACKEND_MODE = os.environ.get("PCA_BACKEND_MODE", "auto").strip().lower() + +APP_THEME = gr.themes.Soft( + primary_hue="cyan", + secondary_hue="orange", + neutral_hue="slate", + font=[gr.themes.GoogleFont("Space Grotesk"), "IBM Plex Sans", "sans-serif"], +) + +APP_CSS = """ +:root { + --bg0: #07111e; + --bg1: #0d1727; + --bg2: rgba(12, 20, 34, 0.78); + --stroke: rgba(170, 210, 255, 0.16); + --text: #edf4ff; + --muted: #9fb0c8; + --accent: #65f0ff; + --accent2: #ffba6e; + --shadow: 0 24px 80px rgba(0, 0, 0, 0.35); +} + +body, .gradio-container { + background: + radial-gradient(circle at top left, rgba(101, 240, 255, 0.20), transparent 34%), + radial-gradient(circle at 85% 15%, rgba(255, 186, 110, 0.14), transparent 24%), + linear-gradient(180deg, #07111e 0%, #091422 50%, #050a12 100%) !important; + color: var(--text) !important; +} + +.assistant-shell { + max-width: 1180px; + margin: 0 auto; + padding: 18px 16px 28px; +} + +.assistant-hero { + border: 1px solid var(--stroke); + border-radius: 28px; + padding: 22px 22px 20px; + background: linear-gradient(180deg, rgba(19, 29, 48, 0.95), rgba(11, 18, 31, 0.88)); + box-shadow: var(--shadow); + margin-bottom: 16px; +} + +.assistant-kicker { + letter-spacing: 0.16em; + text-transform: uppercase; + font-size: 0.72rem; + color: var(--accent); + margin-bottom: 10px; +} + +.assistant-hero h1 { + margin: 0; + font-size: clamp(2rem, 5vw, 3.75rem); + line-height: 0.98; + letter-spacing: -0.05em; +} + +.assistant-hero p { + margin: 12px 0 0; + color: var(--muted); + max-width: 70ch; + font-size: 1rem; +} + +.assistant-grid { + display: grid; + grid-template-columns: 1.35fr 1fr; + gap: 16px; + align-items: start; +} + +.panel { + border: 1px solid var(--stroke); + border-radius: 24px; + background: var(--bg2); + backdrop-filter: blur(18px); + box-shadow: var(--shadow); + padding: 16px; +} + +.panel-title { + color: var(--text); + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.18em; + margin-bottom: 10px; +} + +.gradio-container .gr-button.primary, +.gradio-container .gr-button.gr-button-primary { + background: linear-gradient(135deg, var(--accent), #7bdbff) !important; + color: #03131b !important; + border: 0 !important; + font-weight: 700 !important; + box-shadow: 0 12px 30px rgba(101, 240, 255, 0.22); +} + +.gradio-container .gr-button.secondary { + border: 1px solid var(--stroke) !important; + background: rgba(255, 255, 255, 0.03) !important; +} + +.gradio-container textarea, +.gradio-container input, +.gradio-container .wrap { + color: var(--text) !important; +} + +.gradio-container .tab-nav { + gap: 8px; +} + +.gradio-container .chatbot { + min-height: 460px; +} + +.gradio-container .gr-box, +.gradio-container .gr-panel { + border-color: var(--stroke) !important; +} + +@media (max-width: 980px) { + .assistant-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .assistant-shell { + padding: 10px 8px 22px; + } + + .assistant-hero { + padding: 18px 16px 16px; + border-radius: 22px; + } + + .panel { + border-radius: 20px; + padding: 14px; + } +} +""" + + +def _clean_text(value: Optional[str]) -> str: + return (value or "").strip() + + +def _normalize_spaces(value: str) -> str: + return " ".join(value.replace("\n", " ").split()) + + +def _format_history(history: list[tuple[str, str]]) -> list[tuple[str, str]]: + return history or [] + + +def _build_final_text(text: str, control: str) -> str: + text = _normalize_spaces(_clean_text(text)) + control = _normalize_spaces(_clean_text(control)) + return f"({control}){text}" if control else text + + +def _extract_reply_text(payload: Any) -> str: + if isinstance(payload, str): + return payload.strip() + + if isinstance(payload, dict): + for key in ("reply", "text", "message", "content", "answer"): + value = payload.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + + choices = payload.get("choices") + if isinstance(choices, list) and choices: + first = choices[0] + if isinstance(first, dict): + message = first.get("message") + if isinstance(message, dict): + content = message.get("content") + if isinstance(content, str) and content.strip(): + return content.strip() + content = first.get("text") + if isinstance(content, str) and content.strip(): + return content.strip() + + data = payload.get("data") + if isinstance(data, str) and data.strip(): + return data.strip() + + return "" + + +def _build_backend_messages( + user_message: str, + history: list[tuple[str, str]], + assistant_context: str, +) -> list[dict[str, str]]: + messages: list[dict[str, str]] = [] + assistant_context = _clean_text(assistant_context) + if assistant_context: + messages.append({"role": "system", "content": assistant_context}) + + for user_turn, assistant_turn in history: + if _clean_text(user_turn): + messages.append({"role": "user", "content": user_turn}) + if _clean_text(assistant_turn): + messages.append({"role": "assistant", "content": assistant_turn}) + + messages.append({"role": "user", "content": user_message}) + return messages + + +def _profile_path(profile_name: str) -> Path: + safe_name = "".join(ch for ch in profile_name.strip().lower() if ch.isalnum() or ch in ("-", "_")) + if not safe_name: + safe_name = "default" + return PROFILE_DIR / f"{safe_name}.json" + + +def _copy_reference_audio(source_path: str, profile_name: str) -> str: + PROFILE_DIR.mkdir(parents=True, exist_ok=True) + source = Path(source_path) + suffix = source.suffix or ".wav" + target_name = "".join(ch for ch in profile_name.strip().lower() if ch.isalnum() or ch in ("-", "_")) or "default" + target = PROFILE_DIR / f"{target_name}{suffix}" + shutil.copy2(source, target) + return str(target) + + +def _load_profile(profile_name: str) -> dict[str, Any]: + path = _profile_path(profile_name) + if not path.exists(): + raise FileNotFoundError(f"Voice profile not found: {profile_name}") + return json.loads(path.read_text(encoding="utf-8")) + + +def _save_profile(profile_name: str, payload: dict[str, Any]) -> str: + PROFILE_DIR.mkdir(parents=True, exist_ok=True) + path = _profile_path(profile_name) + path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8") + return str(path) + + +class PhoneAssistantRuntime: + def __init__( + self, + model_id: str = DEFAULT_MODEL_ID, + device: str = "auto", + load_denoiser: bool = True, + optimize: bool = True, + zipenhancer_model_path: str | None = None, + ) -> None: + self._model_id = model_id + self.device = resolve_runtime_device(device, "cuda") + self.optimize = optimize and self.device.startswith("cuda") + self.load_denoiser = load_denoiser + self.zipenhancer_model_path = zipenhancer_model_path + self._model: Optional[VoxCPM] = None + self._asr_model: Optional[Any] = None + self._asr_device = "cuda:0" if self.device.startswith("cuda") else "cpu" + + def model(self) -> VoxCPM: + if self._model is not None: + return self._model + + logger.info("Loading VoxCPM model: %s", self._model_id) + self._model = VoxCPM.from_pretrained( + hf_model_id=self._model_id, + load_denoiser=self.load_denoiser, + zipenhancer_model_id=self.zipenhancer_model_path + if self.zipenhancer_model_path + else None, + optimize=self.optimize, + device=self.device, + ) + logger.info("VoxCPM loaded successfully.") + return self._model + + def asr_model(self): + if AutoModel is None: + raise RuntimeError( + "Speech-to-text is not installed. Type your message manually or install the optional ASR dependency." + ) + if self._asr_model is not None: + return self._asr_model + + logger.info("Loading ASR model: %s on %s", DEFAULT_ASR_MODEL_ID, self._asr_device) + self._asr_model = AutoModel( + model=DEFAULT_ASR_MODEL_ID, + disable_update=True, + log_level="ERROR", + device=self._asr_device, + ) + return self._asr_model + + def transcribe(self, audio_path: Optional[str]) -> str: + if not audio_path: + return "" + if not Path(audio_path).exists(): + raise FileNotFoundError(f"Audio file does not exist: {audio_path}") + + result = self.asr_model().generate( + input=audio_path, + language="auto", + use_itn=True, + ) + + if not result: + return "" + transcript = result[0].get("text", "") + if not isinstance(transcript, str): + return "" + return transcript.split("|>")[-1].strip() + + def synthesize( + self, + *, + text: str, + reference_audio: Optional[str], + reference_transcript: Optional[str], + control: Optional[str], + normalize: bool, + denoise: bool, + cfg_value: float, + inference_timesteps: int, + auto_transcribe_reference: bool, + ) -> tuple[int, np.ndarray, str]: + model = self.model() + + target_text = _build_final_text(text, control or "") + reference_audio = _clean_text(reference_audio) or None + reference_transcript = _clean_text(reference_transcript) or None + + if reference_audio and not reference_transcript and auto_transcribe_reference: + try: + reference_transcript = self.transcribe(reference_audio) + except Exception as exc: # pragma: no cover - depends on optional ASR + logger.warning("Reference transcription failed: %s", exc) + + kwargs: dict[str, Any] = dict( + text=target_text, + cfg_value=float(cfg_value), + inference_timesteps=int(inference_timesteps), + normalize=bool(normalize), + denoise=bool(denoise and reference_audio), + ) + + if reference_audio: + kwargs["reference_wav_path"] = reference_audio + if reference_transcript: + kwargs["prompt_wav_path"] = reference_audio + kwargs["prompt_text"] = reference_transcript + + logger.info("Generating audio: cfg=%s steps=%s reference=%s", cfg_value, inference_timesteps, bool(reference_audio)) + wav = model.generate(**kwargs) + return model.tts_model.sample_rate, wav, reference_transcript or "" + + def request_reply( + self, + *, + backend_url: str, + backend_token: str, + user_message: str, + history: list[tuple[str, str]], + assistant_context: str, + backend_mode: str = "auto", + timeout: int = 30, + ) -> str: + backend_url = _clean_text(backend_url) + if not backend_url: + raise ValueError("No backend URL configured.") + + backend_mode = _clean_text(backend_mode).lower() or "auto" + openai_compatible = backend_mode == "openai" or ( + backend_mode == "auto" and "/v1/chat/completions" in backend_url + ) + + if openai_compatible: + payload = { + "model": "pca", + "messages": _build_backend_messages(user_message, history, assistant_context), + "temperature": 0.4, + "stream": False, + } + else: + history_payload: list[dict[str, str]] = [] + for user_turn, assistant_turn in history: + history_payload.append({"role": "user", "content": user_turn}) + history_payload.append({"role": "assistant", "content": assistant_turn}) + + payload = { + "message": user_message, + "history": history_payload, + "assistant_context": assistant_context, + "device": self.device, + } + + headers = {"Content-Type": "application/json"} + if backend_token: + headers["Authorization"] = f"Bearer {backend_token.strip()}" + + response = requests.post(backend_url, json=payload, headers=headers, timeout=timeout) + response.raise_for_status() + + reply = "" + content_type = response.headers.get("content-type", "") + if "application/json" in content_type.lower(): + try: + reply = _extract_reply_text(response.json()) + except Exception: + reply = response.text.strip() + else: + reply = response.text.strip() + + reply = _clean_text(reply) + if not reply: + raise ValueError("Webhook returned an empty reply.") + return reply + + +def _history_to_chatbot(history: list[tuple[str, str]]) -> list[tuple[str, str]]: + return _format_history(history) + + +def build_interface(runtime: PhoneAssistantRuntime, default_profile_data: dict[str, Any] | None = None): + default_profile_data = default_profile_data or {} + backend_url_default = _clean_text(os.environ.get("PCA_BACKEND_URL", DEFAULT_BACKEND_URL)) + backend_token_default = _clean_text(os.environ.get("PCA_BACKEND_TOKEN", DEFAULT_BACKEND_TOKEN)) + backend_context_default = _clean_text(os.environ.get("PCA_ASSISTANT_CONTEXT", DEFAULT_BACKEND_CONTEXT)) + backend_mode_default = _clean_text(os.environ.get("PCA_BACKEND_MODE", DEFAULT_BACKEND_MODE)).lower() or "auto" + if backend_mode_default not in {"auto", "openai", "custom"}: + backend_mode_default = "auto" + + def _merge_profile_inputs( + profile_data: dict[str, Any], + reference_audio: Optional[str], + reference_transcript: str, + control_text: str, + ) -> tuple[Optional[str], str, str]: + profile_data = profile_data or {} + + effective_audio = _clean_text(reference_audio) or _clean_text(profile_data.get("reference_audio")) + effective_transcript = _clean_text(reference_transcript) or _clean_text(profile_data.get("reference_transcript")) + effective_control = _clean_text(control_text) or _clean_text(profile_data.get("control_text")) + return ( + effective_audio or None, + effective_transcript, + effective_control, + ) + + def _profile_summary(profile_data: dict[str, Any]) -> str: + if not profile_data: + return "No saved profile loaded." + name = profile_data.get("name", "default") + audio = "yes" if profile_data.get("reference_audio") else "no" + transcript = "yes" if profile_data.get("reference_transcript") else "no" + return f"Loaded profile: {name} | audio: {audio} | transcript: {transcript}" + + def save_voice_profile( + profile_name: str, + reference_audio: Optional[str], + reference_transcript: str, + control_text: str, + auto_transcribe_reference: bool, + ): + profile_name = _clean_text(profile_name) or "default" + if not reference_audio: + raise ValueError("Upload a reference voice sample before saving the profile.") + + transcript = _clean_text(reference_transcript) + if not transcript and auto_transcribe_reference: + transcript = runtime.transcribe(reference_audio) + + stored_audio = _copy_reference_audio(reference_audio, profile_name) + payload = { + "name": profile_name, + "reference_audio": stored_audio, + "reference_transcript": transcript, + "control_text": _clean_text(control_text), + } + saved_path = _save_profile(profile_name, payload) + payload["profile_path"] = saved_path + return payload, _profile_summary(payload), stored_audio, transcript, payload.get("control_text", "") + + def load_voice_profile(profile_name: str): + profile_name = _clean_text(profile_name) or "default" + payload = _load_profile(profile_name) + return ( + payload, + payload.get("reference_audio", ""), + payload.get("reference_transcript", ""), + payload.get("control_text", ""), + _profile_summary(payload), + ) + + def clear_voice_profile(): + return {}, "", "", "", "No saved profile loaded." + + def prepare_turn( + user_audio: Optional[str], + user_text: str, + assistant_reply: str, + backend_url: str, + backend_token: str, + assistant_context: str, + backend_mode: str, + reference_audio: Optional[str], + reference_transcript: str, + control_text: str, + normalize_text: bool, + denoise_reference: bool, + auto_transcribe_reference: bool, + cfg_value: float, + inference_timesteps: int, + history: list[tuple[str, str]], + profile_data: dict[str, Any], + ): + history = _history_to_chatbot(history) + user_text_clean = _clean_text(user_text) + + if not user_text_clean and user_audio: + if AutoModel is None: + raise RuntimeError( + "No text message provided and speech-to-text is unavailable. Type the message manually or install the optional ASR dependency." + ) + user_text_clean = runtime.transcribe(user_audio) + + user_text_clean = _normalize_spaces(user_text_clean) + if not user_text_clean: + raise ValueError("Provide a text message or record one with the phone microphone.") + + if backend_url.strip(): + assistant_reply = runtime.request_reply( + backend_url=backend_url, + backend_token=backend_token, + user_message=user_text_clean, + history=history, + assistant_context=assistant_context, + backend_mode=backend_mode, + ) + else: + assistant_reply = _clean_text(assistant_reply) + if not assistant_reply: + raise ValueError("Type the assistant reply, or configure a backend URL.") + + reference_audio, reference_transcript, control_text = _merge_profile_inputs( + profile_data, + reference_audio, + reference_transcript, + control_text, + ) + + sample_rate, wav, transcript = runtime.synthesize( + text=assistant_reply, + reference_audio=reference_audio, + reference_transcript=reference_transcript, + control=control_text, + normalize=normalize_text, + denoise=denoise_reference, + cfg_value=cfg_value, + inference_timesteps=inference_timesteps, + auto_transcribe_reference=auto_transcribe_reference, + ) + + if reference_audio and not reference_transcript and transcript: + reference_transcript = transcript + + history = history + [(f"You: {user_text_clean}", f"PCA: {assistant_reply}")] + status = "Generated cloned speech." + if reference_audio and transcript: + status = "Generated cloned speech with transcript-guided cloning." + + return ( + history, + user_text_clean, + assistant_reply, + reference_transcript, + (sample_rate, wav), + status, + ) + + def speak_reply( + assistant_reply: str, + reference_audio: Optional[str], + reference_transcript: str, + control_text: str, + profile_data: dict[str, Any], + normalize_text: bool, + denoise_reference: bool, + auto_transcribe_reference: bool, + cfg_value: float, + inference_timesteps: int, + ): + assistant_reply = _clean_text(assistant_reply) + if not assistant_reply: + raise ValueError("Type a reply to speak first.") + + reference_audio, reference_transcript, control_text = _merge_profile_inputs( + profile_data, + reference_audio, + reference_transcript, + control_text, + ) + + sample_rate, wav, transcript = runtime.synthesize( + text=assistant_reply, + reference_audio=reference_audio, + reference_transcript=reference_transcript, + control=control_text, + normalize=normalize_text, + denoise=denoise_reference, + cfg_value=cfg_value, + inference_timesteps=inference_timesteps, + auto_transcribe_reference=auto_transcribe_reference, + ) + + return (sample_rate, wav), transcript or reference_transcript, "Playback updated." + + def clear_state(profile_data: dict[str, Any]): + return [], "", "", "", None, "Ready.", profile_data + + with gr.Blocks(theme=APP_THEME, css=APP_CSS, fill_height=True) as demo: + profile_state = gr.State(default_profile_data) + with gr.Column(elem_classes=["assistant-shell"]): + gr.HTML( + """ +
+
VoxCPM Phone Assistant
+

Cloned voice for your mobile assistant.

+

+ Record a message from your phone, send it to your assistant or webhook, + and hear the reply in the cloned voice from your reference samples. + Best results come from a clean 10-30 second clip with an exact transcript. +

+
+ """ + ) + + with gr.Row(elem_classes=["assistant-grid"]): + with gr.Column(elem_classes=["panel"]): + gr.Markdown("### Conversation") + conversation = gr.Chatbot( + label="Conversation", + height=460, + bubble_full_width=False, + show_copy_button=True, + ) + + user_text = gr.Textbox( + label="Your message", + placeholder="Type a message, or record one with the phone microphone below.", + lines=3, + ) + user_audio = gr.Audio( + label="Voice input", + sources=["upload", "microphone"], + type="filepath", + ) + + with gr.Row(): + send_btn = gr.Button("Send & Speak", variant="primary") + speak_btn = gr.Button("Speak reply", variant="secondary") + clear_btn = gr.Button("Clear", variant="secondary") + + assistant_reply = gr.Textbox( + label="Assistant reply", + placeholder="Webhook reply appears here, or type the response you want VoxCPM to speak.", + lines=4, + ) + audio_out = gr.Audio(label="Voiced reply") + status = gr.Textbox(label="Status", value="Ready.", interactive=False) + + with gr.Column(elem_classes=["panel"]): + gr.Markdown("### Voice profile") + profile_name = gr.Textbox( + label="Profile name", + value=default_profile_data.get("name", "default"), + placeholder="A short name for this voice profile, like my-voice.", + ) + reference_audio = gr.Audio( + label="Reference voice sample", + sources=["upload", "microphone"], + value=default_profile_data.get("reference_audio"), + type="filepath", + ) + reference_transcript = gr.Textbox( + label="Reference transcript", + value=default_profile_data.get("reference_transcript", ""), + placeholder="Paste the exact transcript for best cloning quality. Leave blank to auto-transcribe if available.", + lines=3, + ) + control_text = gr.Textbox( + label="Voice control", + value=default_profile_data.get("control_text", ""), + placeholder="Optional style guidance such as warm, calm, faster, younger, smiling.", + lines=2, + ) + + with gr.Row(): + save_profile_btn = gr.Button("Save enrollment", variant="primary") + load_profile_btn = gr.Button("Load enrollment", variant="secondary") + clear_profile_btn = gr.Button("Clear enrollment", variant="secondary") + profile_status = gr.Textbox( + label="Profile status", + value=_profile_summary(default_profile_data), + interactive=False, + ) + + with gr.Accordion("Advanced", open=False): + assistant_webhook_url = gr.Textbox( + label="PCA backend URL", + value=backend_url_default, + placeholder="OpenAI-compatible /v1/chat/completions or a custom webhook endpoint.", + ) + assistant_webhook_token = gr.Textbox( + label="PCA backend token", + value=backend_token_default, + placeholder="Optional bearer token.", + ) + assistant_context = gr.Textbox( + label="Assistant context", + value=backend_context_default, + placeholder="Optional system prompt or assistant instructions.", + lines=3, + ) + backend_mode = gr.Dropdown( + choices=["auto", "openai", "custom"], + value=backend_mode_default, + label="Backend mode", + info="Auto detects OpenAI-compatible chat endpoints; custom uses the webhook payload format.", + ) + auto_transcribe_reference = gr.Checkbox( + value=True, + label="Auto-transcribe reference audio when ASR is installed", + ) + normalize_text = gr.Checkbox( + value=False, + label="Normalize text", + ) + denoise_reference = gr.Checkbox( + value=False, + label="Denoise reference audio", + ) + cfg_value = gr.Slider( + minimum=1.0, + maximum=3.0, + value=2.0, + step=0.1, + label="CFG guidance scale", + ) + inference_timesteps = gr.Slider( + minimum=1, + maximum=50, + value=10, + step=1, + label="Inference steps", + ) + + gr.Markdown( + """ + **Backend payloads** + + OpenAI-compatible mode sends `{"model":"pca","messages":[...],"temperature":0.4,"stream":false}`. + + Custom mode sends `{"message": "...", "history": [...], "assistant_context": "..."}`. + + The response can be plain text or JSON with `reply`, `text`, `content`, or OpenAI-style `choices[0].message.content`. + """ + ) + + send_btn.click( + fn=prepare_turn, + inputs=[ + user_audio, + user_text, + assistant_reply, + assistant_webhook_url, + assistant_webhook_token, + assistant_context, + backend_mode, + reference_audio, + reference_transcript, + control_text, + normalize_text, + denoise_reference, + auto_transcribe_reference, + cfg_value, + inference_timesteps, + conversation, + profile_state, + ], + outputs=[ + conversation, + user_text, + assistant_reply, + reference_transcript, + audio_out, + status, + profile_state, + ], + show_progress=True, + ) + + speak_btn.click( + fn=speak_reply, + inputs=[ + assistant_reply, + reference_audio, + reference_transcript, + control_text, + profile_state, + normalize_text, + denoise_reference, + auto_transcribe_reference, + cfg_value, + inference_timesteps, + ], + outputs=[audio_out, reference_transcript, status], + show_progress=True, + ) + + save_profile_btn.click( + fn=save_voice_profile, + inputs=[ + profile_name, + reference_audio, + reference_transcript, + control_text, + auto_transcribe_reference, + ], + outputs=[ + profile_state, + profile_status, + reference_audio, + reference_transcript, + control_text, + ], + show_progress=True, + ) + + load_profile_btn.click( + fn=load_voice_profile, + inputs=[profile_name], + outputs=[ + profile_state, + reference_audio, + reference_transcript, + control_text, + profile_status, + ], + show_progress=True, + ) + + clear_profile_btn.click( + fn=clear_voice_profile, + inputs=[], + outputs=[profile_state, reference_audio, reference_transcript, control_text, profile_status], + ) + + clear_btn.click( + fn=clear_state, + inputs=[profile_state], + outputs=[conversation, user_text, assistant_reply, reference_transcript, audio_out, status, profile_state], + ) + + return demo + + +def launch( + *, + model_id: str = DEFAULT_MODEL_ID, + device: str = "auto", + port: int = 8809, + host: str = "0.0.0.0", + share: bool = False, + no_denoiser: bool = False, + optimize: bool = True, + zipenhancer_path: Optional[str] = None, + default_profile_name: str = "reddit-female", + backend_url: str = DEFAULT_BACKEND_URL, + backend_token: str = DEFAULT_BACKEND_TOKEN, + backend_context: str = DEFAULT_BACKEND_CONTEXT, + backend_mode: str = DEFAULT_BACKEND_MODE if DEFAULT_BACKEND_MODE in {"auto", "openai", "custom"} else "auto", +): + runtime = PhoneAssistantRuntime( + model_id=model_id, + device=device, + load_denoiser=not no_denoiser, + optimize=optimize, + zipenhancer_model_path=zipenhancer_path, + ) + default_profile_data: dict[str, Any] = {} + if _clean_text(default_profile_name): + try: + default_profile_data = _load_profile(default_profile_name) + except FileNotFoundError: + logger.warning("Default profile not found: %s", default_profile_name) + + if _clean_text(backend_url): + os.environ["PCA_BACKEND_URL"] = backend_url + if _clean_text(backend_token): + os.environ["PCA_BACKEND_TOKEN"] = backend_token + if _clean_text(backend_context): + os.environ["PCA_ASSISTANT_CONTEXT"] = backend_context + if _clean_text(backend_mode): + os.environ["PCA_BACKEND_MODE"] = backend_mode + + demo = build_interface(runtime, default_profile_data=default_profile_data) + demo.queue(max_size=16, default_concurrency_limit=1).launch( + server_name=host, + server_port=port, + share=share, + show_error=True, + ) + + +def main(): + parser = argparse.ArgumentParser(description="VoxCPM mobile assistant bridge") + parser.add_argument("--model-id", type=str, default=DEFAULT_MODEL_ID) + parser.add_argument("--device", type=str, default="auto") + parser.add_argument("--port", type=int, default=8809) + parser.add_argument("--host", type=str, default="0.0.0.0") + parser.add_argument("--share", action="store_true") + parser.add_argument("--no-denoiser", action="store_true") + parser.add_argument("--no-optimize", action="store_true") + parser.add_argument("--zipenhancer-path", type=str, default=None) + parser.add_argument("--default-profile-name", type=str, default="reddit-female") + parser.add_argument("--backend-url", type=str, default=DEFAULT_BACKEND_URL) + parser.add_argument("--backend-token", type=str, default=DEFAULT_BACKEND_TOKEN) + parser.add_argument("--backend-context", type=str, default=DEFAULT_BACKEND_CONTEXT) + parser.add_argument( + "--backend-mode", + type=str, + default=DEFAULT_BACKEND_MODE if DEFAULT_BACKEND_MODE in {"auto", "openai", "custom"} else "auto", + choices=["auto", "openai", "custom"], + ) + args = parser.parse_args() + + launch( + model_id=args.model_id, + device=args.device, + port=args.port, + host=args.host, + share=args.share, + no_denoiser=args.no_denoiser, + optimize=not args.no_optimize, + zipenhancer_path=args.zipenhancer_path, + default_profile_name=args.default_profile_name, + backend_url=args.backend_url, + backend_token=args.backend_token, + backend_context=args.backend_context, + backend_mode=args.backend_mode, + ) + + +if __name__ == "__main__": + main() diff --git a/tests/test_phone_assistant.py b/tests/test_phone_assistant.py new file mode 100644 index 00000000..dc827706 --- /dev/null +++ b/tests/test_phone_assistant.py @@ -0,0 +1,37 @@ +from voxcpm.phone_assistant import ( + _build_backend_messages, + _build_final_text, + _extract_reply_text, + _profile_path, +) + + +def test_build_final_text_wraps_control_instruction(): + assert _build_final_text("Hello world", "warm female voice") == "(warm female voice)Hello world" + + +def test_build_final_text_leaves_plain_text_when_no_control(): + assert _build_final_text("Hello world", "") == "Hello world" + + +def test_extract_reply_text_supports_plain_reply_field(): + assert _extract_reply_text({"reply": " cloned voice text "}) == "cloned voice text" + + +def test_extract_reply_text_supports_openai_style_response(): + payload = {"choices": [{"message": {"content": " assistant reply "}}]} + assert _extract_reply_text(payload) == "assistant reply" + + +def test_profile_path_sanitizes_name(): + assert _profile_path("My Voice!").name == "myvoice.json" + + +def test_build_backend_messages_includes_context_and_history(): + messages = _build_backend_messages( + "hello", + [("old user", "old assistant")], + "system context", + ) + assert messages[0] == {"role": "system", "content": "system context"} + assert messages[-1] == {"role": "user", "content": "hello"} diff --git a/uv.lock b/uv.lock index 8577c08b..90ba4cce 100644 --- a/uv.lock +++ b/uv.lock @@ -168,28 +168,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] -[[package]] -name = "aliyun-python-sdk-core" -version = "2.16.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "jmespath" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/09/da9f58eb38b4fdb97ba6523274fbf445ef6a06be64b433693da8307b4bec/aliyun-python-sdk-core-2.16.0.tar.gz", hash = "sha256:651caad597eb39d4fad6cf85133dffe92837d53bdf62db9d8f37dab6508bb8f9", size = 449555, upload-time = "2024-10-09T06:01:01.762Z" } - -[[package]] -name = "aliyun-python-sdk-kms" -version = "2.16.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aliyun-python-sdk-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/2c/9877d0e6b18ecf246df671ac65a5d1d9fecbf85bdcb5d43efbde0d4662eb/aliyun-python-sdk-kms-2.16.5.tar.gz", hash = "sha256:f328a8a19d83ecbb965ffce0ec1e9930755216d104638cd95ecd362753b813b3", size = 12018, upload-time = "2024-08-30T09:01:20.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/5c/0132193d7da2c735669a1ed103b142fd63c9455984d48c5a88a1a516efaa/aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0", size = 99495, upload-time = "2024-08-30T09:01:18.462Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.4" @@ -208,12 +186,6 @@ 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 = "antlr4-python3-runtime" -version = "4.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } - [[package]] name = "anyascii" version = "0.3.3" @@ -948,72 +920,6 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] -[[package]] -name = "crcmod" -version = "1.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670, upload-time = "2010-06-27T14:35:29.538Z" } - -[[package]] -name = "cryptography" -version = "46.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, -] - [[package]] name = "cuda-bindings" version = "12.9.4" @@ -1109,52 +1015,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] -[[package]] -name = "editdistance" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/18/9f4f975ca87a390832b1c22478f3702fcdf739f83211e24d054b7551270d/editdistance-0.8.1.tar.gz", hash = "sha256:d1cdf80a5d5014b0c9126a69a42ce55a457b457f6986ff69ca98e4fe4d2d8fed", size = 50006, upload-time = "2024-02-10T07:44:53.914Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/c9/302658ce7f4c537a4e85cf578d11bbf7af120a712e1d78fedc6cb8823c65/editdistance-0.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:adeb705f32b93accc74960d227875abff150ee42d676e428536361fe5f8f5388", size = 106150, upload-time = "2024-02-10T07:43:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/45/80/0b3c7d2c0e183725986fea5dd2df11f0b4b46320e9a64f6077a121ab1f64/editdistance-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3de77951b105d0972deec7684a0b3d1a9dee69c9b5d34f6e2acc0d76cd4a1c52", size = 80551, upload-time = "2024-02-10T07:43:17.64Z" }, - { url = "https://files.pythonhosted.org/packages/b5/14/681460965c6a4a48321b07f88de2273d097fdca0491ff55db891aacbd291/editdistance-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e88efb052d45e924606c305cb833a80579dca3e8e4ff01309d50ba2c1c0bbd5", size = 79142, upload-time = "2024-02-10T07:43:19.195Z" }, - { url = "https://files.pythonhosted.org/packages/ed/0d/abdbc8e394a9461cf2ae27c16564fadaa65f52bd242dd1582ae5e7736dc3/editdistance-0.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0247e7a1e9c66ea75211a97e725366bff19a52aac2c838ed5f90025630e976dd", size = 396768, upload-time = "2024-02-10T07:43:20.912Z" }, - { url = "https://files.pythonhosted.org/packages/c2/fb/2940d26ebda12efd280ae939436f17ac482930d862df9e774cb8b771ab03/editdistance-0.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67d143429a49ab552411505f550a0fb4285a1d4336e096804d233ec495ac20fc", size = 401846, upload-time = "2024-02-10T07:43:23.169Z" }, - { url = "https://files.pythonhosted.org/packages/53/cc/c63d75c7f387d4df0645682c1ab8706c2dfe5c9c0c4999723ce9a3ba0853/editdistance-0.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca9d3be2b10e5d44a950a4bd1e84bca9ebbecd364bce0cf5693bf8224c78eaef", size = 397543, upload-time = "2024-02-10T07:43:24.621Z" }, - { url = "https://files.pythonhosted.org/packages/8e/38/bb0f734a7571e093184606b930734b12da5b6bff2635eba9312fe4536dd9/editdistance-0.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5c72aa1df8535f2e2b3d8773a1a7da091bc1a7e52bb396e7e48d375ba687e7b2", size = 898934, upload-time = "2024-02-10T07:43:26.926Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/624fc7a09918f850a057465f02e86f269e139a457f48ff8cabfb12701756/editdistance-0.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9a606c34a2a6cc190e4fffc856b36333cdcf1f1fab5b22bd3088e585c22d6ca0", size = 959637, upload-time = "2024-02-10T07:43:28.997Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5c/7fa6cc277f91c477ee370807d51c1826891dc6dfc307544223ce7f2687de/editdistance-0.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5af173d442ffac33b7c7990132f97f88818a3abf4b21c0c702a7022df37c0c5c", size = 911024, upload-time = "2024-02-10T07:43:30.449Z" }, - { url = "https://files.pythonhosted.org/packages/ad/97/556215f71184291155aee340a6d34f0676e7238fdfd10615b6b775ce25fe/editdistance-0.8.1-cp310-cp310-win32.whl", hash = "sha256:fd64b58f5a7b59afd9d75982aaeeacd2a98498bf472fa0360c122ffe6ea4c871", size = 80834, upload-time = "2024-02-10T07:43:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d1/7ec5f5cbb95838d0eff7f980a660c81acd1363d658f2f5d4ceba38877c5a/editdistance-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:6c7c62c3cae45ca1fa01bb2722b297b9de1e3a244ac44cfba88bdcb488fe6aee", size = 79614, upload-time = "2024-02-10T07:43:33.255Z" }, - { url = "https://files.pythonhosted.org/packages/e2/dc/d0c29fd52d8f9e795653ed2b838a2a48c739cdfff04ac5b79c6c0ecbdf79/editdistance-0.8.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:486105603a273d73d12a54f347dffa70ab281749d7c3879658b377bc49e4b98c", size = 106079, upload-time = "2024-02-10T07:43:34.34Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c6/75fa45d7b78fbea6fd894f4e48895a75bd3c83d4a9a6b57673881d74d3e0/editdistance-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fad081f5f86a175c1a09a4e9e45b95c9349e454c21e181e842e01c85f1f536fc", size = 80580, upload-time = "2024-02-10T07:43:35.947Z" }, - { url = "https://files.pythonhosted.org/packages/b7/a3/058d823b6285c3511dc94ed80620c3fb0c18b4aaa708f70ba71f3af28436/editdistance-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cb78e125f6759398885a775f5eed07c2bb72b2f86da43e674c6b6a3335b273b", size = 79087, upload-time = "2024-02-10T07:43:36.923Z" }, - { url = "https://files.pythonhosted.org/packages/a0/3a/0b13c7864c93b1e9b9952bd2a33c5ef3c4fd1bf70a5fad6924789e70e5eb/editdistance-0.8.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3778ca60aa89def9144b70e330bcec5330c7da1d69cb28c612e90b84510a1d3d", size = 409296, upload-time = "2024-02-10T07:43:38.52Z" }, - { url = "https://files.pythonhosted.org/packages/96/8a/db0fd79e8ddb9b5f86f274107c5d0a27ec4f2af88877df1f26c2c6d150cc/editdistance-0.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fba945eaa0436cf40bc53d7e299dc537c7c71353379a095b7459ff4af910da33", size = 412913, upload-time = "2024-02-10T07:43:39.852Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d2/98be7112750ff17b436dd76f988f1e38570dcec0df8578ee19ef046f22fe/editdistance-0.8.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:877f2a0d801f32bc1a1878901ffb947b974361e849c66e314a7f1d786a446b58", size = 407430, upload-time = "2024-02-10T07:43:41.048Z" }, - { url = "https://files.pythonhosted.org/packages/03/62/1815e3bf164910c47ba1948c8b5e937a40c7f9763b64e98fb6666b01dd06/editdistance-0.8.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e79d351ca40a6ead5f3763253fd7521572ee0d3e5d42538630e56d10f48db481", size = 909217, upload-time = "2024-02-10T07:43:42.916Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d3/a832cea7b507a9be54e4ac3d1340fb66dca5f9c16c70bf38d5039e8fdede/editdistance-0.8.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:70ed382b3052a51161bad0149d4665003bf3b949fce0b01bf1253a4cc1a88239", size = 969407, upload-time = "2024-02-10T07:43:44.912Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b4/db291d2a3845cbf8047b4b5aad3b3e038a8a2994d87027b40e1a1b0f4b74/editdistance-0.8.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a529bfb384c4000775d76739c4e64f73337f0f5a3784933b1321b577a62bed4e", size = 922112, upload-time = "2024-02-10T07:43:47.047Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/7ddeacada4982d0b892a28897e21871d0f25bca165e3663e37c3a272808a/editdistance-0.8.1-cp311-cp311-win32.whl", hash = "sha256:b082232429e731f181af7f7d2bcf79da6ca8fadd04e9086c11e2973f7d330c81", size = 80799, upload-time = "2024-02-10T07:43:48.231Z" }, - { url = "https://files.pythonhosted.org/packages/52/a1/778af8590b8b12f03f62eacc3c8744407ade9e3d69be6dabe38d0afbf2dd/editdistance-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:cef1a4359252a49f2c4718e64e9d40027d9d951b289d045bdb278656e59f6af8", size = 79698, upload-time = "2024-02-10T07:43:49.234Z" }, - { url = "https://files.pythonhosted.org/packages/cb/4c/7f195588949b4e72436dc7fc902632381f96e586af829685b56daebb38b8/editdistance-0.8.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04af61b3fcdd287a07c15b6ae3b02af01c5e3e9c3aca76b8c1d13bd266b6f57", size = 106723, upload-time = "2024-02-10T07:43:50.268Z" }, - { url = "https://files.pythonhosted.org/packages/8d/82/31dc1640d830cd7d36865098329f34e4dad3b77f31cfb9404b347e700196/editdistance-0.8.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:18fc8b6eaae01bfd9cf999af726c1e8dcf667d120e81aa7dbd515bea7427f62f", size = 80998, upload-time = "2024-02-10T07:43:51.259Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2a/6b823e71cef694d6f070a1d82be2842706fa193541aab8856a8f42044cd0/editdistance-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a87839450a5987028738d061ffa5ef6a68bac2ddc68c9147a8aae9806629c7f", size = 79248, upload-time = "2024-02-10T07:43:52.873Z" }, - { url = "https://files.pythonhosted.org/packages/e1/31/bfb8e590f922089dc3471ed7828a6da2fc9453eba38c332efa9ee8749fd7/editdistance-0.8.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24b5f9c9673c823d91b5973d0af8b39f883f414a55ade2b9d097138acd10f31e", size = 415262, upload-time = "2024-02-10T07:43:54.498Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/57423942b2f847cdbbb46494568d00cd8a45500904ea026f0aad6ca01bc7/editdistance-0.8.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c59248eabfad603f0fba47b0c263d5dc728fb01c2b6b50fb6ca187cec547fdb3", size = 418905, upload-time = "2024-02-10T07:43:55.779Z" }, - { url = "https://files.pythonhosted.org/packages/1b/05/dfa4cdcce063596cbf0d7a32c46cd0f4fa70980311b7da64d35f33ad02a0/editdistance-0.8.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e239d88ff52821cf64023fabd06a1d9a07654f364b64bf1284577fd3a79d0e", size = 412511, upload-time = "2024-02-10T07:43:57.567Z" }, - { url = "https://files.pythonhosted.org/packages/0e/14/39608ff724a9523f187c4e28926d78bc68f2798f74777ac6757981108345/editdistance-0.8.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2f7f71698f83e8c83839ac0d876a0f4ef996c86c5460aebd26d85568d4afd0db", size = 917293, upload-time = "2024-02-10T07:43:59.559Z" }, - { url = "https://files.pythonhosted.org/packages/df/92/4a1c61d72da40dedfd0ff950fdc71ae83f478330c58a8bccfd776518bd67/editdistance-0.8.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:04e229d6f4ce0c12abc9f4cd4023a5b5fa9620226e0207b119c3c2778b036250", size = 975580, upload-time = "2024-02-10T07:44:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/47/3d/9877566e724c8a37f2228a84ec5cbf66dbfd0673515baf68a0fe07caff40/editdistance-0.8.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e16721636da6d6b68a2c09eaced35a94f4a4a704ec09f45756d4fd5e128ed18d", size = 929121, upload-time = "2024-02-10T07:44:02.764Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/8c50757d198b8ca30ddb91e8b8f0247a8dca04ff2ec30755245f0ab1ff0c/editdistance-0.8.1-cp312-cp312-win32.whl", hash = "sha256:87533cf2ebc3777088d991947274cd7e1014b9c861a8aa65257bcdc0ee492526", size = 81039, upload-time = "2024-02-10T07:44:04.134Z" }, - { url = "https://files.pythonhosted.org/packages/28/f0/65101e51dc7c850e7b7581a5d8fa8721a1d7479a0dca6c08386328e19882/editdistance-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:09f01ed51746d90178af7dd7ea4ebb41497ef19f53c7f327e864421743dffb0a", size = 79853, upload-time = "2024-02-10T07:44:05.687Z" }, - { url = "https://files.pythonhosted.org/packages/d4/4c/c9d02eeb47815d35f8d324b52f6704ea7beb032bcb209358cac44047d413/editdistance-0.8.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a4a90c6b03094c07358572027a8d0a13cca7450b1aa6caca98a5f1fa4f0b8961", size = 76455, upload-time = "2024-02-10T07:44:36.838Z" }, - { url = "https://files.pythonhosted.org/packages/af/b0/2818fa6a24595dac069b0bfb9d05658406779a1ded8fd2b0c9066396cf99/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510a4f9ced348a4fd89ae2e102357d4d801a771e29bb2bc2f130a1692193407f", size = 84104, upload-time = "2024-02-10T07:44:37.928Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d1/3d5e09bcf7fdb7aed705bf74047a8634bd2b8fd92177c25a2547e6dbadfb/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4787fa7228ba6a34b430066d174320f011d605015baa7299c2c4911e6ea6bd46", size = 89058, upload-time = "2024-02-10T07:44:39.113Z" }, - { url = "https://files.pythonhosted.org/packages/cd/88/fca5d7b1a1edf66ce1e5b6b60bff75842e6814b4f5facbdf4585d88c912d/editdistance-0.8.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee02601375073afccd6b4d811129ce1cb696d47db734784d8dbd1fddcea75447", size = 84635, upload-time = "2024-02-10T07:44:40.714Z" }, - { url = "https://files.pythonhosted.org/packages/a9/91/0e6285bbe2358d81fd16313d30306b2d0036387348f7bc11d8c076ca3c72/editdistance-0.8.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bc7ad9f9a20e6f351523de77c59249f005242e3f317b5de45d02c378d24f6531", size = 77389, upload-time = "2024-02-10T07:44:41.725Z" }, -] - [[package]] name = "einops" version = "0.8.2" @@ -1416,37 +1276,6 @@ http = [ { name = "aiohttp" }, ] -[[package]] -name = "funasr" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "editdistance" }, - { name = "hydra-core" }, - { name = "jaconv" }, - { name = "jamo" }, - { name = "jieba" }, - { name = "kaldiio" }, - { name = "librosa" }, - { name = "modelscope" }, - { name = "oss2" }, - { name = "pytorch-wpe" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sentencepiece" }, - { name = "soundfile" }, - { name = "tensorboardx" }, - { name = "torch-complex" }, - { name = "tqdm" }, - { name = "umap-learn" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/dc/d5586cd788bdf75286d73e646f5f354f8b3220b75a61c0325a177689065d/funasr-1.3.1.tar.gz", hash = "sha256:ed813c0ecade7d24393943a82a91f7cee822178ab4bfd303adb8ad318605e0a5", size = 664362, upload-time = "2026-01-26T13:07:43.579Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/89/61c09967f0f4f091402367215000c3c060bfeaadbbdc9aca42856d299c95/funasr-1.3.1-py3-none-any.whl", hash = "sha256:f63050d7d625f287ec741b84a0325365699c49550a4bfd3ace4acb43e41a87a8", size = 811975, upload-time = "2026-01-26T13:07:41.866Z" }, -] - [[package]] name = "gradio" version = "6.9.0" @@ -1602,20 +1431,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/75/ca21955d6117a394a482c7862ce96216239d0e3a53133ae8510727a8bcfa/huggingface_hub-1.7.1-py3-none-any.whl", hash = "sha256:38c6cce7419bbde8caac26a45ed22b0cea24152a8961565d70ec21f88752bfaa", size = 616308, upload-time = "2026-03-13T09:36:06.062Z" }, ] -[[package]] -name = "hydra-core" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "omegaconf" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, -] - [[package]] name = "identify" version = "2.6.18" @@ -1656,30 +1471,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "jaconv" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/0e/9fffaacda59bdfa479372c71d18d72968d2af5a36a5a2086b02a60124b98/jaconv-0.5.0.tar.gz", hash = "sha256:53f6f968276846716f0f37100a6d5c7308cfa1e0c714eb41287d5bb09345c40f", size = 21816, upload-time = "2026-02-08T11:15:57.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/da/9657d637bcacdbaf6a914ce504000da5639f9d945f8d3552a940f021d6c0/jaconv-0.5.0-py3-none-any.whl", hash = "sha256:2914114fe761ca49fc7089e25e6ad4a400c26f262ffce84e13b176916b71610a", size = 16831, upload-time = "2026-02-08T11:15:55.322Z" }, -] - -[[package]] -name = "jamo" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/a2/bda770579809726e929ca6356743f9f50f64a2cbaee578fa9d4824afb00e/jamo-0.4.1.tar.gz", hash = "sha256:ea65cf9d35338d0e0af48d75ff426d8a369b0ebde6f07051c3ac37256f56d025", size = 7386, upload-time = "2017-11-06T19:28:51.729Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/cc/49812faae67f9a24be6ddaf58a2cf7e8c3cbfcf5b762d9414f7103d2ea2c/jamo-0.4.1-py3-none-any.whl", hash = "sha256:d4b94fd23324c606ed2fbc4037c603e2c3a7ae9390c05d3473aea1ccb6b1c3fb", size = 9543, upload-time = "2017-11-06T19:28:49.624Z" }, -] - -[[package]] -name = "jieba" -version = "0.42.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } - [[package]] name = "jinja2" version = "3.1.6" @@ -1692,15 +1483,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jmespath" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3c/56/3f325b1eef9791759784aa5046a8f6a1aff8f7c898a2e34506771d3b99d8/jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", size = 21607, upload-time = "2020-05-12T22:03:47.267Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489, upload-time = "2020-05-12T22:03:45.643Z" }, -] - [[package]] name = "joblib" version = "1.5.3" @@ -1752,19 +1534,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/c4/ece538e3f5bbb091a9ae68f2ec0c02a18b2cdb60f1cb9f973586f95b3631/kaldifst-1.7.17-cp314-cp314-win32.whl", hash = "sha256:8cde76483ffe39edf747a7b1133e364546d2eca49e36975c27e92842f9499b72", size = 9062227, upload-time = "2025-09-01T13:58:53.994Z" }, ] -[[package]] -name = "kaldiio" -version = "2.18.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/85/92435e8e62eb3d43eded9f24643fc2a6dbce031cebceed11528147c7873f/kaldiio-2.18.1.tar.gz", hash = "sha256:0283d197fac6ac683f7a9e6af8d18aad9dbd2c4a997f22e45294f2ac1ee3c432", size = 35570, upload-time = "2025-03-06T15:57:52.375Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/e3/6c3b42233225f398f7a72988b524f654ae818cca0d441db847a2761203e9/kaldiio-2.18.1-py3-none-any.whl", hash = "sha256:397a4cd18977acaae7acabfba6807ee0a6978c620064381a266eac15b3c1a0a0", size = 29330, upload-time = "2025-03-06T15:57:50.82Z" }, -] - [[package]] name = "kiwisolver" version = "1.5.0" @@ -2759,19 +2528,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, ] -[[package]] -name = "omegaconf" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, -] - [[package]] name = "orjson" version = "3.11.7" @@ -2853,20 +2609,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, ] -[[package]] -name = "oss2" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aliyun-python-sdk-core" }, - { name = "aliyun-python-sdk-kms" }, - { name = "crcmod" }, - { name = "pycryptodome" }, - { name = "requests" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/f2cb1950dda46ac2284d6c950489fdacd0e743c2d79a347924d3cc44b86f/oss2-2.19.1.tar.gz", hash = "sha256:a8ab9ee7eb99e88a7e1382edc6ea641d219d585a7e074e3776e9dec9473e59c1", size = 298845, upload-time = "2024-10-25T11:37:46.638Z" } - [[package]] name = "packaging" version = "26.0" @@ -3211,21 +2953,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] -[[package]] -name = "protobuf" -version = "7.34.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/00/04a2ab36b70a52d0356852979e08b44edde0435f2115dc66e25f2100f3ab/protobuf-7.34.0.tar.gz", hash = "sha256:3871a3df67c710aaf7bb8d214cc997342e63ceebd940c8c7fc65c9b3d697591a", size = 454726, upload-time = "2026-02-27T00:30:25.421Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/c4/6322ab5c8f279c4c358bc14eb8aefc0550b97222a39f04eb3c1af7a830fa/protobuf-7.34.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e329966799f2c271d5e05e236459fe1cbfdb8755aaa3b0914fa60947ddea408", size = 429248, upload-time = "2026-02-27T00:30:14.924Z" }, - { url = "https://files.pythonhosted.org/packages/45/99/b029bbbc61e8937545da5b79aa405ab2d9cf307a728f8c9459ad60d7a481/protobuf-7.34.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:9d7a5005fb96f3c1e64f397f91500b0eb371b28da81296ae73a6b08a5b76cdd6", size = 325753, upload-time = "2026-02-27T00:30:17.247Z" }, - { url = "https://files.pythonhosted.org/packages/cc/79/09f02671eb75b251c5550a1c48e7b3d4b0623efd7c95a15a50f6f9fc1e2e/protobuf-7.34.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4a72a8ec94e7a9f7ef7fe818ed26d073305f347f8b3b5ba31e22f81fd85fca02", size = 340200, upload-time = "2026-02-27T00:30:18.672Z" }, - { url = "https://files.pythonhosted.org/packages/b5/57/89727baef7578897af5ed166735ceb315819f1c184da8c3441271dbcfde7/protobuf-7.34.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:964cf977e07f479c0697964e83deda72bcbc75c3badab506fb061b352d991b01", size = 324268, upload-time = "2026-02-27T00:30:20.088Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3e/38ff2ddee5cc946f575c9d8cc822e34bde205cf61acf8099ad88ef19d7d2/protobuf-7.34.0-cp310-abi3-win32.whl", hash = "sha256:f791ec509707a1d91bd02e07df157e75e4fb9fbdad12a81b7396201ec244e2e3", size = 426628, upload-time = "2026-02-27T00:30:21.555Z" }, - { url = "https://files.pythonhosted.org/packages/cb/71/7c32eaf34a61a1bae1b62a2ac4ffe09b8d1bb0cf93ad505f42040023db89/protobuf-7.34.0-cp310-abi3-win_amd64.whl", hash = "sha256:9f9079f1dde4e32342ecbd1c118d76367090d4aaa19da78230c38101c5b3dd40", size = 437901, upload-time = "2026-02-27T00:30:22.836Z" }, - { url = "https://files.pythonhosted.org/packages/a4/e7/14dc9366696dcb53a413449881743426ed289d687bcf3d5aee4726c32ebb/protobuf-7.34.0-py3-none-any.whl", hash = "sha256:e3b914dd77fa33fa06ab2baa97937746ab25695f389869afdf03e81f34e45dc7", size = 170716, upload-time = "2026-02-27T00:30:23.994Z" }, -] - [[package]] name = "psutil" version = "5.9.8" @@ -3348,41 +3075,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] -[[package]] -name = "pycryptodome" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, - { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, - { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, - { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, - { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, - { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, - { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, - { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, - { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, - { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, - { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, - { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, -] - [[package]] name = "pydantic" version = "2.12.3" @@ -3539,24 +3231,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pynndescent" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "joblib" }, - { name = "llvmlite" }, - { name = "numba" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4a/fb/7f58c397fb31666756457ee2ac4c0289ef2daad57f4ae4be8dec12f80b03/pynndescent-0.6.0.tar.gz", hash = "sha256:7ffde0fb5b400741e055a9f7d377e3702e02250616834231f6c209e39aac24f5", size = 2992987, upload-time = "2026-01-08T21:29:58.943Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/e6/94145d714402fd5ade00b5661f2d0ab981219e07f7db9bfa16786cdb9c04/pynndescent-0.6.0-py3-none-any.whl", hash = "sha256:dc8c74844e4c7f5cbd1e0cd6909da86fdc789e6ff4997336e344779c3d5538ef", size = 73511, upload-time = "2026-01-08T21:29:57.306Z" }, -] - [[package]] name = "pyparsing" version = "3.3.2" @@ -3671,19 +3345,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] -[[package]] -name = "pytorch-wpe" -version = "0.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/db/39/8d94737fd6fab4028687575099566a125100f3ba8c638f861506747d7b7c/pytorch_wpe-0.0.1.tar.gz", hash = "sha256:fc7e706b5411800c4483fe94db7dcd82ecf6c57bc013af529ab4fb675c9cc29c", size = 4457, upload-time = "2021-03-05T10:10:09.593Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/de/c47967a11bfe68cb28d2f19e55c7027993c3721eba79813db65d245e4ced/pytorch_wpe-0.0.1-py3-none-any.whl", hash = "sha256:fa0dc9f818fba81b36c1a51a53331cf6ed975f29b33f23e07b0deb4bee82eaad", size = 8080, upload-time = "2021-03-05T10:10:08.686Z" }, -] - [[package]] name = "pytz" version = "2026.1.post1" @@ -4190,70 +3851,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, ] -[[package]] -name = "sentencepiece" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/31/5b7cccb307b485db1a2372d6d2980b0a65d067f8be5ca943a103b4acd5b3/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e10fa50bdbaa5e2445dbd387979980d391760faf0ec99a09bd7780ff37eaec44", size = 1942557, upload-time = "2025-08-12T06:59:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/1f/41/0ac923a8e685ad290c5afc8ae55c5844977b8d75076fcc04302b9a324274/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f27ae6deea72efdb6f361750c92f6c21fd0ad087445082770cc34015213c526", size = 1325384, upload-time = "2025-08-12T06:59:14.334Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ef/3751555d67daf9003384978f169d31c775cb5c7baf28633caaf1eb2b2b4d/sentencepiece-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60937c959e6f44159fdd9f56fbdd302501f96114a5ba436829496d5f32d8de3f", size = 1253317, upload-time = "2025-08-12T06:59:16.247Z" }, - { url = "https://files.pythonhosted.org/packages/46/a5/742c69b7bd144eb32b6e5fd50dbd8abbbc7a95fce2fe16e50156fa400e3b/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8b1d91545578852f128650b8cce4ec20f93d39b378ff554ebe66290f2dabb92", size = 1316379, upload-time = "2025-08-12T06:59:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/c8/89/8deeafbba2871e8fa10f20f17447786f4ac38085925335728d360eaf4cae/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27e38eee653abc3d387862e67bc5c8b6f428cd604e688b85d29170b7e725c26c", size = 1387926, upload-time = "2025-08-12T06:59:19.395Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ca/67fe73005f0ab617c6a970b199754e28e524b6873aa7025224fad3cda252/sentencepiece-0.2.1-cp310-cp310-win32.whl", hash = "sha256:251874d720ac7f28024a168501f3c7bb15d1802245f6e66de565f18bbb9b5eaa", size = 999550, upload-time = "2025-08-12T06:59:20.844Z" }, - { url = "https://files.pythonhosted.org/packages/6d/33/dc5b54042050d2dda4229c3ce1f862541c99966390b6aa20f54d520d2dc2/sentencepiece-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e52144670738b4b477fade6c2a9b6af71a8d0094514c9853ac9f6fc1fcfabae7", size = 1054613, upload-time = "2025-08-12T06:59:22.255Z" }, - { url = "https://files.pythonhosted.org/packages/fa/19/1ea47f46ff97fe04422b78997da1a37cd632f414aae042d27a9009c5b733/sentencepiece-0.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:9076430ac25dfa7147d9d05751dbc66a04bc1aaac371c07f84952979ea59f0d0", size = 1033884, upload-time = "2025-08-12T06:59:24.194Z" }, - { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, - { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, - { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, - { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, - { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, - { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, - { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, - { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, - { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, - { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, - { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, - { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, - { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, - { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, - { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, - { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, - { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, - { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, - { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, - { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, - { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, - { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, - { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, - { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" }, - { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" }, - { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" }, - { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" }, - { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" }, - { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" }, - { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" }, - { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" }, - { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" }, - { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" }, - { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" }, - { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" }, - { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" }, - { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" }, -] - [[package]] name = "setuptools" version = "82.0.1" @@ -4480,21 +4077,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] -[[package]] -name = "tensorboardx" -version = "2.6.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/c5/d4cc6e293fb837aaf9f76dd7745476aeba8ef7ef5146c3b3f9ee375fe7a5/tensorboardx-2.6.4.tar.gz", hash = "sha256:b163ccb7798b31100b9f5fa4d6bc22dad362d7065c2f24b51e50731adde86828", size = 4769801, upload-time = "2025-06-10T22:37:07.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/1d/b5d63f1a6b824282b57f7b581810d20b7a28ca951f2d5b59f1eb0782c12b/tensorboardx-2.6.4-py3-none-any.whl", hash = "sha256:5970cf3a1f0a6a6e8b180ccf46f3fe832b8a25a70b86e5a237048a7c0beb18e2", size = 87201, upload-time = "2025-06-10T22:37:05.44Z" }, -] - [[package]] name = "textsearch" version = "0.0.24" @@ -4683,20 +4265,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, ] -[[package]] -name = "torch-complex" -version = "0.4.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/17cb15a383cf2135330371e034d13b9043dc6d8bd07c871b5aa3064fbed1/torch_complex-0.4.4.tar.gz", hash = "sha256:4153fd6b24a0bad689e6f193bfbd00f38283b1890d808bef684ddc6d1f63fd3f", size = 10025, upload-time = "2024-06-28T07:10:28.136Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/c5/9b4d756a7ada951e9b17dcc636f98ed1073c737ae809b150ef408afb6298/torch_complex-0.4.4-py3-none-any.whl", hash = "sha256:6ab4ecd4f3a16e3adb70a7f7cd2e769a9dfd07d7a8e27d04ff9c621ebbe34b13", size = 9125, upload-time = "2024-06-28T07:10:26.651Z" }, -] - [[package]] name = "torchaudio" version = "2.10.0" @@ -4861,26 +4429,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] -[[package]] -name = "umap-learn" -version = "0.5.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numba" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pynndescent" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/9a/a1e4a257a9aa979dac4f6d5781dac929cbb0949959e2003ed82657d10b0f/umap_learn-0.5.11.tar.gz", hash = "sha256:31566ffd495fbf05d7ab3efcba703861c0f5e6fc6998a838d0e2becdd00e54f5", size = 96409, upload-time = "2026-01-12T20:44:47.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/d2/fcf7192dd1cd8c090b6cfd53fa223c4fb2887a17c47e06bc356d44f40dfb/umap_learn-0.5.11-py3-none-any.whl", hash = "sha256:cb17adbde9d544ba79481b3ab4d81ac222e940f3d9219307bea6044f869af3cc", size = 90890, upload-time = "2026-01-12T20:44:46.511Z" }, -] - [[package]] name = "urllib3" version = "2.6.3" @@ -4928,7 +4476,6 @@ dependencies = [ { name = "argbind" }, { name = "datasets" }, { name = "einops" }, - { name = "funasr" }, { name = "gradio" }, { name = "huggingface-hub" }, { name = "inflect" }, @@ -4966,7 +4513,6 @@ requires-dist = [ { name = "datasets", specifier = ">=3,<4" }, { name = "einops" }, { name = "flake8", marker = "extra == 'dev'", specifier = ">=3.8" }, - { name = "funasr" }, { name = "gradio", specifier = ">=6,<7" }, { name = "huggingface-hub" }, { name = "inflect" }, From 668f5b8c53457b323391185f16d0482efdf32473 Mon Sep 17 00:00:00 2001 From: James Juniper <217263268+jjuniper-dev@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:24:00 -0400 Subject: [PATCH 2/5] feat: stage pca captures from phone assistant --- README.md | 1 + src/voxcpm/phone_assistant.py | 133 ++++++++++++++++++++++++++++++++++ tests/test_phone_assistant.py | 22 ++++++ 3 files changed, 156 insertions(+) diff --git a/README.md b/README.md index e15fb062..593b8f4a 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ If you want phone microphone dictation, install the optional ASR dependency sepa You can also save a named voice enrollment once and reuse it later from the app's profile panel. By default, the assistant loads the saved `reddit-female` profile if it exists. To wire in your PCA backend automatically, set `PCA_BACKEND_URL` plus optional `PCA_BACKEND_TOKEN`, `PCA_ASSISTANT_CONTEXT`, and `PCA_BACKEND_MODE=auto|openai|custom` before launching. +To stage the phone message into PCA as a capture event, also set `PCA_CAPTURE_URL` plus optional `PCA_CAPTURE_TOKEN`. The assistant sends `source: iphone`, `capture_type: text`, the user message, and capture metadata that includes the assistant reply. ### 🚢 Production Deployment (Nano-vLLM) diff --git a/src/voxcpm/phone_assistant.py b/src/voxcpm/phone_assistant.py index 29856538..243e8cad 100644 --- a/src/voxcpm/phone_assistant.py +++ b/src/voxcpm/phone_assistant.py @@ -42,6 +42,8 @@ DEFAULT_BACKEND_TOKEN = os.environ.get("PCA_BACKEND_TOKEN", "").strip() DEFAULT_BACKEND_CONTEXT = os.environ.get("PCA_ASSISTANT_CONTEXT", "").strip() DEFAULT_BACKEND_MODE = os.environ.get("PCA_BACKEND_MODE", "auto").strip().lower() +DEFAULT_CAPTURE_URL = os.environ.get("PCA_CAPTURE_URL", "").strip() +DEFAULT_CAPTURE_TOKEN = os.environ.get("PCA_CAPTURE_TOKEN", "").strip() APP_THEME = gr.themes.Soft( primary_hue="cyan", @@ -237,6 +239,45 @@ def _extract_reply_text(payload: Any) -> str: return "" +def _build_capture_payload( + *, + user_message: str, + assistant_reply: str, + history: list[tuple[str, str]], + profile_name: str, + backend_mode: str, +) -> dict[str, Any]: + return { + "source": "iphone", + "capture_type": "text", + "content": _clean_text(user_message), + "metadata": { + "assistant_reply": _clean_text(assistant_reply), + "voice_profile": _clean_text(profile_name), + "backend_mode": _clean_text(backend_mode).lower() or "auto", + "turn_count": len(history) + 1, + "origin": "voxcpm-phone-assistant", + }, + } + + +def _extract_capture_receipt(payload: Any) -> str: + if isinstance(payload, str): + return payload.strip() + + if isinstance(payload, dict): + for key in ("capture_id", "receipt", "id", "status", "message"): + value = payload.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + + data = payload.get("data") + if isinstance(data, str) and data.strip(): + return data.strip() + + return "" + + def _build_backend_messages( user_message: str, history: list[tuple[str, str]], @@ -464,6 +505,49 @@ def request_reply( raise ValueError("Webhook returned an empty reply.") return reply + def request_capture( + self, + *, + capture_url: str, + capture_token: str, + user_message: str, + assistant_reply: str, + history: list[tuple[str, str]], + profile_name: str, + backend_mode: str, + timeout: int = 30, + ) -> str: + capture_url = _clean_text(capture_url) + if not capture_url: + raise ValueError("No capture URL configured.") + + payload = _build_capture_payload( + user_message=user_message, + assistant_reply=assistant_reply, + history=history, + profile_name=profile_name, + backend_mode=backend_mode, + ) + + headers = {"Content-Type": "application/json"} + if capture_token: + headers["Authorization"] = f"Bearer {capture_token.strip()}" + + response = requests.post(capture_url, json=payload, headers=headers, timeout=timeout) + response.raise_for_status() + + receipt = "" + content_type = response.headers.get("content-type", "") + if "application/json" in content_type.lower(): + try: + receipt = _extract_capture_receipt(response.json()) + except Exception: + receipt = response.text.strip() + else: + receipt = response.text.strip() + + return _clean_text(receipt) + def _history_to_chatbot(history: list[tuple[str, str]]) -> list[tuple[str, str]]: return _format_history(history) @@ -475,6 +559,8 @@ def build_interface(runtime: PhoneAssistantRuntime, default_profile_data: dict[s backend_token_default = _clean_text(os.environ.get("PCA_BACKEND_TOKEN", DEFAULT_BACKEND_TOKEN)) backend_context_default = _clean_text(os.environ.get("PCA_ASSISTANT_CONTEXT", DEFAULT_BACKEND_CONTEXT)) backend_mode_default = _clean_text(os.environ.get("PCA_BACKEND_MODE", DEFAULT_BACKEND_MODE)).lower() or "auto" + capture_url_default = _clean_text(os.environ.get("PCA_CAPTURE_URL", DEFAULT_CAPTURE_URL)) + capture_token_default = _clean_text(os.environ.get("PCA_CAPTURE_TOKEN", DEFAULT_CAPTURE_TOKEN)) if backend_mode_default not in {"auto", "openai", "custom"}: backend_mode_default = "auto" @@ -549,6 +635,8 @@ def prepare_turn( assistant_reply: str, backend_url: str, backend_token: str, + capture_url: str, + capture_token: str, assistant_context: str, backend_mode: str, reference_audio: Optional[str], @@ -590,6 +678,25 @@ def prepare_turn( if not assistant_reply: raise ValueError("Type the assistant reply, or configure a backend URL.") + capture_status = "" + if capture_url.strip(): + try: + capture_receipt = runtime.request_capture( + capture_url=capture_url, + capture_token=capture_token, + user_message=user_text_clean, + assistant_reply=assistant_reply, + history=history, + profile_name=_clean_text(profile_data.get("name", "default")) or "default", + backend_mode=backend_mode, + ) + capture_status = "Capture staged in PCA." + if capture_receipt: + capture_status = f"Capture staged in PCA ({capture_receipt})." + except Exception as exc: + logger.warning("Capture staging failed: %s", exc) + capture_status = f"Capture staging failed: {exc}" + reference_audio, reference_transcript, control_text = _merge_profile_inputs( profile_data, reference_audio, @@ -616,6 +723,8 @@ def prepare_turn( status = "Generated cloned speech." if reference_audio and transcript: status = "Generated cloned speech with transcript-guided cloning." + if capture_status: + status = f"{status} {capture_status}" return ( history, @@ -764,6 +873,16 @@ def clear_state(profile_data: dict[str, Any]): value=backend_token_default, placeholder="Optional bearer token.", ) + capture_webhook_url = gr.Textbox( + label="PCA capture URL", + value=capture_url_default, + placeholder="Optional capture webhook URL for staging the phone message in PCA.", + ) + capture_webhook_token = gr.Textbox( + label="PCA capture token", + value=capture_token_default, + placeholder="Optional bearer token for the capture webhook.", + ) assistant_context = gr.Textbox( label="Assistant context", value=backend_context_default, @@ -811,6 +930,8 @@ def clear_state(profile_data: dict[str, Any]): Custom mode sends `{"message": "...", "history": [...], "assistant_context": "..."}`. + If a PCA capture URL is configured, the phone message is also staged with `source: iphone`, `capture_type: text`, and metadata that includes the assistant reply. + The response can be plain text or JSON with `reply`, `text`, `content`, or OpenAI-style `choices[0].message.content`. """ ) @@ -823,6 +944,8 @@ def clear_state(profile_data: dict[str, Any]): assistant_reply, assistant_webhook_url, assistant_webhook_token, + capture_webhook_url, + capture_webhook_token, assistant_context, backend_mode, reference_audio, @@ -926,6 +1049,8 @@ def launch( default_profile_name: str = "reddit-female", backend_url: str = DEFAULT_BACKEND_URL, backend_token: str = DEFAULT_BACKEND_TOKEN, + capture_url: str = DEFAULT_CAPTURE_URL, + capture_token: str = DEFAULT_CAPTURE_TOKEN, backend_context: str = DEFAULT_BACKEND_CONTEXT, backend_mode: str = DEFAULT_BACKEND_MODE if DEFAULT_BACKEND_MODE in {"auto", "openai", "custom"} else "auto", ): @@ -947,6 +1072,10 @@ def launch( os.environ["PCA_BACKEND_URL"] = backend_url if _clean_text(backend_token): os.environ["PCA_BACKEND_TOKEN"] = backend_token + if _clean_text(capture_url): + os.environ["PCA_CAPTURE_URL"] = capture_url + if _clean_text(capture_token): + os.environ["PCA_CAPTURE_TOKEN"] = capture_token if _clean_text(backend_context): os.environ["PCA_ASSISTANT_CONTEXT"] = backend_context if _clean_text(backend_mode): @@ -974,6 +1103,8 @@ def main(): parser.add_argument("--default-profile-name", type=str, default="reddit-female") parser.add_argument("--backend-url", type=str, default=DEFAULT_BACKEND_URL) parser.add_argument("--backend-token", type=str, default=DEFAULT_BACKEND_TOKEN) + parser.add_argument("--capture-url", type=str, default=DEFAULT_CAPTURE_URL) + parser.add_argument("--capture-token", type=str, default=DEFAULT_CAPTURE_TOKEN) parser.add_argument("--backend-context", type=str, default=DEFAULT_BACKEND_CONTEXT) parser.add_argument( "--backend-mode", @@ -995,6 +1126,8 @@ def main(): default_profile_name=args.default_profile_name, backend_url=args.backend_url, backend_token=args.backend_token, + capture_url=args.capture_url, + capture_token=args.capture_token, backend_context=args.backend_context, backend_mode=args.backend_mode, ) diff --git a/tests/test_phone_assistant.py b/tests/test_phone_assistant.py index dc827706..4ee89331 100644 --- a/tests/test_phone_assistant.py +++ b/tests/test_phone_assistant.py @@ -1,7 +1,9 @@ from voxcpm.phone_assistant import ( _build_backend_messages, + _build_capture_payload, _build_final_text, _extract_reply_text, + _extract_capture_receipt, _profile_path, ) @@ -23,6 +25,26 @@ def test_extract_reply_text_supports_openai_style_response(): assert _extract_reply_text(payload) == "assistant reply" +def test_build_capture_payload_uses_phone_capture_contract(): + payload = _build_capture_payload( + user_message="Hello PCA", + assistant_reply="Cloned reply", + history=[("u1", "a1")], + profile_name="reddit-female", + backend_mode="custom", + ) + assert payload["source"] == "iphone" + assert payload["capture_type"] == "text" + assert payload["content"] == "Hello PCA" + assert payload["metadata"]["assistant_reply"] == "Cloned reply" + assert payload["metadata"]["voice_profile"] == "reddit-female" + assert payload["metadata"]["turn_count"] == 2 + + +def test_extract_capture_receipt_supports_capture_id(): + assert _extract_capture_receipt({"capture_id": "cap_123"}) == "cap_123" + + def test_profile_path_sanitizes_name(): assert _profile_path("My Voice!").name == "myvoice.json" From 638c6ec8b78955ef80f218b7e984386774401ad9 Mon Sep 17 00:00:00 2001 From: James Juniper <217263268+jjuniper-dev@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:28:20 -0400 Subject: [PATCH 3/5] fix: align capture payload with pca webhook schema --- README.md | 2 +- src/voxcpm/phone_assistant.py | 6 +++++- tests/test_phone_assistant.py | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 593b8f4a..818f7299 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ If you want phone microphone dictation, install the optional ASR dependency sepa You can also save a named voice enrollment once and reuse it later from the app's profile panel. By default, the assistant loads the saved `reddit-female` profile if it exists. To wire in your PCA backend automatically, set `PCA_BACKEND_URL` plus optional `PCA_BACKEND_TOKEN`, `PCA_ASSISTANT_CONTEXT`, and `PCA_BACKEND_MODE=auto|openai|custom` before launching. -To stage the phone message into PCA as a capture event, also set `PCA_CAPTURE_URL` plus optional `PCA_CAPTURE_TOKEN`. The assistant sends `source: iphone`, `capture_type: text`, the user message, and capture metadata that includes the assistant reply. +To stage the phone message into PCA as a capture event, also set `PCA_CAPTURE_URL` plus optional `PCA_CAPTURE_TOKEN`. The assistant sends the PCA contract fields `source: iphone`, `capture_type: text`, `timestamp`, and `text`, plus capture metadata that includes the assistant reply. ### 🚢 Production Deployment (Nano-vLLM) diff --git a/src/voxcpm/phone_assistant.py b/src/voxcpm/phone_assistant.py index 243e8cad..4ab33b8c 100644 --- a/src/voxcpm/phone_assistant.py +++ b/src/voxcpm/phone_assistant.py @@ -6,6 +6,7 @@ import os import sys import shutil +from datetime import datetime, timezone from pathlib import Path from typing import Any, Optional @@ -250,7 +251,10 @@ def _build_capture_payload( return { "source": "iphone", "capture_type": "text", - "content": _clean_text(user_message), + "timestamp": datetime.now(timezone.utc).isoformat(), + "text": _clean_text(user_message), + "context_note": "Captured from VoxCPM phone assistant", + "tags": [tag for tag in ("voxcpm-phone-assistant", _clean_text(profile_name)) if tag], "metadata": { "assistant_reply": _clean_text(assistant_reply), "voice_profile": _clean_text(profile_name), diff --git a/tests/test_phone_assistant.py b/tests/test_phone_assistant.py index 4ee89331..d4166ba2 100644 --- a/tests/test_phone_assistant.py +++ b/tests/test_phone_assistant.py @@ -35,7 +35,10 @@ def test_build_capture_payload_uses_phone_capture_contract(): ) assert payload["source"] == "iphone" assert payload["capture_type"] == "text" - assert payload["content"] == "Hello PCA" + assert payload["text"] == "Hello PCA" + assert payload["timestamp"] + assert payload["context_note"] == "Captured from VoxCPM phone assistant" + assert "voxcpm-phone-assistant" in payload["tags"] assert payload["metadata"]["assistant_reply"] == "Cloned reply" assert payload["metadata"]["voice_profile"] == "reddit-female" assert payload["metadata"]["turn_count"] == 2 From 8815c656cca53c1204e9eabed64ba415749f4691 Mon Sep 17 00:00:00 2001 From: jjuniper-dev Date: Sun, 12 Jul 2026 16:01:38 +0000 Subject: [PATCH 4/5] fix: conform phone capture payload to PCA schema and fix send output arity The capture payload emitted by the phone assistant did not match schemas/pca_capture_event.schema.json (additionalProperties: false, required classification/provenance, content not text), so a validating WF10/WF-Dispatch gateway would reject every capture with HTTP 400 and the failure was swallowed silently. Rebuild the payload to the contract: source=iphone_shortcut, capture_type, timestamp, content, classification (env-overridable via PCA_CAPTURE_CLASSIFICATION, default confidential), and provenance. Off-contract keys (text/context_note/metadata) are dropped. Also fix the "Send & Speak" handler: send_btn declared 7 outputs but prepare_turn returned 6, so Gradio raised on every click. Return profile_data as the 7th value. Update tests and README to the conformant contract. Claude-Session: https://claude.ai/code/session_01RwWk3CSfGsNqpdr47PEMFN --- README.md | 2 +- src/voxcpm/phone_assistant.py | 37 ++++++++++++++++++-------- tests/test_phone_assistant.py | 49 ++++++++++++++++++++++++++++++----- 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 818f7299..c74651fa 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ If you want phone microphone dictation, install the optional ASR dependency sepa You can also save a named voice enrollment once and reuse it later from the app's profile panel. By default, the assistant loads the saved `reddit-female` profile if it exists. To wire in your PCA backend automatically, set `PCA_BACKEND_URL` plus optional `PCA_BACKEND_TOKEN`, `PCA_ASSISTANT_CONTEXT`, and `PCA_BACKEND_MODE=auto|openai|custom` before launching. -To stage the phone message into PCA as a capture event, also set `PCA_CAPTURE_URL` plus optional `PCA_CAPTURE_TOKEN`. The assistant sends the PCA contract fields `source: iphone`, `capture_type: text`, `timestamp`, and `text`, plus capture metadata that includes the assistant reply. +To stage the phone message into PCA as a capture event, also set `PCA_CAPTURE_URL` plus optional `PCA_CAPTURE_TOKEN`. The assistant POSTs a PCA capture event that conforms to the PCA capture schema: `source: iphone_shortcut`, `capture_type: text`, `timestamp`, `content` (the user message), `classification`, and `provenance`. Set `PCA_CAPTURE_CLASSIFICATION` (`public|internal|confidential|restricted`, default `confidential`) to control routing sensitivity. ### 🚢 Production Deployment (Nano-vLLM) diff --git a/src/voxcpm/phone_assistant.py b/src/voxcpm/phone_assistant.py index 4ab33b8c..046bbfae 100644 --- a/src/voxcpm/phone_assistant.py +++ b/src/voxcpm/phone_assistant.py @@ -46,6 +46,12 @@ DEFAULT_CAPTURE_URL = os.environ.get("PCA_CAPTURE_URL", "").strip() DEFAULT_CAPTURE_TOKEN = os.environ.get("PCA_CAPTURE_TOKEN", "").strip() +CAPTURE_CLASSIFICATIONS = {"public", "internal", "confidential", "restricted"} +_capture_classification = os.environ.get("PCA_CAPTURE_CLASSIFICATION", "confidential").strip().lower() +DEFAULT_CAPTURE_CLASSIFICATION = ( + _capture_classification if _capture_classification in CAPTURE_CLASSIFICATIONS else "confidential" +) + APP_THEME = gr.themes.Soft( primary_hue="cyan", secondary_hue="orange", @@ -247,21 +253,29 @@ def _build_capture_payload( history: list[tuple[str, str]], profile_name: str, backend_mode: str, + classification: str = DEFAULT_CAPTURE_CLASSIFICATION, ) -> dict[str, Any]: + """Build a PCA capture event conforming to pca_capture_event.schema.json. + + The gateway validates against that schema with additionalProperties: false, + so only the contract fields are emitted. `assistant_reply`, `backend_mode` + and turn count are not part of the capture contract and are intentionally + dropped — the capture records the user's phone message, not VoxCPM state. + """ + classification = _clean_text(classification).lower() + if classification not in CAPTURE_CLASSIFICATIONS: + classification = "confidential" + return { - "source": "iphone", + "source": "iphone_shortcut", "capture_type": "text", "timestamp": datetime.now(timezone.utc).isoformat(), - "text": _clean_text(user_message), - "context_note": "Captured from VoxCPM phone assistant", - "tags": [tag for tag in ("voxcpm-phone-assistant", _clean_text(profile_name)) if tag], - "metadata": { - "assistant_reply": _clean_text(assistant_reply), - "voice_profile": _clean_text(profile_name), - "backend_mode": _clean_text(backend_mode).lower() or "auto", - "turn_count": len(history) + 1, - "origin": "voxcpm-phone-assistant", + "content": _clean_text(user_message), + "classification": classification, + "provenance": { + "agent": "voxcpm-phone-assistant", }, + "tags": [tag for tag in ("voxcpm-phone-assistant", _clean_text(profile_name)) if tag], } @@ -737,6 +751,7 @@ def prepare_turn( reference_transcript, (sample_rate, wav), status, + profile_data, ) def speak_reply( @@ -934,7 +949,7 @@ def clear_state(profile_data: dict[str, Any]): Custom mode sends `{"message": "...", "history": [...], "assistant_context": "..."}`. - If a PCA capture URL is configured, the phone message is also staged with `source: iphone`, `capture_type: text`, and metadata that includes the assistant reply. + If a PCA capture URL is configured, the phone message is also staged as a PCA capture event (`source: iphone_shortcut`, `capture_type: text`, `content`, `classification`, and `provenance`) conforming to the PCA capture schema. The response can be plain text or JSON with `reply`, `text`, `content`, or OpenAI-style `choices[0].message.content`. """ diff --git a/tests/test_phone_assistant.py b/tests/test_phone_assistant.py index d4166ba2..35773215 100644 --- a/tests/test_phone_assistant.py +++ b/tests/test_phone_assistant.py @@ -25,7 +25,7 @@ def test_extract_reply_text_supports_openai_style_response(): assert _extract_reply_text(payload) == "assistant reply" -def test_build_capture_payload_uses_phone_capture_contract(): +def test_build_capture_payload_matches_pca_schema(): payload = _build_capture_payload( user_message="Hello PCA", assistant_reply="Cloned reply", @@ -33,15 +33,50 @@ def test_build_capture_payload_uses_phone_capture_contract(): profile_name="reddit-female", backend_mode="custom", ) - assert payload["source"] == "iphone" + # Required fields per schemas/pca_capture_event.schema.json. + assert payload["source"] == "iphone_shortcut" assert payload["capture_type"] == "text" - assert payload["text"] == "Hello PCA" assert payload["timestamp"] - assert payload["context_note"] == "Captured from VoxCPM phone assistant" + assert payload["content"] == "Hello PCA" + assert payload["classification"] in {"public", "internal", "confidential", "restricted"} + assert payload["provenance"]["agent"] == "voxcpm-phone-assistant" assert "voxcpm-phone-assistant" in payload["tags"] - assert payload["metadata"]["assistant_reply"] == "Cloned reply" - assert payload["metadata"]["voice_profile"] == "reddit-female" - assert payload["metadata"]["turn_count"] == 2 + # Off-contract keys rejected by the gateway (additionalProperties: false) + # must not be emitted. + assert set(payload) <= { + "source", + "capture_type", + "timestamp", + "content", + "classification", + "provenance", + "tags", + } + assert "text" not in payload + assert "metadata" not in payload + assert "context_note" not in payload + + +def test_build_capture_payload_classification_override_validates(): + ok = _build_capture_payload( + user_message="hi", + assistant_reply="", + history=[], + profile_name="p", + backend_mode="auto", + classification="internal", + ) + assert ok["classification"] == "internal" + + fallback = _build_capture_payload( + user_message="hi", + assistant_reply="", + history=[], + profile_name="p", + backend_mode="auto", + classification="bogus", + ) + assert fallback["classification"] == "confidential" def test_extract_capture_receipt_supports_capture_id(): From cc834cfcdd47b8d07190659299a7a274c71a0f90 Mon Sep 17 00:00:00 2001 From: jjuniper-dev Date: Mon, 13 Jul 2026 12:43:47 +0000 Subject: [PATCH 5/5] refactor: generalize phone assistant to a neutral webhook Decouple the phone assistant bridge from PCA so it can be upstreamed: the PCA-specific ingestion schema mapping now lives downstream, not in this public TTS project. - Rename env/UI config to neutral names: ASSISTANT_BACKEND_URL/TOKEN, ASSISTANT_CONTEXT, ASSISTANT_BACKEND_MODE, ASSISTANT_BACKEND_MODEL, CAPTURE_WEBHOOK_URL/TOKEN. No "PCA" identifiers remain in the code. - Capture webhook now POSTs a neutral, backend-agnostic event ({source, type, timestamp, message, reply, profile, turn}); mapping it onto any ingestion schema is the receiving webhook's job. - OpenAI-compatible backend model name is configurable (default "assistant") instead of hardcoded "pca". - Drop the personal "reddit-female" default profile; --default-profile-name now defaults to empty (no auto-load). - Mask backend/capture tokens in the UI (type="password"). - Update tests and README to the neutral contract. Claude-Session: https://claude.ai/code/session_01RwWk3CSfGsNqpdr47PEMFN --- README.md | 16 ++++-- src/voxcpm/phone_assistant.py | 100 +++++++++++++++------------------- tests/test_phone_assistant.py | 60 +++++--------------- 3 files changed, 71 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index c74651fa..14bd8d89 100644 --- a/README.md +++ b/README.md @@ -257,10 +257,18 @@ voxcpm-assistant --port 8809 Use a clean reference sample plus an exact transcript for the highest-quality cloned voice. If you connect a webhook, the app POSTs the user message and conversation history and expects a reply in plain text or in a `reply` field. If you want phone microphone dictation, install the optional ASR dependency separately with `pip install funasr`. -You can also save a named voice enrollment once and reuse it later from the app's profile panel. -By default, the assistant loads the saved `reddit-female` profile if it exists. -To wire in your PCA backend automatically, set `PCA_BACKEND_URL` plus optional `PCA_BACKEND_TOKEN`, `PCA_ASSISTANT_CONTEXT`, and `PCA_BACKEND_MODE=auto|openai|custom` before launching. -To stage the phone message into PCA as a capture event, also set `PCA_CAPTURE_URL` plus optional `PCA_CAPTURE_TOKEN`. The assistant POSTs a PCA capture event that conforms to the PCA capture schema: `source: iphone_shortcut`, `capture_type: text`, `timestamp`, `content` (the user message), `classification`, and `provenance`. Set `PCA_CAPTURE_CLASSIFICATION` (`public|internal|confidential|restricted`, default `confidential`) to control routing sensitivity. +You can also save a named voice enrollment once and reuse it later from the app's profile panel. Pass `--default-profile-name ` to auto-load a saved profile on start. + +To wire in an assistant backend automatically, set `ASSISTANT_BACKEND_URL` plus optional `ASSISTANT_BACKEND_TOKEN`, `ASSISTANT_CONTEXT`, `ASSISTANT_BACKEND_MODE=auto|openai|custom`, and `ASSISTANT_BACKEND_MODEL` (model name for OpenAI-compatible endpoints, default `assistant`) before launching. + +To stage each turn to a capture webhook, also set `CAPTURE_WEBHOOK_URL` plus optional `CAPTURE_WEBHOOK_TOKEN`. The assistant POSTs a neutral, backend-agnostic capture event: + +```json +{"source": "voxcpm-phone-assistant", "type": "text", "timestamp": "...", + "message": "...", "reply": "...", "profile": "...", "turn": 1} +``` + +Mapping this onto a specific ingestion schema (field renames, classification, validation) is the receiving webhook's responsibility. ### 🚢 Production Deployment (Nano-vLLM) diff --git a/src/voxcpm/phone_assistant.py b/src/voxcpm/phone_assistant.py index 046bbfae..ac5cf940 100644 --- a/src/voxcpm/phone_assistant.py +++ b/src/voxcpm/phone_assistant.py @@ -39,18 +39,13 @@ DEFAULT_MODEL_ID = "openbmb/VoxCPM2" DEFAULT_ASR_MODEL_ID = "iic/SenseVoiceSmall" PROFILE_DIR = Path.cwd() / ".voxcpm-phone-profiles" -DEFAULT_BACKEND_URL = os.environ.get("PCA_BACKEND_URL", "").strip() -DEFAULT_BACKEND_TOKEN = os.environ.get("PCA_BACKEND_TOKEN", "").strip() -DEFAULT_BACKEND_CONTEXT = os.environ.get("PCA_ASSISTANT_CONTEXT", "").strip() -DEFAULT_BACKEND_MODE = os.environ.get("PCA_BACKEND_MODE", "auto").strip().lower() -DEFAULT_CAPTURE_URL = os.environ.get("PCA_CAPTURE_URL", "").strip() -DEFAULT_CAPTURE_TOKEN = os.environ.get("PCA_CAPTURE_TOKEN", "").strip() - -CAPTURE_CLASSIFICATIONS = {"public", "internal", "confidential", "restricted"} -_capture_classification = os.environ.get("PCA_CAPTURE_CLASSIFICATION", "confidential").strip().lower() -DEFAULT_CAPTURE_CLASSIFICATION = ( - _capture_classification if _capture_classification in CAPTURE_CLASSIFICATIONS else "confidential" -) +DEFAULT_BACKEND_URL = os.environ.get("ASSISTANT_BACKEND_URL", "").strip() +DEFAULT_BACKEND_TOKEN = os.environ.get("ASSISTANT_BACKEND_TOKEN", "").strip() +DEFAULT_BACKEND_CONTEXT = os.environ.get("ASSISTANT_CONTEXT", "").strip() +DEFAULT_BACKEND_MODE = os.environ.get("ASSISTANT_BACKEND_MODE", "auto").strip().lower() +DEFAULT_BACKEND_MODEL = os.environ.get("ASSISTANT_BACKEND_MODEL", "assistant").strip() or "assistant" +DEFAULT_CAPTURE_URL = os.environ.get("CAPTURE_WEBHOOK_URL", "").strip() +DEFAULT_CAPTURE_TOKEN = os.environ.get("CAPTURE_WEBHOOK_TOKEN", "").strip() APP_THEME = gr.themes.Soft( primary_hue="cyan", @@ -253,29 +248,21 @@ def _build_capture_payload( history: list[tuple[str, str]], profile_name: str, backend_mode: str, - classification: str = DEFAULT_CAPTURE_CLASSIFICATION, ) -> dict[str, Any]: - """Build a PCA capture event conforming to pca_capture_event.schema.json. + """Build a neutral capture event for the configured capture webhook. - The gateway validates against that schema with additionalProperties: false, - so only the contract fields are emitted. `assistant_reply`, `backend_mode` - and turn count are not part of the capture contract and are intentionally - dropped — the capture records the user's phone message, not VoxCPM state. + This is a generic, backend-agnostic shape. Mapping it onto a specific + downstream ingestion schema (field renames, classification, provenance, + validation) is the receiving webhook's responsibility, not VoxCPM's. """ - classification = _clean_text(classification).lower() - if classification not in CAPTURE_CLASSIFICATIONS: - classification = "confidential" - return { - "source": "iphone_shortcut", - "capture_type": "text", + "source": "voxcpm-phone-assistant", + "type": "text", "timestamp": datetime.now(timezone.utc).isoformat(), - "content": _clean_text(user_message), - "classification": classification, - "provenance": { - "agent": "voxcpm-phone-assistant", - }, - "tags": [tag for tag in ("voxcpm-phone-assistant", _clean_text(profile_name)) if tag], + "message": _clean_text(user_message), + "reply": _clean_text(assistant_reply), + "profile": _clean_text(profile_name), + "turn": len(history) + 1, } @@ -470,6 +457,7 @@ def request_reply( history: list[tuple[str, str]], assistant_context: str, backend_mode: str = "auto", + backend_model: str = DEFAULT_BACKEND_MODEL, timeout: int = 30, ) -> str: backend_url = _clean_text(backend_url) @@ -483,7 +471,7 @@ def request_reply( if openai_compatible: payload = { - "model": "pca", + "model": _clean_text(backend_model) or "assistant", "messages": _build_backend_messages(user_message, history, assistant_context), "temperature": 0.4, "stream": False, @@ -573,12 +561,12 @@ def _history_to_chatbot(history: list[tuple[str, str]]) -> list[tuple[str, str]] def build_interface(runtime: PhoneAssistantRuntime, default_profile_data: dict[str, Any] | None = None): default_profile_data = default_profile_data or {} - backend_url_default = _clean_text(os.environ.get("PCA_BACKEND_URL", DEFAULT_BACKEND_URL)) - backend_token_default = _clean_text(os.environ.get("PCA_BACKEND_TOKEN", DEFAULT_BACKEND_TOKEN)) - backend_context_default = _clean_text(os.environ.get("PCA_ASSISTANT_CONTEXT", DEFAULT_BACKEND_CONTEXT)) - backend_mode_default = _clean_text(os.environ.get("PCA_BACKEND_MODE", DEFAULT_BACKEND_MODE)).lower() or "auto" - capture_url_default = _clean_text(os.environ.get("PCA_CAPTURE_URL", DEFAULT_CAPTURE_URL)) - capture_token_default = _clean_text(os.environ.get("PCA_CAPTURE_TOKEN", DEFAULT_CAPTURE_TOKEN)) + backend_url_default = _clean_text(os.environ.get("ASSISTANT_BACKEND_URL", DEFAULT_BACKEND_URL)) + backend_token_default = _clean_text(os.environ.get("ASSISTANT_BACKEND_TOKEN", DEFAULT_BACKEND_TOKEN)) + backend_context_default = _clean_text(os.environ.get("ASSISTANT_CONTEXT", DEFAULT_BACKEND_CONTEXT)) + backend_mode_default = _clean_text(os.environ.get("ASSISTANT_BACKEND_MODE", DEFAULT_BACKEND_MODE)).lower() or "auto" + capture_url_default = _clean_text(os.environ.get("CAPTURE_WEBHOOK_URL", DEFAULT_CAPTURE_URL)) + capture_token_default = _clean_text(os.environ.get("CAPTURE_WEBHOOK_TOKEN", DEFAULT_CAPTURE_TOKEN)) if backend_mode_default not in {"auto", "openai", "custom"}: backend_mode_default = "auto" @@ -708,9 +696,9 @@ def prepare_turn( profile_name=_clean_text(profile_data.get("name", "default")) or "default", backend_mode=backend_mode, ) - capture_status = "Capture staged in PCA." + capture_status = "Capture staged." if capture_receipt: - capture_status = f"Capture staged in PCA ({capture_receipt})." + capture_status = f"Capture staged ({capture_receipt})." except Exception as exc: logger.warning("Capture staging failed: %s", exc) capture_status = f"Capture staging failed: {exc}" @@ -737,7 +725,7 @@ def prepare_turn( if reference_audio and not reference_transcript and transcript: reference_transcript = transcript - history = history + [(f"You: {user_text_clean}", f"PCA: {assistant_reply}")] + history = history + [(f"You: {user_text_clean}", f"Assistant: {assistant_reply}")] status = "Generated cloned speech." if reference_audio and transcript: status = "Generated cloned speech with transcript-guided cloning." @@ -883,24 +871,26 @@ def clear_state(profile_data: dict[str, Any]): with gr.Accordion("Advanced", open=False): assistant_webhook_url = gr.Textbox( - label="PCA backend URL", + label="Assistant backend URL", value=backend_url_default, placeholder="OpenAI-compatible /v1/chat/completions or a custom webhook endpoint.", ) assistant_webhook_token = gr.Textbox( - label="PCA backend token", + label="Assistant backend token", value=backend_token_default, placeholder="Optional bearer token.", + type="password", ) capture_webhook_url = gr.Textbox( - label="PCA capture URL", + label="Capture webhook URL", value=capture_url_default, - placeholder="Optional capture webhook URL for staging the phone message in PCA.", + placeholder="Optional webhook URL for staging the phone message downstream.", ) capture_webhook_token = gr.Textbox( - label="PCA capture token", + label="Capture webhook token", value=capture_token_default, placeholder="Optional bearer token for the capture webhook.", + type="password", ) assistant_context = gr.Textbox( label="Assistant context", @@ -945,11 +935,11 @@ def clear_state(profile_data: dict[str, Any]): """ **Backend payloads** - OpenAI-compatible mode sends `{"model":"pca","messages":[...],"temperature":0.4,"stream":false}`. + OpenAI-compatible mode sends `{"model":"assistant","messages":[...],"temperature":0.4,"stream":false}` (model name is configurable). Custom mode sends `{"message": "...", "history": [...], "assistant_context": "..."}`. - If a PCA capture URL is configured, the phone message is also staged as a PCA capture event (`source: iphone_shortcut`, `capture_type: text`, `content`, `classification`, and `provenance`) conforming to the PCA capture schema. + If a capture webhook URL is configured, the turn is also POSTed as a neutral capture event (`source`, `type`, `timestamp`, `message`, `reply`, `profile`, `turn`). Mapping it onto any downstream ingestion schema is the receiving webhook's job. The response can be plain text or JSON with `reply`, `text`, `content`, or OpenAI-style `choices[0].message.content`. """ @@ -1065,7 +1055,7 @@ def launch( no_denoiser: bool = False, optimize: bool = True, zipenhancer_path: Optional[str] = None, - default_profile_name: str = "reddit-female", + default_profile_name: str = "", backend_url: str = DEFAULT_BACKEND_URL, backend_token: str = DEFAULT_BACKEND_TOKEN, capture_url: str = DEFAULT_CAPTURE_URL, @@ -1088,17 +1078,17 @@ def launch( logger.warning("Default profile not found: %s", default_profile_name) if _clean_text(backend_url): - os.environ["PCA_BACKEND_URL"] = backend_url + os.environ["ASSISTANT_BACKEND_URL"] = backend_url if _clean_text(backend_token): - os.environ["PCA_BACKEND_TOKEN"] = backend_token + os.environ["ASSISTANT_BACKEND_TOKEN"] = backend_token if _clean_text(capture_url): - os.environ["PCA_CAPTURE_URL"] = capture_url + os.environ["CAPTURE_WEBHOOK_URL"] = capture_url if _clean_text(capture_token): - os.environ["PCA_CAPTURE_TOKEN"] = capture_token + os.environ["CAPTURE_WEBHOOK_TOKEN"] = capture_token if _clean_text(backend_context): - os.environ["PCA_ASSISTANT_CONTEXT"] = backend_context + os.environ["ASSISTANT_CONTEXT"] = backend_context if _clean_text(backend_mode): - os.environ["PCA_BACKEND_MODE"] = backend_mode + os.environ["ASSISTANT_BACKEND_MODE"] = backend_mode demo = build_interface(runtime, default_profile_data=default_profile_data) demo.queue(max_size=16, default_concurrency_limit=1).launch( @@ -1119,7 +1109,7 @@ def main(): parser.add_argument("--no-denoiser", action="store_true") parser.add_argument("--no-optimize", action="store_true") parser.add_argument("--zipenhancer-path", type=str, default=None) - parser.add_argument("--default-profile-name", type=str, default="reddit-female") + parser.add_argument("--default-profile-name", type=str, default="") parser.add_argument("--backend-url", type=str, default=DEFAULT_BACKEND_URL) parser.add_argument("--backend-token", type=str, default=DEFAULT_BACKEND_TOKEN) parser.add_argument("--capture-url", type=str, default=DEFAULT_CAPTURE_URL) diff --git a/tests/test_phone_assistant.py b/tests/test_phone_assistant.py index 35773215..fa774f13 100644 --- a/tests/test_phone_assistant.py +++ b/tests/test_phone_assistant.py @@ -25,58 +25,26 @@ def test_extract_reply_text_supports_openai_style_response(): assert _extract_reply_text(payload) == "assistant reply" -def test_build_capture_payload_matches_pca_schema(): +def test_build_capture_payload_is_neutral_shape(): payload = _build_capture_payload( - user_message="Hello PCA", + user_message="Hello there", assistant_reply="Cloned reply", history=[("u1", "a1")], - profile_name="reddit-female", + profile_name="my-voice", backend_mode="custom", ) - # Required fields per schemas/pca_capture_event.schema.json. - assert payload["source"] == "iphone_shortcut" - assert payload["capture_type"] == "text" + # Neutral, backend-agnostic shape. Mapping onto a downstream ingestion + # schema is the receiving webhook's responsibility, not VoxCPM's. + assert payload["source"] == "voxcpm-phone-assistant" + assert payload["type"] == "text" assert payload["timestamp"] - assert payload["content"] == "Hello PCA" - assert payload["classification"] in {"public", "internal", "confidential", "restricted"} - assert payload["provenance"]["agent"] == "voxcpm-phone-assistant" - assert "voxcpm-phone-assistant" in payload["tags"] - # Off-contract keys rejected by the gateway (additionalProperties: false) - # must not be emitted. - assert set(payload) <= { - "source", - "capture_type", - "timestamp", - "content", - "classification", - "provenance", - "tags", - } - assert "text" not in payload - assert "metadata" not in payload - assert "context_note" not in payload - - -def test_build_capture_payload_classification_override_validates(): - ok = _build_capture_payload( - user_message="hi", - assistant_reply="", - history=[], - profile_name="p", - backend_mode="auto", - classification="internal", - ) - assert ok["classification"] == "internal" - - fallback = _build_capture_payload( - user_message="hi", - assistant_reply="", - history=[], - profile_name="p", - backend_mode="auto", - classification="bogus", - ) - assert fallback["classification"] == "confidential" + assert payload["message"] == "Hello there" + assert payload["reply"] == "Cloned reply" + assert payload["profile"] == "my-voice" + assert payload["turn"] == 2 + # No downstream-specific coupling leaks into the neutral payload. + assert "classification" not in payload + assert "provenance" not in payload def test_extract_capture_receipt_supports_capture_id():