🩹 fix(bot-services): export LOGIN_MESSAGES and auth error helpers

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 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-14 11:19:55 +01:00
parent ff1affb268
commit 4a26926fae
5 changed files with 244 additions and 12 deletions

View file

@ -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,

View file

@ -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';

View file

@ -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;

View file

@ -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**

View file

@ -13,6 +13,73 @@ export interface VoicesResponse {
custom_voices: VoiceInfo[];
}
// German voice mapping
const GERMAN_VOICES: Record<string, string> = {
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<Buffer> {
// 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<Buffer> {
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<string, string> = { '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<Buffer> {
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<string, string> = { '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
*/