mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:21:10 +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,
|
REDIS_CLIENT,
|
||||||
SESSION_MODULE_OPTIONS,
|
SESSION_MODULE_OPTIONS,
|
||||||
DEFAULT_SESSION_EXPIRY_MS,
|
DEFAULT_SESSION_EXPIRY_MS,
|
||||||
|
formatAuthErrorMessage,
|
||||||
|
AUTH_ERROR_MESSAGES,
|
||||||
|
// Deprecated - kept for backwards compatibility
|
||||||
|
formatLoginRequiredMessage,
|
||||||
|
LOGIN_MESSAGES,
|
||||||
} from './session/index.js';
|
} from './session/index.js';
|
||||||
export type {
|
export type {
|
||||||
UserSession,
|
UserSession,
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,12 @@ export type {
|
||||||
SessionModuleOptions,
|
SessionModuleOptions,
|
||||||
SessionStorageMode,
|
SessionStorageMode,
|
||||||
} from './types';
|
} 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
|
* Default session expiry: 7 days in milliseconds
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_SESSION_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
|
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: {
|
tts: {
|
||||||
url: process.env.TTS_URL || 'http://localhost:3022',
|
url: process.env.TTS_URL || 'http://localhost:3022',
|
||||||
apiKey: process.env.TTS_API_KEY || '',
|
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'),
|
defaultSpeed: parseFloat(process.env.DEFAULT_SPEED || '1.0'),
|
||||||
maxTextLength: parseInt(process.env.MAX_TEXT_LENGTH || '500', 10),
|
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!
|
Ich wandle deine Textnachrichten in Sprache um!
|
||||||
|
|
||||||
**Befehle:**
|
**Befehle:**
|
||||||
- \`!voice [name]\` - Stimme wechseln (z.B. \`!voice bm_daniel\`)
|
- \`!voice [name]\` - Stimme wechseln (z.B. \`!voice de_thorsten\`)
|
||||||
- \`!voices\` - Alle verfugbaren Stimmen anzeigen
|
- \`!voices\` - Alle verfügbaren Stimmen anzeigen
|
||||||
- \`!speed [0.5-2.0]\` - Geschwindigkeit andern
|
- \`!speed [0.5-2.0]\` - Geschwindigkeit ändern
|
||||||
- \`!status\` - Aktuelle Einstellungen
|
- \`!status\` - Aktuelle Einstellungen
|
||||||
- \`!help\` - Diese Hilfe
|
- \`!help\` - Diese Hilfe
|
||||||
|
|
||||||
**Verwendung:**
|
**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)
|
- \`af_heart\` - Amerikanisch weiblich (warm)
|
||||||
- \`bm_daniel\` - Britisch mannlich (klassisch)
|
- \`bm_daniel\` - Britisch männlich
|
||||||
- \`am_michael\` - Amerikanisch mannlich`;
|
- \`am_michael\` - Amerikanisch männlich`;
|
||||||
|
|
||||||
export const WELCOME_TEXT = `**TTS Bot**
|
export const WELCOME_TEXT = `**TTS Bot**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,73 @@ export interface VoicesResponse {
|
||||||
custom_voices: VoiceInfo[];
|
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()
|
@Injectable()
|
||||||
export class TtsService {
|
export class TtsService {
|
||||||
private readonly logger = new Logger(TtsService.name);
|
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> {
|
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`;
|
const url = `${this.ttsUrl}/synthesize/kokoro`;
|
||||||
|
|
||||||
this.logger.debug(
|
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' };
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||||
|
|
@ -52,7 +171,7 @@ export class TtsService {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
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}`);
|
throw new Error(`TTS synthesis failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +181,54 @@ export class TtsService {
|
||||||
return Buffer.from(arrayBuffer);
|
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
|
* Get list of available voices
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue