🩹 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

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