feat(teamsbot): AI voice test, config save, camelCase mapping, default voices
- Voice test endpoint generates sample text dynamically via AI in selected language - Fixed config save: added "config" to allowed update fields in interfaceFeatures - Clean camelCase mapping in interfaceVoiceObjects (audio_content -> audioContent) - Default TTS voices for common languages in connectorVoiceGoogle - Fixed updateFeatureInstanceConfig -> updateFeatureInstance with config field Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
89166a8f70
commit
e24ef42617
4 changed files with 50 additions and 9 deletions
|
|
@ -778,9 +778,17 @@ class ConnectorGoogleSpeech:
|
||||||
def _getDefaultVoice(self, languageCode: str) -> str:
|
def _getDefaultVoice(self, languageCode: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get default voice name for a language code.
|
Get default voice name for a language code.
|
||||||
Returns None - no defaults, let the frontend handle voice selection.
|
Falls back to a Wavenet voice for common languages.
|
||||||
"""
|
"""
|
||||||
return None
|
_defaults = {
|
||||||
|
"de-DE": "de-DE-Wavenet-A",
|
||||||
|
"de-CH": "de-DE-Wavenet-A",
|
||||||
|
"en-US": "en-US-Wavenet-C",
|
||||||
|
"en-GB": "en-GB-Wavenet-A",
|
||||||
|
"fr-FR": "fr-FR-Wavenet-A",
|
||||||
|
"it-IT": "it-IT-Wavenet-A",
|
||||||
|
}
|
||||||
|
return _defaults.get(languageCode)
|
||||||
|
|
||||||
async def getAvailableLanguages(self) -> Dict[str, Any]:
|
async def getAvailableLanguages(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -391,7 +391,7 @@ async def updateConfig(
|
||||||
# Save to FeatureInstance.config
|
# Save to FeatureInstance.config
|
||||||
rootInterface = getRootInterface()
|
rootInterface = getRootInterface()
|
||||||
featureInterface = getFeatureInterface(rootInterface.db)
|
featureInterface = getFeatureInterface(rootInterface.db)
|
||||||
featureInterface.updateFeatureInstanceConfig(instanceId, mergedConfig.model_dump())
|
featureInterface.updateFeatureInstance(instanceId, {"config": mergedConfig.model_dump()})
|
||||||
|
|
||||||
logger.info(f"Teamsbot config updated for instance {instanceId}: {list(updateDict.keys())}")
|
logger.info(f"Teamsbot config updated for instance {instanceId}: {list(updateDict.keys())}")
|
||||||
return {"config": mergedConfig.model_dump()}
|
return {"config": mergedConfig.model_dump()}
|
||||||
|
|
@ -408,20 +408,45 @@ async def testVoice(
|
||||||
instanceId: str,
|
instanceId: str,
|
||||||
context: RequestContext = Depends(getRequestContext),
|
context: RequestContext = Depends(getRequestContext),
|
||||||
):
|
):
|
||||||
"""Test TTS voice with a sample text. Returns base64-encoded audio."""
|
"""Test TTS voice with AI-generated sample text in the correct language."""
|
||||||
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
|
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
|
||||||
|
from modules.services.serviceAi.mainServiceAi import AiService
|
||||||
|
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum
|
||||||
|
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
|
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
text = body.get("text", "Hallo, ich bin der AI-Assistent. Wie kann ich helfen?")
|
|
||||||
language = body.get("language", "de-DE")
|
language = body.get("language", "de-DE")
|
||||||
voiceId = body.get("voiceId")
|
voiceId = body.get("voiceId")
|
||||||
|
botName = body.get("botName", "AI Assistant")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Generate test text dynamically via AI in the correct language
|
||||||
|
serviceContext = type('Ctx', (), {
|
||||||
|
'user': context.user, 'mandateId': mandateId,
|
||||||
|
'featureInstanceId': instanceId, 'featureCode': 'teamsbot'
|
||||||
|
})()
|
||||||
|
aiService = AiService(serviceCenter=serviceContext)
|
||||||
|
await aiService.ensureAiObjectsInitialized()
|
||||||
|
|
||||||
|
aiRequest = AiCallRequest(
|
||||||
|
prompt=f"Generate a short, friendly introduction sentence (max 2 sentences) for a meeting bot named '{botName}'. "
|
||||||
|
f"Write ONLY in the language '{language}'. No quotes, no explanation, just the text to speak.",
|
||||||
|
context="",
|
||||||
|
options=AiCallOptions(
|
||||||
|
operationType=OperationTypeEnum.DATA_ANALYSE,
|
||||||
|
priority=PriorityEnum.SPEED,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
aiResponse = await aiService.callAi(aiRequest)
|
||||||
|
testText = aiResponse.content.strip().strip('"').strip("'") if aiResponse and aiResponse.errorCount == 0 else f"Hello, I am {botName}."
|
||||||
|
|
||||||
|
logger.info(f"Voice test: generated text in {language}: '{testText[:60]}...'")
|
||||||
|
|
||||||
|
# Convert to speech
|
||||||
voiceInterface = getVoiceInterface(context.user, mandateId)
|
voiceInterface = getVoiceInterface(context.user, mandateId)
|
||||||
result = await voiceInterface.textToSpeech(
|
result = await voiceInterface.textToSpeech(
|
||||||
text=text,
|
text=testText,
|
||||||
languageCode=language,
|
languageCode=language,
|
||||||
voiceName=voiceId,
|
voiceName=voiceId,
|
||||||
)
|
)
|
||||||
|
|
@ -439,6 +464,7 @@ async def testVoice(
|
||||||
"format": "mp3",
|
"format": "mp3",
|
||||||
"language": language,
|
"language": language,
|
||||||
"voiceId": voiceId,
|
"voiceId": voiceId,
|
||||||
|
"text": testText,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {"success": False, "error": "TTS returned no audio"}
|
return {"success": False, "error": "TTS returned no audio"}
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,7 @@ class FeatureInterface:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Only allow updating specific fields
|
# Only allow updating specific fields
|
||||||
allowedFields = {"label", "enabled"}
|
allowedFields = {"label", "enabled", "config"}
|
||||||
filteredData = {k: v for k, v in updateData.items() if k in allowedFields}
|
filteredData = {k: v for k, v in updateData.items() if k in allowedFields}
|
||||||
|
|
||||||
if not filteredData:
|
if not filteredData:
|
||||||
|
|
|
||||||
|
|
@ -271,10 +271,17 @@ class VoiceObjects:
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
logger.info(f"✅ Text-to-Speech successful: {len(result['audio_content'])} bytes")
|
logger.info(f"✅ Text-to-Speech successful: {len(result['audio_content'])} bytes")
|
||||||
|
# Map connector snake_case keys to camelCase for consistent API
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"audioContent": result["audio_content"],
|
||||||
|
"audioFormat": result.get("audio_format", "mp3"),
|
||||||
|
"languageCode": result.get("language_code", languageCode),
|
||||||
|
"voiceName": result.get("voice_name", voiceName),
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ Text-to-Speech failed: {result.get('error', 'Unknown error')}")
|
logger.warning(f"⚠️ Text-to-Speech failed: {result.get('error', 'Unknown error')}")
|
||||||
|
return result
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Text-to-Speech error: {e}")
|
logger.error(f"❌ Text-to-Speech error: {e}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue