From 150dd5125fb961deda818637ab8b2bdb4c740555 Mon Sep 17 00:00:00 2001
From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com>
Date: Fri, 6 Feb 2026 19:22:19 +0000
Subject: [PATCH 1/4] Enhance setup utilities and wizard functionality
- Introduced `detect_tailscale_info` function to automatically retrieve Tailscale DNS name and IP address, improving user experience for service configuration.
- Added `detect_cuda_version` function to identify the system's CUDA version, streamlining compatibility checks for GPU-based services.
- Updated `wizard.py` to utilize the new detection functions, enhancing service selection and configuration processes based on user input.
- Improved error handling and user feedback in service setup, ensuring clearer communication during configuration steps.
- Refactored existing code to improve maintainability and code reuse across setup utilities.
---
backends/advanced/init.py | 103 ++++++++++++++++++++---------
extras/asr-services/init.py | 60 +++++------------
extras/speaker-recognition/init.py | 64 +++---------------
setup_utils.py | 85 +++++++++++++++++++++++-
wizard.py | 89 +++++++++++++++++++------
5 files changed, 248 insertions(+), 153 deletions(-)
diff --git a/backends/advanced/init.py b/backends/advanced/init.py
index 06c0ad7a..aad7ff0e 100644
--- a/backends/advanced/init.py
+++ b/backends/advanced/init.py
@@ -15,7 +15,7 @@
from pathlib import Path
from typing import Any, Dict
-from dotenv import get_key, set_key
+from dotenv import set_key
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Confirm, Prompt
@@ -24,12 +24,9 @@
# Add repo root to path for imports
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from config_manager import ConfigManager
-from setup_utils import (
- prompt_password as util_prompt_password,
- prompt_with_existing_masked,
- mask_value,
- read_env_value
-)
+from setup_utils import detect_tailscale_info, mask_value
+from setup_utils import prompt_password as util_prompt_password
+from setup_utils import prompt_with_existing_masked, read_env_value
class ChronicleSetup:
@@ -174,24 +171,39 @@ def setup_authentication(self):
self.console.print("Configure admin account for the dashboard")
self.console.print()
- self.config["ADMIN_EMAIL"] = self.prompt_value("Admin email", "admin@example.com")
- self.config["ADMIN_PASSWORD"] = self.prompt_password("Admin password (min 8 chars)")
- self.config["AUTH_SECRET_KEY"] = secrets.token_hex(32)
+ # Read existing values for re-run support
+ existing_email = self.read_existing_env_value("ADMIN_EMAIL")
+ default_email = existing_email if existing_email else "admin@example.com"
+ self.config["ADMIN_EMAIL"] = self.prompt_value("Admin email", default_email)
+
+ # Allow reusing existing admin password
+ existing_password = self.read_existing_env_value("ADMIN_PASSWORD")
+ if existing_password:
+ password = prompt_with_existing_masked(
+ prompt_text="Admin password (min 8 chars)",
+ existing_value=existing_password,
+ is_password=True,
+ )
+ self.config["ADMIN_PASSWORD"] = password
+ else:
+ self.config["ADMIN_PASSWORD"] = self.prompt_password("Admin password (min 8 chars)")
+
+ # Preserve existing AUTH_SECRET_KEY to avoid invalidating JWTs
+ existing_secret = self.read_existing_env_value("AUTH_SECRET_KEY")
+ if existing_secret:
+ self.config["AUTH_SECRET_KEY"] = existing_secret
+ self.console.print("[blue][INFO][/blue] Reusing existing AUTH_SECRET_KEY (existing JWT tokens remain valid)")
+ else:
+ self.config["AUTH_SECRET_KEY"] = secrets.token_hex(32)
self.console.print("[green][SUCCESS][/green] Admin account configured")
def setup_transcription(self):
"""Configure transcription provider - updates config.yml and .env"""
- self.print_section("Speech-to-Text Configuration")
-
- self.console.print("[blue][INFO][/blue] Provider selection is configured in config.yml (defaults.stt)")
- self.console.print("[blue][INFO][/blue] API keys are stored in .env")
- self.console.print()
-
# Check if transcription provider was provided via command line
if hasattr(self.args, 'transcription_provider') and self.args.transcription_provider:
provider = self.args.transcription_provider
- self.console.print(f"[green][SUCCESS][/green] Transcription provider configured via wizard: {provider}")
+ self.console.print(f"[green]✅[/green] Transcription: {provider} (configured via wizard)")
# Map provider to choice
if provider == "deepgram":
@@ -205,6 +217,12 @@ def setup_transcription(self):
else:
choice = "1" # Default to Deepgram
else:
+ self.print_section("Speech-to-Text Configuration")
+
+ self.console.print("[blue][INFO][/blue] Provider selection is configured in config.yml (defaults.stt)")
+ self.console.print("[blue][INFO][/blue] API keys are stored in .env")
+ self.console.print()
+
# Interactive prompt
is_macos = platform.system() == 'Darwin'
@@ -395,13 +413,20 @@ def setup_memory(self):
def setup_optional_services(self):
"""Configure optional services"""
- self.print_section("Optional Services")
-
# Check if speaker service URL provided via args
- if hasattr(self.args, 'speaker_service_url') and self.args.speaker_service_url:
+ has_speaker_arg = hasattr(self.args, 'speaker_service_url') and self.args.speaker_service_url
+ has_asr_arg = hasattr(self.args, 'parakeet_asr_url') and self.args.parakeet_asr_url
+
+ if has_speaker_arg:
self.config["SPEAKER_SERVICE_URL"] = self.args.speaker_service_url
- self.console.print(f"[green][SUCCESS][/green] Speaker Recognition configured via args: {self.args.speaker_service_url}")
- else:
+ self.console.print(f"[green]✅[/green] Speaker Recognition: {self.args.speaker_service_url} (configured via wizard)")
+
+ if has_asr_arg:
+ self.config["PARAKEET_ASR_URL"] = self.args.parakeet_asr_url
+ self.console.print(f"[green]✅[/green] Parakeet ASR: {self.args.parakeet_asr_url} (configured via wizard)")
+
+ # Only show interactive section if not all configured via args
+ if not has_speaker_arg:
try:
enable_speaker = Confirm.ask("Enable Speaker Recognition?", default=False)
except EOFError:
@@ -414,11 +439,6 @@ def setup_optional_services(self):
self.console.print("[green][SUCCESS][/green] Speaker Recognition configured")
self.console.print("[blue][INFO][/blue] Start with: cd ../../extras/speaker-recognition && docker compose up -d")
- # Check if ASR service URL provided via args
- if hasattr(self.args, 'parakeet_asr_url') and self.args.parakeet_asr_url:
- self.config["PARAKEET_ASR_URL"] = self.args.parakeet_asr_url
- self.console.print(f"[green][SUCCESS][/green] Parakeet ASR configured via args: {self.args.parakeet_asr_url}")
-
# Check if Tailscale auth key provided via args
if hasattr(self.args, 'ts_authkey') and self.args.ts_authkey:
self.config["TS_AUTHKEY"] = self.args.ts_authkey
@@ -434,6 +454,8 @@ def setup_obsidian(self):
if not neo4j_password:
self.console.print("[yellow][WARNING][/yellow] --enable-obsidian provided but no password")
neo4j_password = self.prompt_password("Neo4j password (min 8 chars)")
+
+ self.console.print(f"[green]✅[/green] Obsidian/Neo4j: enabled (configured via wizard)")
else:
# Interactive prompt (fallback)
self.console.print()
@@ -557,7 +579,7 @@ def setup_https(self):
if hasattr(self.args, 'enable_https') and self.args.enable_https:
enable_https = True
server_ip = getattr(self.args, 'server_ip', 'localhost')
- self.console.print(f"[green][SUCCESS][/green] HTTPS configured via command line: {server_ip}")
+ self.console.print(f"[green]✅[/green] HTTPS: {server_ip} (configured via wizard)")
else:
# Interactive configuration
self.print_section("HTTPS Configuration (Optional)")
@@ -570,16 +592,32 @@ def setup_https(self):
if enable_https:
self.console.print("[blue][INFO][/blue] HTTPS enables microphone access in browsers")
- self.console.print("[blue][INFO][/blue] For distributed deployments, use your Tailscale IP (e.g., 100.64.1.2)")
+
+ # Try to auto-detect Tailscale address
+ ts_dns, ts_ip = detect_tailscale_info()
+
+ if ts_dns:
+ self.console.print(f"[green][AUTO-DETECTED][/green] Tailscale DNS: {ts_dns}")
+ if ts_ip:
+ self.console.print(f"[green][AUTO-DETECTED][/green] Tailscale IP: {ts_ip}")
+ default_address = ts_dns
+ elif ts_ip:
+ self.console.print(f"[green][AUTO-DETECTED][/green] Tailscale IP: {ts_ip}")
+ default_address = ts_ip
+ else:
+ self.console.print("[blue][INFO][/blue] Tailscale not detected")
+ self.console.print("[blue][INFO][/blue] To find your Tailscale address: tailscale status --json | jq -r '.Self.DNSName'")
+ default_address = "localhost"
+
self.console.print("[blue][INFO][/blue] For local-only access, use 'localhost'")
# Use the new masked prompt function (not masked for IP, but shows existing)
server_ip = self.prompt_with_existing_masked(
- prompt_text="Server IP/Domain for SSL certificate (Tailscale IP or localhost)",
+ prompt_text="Server IP/Domain for SSL certificate",
env_key="SERVER_IP",
placeholders=['localhost', 'your-server-ip-here'],
is_password=False,
- default="localhost"
+ default=default_address
)
if enable_https:
@@ -790,7 +828,8 @@ def run(self):
"""Run the complete setup process"""
self.print_header("🚀 Chronicle Interactive Setup")
self.console.print("This wizard will help you configure Chronicle with all necessary services.")
- self.console.print("We'll ask for your API keys and preferences step by step.")
+ self.console.print("[dim]Safe to run again — it backs up your config and preserves previous values.[/dim]")
+ self.console.print("[dim]When unsure, just press Enter — the defaults will work.[/dim]")
self.console.print()
try:
diff --git a/extras/asr-services/init.py b/extras/asr-services/init.py
index c8cb0145..fd8e14d9 100755
--- a/extras/asr-services/init.py
+++ b/extras/asr-services/init.py
@@ -7,16 +7,13 @@
import argparse
import os
import platform
-import re
import shutil
-import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
from dotenv import set_key
-from rich import print as rprint
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Confirm, Prompt
@@ -26,7 +23,8 @@
# Add repo root to path for imports
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from config_manager import ConfigManager
-
+from setup_utils import detect_cuda_version as _detect_cuda_version
+from setup_utils import read_env_value
# Provider and model definitions
PROVIDERS = {
@@ -127,14 +125,8 @@ def prompt_choice(self, prompt: str, choices: Dict[str, str], default: str = "1"
return default
def read_existing_env_value(self, key: str) -> Optional[str]:
- """Read a value from existing .env file"""
- env_path = Path(".env")
- if not env_path.exists():
- return None
-
- from dotenv import get_key
- value = get_key(str(env_path), key)
- return value if value else None
+ """Read a value from existing .env file (delegates to shared utility)"""
+ return read_env_value(".env", key)
def backup_existing_env(self):
"""Backup existing .env file"""
@@ -146,32 +138,18 @@ def backup_existing_env(self):
self.console.print(f"[blue][INFO][/blue] Backed up existing .env file to {backup_path}")
def detect_cuda_version(self) -> str:
- """Detect system CUDA version from nvidia-smi"""
- try:
- result = subprocess.run(
- ["nvidia-smi"],
- capture_output=True,
- text=True,
- timeout=5
- )
- if result.returncode == 0:
- output = result.stdout
- match = re.search(r'CUDA Version:\s*(\d+)\.(\d+)', output)
- if match:
- major, minor = match.groups()
- cuda_ver = f"{major}.{minor}"
- if cuda_ver >= "12.8":
- return "cu128"
- elif cuda_ver >= "12.6":
- return "cu126"
- elif cuda_ver >= "12.1":
- return "cu121"
- except (subprocess.SubprocessError, FileNotFoundError):
- pass
- return "cu126"
+ """Detect system CUDA version (delegates to shared utility)"""
+ return _detect_cuda_version(default="cu126")
def select_provider(self) -> str:
"""Select ASR provider"""
+ # Check for command-line provider first (skip interactive UI)
+ if hasattr(self.args, 'provider') and self.args.provider:
+ provider = self.args.provider
+ provider_name = PROVIDERS.get(provider, {}).get('name', provider)
+ self.console.print(f"[green]✅[/green] ASR Provider: {provider_name} (configured via wizard)")
+ return provider
+
self.print_section("Provider Selection")
# Show provider comparison table
@@ -203,12 +181,6 @@ def select_provider(self) -> str:
self.console.print(table)
self.console.print()
- # Check for command-line provider
- if hasattr(self.args, 'provider') and self.args.provider:
- provider = self.args.provider
- self.console.print(f"[green][SUCCESS][/green] Provider set from command line: {provider}")
- return provider
-
provider_choices = {
"1": "vibevoice - Microsoft VibeVoice-ASR (Built-in diarization)",
"2": "faster-whisper - Fast Whisper inference (Recommended for general use)",
@@ -222,8 +194,6 @@ def select_provider(self) -> str:
def select_model(self, provider: str) -> str:
"""Select model for the chosen provider"""
- self.print_section(f"Model Selection ({PROVIDERS[provider]['name']})")
-
provider_info = PROVIDERS[provider]
models = provider_info["models"]
default_model = provider_info["default_model"]
@@ -231,9 +201,11 @@ def select_model(self, provider: str) -> str:
# Check for command-line model
if hasattr(self.args, 'model') and self.args.model:
model = self.args.model
- self.console.print(f"[green][SUCCESS][/green] Model set from command line: {model}")
+ self.console.print(f"[green]✅[/green] ASR Model: {model} (configured via wizard)")
return model
+ self.print_section(f"Model Selection ({PROVIDERS[provider]['name']})")
+
# Show available models
self.console.print(f"[blue]Available models for {provider_info['name']}:[/blue]")
model_choices = {}
diff --git a/extras/speaker-recognition/init.py b/extras/speaker-recognition/init.py
index 649238a9..34edb58b 100755
--- a/extras/speaker-recognition/init.py
+++ b/extras/speaker-recognition/init.py
@@ -16,12 +16,15 @@
from typing import Any, Dict
from dotenv import set_key
-from rich import print as rprint
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Confirm, Prompt
from rich.text import Text
+# Add repo root to path for imports
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
+from setup_utils import detect_cuda_version, mask_value, read_env_value
+
class SpeakerRecognitionSetup:
def __init__(self, args=None):
@@ -67,25 +70,12 @@ def prompt_password(self, prompt: str) -> str:
sys.exit(1)
def read_existing_env_value(self, key: str) -> str:
- """Read a value from existing .env file"""
- env_path = Path(".env")
- if not env_path.exists():
- return None
-
- from dotenv import get_key
- value = get_key(str(env_path), key)
- # get_key returns None if key doesn't exist or value is empty
- return value if value else None
+ """Read a value from existing .env file (delegates to shared utility)"""
+ return read_env_value(".env", key)
def mask_api_key(self, key: str, show_chars: int = 5) -> str:
- """Mask API key showing only first and last few characters"""
- if not key or len(key) <= show_chars * 2:
- return key
-
- # Remove quotes if present
- key_clean = key.strip("'\"")
-
- return f"{key_clean[:show_chars]}{'*' * min(15, len(key_clean) - show_chars * 2)}{key_clean[-show_chars:]}"
+ """Mask API key (delegates to shared utility)"""
+ return mask_value(key, show_chars)
def prompt_choice(self, prompt: str, choices: Dict[str, str], default: str = "1") -> str:
"""Prompt for a choice from options"""
@@ -148,42 +138,8 @@ def setup_hf_token(self):
self.console.print("[green][SUCCESS][/green] HF Token configured")
def detect_cuda_version(self) -> str:
- """Detect system CUDA version from nvidia-smi"""
- try:
- result = subprocess.run(
- ["nvidia-smi", "--query-gpu=driver_version", "--format=csv,noheader"],
- capture_output=True,
- text=True,
- timeout=5
- )
- if result.returncode == 0:
- # Try to get CUDA version from nvidia-smi
- result = subprocess.run(
- ["nvidia-smi"],
- capture_output=True,
- text=True,
- timeout=5
- )
- if result.returncode == 0:
- output = result.stdout
- # Parse CUDA Version from nvidia-smi output
- # Format: "CUDA Version: 12.6"
- import re
- match = re.search(r'CUDA Version:\s*(\d+)\.(\d+)', output)
- if match:
- major, minor = match.groups()
- cuda_ver = f"{major}.{minor}"
-
- # Map to available PyTorch CUDA versions
- if cuda_ver >= "12.8":
- return "cu128"
- elif cuda_ver >= "12.6":
- return "cu126"
- elif cuda_ver >= "12.1":
- return "cu121"
- except (subprocess.SubprocessError, FileNotFoundError):
- pass
- return "cu121" # Default fallback
+ """Detect system CUDA version (delegates to shared utility)"""
+ return detect_cuda_version(default="cu121")
def setup_compute_mode(self):
"""Configure compute mode (CPU/GPU)"""
diff --git a/setup_utils.py b/setup_utils.py
index 200bd1e7..e9072c76 100644
--- a/setup_utils.py
+++ b/setup_utils.py
@@ -6,9 +6,12 @@
"""
import getpass
+import json
+import re
import secrets
+import subprocess
from pathlib import Path
-from typing import List, Optional
+from typing import List, Optional, Tuple
from dotenv import get_key
@@ -324,3 +327,83 @@ def prompt_token(
placeholders=placeholders,
is_password=True
)
+
+
+def detect_tailscale_info() -> Tuple[Optional[str], Optional[str]]:
+ """
+ Detect Tailscale DNS name and IPv4 address.
+
+ Returns:
+ (dns_name, ip) tuple. dns_name is the MagicDNS hostname (e.g. "myhost.tail1234.ts.net"),
+ ip is the Tailscale IPv4 address (e.g. "100.64.1.5").
+ Either or both may be None if Tailscale is not available.
+ """
+ dns_name = None
+ ip = None
+
+ # Get MagicDNS name from tailscale status --json
+ try:
+ result = subprocess.run(
+ ["tailscale", "status", "--json"],
+ capture_output=True,
+ text=True,
+ timeout=5
+ )
+ if result.returncode == 0:
+ status = json.loads(result.stdout)
+ raw_dns = status.get("Self", {}).get("DNSName", "")
+ # DNSName has trailing dot, strip it
+ if raw_dns:
+ dns_name = raw_dns.rstrip(".")
+ except (subprocess.SubprocessError, FileNotFoundError, json.JSONDecodeError):
+ pass
+
+ # Get IPv4 address as fallback
+ try:
+ result = subprocess.run(
+ ["tailscale", "ip", "-4"],
+ capture_output=True,
+ text=True,
+ timeout=5
+ )
+ if result.returncode == 0:
+ ip = result.stdout.strip()
+ except (subprocess.SubprocessError, FileNotFoundError):
+ pass
+
+ return dns_name, ip
+
+
+def detect_cuda_version(default: str = "cu126") -> str:
+ """
+ Detect system CUDA version from nvidia-smi output.
+
+ Parses "CUDA Version: X.Y" from nvidia-smi and maps to PyTorch CUDA version strings.
+
+ Args:
+ default: Default CUDA version if detection fails (default: "cu126")
+
+ Returns:
+ PyTorch CUDA version string: "cu121", "cu126", or "cu128"
+ """
+ try:
+ result = subprocess.run(
+ ["nvidia-smi"],
+ capture_output=True,
+ text=True,
+ timeout=5
+ )
+ if result.returncode == 0:
+ match = re.search(r'CUDA Version:\s*(\d+)\.(\d+)', result.stdout)
+ if match:
+ major, minor = match.groups()
+ cuda_ver = f"{major}.{minor}"
+ if cuda_ver >= "12.8":
+ return "cu128"
+ elif cuda_ver >= "12.6":
+ return "cu126"
+ elif cuda_ver >= "12.1":
+ return "cu121"
+ except (subprocess.SubprocessError, FileNotFoundError):
+ pass
+ return default
diff --git a/wizard.py b/wizard.py
index 9583cb12..2ede3a6a 100755
--- a/wizard.py
+++ b/wizard.py
@@ -6,23 +6,20 @@
import shutil
import subprocess
-import sys
from datetime import datetime
from pathlib import Path
import yaml
-from rich import print as rprint
from rich.console import Console
from rich.prompt import Confirm, Prompt
# Import shared setup utilities
from setup_utils import (
- prompt_password,
- prompt_value,
+ detect_tailscale_info,
+ is_placeholder,
+ mask_value,
prompt_with_existing_masked,
read_env_value,
- mask_value,
- is_placeholder
)
console = Console()
@@ -112,36 +109,46 @@ def check_service_exists(service_name, service_config):
return True, "OK"
-def select_services():
+def select_services(transcription_provider=None):
"""Let user select which services to setup"""
console.print("🚀 [bold cyan]Chronicle Service Setup[/bold cyan]")
console.print("Select which services to configure:\n")
-
+
selected = []
-
+
# Backend is required
console.print("📱 [bold]Backend (Required):[/bold]")
console.print(" ✅ Advanced Backend - Full AI features")
selected.append('advanced')
-
+
+ # Services that will be auto-added based on transcription provider choice
+ auto_added = set()
+ if transcription_provider in ("parakeet", "vibevoice"):
+ auto_added.add('asr-services')
+
# Optional extras
console.print("\n🔧 [bold]Optional Services:[/bold]")
for service_name, service_config in SERVICES['extras'].items():
+ # Skip services that will be auto-added based on earlier choices
+ if service_name in auto_added:
+ console.print(f" ✅ {service_config['description']} [dim](auto-selected for {transcription_provider})[/dim]")
+ continue
+
# Check if service exists
exists, msg = check_service_exists(service_name, service_config)
if not exists:
console.print(f" ⏸️ {service_config['description']} - [dim]{msg}[/dim]")
continue
-
+
try:
enable_service = Confirm.ask(f" Setup {service_config['description']}?", default=False)
except EOFError:
console.print("Using default: No")
enable_service = False
-
+
if enable_service:
selected.append(service_name)
-
+
return selected
def cleanup_unselected_services(selected_services):
@@ -222,8 +229,18 @@ def run_service_setup(service_name, selected_services, https_enabled=False, serv
cmd.extend(['--compute-mode', compute_mode])
console.print(f"[blue][INFO][/blue] Found existing COMPUTE_MODE ({compute_mode}), reusing")
- # For asr-services, try to reuse PYTORCH_CUDA_VERSION from speaker-recognition
+ # For asr-services, pass provider from wizard's transcription choice and reuse CUDA version
if service_name == 'asr-services':
+ # Map wizard transcription provider to asr-services provider name
+ wizard_to_asr_provider = {
+ 'vibevoice': 'vibevoice',
+ 'parakeet': 'nemo',
+ }
+ asr_provider = wizard_to_asr_provider.get(transcription_provider)
+ if asr_provider:
+ cmd.extend(['--provider', asr_provider])
+ console.print(f"[blue][INFO][/blue] Pre-selecting ASR provider: {asr_provider} (from wizard choice: {transcription_provider})")
+
speaker_env_path = 'extras/speaker-recognition/.env'
cuda_version = read_env_value(speaker_env_path, 'PYTORCH_CUDA_VERSION')
if cuda_version and cuda_version in ['cu121', 'cu126', 'cu128']:
@@ -259,12 +276,20 @@ def run_service_setup(service_name, selected_services, https_enabled=False, serv
except FileNotFoundError as e:
console.print(f"❌ {service_name} setup failed: {e}")
+ console.print(f"[yellow] Check that the service directory exists: {service['path']}[/yellow]")
+ console.print(f"[yellow] And that 'uv' is installed and on your PATH[/yellow]")
return False
except subprocess.TimeoutExpired as e:
- console.print(f"❌ {service_name} setup timed out after {e.timeout} seconds")
+ console.print(f"❌ {service_name} setup timed out after {e.timeout}s")
+ console.print(f"[yellow] Configuration may be partially written.[/yellow]")
+ console.print(f"[yellow] To retry just this service:[/yellow]")
+ console.print(f"[yellow] cd {service['path']} && {' '.join(service['cmd'])}[/yellow]")
return False
except subprocess.CalledProcessError as e:
console.print(f"❌ {service_name} setup failed with exit code {e.returncode}")
+ console.print(f"[yellow] Check the error output above for details.[/yellow]")
+ console.print(f"[yellow] To retry just this service:[/yellow]")
+ console.print(f"[yellow] cd {service['path']} && {' '.join(service['cmd'])}[/yellow]")
return False
except Exception as e:
console.print(f"❌ {service_name} setup failed: {e}")
@@ -399,7 +424,7 @@ def setup_hf_token_if_needed(selected_services):
HF_TOKEN string if provided, None otherwise
"""
# Check if any selected services need HF_TOKEN
- needs_hf_token = 'speaker-recognition' in selected_services or 'advanced' in selected_services
+ needs_hf_token = 'speaker-recognition' in selected_services
if not needs_hf_token:
return None
@@ -482,6 +507,9 @@ def select_transcription_provider():
def main():
"""Main orchestration logic"""
console.print("🎉 [bold green]Welcome to Chronicle![/bold green]\n")
+ console.print("[dim]This wizard is safe to run as many times as you like.[/dim]")
+ console.print("[dim]It backs up your existing config and preserves previously entered values.[/dim]")
+ console.print("[dim]When unsure, just press Enter — the defaults will work.[/dim]\n")
# Setup config file from template
setup_config_file()
@@ -495,8 +523,8 @@ def main():
# Ask about transcription provider FIRST (determines which services are needed)
transcription_provider = select_transcription_provider()
- # Service Selection
- selected_services = select_services()
+ # Service Selection (pass transcription_provider so we skip asking about ASR when already chosen)
+ selected_services = select_services(transcription_provider)
# Auto-add asr-services if local ASR was chosen (Parakeet or VibeVoice)
if transcription_provider in ("parakeet", "vibevoice") and 'asr-services' not in selected_services:
@@ -529,21 +557,38 @@ def main():
https_enabled = False
if https_enabled:
- console.print("\n[blue][INFO][/blue] For distributed deployments, use your Tailscale IP")
+ # Try to auto-detect Tailscale address
+ ts_dns, ts_ip = detect_tailscale_info()
+
+ if ts_dns:
+ console.print(f"\n[green][AUTO-DETECTED][/green] Tailscale DNS: {ts_dns}")
+ if ts_ip:
+ console.print(f"[green][AUTO-DETECTED][/green] Tailscale IP: {ts_ip}")
+ default_address = ts_dns
+ elif ts_ip:
+ console.print(f"\n[green][AUTO-DETECTED][/green] Tailscale IP: {ts_ip}")
+ default_address = ts_ip
+ else:
+ console.print("\n[blue][INFO][/blue] Tailscale not detected")
+ console.print("[blue][INFO][/blue] To find your Tailscale address: tailscale status --json | jq -r '.Self.DNSName'")
+ default_address = None
+
console.print("[blue][INFO][/blue] For local-only access, use 'localhost'")
- console.print("Examples: localhost, 100.64.1.2, your-domain.com")
+ console.print("Examples: localhost, myhost.tail1234.ts.net, 100.64.1.2")
# Check for existing SERVER_IP from backend .env
backend_env_path = 'backends/advanced/.env'
existing_ip = read_env_value(backend_env_path, 'SERVER_IP')
- # Use the new masked prompt function
+ # Use existing value, or auto-detected address, or localhost as default
+ effective_default = default_address or "localhost"
+
server_ip = prompt_with_existing_masked(
prompt_text="Server IP/Domain for SSL certificates",
existing_value=existing_ip,
placeholders=['localhost', 'your-server-ip-here'],
is_password=False,
- default="localhost"
+ default=effective_default
)
console.print(f"[green]✅[/green] HTTPS configured for: {server_ip}")
From 7c098c71eecff1e8a429ec125bdfefcb8026f7e8 Mon Sep 17 00:00:00 2001
From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com>
Date: Fri, 6 Feb 2026 19:41:20 +0000
Subject: [PATCH 2/4] Update ASR service capabilities and improve speaker
identification handling
- Modified the capabilities of the VibeVoice ASR provider to include 'speaker_identification' and 'long_form', enhancing its feature set.
- Adjusted the speaker identification logic in the VibeVoiceTranscriber to prevent double-prefixing and ensure accurate speaker representation.
- Updated protocol tests to reflect the expanded list of known ASR capabilities, ensuring comprehensive validation of reported features.
---
extras/asr-services/init.py | 2 +-
.../asr-services/providers/vibevoice/transcriber.py | 11 ++++++++---
tests/asr/protocol_tests.robot | 10 +++++++---
3 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/extras/asr-services/init.py b/extras/asr-services/init.py
index fd8e14d9..2c800609 100755
--- a/extras/asr-services/init.py
+++ b/extras/asr-services/init.py
@@ -37,7 +37,7 @@
"default_model": "microsoft/VibeVoice-ASR",
"service": "vibevoice-asr",
# Note: VibeVoice provides diarization but NOT word_timestamps
- "capabilities": ["segments", "diarization", "timestamps"],
+ "capabilities": ["timestamps", "diarization", "speaker_identification", "long_form"],
},
"faster-whisper": {
"name": "Faster-Whisper",
diff --git a/extras/asr-services/providers/vibevoice/transcriber.py b/extras/asr-services/providers/vibevoice/transcriber.py
index 17f6205d..16757f16 100644
--- a/extras/asr-services/providers/vibevoice/transcriber.py
+++ b/extras/asr-services/providers/vibevoice/transcriber.py
@@ -288,7 +288,7 @@ def _parse_vibevoice_output(self, raw_output: str) -> dict:
"text": seg.get("Content", ""),
"start": float(seg.get("Start", 0.0)),
"end": float(seg.get("End", 0.0)),
- "speaker": f"Speaker {seg.get('Speaker', 0)}",
+ "speaker": seg.get("Speaker", 0),
})
return {"raw_text": raw_output, "segments": segments}
@@ -317,8 +317,13 @@ def _map_to_result(self, processed: dict, raw_output: str) -> TranscriptionResul
start = seg_data.get("start_time", seg_data.get("start", 0.0))
end = seg_data.get("end_time", seg_data.get("end", 0.0))
speaker_raw = seg_data.get("speaker_id", seg_data.get("speaker"))
- # Convert speaker to string (VibeVoice returns int)
- speaker_id = f"Speaker {speaker_raw}" if speaker_raw is not None else None
+ # Convert speaker to string, avoiding double-prefix from fallback parser
+ if speaker_raw is None:
+ speaker_id = None
+ elif isinstance(speaker_raw, str) and speaker_raw.startswith("Speaker "):
+ speaker_id = speaker_raw
+ else:
+ speaker_id = f"Speaker {speaker_raw}"
if text:
text_parts.append(text)
diff --git a/tests/asr/protocol_tests.robot b/tests/asr/protocol_tests.robot
index 705b240d..8e6462da 100644
--- a/tests/asr/protocol_tests.robot
+++ b/tests/asr/protocol_tests.robot
@@ -158,13 +158,17 @@ ASR Capabilities Format Is Valid List
ASR Capabilities Are From Known Set
[Documentation] Verify reported capabilities are valid known capabilities
- ... Known capabilities: timestamps, word_timestamps, segments, diarization
+ ... Known capabilities: timestamps, word_timestamps, diarization,
+ ... speaker_identification, long_form, language_detection, vad_filter,
+ ... translation, chunked_processing
[Tags] infra
${info}= Get ASR Service Info ${ASR_URL}
- # Define known capabilities
- @{known_caps}= Create List timestamps word_timestamps segments diarization
+ # Define known capabilities (union of all provider capabilities + mock server)
+ @{known_caps}= Create List timestamps word_timestamps diarization
+ ... segments speaker_identification long_form language_detection
+ ... vad_filter translation chunked_processing
# All reported capabilities should be known
FOR ${cap} IN @{info}[capabilities]
From 7b37396be90a898f019ab191d477bf236d9df563 Mon Sep 17 00:00:00 2001
From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com>
Date: Fri, 6 Feb 2026 20:29:32 +0000
Subject: [PATCH 3/4] Refactor audio recording controls for improved UI and
functionality
- Replaced MicOff icon with Square icon in MainRecordingControls and SimplifiedControls for a more intuitive user experience.
- Enhanced button interactions to streamline recording start/stop actions, including a pulsing effect during recording.
- Updated status messages and button states to provide clearer feedback on recording status and actions.
- Improved accessibility by ensuring buttons are disabled appropriately based on recording state and microphone access.
---
.../audio/MainRecordingControls.tsx | 57 ++++++------
.../components/audio/SimplifiedControls.tsx | 93 ++++++++++---------
2 files changed, 82 insertions(+), 68 deletions(-)
diff --git a/backends/advanced/webui/src/components/audio/MainRecordingControls.tsx b/backends/advanced/webui/src/components/audio/MainRecordingControls.tsx
index 5a075829..bb1daff4 100644
--- a/backends/advanced/webui/src/components/audio/MainRecordingControls.tsx
+++ b/backends/advanced/webui/src/components/audio/MainRecordingControls.tsx
@@ -1,4 +1,4 @@
-import { Mic, MicOff } from 'lucide-react'
+import { Mic, Square } from 'lucide-react'
import { UseAudioRecordingReturn } from '../../hooks/useAudioRecording'
interface MainRecordingControlsProps {
@@ -11,45 +11,50 @@ export default function MainRecordingControls({ recording }: MainRecordingContro
return (
-
- {recording.isRecording ? (
-
- ) : (
-
- )}
+
+
+ {recording.isRecording && (
+
+ )}
+ {recording.isRecording ? (
+
+ ) : (
+
+ )}
+
{recording.isRecording ? 'Recording...' : 'Ready to Record'}
-
+
{recording.isRecording && (
-
+
{recording.formatDuration(recording.recordingDuration)}
)}
-
+
- {recording.isRecording
- ? `Audio streaming via ${isHttps ? 'WSS (secure)' : 'WS'} to backend for processing`
- : recording.canAccessMicrophone
- ? 'Click the microphone to start recording'
+ {recording.isRecording
+ ? `Click to stop \u00b7 Streaming via ${isHttps ? 'WSS (secure)' : 'WS'}`
+ : recording.canAccessMicrophone
+ ? 'Click to start recording'
: 'Secure connection required for microphone access'}
)
-}
\ No newline at end of file
+}
diff --git a/backends/advanced/webui/src/components/audio/SimplifiedControls.tsx b/backends/advanced/webui/src/components/audio/SimplifiedControls.tsx
index a3299deb..27ae58bc 100644
--- a/backends/advanced/webui/src/components/audio/SimplifiedControls.tsx
+++ b/backends/advanced/webui/src/components/audio/SimplifiedControls.tsx
@@ -1,4 +1,4 @@
-import { Mic, MicOff, Loader2 } from 'lucide-react'
+import { Mic, Square, Loader2 } from 'lucide-react'
import { RecordingContextType } from '../../contexts/RecordingContext'
interface SimplifiedControlsProps {
@@ -18,73 +18,82 @@ const getStepText = (step: string): string => {
}
}
-const getButtonColor = (step: string, isRecording: boolean): string => {
- if (step === 'error') return 'bg-red-600 hover:bg-red-700'
- if (isRecording) return 'bg-red-600 hover:bg-red-700'
- if (step === 'idle') return 'bg-blue-600 hover:bg-blue-700'
- return 'bg-yellow-600 hover:bg-yellow-700'
-}
-
const isProcessing = (step: string): boolean => {
return ['mic', 'websocket', 'audio-start', 'streaming', 'stopping'].includes(step)
}
export default function SimplifiedControls({ recording }: SimplifiedControlsProps) {
- const startButtonDisabled = !recording.canAccessMicrophone || isProcessing(recording.currentStep) || recording.isRecording
-
+ const processing = isProcessing(recording.currentStep)
+ const canStart = recording.canAccessMicrophone && !processing && !recording.isRecording
+
+ const handleClick = () => {
+ if (recording.isRecording) {
+ recording.stopRecording()
+ } else if (canStart) {
+ recording.startRecording()
+ }
+ }
+
+ // Button appearance based on state
+ const getButtonClasses = (): string => {
+ if (recording.isRecording) return 'bg-red-600 hover:bg-red-700'
+ if (processing) return 'bg-yellow-600'
+ if (recording.currentStep === 'error') return 'bg-red-600 hover:bg-red-700'
+ return 'bg-blue-600 hover:bg-blue-700'
+ }
+
+ const isDisabled = recording.isRecording ? false : (processing || !canStart)
+
return (
- {/* Control Buttons */}
-
- {/* START Button */}
-