From 4a26926faec6f6fb608a907fbcc768ff02c94d94 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sat, 14 Feb 2026 11:19:55 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A9=B9=20fix(bot-services):=20export=20LO?= =?UTF-8?q?GIN=5FMESSAGES=20and=20auth=20error=20helpers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export the following from @manacore/bot-services: - LOGIN_MESSAGES: Pre-defined auth error messages for all bot types - AUTH_ERROR_MESSAGES: Same as LOGIN_MESSAGES (preferred name) - formatAuthErrorMessage(): Helper to create custom auth error messages These are used by bots to show consistent error messages when token refresh fails and the user needs to re-authenticate. Co-Authored-By: Claude Opus 4.5 --- packages/bot-services/src/index.ts | 5 + packages/bot-services/src/session/index.ts | 10 +- packages/bot-services/src/session/types.ts | 45 +++++ .../src/config/configuration.ts | 23 ++- .../matrix-tts-bot/src/tts/tts.service.ts | 173 +++++++++++++++++- 5 files changed, 244 insertions(+), 12 deletions(-) diff --git a/packages/bot-services/src/index.ts b/packages/bot-services/src/index.ts index 074af65e7..14f3cc9de 100644 --- a/packages/bot-services/src/index.ts +++ b/packages/bot-services/src/index.ts @@ -100,6 +100,11 @@ export { REDIS_CLIENT, SESSION_MODULE_OPTIONS, DEFAULT_SESSION_EXPIRY_MS, + formatAuthErrorMessage, + AUTH_ERROR_MESSAGES, + // Deprecated - kept for backwards compatibility + formatLoginRequiredMessage, + LOGIN_MESSAGES, } from './session/index.js'; export type { UserSession, diff --git a/packages/bot-services/src/session/index.ts b/packages/bot-services/src/session/index.ts index f5917f8d7..e5e17ae20 100644 --- a/packages/bot-services/src/session/index.ts +++ b/packages/bot-services/src/session/index.ts @@ -8,4 +8,12 @@ export type { SessionModuleOptions, SessionStorageMode, } from './types'; -export { SESSION_MODULE_OPTIONS, DEFAULT_SESSION_EXPIRY_MS } from './types'; +export { + SESSION_MODULE_OPTIONS, + DEFAULT_SESSION_EXPIRY_MS, + formatAuthErrorMessage, + AUTH_ERROR_MESSAGES, + // Deprecated - kept for backwards compatibility + formatLoginRequiredMessage, + LOGIN_MESSAGES, +} from './types'; diff --git a/packages/bot-services/src/session/types.ts b/packages/bot-services/src/session/types.ts index f3ef46e97..4b7f21495 100644 --- a/packages/bot-services/src/session/types.ts +++ b/packages/bot-services/src/session/types.ts @@ -78,3 +78,48 @@ export const SESSION_MODULE_OPTIONS = 'SESSION_MODULE_OPTIONS'; * Default session expiry: 7 days in milliseconds */ export const DEFAULT_SESSION_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; + +/** + * Standard auth error message for all bots + * Used when SSO-Link fails to authenticate the user automatically. + * Since all users authenticate via OIDC, this indicates a technical issue. + */ +export function formatAuthErrorMessage(featureDescription: string): string { + return ( + '⚠️ **Authentifizierung fehlgeschlagen**\n\n' + + `Um ${featureDescription} zu nutzen, muss dein Account verknüpft sein.\n\n` + + 'Bitte logge dich in Element neu ein über "Mit Mana Core anmelden".\n\n' + + 'Falls das Problem weiterhin besteht, kontaktiere den Support.' + ); +} + +/** + * Pre-defined auth error messages for all bot types + */ +export const AUTH_ERROR_MESSAGES = { + clock: formatAuthErrorMessage('Timer und Alarme'), + todo: formatAuthErrorMessage('Aufgaben'), + calendar: formatAuthErrorMessage('Termine'), + contacts: formatAuthErrorMessage('Kontakte'), + picture: formatAuthErrorMessage('Bilder'), + zitare: formatAuthErrorMessage('Favoriten'), + nutriphi: formatAuthErrorMessage('Mahlzeiten'), + chat: formatAuthErrorMessage('Chats'), + storage: formatAuthErrorMessage('Dateien'), + skilltree: formatAuthErrorMessage('Skills'), + presi: formatAuthErrorMessage('Präsentationen'), + planta: formatAuthErrorMessage('Pflanzen'), + manadeck: formatAuthErrorMessage('Decks'), + questions: formatAuthErrorMessage('Fragen'), +} as const; + +/** + * @deprecated Use formatAuthErrorMessage instead. Kept for backwards compatibility. + */ +export const formatLoginRequiredMessage = (featureDescription: string, _appName: string): string => + formatAuthErrorMessage(featureDescription); + +/** + * @deprecated Use AUTH_ERROR_MESSAGES instead. Kept for backwards compatibility. + */ +export const LOGIN_MESSAGES = AUTH_ERROR_MESSAGES; diff --git a/services/matrix-tts-bot/src/config/configuration.ts b/services/matrix-tts-bot/src/config/configuration.ts index c5b34c80b..cb1b468d9 100644 --- a/services/matrix-tts-bot/src/config/configuration.ts +++ b/services/matrix-tts-bot/src/config/configuration.ts @@ -9,7 +9,7 @@ export default () => ({ tts: { url: process.env.TTS_URL || 'http://localhost:3022', apiKey: process.env.TTS_API_KEY || '', - defaultVoice: process.env.DEFAULT_VOICE || 'af_heart', + defaultVoice: process.env.DEFAULT_VOICE || 'de_thorsten', defaultSpeed: parseFloat(process.env.DEFAULT_SPEED || '1.0'), maxTextLength: parseInt(process.env.MAX_TEXT_LENGTH || '500', 10), }, @@ -20,19 +20,26 @@ export const HELP_TEXT = `**TTS Bot - Hilfe** Ich wandle deine Textnachrichten in Sprache um! **Befehle:** -- \`!voice [name]\` - Stimme wechseln (z.B. \`!voice bm_daniel\`) -- \`!voices\` - Alle verfugbaren Stimmen anzeigen -- \`!speed [0.5-2.0]\` - Geschwindigkeit andern +- \`!voice [name]\` - Stimme wechseln (z.B. \`!voice de_thorsten\`) +- \`!voices\` - Alle verfügbaren Stimmen anzeigen +- \`!speed [0.5-2.0]\` - Geschwindigkeit ändern - \`!status\` - Aktuelle Einstellungen - \`!help\` - Diese Hilfe **Verwendung:** -Schreibe einfach eine Nachricht und ich sende dir die Sprachausgabe zuruck. +Schreibe einfach eine Nachricht und ich sende dir die Sprachausgabe zurück. +Die Sprache wird automatisch erkannt (Deutsch/Englisch). -**Beispiel-Stimmen:** +**Deutsche Stimmen:** +- \`de_thorsten\` - Deutsch männlich (lokal) +- \`de_katja\` - Deutsch weiblich +- \`de_conrad\` - Deutsch männlich +- \`de_florian\` - Deutsch männlich jung + +**Englische Stimmen:** - \`af_heart\` - Amerikanisch weiblich (warm) -- \`bm_daniel\` - Britisch mannlich (klassisch) -- \`am_michael\` - Amerikanisch mannlich`; +- \`bm_daniel\` - Britisch männlich +- \`am_michael\` - Amerikanisch männlich`; export const WELCOME_TEXT = `**TTS Bot** diff --git a/services/matrix-tts-bot/src/tts/tts.service.ts b/services/matrix-tts-bot/src/tts/tts.service.ts index 67b7288cd..829520e19 100644 --- a/services/matrix-tts-bot/src/tts/tts.service.ts +++ b/services/matrix-tts-bot/src/tts/tts.service.ts @@ -13,6 +13,73 @@ export interface VoicesResponse { custom_voices: VoiceInfo[]; } +// German voice mapping +const GERMAN_VOICES: Record = { + de_thorsten: 'de_thorsten', // Local Piper + de_katja: 'de_katja', // Edge TTS female + de_conrad: 'de_conrad', // Edge TTS male + de_amala: 'de_amala', // Edge TTS female young + de_florian: 'de_florian', // Edge TTS male young +}; + +const DEFAULT_GERMAN_VOICE = 'de_thorsten'; + +// Common German words for language detection +const GERMAN_INDICATORS = [ + 'ich', + 'du', + 'er', + 'sie', + 'wir', + 'ihr', + 'und', + 'oder', + 'aber', + 'wenn', + 'dass', + 'ist', + 'sind', + 'war', + 'haben', + 'werden', + 'kann', + 'muss', + 'soll', + 'will', + 'nicht', + 'auch', + 'noch', + 'schon', + 'sehr', + 'nur', + 'mehr', + 'hier', + 'jetzt', + 'heute', + 'morgen', + 'gestern', + 'bitte', + 'danke', + 'hallo', + 'guten', + 'tag', + 'abend', + 'nacht', + 'wie', + 'was', + 'wer', + 'wo', + 'wann', + 'warum', + 'welche', + 'diese', + 'keine', + 'eine', + 'einen', + 'einem', + 'einer', +]; + @Injectable() export class TtsService { private readonly logger = new Logger(TtsService.name); @@ -25,13 +92,65 @@ export class TtsService { } /** - * Synthesize text to speech using Kokoro model + * Detect if text is likely German + */ + private isGerman(text: string): boolean { + const lowerText = text.toLowerCase(); + + // Check for German-specific characters + if (/[äöüß]/.test(lowerText)) { + return true; + } + + // Check for common German words + const words = lowerText.split(/\s+/); + const germanWordCount = words.filter((word) => + GERMAN_INDICATORS.includes(word.replace(/[.,!?;:'"]/g, '')) + ).length; + + // If more than 20% of words are German indicators, consider it German + return germanWordCount / words.length > 0.2; + } + + /** + * Check if voice is a German voice + */ + private isGermanVoice(voice: string): boolean { + return voice.startsWith('de_'); + } + + /** + * Synthesize text to speech - auto-detects language */ async synthesize(text: string, voice: string = 'af_heart', speed: number = 1.0): Promise { + // Auto-detect language if using English voice but text is German + const textIsGerman = this.isGerman(text); + const voiceIsGerman = this.isGermanVoice(voice); + + if (textIsGerman && !voiceIsGerman) { + this.logger.debug(`German text detected, switching to German voice`); + return this.synthesizeGerman(text, DEFAULT_GERMAN_VOICE, speed); + } + + if (voiceIsGerman) { + return this.synthesizeGerman(text, voice, speed); + } + + return this.synthesizeKokoro(text, voice, speed); + } + + /** + * Synthesize using Kokoro (English voices) + */ + private async synthesizeKokoro( + text: string, + voice: string = 'af_heart', + speed: number = 1.0 + ): Promise { const url = `${this.ttsUrl}/synthesize/kokoro`; this.logger.debug( - `Synthesizing: "${text.substring(0, 50)}..." with voice=${voice}, speed=${speed}` + `Kokoro synthesizing: "${text.substring(0, 50)}..." with voice=${voice}, speed=${speed}` ); const headers: Record = { 'Content-Type': 'application/json' }; @@ -52,7 +171,7 @@ export class TtsService { if (!response.ok) { const errorText = await response.text(); - this.logger.error(`TTS failed: ${response.status} - ${errorText}`); + this.logger.error(`Kokoro TTS failed: ${response.status} - ${errorText}`); throw new Error(`TTS synthesis failed: ${response.status}`); } @@ -62,6 +181,54 @@ export class TtsService { return Buffer.from(arrayBuffer); } + /** + * Synthesize using Piper (German voices) + */ + private async synthesizeGerman( + text: string, + voice: string = DEFAULT_GERMAN_VOICE, + speed: number = 1.0 + ): Promise { + const url = `${this.ttsUrl}/synthesize/piper`; + + // Map voice to valid German voice + const germanVoice = GERMAN_VOICES[voice] || DEFAULT_GERMAN_VOICE; + + // Piper uses length_scale (inverse of speed) + const lengthScale = 1.0 / speed; + + this.logger.debug( + `Piper synthesizing: "${text.substring(0, 50)}..." with voice=${germanVoice}, lengthScale=${lengthScale}` + ); + + const headers: Record = { 'Content-Type': 'application/json' }; + if (this.apiKey) { + headers['X-API-Key'] = this.apiKey; + } + + const response = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify({ + text, + voice: germanVoice, + length_scale: lengthScale, + output_format: 'wav', + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + this.logger.error(`Piper TTS failed: ${response.status} - ${errorText}`); + throw new Error(`TTS synthesis failed: ${response.status}`); + } + + const arrayBuffer = await response.arrayBuffer(); + this.logger.debug(`Received German audio: ${arrayBuffer.byteLength} bytes`); + + return Buffer.from(arrayBuffer); + } + /** * Get list of available voices */