Spaces:
Building
Building
""" | |
Flare – ConfigProvider (TTS/STT support) | |
""" | |
from __future__ import annotations | |
import json, os | |
from pathlib import Path | |
from typing import Any, Dict, List, Optional, Union | |
from datetime import datetime | |
import commentjson | |
from utils import log | |
from pydantic import BaseModel, Field, HttpUrl, ValidationError, field_validator | |
from encryption_utils import decrypt | |
class GlobalConfig(BaseModel): | |
work_mode: str = Field("hfcloud", pattern=r"^(hfcloud|cloud|on-premise|gpt4o|gpt4o-mini)$") | |
cloud_token: Optional[str] = None | |
spark_endpoint: HttpUrl | |
internal_prompt: Optional[str] = None | |
# TTS configurations | |
tts_engine: str = Field("no_tts", pattern=r"^(no_tts|elevenlabs|blaze)$") | |
tts_engine_api_key: Optional[str] = None | |
tts_settings: Optional[Dict[str, Any]] = Field(default_factory=lambda: { | |
"use_ssml": False | |
}) | |
# STT configurations | |
stt_engine: str = Field("no_stt", pattern=r"^(no_stt|google|azure|amazon|gpt4o_realtime|flicker)$") | |
stt_engine_api_key: Optional[str] = None | |
stt_settings: Optional[Dict[str, Any]] = Field(default_factory=lambda: { | |
"speech_timeout_ms": 2000, | |
"noise_reduction_level": 2, | |
"vad_sensitivity": 0.5, | |
"language": "tr-TR", | |
"model": "latest_long", | |
"use_enhanced": True, | |
"enable_punctuation": True, | |
"interim_results": True | |
}) | |
users: List["UserConfig"] = [] | |
def is_gpt_mode(self) -> bool: | |
"""Check if running in GPT mode (any variant)""" | |
return self.work_mode in ("gpt4o", "gpt4o-mini") | |
def get_gpt_model(self) -> str: | |
"""Get the GPT model name for OpenAI API""" | |
if self.work_mode == "gpt4o": | |
return "gpt-4o" | |
elif self.work_mode == "gpt4o-mini": | |
return "gpt-4o-mini" | |
return None | |
def get_plain_token(self) -> Optional[str]: | |
if self.cloud_token: | |
# Lazy import to avoid circular dependency | |
from encryption_utils import decrypt | |
return decrypt(self.cloud_token) | |
return None | |
def get_tts_api_key(self) -> Optional[str]: | |
"""Get decrypted TTS API key""" | |
raw_key = self.tts_engine_api_key | |
if raw_key and raw_key.startswith("enc:"): | |
from encryption_utils import decrypt | |
decrypted = decrypt(raw_key) | |
log(f"🔓 TTS key decrypted: {'***' + decrypted[-4:] if decrypted else 'None'}") | |
return decrypted | |
log(f"🔑 TTS key not encrypted: {'***' + raw_key[-4:] if raw_key else 'None'}") | |
return raw_key | |
def get_tts_settings(self) -> Dict[str, Any]: | |
"""Get TTS settings with defaults""" | |
return self.tts_settings or { | |
"use_ssml": False | |
} | |
def get_stt_api_key(self) -> Optional[str]: | |
"""Get decrypted STT API key or credentials path""" | |
raw_key = self.stt_engine_api_key | |
if raw_key and raw_key.startswith("enc:"): | |
from encryption_utils import decrypt | |
decrypted = decrypt(raw_key) | |
log(f"🔓 STT key decrypted: {'***' + decrypted[-4:] if decrypted else 'None'}") | |
return decrypted | |
log(f"🔑 STT key/path: {'***' + raw_key[-4:] if raw_key else 'None'}") | |
return raw_key | |
def get_stt_settings(self) -> Dict[str, Any]: | |
"""Get STT settings with defaults""" | |
return self.stt_settings or { | |
"speech_timeout_ms": 2000, | |
"noise_reduction_level": 2, | |
"vad_sensitivity": 0.5, | |
"language": "tr-TR", | |
"model": "latest_long", | |
"use_enhanced": True, | |
"enable_punctuation": True, | |
"interim_results": True | |
} | |
def is_cloud_mode(self) -> bool: | |
"""Check if running in cloud mode (hfcloud or cloud)""" | |
return self.work_mode in ("hfcloud", "cloud") | |
def is_on_premise(self) -> bool: | |
"""Check if running in on-premise mode""" | |
return self.work_mode == "on-premise" | |
# ---------------- Global ----------------- | |
class UserConfig(BaseModel): | |
username: str | |
password_hash: str | |
salt: str | |
# ---------------- Retry / Proxy ---------- | |
class RetryConfig(BaseModel): | |
retry_count: int = Field(3, alias="max_attempts") | |
backoff_seconds: int = 2 | |
strategy: str = Field("static", pattern=r"^(static|exponential)$") | |
class ProxyConfig(BaseModel): | |
enabled: bool = True | |
url: HttpUrl | |
# ---------------- API & Auth ------------- | |
class APIAuthConfig(BaseModel): | |
enabled: bool = False | |
token_endpoint: Optional[HttpUrl] = None | |
response_token_path: str = "access_token" | |
token_request_body: Dict[str, Any] = Field({}, alias="body_template") | |
token_refresh_endpoint: Optional[HttpUrl] = None | |
token_refresh_body: Dict[str, Any] = {} | |
class Config: | |
extra = "allow" | |
populate_by_name = True | |
class APIConfig(BaseModel): | |
name: str | |
url: HttpUrl | |
method: str = Field("GET", pattern=r"^(GET|POST|PUT|PATCH|DELETE)$") | |
headers: Dict[str, Any] = {} | |
body_template: Dict[str, Any] = {} | |
timeout_seconds: int = 10 | |
retry: RetryConfig = RetryConfig() | |
proxy: Optional[str | ProxyConfig] = None | |
auth: Optional[APIAuthConfig] = None | |
response_prompt: Optional[str] = None | |
class Config: | |
extra = "allow" | |
populate_by_name = True | |
# ---------------- Intent / Param --------- | |
class ParameterConfig(BaseModel): | |
name: str | |
caption: Optional[str] = "" | |
type: str = Field(..., pattern=r"^(int|float|bool|str|string|date)$") # Added 'date' | |
required: bool = True | |
variable_name: str | |
extraction_prompt: Optional[str] = None | |
validation_regex: Optional[str] = None | |
invalid_prompt: Optional[str] = None | |
type_error_prompt: Optional[str] = None | |
def canonical_type(self) -> str: | |
if self.type == "string": | |
return "str" | |
elif self.type == "date": | |
return "str" # Store dates as strings in ISO format | |
return self.type | |
class IntentConfig(BaseModel): | |
name: str | |
caption: Optional[str] = "" | |
locale: str = "tr-TR" | |
dependencies: List[str] = [] | |
examples: List[str] = [] | |
detection_prompt: Optional[str] = None | |
parameters: List[ParameterConfig] = [] | |
action: str | |
fallback_timeout_prompt: Optional[str] = None | |
fallback_error_prompt: Optional[str] = None | |
class Config: | |
extra = "allow" | |
# ---------------- Version / Project ------ | |
class LLMConfig(BaseModel): | |
repo_id: str | |
generation_config: Dict[str, Any] = {} | |
use_fine_tune: bool = False | |
fine_tune_zip: str = "" | |
class VersionConfig(BaseModel): | |
id: int = Field(..., alias="version_number") | |
caption: Optional[str] = "" | |
published: bool = False | |
general_prompt: str | |
llm: "LLMConfig" | |
intents: List["IntentConfig"] | |
class Config: | |
extra = "allow" | |
populate_by_name = True | |
class ProjectConfig(BaseModel): | |
id: Optional[int] = None | |
name: str | |
caption: Optional[str] = "" | |
enabled: bool = True | |
last_version_number: Optional[int] = None | |
versions: List[VersionConfig] | |
class Config: | |
extra = "allow" | |
# ---------------- Activity Log ----------- | |
class ActivityLogEntry(BaseModel): | |
timestamp: str | |
username: str | |
action: str | |
entity_type: str | |
entity_id: Optional[int] = None | |
entity_name: Optional[str] = None | |
details: Optional[str] = None | |
# ---------------- Service Config --------- | |
class ServiceConfig(BaseModel): | |
global_config: GlobalConfig = Field(..., alias="config") | |
projects: List[ProjectConfig] | |
apis: List[APIConfig] | |
activity_log: List[ActivityLogEntry] = [] | |
# Config level fields | |
project_id_counter: int = 1 | |
last_update_date: Optional[str] = None | |
last_update_user: Optional[str] = None | |
# runtime helpers (skip validation) | |
_api_by_name: Dict[str, APIConfig] = {} | |
def build_index(self): | |
self._api_by_name = {a.name: a for a in self.apis} | |
def get_api(self, name: str) -> Optional[APIConfig]: | |
return self._api_by_name.get(name) | |
def to_jsonc_dict(self) -> dict: | |
"""Convert to dict for saving to JSONC file""" | |
data = self.model_dump(by_alias=True, exclude={'_api_by_name'}) | |
# Convert API configs | |
for api in data.get('apis', []): | |
# Convert headers and body_template to JSON strings | |
if 'headers' in api and isinstance(api['headers'], dict): | |
api['headers'] = json.dumps(api['headers'], ensure_ascii=False) | |
if 'body_template' in api and isinstance(api['body_template'], dict): | |
api['body_template'] = json.dumps(api['body_template'], ensure_ascii=False) | |
# Convert auth configs | |
if 'auth' in api and api['auth']: | |
if 'token_request_body' in api['auth'] and isinstance(api['auth']['token_request_body'], dict): | |
api['auth']['token_request_body'] = json.dumps(api['auth']['token_request_body'], ensure_ascii=False) | |
if 'token_refresh_body' in api['auth'] and isinstance(api['auth']['token_refresh_body'], dict): | |
api['auth']['token_refresh_body'] = json.dumps(api['auth']['token_refresh_body'], ensure_ascii=False) | |
return data | |
def save(self): | |
"""Save configuration to file""" | |
config_path = Path(__file__).parent / "service_config.jsonc" | |
data = self.to_jsonc_dict() | |
with open(config_path, 'w', encoding='utf-8') as f: | |
json.dump(data, f, ensure_ascii=False, indent=2) | |
log("✅ Configuration saved to service_config.jsonc") | |
# ---------------- Provider Singleton ----- | |
class ConfigProvider: | |
_instance: Optional[ServiceConfig] = None | |
_CONFIG_PATH = Path(__file__).parent / "service_config.jsonc" | |
def get(cls) -> ServiceConfig: | |
if cls._instance is None: | |
cls._instance = cls._load() | |
cls._instance.build_index() | |
cls._check_environment_setup() | |
return cls._instance | |
def reload(cls) -> ServiceConfig: | |
"""Force reload configuration from file""" | |
cls._instance = None | |
return cls.get() | |
def update_config(cls, config_dict: dict): | |
"""Update the current configuration with new values""" | |
if cls._instance is None: | |
cls.get() | |
# Update global config | |
if 'config' in config_dict: | |
for key, value in config_dict['config'].items(): | |
if hasattr(cls._instance.global_config, key): | |
setattr(cls._instance.global_config, key, value) | |
# Save to file | |
cls._instance.save() | |
def _load(cls) -> ServiceConfig: | |
"""Load configuration from service_config.jsonc""" | |
try: | |
log(f"📂 Loading config from: {cls._CONFIG_PATH}") | |
if not cls._CONFIG_PATH.exists(): | |
raise FileNotFoundError(f"Config file not found: {cls._CONFIG_PATH}") | |
with open(cls._CONFIG_PATH, 'r', encoding='utf-8') as f: | |
config_data = commentjson.load(f) | |
# Ensure required fields exist in config data | |
if 'config' not in config_data: | |
config_data['config'] = {} | |
config_section = config_data['config'] | |
# Set defaults for missing fields | |
config_section.setdefault('work_mode', 'hfcloud') | |
config_section.setdefault('spark_endpoint', 'http://localhost:7861') | |
config_section.setdefault('cloud_token', None) | |
config_section.setdefault('internal_prompt', None) | |
config_section.setdefault('tts_engine', 'no_tts') | |
config_section.setdefault('tts_engine_api_key', None) | |
config_section.setdefault('tts_settings', {'use_ssml': False}) | |
config_section.setdefault('stt_engine', 'no_stt') | |
config_section.setdefault('stt_engine_api_key', None) | |
config_section.setdefault('stt_settings', { | |
'speech_timeout_ms': 2000, | |
'noise_reduction_level': 2, | |
'vad_sensitivity': 0.5, | |
'language': 'tr-TR', | |
'model': 'latest_long', | |
'use_enhanced': True, | |
'enable_punctuation': True, | |
'interim_results': True | |
}) | |
config_section.setdefault('users', []) | |
# Convert string body/headers to dict if needed | |
for api in config_data.get('apis', []): | |
if isinstance(api.get('headers'), str): | |
try: | |
api['headers'] = json.loads(api['headers']) | |
except: | |
api['headers'] = {} | |
if isinstance(api.get('body_template'), str): | |
try: | |
api['body_template'] = json.loads(api['body_template']) | |
except: | |
api['body_template'] = {} | |
# Handle auth section | |
if api.get('auth'): | |
auth = api['auth'] | |
if isinstance(auth.get('token_request_body'), str): | |
try: | |
auth['token_request_body'] = json.loads(auth['token_request_body']) | |
except: | |
auth['token_request_body'] = {} | |
if isinstance(auth.get('token_refresh_body'), str): | |
try: | |
auth['token_refresh_body'] = json.loads(auth['token_refresh_body']) | |
except: | |
auth['token_refresh_body'] = {} | |
# Fix activity_log entries if needed | |
if 'activity_log' in config_data: | |
for entry in config_data['activity_log']: | |
# Add missing username field | |
if 'username' not in entry: | |
entry['username'] = entry.get('user', 'system') | |
# Ensure all required fields exist | |
entry.setdefault('action', 'UNKNOWN') | |
entry.setdefault('entity_type', 'unknown') | |
entry.setdefault('timestamp', datetime.now().isoformat()) | |
# Create ServiceConfig instance | |
service_config = ServiceConfig(**config_data) | |
log("✅ Configuration loaded successfully") | |
return service_config | |
except FileNotFoundError as e: | |
log(f"❌ Config file not found: {e}") | |
raise | |
except json.JSONDecodeError as e: | |
log(f"❌ Invalid JSON in config file: {e}") | |
raise | |
except ValidationError as e: | |
log(f"❌ Config validation error: {e}") | |
raise | |
except Exception as e: | |
log(f"❌ Unexpected error loading config: {e}") | |
raise | |
def _check_environment_setup(cls): | |
"""Check if environment is properly configured based on work_mode""" | |
config = cls._instance.global_config | |
if config.is_cloud_mode(): | |
# Cloud mode - check for HuggingFace Secrets | |
missing_secrets = [] | |
if not os.getenv("JWT_SECRET"): | |
missing_secrets.append("JWT_SECRET") | |
if not os.getenv("FLARE_TOKEN_KEY"): | |
missing_secrets.append("FLARE_TOKEN_KEY") | |
if not os.getenv("SPARK_TOKEN"): | |
missing_secrets.append("SPARK_TOKEN") | |
if missing_secrets: | |
log(f"⚠️ Running in {config.work_mode} mode. Missing secrets: {', '.join(missing_secrets)}") | |
log("Please set these as HuggingFace Space Secrets for cloud deployment.") | |
else: | |
# On-premise mode - check for .env file | |
env_path = Path(__file__).parent / ".env" | |
if not env_path.exists(): | |
log("⚠️ Running in on-premise mode but .env file not found") | |
# Docker ortamında yazma izni olmayabilir, sadece uyarı ver | |
log("⚠️ Cannot create .env file in Docker environment. Using default values.") | |
# Set default environment variables if not already set | |
if not os.getenv("JWT_SECRET"): | |
os.environ["JWT_SECRET"] = "flare-admin-secret-key-change-in-production" | |
if not os.getenv("JWT_ALGORITHM"): | |
os.environ["JWT_ALGORITHM"] = "HS256" | |
if not os.getenv("JWT_EXPIRATION_HOURS"): | |
os.environ["JWT_EXPIRATION_HOURS"] = "24" | |
if not os.getenv("FLARE_TOKEN_KEY"): | |
os.environ["FLARE_TOKEN_KEY"] = "flare-token-encryption-key" | |
if not os.getenv("SPARK_TOKEN"): | |
os.environ["SPARK_TOKEN"] = "your-spark-token-here" | |
log("✅ Default environment variables set.") | |
# Forward references | |
GlobalConfig.model_rebuild() | |
VersionConfig.model_rebuild() | |
ServiceConfig.model_rebuild() |