mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:21:09 +02:00
🩹 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:
parent
ff1affb268
commit
4a26926fae
5 changed files with 244 additions and 12 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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**
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue