← Files ClaraARCHIVED FILE
scripts/launch_hosted_voice.py
13.2 KB · Oct 2, 2026 · 00:29 UTC
"""Launch the hosted Clara voice service from the local plugin workflow."""
from __future__ import annotations
# Direct CLI calls must select dependencies before importing workflow modules.
if __name__ == "__main__":
import runpy as _runpy
from pathlib import Path as _Path
for _parent in _Path(__file__).resolve().parents:
_launcher = _parent / "scripts" / "self_relaunch.py"
if not _launcher.is_file():
_launcher = _parent / "clara" / "scripts" / "self_relaunch.py"
if _launcher.is_file():
_runpy.run_path(str(_launcher))["ensure_running_in_managed_venv"](__file__)
break
else:
# Standalone components retain their host's dependency setup.
if any(
(_p / "components.json").is_file()
for _p in _Path(__file__).resolve().parents
):
raise SystemExit(
"Managed Python launcher is missing; rebuild the plugin package."
)
import argparse
import logging
import os
import subprocess
import sys
import webbrowser
from pathlib import Path
from urllib.parse import urlencode, urlsplit, urlunsplit
from advisor_case_core import (
CASE_BRIEF_FILENAME,
SUPPORTED_LANGUAGES,
CaseWorkspaceError,
load_case_file,
refresh_case_brief,
validate_case_workspace,
)
__all__ = [
"DEFAULT_VOICE_LAUNCH_URL",
"SUPPORTED_TRANSCRIPTION_LANGUAGES",
"build_case_context",
"build_launch_url",
"build_voice_session_url",
"build_chrome_launch_args",
"main",
"open_launch_url",
"prepare_launch_url",
"read_private_text_file",
"validate_hosted_url",
]
LOGGER = logging.getLogger(__name__)
DEFAULT_VOICE_LAUNCH_URL = "https://mparanza.com/case-notes/voice/launch"
SUPPORTED_TRANSCRIPTION_LANGUAGES = {*SUPPORTED_LANGUAGES, "ar"}
MAX_CASE_CONTEXT_CHARS = 2_500
DEFAULT_CHROME_PROFILE_DIR = Path("/private/tmp/mparanza-case-notes-chrome-voice")
_HOSTED_HOST = "mparanza.com"
_TEST_LOCAL_HOSTS = {"127.0.0.1", "::1", "localhost"}
def read_private_text_file(path: Path, *, label: str) -> str:
"""Read one local secret after forcing owner-only file permissions."""
source = path.expanduser()
if source.is_symlink() or not source.is_file():
raise CaseWorkspaceError(f"{label} file must be a regular local file.")
try:
source.chmod(0o600)
value = source.read_text(encoding="utf-8").strip()
except OSError as exc:
raise CaseWorkspaceError(f"Could not read {label} file: {source}") from exc
if not value:
raise CaseWorkspaceError(f"{label} file is empty.")
return value
def build_case_context(
case_dir: Path,
max_chars: int = MAX_CASE_CONTEXT_CHARS,
*,
purpose: str = "transcription",
) -> str:
"""Return compact case context for hosted transcription prompts."""
if purpose != "transcription":
raise CaseWorkspaceError(f"Unsupported voice purpose: {purpose}")
brief_path = case_dir / CASE_BRIEF_FILENAME
if not brief_path.exists():
refresh_case_brief(case_dir)
brief = "\n".join(
line.rstrip() for line in brief_path.read_text(encoding="utf-8").splitlines()
).strip()
if len(brief) > max_chars:
return brief[:max_chars].rstrip() + "\n\n[Case brief truncated.]"
return brief
def validate_hosted_url(
value: str,
*,
required_path: str | None = None,
allow_query: bool = False,
allow_localhost_for_tests: bool = False,
) -> str:
"""Return a pinned Hosted Voice URL or reject the destination."""
clean = value.strip()
try:
parts = urlsplit(clean)
port = parts.port
except ValueError as exc:
raise CaseWorkspaceError(
"Hosted Voice destination is not a valid URL."
) from exc
is_mparanza = (
parts.scheme == "https"
and (parts.hostname or "").lower() == _HOSTED_HOST
and port in {None, 443}
)
is_test_local = (
allow_localhost_for_tests
and parts.scheme in {"http", "https"}
and (parts.hostname or "").lower() in _TEST_LOCAL_HOSTS
)
if not (is_mparanza or is_test_local):
raise CaseWorkspaceError(
"Hosted Voice destination must be https://mparanza.com."
)
if parts.username or parts.password or parts.fragment:
raise CaseWorkspaceError(
"Hosted Voice URL contains unsupported credentials or fragment."
)
if required_path is not None and parts.path != required_path:
raise CaseWorkspaceError(f"Hosted Voice URL must use the {required_path} path.")
if parts.query and not allow_query:
raise CaseWorkspaceError("Hosted Voice URL must not contain a query string.")
return clean
def build_launch_url(
base_url: str = DEFAULT_VOICE_LAUNCH_URL,
) -> str:
"""Return the context-free browser-authenticated launch URL."""
return validate_hosted_url(
base_url,
required_path="/case-notes/voice/launch",
)
def build_voice_session_url(
base_url: str,
*,
launch_token: str,
) -> str:
"""Return a Hosted Voice page URL containing only an opaque launch token."""
clean_token = launch_token.strip()
if not clean_token or any(
char not in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
for char in clean_token
):
raise CaseWorkspaceError("Hosted Voice returned an invalid launch token.")
parts = urlsplit(validate_hosted_url(base_url))
return urlunsplit(
(
parts.scheme,
parts.netloc,
"/case-notes/voice",
urlencode({"session": clean_token}),
"",
)
)
def prepare_launch_url(
case_dir: Path,
*,
server_url: str = DEFAULT_VOICE_LAUNCH_URL,
purpose: str = "transcription",
max_context_chars: int = MAX_CASE_CONTEXT_CHARS,
cookie_header: str = "",
magic_link: str = "",
language: str | None = None,
) -> tuple[str, bool]:
"""Prepare an authenticated context launch or an explicit context-free fallback."""
launch_base_url = build_launch_url(server_url)
if not (cookie_header.strip() or magic_link.strip()):
return launch_base_url, False
from upload_hosted_audio import (
_new_opener,
authenticate_with_magic_link,
bind_session_cookie,
request_context_launch_token,
)
refresh_case_brief(case_dir)
context = build_case_context(
case_dir,
max_chars=max_context_chars,
purpose=purpose,
)
manifest = load_case_file(case_dir, "manifest")
resolved_language = (
str(language or manifest.get("output_language") or "it").strip().lower()
)
if resolved_language not in SUPPORTED_TRANSCRIPTION_LANGUAGES:
raise CaseWorkspaceError(f"Unsupported voice language: {resolved_language}")
base_parts = urlsplit(launch_base_url)
hosted_base_url = urlunsplit((base_parts.scheme, base_parts.netloc, "", "", ""))
opener = _new_opener()
if magic_link.strip():
authenticate_with_magic_link(opener, magic_link)
if cookie_header.strip():
bind_session_cookie(
opener,
base_url=hosted_base_url,
cookie_header=cookie_header,
)
token = request_context_launch_token(
opener,
base_url=hosted_base_url,
case_context=context,
language=resolved_language,
)
return (
build_voice_session_url(hosted_base_url, launch_token=token),
True,
)
def build_chrome_launch_args(
launch_url: str,
*,
profile_dir: Path = DEFAULT_CHROME_PROFILE_DIR,
remote_debugging_port: int = 0,
auto_accept_microphone: bool = True,
allow_localhost_for_tests: bool = False,
) -> list[str]:
"""Return Chrome arguments for a clean Clara voice session."""
validate_hosted_url(
launch_url,
allow_query=True,
allow_localhost_for_tests=allow_localhost_for_tests,
)
args = [
f"--user-data-dir={profile_dir}",
"--no-first-run",
"--no-default-browser-check",
"--disable-search-engine-choice-screen",
"--autoplay-policy=no-user-gesture-required",
]
if remote_debugging_port > 0:
args.append(f"--remote-debugging-port={remote_debugging_port}")
if auto_accept_microphone:
args.append("--use-fake-ui-for-media-stream")
parts = urlsplit(launch_url)
if parts.scheme and parts.netloc:
args.append(
f"--unsafely-treat-insecure-origin-as-secure={parts.scheme}://{parts.netloc}"
)
args.append(launch_url)
return args
def open_launch_url(
launch_url: str,
*,
browser: str = "default",
chrome_profile_dir: Path = DEFAULT_CHROME_PROFILE_DIR,
chrome_remote_debugging_port: int = 0,
chrome_auto_accept_microphone: bool = True,
) -> None:
"""Open *launch_url* in the requested browser."""
validate_hosted_url(launch_url, allow_query=True)
if browser == "default":
webbrowser.open(launch_url)
return
if browser != "chrome":
raise CaseWorkspaceError(f"Unsupported browser: {browser}")
chrome_profile_dir.mkdir(parents=True, exist_ok=True)
chrome_args = build_chrome_launch_args(
launch_url,
profile_dir=chrome_profile_dir,
remote_debugging_port=chrome_remote_debugging_port,
auto_accept_microphone=chrome_auto_accept_microphone,
)
if sys.platform == "darwin":
command = ["/usr/bin/open", "-na", "Google Chrome", "--args", *chrome_args]
else:
chrome_executable = os.environ.get("GOOGLE_CHROME_BIN") or "google-chrome"
command = [chrome_executable, *chrome_args]
subprocess.Popen( # noqa: S603,S607 - arguments are constructed without a shell.
command,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def main() -> int:
"""Open the hosted voice launch URL."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("case_dir", type=Path)
parser.add_argument("--server-url", default=DEFAULT_VOICE_LAUNCH_URL)
parser.add_argument(
"--purpose",
choices=("transcription",),
default="transcription",
help="Open Clara Voice Capture for recording or uploaded-audio transcription.",
)
parser.add_argument(
"--language",
choices=sorted(SUPPORTED_TRANSCRIPTION_LANGUAGES),
help="Transcription language; defaults to the Clara case output language.",
)
parser.add_argument("--no-open", action="store_true")
parser.add_argument("--magic-link-file", type=Path)
parser.add_argument("--cookie-header-file", type=Path)
parser.add_argument("--browser", choices=("default", "chrome"), default="default")
parser.add_argument(
"--chrome-profile-dir",
type=Path,
default=DEFAULT_CHROME_PROFILE_DIR,
help="Dedicated Chrome profile directory used with --browser chrome.",
)
parser.add_argument(
"--chrome-remote-debugging-port",
type=int,
default=0,
help="Optional Chrome remote debugging port used with --browser chrome.",
)
parser.add_argument(
"--no-chrome-auto-accept-microphone",
action="store_true",
help="Do not auto-accept the local Chrome microphone prompt.",
)
parser.add_argument(
"--max-context-chars",
type=int,
default=MAX_CASE_CONTEXT_CHARS,
help="Maximum case context characters sent in the authenticated body.",
)
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(message)s")
errors = validate_case_workspace(args.case_dir)
if errors:
raise CaseWorkspaceError("; ".join(errors))
cookie_header = ""
if args.cookie_header_file:
cookie_header = read_private_text_file(
args.cookie_header_file,
label="session cookie",
)
magic_link = ""
if args.magic_link_file:
magic_link = read_private_text_file(
args.magic_link_file,
label="magic link",
)
launch_url, context_attached = prepare_launch_url(
args.case_dir,
server_url=args.server_url,
purpose=args.purpose,
max_context_chars=args.max_context_chars,
cookie_header=cookie_header,
magic_link=magic_link,
language=args.language,
)
if context_attached:
LOGGER.info(
"Hosted voice launch prepared with authenticated case context; "
"the opaque token is not printed."
)
else:
LOGGER.warning(
"No hosted authentication material was supplied. Opening the "
"browser-authenticated fallback without attaching case context."
)
LOGGER.info("Hosted voice destination: https://mparanza.com/case-notes/voice")
if not args.no_open:
open_launch_url(
launch_url,
browser=args.browser,
chrome_profile_dir=args.chrome_profile_dir,
chrome_remote_debugging_port=args.chrome_remote_debugging_port,
chrome_auto_accept_microphone=(not args.no_chrome_auto_accept_microphone),
)
LOGGER.info(
"After the browser downloads the voice bundle, import the newest valid "
"bundle from Downloads with scripts/import_latest_hosted_voice_bundle.py."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: bf73a10f86a388df87737528b9debce56b453a5d1cfa9d70665b85ed688b74b6