408 lines
14 KiB
Python
408 lines
14 KiB
Python
import asyncio
|
|
import os
|
|
from typing import Any, cast
|
|
|
|
from nicegui import app, ui
|
|
|
|
from journal.core.config import (
|
|
BACKEND_MODE,
|
|
CHUNK_TOKEN_BUDGET,
|
|
CLOUDAI_API_KEY,
|
|
CLOUDAI_API_URL,
|
|
CLOUDAI_TIMEOUT,
|
|
CSHARP_SIDECAR_PATH,
|
|
EMBEDDING_API_URL,
|
|
EMBEDDING_MODEL_NAME,
|
|
FASTER_WHISPER_COMPUTE_TYPE,
|
|
FASTER_WHISPER_DEVICE,
|
|
LLAMA_CPP_MODEL,
|
|
LLAMA_CPP_TIMEOUT,
|
|
LLAMA_CPP_URL,
|
|
MODEL_CONTEXT_TOKENS,
|
|
NLP_BACKEND,
|
|
SERVER_CONTROL_FILE,
|
|
SPEECH_RECOGNITION_ENGINE,
|
|
WHISPER_MODEL_SIZE,
|
|
)
|
|
|
|
_DEFAULT_SETTINGS: dict[str, Any] = {
|
|
"backend_mode": BACKEND_MODE,
|
|
"csharp_sidecar_path": CSHARP_SIDECAR_PATH,
|
|
"nlp_backend": NLP_BACKEND,
|
|
"speech_engine": SPEECH_RECOGNITION_ENGINE,
|
|
"whisper_model": WHISPER_MODEL_SIZE,
|
|
"faster_whisper_device": FASTER_WHISPER_DEVICE,
|
|
"faster_whisper_compute_type": FASTER_WHISPER_COMPUTE_TYPE,
|
|
"llama_cpp_url": LLAMA_CPP_URL,
|
|
"llama_cpp_model": LLAMA_CPP_MODEL,
|
|
"llama_cpp_timeout": LLAMA_CPP_TIMEOUT,
|
|
"embedding_api_url": EMBEDDING_API_URL,
|
|
"embedding_model_name": EMBEDDING_MODEL_NAME,
|
|
"cloudai_api_url": CLOUDAI_API_URL,
|
|
"cloudai_api_key": CLOUDAI_API_KEY,
|
|
"cloudai_timeout": CLOUDAI_TIMEOUT,
|
|
"model_context_tokens": MODEL_CONTEXT_TOKENS,
|
|
"chunk_token_budget": CHUNK_TOKEN_BUDGET,
|
|
}
|
|
|
|
_ENV_KEYS: dict[str, str] = {
|
|
"backend_mode": "JOURNAL_BACKEND_MODE",
|
|
"csharp_sidecar_path": "JOURNAL_CSHARP_SIDECAR_PATH",
|
|
"nlp_backend": "JOURNAL_NLP_BACKEND",
|
|
"speech_engine": "JOURNAL_SPEECH_ENGINE",
|
|
"whisper_model": "JOURNAL_WHISPER_MODEL",
|
|
"faster_whisper_device": "JOURNAL_FASTER_WHISPER_DEVICE",
|
|
"faster_whisper_compute_type": "JOURNAL_FASTER_WHISPER_COMPUTE_TYPE",
|
|
"llama_cpp_url": "JOURNAL_LLAMA_CPP_URL",
|
|
"llama_cpp_model": "JOURNAL_LLAMA_CPP_MODEL",
|
|
"llama_cpp_timeout": "JOURNAL_LLAMA_CPP_TIMEOUT",
|
|
"embedding_api_url": "JOURNAL_EMBEDDING_API_URL",
|
|
"embedding_model_name": "JOURNAL_EMBEDDING_MODEL_NAME",
|
|
"cloudai_api_url": "JOURNAL_CLOUDAI_API_URL",
|
|
"cloudai_api_key": "JOURNAL_CLOUDAI_API_KEY",
|
|
"cloudai_timeout": "JOURNAL_CLOUDAI_TIMEOUT",
|
|
"model_context_tokens": "JOURNAL_MODEL_CONTEXT_TOKENS",
|
|
"chunk_token_budget": "JOURNAL_CHUNK_TOKEN_BUDGET",
|
|
}
|
|
|
|
|
|
def _ensure_defaults() -> None:
|
|
for key, value in _DEFAULT_SETTINGS.items():
|
|
app.storage.user.setdefault(key, value)
|
|
|
|
|
|
def _apply_env_from_storage() -> None:
|
|
for key, env_key in _ENV_KEYS.items():
|
|
value = app.storage.user.get(key, _DEFAULT_SETTINGS.get(key, ""))
|
|
os.environ[env_key] = str(value).strip()
|
|
|
|
|
|
def _on_text_change(user_key: str, env_key: str, label: str, restart_required: bool = False):
|
|
def handler(e: Any) -> None:
|
|
value = str(e.value or "").strip()
|
|
app.storage.user[user_key] = value
|
|
os.environ[env_key] = value
|
|
suffix = " Restart server to apply." if restart_required else ""
|
|
ui.notify(f"{label} updated.{suffix}")
|
|
|
|
return handler
|
|
|
|
|
|
def _on_int_change(user_key: str, env_key: str, label: str, restart_required: bool = False):
|
|
def handler(e: Any) -> None:
|
|
try:
|
|
value = int(float(cast(Any, e.value)))
|
|
except (TypeError, ValueError):
|
|
ui.notify(f"{label} must be a valid number.", type="negative")
|
|
return
|
|
|
|
if value <= 0:
|
|
ui.notify(f"{label} must be greater than 0.", type="negative")
|
|
return
|
|
|
|
app.storage.user[user_key] = value
|
|
os.environ[env_key] = str(value)
|
|
suffix = " Restart server to apply." if restart_required else ""
|
|
ui.notify(f"{label} updated.{suffix}")
|
|
|
|
return handler
|
|
|
|
|
|
def settings_dialog():
|
|
"""Creates a dialog for runtime/application settings."""
|
|
|
|
_ensure_defaults()
|
|
_apply_env_from_storage()
|
|
|
|
async def request_server_action(action: str) -> None:
|
|
try:
|
|
SERVER_CONTROL_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
SERVER_CONTROL_FILE.write_text(action, encoding="utf-8")
|
|
except Exception as error:
|
|
ui.notify(f"Failed to queue server action: {error}", type="negative")
|
|
return
|
|
|
|
if action == "restart":
|
|
ui.notify("Server restart requested...", type="warning")
|
|
else:
|
|
ui.notify("Server shutdown requested...", type="warning")
|
|
|
|
await asyncio.sleep(0.2)
|
|
app.shutdown()
|
|
|
|
async def restart_server() -> None:
|
|
await request_server_action("restart")
|
|
|
|
async def shutdown_server() -> None:
|
|
await request_server_action("shutdown")
|
|
|
|
with ui.dialog() as dialog, ui.card().classes("w-full max-w-[38rem] max-h-[85vh] overflow-y-auto"):
|
|
_ = ui.label("Settings").classes("text-xl font-bold")
|
|
_ = ui.markdown(
|
|
"Most values apply immediately. "
|
|
"Some runtime/module values are marked with restart hints."
|
|
).classes("text-xs text-gray-500")
|
|
|
|
_ = ui.separator().classes("my-2")
|
|
_ = ui.label("Backend").classes("text-lg font-medium")
|
|
_ = (
|
|
ui.select(
|
|
["csharp-hybrid", "python"],
|
|
label="Backend Mode",
|
|
value=cast(str, app.storage.user["backend_mode"]),
|
|
on_change=_on_text_change(
|
|
"backend_mode",
|
|
"JOURNAL_BACKEND_MODE",
|
|
"Backend mode",
|
|
restart_required=True,
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "backend_mode")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.input(
|
|
label="C# Sidecar Path (optional override)",
|
|
value=cast(str, app.storage.user["csharp_sidecar_path"]),
|
|
on_change=_on_text_change(
|
|
"csharp_sidecar_path",
|
|
"JOURNAL_CSHARP_SIDECAR_PATH",
|
|
"C# sidecar path",
|
|
restart_required=True,
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "csharp_sidecar_path")
|
|
.classes("w-full")
|
|
)
|
|
|
|
_ = ui.separator().classes("my-2")
|
|
_ = ui.label("AI: Local Model Endpoints").classes("text-lg font-medium")
|
|
_ = (
|
|
ui.input(
|
|
label="Llama.cpp URL",
|
|
value=cast(str, app.storage.user["llama_cpp_url"]),
|
|
on_change=_on_text_change(
|
|
"llama_cpp_url", "JOURNAL_LLAMA_CPP_URL", "Llama.cpp URL"
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "llama_cpp_url")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.input(
|
|
label="Llama.cpp Model",
|
|
value=cast(str, app.storage.user["llama_cpp_model"]),
|
|
on_change=_on_text_change(
|
|
"llama_cpp_model", "JOURNAL_LLAMA_CPP_MODEL", "Llama.cpp model"
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "llama_cpp_model")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.number(
|
|
label="Llama.cpp Timeout (ms)",
|
|
value=float(cast(str, app.storage.user["llama_cpp_timeout"])),
|
|
on_change=_on_int_change(
|
|
"llama_cpp_timeout", "JOURNAL_LLAMA_CPP_TIMEOUT", "Llama timeout"
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "llama_cpp_timeout")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.input(
|
|
label="Embedding API URL",
|
|
value=cast(str, app.storage.user["embedding_api_url"]),
|
|
on_change=_on_text_change(
|
|
"embedding_api_url", "JOURNAL_EMBEDDING_API_URL", "Embedding API URL"
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "embedding_api_url")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.input(
|
|
label="Embedding Model Name",
|
|
value=cast(str, app.storage.user["embedding_model_name"]),
|
|
on_change=_on_text_change(
|
|
"embedding_model_name",
|
|
"JOURNAL_EMBEDDING_MODEL_NAME",
|
|
"Embedding model name",
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "embedding_model_name")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.number(
|
|
label="Model Context Tokens",
|
|
value=float(cast(str, app.storage.user["model_context_tokens"])),
|
|
on_change=_on_int_change(
|
|
"model_context_tokens",
|
|
"JOURNAL_MODEL_CONTEXT_TOKENS",
|
|
"Model context tokens",
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "model_context_tokens")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.number(
|
|
label="Chunk Token Budget",
|
|
value=float(cast(str, app.storage.user["chunk_token_budget"])),
|
|
on_change=_on_int_change(
|
|
"chunk_token_budget",
|
|
"JOURNAL_CHUNK_TOKEN_BUDGET",
|
|
"Chunk token budget",
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "chunk_token_budget")
|
|
.classes("w-full")
|
|
)
|
|
|
|
_ = ui.separator().classes("my-2")
|
|
_ = ui.label("AI: Cloud Endpoint").classes("text-lg font-medium")
|
|
_ = (
|
|
ui.input(
|
|
label="Cloud AI URL",
|
|
value=cast(str, app.storage.user["cloudai_api_url"]),
|
|
on_change=_on_text_change(
|
|
"cloudai_api_url", "JOURNAL_CLOUDAI_API_URL", "Cloud AI URL"
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "cloudai_api_url")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.input(
|
|
label="Cloud AI API Key",
|
|
password=True,
|
|
password_toggle_button=True,
|
|
value=cast(str, app.storage.user["cloudai_api_key"]),
|
|
on_change=_on_text_change(
|
|
"cloudai_api_key", "JOURNAL_CLOUDAI_API_KEY", "Cloud AI API key"
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "cloudai_api_key")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.number(
|
|
label="Cloud AI Timeout (s)",
|
|
value=float(cast(str, app.storage.user["cloudai_timeout"])),
|
|
on_change=_on_int_change(
|
|
"cloudai_timeout", "JOURNAL_CLOUDAI_TIMEOUT", "Cloud AI timeout"
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "cloudai_timeout")
|
|
.classes("w-full")
|
|
)
|
|
|
|
_ = ui.separator().classes("my-2")
|
|
_ = ui.label("NLP + Speech").classes("text-lg font-medium")
|
|
_ = (
|
|
ui.select(
|
|
["auto", "spacy", "fallback"],
|
|
label="NLP Backend",
|
|
value=cast(str, app.storage.user["nlp_backend"]),
|
|
on_change=_on_text_change(
|
|
"nlp_backend",
|
|
"JOURNAL_NLP_BACKEND",
|
|
"NLP backend",
|
|
restart_required=True,
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "nlp_backend")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.select(
|
|
["faster-whisper", "whisper", "google", "sphinx"],
|
|
label="Speech Recognition Engine",
|
|
value=cast(str, app.storage.user["speech_engine"]),
|
|
on_change=_on_text_change(
|
|
"speech_engine", "JOURNAL_SPEECH_ENGINE", "Speech engine"
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "speech_engine")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.select(
|
|
["tiny", "base", "small", "medium", "large"],
|
|
label="Whisper Model Size",
|
|
value=cast(str, app.storage.user["whisper_model"]),
|
|
on_change=_on_text_change(
|
|
"whisper_model", "JOURNAL_WHISPER_MODEL", "Whisper model"
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "whisper_model")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.select(
|
|
["auto", "cpu", "cuda"],
|
|
label="faster-whisper Device",
|
|
value=cast(str, app.storage.user["faster_whisper_device"]),
|
|
on_change=_on_text_change(
|
|
"faster_whisper_device",
|
|
"JOURNAL_FASTER_WHISPER_DEVICE",
|
|
"faster-whisper device",
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "faster_whisper_device")
|
|
.classes("w-full")
|
|
)
|
|
_ = (
|
|
ui.select(
|
|
["float32", "int8", "default", "auto", "float16", "int8_float32", "int8_float16"],
|
|
label="faster-whisper Compute Type",
|
|
value=cast(str, app.storage.user["faster_whisper_compute_type"]),
|
|
on_change=_on_text_change(
|
|
"faster_whisper_compute_type",
|
|
"JOURNAL_FASTER_WHISPER_COMPUTE_TYPE",
|
|
"faster-whisper compute type",
|
|
),
|
|
)
|
|
.bind_value(app.storage.user, "faster_whisper_compute_type")
|
|
.classes("w-full")
|
|
)
|
|
|
|
_ = ui.separator().classes("my-2")
|
|
_ = ui.label("Server Controls").classes("font-medium")
|
|
if cast(bool, app.storage.user.get("authenticated", False)):
|
|
_ = ui.markdown(
|
|
"Use these when running via `run_desktop.py`.\n"
|
|
"`Restart` reconnects quickly, `Shutdown` stops the server wrapper."
|
|
).classes("text-xs text-gray-500")
|
|
|
|
with ui.row().classes("w-full gap-2 mt-1"):
|
|
_ = (
|
|
ui.button(
|
|
"Restart Server",
|
|
on_click=restart_server,
|
|
)
|
|
.props("color=warning")
|
|
.classes("flex-1")
|
|
)
|
|
_ = (
|
|
ui.button(
|
|
"Shutdown Server",
|
|
on_click=shutdown_server,
|
|
)
|
|
.props("color=negative")
|
|
.classes("flex-1")
|
|
)
|
|
else:
|
|
_ = ui.label("Log in to enable restart/shutdown controls.").classes(
|
|
"text-xs text-gray-500"
|
|
)
|
|
|
|
_ = (
|
|
ui.button("Close", on_click=dialog.close)
|
|
.props("flat")
|
|
.classes("mt-4 self-end")
|
|
)
|
|
return dialog
|