google keys transferred to account poweron.center.ai
This commit is contained in:
parent
7d43d032c3
commit
75e07743a6
13 changed files with 257 additions and 43 deletions
|
|
@ -19,7 +19,7 @@ APP_JWT_KEY_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpERjlrSktmZHVuQnJ1VVJDdndLaUcxZGJsT2Z
|
||||||
APP_TOKEN_EXPIRY=300
|
APP_TOKEN_EXPIRY=300
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://playground.poweron.swiss
|
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net
|
||||||
|
|
||||||
# Logging configuration
|
# Logging configuration
|
||||||
APP_LOGGING_LOG_LEVEL = DEBUG
|
APP_LOGGING_LOG_LEVEL = DEBUG
|
||||||
|
|
@ -39,11 +39,11 @@ Service_MSFT_DATA_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
|
||||||
Service_MSFT_DATA_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQm83T29rV1pQelMtc1p1MXR4NTFpa19CTEhHQ0xfNmdPUmZqcWp5UHBMS0hYTGl4c1pPdmhTNTJVWUl5WnlnUUZhV0VTRzVCb0d5YjR1NnZPZk5CZ0dGazNGdUJVbjkxeVdrYlNiVjJUYzF2aVFtQnVxTHFqTTJqZlF0RTFGNmE1OGN1TEk=
|
Service_MSFT_DATA_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQm83T29rV1pQelMtc1p1MXR4NTFpa19CTEhHQ0xfNmdPUmZqcWp5UHBMS0hYTGl4c1pPdmhTNTJVWUl5WnlnUUZhV0VTRzVCb0d5YjR1NnZPZk5CZ0dGazNGdUJVbjkxeVdrYlNiVjJUYzF2aVFtQnVxTHFqTTJqZlF0RTFGNmE1OGN1TEk=
|
||||||
Service_MSFT_DATA_REDIRECT_URI = http://localhost:8000/api/msft/auth/connect/callback
|
Service_MSFT_DATA_REDIRECT_URI = http://localhost:8000/api/msft/auth/connect/callback
|
||||||
|
|
||||||
Service_GOOGLE_AUTH_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
|
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
|
||||||
Service_GOOGLE_AUTH_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpETDJhbGVQMHlFQzNPVFI1ZzBMa3pNMGlQUHhaQm10eVl1bFlSeTBybzlTOWE2MURXQ0hkRlo0NlNGbHQxWEl1OVkxQnVKYlhhOXR1cUF4T3k0WDdscktkY1oyYllRTmdDTWpfbUdwWGtSd1JvNlYxeTBJdEtaaS1vYnItcW0yaFM=
|
Service_GOOGLE_AUTH_CLIENT_SECRET = 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
|
||||||
Service_GOOGLE_AUTH_REDIRECT_URI = http://localhost:8000/api/google/auth/login/callback
|
Service_GOOGLE_AUTH_REDIRECT_URI = http://localhost:8000/api/google/auth/login/callback
|
||||||
Service_GOOGLE_DATA_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
|
Service_GOOGLE_DATA_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
|
||||||
Service_GOOGLE_DATA_CLIENT_SECRET = DEV_ENC:Z0FBQUFBQm8xSUpETDJhbGVQMHlFQzNPVFI1ZzBMa3pNMGlQUHhaQm10eVl1bFlSeTBybzlTOWE2MURXQ0hkRlo0NlNGbHQxWEl1OVkxQnVKYlhhOXR1cUF4T3k0WDdscktkY1oyYllRTmdDTWpfbUdwWGtSd1JvNlYxeTBJdEtaaS1vYnItcW0yaFM=
|
Service_GOOGLE_DATA_CLIENT_SECRET = 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
|
||||||
Service_GOOGLE_DATA_REDIRECT_URI = http://localhost:8000/api/google/auth/connect/callback
|
Service_GOOGLE_DATA_REDIRECT_URI = http://localhost:8000/api/google/auth/connect/callback
|
||||||
|
|
||||||
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
|
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ APP_JWT_KEY_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNUctb2RwU25iR3ZnanBOdHZhWUtIajZ1RnZ
|
||||||
APP_TOKEN_EXPIRY=300
|
APP_TOKEN_EXPIRY=300
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://playground.poweron.swiss,https://playground-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net
|
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net
|
||||||
|
|
||||||
# Logging configuration
|
# Logging configuration
|
||||||
APP_LOGGING_LOG_LEVEL = DEBUG
|
APP_LOGGING_LOG_LEVEL = DEBUG
|
||||||
|
|
@ -41,11 +41,11 @@ Service_MSFT_DATA_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
|
||||||
Service_MSFT_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm83T29rMDZvcV9qTG5xb1FzUkdqS1llbzRxSEJXbmpONFFtcUtfZXdtZjQybmJSMjBjMEpnRVhiOGRuczZvVFBFdVVTQV80SG9PSnRQTEpLdVViNm5wc2E5aGRLWjZ4TGF1QjVkNmdRSzBpNWNkYXVublFYclVEdEM5TVBBZWVVMW5RVWk=
|
Service_MSFT_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm83T29rMDZvcV9qTG5xb1FzUkdqS1llbzRxSEJXbmpONFFtcUtfZXdtZjQybmJSMjBjMEpnRVhiOGRuczZvVFBFdVVTQV80SG9PSnRQTEpLdVViNm5wc2E5aGRLWjZ4TGF1QjVkNmdRSzBpNWNkYXVublFYclVEdEM5TVBBZWVVMW5RVWk=
|
||||||
Service_MSFT_DATA_REDIRECT_URI = https://gateway-int.poweron.swiss/api/msft/auth/connect/callback
|
Service_MSFT_DATA_REDIRECT_URI = https://gateway-int.poweron.swiss/api/msft/auth/connect/callback
|
||||||
|
|
||||||
Service_GOOGLE_AUTH_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
|
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
|
||||||
Service_GOOGLE_AUTH_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNThGeVRNd3hacThtRnE0bzlDa0JPUWQyaEd6QjlFckdsMGZjRlRfUks2bXV3aDdVRTF3LVRlZVY5WjVzSXV4ZGNnX002RDl3dkNYdGFzZkxVUW01My1wTHRCanVCLUozZEx4TlduQlB5MnpvNTR2SGlvbFl1YkhzTEtsSi1SOEo=
|
Service_GOOGLE_AUTH_CLIENT_SECRET = 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
|
||||||
Service_GOOGLE_AUTH_REDIRECT_URI = https://gateway-int.poweron.swiss/api/google/auth/login/callback
|
Service_GOOGLE_AUTH_REDIRECT_URI = https://gateway-int.poweron.swiss/api/google/auth/login/callback
|
||||||
Service_GOOGLE_DATA_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
|
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
|
||||||
Service_GOOGLE_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm8xSVRjNThGeVRNd3hacThtRnE0bzlDa0JPUWQyaEd6QjlFckdsMGZjRlRfUks2bXV3aDdVRTF3LVRlZVY5WjVzSXV4ZGNnX002RDl3dkNYdGFzZkxVUW01My1wTHRCanVCLUozZEx4TlduQlB5MnpvNTR2SGlvbFl1YkhzTEtsSi1SOEo=
|
Service_GOOGLE_AUTH_CLIENT_SECRET = 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
|
||||||
Service_GOOGLE_DATA_REDIRECT_URI = https://gateway-int.poweron.swiss/api/google/auth/connect/callback
|
Service_GOOGLE_DATA_REDIRECT_URI = https://gateway-int.poweron.swiss/api/google/auth/connect/callback
|
||||||
|
|
||||||
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
|
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,11 @@ Service_MSFT_DATA_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
|
||||||
Service_MSFT_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBESkk2T25scFU1T1pNd2FENTFRM3kzcEpSXy1HT0trQkR2Wnl3U3RYbExzRy1YUTkxd3lPZE84U2lhX3FZanp5TjhYRGluLXVjU3hjaWRBUnZLbVhtRDItZ3FxNXJ3MUxicUZTXzJWZVNrR0VKN3ZlNEtET1ppOFk0MzNmbkwyRmROUk4=
|
Service_MSFT_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBESkk2T25scFU1T1pNd2FENTFRM3kzcEpSXy1HT0trQkR2Wnl3U3RYbExzRy1YUTkxd3lPZE84U2lhX3FZanp5TjhYRGluLXVjU3hjaWRBUnZLbVhtRDItZ3FxNXJ3MUxicUZTXzJWZVNrR0VKN3ZlNEtET1ppOFk0MzNmbkwyRmROUk4=
|
||||||
Service_MSFT_DATA_REDIRECT_URI = https://api.poweron.swiss/api/msft/auth/connect/callback
|
Service_MSFT_DATA_REDIRECT_URI = https://api.poweron.swiss/api/msft/auth/connect/callback
|
||||||
|
|
||||||
Service_GOOGLE_AUTH_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
|
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
|
||||||
Service_GOOGLE_AUTH_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3eWFwSEZ4YnRJcjU1OW5kcXZKdkt1Z3gzWDFhVW5Eelh3VnpnNlppcWxweHY5UUQzeDIyVk83cW1XNVE4bllVWnR2MjlSQzFrV1UyUVV6OUt5b3Vqa3QzMUIwNFBqc2FVSXRxTlQ1OHVJZVFibnhBQ2puXzBwSXp5NUZhZjM1d1o=
|
Service_GOOGLE_AUTH_CLIENT_SECRET = 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
|
||||||
Service_GOOGLE_AUTH_REDIRECT_URI =
|
Service_GOOGLE_AUTH_REDIRECT_URI =
|
||||||
Service_GOOGLE_DATA_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
|
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
|
||||||
Service_GOOGLE_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3eWFwSEZ4YnRJcjU1OW5kcXZKdkt1Z3gzWDFhVW5Eelh3VnpnNlppcWxweHY5UUQzeDIyVk83cW1XNVE4bllVWnR2MjlSQzFrV1UyUVV6OUt5b3Vqa3QzMUIwNFBqc2FVSXRxTlQ1OHVJZVFibnhBQ2puXzBwSXp5NUZhZjM1d1o=
|
Service_GOOGLE_AUTH_CLIENT_SECRET = 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
|
||||||
Service_GOOGLE_DATA_REDIRECT_URI =
|
Service_GOOGLE_DATA_REDIRECT_URI =
|
||||||
|
|
||||||
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
|
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ APP_JWT_KEY_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3elhfV0Rnd2pQRjlMdkVwX1FnSmRhSzNZUl
|
||||||
APP_TOKEN_EXPIRY=300
|
APP_TOKEN_EXPIRY=300
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://playground.poweron.swiss,https://playground-int.poweron.swiss,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net
|
APP_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5176,https://nyla.poweron.swiss,https://nyla-int.poweron.swiss,https://nyla.poweron-center.net,https://nyla-int.poweron-center.net
|
||||||
|
|
||||||
# Logging configuration
|
# Logging configuration
|
||||||
APP_LOGGING_LOG_LEVEL = DEBUG
|
APP_LOGGING_LOG_LEVEL = DEBUG
|
||||||
|
|
@ -40,11 +40,11 @@ Service_MSFT_DATA_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
|
||||||
Service_MSFT_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBESkk2T25scFU1T1pNd2FENTFRM3kzcEpSXy1HT0trQkR2Wnl3U3RYbExzRy1YUTkxd3lPZE84U2lhX3FZanp5TjhYRGluLXVjU3hjaWRBUnZLbVhtRDItZ3FxNXJ3MUxicUZTXzJWZVNrR0VKN3ZlNEtET1ppOFk0MzNmbkwyRmROUk4=
|
Service_MSFT_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBESkk2T25scFU1T1pNd2FENTFRM3kzcEpSXy1HT0trQkR2Wnl3U3RYbExzRy1YUTkxd3lPZE84U2lhX3FZanp5TjhYRGluLXVjU3hjaWRBUnZLbVhtRDItZ3FxNXJ3MUxicUZTXzJWZVNrR0VKN3ZlNEtET1ppOFk0MzNmbkwyRmROUk4=
|
||||||
Service_MSFT_DATA_REDIRECT_URI = https://gateway-prod.poweron.swiss/api/msft/auth/connect/callback
|
Service_MSFT_DATA_REDIRECT_URI = https://gateway-prod.poweron.swiss/api/msft/auth/connect/callback
|
||||||
|
|
||||||
Service_GOOGLE_AUTH_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
|
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
|
||||||
Service_GOOGLE_AUTH_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3eWFwSEZ4YnRJcjU1OW5kcXZKdkt1Z3gzWDFhVW5Eelh3VnpnNlppcWxweHY5UUQzeDIyVk83cW1XNVE4bllVWnR2MjlSQzFrV1UyUVV6OUt5b3Vqa3QzMUIwNFBqc2FVSXRxTlQ1OHVJZVFibnhBQ2puXzBwSXp5NUZhZjM1d1o=
|
Service_GOOGLE_AUTH_CLIENT_SECRET = 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
|
||||||
Service_GOOGLE_AUTH_REDIRECT_URI = https://gateway-prod.poweron.swiss/api/google/auth/login/callback
|
Service_GOOGLE_AUTH_REDIRECT_URI = https://gateway-prod.poweron.swiss/api/google/auth/login/callback
|
||||||
Service_GOOGLE_DATA_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
|
Service_GOOGLE_AUTH_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.apps.googleusercontent.com
|
||||||
Service_GOOGLE_DATA_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQnBDM1Z3eWFwSEZ4YnRJcjU1OW5kcXZKdkt1Z3gzWDFhVW5Eelh3VnpnNlppcWxweHY5UUQzeDIyVk83cW1XNVE4bllVWnR2MjlSQzFrV1UyUVV6OUt5b3Vqa3QzMUIwNFBqc2FVSXRxTlQ1OHVJZVFibnhBQ2puXzBwSXp5NUZhZjM1d1o=
|
Service_GOOGLE_AUTH_CLIENT_SECRET = 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
|
||||||
Service_GOOGLE_DATA_REDIRECT_URI = https://gateway-prod.poweron.swiss/api/google/auth/connect/callback
|
Service_GOOGLE_DATA_REDIRECT_URI = https://gateway-prod.poweron.swiss/api/google/auth/connect/callback
|
||||||
|
|
||||||
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
|
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,30 @@ from modules.shared.voiceCatalog import getDefaultVoice as _catalogDefaultVoice
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _buildPrimarySttRecognitionFields(
|
||||||
|
*,
|
||||||
|
model: str,
|
||||||
|
lightweight: bool,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Shared fields for batch + streaming primary RecognitionConfig."""
|
||||||
|
base: Dict[str, Any] = {
|
||||||
|
"enable_automatic_punctuation": True,
|
||||||
|
"model": model,
|
||||||
|
}
|
||||||
|
if lightweight:
|
||||||
|
base["enable_word_time_offsets"] = False
|
||||||
|
base["enable_word_confidence"] = False
|
||||||
|
base["max_alternatives"] = 1
|
||||||
|
base["use_enhanced"] = False
|
||||||
|
else:
|
||||||
|
base["enable_word_time_offsets"] = True
|
||||||
|
base["enable_word_confidence"] = True
|
||||||
|
base["max_alternatives"] = 3
|
||||||
|
base["use_enhanced"] = True
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
# Gemini-TTS speaker IDs from voices.list use short names (e.g. "Kore") and require
|
# Gemini-TTS speaker IDs from voices.list use short names (e.g. "Kore") and require
|
||||||
# SynthesisInput.prompt + VoiceSelectionParams.model_name (google-cloud-texttospeech >= 2.24.0).
|
# SynthesisInput.prompt + VoiceSelectionParams.model_name (google-cloud-texttospeech >= 2.24.0).
|
||||||
_GEMINI_TTS_DEFAULT_MODEL = "gemini-2.5-flash-tts"
|
_GEMINI_TTS_DEFAULT_MODEL = "gemini-2.5-flash-tts"
|
||||||
|
|
@ -73,7 +97,10 @@ class ConnectorGoogleSpeech:
|
||||||
sampleRate: int = None, channels: int = None,
|
sampleRate: int = None, channels: int = None,
|
||||||
skipFallbacks: bool = False,
|
skipFallbacks: bool = False,
|
||||||
phraseHints: Optional[list] = None,
|
phraseHints: Optional[list] = None,
|
||||||
alternativeLanguages: Optional[list] = None) -> Dict:
|
alternativeLanguages: Optional[list] = None,
|
||||||
|
model: str = "latest_long",
|
||||||
|
lightweight: bool = False,
|
||||||
|
audioFormat: Optional[str] = None) -> Dict:
|
||||||
"""
|
"""
|
||||||
Convert speech to text using Google Cloud Speech-to-Text API.
|
Convert speech to text using Google Cloud Speech-to-Text API.
|
||||||
|
|
||||||
|
|
@ -82,6 +109,9 @@ class ConnectorGoogleSpeech:
|
||||||
language: Language code (e.g., 'de-DE', 'en-US')
|
language: Language code (e.g., 'de-DE', 'en-US')
|
||||||
sample_rate: Audio sample rate (auto-detected if None)
|
sample_rate: Audio sample rate (auto-detected if None)
|
||||||
channels: Number of audio channels (auto-detected if None)
|
channels: Number of audio channels (auto-detected if None)
|
||||||
|
model: Google recognition model (e.g. latest_long, latest_short)
|
||||||
|
lightweight: If True, omit word timings/confidence, single alternative, no enhanced model
|
||||||
|
audioFormat: If set (webm_opus, linear16, mp3, flac, wav), skip auto-detection
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict containing transcribed text, confidence, and metadata
|
Dict containing transcribed text, confidence, and metadata
|
||||||
|
|
@ -92,8 +122,24 @@ class ConnectorGoogleSpeech:
|
||||||
logger.warning(f"Invalid sampleRate={sampleRate}, treating as unknown for auto-detection")
|
logger.warning(f"Invalid sampleRate={sampleRate}, treating as unknown for auto-detection")
|
||||||
sampleRate = None
|
sampleRate = None
|
||||||
|
|
||||||
# Auto-detect audio format if not provided
|
explicitFormat = (audioFormat or "").strip().lower() or None
|
||||||
if sampleRate is None or channels is None:
|
if explicitFormat:
|
||||||
|
if channels is None:
|
||||||
|
channels = 1
|
||||||
|
if sampleRate is None:
|
||||||
|
if explicitFormat == "webm_opus":
|
||||||
|
sampleRate = 48000
|
||||||
|
elif explicitFormat == "linear16":
|
||||||
|
sampleRate = 16000
|
||||||
|
elif explicitFormat in ("mp3", "flac"):
|
||||||
|
sampleRate = 44100
|
||||||
|
elif explicitFormat == "wav":
|
||||||
|
sampleRate = 16000
|
||||||
|
else:
|
||||||
|
sampleRate = 16000
|
||||||
|
audioFormat = explicitFormat
|
||||||
|
logger.info(f"STT explicit format: {audioFormat}, {sampleRate}Hz, {channels}ch")
|
||||||
|
elif sampleRate is None or channels is None:
|
||||||
validation = self.validateAudioFormat(audioContent)
|
validation = self.validateAudioFormat(audioContent)
|
||||||
if not validation["valid"]:
|
if not validation["valid"]:
|
||||||
return {
|
return {
|
||||||
|
|
@ -156,12 +202,7 @@ class ConnectorGoogleSpeech:
|
||||||
"encoding": encoding,
|
"encoding": encoding,
|
||||||
"audio_channel_count": channels,
|
"audio_channel_count": channels,
|
||||||
"language_code": language,
|
"language_code": language,
|
||||||
"enable_automatic_punctuation": True,
|
**_buildPrimarySttRecognitionFields(model=model, lightweight=lightweight),
|
||||||
"model": "latest_long",
|
|
||||||
"enable_word_time_offsets": True,
|
|
||||||
"enable_word_confidence": True,
|
|
||||||
"max_alternatives": 3,
|
|
||||||
"use_enhanced": True,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if phraseHints:
|
if phraseHints:
|
||||||
|
|
@ -205,8 +246,7 @@ class ConnectorGoogleSpeech:
|
||||||
sample_rate_hertz=16000,
|
sample_rate_hertz=16000,
|
||||||
audio_channel_count=1,
|
audio_channel_count=1,
|
||||||
language_code=language,
|
language_code=language,
|
||||||
enable_automatic_punctuation=True,
|
**_buildPrimarySttRecognitionFields(model=model, lightweight=lightweight),
|
||||||
model="latest_long"
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
response = await asyncio.to_thread(
|
response = await asyncio.to_thread(
|
||||||
|
|
@ -343,7 +383,7 @@ class ConnectorGoogleSpeech:
|
||||||
"error": "No recognition results (silence or unclear audio)"
|
"error": "No recognition results (silence or unclear audio)"
|
||||||
}
|
}
|
||||||
|
|
||||||
models = ["latest_long", "phone_call", "latest_short"]
|
models = list(dict.fromkeys([model, "latest_long", "phone_call", "latest_short"]))
|
||||||
|
|
||||||
for fallback_config in fallback_configs:
|
for fallback_config in fallback_configs:
|
||||||
for model in models:
|
for model in models:
|
||||||
|
|
@ -419,6 +459,9 @@ class ConnectorGoogleSpeech:
|
||||||
audioQueue: asyncio.Queue,
|
audioQueue: asyncio.Queue,
|
||||||
language: str = "de-DE",
|
language: str = "de-DE",
|
||||||
phraseHints: Optional[list] = None,
|
phraseHints: Optional[list] = None,
|
||||||
|
model: str = "latest_long",
|
||||||
|
lightweight: bool = False,
|
||||||
|
singleUtterance: bool = False,
|
||||||
) -> AsyncGenerator[Dict[str, Any], None]:
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
||||||
"""
|
"""
|
||||||
Stream audio chunks to Google Cloud Speech-to-Text Streaming API.
|
Stream audio chunks to Google Cloud Speech-to-Text Streaming API.
|
||||||
|
|
@ -429,9 +472,13 @@ class ConnectorGoogleSpeech:
|
||||||
Send (b"", True) to signal end of stream.
|
Send (b"", True) to signal end of stream.
|
||||||
language: Language code
|
language: Language code
|
||||||
phraseHints: Optional boost phrases
|
phraseHints: Optional boost phrases
|
||||||
|
model: Google recognition model (e.g. latest_long, latest_short)
|
||||||
|
lightweight: If True, use non-enhanced primary config (lower latency)
|
||||||
|
singleUtterance: If True, end stream after first utterance (client should reconnect)
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
Dicts with keys: isFinal, transcript, confidence, stabilityScore, audioDurationSec
|
Dicts with keys: isFinal, transcript, confidence, stabilityScore, audioDurationSec;
|
||||||
|
optionally endOfSingleUtterance, reconnectRequired
|
||||||
"""
|
"""
|
||||||
STREAM_LIMIT_SEC = 290
|
STREAM_LIMIT_SEC = 290
|
||||||
streamStartTs = time.time()
|
streamStartTs = time.time()
|
||||||
|
|
@ -442,9 +489,7 @@ class ConnectorGoogleSpeech:
|
||||||
"sample_rate_hertz": 48000,
|
"sample_rate_hertz": 48000,
|
||||||
"audio_channel_count": 1,
|
"audio_channel_count": 1,
|
||||||
"language_code": language,
|
"language_code": language,
|
||||||
"enable_automatic_punctuation": True,
|
**_buildPrimarySttRecognitionFields(model=model, lightweight=lightweight),
|
||||||
"model": "latest_long",
|
|
||||||
"use_enhanced": True,
|
|
||||||
}
|
}
|
||||||
if phraseHints:
|
if phraseHints:
|
||||||
configParams["speech_contexts"] = [speech.SpeechContext(phrases=phraseHints, boost=15.0)]
|
configParams["speech_contexts"] = [speech.SpeechContext(phrases=phraseHints, boost=15.0)]
|
||||||
|
|
@ -453,7 +498,7 @@ class ConnectorGoogleSpeech:
|
||||||
streamingConfig = speech.StreamingRecognitionConfig(
|
streamingConfig = speech.StreamingRecognitionConfig(
|
||||||
config=recognitionConfig,
|
config=recognitionConfig,
|
||||||
interim_results=True,
|
interim_results=True,
|
||||||
single_utterance=False,
|
single_utterance=singleUtterance,
|
||||||
)
|
)
|
||||||
|
|
||||||
import queue as threadQueue
|
import queue as threadQueue
|
||||||
|
|
@ -490,7 +535,22 @@ class ConnectorGoogleSpeech:
|
||||||
)
|
)
|
||||||
for response in responseStream:
|
for response in responseStream:
|
||||||
elapsed = time.time() - streamStartTs
|
elapsed = time.time() - streamStartTs
|
||||||
estimatedDurationSec = totalAudioBytes / (48000 * 1 * 2) if totalAudioBytes else 0
|
|
||||||
|
durationFromResults = 0.0
|
||||||
|
for result in response.results:
|
||||||
|
rt = getattr(result, "result_end_time", None)
|
||||||
|
if rt is None:
|
||||||
|
continue
|
||||||
|
if hasattr(rt, "total_seconds"):
|
||||||
|
durationFromResults = max(durationFromResults, float(rt.total_seconds()))
|
||||||
|
else:
|
||||||
|
durationFromResults = max(
|
||||||
|
durationFromResults,
|
||||||
|
float(getattr(rt, "seconds", 0)) + float(getattr(rt, "nanos", 0)) * 1e-9,
|
||||||
|
)
|
||||||
|
estimatedDurationSec = durationFromResults if durationFromResults > 0 else (
|
||||||
|
totalAudioBytes / (48000 * 1 * 2) if totalAudioBytes else 0.0
|
||||||
|
)
|
||||||
|
|
||||||
finalTexts = []
|
finalTexts = []
|
||||||
interimTexts = []
|
interimTexts = []
|
||||||
|
|
@ -524,6 +584,13 @@ class ConnectorGoogleSpeech:
|
||||||
"stabilityScore": 0.0,
|
"stabilityScore": 0.0,
|
||||||
"audioDurationSec": estimatedDurationSec,
|
"audioDurationSec": estimatedDurationSec,
|
||||||
}), loop)
|
}), loop)
|
||||||
|
|
||||||
|
speechEvt = getattr(response, "speech_event_type", None)
|
||||||
|
if speechEvt and "END_OF_SINGLE_UTTERANCE" in str(speechEvt):
|
||||||
|
asyncio.run_coroutine_threadsafe(resultOutQ.put({
|
||||||
|
"endOfSingleUtterance": True,
|
||||||
|
"audioDurationSec": estimatedDurationSec,
|
||||||
|
}), loop)
|
||||||
if elapsed >= STREAM_LIMIT_SEC:
|
if elapsed >= STREAM_LIMIT_SEC:
|
||||||
logger.info("Streaming STT approaching 5-min limit, client should reconnect")
|
logger.info("Streaming STT approaching 5-min limit, client should reconnect")
|
||||||
asyncio.run_coroutine_threadsafe(resultOutQ.put({
|
asyncio.run_coroutine_threadsafe(resultOutQ.put({
|
||||||
|
|
|
||||||
|
|
@ -1080,6 +1080,8 @@ class CommcoachService:
|
||||||
audioContent=audioContent,
|
audioContent=audioContent,
|
||||||
language=language,
|
language=language,
|
||||||
skipFallbacks=True,
|
skipFallbacks=True,
|
||||||
|
model="latest_short",
|
||||||
|
lightweight=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
transcribedText = ""
|
transcribedText = ""
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,14 @@ class TeamsbotMeetingModule(PowerOnModel):
|
||||||
defaultDirectorPrompts: Optional[str] = Field(default=None, description="JSON list of default director prompts")
|
defaultDirectorPrompts: Optional[str] = Field(default=None, description="JSON list of default director prompts")
|
||||||
goals: Optional[str] = Field(default=None, description="Free-text goals")
|
goals: Optional[str] = Field(default=None, description="Free-text goals")
|
||||||
kpiTargets: Optional[str] = Field(default=None, description="JSON object with structured KPI targets")
|
kpiTargets: Optional[str] = Field(default=None, description="JSON object with structured KPI targets")
|
||||||
|
defaultMeetingLink: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Default Teams meeting URL for new sessions in this module (user can override)",
|
||||||
|
)
|
||||||
|
defaultBotName: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Default display name for the bot when starting a session from this module",
|
||||||
|
)
|
||||||
status: TeamsbotModuleStatus = Field(default=TeamsbotModuleStatus.ACTIVE)
|
status: TeamsbotModuleStatus = Field(default=TeamsbotModuleStatus.ACTIVE)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -257,6 +265,7 @@ class TeamsbotStartSessionRequest(BaseModel):
|
||||||
"""Request to start a new Teams Bot session."""
|
"""Request to start a new Teams Bot session."""
|
||||||
meetingLink: str = Field(description="Teams meeting join link (e.g., https://teams.microsoft.com/l/meetup-join/...)")
|
meetingLink: str = Field(description="Teams meeting join link (e.g., https://teams.microsoft.com/l/meetup-join/...)")
|
||||||
botName: Optional[str] = Field(default=None, description="Override bot name for this session")
|
botName: Optional[str] = Field(default=None, description="Override bot name for this session")
|
||||||
|
moduleId: Optional[str] = Field(default=None, description="Optional MeetingModule to attach this session to")
|
||||||
connectionId: Optional[str] = Field(default=None, description="Microsoft connection ID for Graph API access")
|
connectionId: Optional[str] = Field(default=None, description="Microsoft connection ID for Graph API access")
|
||||||
joinMode: Optional[TeamsbotJoinMode] = Field(default=None, description="How the bot joins: systemBot, anonymous, or userAccount. Defaults to systemBot if credentials configured, else anonymous.")
|
joinMode: Optional[TeamsbotJoinMode] = Field(default=None, description="How the bot joins: systemBot, anonymous, or userAccount. Defaults to systemBot if credentials configured, else anonymous.")
|
||||||
sessionContext: Optional[str] = Field(default=None, description="Custom context/knowledge to provide to the bot for this session (e.g. meeting agenda, documents, background info)")
|
sessionContext: Optional[str] = Field(default=None, description="Custom context/knowledge to provide to the bot for this session (e.g. meeting agenda, documents, background info)")
|
||||||
|
|
@ -277,6 +286,8 @@ class CreateMeetingModuleRequest(BaseModel):
|
||||||
defaultDirectorPrompts: Optional[str] = None
|
defaultDirectorPrompts: Optional[str] = None
|
||||||
goals: Optional[str] = None
|
goals: Optional[str] = None
|
||||||
kpiTargets: Optional[str] = None
|
kpiTargets: Optional[str] = None
|
||||||
|
defaultMeetingLink: Optional[str] = None
|
||||||
|
defaultBotName: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateMeetingModuleRequest(BaseModel):
|
class UpdateMeetingModuleRequest(BaseModel):
|
||||||
|
|
@ -287,6 +298,8 @@ class UpdateMeetingModuleRequest(BaseModel):
|
||||||
defaultDirectorPrompts: Optional[str] = None
|
defaultDirectorPrompts: Optional[str] = None
|
||||||
goals: Optional[str] = None
|
goals: Optional[str] = None
|
||||||
kpiTargets: Optional[str] = None
|
kpiTargets: Optional[str] = None
|
||||||
|
defaultMeetingLink: Optional[str] = None
|
||||||
|
defaultBotName: Optional[str] = None
|
||||||
status: Optional[TeamsbotModuleStatus] = None
|
status: Optional[TeamsbotModuleStatus] = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,19 @@ def _runMigrations():
|
||||||
|
|
||||||
migrated = False
|
migrated = False
|
||||||
|
|
||||||
|
# M2: MeetingModule default meeting link / bot name (additive columns)
|
||||||
|
if _tableExists("TeamsbotMeetingModule"):
|
||||||
|
for col, sqlType in (
|
||||||
|
("defaultMeetingLink", "TEXT"),
|
||||||
|
("defaultBotName", "TEXT"),
|
||||||
|
):
|
||||||
|
if not _columnExists("TeamsbotMeetingModule", col):
|
||||||
|
cur.execute(
|
||||||
|
f'ALTER TABLE "TeamsbotMeetingModule" ADD COLUMN "{col}" {sqlType} NULL',
|
||||||
|
)
|
||||||
|
logger.info(f"Migration M2: Added TeamsbotMeetingModule.{col}")
|
||||||
|
migrated = True
|
||||||
|
|
||||||
# M1: Create default Adhoc modules for orphaned sessions
|
# M1: Create default Adhoc modules for orphaned sessions
|
||||||
# (only runs if TeamsbotSession table exists with moduleId column
|
# (only runs if TeamsbotSession table exists with moduleId column
|
||||||
# and there are sessions without a moduleId)
|
# and there are sessions without a moduleId)
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,11 @@ async def startSession(
|
||||||
mandateId = _validateInstanceAccess(instanceId, context)
|
mandateId = _validateInstanceAccess(instanceId, context)
|
||||||
interface = _getInterface(context, instanceId)
|
interface = _getInterface(context, instanceId)
|
||||||
config = _getInstanceConfig(instanceId)
|
config = _getInstanceConfig(instanceId)
|
||||||
|
|
||||||
|
if body.moduleId:
|
||||||
|
mod = interface.getModule(body.moduleId)
|
||||||
|
if not mod or str(mod.get("instanceId") or "") != str(instanceId):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid moduleId for this instance")
|
||||||
|
|
||||||
# Extract and validate meeting URL from user input (handles SafeLinks, invitation text, etc.)
|
# Extract and validate meeting URL from user input (handles SafeLinks, invitation text, etc.)
|
||||||
cleanMeetingUrl = _extractTeamsMeetingUrl(body.meetingLink)
|
cleanMeetingUrl = _extractTeamsMeetingUrl(body.meetingLink)
|
||||||
|
|
@ -288,6 +293,7 @@ async def startSession(
|
||||||
sessionData = TeamsbotSession(
|
sessionData = TeamsbotSession(
|
||||||
instanceId=instanceId,
|
instanceId=instanceId,
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
|
moduleId=body.moduleId,
|
||||||
meetingLink=cleanMeetingUrl,
|
meetingLink=cleanMeetingUrl,
|
||||||
botName=body.botName or config.botName,
|
botName=body.botName or config.botName,
|
||||||
sessionContext=body.sessionContext,
|
sessionContext=body.sessionContext,
|
||||||
|
|
@ -426,6 +432,54 @@ async def listSessions(
|
||||||
return {"sessions": sessions}
|
return {"sessions": sessions}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{instanceId}/dashboard/stream")
|
||||||
|
@limiter.limit("60/minute")
|
||||||
|
async def streamDashboard(
|
||||||
|
request: Request,
|
||||||
|
instanceId: str,
|
||||||
|
context: RequestContext = Depends(getRequestContext),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
SSE channel for the Teamsbot dashboard: repeated snapshots of sessions and meeting modules.
|
||||||
|
Push interval: 3s while any own session is pending/joining/active, otherwise 20s.
|
||||||
|
Same session visibility rules as GET /sessions (own sessions unless platform admin).
|
||||||
|
"""
|
||||||
|
_validateInstanceAccess(instanceId, context)
|
||||||
|
interface = _getInterface(context, instanceId)
|
||||||
|
userId = None if context.isPlatformAdmin else str(context.user.id)
|
||||||
|
activeStatuses = {
|
||||||
|
TeamsbotSessionStatus.PENDING.value,
|
||||||
|
TeamsbotSessionStatus.JOINING.value,
|
||||||
|
TeamsbotSessionStatus.ACTIVE.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def eventGenerator():
|
||||||
|
while True:
|
||||||
|
sessionRows = []
|
||||||
|
try:
|
||||||
|
sessionRows = interface.getSessions(instanceId, includeEnded=True, userId=userId)
|
||||||
|
moduleRows = interface.getModules(instanceId)
|
||||||
|
payload = {"type": "dashboardState", "sessions": sessionRows, "modules": moduleRows}
|
||||||
|
yield f"data: {json.dumps(payload, default=str)}\n\n"
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning("dashboard stream tick failed: %s", ex)
|
||||||
|
yield f"data: {json.dumps({'type': 'error', 'message': 'dashboard_tick_failed'})}\n\n"
|
||||||
|
hasActive = any((s.get("status") in activeStatuses) for s in sessionRows)
|
||||||
|
await asyncio.sleep(3.0 if hasActive else 20.0)
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
eventGenerator(),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"X-Accel-Buffering": "no",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{instanceId}/sessions/{sessionId}")
|
@router.get("/{instanceId}/sessions/{sessionId}")
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def getSession(
|
async def getSession(
|
||||||
|
|
|
||||||
|
|
@ -1225,6 +1225,7 @@ class TeamsbotService:
|
||||||
skipFallbacks=True,
|
skipFallbacks=True,
|
||||||
phraseHints=phraseHints if phraseHints else None,
|
phraseHints=phraseHints if phraseHints else None,
|
||||||
alternativeLanguages=["en-US"],
|
alternativeLanguages=["en-US"],
|
||||||
|
audioFormat="linear16",
|
||||||
)
|
)
|
||||||
|
|
||||||
if sttResult and sttResult.get("success") and sttResult.get("text"):
|
if sttResult and sttResult.get("success") and sttResult.get("text"):
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,10 @@ class VoiceObjects:
|
||||||
sampleRate: int = None, channels: int = None,
|
sampleRate: int = None, channels: int = None,
|
||||||
skipFallbacks: bool = False,
|
skipFallbacks: bool = False,
|
||||||
phraseHints: list = None,
|
phraseHints: list = None,
|
||||||
alternativeLanguages: list = None) -> Dict[str, Any]:
|
alternativeLanguages: list = None,
|
||||||
|
model: str = "latest_long",
|
||||||
|
lightweight: bool = False,
|
||||||
|
audioFormat: Optional[str] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Convert speech to text using Google Cloud Speech-to-Text API.
|
Convert speech to text using Google Cloud Speech-to-Text API.
|
||||||
|
|
||||||
|
|
@ -81,6 +84,9 @@ class VoiceObjects:
|
||||||
skipFallbacks: If True, skip fallback attempts (use when audio format is known)
|
skipFallbacks: If True, skip fallback attempts (use when audio format is known)
|
||||||
phraseHints: Optional list of phrases to boost recognition (names, terms)
|
phraseHints: Optional list of phrases to boost recognition (names, terms)
|
||||||
alternativeLanguages: Optional list of additional language codes for multi-language
|
alternativeLanguages: Optional list of additional language codes for multi-language
|
||||||
|
model: Google STT model (e.g. latest_long, latest_short)
|
||||||
|
lightweight: If True, omit word-level features and enhanced model
|
||||||
|
audioFormat: If set (webm_opus, linear16, ...), skip format auto-detection
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict containing transcribed text, confidence, and metadata
|
Dict containing transcribed text, confidence, and metadata
|
||||||
|
|
@ -97,6 +103,9 @@ class VoiceObjects:
|
||||||
skipFallbacks=skipFallbacks,
|
skipFallbacks=skipFallbacks,
|
||||||
phraseHints=phraseHints,
|
phraseHints=phraseHints,
|
||||||
alternativeLanguages=alternativeLanguages,
|
alternativeLanguages=alternativeLanguages,
|
||||||
|
model=model,
|
||||||
|
lightweight=lightweight,
|
||||||
|
audioFormat=audioFormat,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
|
@ -120,13 +129,23 @@ class VoiceObjects:
|
||||||
audioQueue: asyncio.Queue,
|
audioQueue: asyncio.Queue,
|
||||||
language: str = "de-DE",
|
language: str = "de-DE",
|
||||||
phraseHints: Optional[list] = None,
|
phraseHints: Optional[list] = None,
|
||||||
|
model: str = "latest_long",
|
||||||
|
lightweight: bool = False,
|
||||||
|
singleUtterance: bool = False,
|
||||||
) -> AsyncGenerator[Dict[str, Any], None]:
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
||||||
"""
|
"""
|
||||||
Stream audio to Google Streaming STT and yield interim/final results.
|
Stream audio to Google Streaming STT and yield interim/final results.
|
||||||
Billing is recorded for each final result.
|
Billing is recorded for each final result.
|
||||||
"""
|
"""
|
||||||
connector = self._getGoogleSpeechConnector()
|
connector = self._getGoogleSpeechConnector()
|
||||||
async for event in connector.streamingRecognize(audioQueue, language, phraseHints):
|
async for event in connector.streamingRecognize(
|
||||||
|
audioQueue,
|
||||||
|
language,
|
||||||
|
phraseHints,
|
||||||
|
model=model,
|
||||||
|
lightweight=lightweight,
|
||||||
|
singleUtterance=singleUtterance,
|
||||||
|
):
|
||||||
if event.get("isFinal") and self.billingCallback:
|
if event.get("isFinal") and self.billingCallback:
|
||||||
durationSec = event.get("audioDurationSec", 0)
|
durationSec = event.get("audioDurationSec", 0)
|
||||||
priceCHF = connector.calculateSttCostCHF(durationSec)
|
priceCHF = connector.calculateSttCostCHF(durationSec)
|
||||||
|
|
|
||||||
|
|
@ -155,12 +155,13 @@ async def sttStream(
|
||||||
|
|
||||||
Protocol:
|
Protocol:
|
||||||
Client sends JSON:
|
Client sends JSON:
|
||||||
{"type": "open", "language": "de-DE"}
|
{"type": "open", "language": "de-DE", "model": "latest_short", "lightweight": true, "singleUtterance": true}
|
||||||
{"type": "audio", "chunk": "<base64>"}
|
{"type": "audio", "chunk": "<base64>"}
|
||||||
{"type": "close"}
|
{"type": "close"}
|
||||||
Server sends JSON:
|
Server sends JSON:
|
||||||
{"type": "interim", "text": "..."}
|
{"type": "interim", "text": "..."}
|
||||||
{"type": "final", "text": "...", "confidence": 0.95}
|
{"type": "final", "text": "...", "confidence": 0.95}
|
||||||
|
{"type": "end_of_single_utterance", "audioDurationSec": 0.0}
|
||||||
{"type": "error", "message": "..."}
|
{"type": "error", "message": "..."}
|
||||||
{"type": "closed"}
|
{"type": "closed"}
|
||||||
"""
|
"""
|
||||||
|
|
@ -205,7 +206,12 @@ async def sttStream(
|
||||||
logger.warning(f"STT billing pre-flight skipped: {e}")
|
logger.warning(f"STT billing pre-flight skipped: {e}")
|
||||||
|
|
||||||
audioQueue: asyncio.Queue = asyncio.Queue()
|
audioQueue: asyncio.Queue = asyncio.Queue()
|
||||||
language = "de-DE"
|
sttOpenOptions: Dict[str, Any] = {
|
||||||
|
"language": "de-DE",
|
||||||
|
"model": "latest_long",
|
||||||
|
"lightweight": False,
|
||||||
|
"singleUtterance": False,
|
||||||
|
}
|
||||||
streamingTask: Optional[asyncio.Task] = None
|
streamingTask: Optional[asyncio.Task] = None
|
||||||
voiceInterface: Optional[VoiceObjects] = None
|
voiceInterface: Optional[VoiceObjects] = None
|
||||||
|
|
||||||
|
|
@ -233,10 +239,23 @@ async def sttStream(
|
||||||
voiceInterface.billingCallback = _billingCb
|
voiceInterface.billingCallback = _billingCb
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for event in voiceInterface.streamingSpeechToText(audioQueue, language):
|
async for event in voiceInterface.streamingSpeechToText(
|
||||||
|
audioQueue,
|
||||||
|
sttOpenOptions["language"],
|
||||||
|
phraseHints=None,
|
||||||
|
model=sttOpenOptions["model"],
|
||||||
|
lightweight=sttOpenOptions["lightweight"],
|
||||||
|
singleUtterance=sttOpenOptions["singleUtterance"],
|
||||||
|
):
|
||||||
if event.get("reconnectRequired"):
|
if event.get("reconnectRequired"):
|
||||||
await _sendJson({"type": "reconnect_required"})
|
await _sendJson({"type": "reconnect_required"})
|
||||||
return
|
return
|
||||||
|
if event.get("endOfSingleUtterance"):
|
||||||
|
await _sendJson({
|
||||||
|
"type": "end_of_single_utterance",
|
||||||
|
"audioDurationSec": event.get("audioDurationSec", 0.0),
|
||||||
|
})
|
||||||
|
continue
|
||||||
if event.get("isFinal"):
|
if event.get("isFinal"):
|
||||||
if event.get("transcript"):
|
if event.get("transcript"):
|
||||||
await _sendJson({"type": "final", "text": event["transcript"], "confidence": event.get("confidence", 0.0)})
|
await _sendJson({"type": "final", "text": event["transcript"], "confidence": event.get("confidence", 0.0)})
|
||||||
|
|
@ -258,7 +277,10 @@ async def sttStream(
|
||||||
msgType = (msg.get("type") or "").strip()
|
msgType = (msg.get("type") or "").strip()
|
||||||
|
|
||||||
if msgType == "open":
|
if msgType == "open":
|
||||||
language = msg.get("language") or "de-DE"
|
sttOpenOptions["language"] = msg.get("language") or "de-DE"
|
||||||
|
sttOpenOptions["model"] = msg.get("model") or "latest_long"
|
||||||
|
sttOpenOptions["lightweight"] = bool(msg.get("lightweight"))
|
||||||
|
sttOpenOptions["singleUtterance"] = bool(msg.get("singleUtterance"))
|
||||||
if streamingTask and not streamingTask.done():
|
if streamingTask and not streamingTask.done():
|
||||||
await audioQueue.put((b"", True))
|
await audioQueue.put((b"", True))
|
||||||
streamingTask.cancel()
|
streamingTask.cancel()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Copyright (c) 2025 Patrick Motsch
|
||||||
|
"""Unit tests for Google STT helper config (no API calls)."""
|
||||||
|
|
||||||
|
from modules.connectors.connectorVoiceGoogle import _buildPrimarySttRecognitionFields
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildPrimaryStt_lightweight_stripsHeavyFeatures():
|
||||||
|
d = _buildPrimarySttRecognitionFields(model="latest_short", lightweight=True)
|
||||||
|
assert d["model"] == "latest_short"
|
||||||
|
assert d["enable_word_time_offsets"] is False
|
||||||
|
assert d["enable_word_confidence"] is False
|
||||||
|
assert d["max_alternatives"] == 1
|
||||||
|
assert d["use_enhanced"] is False
|
||||||
|
assert d["enable_automatic_punctuation"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_buildPrimaryStt_full_matchesLegacyDefaults():
|
||||||
|
d = _buildPrimarySttRecognitionFields(model="latest_long", lightweight=False)
|
||||||
|
assert d["model"] == "latest_long"
|
||||||
|
assert d["enable_word_time_offsets"] is True
|
||||||
|
assert d["enable_word_confidence"] is True
|
||||||
|
assert d["max_alternatives"] == 3
|
||||||
|
assert d["use_enhanced"] is True
|
||||||
Loading…
Reference in a new issue