Make platform defaults OS-aware and balance whisper aliases

parent 52316b8b
...@@ -54,7 +54,10 @@ def get_or_create_secret(config_dir: Path) -> bytes: ...@@ -54,7 +54,10 @@ def get_or_create_secret(config_dir: Path) -> bytes:
secret = secrets.token_bytes(32) secret = secrets.token_bytes(32)
with open(secret_path, 'wb') as f: with open(secret_path, 'wb') as f:
f.write(secret) f.write(secret)
secret_path.chmod(0o600) try:
secret_path.chmod(0o600)
except OSError:
pass
return secret return secret
......
...@@ -25,6 +25,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse, Stre ...@@ -25,6 +25,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse, Stre
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from codai.admin.auth import SessionManager from codai.admin.auth import SessionManager
from codai.platform_paths import default_whisper_server_path
import queue as _q import queue as _q
import threading as _t import threading as _t
import uuid as _uuid import uuid as _uuid
...@@ -90,7 +91,7 @@ def _next_whisper_server_model_id(audio_models) -> str: ...@@ -90,7 +91,7 @@ def _next_whisper_server_model_id(audio_models) -> str:
def _default_whisper_server_path() -> str: def _default_whisper_server_path() -> str:
return shutil.which("whisper-server") or "/usr/local/bin/whisper-server" return shutil.which("whisper-server") or default_whisper_server_path()
def get_current_user(request: Request) -> Optional[str]: def get_current_user(request: Request) -> Optional[str]:
...@@ -239,7 +240,11 @@ async def admin_dashboard(request: Request, username: str = Depends(require_auth ...@@ -239,7 +240,11 @@ async def admin_dashboard(request: Request, username: str = Depends(require_auth
@router.get("/admin/models", response_class=HTMLResponse) @router.get("/admin/models", response_class=HTMLResponse)
async def models_page(request: Request, username: str = Depends(require_admin)): async def models_page(request: Request, username: str = Depends(require_admin)):
return _tmpl(request, "models.html", {"username": username, "is_admin": True}) return _tmpl(request, "models.html", {
"username": username,
"is_admin": True,
"default_whisper_server_path": _default_whisper_server_path(),
})
@router.get("/admin/tokens", response_class=HTMLResponse) @router.get("/admin/tokens", response_class=HTMLResponse)
...@@ -1434,12 +1439,14 @@ async def api_model_configure(request: Request, username: str = Depends(require_ ...@@ -1434,12 +1439,14 @@ async def api_model_configure(request: Request, username: str = Depends(require_
gpu_device = int(data.get("gpu_device", 0)) gpu_device = int(data.get("gpu_device", 0))
if gpu_device < 0: if gpu_device < 0:
raise HTTPException(status_code=400, detail="gpu_device must be >= 0") raise HTTPException(status_code=400, detail="gpu_device must be >= 0")
# Remove existing entry with same id (update semantics)
audio_list = config_manager.models_data.get("audio_models", []) audio_list = config_manager.models_data.get("audio_models", [])
config_manager.models_data["audio_models"] = [ if any(
m for m in audio_list isinstance(m, dict)
if not (isinstance(m, dict) and m.get("id") == model_id) and m.get("backend") == "whisper-server"
] and m.get("id") == model_id
for m in audio_list
):
raise HTTPException(status_code=409, detail=f"whisper-server model '{model_id}' already exists")
alias = (data.get("alias") or "").strip() or None alias = (data.get("alias") or "").strip() or None
entry = { entry = {
"id": model_id, "id": model_id,
......
...@@ -26,6 +26,9 @@ ...@@ -26,6 +26,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<script>
window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson }};
</script>
<div class="page-header"> <div class="page-header">
<div> <div>
<h1>Models</h1> <h1>Models</h1>
...@@ -1253,7 +1256,7 @@ function nextWhisperServerModelId(){ ...@@ -1253,7 +1256,7 @@ function nextWhisperServerModelId(){
} }
function defaultWhisperServerPath(){ function defaultWhisperServerPath(){
return '/usr/local/bin/whisper-server'; return window.__DEFAULT_WHISPER_SERVER_PATH__ || '';
} }
function resetWhisperServerBuilderDefaults(){ function resetWhisperServerBuilderDefaults(){
......
...@@ -40,6 +40,8 @@ from typing import List, Optional ...@@ -40,6 +40,8 @@ from typing import List, Optional
from fastapi import APIRouter, HTTPException, Request from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from codai.platform_paths import default_characters_dir, legacy_style_config_dir
router = APIRouter() router = APIRouter()
_CHARS_DIR: Optional[str] = None _CHARS_DIR: Optional[str] = None
...@@ -47,8 +49,8 @@ _CHARS_DIR: Optional[str] = None ...@@ -47,8 +49,8 @@ _CHARS_DIR: Optional[str] = None
def set_global_args(args): def set_global_args(args):
global _CHARS_DIR global _CHARS_DIR
base = getattr(args, 'file_path', None) or os.path.expanduser('~/.coderai') base = getattr(args, 'file_path', None) or str(legacy_style_config_dir())
root = base if os.path.isdir(base) else (os.path.dirname(base) if base else os.path.expanduser('~/.coderai')) root = base if os.path.isdir(base) else (os.path.dirname(base) if base else str(legacy_style_config_dir()))
_CHARS_DIR = os.path.join(root, 'characters') _CHARS_DIR = os.path.join(root, 'characters')
os.makedirs(_CHARS_DIR, exist_ok=True) os.makedirs(_CHARS_DIR, exist_ok=True)
...@@ -60,7 +62,7 @@ def set_global_file_path(path: str): ...@@ -60,7 +62,7 @@ def set_global_file_path(path: str):
def _chars_dir() -> str: def _chars_dir() -> str:
if _CHARS_DIR: if _CHARS_DIR:
return _CHARS_DIR return _CHARS_DIR
d = os.path.expanduser('~/.coderai/characters') d = str(default_characters_dir())
os.makedirs(d, exist_ok=True) os.makedirs(d, exist_ok=True)
return d return d
......
...@@ -42,6 +42,8 @@ from typing import List, Optional ...@@ -42,6 +42,8 @@ from typing import List, Optional
from fastapi import APIRouter, HTTPException, Request from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from codai.platform_paths import default_environments_dir, legacy_style_config_dir
router = APIRouter() router = APIRouter()
_ENVS_DIR: Optional[str] = None _ENVS_DIR: Optional[str] = None
...@@ -49,8 +51,8 @@ _ENVS_DIR: Optional[str] = None ...@@ -49,8 +51,8 @@ _ENVS_DIR: Optional[str] = None
def set_global_args(args): def set_global_args(args):
global _ENVS_DIR global _ENVS_DIR
base = getattr(args, 'file_path', None) or os.path.expanduser('~/.coderai') base = getattr(args, 'file_path', None) or str(legacy_style_config_dir())
root = base if os.path.isdir(base) else (os.path.dirname(base) if base else os.path.expanduser('~/.coderai')) root = base if os.path.isdir(base) else (os.path.dirname(base) if base else str(legacy_style_config_dir()))
_ENVS_DIR = os.path.join(root, 'environments') _ENVS_DIR = os.path.join(root, 'environments')
os.makedirs(_ENVS_DIR, exist_ok=True) os.makedirs(_ENVS_DIR, exist_ok=True)
...@@ -62,7 +64,7 @@ def set_global_file_path(path: str): ...@@ -62,7 +64,7 @@ def set_global_file_path(path: str):
def _envs_dir() -> str: def _envs_dir() -> str:
if _ENVS_DIR: if _ENVS_DIR:
return _ENVS_DIR return _ENVS_DIR
d = os.path.expanduser('~/.coderai/environments') d = str(default_environments_dir())
os.makedirs(d, exist_ok=True) os.makedirs(d, exist_ok=True)
return d return d
......
...@@ -20,13 +20,14 @@ from PIL import Image ...@@ -20,13 +20,14 @@ from PIL import Image
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from codai.api.images import save_image_response from codai.api.images import save_image_response
from codai.platform_paths import default_insightface_model_path
router = APIRouter() router = APIRouter()
global_args = None global_args = None
global_file_path = None global_file_path = None
_INSWAPPER_MODEL_PATH = os.path.expanduser('~/.insightface/models/inswapper_128.onnx') _INSWAPPER_MODEL_PATH = str(default_insightface_model_path())
_INSWAPPER_HF_REPO = 'deepinsight/inswapper' _INSWAPPER_HF_REPO = 'deepinsight/inswapper'
_INSWAPPER_HF_FILE = 'inswapper_128.onnx' _INSWAPPER_HF_FILE = 'inswapper_128.onnx'
......
...@@ -1713,7 +1713,8 @@ def _run_unpixelate(image_bytes: bytes, scale: int, model_path: Optional[str]) - ...@@ -1713,7 +1713,8 @@ def _run_unpixelate(image_bytes: bytes, scale: int, model_path: Optional[str]) -
mp = model_path mp = model_path
else: else:
# Download RealESRGAN_x4plus on demand # Download RealESRGAN_x4plus on demand
mp = os.path.expanduser('~/.cache/realesrgan/RealESRGAN_x4plus.pth') from codai.platform_paths import default_realesrgan_model_path
mp = str(default_realesrgan_model_path())
if not os.path.exists(mp): if not os.path.exists(mp):
os.makedirs(os.path.dirname(mp), exist_ok=True) os.makedirs(os.path.dirname(mp), exist_ok=True)
import urllib.request import urllib.request
......
...@@ -136,9 +136,11 @@ async def create_transcription( ...@@ -136,9 +136,11 @@ async def create_transcription(
# Check if the requested model maps to a configured whisper-server instance first. # Check if the requested model maps to a configured whisper-server instance first.
# Try alias round-robin resolution before direct ID lookup. # Try alias round-robin resolution before direct ID lookup.
whisper_model_id = multi_model_manager.resolve_whisper_alias_model_id(model)
whisper_server = ( whisper_server = (
multi_model_manager.resolve_whisper_alias(model) multi_model_manager.whisper_servers.get(whisper_model_id)
or multi_model_manager.whisper_servers.get(model) if whisper_model_id is not None
else multi_model_manager.whisper_servers.get(model)
) )
if whisper_server is not None: if whisper_server is not None:
multi_model_manager.request_model(requested_model=model, model_type="audio") multi_model_manager.request_model(requested_model=model, model_type="audio")
...@@ -148,7 +150,7 @@ async def create_transcription( ...@@ -148,7 +150,7 @@ async def create_transcription(
gpu_device=getattr(whisper_server, "_gpu_device", 0), gpu_device=getattr(whisper_server, "_gpu_device", 0),
) )
if whisper_server.is_running(): if whisper_server.is_running():
ws_key = f"audio:{model}" ws_key = f"audio:{whisper_model_id or model}"
multi_model_manager.models[ws_key] = whisper_server multi_model_manager.models[ws_key] = whisper_server
multi_model_manager.active_in_vram = ws_key multi_model_manager.active_in_vram = ws_key
multi_model_manager.models_in_vram.add(ws_key) multi_model_manager.models_in_vram.add(ws_key)
......
...@@ -22,6 +22,8 @@ from typing import Optional ...@@ -22,6 +22,8 @@ from typing import Optional
from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from codai.platform_paths import default_voices_dir
router = APIRouter() router = APIRouter()
global_args = None global_args = None
...@@ -35,8 +37,8 @@ def set_global_args(args): ...@@ -35,8 +37,8 @@ def set_global_args(args):
global global_args, _VOICES_DIR global global_args, _VOICES_DIR
global_args = args global_args = args
# Store voice profiles alongside output files, or in a default location # Store voice profiles alongside output files, or in a default location
base = getattr(args, 'file_path', None) or os.path.expanduser('~/.coderai/voices') base = getattr(args, 'file_path', None) or str(default_voices_dir())
_VOICES_DIR = os.path.join(base if os.path.isdir(base) else os.path.dirname(base) if base else os.path.expanduser('~/.coderai'), 'voices') _VOICES_DIR = os.path.join(base if os.path.isdir(base) else os.path.dirname(base) if base else str(default_voices_dir().parent), 'voices')
os.makedirs(_VOICES_DIR, exist_ok=True) os.makedirs(_VOICES_DIR, exist_ok=True)
...@@ -48,7 +50,7 @@ def set_global_file_path(path): ...@@ -48,7 +50,7 @@ def set_global_file_path(path):
def _voices_dir() -> str: def _voices_dir() -> str:
if _VOICES_DIR: if _VOICES_DIR:
return _VOICES_DIR return _VOICES_DIR
d = os.path.expanduser('~/.coderai/voices') d = str(default_voices_dir())
os.makedirs(d, exist_ok=True) os.makedirs(d, exist_ok=True)
return d return d
......
...@@ -20,6 +20,8 @@ import json ...@@ -20,6 +20,8 @@ import json
import os import os
from pathlib import Path from pathlib import Path
from codai.platform_paths import legacy_style_config_dir
def load_config_file(config_dir: Path) -> dict: def load_config_file(config_dir: Path) -> dict:
"""Load the main config.json file.""" """Load the main config.json file."""
...@@ -166,11 +168,12 @@ def setup_default_config(config_dir: Path): ...@@ -166,11 +168,12 @@ def setup_default_config(config_dir: Path):
def parse_args(): def parse_args():
"""Parse command line arguments.""" """Parse command line arguments."""
default_config = str(legacy_style_config_dir())
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="OpenAI-compatible API server supporting NVIDIA (CUDA) and Vulkan backends", description="OpenAI-compatible API server supporting NVIDIA (CUDA) and Vulkan backends",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""Configuration: All settings are loaded from JSON config files in the epilog="""Configuration: All settings are loaded from JSON config files in the
configuration directory (--config DIR, default: ~/.coderai/). Key files: configuration directory (--config DIR, default: OS-specific CoderAI directory). Key files:
config.json - Server and backend settings config.json - Server and backend settings
models.json - Model registry and configurations models.json - Model registry and configurations
auth.json - Users, tokens, and sessions""" auth.json - Users, tokens, and sessions"""
...@@ -178,8 +181,8 @@ configuration directory (--config DIR, default: ~/.coderai/). Key files: ...@@ -178,8 +181,8 @@ configuration directory (--config DIR, default: ~/.coderai/). Key files:
parser.add_argument( parser.add_argument(
"--config", "--config",
type=str, type=str,
default=os.path.expanduser("~/.coderai/"), default=default_config,
help="Configuration directory (default: ~/.coderai/)", help=f"Configuration directory (default: {default_config})",
) )
parser.add_argument( parser.add_argument(
"--debug", "--debug",
...@@ -224,4 +227,4 @@ configuration directory (--config DIR, default: ~/.coderai/). Key files: ...@@ -224,4 +227,4 @@ configuration directory (--config DIR, default: ~/.coderai/). Key files:
action="store_true", action="store_true",
help="List available Vulkan GPU devices and exit", help="List available Vulkan GPU devices and exit",
) )
return parser.parse_args() return parser.parse_args()
\ No newline at end of file
...@@ -40,6 +40,8 @@ import hashlib ...@@ -40,6 +40,8 @@ import hashlib
import pathlib import pathlib
from typing import Optional, Dict, List, Tuple from typing import Optional, Dict, List, Tuple
from codai.platform_paths import default_model_cache_dir, legacy_style_cache_root
# For type hints # For type hints
import time import time
...@@ -49,8 +51,7 @@ def get_model_cache_dir() -> str: ...@@ -49,8 +51,7 @@ def get_model_cache_dir() -> str:
if os.environ.get('CODERAI_CACHE_DIR'): if os.environ.get('CODERAI_CACHE_DIR'):
cache_dir = os.environ['CODERAI_CACHE_DIR'] cache_dir = os.environ['CODERAI_CACHE_DIR']
else: else:
cache_home = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) cache_dir = str(default_model_cache_dir())
cache_dir = os.path.join(cache_home, 'coderai', 'models')
pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True) pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True)
return cache_dir return cache_dir
...@@ -58,7 +59,7 @@ def get_model_cache_dir() -> str: ...@@ -58,7 +59,7 @@ def get_model_cache_dir() -> str:
def get_all_cache_dirs() -> dict: def get_all_cache_dirs() -> dict:
"""Get all model cache directories.""" """Get all model cache directories."""
caches = {} caches = {}
cache_home = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) cache_home = str(legacy_style_cache_root())
# Coderai GGUF cache # Coderai GGUF cache
coderai_cache = get_model_cache_dir() coderai_cache = get_model_cache_dir()
...@@ -79,7 +80,7 @@ def get_all_cache_dirs() -> dict: ...@@ -79,7 +80,7 @@ def get_all_cache_dirs() -> dict:
caches['huggingface'] = hf_cache caches['huggingface'] = hf_cache
# Local diffusers cache # Local diffusers cache
local_diffusers = os.path.expanduser('~/.cache/diffusers') local_diffusers = os.path.join(cache_home, 'diffusers')
if os.path.exists(local_diffusers): if os.path.exists(local_diffusers):
caches['diffusers'] = local_diffusers caches['diffusers'] = local_diffusers
...@@ -549,4 +550,4 @@ __all__ = [ ...@@ -549,4 +550,4 @@ __all__ = [
'remove_cached_model', 'remove_cached_model',
'list_cached_models_info', 'list_cached_models_info',
'remove_all_cached_models', 'remove_all_cached_models',
] ]
\ No newline at end of file
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
import os import os
import random
import subprocess import subprocess
import signal import signal
import requests import requests
...@@ -256,6 +257,7 @@ class WhisperServerManager: ...@@ -256,6 +257,7 @@ class WhisperServerManager:
self.current_model = None self.current_model = None
self.base_url = f"http://127.0.0.1:{port}" self.base_url = f"http://127.0.0.1:{port}"
self.lock = threading.Lock() self.lock = threading.Lock()
self._active_requests = 0
# Check if port is available # Check if port is available
if not self._is_port_available(port): if not self._is_port_available(port):
...@@ -351,7 +353,8 @@ class WhisperServerManager: ...@@ -351,7 +353,8 @@ class WhisperServerManager:
"""Send transcription request to whisper-server.""" """Send transcription request to whisper-server."""
if not self.is_running(): if not self.is_running():
return {"error": "whisper-server not running"} return {"error": "whisper-server not running"}
with self.lock:
self._active_requests += 1
try: try:
files = {"file": ("audio.wav", audio_data, "audio/wav")} files = {"file": ("audio.wav", audio_data, "audio/wav")}
data = {} data = {}
...@@ -373,6 +376,16 @@ class WhisperServerManager: ...@@ -373,6 +376,16 @@ class WhisperServerManager:
return {"error": f"Server error: {response.status_code}", "detail": response.text} return {"error": f"Server error: {response.status_code}", "detail": response.text}
except Exception as e: except Exception as e:
return {"error": str(e)} return {"error": str(e)}
finally:
with self.lock:
self._active_requests = max(0, self._active_requests - 1)
def active_requests(self) -> int:
with self.lock:
return self._active_requests
def is_idle(self) -> bool:
return self.active_requests() == 0
def _wait_for_server(self, timeout: int = 30) -> bool: def _wait_for_server(self, timeout: int = 30) -> bool:
"""Wait for whisper-server to be ready.""" """Wait for whisper-server to be ready."""
...@@ -810,6 +823,17 @@ class MultiModelManager: ...@@ -810,6 +823,17 @@ class MultiModelManager:
idx = self._whisper_alias_counters.get(name, 0) % len(ids) idx = self._whisper_alias_counters.get(name, 0) % len(ids)
self._whisper_alias_counters[name] = idx + 1 self._whisper_alias_counters[name] = idx + 1
return self.whisper_servers.get(ids[idx]) return self.whisper_servers.get(ids[idx])
def resolve_whisper_alias_model_id(self, name: str) -> Optional[str]:
"""Return an idle whisper-server model id for an alias, else a random one."""
ids = self.whisper_aliases.get(name)
if not ids:
return None
idle_ids = [mid for mid in ids if (self.whisper_servers.get(mid) and self.whisper_servers[mid].is_idle())]
choices = idle_ids or list(ids)
if len(choices) == 1:
return choices[0]
return random.choice(choices)
def set_tts_model(self, model_name: str, config: Dict = None): def set_tts_model(self, model_name: str, config: Dict = None):
"""Set the text-to-speech model and download/cache it if needed.""" """Set the text-to-speech model and download/cache it if needed."""
...@@ -900,6 +924,9 @@ class MultiModelManager: ...@@ -900,6 +924,9 @@ class MultiModelManager:
for m in self.audio_models: for m in self.audio_models:
allowed.add(m) allowed.add(m)
allowed.add(f"audio:{m}") allowed.add(f"audio:{m}")
for alias in self.whisper_aliases:
allowed.add(alias)
allowed.add(f"audio:{alias}")
# TTS model # TTS model
if self.tts_model: if self.tts_model:
...@@ -963,10 +990,18 @@ class MultiModelManager: ...@@ -963,10 +990,18 @@ class MultiModelManager:
"video_models", "audio_gen_models", "embedding_models", "video_models", "audio_gen_models", "embedding_models",
"spatial_models"): "spatial_models"):
for m in md.get(cat, []): for m in md.get(cat, []):
mid = (m if isinstance(m, str) else if isinstance(m, str):
m.get("alias") or m.get("path") or m.get("id") or "") mid = m
elif m.get("backend") == "whisper-server":
mid = m.get("id") or ""
else:
mid = m.get("alias") or m.get("path") or m.get("id") or ""
raw = (m if isinstance(m, str) else m.get("path") or m.get("id") or "") raw = (m if isinstance(m, str) else m.get("path") or m.get("id") or "")
for val in (mid, raw): alias = "" if isinstance(m, str) else (m.get("alias") or "")
vals = [mid, raw]
if alias:
vals.append(alias)
for val in vals:
if val: if val:
allowed.add(val) allowed.add(val)
short = val.split("/")[-1] if "/" in val else val short = val.split("/")[-1] if "/" in val else val
......
"""Platform-aware filesystem helpers for CoderAI."""
from __future__ import annotations
import os
from pathlib import Path
APP_NAME = "coderai"
def _home_dir() -> Path:
return Path.home()
def _windows_dir(env_var: str, fallback: Path) -> Path:
value = os.environ.get(env_var)
return Path(value).expanduser() if value else fallback
def user_config_dir() -> Path:
if os.name == "nt":
base = _windows_dir("APPDATA", _home_dir() / "AppData" / "Roaming")
return base / "CoderAI"
if sys_platform() == "darwin":
return _home_dir() / "Library" / "Application Support" / "CoderAI"
xdg = os.environ.get("XDG_CONFIG_HOME")
base = Path(xdg).expanduser() if xdg else (_home_dir() / ".config")
return base / APP_NAME
def user_data_dir() -> Path:
if os.name == "nt":
base = _windows_dir("LOCALAPPDATA", _home_dir() / "AppData" / "Local")
return base / "CoderAI"
if sys_platform() == "darwin":
return _home_dir() / "Library" / "Application Support" / "CoderAI"
xdg = os.environ.get("XDG_DATA_HOME")
base = Path(xdg).expanduser() if xdg else (_home_dir() / ".local" / "share")
return base / APP_NAME
def user_cache_dir() -> Path:
if os.name == "nt":
base = _windows_dir("LOCALAPPDATA", _home_dir() / "AppData" / "Local")
return base / "CoderAI" / "Cache"
if sys_platform() == "darwin":
return _home_dir() / "Library" / "Caches" / "CoderAI"
xdg = os.environ.get("XDG_CACHE_HOME")
base = Path(xdg).expanduser() if xdg else (_home_dir() / ".cache")
return base / APP_NAME
def ensure_dir(path: Path) -> Path:
path.mkdir(parents=True, exist_ok=True)
return path
def default_config_dir() -> Path:
return ensure_dir(user_config_dir())
def default_data_dir() -> Path:
return ensure_dir(user_data_dir())
def default_cache_dir() -> Path:
return ensure_dir(user_cache_dir())
def legacy_style_config_dir() -> Path:
if os.name == "nt":
return _home_dir() / ".coderai"
return _home_dir() / ".coderai"
def legacy_style_cache_root() -> Path:
if os.name == "nt":
base = _windows_dir("LOCALAPPDATA", _home_dir() / "AppData" / "Local")
return base / ".cache"
if sys_platform() == "darwin":
return _home_dir() / "Library" / "Caches"
xdg = os.environ.get("XDG_CACHE_HOME")
return Path(xdg).expanduser() if xdg else (_home_dir() / ".cache")
def default_model_cache_dir() -> Path:
return ensure_dir(legacy_style_cache_root() / APP_NAME / "models")
def default_diffusers_cache_dir() -> Path:
return ensure_dir(legacy_style_cache_root() / "diffusers")
def default_realesrgan_model_path() -> Path:
return default_diffusers_cache_dir().parent / "realesrgan" / "RealESRGAN_x4plus.pth"
def default_insightface_model_path() -> Path:
return ensure_dir(legacy_style_cache_root() / "insightface" / "models") / "inswapper_128.onnx"
def default_voices_dir() -> Path:
return ensure_dir(legacy_style_config_dir() / "voices")
def default_characters_dir() -> Path:
return ensure_dir(legacy_style_config_dir() / "characters")
def default_environments_dir() -> Path:
return ensure_dir(legacy_style_config_dir() / "environments")
def default_whisper_server_path() -> str:
if os.name == "nt":
local = _windows_dir("LOCALAPPDATA", _home_dir() / "AppData" / "Local")
return str(local / "Programs" / "whisper-server" / "whisper-server.exe")
if sys_platform() == "darwin":
return "/usr/local/bin/whisper-server"
return "/usr/local/bin/whisper-server"
def sys_platform() -> str:
return os.sys.platform
...@@ -247,6 +247,7 @@ def test_model_configure_allows_whisper_server_id_coexistence_with_other_audio_b ...@@ -247,6 +247,7 @@ def test_model_configure_allows_whisper_server_id_coexistence_with_other_audio_b
def test_model_configure_defaults_missing_whisper_server_path_to_usr_local_bin(monkeypatch, tmp_path): def test_model_configure_defaults_missing_whisper_server_path_to_usr_local_bin(monkeypatch, tmp_path):
from codai.admin import routes from codai.admin import routes
from codai.platform_paths import default_whisper_server_path
cfg = _build_config(tmp_path) cfg = _build_config(tmp_path)
monkeypatch.setattr(routes, "config_manager", cfg, raising=False) monkeypatch.setattr(routes, "config_manager", cfg, raising=False)
...@@ -267,7 +268,7 @@ def test_model_configure_defaults_missing_whisper_server_path_to_usr_local_bin(m ...@@ -267,7 +268,7 @@ def test_model_configure_defaults_missing_whisper_server_path_to_usr_local_bin(m
) )
assert response.status_code == 200 assert response.status_code == 200
assert cfg.models_data["audio_models"][0]["server_path"] == "/usr/local/bin/whisper-server" assert cfg.models_data["audio_models"][0]["server_path"] == default_whisper_server_path()
app.dependency_overrides.clear() app.dependency_overrides.clear()
...@@ -546,6 +547,67 @@ def test_transcription_requires_configured_whisper_server_model_id(): ...@@ -546,6 +547,67 @@ def test_transcription_requires_configured_whisper_server_model_id():
assert "not configured" in str(exc.value.detail).lower() or "not available" in str(exc.value.detail).lower() assert "not configured" in str(exc.value.detail).lower() or "not available" in str(exc.value.detail).lower()
def test_whisper_alias_prefers_idle_instance(monkeypatch):
from codai.models.manager import MultiModelManager
manager = MultiModelManager()
busy = SimpleNamespace(is_idle=lambda: False)
idle = SimpleNamespace(is_idle=lambda: True)
manager.whisper_servers = {"whisper0": busy, "whisper1": idle}
manager.whisper_aliases = {"shared-whisper": ["whisper0", "whisper1"]}
assert manager.resolve_whisper_alias_model_id("shared-whisper") == "whisper1"
def test_whisper_alias_uses_random_choice_when_multiple_idle(monkeypatch):
from codai.models.manager import MultiModelManager
manager = MultiModelManager()
idle0 = SimpleNamespace(is_idle=lambda: True)
idle1 = SimpleNamespace(is_idle=lambda: True)
manager.whisper_servers = {"whisper0": idle0, "whisper1": idle1}
manager.whisper_aliases = {"shared-whisper": ["whisper0", "whisper1"]}
monkeypatch.setattr("codai.models.manager.random.choice", lambda items: items[-1])
assert manager.resolve_whisper_alias_model_id("shared-whisper") == "whisper1"
def test_get_all_allowed_identifiers_includes_whisper_alias(monkeypatch):
from codai.admin import routes
from codai.models.manager import MultiModelManager
manager = MultiModelManager()
manager.audio_models[:] = ["whisper0", "whisper1"]
manager.whisper_aliases = {"shared-whisper": ["whisper0", "whisper1"]}
monkeypatch.setattr(
routes,
"config_manager",
SimpleNamespace(
models_data={
"text_models": [],
"image_models": [],
"audio_models": [
{"id": "whisper0", "backend": "whisper-server", "alias": "shared-whisper"},
{"id": "whisper1", "backend": "whisper-server", "alias": "shared-whisper"},
],
"vision_models": [],
"tts_models": [],
"gguf_models": [],
"video_models": [],
"audio_gen_models": [],
"embedding_models": [],
"aliases": {},
}
),
raising=False,
)
allowed = manager.get_all_allowed_identifiers()
assert "shared-whisper" in allowed
assert "audio:shared-whisper" in allowed
def test_get_all_allowed_identifiers_includes_configured_whisper_server_id_without_legacy_alias(monkeypatch): def test_get_all_allowed_identifiers_includes_configured_whisper_server_id_without_legacy_alias(monkeypatch):
from codai.admin import routes from codai.admin import routes
from codai.models.manager import MultiModelManager from codai.models.manager import MultiModelManager
...@@ -673,7 +735,7 @@ def test_models_template_defines_whisper_server_builder_default_helpers(): ...@@ -673,7 +735,7 @@ def test_models_template_defines_whisper_server_builder_default_helpers():
template = Path("codai/admin/templates/models.html").read_text() template = Path("codai/admin/templates/models.html").read_text()
assert "function defaultWhisperServerPath()" in template assert "function defaultWhisperServerPath()" in template
assert "return '/usr/local/bin/whisper-server';" in template assert "return window.__DEFAULT_WHISPER_SERVER_PATH__ || '';" in template
assert "function resetWhisperServerBuilderDefaults()" in template assert "function resetWhisperServerBuilderDefaults()" in template
assert "const modelIdInput = document.getElementById('ws-model-id');" in template assert "const modelIdInput = document.getElementById('ws-model-id');" in template
assert "const serverPathInput = document.getElementById('ws-server-path');" in template assert "const serverPathInput = document.getElementById('ws-server-path');" in template
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment