Improved normalizazation visualization

This commit is contained in:
James Ketr 2025-09-15 16:59:28 -07:00
parent 916151307f
commit 3cfc148724
8 changed files with 528 additions and 315 deletions

View File

@ -604,7 +604,7 @@
"Bot Configuration" "Bot Configuration"
], ],
"summary": "Get Bot Config Schema", "summary": "Get Bot Config Schema",
"description": "Get configuration schema for a specific bot", "description": "Get configuration schema for a specific bot.\n\nThis endpoint will query registered bot providers each time and\nrequest the bot's /config-schema endpoint without relying on any\nserver-side cached schema. This ensures the UI always receives the\nup-to-date schema from the provider.",
"operationId": "get_bot_config_schema_ai_voicebot_api_bots_config_schema__bot_name__get", "operationId": "get_bot_config_schema_ai_voicebot_api_bots_config_schema__bot_name__get",
"parameters": [ "parameters": [
{ {
@ -641,6 +641,94 @@
} }
} }
}, },
"/ai-voicebot/api/bots/config/schema/instance/{bot_instance_id}": {
"get": {
"tags": [
"Bot Configuration"
],
"summary": "Get Bot Config Schema By Instance",
"description": "Get configuration schema for a specific bot instance",
"operationId": "get_bot_config_schema_by_instance_ai_voicebot_api_bots_config_schema_instance__bot_instance_id__get",
"parameters": [
{
"name": "bot_instance_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bot Instance Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BotConfigSchema"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/ai-voicebot/api/bots/config/schema/instance/{bot_instance_id}/refresh": {
"post": {
"tags": [
"Bot Configuration"
],
"summary": "Refresh Bot Schema By Instance",
"description": "Refresh configuration schema for a specific bot instance",
"operationId": "refresh_bot_schema_by_instance_ai_voicebot_api_bots_config_schema_instance__bot_instance_id__refresh_post",
"parameters": [
{
"name": "bot_instance_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bot Instance Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"title": "Response Refresh Bot Schema By Instance Ai Voicebot Api Bots Config Schema Instance Bot Instance Id Refresh Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/ai-voicebot/api/bots/config/lobby/{lobby_id}": { "/ai-voicebot/api/bots/config/lobby/{lobby_id}": {
"get": { "get": {
"tags": [ "tags": [
@ -978,51 +1066,6 @@
} }
} }
}, },
"/ai-voicebot/api/bots/config/schema/{bot_name}/cache": {
"delete": {
"tags": [
"Bot Configuration"
],
"summary": "Clear Bot Schema Cache",
"description": "Clear cached schema for a specific bot",
"operationId": "clear_bot_schema_cache_ai_voicebot_api_bots_config_schema__bot_name__cache_delete",
"parameters": [
{
"name": "bot_name",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Bot Name"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"title": "Response Clear Bot Schema Cache Ai Voicebot Api Bots Config Schema Bot Name Cache Delete"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/ai-voicebot/api/health/ready": { "/ai-voicebot/api/health/ready": {
"get": { "get": {
"tags": [ "tags": [

View File

@ -209,6 +209,14 @@ export class ApiClient {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}`), { method: "GET" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}`), { method: "GET" });
} }
async getBotConfigSchemaByInstance(bot_instance_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/instance/${bot_instance_id}`), { method: "GET" });
}
async createRefreshBotSchemaByInstance(bot_instance_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/instance/${bot_instance_id}/refresh`), { method: "POST" });
}
async getLobbyBotConfigs(lobby_id: string): Promise<any> { async getLobbyBotConfigs(lobby_id: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}`), { method: "GET" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/lobby/${lobby_id}`), { method: "GET" });
} }
@ -241,10 +249,6 @@ export class ApiClient {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/refresh`), { method: "POST" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/refresh`), { method: "POST" });
} }
async deleteClearBotSchemaCache(bot_name: string): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/bots/config/schema/${bot_name}/cache`), { method: "DELETE" });
}
async getReadinessProbe(): Promise<any> { async getReadinessProbe(): Promise<any> {
return this.request<any>(this.getApiPath(`/ai-voicebot/api/health/ready`), { method: "GET" }); return this.request<any>(this.getApiPath(`/ai-voicebot/api/health/ready`), { method: "GET" });
} }
@ -321,6 +325,8 @@ export const botsApi = {
requestLeaveLobby: (bot_instance_id: string) => apiClient.createRequestBotLeaveLobby(bot_instance_id), requestLeaveLobby: (bot_instance_id: string) => apiClient.createRequestBotLeaveLobby(bot_instance_id),
getInstance: (bot_instance_id: string) => apiClient.getBotInstance(bot_instance_id), getInstance: (bot_instance_id: string) => apiClient.getBotInstance(bot_instance_id),
getSchema: (bot_name: string) => apiClient.getBotConfigSchema(bot_name), getSchema: (bot_name: string) => apiClient.getBotConfigSchema(bot_name),
getSchemaByInstance: (bot_instance_id: string) => apiClient.getBotConfigSchemaByInstance(bot_instance_id),
refreshSchema: (bot_instance_id: string) => apiClient.createRefreshBotSchemaByInstance(bot_instance_id),
getBotConfigs: (lobby_id: string) => apiClient.getLobbyBotConfigs(lobby_id), getBotConfigs: (lobby_id: string) => apiClient.getLobbyBotConfigs(lobby_id),
deleteBotConfigs: (lobby_id: string) => apiClient.deleteLobbyConfigs(lobby_id), deleteBotConfigs: (lobby_id: string) => apiClient.deleteLobbyConfigs(lobby_id),
getBotConfig: (lobby_id: string, bot_instance_id: string) => apiClient.getLobbyBotConfig(lobby_id, bot_instance_id), getBotConfig: (lobby_id: string, bot_instance_id: string) => apiClient.getLobbyBotConfig(lobby_id, bot_instance_id),
@ -328,8 +334,7 @@ export const botsApi = {
updateConfig: (data: any) => apiClient.createUpdateBotConfig(data), updateConfig: (data: any) => apiClient.createUpdateBotConfig(data),
getConfigStatistics: () => apiClient.getConfigStatistics(), getConfigStatistics: () => apiClient.getConfigStatistics(),
refreshAllSchemas: () => apiClient.createRefreshBotSchemas(), refreshAllSchemas: () => apiClient.createRefreshBotSchemas(),
refreshSchema: (bot_name: string) => apiClient.createRefreshBotSchema(bot_name), refreshSchemaByName: (bot_name: string) => apiClient.createRefreshBotSchema(bot_name)
clearSchemaCache: (bot_name: string) => apiClient.deleteClearBotSchemaCache(bot_name)
}; };
export const metricsApi = { export const metricsApi = {

View File

@ -6,13 +6,13 @@ import { base } from './Common';
export const knownEndpoints = new Set([ export const knownEndpoints = new Set([
`DELETE:${base}/api/bots/config/lobby/{lobby_id}`, `DELETE:${base}/api/bots/config/lobby/{lobby_id}`,
`DELETE:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_instance_id}`, `DELETE:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_instance_id}`,
`DELETE:${base}/api/bots/config/schema/{bot_name}/cache`,
`GET:${base}/api/admin/names`, `GET:${base}/api/admin/names`,
`GET:${base}/api/admin/session_metrics`, `GET:${base}/api/admin/session_metrics`,
`GET:${base}/api/admin/validate_sessions`, `GET:${base}/api/admin/validate_sessions`,
`GET:${base}/api/bots`, `GET:${base}/api/bots`,
`GET:${base}/api/bots/config/lobby/{lobby_id}`, `GET:${base}/api/bots/config/lobby/{lobby_id}`,
`GET:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_instance_id}`, `GET:${base}/api/bots/config/lobby/{lobby_id}/bot/{bot_instance_id}`,
`GET:${base}/api/bots/config/schema/instance/{bot_instance_id}`,
`GET:${base}/api/bots/config/schema/{bot_name}`, `GET:${base}/api/bots/config/schema/{bot_name}`,
`GET:${base}/api/bots/config/statistics`, `GET:${base}/api/bots/config/statistics`,
`GET:${base}/api/bots/instances/{bot_instance_id}`, `GET:${base}/api/bots/instances/{bot_instance_id}`,
@ -34,6 +34,7 @@ export const knownEndpoints = new Set([
`POST:${base}/api/admin/clear_password`, `POST:${base}/api/admin/clear_password`,
`POST:${base}/api/admin/set_password`, `POST:${base}/api/admin/set_password`,
`POST:${base}/api/bots/config/refresh-schemas`, `POST:${base}/api/bots/config/refresh-schemas`,
`POST:${base}/api/bots/config/schema/instance/{bot_instance_id}/refresh`,
`POST:${base}/api/bots/config/schema/{bot_name}/refresh`, `POST:${base}/api/bots/config/schema/{bot_name}/refresh`,
`POST:${base}/api/bots/config/update`, `POST:${base}/api/bots/config/update`,
`POST:${base}/api/bots/instances/{bot_instance_id}/leave`, `POST:${base}/api/bots/instances/{bot_instance_id}/leave`,

View File

@ -111,10 +111,29 @@ export interface paths {
"/ai-voicebot/api/bots/config/schema/{bot_name}": { "/ai-voicebot/api/bots/config/schema/{bot_name}": {
/** /**
* Get Bot Config Schema * Get Bot Config Schema
* @description Get configuration schema for a specific bot * @description Get configuration schema for a specific bot.
*
* This endpoint will query registered bot providers each time and
* request the bot's /config-schema endpoint without relying on any
* server-side cached schema. This ensures the UI always receives the
* up-to-date schema from the provider.
*/ */
get: operations["get_bot_config_schema_ai_voicebot_api_bots_config_schema__bot_name__get"]; get: operations["get_bot_config_schema_ai_voicebot_api_bots_config_schema__bot_name__get"];
}; };
"/ai-voicebot/api/bots/config/schema/instance/{bot_instance_id}": {
/**
* Get Bot Config Schema By Instance
* @description Get configuration schema for a specific bot instance
*/
get: operations["get_bot_config_schema_by_instance_ai_voicebot_api_bots_config_schema_instance__bot_instance_id__get"];
};
"/ai-voicebot/api/bots/config/schema/instance/{bot_instance_id}/refresh": {
/**
* Refresh Bot Schema By Instance
* @description Refresh configuration schema for a specific bot instance
*/
post: operations["refresh_bot_schema_by_instance_ai_voicebot_api_bots_config_schema_instance__bot_instance_id__refresh_post"];
};
"/ai-voicebot/api/bots/config/lobby/{lobby_id}": { "/ai-voicebot/api/bots/config/lobby/{lobby_id}": {
/** /**
* Get Lobby Bot Configs * Get Lobby Bot Configs
@ -167,13 +186,6 @@ export interface paths {
*/ */
post: operations["refresh_bot_schema_ai_voicebot_api_bots_config_schema__bot_name__refresh_post"]; post: operations["refresh_bot_schema_ai_voicebot_api_bots_config_schema__bot_name__refresh_post"];
}; };
"/ai-voicebot/api/bots/config/schema/{bot_name}/cache": {
/**
* Clear Bot Schema Cache
* @description Clear cached schema for a specific bot
*/
delete: operations["clear_bot_schema_cache_ai_voicebot_api_bots_config_schema__bot_name__cache_delete"];
};
"/ai-voicebot/api/health/ready": { "/ai-voicebot/api/health/ready": {
/** /**
* Readiness Probe * Readiness Probe
@ -1146,7 +1158,12 @@ export interface operations {
}; };
/** /**
* Get Bot Config Schema * Get Bot Config Schema
* @description Get configuration schema for a specific bot * @description Get configuration schema for a specific bot.
*
* This endpoint will query registered bot providers each time and
* request the bot's /config-schema endpoint without relying on any
* server-side cached schema. This ensures the UI always receives the
* up-to-date schema from the provider.
*/ */
get_bot_config_schema_ai_voicebot_api_bots_config_schema__bot_name__get: { get_bot_config_schema_ai_voicebot_api_bots_config_schema__bot_name__get: {
parameters: { parameters: {
@ -1169,6 +1186,58 @@ export interface operations {
}; };
}; };
}; };
/**
* Get Bot Config Schema By Instance
* @description Get configuration schema for a specific bot instance
*/
get_bot_config_schema_by_instance_ai_voicebot_api_bots_config_schema_instance__bot_instance_id__get: {
parameters: {
path: {
bot_instance_id: string;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": components["schemas"]["BotConfigSchema"];
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/**
* Refresh Bot Schema By Instance
* @description Refresh configuration schema for a specific bot instance
*/
refresh_bot_schema_by_instance_ai_voicebot_api_bots_config_schema_instance__bot_instance_id__refresh_post: {
parameters: {
path: {
bot_instance_id: string;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": {
[key: string]: unknown;
};
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/** /**
* Get Lobby Bot Configs * Get Lobby Bot Configs
* @description Get all bot configurations for a lobby * @description Get all bot configurations for a lobby
@ -1364,33 +1433,6 @@ export interface operations {
}; };
}; };
}; };
/**
* Clear Bot Schema Cache
* @description Clear cached schema for a specific bot
*/
clear_bot_schema_cache_ai_voicebot_api_bots_config_schema__bot_name__cache_delete: {
parameters: {
path: {
bot_name: string;
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": {
[key: string]: unknown;
};
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/** /**
* Readiness Probe * Readiness Probe
* @description Kubernetes readiness probe endpoint. * @description Kubernetes readiness probe endpoint.

View File

@ -201,7 +201,31 @@ class ApiClientGenerator {
// Generate convenience methods for each namespace // Generate convenience methods for each namespace
const namespaceDefinitions = Object.entries(namespaceGroups).map(([namespace, endpoints]) => { const namespaceDefinitions = Object.entries(namespaceGroups).map(([namespace, endpoints]) => {
const methods = endpoints.map(endpoint => this.generateConvenienceMethod(endpoint)).join(',\n '); // Track used method names to avoid duplicate object keys
const usedNames = new Set();
const methods = endpoints.map(endpoint => {
// Compute desired method name
let candidate = this.generateConvenienceMethodName(endpoint);
// If name already used, try to disambiguate with contextual suffixes
if (usedNames.has(candidate)) {
if (endpoint.path.includes('/instance/')) {
candidate = candidate + 'ByInstance';
} else if (endpoint.path.match(/\/config\/schema\/\{?bot_name\}?/)) {
candidate = candidate + 'ByName';
} else {
// Fallback: append a numeric suffix to ensure uniqueness
let i = 2;
while (usedNames.has(candidate + i)) i++;
candidate = candidate + i;
}
}
usedNames.add(candidate);
return this.generateConvenienceMethod(endpoint, candidate);
}).join(',\n ');
return `export const ${namespace}Api = {\n ${methods}\n};`; return `export const ${namespace}Api = {\n ${methods}\n};`;
}).join('\n\n'); }).join('\n\n');
@ -255,8 +279,8 @@ class ApiClientGenerator {
/** /**
* Generate a convenience method for an endpoint with intuitive naming * Generate a convenience method for an endpoint with intuitive naming
*/ */
generateConvenienceMethod(endpoint) { generateConvenienceMethod(endpoint, overrideName) {
const methodName = this.generateConvenienceMethodName(endpoint); const methodName = overrideName || this.generateConvenienceMethodName(endpoint);
const clientMethodName = this.generateMethodName(endpoint); const clientMethodName = this.generateMethodName(endpoint);
const params = this.extractMethodParameters(endpoint); const params = this.extractMethodParameters(endpoint);

View File

@ -61,64 +61,39 @@ def create_bot_config_router(
@router.get("/schema/{bot_name}") @router.get("/schema/{bot_name}")
async def get_bot_config_schema(bot_name: str) -> BotConfigSchema: # type: ignore async def get_bot_config_schema(bot_name: str) -> BotConfigSchema: # type: ignore
"""Get configuration schema for a specific bot""" """Get configuration schema for a specific bot.
This endpoint will query registered bot providers each time and
request the bot's /config-schema endpoint without relying on any
server-side cached schema. This ensures the UI always receives the
up-to-date schema from the provider.
"""
try: try:
# Check if we have cached schema providers_response = bot_manager.list_providers()
schema = config_manager.get_bot_config_schema(bot_name) schema = None
for provider in providers_response.providers:
try:
# Check if this provider has the bot
provider_bots = await bot_manager.get_provider_bots(
provider.provider_id
)
bot_names = [bot.name for bot in provider_bots.bots]
if not schema: if bot_name in bot_names:
# Try to discover schema from bot provider # Get the full provider object to access base_url
providers_response = bot_manager.list_providers() full_provider = bot_manager.get_provider(provider.provider_id)
for provider in providers_response.providers: if full_provider:
try: # Force discovery from the provider on every request
# Check if this provider has the bot schema = await config_manager.discover_bot_config_schema(
provider_bots = await bot_manager.get_provider_bots( bot_name, full_provider.base_url, force_refresh=True
provider.provider_id )
) if schema:
bot_names = [bot.name for bot in provider_bots.bots] break
except Exception as e:
if bot_name in bot_names: logger.warning(
# Get the full provider object to access base_url f"Failed to check provider {provider.provider_id} for bot {bot_name}: {e}"
full_provider = bot_manager.get_provider(provider.provider_id) )
if full_provider: continue
schema = await config_manager.discover_bot_config_schema(
bot_name, full_provider.base_url
)
if schema:
break
except Exception as e:
logger.warning(
f"Failed to check provider {provider.provider_id} for bot {bot_name}: {e}"
)
continue
else:
# We have a cached schema, but check if it might be stale
# Try to refresh it automatically if it's older than 1 hour
providers_response = bot_manager.list_providers()
for provider in providers_response.providers:
try:
provider_bots = await bot_manager.get_provider_bots(
provider.provider_id
)
bot_names = [bot.name for bot in provider_bots.bots]
if bot_name in bot_names:
# This will only refresh if the cached schema is older than 1 hour
# Get the full provider object to access base_url
full_provider = bot_manager.get_provider(provider.provider_id)
if full_provider:
fresh_schema = (
await config_manager.discover_bot_config_schema(
bot_name, full_provider.base_url, force_refresh=False
)
)
if fresh_schema:
schema = fresh_schema
break
except Exception as e:
logger.debug(f"Failed to refresh schema for {bot_name}: {e}")
# Continue with cached schema if refresh fails
continue
if not schema: if not schema:
raise HTTPException( raise HTTPException(
@ -142,23 +117,19 @@ def create_bot_config_router(
bot_instance = await bot_manager.get_bot_instance(bot_instance_id) bot_instance = await bot_manager.get_bot_instance(bot_instance_id)
bot_name = bot_instance.bot_name bot_name = bot_instance.bot_name
# Check if we have cached schema # Always query the provider directly for the latest schema
schema = config_manager.get_bot_config_schema(bot_name) provider = bot_manager.get_provider(bot_instance.provider_id)
if provider:
if not schema: try:
# Try to discover schema from bot provider schema = await config_manager.discover_bot_config_schema(
provider = bot_manager.get_provider(bot_instance.provider_id) bot_name, provider.base_url, force_refresh=True
if provider: )
try: except Exception as e:
schema = await config_manager.discover_bot_config_schema( logger.warning(
bot_name, provider.base_url f"Failed to discover schema for bot {bot_name} from provider {bot_instance.provider_id}: {e}"
) )
except Exception as e: else:
logger.warning( logger.warning(f"Provider {bot_instance.provider_id} not found")
f"Failed to discover schema for bot {bot_name} from provider {bot_instance.provider_id}: {e}"
)
else:
logger.warning(f"Provider {bot_instance.provider_id} not found")
if not schema: if not schema:
raise HTTPException( raise HTTPException(
@ -195,8 +166,9 @@ def create_bot_config_router(
status_code=404, detail=f"Provider '{provider_id}' not found" status_code=404, detail=f"Provider '{provider_id}' not found"
) )
schema = await config_manager.refresh_bot_schema( # Force a fresh fetch from the provider
bot_name, provider.base_url schema = await config_manager.discover_bot_config_schema(
bot_name, provider.base_url, force_refresh=True
) )
if schema: if schema:
return { return {
@ -478,25 +450,9 @@ def create_bot_config_router(
logger.error(f"Failed to refresh schema for bot {bot_name}: {e}") logger.error(f"Failed to refresh schema for bot {bot_name}: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/schema/{bot_name}/cache") # NOTE: schema cache management endpoints removed. Server no longer
async def clear_bot_schema_cache(bot_name: str) -> Dict[str, Any]: # stores or serves cached bot config schemas; callers should fetch
"""Clear cached schema for a specific bot""" # live schemas from providers via the /schema endpoints.
try:
success = config_manager.clear_bot_schema_cache(bot_name)
if success:
return {
"success": True,
"message": f"Schema cache cleared for bot {bot_name}",
}
else:
raise HTTPException(
status_code=404, detail=f"No cached schema found for bot {bot_name}"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to clear schema cache for bot {bot_name}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
return router return router

View File

@ -31,22 +31,22 @@ class BotConfigManager:
def __init__(self, storage_dir: str = "./bot_configs"): def __init__(self, storage_dir: str = "./bot_configs"):
self.storage_dir = Path(storage_dir) self.storage_dir = Path(storage_dir)
self.storage_dir.mkdir(exist_ok=True) self.storage_dir.mkdir(exist_ok=True)
# In-memory cache for fast access # In-memory cache for fast access
self.config_cache: Dict[str, Dict[str, BotLobbyConfig]] = {} # lobby_id -> bot_name -> config self.config_cache: Dict[
self.schema_cache: Dict[str, BotConfigSchema] = {} # bot_name -> schema str, Dict[str, BotLobbyConfig]
] = {} # lobby_id -> bot_name -> config
# Load existing configurations # Load existing configurations
self._load_configurations() self._load_configurations()
def _get_config_file(self, lobby_id: str) -> Path: def _get_config_file(self, lobby_id: str) -> Path:
"""Get configuration file path for a lobby""" """Get configuration file path for a lobby"""
return self.storage_dir / f"lobby_{lobby_id}.json" return self.storage_dir / f"lobby_{lobby_id}.json"
def _get_schema_file(self, bot_name: str) -> Path: def _get_schema_file(self, bot_name: str) -> Path:
"""Get schema file path for a bot""" """Get schema file path for a bot"""
return self.storage_dir / f"schema_{bot_name}.json" return self.storage_dir / f"schema_{bot_name}.json"
def _load_configurations(self): def _load_configurations(self):
"""Load all configurations from disk""" """Load all configurations from disk"""
try: try:
@ -54,133 +54,86 @@ class BotConfigManager:
for config_file in self.storage_dir.glob("lobby_*.json"): for config_file in self.storage_dir.glob("lobby_*.json"):
try: try:
lobby_id = config_file.stem.replace("lobby_", "") lobby_id = config_file.stem.replace("lobby_", "")
with open(config_file, 'r') as f: with open(config_file, "r") as f:
data = json.load(f) data = json.load(f)
self.config_cache[lobby_id] = {} self.config_cache[lobby_id] = {}
for bot_name, config_data in data.items(): for bot_name, config_data in data.items():
config = BotLobbyConfig(**config_data) config = BotLobbyConfig(**config_data)
self.config_cache[lobby_id][bot_name] = config self.config_cache[lobby_id][bot_name] = config
except Exception as e: except Exception as e:
logger.error(f"Failed to load lobby config {config_file}: {e}") logger.error(f"Failed to load lobby config {config_file}: {e}")
# Load bot schemas logger.info(f"Loaded configurations for {len(self.config_cache)} lobbies")
for schema_file in self.storage_dir.glob("schema_*.json"):
try:
bot_name = schema_file.stem.replace("schema_", "")
with open(schema_file, 'r') as f:
schema_data = json.load(f)
schema = BotConfigSchema(**schema_data)
self.schema_cache[bot_name] = schema
except Exception as e:
logger.error(f"Failed to load bot schema {schema_file}: {e}")
logger.info(f"Loaded configurations for {len(self.config_cache)} lobbies and {len(self.schema_cache)} bot schemas")
except Exception as e: except Exception as e:
logger.error(f"Failed to load configurations: {e}") logger.error(f"Failed to load configurations: {e}")
def _save_lobby_config(self, lobby_id: str): def _save_lobby_config(self, lobby_id: str):
"""Save lobby configuration to disk""" """Save lobby configuration to disk"""
try: try:
config_file = self._get_config_file(lobby_id) config_file = self._get_config_file(lobby_id)
if lobby_id not in self.config_cache: if lobby_id not in self.config_cache:
return return
# Convert to serializable format # Convert to serializable format
data = {} data = {}
for bot_name, config in self.config_cache[lobby_id].items(): for bot_name, config in self.config_cache[lobby_id].items():
data[bot_name] = config.model_dump() data[bot_name] = config.model_dump()
with open(config_file, 'w') as f: with open(config_file, "w") as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2)
except Exception as e: except Exception as e:
logger.error(f"Failed to save lobby config {lobby_id}: {e}") logger.error(f"Failed to save lobby config {lobby_id}: {e}")
def _save_bot_schema(self, bot_name: str): def _save_bot_schema(self, bot_name: str):
"""Save bot schema to disk""" """Save bot schema to disk"""
try: # Schema file persistence disabled: server no longer caches provider schemas to disk.
if bot_name not in self.schema_cache: logger.debug(
return f"_save_bot_schema called for {bot_name}, but schema persistence is disabled"
)
schema_file = self._get_schema_file(bot_name)
schema_data = self.schema_cache[bot_name].model_dump()
with open(schema_file, 'w') as f:
json.dump(schema_data, f, indent=2)
except Exception as e:
logger.error(f"Failed to save bot schema {bot_name}: {e}")
async def discover_bot_config_schema( async def discover_bot_config_schema(
self, bot_name: str, provider_url: str, force_refresh: bool = False self, bot_name: str, provider_url: str, force_refresh: bool = False
) -> Optional[BotConfigSchema]: ) -> Optional[BotConfigSchema]:
"""Discover configuration schema from bot provider""" """Discover configuration schema from bot provider"""
try: try:
# Check if we have a cached schema and it's not forced refresh # Always fetch schema directly from the provider; do not use or
if not force_refresh and bot_name in self.schema_cache: # update any server-side caches or files. This ensures callers
cached_schema = self.schema_cache[bot_name] # receive the live schema from the provider on each request.
# Check if schema is less than 1 hour old
schema_file = self._get_schema_file(bot_name)
if schema_file.exists():
file_age = time.time() - schema_file.stat().st_mtime
if file_age < 3600: # 1 hour
logger.debug(
f"Using cached schema for bot {bot_name} (age: {file_age:.0f}s)"
)
return cached_schema
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
# Try to get configuration schema from bot provider
response = await client.get( response = await client.get(
f"{provider_url}/bots/{bot_name}/config-schema", f"{provider_url}/bots/{bot_name}/config-schema",
timeout=10.0 timeout=10.0,
) )
if response.status_code == 200: if response.status_code == 200:
schema_data = response.json() schema_data = response.json()
schema = BotConfigSchema(**schema_data) schema = BotConfigSchema(**schema_data)
logger.info(f"Fetched live config schema for bot {bot_name}")
# Check if schema has actually changed
if bot_name in self.schema_cache:
old_schema = self.schema_cache[bot_name]
if old_schema.model_dump() == schema.model_dump():
logger.debug(
f"Schema for bot {bot_name} unchanged, updating timestamp only"
)
else:
logger.info(f"Schema for bot {bot_name} has been updated")
# Cache the schema
self.schema_cache[bot_name] = schema
self._save_bot_schema(bot_name)
logger.info(
f"Discovered/refreshed config schema for bot {bot_name}"
)
return schema return schema
else: else:
logger.warning(f"Bot {bot_name} does not support configuration (HTTP {response.status_code})") logger.warning(
f"Bot {bot_name} does not support configuration (HTTP {response.status_code})"
)
except Exception as e: except Exception as e:
logger.warning(f"Failed to discover config schema for bot {bot_name}: {e}") logger.warning(f"Failed to discover config schema for bot {bot_name}: {e}")
# Return cached schema if available, even if refresh failed
if bot_name in self.schema_cache:
logger.info(
f"Returning cached schema for bot {bot_name} after refresh failure"
)
return self.schema_cache[bot_name]
return None return None
def get_bot_config_schema(self, bot_name: str) -> Optional[BotConfigSchema]: def get_bot_config_schema(self, bot_name: str) -> Optional[BotConfigSchema]:
"""Get cached configuration schema for a bot""" """Deprecated: server no longer maintains a cached schema.
return self.schema_cache.get(bot_name)
Return None to indicate no server-side cached schema is available.
"""
logger.debug(
"get_bot_config_schema called but server-side schema cache is disabled"
)
return None
async def refresh_bot_schema( async def refresh_bot_schema(
self, bot_name: str, provider_url: str self, bot_name: str, provider_url: str
@ -192,14 +145,11 @@ class BotConfigManager:
def clear_bot_schema_cache(self, bot_name: str) -> bool: def clear_bot_schema_cache(self, bot_name: str) -> bool:
"""Clear cached schema for a specific bot""" """Clear cached schema for a specific bot"""
if bot_name in self.schema_cache: # No-op: server-side schema caching has been disabled. Return False to
del self.schema_cache[bot_name] # indicate there was no cached schema to clear.
# Also remove the cached file logger.info(
schema_file = self._get_schema_file(bot_name) f"clear_bot_schema_cache called for {bot_name} but caching is disabled"
if schema_file.exists(): )
schema_file.unlink()
logger.info(f"Cleared schema cache for bot {bot_name}")
return True
return False return False
def get_lobby_bot_config(self, lobby_id: str, bot_name: str) -> Optional[BotLobbyConfig]: def get_lobby_bot_config(self, lobby_id: str, bot_name: str) -> Optional[BotLobbyConfig]:
@ -221,13 +171,10 @@ class BotConfigManager:
config_values: Dict[str, Any], config_values: Dict[str, Any],
session_id: str) -> BotLobbyConfig: session_id: str) -> BotLobbyConfig:
"""Set or update bot configuration for a lobby""" """Set or update bot configuration for a lobby"""
# Schema validation against a server-side cache is disabled. If
# Validate configuration against schema if available # callers want strict validation, they should fetch the provider's
schema = self.get_bot_config_schema(bot_name) # live schema and validate prior to calling set_bot_config.
if schema: validated_values = config_values
validated_values = self._validate_config_values(config_values, schema)
else:
validated_values = config_values
# Create or update configuration # Create or update configuration
now = time.time() now = time.time()
@ -388,9 +335,9 @@ class BotConfigManager:
return { return {
"total_lobbies": len(self.config_cache), "total_lobbies": len(self.config_cache),
"total_configs": total_configs, "total_configs": total_configs,
"cached_schemas": len(self.schema_cache), # server-side schema caching is disabled; omit cached_schemas
"lobbies": { "lobbies": {
lobby_id: len(configs) lobby_id: len(configs)
for lobby_id, configs in self.config_cache.items() for lobby_id, configs in self.config_cache.items()
} },
} }

View File

@ -254,6 +254,12 @@ VAD_CONFIG = {
"speech_freq_max": 3000, # Hz "speech_freq_max": 3000, # Hz
} }
# Normalization defaults: used to control optional per-stream normalization
# applied before sending audio to the model and for visualization.
NORMALIZATION_ENABLED = True
NORMALIZATION_TARGET_PEAK = 0.95
MAX_NORMALIZATION_GAIN = 3.0
# How long (seconds) of no-arriving audio before we consider the phrase ended # How long (seconds) of no-arriving audio before we consider the phrase ended
INACTIVITY_TIMEOUT = 1.5 INACTIVITY_TIMEOUT = 1.5
@ -1154,6 +1160,15 @@ class OptimizedAudioProcessor:
# Enhanced VAD parameters with EMA for noise adaptation # Enhanced VAD parameters with EMA for noise adaptation
self.advanced_vad = AdvancedVAD(sample_rate=self.sample_rate) self.advanced_vad = AdvancedVAD(sample_rate=self.sample_rate)
# Track maximum observed absolute amplitude for this input stream
# This is used optionally to normalize incoming audio to the "observed"
# maximum which helps models expect a consistent level across peers.
# It's intentionally permissive and capped to avoid amplifying noise.
self.max_observed_amplitude: float = 1e-6
self.normalization_enabled: bool = True
self.normalization_target_peak: float = 0.95
self.max_normalization_gain: float = 3.0 # avoid amplifying tiny noise too much
# Processing state # Processing state
self.current_phrase_audio = np.array([], dtype=np.float32) self.current_phrase_audio = np.array([], dtype=np.float32)
self.transcription_history: List[TranscriptionHistoryItem] = [] self.transcription_history: List[TranscriptionHistoryItem] = []
@ -1232,6 +1247,15 @@ class OptimizedAudioProcessor:
# Update last audio time whenever any audio is received # Update last audio time whenever any audio is received
self.last_audio_time = time.time() self.last_audio_time = time.time()
# Update max observed amplitude (used later for optional normalization)
try:
peak = float(np.max(np.abs(audio_data))) if audio_data.size > 0 else 0.0
if peak > self.max_observed_amplitude:
self.max_observed_amplitude = float(peak)
except Exception:
# Be defensive - don't fail audio ingestion for amplitude tracking
pass
is_speech, vad_metrics = self.advanced_vad.analyze_frame(audio_data) is_speech, vad_metrics = self.advanced_vad.analyze_frame(audio_data)
# Update visualization status # Update visualization status
@ -1386,8 +1410,10 @@ class OptimizedAudioProcessor:
asyncio.run_coroutine_threadsafe(_send_final_marker(), self.main_loop) asyncio.run_coroutine_threadsafe(_send_final_marker(), self.main_loop)
except Exception: except Exception:
logger.debug(f"Could not schedule final marker for {self.peer_name}") logger.debug(f"Could not schedule final marker for {self.peer_name}")
else: # As a fallback (if we couldn't schedule the marker on the
# As a fallback, try to schedule the normal coroutine if possible # main loop), try to schedule the normal async transcription
# coroutine. This is only used when the immediate marker
# cannot be scheduled — avoid scheduling both paths.
try: try:
asyncio.create_task( asyncio.create_task(
self._transcribe_and_send( self._transcribe_and_send(
@ -1538,9 +1564,17 @@ class OptimizedAudioProcessor:
logger.info( logger.info(
f"Final transcription timeout for {self.peer_name} (asyncio.TimeoutError, inactivity)" f"Final transcription timeout for {self.peer_name} (asyncio.TimeoutError, inactivity)"
) )
await self._transcribe_and_send( # Avoid duplicate finals: if a final is already pending
self.current_phrase_audio.copy(), is_final=True # (for example the blocking final was queued), skip scheduling
) # another final. Otherwise set the pending flag and run the
# final transcription.
if not self.final_transcription_pending:
self.final_transcription_pending = True
await self._transcribe_and_send(
self.current_phrase_audio.copy(), is_final=True
)
else:
logger.debug(f"Final already pending for {self.peer_name}; skipping async final")
self.current_phrase_audio = np.array([], dtype=np.float32) self.current_phrase_audio = np.array([], dtype=np.float32)
except Exception as e: except Exception as e:
logger.error( logger.error(
@ -1599,12 +1633,17 @@ class OptimizedAudioProcessor:
logger.info( logger.info(
f"Final transcription from thread for {self.peer_name} (inactivity)" f"Final transcription from thread for {self.peer_name} (inactivity)"
) )
asyncio.run_coroutine_threadsafe( # Avoid scheduling duplicates if a final is already pending
self._transcribe_and_send( if not self.final_transcription_pending:
self.current_phrase_audio.copy(), is_final=True self.final_transcription_pending = True
), asyncio.run_coroutine_threadsafe(
self.main_loop, self._transcribe_and_send(
) self.current_phrase_audio.copy(), is_final=True
),
self.main_loop,
)
else:
logger.debug(f"Final already pending for {self.peer_name}; skipping thread-scheduled final")
self.current_phrase_audio = np.array([], dtype=np.float32) self.current_phrase_audio = np.array([], dtype=np.float32)
except Exception as e: except Exception as e:
logger.error( logger.error(
@ -1679,15 +1718,38 @@ class OptimizedAudioProcessor:
try: try:
audio_duration = len(audio_array) / self.sample_rate audio_duration = len(audio_array) / self.sample_rate
# Skip very short audio # Compute basic energy/peak metrics for filtering decisions
if audio_duration < 0.3: audio_rms = float(np.sqrt(np.mean(audio_array**2)))
audio_peak = float(np.max(np.abs(audio_array))) if audio_array.size > 0 else 0.0
# Short-burst filtering: drop very short bursts that are likely noise.
# - If duration < 0.5s and RMS is very low -> drop
# - If duration < 0.8s and peak is very small relative to the
# max observed amplitude -> drop. This prevents single-packet
# random noises from becoming transcriptions.
short_duration_threshold = 0.5
relaxed_short_duration = 0.8
rms_min_threshold = 0.002
relative_peak_min_ratio = 0.05
if audio_duration < short_duration_threshold and audio_rms < rms_min_threshold:
logger.debug( logger.debug(
f"Skipping {transcription_type} transcription: too short ({audio_duration:.2f}s)" f"Skipping {transcription_type} transcription: short & quiet ({audio_duration:.2f}s, RMS {audio_rms:.6f})"
) )
return return
# Audio quality check # If we have observed a stronger level on this stream, require a
audio_rms = np.sqrt(np.mean(audio_array**2)) # sensible fraction of that to consider this burst valid.
max_amp = getattr(self, "max_observed_amplitude", 0.0) or 0.0
if audio_duration < relaxed_short_duration and max_amp > 0.0:
rel = audio_peak / (max_amp + 1e-12)
if rel < relative_peak_min_ratio:
logger.debug(
f"Skipping {transcription_type} transcription: short burst with low relative peak ({audio_duration:.2f}s, rel {rel:.3f})"
)
return
# Very quiet audio - skip entirely
if audio_rms < 0.001: if audio_rms < 0.001:
logger.debug( logger.debug(
f"Skipping {transcription_type} transcription: too quiet (RMS: {audio_rms:.6f})" f"Skipping {transcription_type} transcription: too quiet (RMS: {audio_rms:.6f})"
@ -1698,8 +1760,28 @@ class OptimizedAudioProcessor:
f"🎬 OpenVINO transcription ({transcription_type}) started: {audio_duration:.2f}s, RMS: {audio_rms:.4f}" f"🎬 OpenVINO transcription ({transcription_type}) started: {audio_duration:.2f}s, RMS: {audio_rms:.4f}"
) )
# Optionally normalize audio prior to feature extraction. We use
# the historical maximum observed amplitude for this stream to
# compute a conservative gain. The gain is clamped to avoid
# amplifying noise excessively.
audio_for_model = audio_array
try:
if getattr(self, "normalization_enabled", False):
stream_max = getattr(self, "max_observed_amplitude", 0.0) or 0.0
# Use the larger of observed max and current peak to avoid
# over-scaling when current chunk is the loudest.
denom = max(stream_max, audio_peak, 1e-12)
gain = float(self.normalization_target_peak) / denom
# Clamp gain
gain = max(min(gain, float(self.max_normalization_gain)), 0.25)
if abs(gain - 1.0) > 1e-3:
logger.debug(f"Applying normalization gain {gain:.3f} for {self.peer_name}")
audio_for_model = np.clip(audio_array * gain, -0.999, 0.999).astype(np.float32)
except Exception as e:
logger.debug(f"Normalization step failed for {self.peer_name}: {e}")
# Extract features for OpenVINO # Extract features for OpenVINO
input_features = extract_input_features(audio_array, self.sample_rate) input_features = extract_input_features(audio_for_model, self.sample_rate)
# logger.info(f"Features extracted for OpenVINO: {input_features.shape}") # logger.info(f"Features extracted for OpenVINO: {input_features.shape}")
# GPU inference with OpenVINO # GPU inference with OpenVINO
@ -2125,8 +2207,29 @@ class WaveformVideoTrack(MediaStreamTrack):
[np.zeros(samples_needed - len(arr), dtype=np.float32), arr] [np.zeros(samples_needed - len(arr), dtype=np.float32), arr]
) )
# Assume arr_segment is already in [-1, 1] # Single normalization code path: normalize based on the historical
norm = arr_segment # peak observed for this stream (proc.max_observed_amplitude). This
# ensures the waveform display is consistent over time and avoids
# using the instantaneous buffer peak.
proc = None
norm = arr_segment.astype(np.float32)
try:
proc = _audio_processors.get(pname)
if proc is not None and getattr(proc, "normalization_enabled", False):
stream_max = getattr(proc, "max_observed_amplitude", 0.0) or 0.0
denom = max(stream_max, 1e-12)
gain = float(proc.normalization_target_peak) / denom
gain = max(min(gain, float(proc.max_normalization_gain)), 0.25)
if abs(gain - 1.0) > 1e-6:
norm = np.clip(arr_segment * gain, -1.0, 1.0).astype(np.float32)
else:
norm = arr_segment.astype(np.float32)
else:
norm = arr_segment.astype(np.float32)
except Exception:
# Fall back to raw samples if normalization computation fails
proc = None
norm = arr_segment.astype(np.float32)
# Map audio samples to pixels across the width # Map audio samples to pixels across the width
if norm.size < self.width: if norm.size < self.width:
@ -2144,12 +2247,17 @@ class WaveformVideoTrack(MediaStreamTrack):
dtype=np.float32, dtype=np.float32,
) )
# For display we use the same `norm` computed above (single code
# path). Use `display_norm` alias to avoid confusion later in the
# code but don't recompute normalization.
display_norm = norm
# Draw waveform with color coding for speech detection # Draw waveform with color coding for speech detection
points: list[tuple[int, int]] = [] points: list[tuple[int, int]] = []
colors: list[tuple[int, int, int]] = [] # Color for each point colors: list[tuple[int, int, int]] = [] # Color for each point
for x in range(self.width): for x in range(self.width):
v = float(norm[x]) if x < norm.size and not np.isnan(norm[x]) else 0.0 v = float(display_norm[x]) if x < display_norm.size and not np.isnan(display_norm[x]) else 0.0
y = int((1.0 - ((v + 1.0) / 2.0)) * (self.height - 120)) + 100 y = int((1.0 - ((v + 1.0) / 2.0)) * (self.height - 120)) + 100
points.append((x, y)) points.append((x, y))
@ -2169,6 +2277,33 @@ class WaveformVideoTrack(MediaStreamTrack):
for i in range(len(points) - 1): for i in range(len(points) - 1):
cv2.line(frame_array, points[i], points[i+1], colors[i], 1) cv2.line(frame_array, points[i], points[i+1], colors[i], 1)
# Draw historical peak indicator (horizontal lines at +/-(target_peak))
try:
if proc is not None and getattr(proc, "normalization_enabled", False):
target_peak = float(getattr(proc, "normalization_target_peak", 0.0))
# Ensure target_peak is within [0, 1]
target_peak = max(0.0, min(1.0, target_peak))
def _amp_to_y(a: float) -> int:
return int((1.0 - ((a + 1.0) / 2.0)) * (self.height - 120)) + 100
top_y = _amp_to_y(target_peak)
bot_y = _amp_to_y(-target_peak)
# Draw thin magenta lines across the waveform area
cv2.line(frame_array, (0, top_y), (self.width - 1, top_y), (255, 0, 255), 1)
cv2.line(frame_array, (0, bot_y), (self.width - 1, bot_y), (255, 0, 255), 1)
# Label the peak with small text near the right edge
label = f"Peak:{target_peak:.2f}"
(tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
lx = max(10, self.width - tw - 12)
ly = max(12, top_y - 6)
cv2.putText(frame_array, label, (lx, ly), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 255), 1)
except Exception:
# Non-critical: ignore any drawing errors
pass
# Add speech detection status overlay # Add speech detection status overlay
if speech_info: if speech_info:
self._draw_speech_status(frame_array, speech_info, pname) self._draw_speech_status(frame_array, speech_info, pname)
@ -2706,11 +2841,41 @@ def get_config_schema() -> Dict[str, Any]:
"max_value": 0.5, "max_value": 0.5,
"step": 0.05 "step": 0.05
} }
,
{
"name": "normalization_enabled",
"type": "boolean",
"label": "Enable Normalization",
"description": "Normalize incoming audio based on observed peak amplitude before transcription and visualization",
"default_value": NORMALIZATION_ENABLED,
"required": False
},
{
"name": "normalization_target_peak",
"type": "number",
"label": "Normalization Target Peak",
"description": "Target peak (0-1) used when normalizing audio",
"default_value": NORMALIZATION_TARGET_PEAK,
"required": False,
"min_value": 0.5,
"max_value": 1.0
},
{
"name": "max_normalization_gain",
"type": "range",
"label": "Max Normalization Gain",
"description": "Maximum allowed gain applied during normalization",
"default_value": MAX_NORMALIZATION_GAIN,
"required": False,
"min_value": 1.0,
"max_value": 10.0,
"step": 0.1
}
], ],
"categories": [ "categories": [
{"Model Settings": ["model_id", "device", "enable_quantization"]}, {"Model Settings": ["model_id", "device", "enable_quantization"]},
{"Performance Settings": ["throughput_streams", "max_threads"]}, {"Performance Settings": ["throughput_streams", "max_threads"]},
{"Audio Settings": ["sample_rate", "chunk_duration_ms"]}, {"Audio Settings": ["sample_rate", "chunk_duration_ms", "normalization_enabled", "normalization_target_peak", "max_normalization_gain"]},
{"Voice Activity Detection": ["vad_threshold", "max_silence_frames", "max_trailing_silence_frames", "vad_energy_threshold", "vad_zcr_min", "vad_zcr_max", "vad_spectral_centroid_min", "vad_spectral_centroid_max", "vad_spectral_rolloff_threshold", "vad_minimum_duration", "vad_max_history", "vad_noise_floor_energy", "vad_adaptation_rate", "vad_harmonic_threshold"]} {"Voice Activity Detection": ["vad_threshold", "max_silence_frames", "max_trailing_silence_frames", "vad_energy_threshold", "vad_zcr_min", "vad_zcr_max", "vad_spectral_centroid_min", "vad_spectral_centroid_max", "vad_spectral_rolloff_threshold", "vad_minimum_duration", "vad_max_history", "vad_noise_floor_energy", "vad_adaptation_rate", "vad_harmonic_threshold"]}
] ]
} }
@ -2838,6 +3003,36 @@ def handle_config_update(lobby_id: str, config_values: Dict[str, Any]) -> bool:
# Note: Existing processors would need to be recreated to pick up VAD changes # Note: Existing processors would need to be recreated to pick up VAD changes
# For now, we'll log that a restart may be needed # For now, we'll log that a restart may be needed
logger.info("VAD configuration updated - existing processors may need restart to take effect") logger.info("VAD configuration updated - existing processors may need restart to take effect")
# Normalization updates: apply to global defaults and active processors
norm_updates = False
if "normalization_enabled" in config_values:
NORMALIZATION_ENABLED = bool(config_values["normalization_enabled"])
norm_updates = True
logger.info(f"Updated NORMALIZATION_ENABLED to: {NORMALIZATION_ENABLED}")
if "normalization_target_peak" in config_values:
NORMALIZATION_TARGET_PEAK = float(config_values["normalization_target_peak"])
norm_updates = True
logger.info(f"Updated NORMALIZATION_TARGET_PEAK to: {NORMALIZATION_TARGET_PEAK}")
if "max_normalization_gain" in config_values:
MAX_NORMALIZATION_GAIN = float(config_values["max_normalization_gain"])
norm_updates = True
logger.info(f"Updated MAX_NORMALIZATION_GAIN to: {MAX_NORMALIZATION_GAIN}")
if norm_updates:
# Propagate changes to existing processors
try:
for pname, proc in list(_audio_processors.items()):
try:
proc.normalization_enabled = NORMALIZATION_ENABLED
proc.normalization_target_peak = NORMALIZATION_TARGET_PEAK
proc.max_normalization_gain = MAX_NORMALIZATION_GAIN
logger.info(f"Applied normalization config to processor: {pname}")
except Exception:
logger.debug(f"Failed to apply normalization config to processor: {pname}")
config_applied = True
except Exception:
logger.debug("Failed to propagate normalization settings to processors")
if config_applied: if config_applied:
logger.info(f"Configuration update completed for lobby {lobby_id}") logger.info(f"Configuration update completed for lobby {lobby_id}")