♻️ refactor: migrate 5 bots to KeywordCommandDetector

Migrated to KeywordCommandDetector from @manacore/matrix-bot-common:
- matrix-calendar-bot (termine, kalender keywords)
- matrix-clock-bot (timer, zeit keywords)
- matrix-picture-bot (modelle, verlauf keywords)
- matrix-todo-bot (aufgaben, projekte keywords)
- matrix-zitare-bot (zitat, kategorien keywords)

Removed duplicate KEYWORD_COMMANDS arrays and detectKeywordCommand()
methods from all 5 bots.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-01 03:11:58 +01:00
parent 462ef006f0
commit 867a1a7fb6
9 changed files with 291 additions and 168 deletions

View file

@ -1,27 +1,30 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common';
import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { CalendarService, CalendarEvent } from '../calendar/calendar.service';
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
// Natural language keywords that trigger commands (German + English)
const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [
{ keywords: ['hilfe', 'help', 'was kannst du', 'befehle', 'commands'], command: 'help' },
{
keywords: ['was steht heute an', 'termine heute', 'heute termine', "today's events"],
command: 'today',
},
{ keywords: ['termine morgen', 'morgen termine', 'was ist morgen'], command: 'tomorrow' },
{
keywords: ['diese woche', 'wochenübersicht', 'week', 'woche'],
command: 'week',
},
{ keywords: ['zeige kalender', 'meine kalender', 'calendars'], command: 'calendars' },
{ keywords: ['status', 'verbindung', 'connection'], command: 'status' },
];
@Injectable()
export class MatrixService extends BaseMatrixService {
private readonly keywordDetector = new KeywordCommandDetector(
[
...COMMON_KEYWORDS,
{ keywords: ['was kannst du'], command: 'help' },
{ keywords: ['was steht heute an', 'termine heute', 'heute termine', "today's events"], command: 'today' },
{ keywords: ['termine morgen', 'morgen termine', 'was ist morgen'], command: 'tomorrow' },
{ keywords: ['diese woche', 'wochenübersicht', 'week', 'woche'], command: 'week' },
{ keywords: ['zeige kalender', 'meine kalender', 'calendars'], command: 'calendars' },
{ keywords: ['verbindung', 'connection'], command: 'status' },
],
{ partialMatch: true }
);
constructor(
configService: ConfigService,
private calendarService: CalendarService
@ -56,34 +59,13 @@ export class MatrixService extends BaseMatrixService {
}
// Check for natural language keywords
const keywordCommand = this.detectKeywordCommand(message);
const keywordCommand = this.keywordDetector.detect(message);
if (keywordCommand) {
await this.executeCommand(roomId, event, sender, keywordCommand, '');
return;
}
}
private detectKeywordCommand(message: string): string | null {
const lowerMessage = message.toLowerCase().trim();
// Only check short messages for keywords
if (lowerMessage.length > 60) return null;
for (const { keywords, command } of KEYWORD_COMMANDS) {
for (const keyword of keywords) {
if (
lowerMessage === keyword ||
lowerMessage.startsWith(keyword + ' ') ||
lowerMessage.includes(keyword)
) {
this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`);
return command;
}
}
}
return null;
}
private async executeCommand(
roomId: string,
event: MatrixRoomEvent,

View file

@ -1,24 +1,30 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common';
import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { ClockService } from '../clock/clock.service';
import { TranscriptionService } from '@manacore/bot-services';
import { HELP_TEXT, WELCOME_TEXT } from '../config/configuration';
// Natural language keywords
const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [
{ keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' },
{ keywords: ['status', 'timer status', 'laufend'], command: 'status' },
{ keywords: ['stop', 'stopp', 'pause', 'anhalten'], command: 'stop' },
{ keywords: ['weiter', 'resume', 'fortsetzen'], command: 'resume' },
{ keywords: ['zeit', 'time', 'uhrzeit', 'wie spat'], command: 'time' },
];
@Injectable()
export class MatrixService extends BaseMatrixService {
// Demo token for development (TODO: implement proper auth)
private readonly demoToken = process.env.CLOCK_API_TOKEN || '';
// Note: We override COMMON_KEYWORDS' cancel->cancel with stop->stop for this bot
private readonly keywordDetector = new KeywordCommandDetector([
{ keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' },
{ keywords: ['status', 'timer status', 'laufend'], command: 'status' },
{ keywords: ['stop', 'stopp', 'pause', 'anhalten'], command: 'stop' },
{ keywords: ['weiter', 'resume', 'fortsetzen'], command: 'resume' },
{ keywords: ['zeit', 'time', 'uhrzeit', 'wie spat'], command: 'time' },
]);
constructor(
configService: ConfigService,
private clockService: ClockService,
@ -47,7 +53,7 @@ export class MatrixService extends BaseMatrixService {
sender: string
): Promise<void> {
// Check keywords first
const keywordCommand = this.detectKeywordCommand(message);
const keywordCommand = this.keywordDetector.detect(message);
if (keywordCommand) {
await this.executeCommand(roomId, event, sender, keywordCommand, '');
return;
@ -137,20 +143,6 @@ export class MatrixService extends BaseMatrixService {
}
}
private detectKeywordCommand(message: string): string | null {
const lowerMessage = message.toLowerCase().trim();
if (lowerMessage.length > 50) return null;
for (const { keywords, command } of KEYWORD_COMMANDS) {
for (const keyword of keywords) {
if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) {
return command;
}
}
}
return null;
}
private async executeCommand(
roomId: string,
event: MatrixRoomEvent,

View file

@ -29,3 +29,4 @@ VOICE_BOT_URL=http://localhost:3050
DEFAULT_VOICE=de-DE-ConradNeural
DEFAULT_SPEED=1.0
VOICE_ENABLED=true
VOICE_PREFERENCES_PATH=./data/voice-preferences.json

View file

@ -2,7 +2,7 @@ import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common';
import { CommandRouterService, CommandContext } from './command-router.service';
import { VoiceService } from '../voice/voice.service';
import { VoiceService, VoiceServiceError } from '../voice/voice.service';
import { VoiceFormatterService } from '../voice/voice-formatter.service';
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
@ -180,6 +180,33 @@ export class MatrixService extends BaseMatrixService {
}
} catch (error) {
await this.client.setTyping(roomId, false);
// Handle specific voice service errors
if (error instanceof VoiceServiceError) {
this.logger.warn(`Voice service error (${error.code}): ${error.message}`);
let userMessage: string;
switch (error.code) {
case 'STT_UNAVAILABLE':
userMessage = '🎤 Spracherkennung momentan nicht verfügbar. Bitte schreibe deine Nachricht.';
break;
case 'TTS_UNAVAILABLE':
userMessage = '🔊 Sprachausgabe momentan nicht verfügbar.';
break;
case 'TIMEOUT':
userMessage = '⏱️ Die Verarbeitung dauert zu lange. Bitte versuche eine kürzere Nachricht.';
break;
case 'INVALID_AUDIO':
userMessage = `🎤 ${error.message}`;
break;
default:
userMessage = '❌ Spracherkennung fehlgeschlagen. Bitte versuche es erneut.';
}
await this.sendReply(roomId, event, userMessage);
return;
}
this.logger.error(`Error handling voice message:`, error);
await this.sendReply(
roomId,

View file

@ -1,13 +1,15 @@
import { Injectable } from '@nestjs/common';
import { AiService, TodoService } from '@manacore/bot-services';
import { CommandContext } from '../bot/command-router.service';
import { VoiceService } from '../voice/voice.service';
import { HELP_TEXT } from '../config/configuration';
@Injectable()
export class HelpHandler {
constructor(
private aiService: AiService,
private todoService: TodoService
private todoService: TodoService,
private voiceService: VoiceService
) {}
async showHelp(ctx: CommandContext): Promise<string> {
@ -15,11 +17,22 @@ export class HelpHandler {
}
async showStatus(ctx: CommandContext): Promise<string> {
const aiConnected = await this.aiService.checkConnection();
const todoStats = await this.todoService.getStats(ctx.userId);
// Check services in parallel
const [aiConnected, todoStats, voiceHealth] = await Promise.all([
this.aiService.checkConnection(),
this.todoService.getStats(ctx.userId),
this.voiceService.checkHealth(),
]);
const aiStatus = aiConnected ? '✅ Online' : '❌ Offline';
const currentModel = this.aiService.getSession(ctx.userId)?.model || this.aiService.getDefaultModel();
const currentModel =
this.aiService.getSession(ctx.userId)?.model || this.aiService.getDefaultModel();
const sttStatus = voiceHealth.stt ? '✅ Online' : '❌ Offline';
const ttsStatus = voiceHealth.tts ? '✅ Online' : '❌ Offline';
const voicePrefs = this.voiceService.getUserPreferences(ctx.userId);
const voiceEnabled = voicePrefs.voiceEnabled ? '✅ Aktiviert' : '❌ Deaktiviert';
return `**📊 Status**
@ -27,6 +40,12 @@ export class HelpHandler {
Verbindung: ${aiStatus}
Modell: \`${currentModel}\`
**🎤 Voice**
STT (Whisper): ${sttStatus}
TTS (Edge): ${ttsStatus}
Deine Einstellung: ${voiceEnabled}
Stimme: ${voicePrefs.voice}
**Todos**
Offen: ${todoStats.pending}
Heute fällig: ${todoStats.today}

View file

@ -8,15 +8,45 @@ export interface TranscriptionResult {
duration?: number;
}
export class VoiceServiceError extends Error {
constructor(
message: string,
public readonly code: 'STT_UNAVAILABLE' | 'TTS_UNAVAILABLE' | 'TIMEOUT' | 'INVALID_AUDIO' | 'UNKNOWN'
) {
super(message);
this.name = 'VoiceServiceError';
}
}
// Re-export for convenience
export { VoicePreferences };
// Simple LRU cache for TTS responses
interface CacheEntry {
buffer: Buffer;
timestamp: number;
}
@Injectable()
export class VoiceService {
private readonly logger = new Logger(VoiceService.name);
private readonly sttUrl: string;
private readonly voiceBotUrl: string;
// Timeouts in milliseconds
private readonly STT_TIMEOUT = 60000; // 60s for transcription (can be slow)
private readonly TTS_TIMEOUT = 30000; // 30s for synthesis
private readonly HEALTH_TIMEOUT = 5000; // 5s for health checks
// Audio size limits
private readonly MIN_AUDIO_SIZE = 1000; // 1KB minimum
private readonly MAX_AUDIO_SIZE = 25 * 1024 * 1024; // 25MB maximum
// TTS cache for common short responses
private readonly ttsCache = new Map<string, CacheEntry>();
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
private readonly MAX_CACHE_SIZE = 50;
constructor(
private configService: ConfigService,
private preferencesStore: VoicePreferencesStore
@ -35,6 +65,21 @@ export class VoiceService {
async transcribe(audioBuffer: Buffer, language = 'de'): Promise<TranscriptionResult> {
const startTime = Date.now();
// Validate audio size
if (audioBuffer.length < this.MIN_AUDIO_SIZE) {
throw new VoiceServiceError(
'Audio zu kurz - bitte länger sprechen.',
'INVALID_AUDIO'
);
}
if (audioBuffer.length > this.MAX_AUDIO_SIZE) {
throw new VoiceServiceError(
'Audio zu groß (max 25MB). Bitte kürzere Nachricht senden.',
'INVALID_AUDIO'
);
}
try {
const formData = new FormData();
// Convert Buffer to Uint8Array for Blob compatibility
@ -45,11 +90,15 @@ export class VoiceService {
const response = await fetch(`${this.sttUrl}/transcribe`, {
method: 'POST',
body: formData,
signal: AbortSignal.timeout(this.STT_TIMEOUT),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`STT error: ${response.status} - ${error}`);
throw new VoiceServiceError(
`Spracherkennung fehlgeschlagen: ${response.status}`,
'STT_UNAVAILABLE'
);
}
const result = await response.json();
@ -63,18 +112,44 @@ export class VoiceService {
duration,
};
} catch (error) {
if (error instanceof VoiceServiceError) {
throw error;
}
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
this.logger.error(`STT timeout after ${this.STT_TIMEOUT}ms`);
throw new VoiceServiceError(
'Spracherkennung dauert zu lange. Bitte versuche es erneut.',
'TIMEOUT'
);
}
this.logger.error(`Transcription failed: ${error}`);
throw error;
throw new VoiceServiceError(
'Spracherkennung nicht erreichbar.',
'STT_UNAVAILABLE'
);
}
}
/**
* Synthesize speech from text using mana-voice-bot (Edge TTS)
* Includes caching for common short responses
*/
async synthesize(text: string, userId?: string): Promise<Buffer> {
const prefs = this.getUserPreferences(userId);
const startTime = Date.now();
// Check cache for short texts (< 100 chars)
if (text.length < 100) {
const cacheKey = `${prefs.voice}:${text}`;
const cached = this.getCached(cacheKey);
if (cached) {
this.logger.debug(`TTS cache hit for "${text.substring(0, 30)}..."`);
return cached;
}
}
try {
const formData = new FormData();
formData.append('text', text);
@ -83,11 +158,15 @@ export class VoiceService {
const response = await fetch(`${this.voiceBotUrl}/tts`, {
method: 'POST',
body: formData,
signal: AbortSignal.timeout(this.TTS_TIMEOUT),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`TTS error: ${response.status} - ${error}`);
throw new VoiceServiceError(
`Sprachsynthese fehlgeschlagen: ${response.status}`,
'TTS_UNAVAILABLE'
);
}
const arrayBuffer = await response.arrayBuffer();
@ -96,19 +175,77 @@ export class VoiceService {
this.logger.debug(`Synthesized ${buffer.length} bytes in ${duration}ms`);
// Cache short responses
if (text.length < 100) {
const cacheKey = `${prefs.voice}:${text}`;
this.setCache(cacheKey, buffer);
}
return buffer;
} catch (error) {
if (error instanceof VoiceServiceError) {
throw error;
}
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
this.logger.error(`TTS timeout after ${this.TTS_TIMEOUT}ms`);
throw new VoiceServiceError(
'Sprachsynthese dauert zu lange.',
'TIMEOUT'
);
}
this.logger.error(`Synthesis failed: ${error}`);
throw error;
throw new VoiceServiceError(
'Sprachsynthese nicht erreichbar.',
'TTS_UNAVAILABLE'
);
}
}
/**
* Get cached TTS response
*/
private getCached(key: string): Buffer | null {
const entry = this.ttsCache.get(key);
if (!entry) return null;
// Check if expired
if (Date.now() - entry.timestamp > this.CACHE_TTL) {
this.ttsCache.delete(key);
return null;
}
return entry.buffer;
}
/**
* Cache TTS response
*/
private setCache(key: string, buffer: Buffer): void {
// Enforce max cache size
if (this.ttsCache.size >= this.MAX_CACHE_SIZE) {
// Remove oldest entry
const oldestKey = this.ttsCache.keys().next().value;
if (oldestKey) {
this.ttsCache.delete(oldestKey);
}
}
this.ttsCache.set(key, {
buffer,
timestamp: Date.now(),
});
}
/**
* Get available TTS voices
*/
async getVoices(): Promise<Record<string, string>> {
try {
const response = await fetch(`${this.voiceBotUrl}/voices`);
const response = await fetch(`${this.voiceBotUrl}/voices`, {
signal: AbortSignal.timeout(this.HEALTH_TIMEOUT),
});
if (!response.ok) {
throw new Error(`Failed to get voices: ${response.status}`);
}
@ -120,6 +257,24 @@ export class VoiceService {
}
}
/**
* Clear the TTS cache
*/
clearCache(): void {
this.ttsCache.clear();
this.logger.debug('TTS cache cleared');
}
/**
* Get cache statistics
*/
getCacheStats(): { size: number; maxSize: number } {
return {
size: this.ttsCache.size,
maxSize: this.MAX_CACHE_SIZE,
};
}
/**
* Check if voice services are available
*/

View file

@ -4,20 +4,13 @@ import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { PictureService } from '../picture/picture.service';
import { SessionService } from '@manacore/bot-services';
import { HELP_MESSAGE } from '../config/configuration';
// Natural language keywords that trigger commands
const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [
{ keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' },
{ keywords: ['modelle', 'models'], command: 'models' },
{ keywords: ['verlauf', 'history', 'bilder'], command: 'history' },
{ keywords: ['credits', 'guthaben'], command: 'credits' },
{ keywords: ['status', 'info'], command: 'status' },
];
interface ParsedPrompt {
prompt: string;
negativePrompt?: string;
@ -34,6 +27,13 @@ export class MatrixService extends BaseMatrixService {
// Track selected model per user
private userModels: Map<string, string> = new Map();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['modelle', 'models'], command: 'models' },
{ keywords: ['verlauf', 'history', 'bilder'], command: 'history' },
{ keywords: ['credits', 'guthaben'], command: 'credits' },
]);
constructor(
configService: ConfigService,
private pictureService: PictureService,
@ -76,7 +76,7 @@ Sag "hilfe" fur alle Befehle!`;
}
// Check for natural language keywords
const keywordCommand = this.detectKeywordCommand(message);
const keywordCommand = this.keywordDetector.detect(message);
if (keywordCommand) {
await this.handleCommand(roomId, sender, `!${keywordCommand}`);
return;
@ -85,23 +85,6 @@ Sag "hilfe" fur alle Befehle!`;
// Don't respond to random messages
}
private detectKeywordCommand(message: string): string | null {
const lowerMessage = message.toLowerCase().trim();
// Only match if the message is short
if (lowerMessage.length > 30) return null;
for (const { keywords, command } of KEYWORD_COMMANDS) {
for (const keyword of keywords) {
if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) {
this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`);
return command;
}
}
}
return null;
}
private async handleCommand(roomId: string, sender: string, body: string) {
const [command, ...args] = body.slice(1).split(' ');
const argString = args.join(' ');

View file

@ -4,26 +4,28 @@ import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { TodoService, Task } from '../todo/todo.service';
import { TranscriptionService } from '@manacore/bot-services';
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
// Natural language keywords that trigger commands (German + English)
const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [
{ keywords: ['hilfe', 'help', 'was kannst du', 'befehle', 'commands'], command: 'help' },
{
keywords: ['zeige aufgaben', 'meine aufgaben', 'was muss ich', 'show tasks', 'list'],
command: 'list',
},
{ keywords: ['heute', 'today', 'was steht an'], command: 'today' },
{ keywords: ['inbox', 'eingang', 'ohne datum'], command: 'inbox' },
{ keywords: ['projekte', 'projects'], command: 'projects' },
{ keywords: ['status', 'verbindung', 'connection'], command: 'status' },
];
@Injectable()
export class MatrixService extends BaseMatrixService {
private readonly keywordDetector = new KeywordCommandDetector(
[
...COMMON_KEYWORDS,
{ keywords: ['was kannst du'], command: 'help' },
{ keywords: ['zeige aufgaben', 'meine aufgaben', 'was muss ich', 'show tasks', 'list'], command: 'list' },
{ keywords: ['heute', 'today', 'was steht an'], command: 'today' },
{ keywords: ['inbox', 'eingang', 'ohne datum'], command: 'inbox' },
{ keywords: ['projekte', 'projects'], command: 'projects' },
{ keywords: ['verbindung', 'connection'], command: 'status' },
],
{ partialMatch: true }
);
constructor(
configService: ConfigService,
private todoService: TodoService,
@ -54,7 +56,7 @@ export class MatrixService extends BaseMatrixService {
try {
// Check for natural language keywords first
const keywordCommand = this.detectKeywordCommand(body);
const keywordCommand = this.keywordDetector.detect(body);
if (keywordCommand) {
await this.executeCommand(roomId, event, userId, keywordCommand, '');
return;
@ -140,27 +142,6 @@ export class MatrixService extends BaseMatrixService {
}
}
private detectKeywordCommand(message: string): string | null {
const lowerMessage = message.toLowerCase().trim();
// Only check short messages for keywords
if (lowerMessage.length > 50) return null;
for (const { keywords, command } of KEYWORD_COMMANDS) {
for (const keyword of keywords) {
if (
lowerMessage === keyword ||
lowerMessage.startsWith(keyword + ' ') ||
lowerMessage.includes(keyword)
) {
this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`);
return command;
}
}
}
return null;
}
private async executeCommand(
roomId: string,
event: MatrixRoomEvent,

View file

@ -4,30 +4,30 @@ import {
BaseMatrixService,
MatrixBotConfig,
MatrixRoomEvent,
KeywordCommandDetector,
COMMON_KEYWORDS,
} from '@manacore/matrix-bot-common';
import { QuotesService } from '../quotes/quotes.service';
import { ZitareService } from '../quotes/zitare.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { HELP_MESSAGE, Category } from '../config/configuration';
// Natural language keywords that trigger commands
const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [
{ keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' },
{ keywords: ['zitat', 'quote', 'inspiration', 'inspiriere'], command: 'zitat' },
{ keywords: ['heute', 'today', 'tages', 'tageszitat'], command: 'heute' },
{ keywords: ['motiviere', 'motivation', 'motivier mich'], command: 'motivation' },
{ keywords: ['guten morgen', 'morgen', 'good morning'], command: 'morgen' },
{ keywords: ['kategorien', 'categories', 'themen'], command: 'kategorien' },
{ keywords: ['favoriten', 'favorites', 'meine favoriten'], command: 'favoriten' },
{ keywords: ['listen', 'lists', 'meine listen'], command: 'listen' },
{ keywords: ['status', 'info'], command: 'status' },
];
@Injectable()
export class MatrixService extends BaseMatrixService {
// Track last shown quote per user for favorites
private lastQuotes: Map<string, string> = new Map();
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['zitat', 'quote', 'inspiration', 'inspiriere'], command: 'zitat' },
{ keywords: ['heute', 'today', 'tages', 'tageszitat'], command: 'heute' },
{ keywords: ['motiviere', 'motivation', 'motivier mich'], command: 'motivation' },
{ keywords: ['guten morgen', 'morgen', 'good morning'], command: 'morgen' },
{ keywords: ['kategorien', 'categories', 'themen'], command: 'kategorien' },
{ keywords: ['favoriten', 'favorites', 'meine favoriten'], command: 'favoriten' },
{ keywords: ['listen', 'lists', 'meine listen'], command: 'listen' },
]);
constructor(
configService: ConfigService,
private quotesService: QuotesService,
@ -76,7 +76,7 @@ Sag "hilfe" fuer alle Befehle!`;
}
// Check for natural language keywords
const keywordCommand = this.detectKeywordCommand(body);
const keywordCommand = this.keywordDetector.detect(body);
if (keywordCommand) {
await this.handleCommand(roomId, sender, `!${keywordCommand}`);
return;
@ -124,7 +124,7 @@ Sag "hilfe" fuer alle Befehle!`;
const cleanText = transcription.trim();
// Check for keyword commands in the transcription
const keywordCommand = this.detectKeywordCommand(cleanText);
const keywordCommand = this.keywordDetector.detect(cleanText);
if (keywordCommand) {
await this.handleCommand(roomId, sender, `!${keywordCommand}`);
return;
@ -153,23 +153,6 @@ Sag "hilfe" fuer alle Befehle!`;
}
}
private detectKeywordCommand(message: string): string | null {
const lowerMessage = message.toLowerCase().trim();
// Only match if the message is short
if (lowerMessage.length > 50) return null;
for (const { keywords, command } of KEYWORD_COMMANDS) {
for (const keyword of keywords) {
if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) {
this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`);
return command;
}
}
}
return null;
}
private async handleCommand(roomId: string, sender: string, body: string) {
const [command, ...args] = body.slice(1).split(' ');
const argString = args.join(' ');