mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-14 02:36:32 +02:00
refactor(settings): remove avatar selector and minimax integration, bump version to 1.0.2
This commit is contained in:
1 parent
bf247e7e5f
commit
ba859aa78c
5 files changed
+2703
-304
No files matched your search
@@ -2,7 +2,7 @@ import { EveAvatar } from './emotionService';
|
||||
import { LogService } from './logService';
|
||||
import { HERMES_TOOLS, executeTool, AppCallbacks } from './toolRegistry';
|
||||
|
||||
export type AgentProvider = 'ollama' | 'hermes' | 'minimax';
|
||||
export type AgentProvider = 'ollama' | 'hermes';
|
||||
|
||||
export interface AgentConfig {
|
||||
provider: AgentProvider;
|
||||
@@ -12,9 +12,6 @@ export interface AgentConfig {
|
||||
hermesModel: string;
|
||||
hermesApiKey: string;
|
||||
hermesSessionKey: string;
|
||||
minimaxApiKey: string;
|
||||
minimaxGroupId: string;
|
||||
minimaxModel: string;
|
||||
}
|
||||
|
||||
export class AgentService {
|
||||
@@ -80,8 +77,6 @@ export class AgentService {
|
||||
return this.sendToOllama(message, updatedHistory, config.ollamaUrl, config.ollamaModel, images);
|
||||
case 'hermes':
|
||||
return this.sendToHermes(message, updatedHistory, config, callbacks, onToken, onToolCall, sessionId, onSessionIdChange, images);
|
||||
case 'minimax':
|
||||
return this.sendToMinimax(message, history, config, avatarId);
|
||||
default:
|
||||
throw new Error("Fournisseur d'agent non reconnu");
|
||||
}
|
||||
@@ -378,107 +373,4 @@ export class AgentService {
|
||||
return fullContent || "Désolé, je n'ai pas reçu de réponse de Hermes Agent.";
|
||||
}
|
||||
|
||||
// ── Connecteur Minimax (API Externe) ─────────────────────────────────────
|
||||
private static async sendToMinimax(
|
||||
message: string,
|
||||
history: { role: 'user' | 'assistant'; content: string }[],
|
||||
config: AgentConfig,
|
||||
avatarId: EveAvatar = 'eve'
|
||||
): Promise<string> {
|
||||
const { minimaxApiKey, minimaxGroupId, minimaxModel } = config;
|
||||
|
||||
if (!minimaxApiKey) {
|
||||
throw new Error("Clé d'API Minimax manquante dans les paramètres.");
|
||||
}
|
||||
|
||||
const systemPrompt = this.getSystemPromptForAvatar(avatarId);
|
||||
const formattedMessages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...history.map(h => ({ role: h.role, content: h.content })),
|
||||
{ role: 'user', content: message }
|
||||
];
|
||||
|
||||
const url = 'https://api.minimax.chat/v1/chat/completions';
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `Bearer ${minimaxApiKey.trim()}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (minimaxGroupId) headers['GroupId'] = minimaxGroupId.trim();
|
||||
|
||||
LogService.info('Minimax', `POST ${url}`, {
|
||||
model: minimaxModel,
|
||||
hasGroupId: !!minimaxGroupId,
|
||||
historyLength: history.length
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ model: minimaxModel || 'abab6.5-chat', messages: formattedMessages, temperature: 0.7 })
|
||||
});
|
||||
|
||||
LogService.info('Minimax', `HTTP ${response.status} ${response.statusText}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const rawErr = await response.text().catch(() => '(no body)');
|
||||
LogService.error('Minimax', `Réponse non-OK`, { status: response.status, body: rawErr });
|
||||
let errorMsg = `Erreur HTTP Minimax: ${response.status}`;
|
||||
try { const p = JSON.parse(rawErr); errorMsg = `Minimax API Error: ${p.base_resp?.status_msg || p.error?.message || response.status}`; } catch {}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
LogService.debug('Minimax', 'Réponse reçue', { base_resp: data.base_resp, replyPreview: data.choices?.[0]?.message?.content?.slice(0, 200) });
|
||||
|
||||
if (data.base_resp && data.base_resp.status_code !== 0) {
|
||||
throw new Error(`Minimax API Error: ${data.base_resp.status_msg}`);
|
||||
}
|
||||
|
||||
return data.choices?.[0]?.message?.content || "Désolé, je n'ai pas reçu de réponse de Minimax.";
|
||||
} catch (e: any) {
|
||||
LogService.error('Minimax', `Exception lors de la requête`, { message: e.message, stack: e.stack });
|
||||
throw new Error(`Échec de connexion à Minimax (${e.message || e})`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Récupérer la liste des modèles Minimax ───────────────────────────────
|
||||
public static async getMinimaxModels(apiKey: string, groupId?: string): Promise<string[]> {
|
||||
if (!apiKey) throw new Error("Clé d'API Minimax requise pour lister les modèles.");
|
||||
|
||||
const url = 'https://api.minimax.chat/v1/models';
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `Bearer ${apiKey.trim()}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
if (groupId) headers['GroupId'] = groupId.trim();
|
||||
|
||||
LogService.info('Minimax', `GET ${url}`, { hasGroupId: !!groupId });
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: 'GET', headers });
|
||||
|
||||
LogService.info('Minimax', `GET /models → HTTP ${response.status}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const rawErr = await response.text().catch(() => '(no body)');
|
||||
LogService.error('Minimax', `Erreur /models`, { status: response.status, body: rawErr });
|
||||
let errorMsg = `Erreur HTTP Minimax (${response.status})`;
|
||||
try { const p = JSON.parse(rawErr); errorMsg = `Minimax API Error: ${p.base_resp?.status_msg || p.error?.message || response.status}`; } catch {}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
LogService.debug('Minimax', 'Liste modèles reçue', data);
|
||||
|
||||
if (data && Array.isArray(data.data)) return data.data.map((m: any) => m.id);
|
||||
if (data && Array.isArray(data.models)) return data.models.map((m: any) => m.model_name || m.id || m);
|
||||
if (Array.isArray(data)) return data.map((m: any) => m.id || m);
|
||||
|
||||
throw new Error("Format de réponse non standard");
|
||||
} catch (e: any) {
|
||||
LogService.error('Minimax', `Exception /models`, { message: e.message });
|
||||
throw new Error(`Échec de récupération des modèles Minimax: ${e.message || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
+129
-13
@@ -4,6 +4,52 @@ export interface VoiceOption {
|
||||
voice: SpeechSynthesisVoice;
|
||||
}
|
||||
|
||||
// Dictionnaire phonétique local pour corriger la prononciation des acronymes et anglicismes en français
|
||||
const PHONETIC_DICTIONARY: Record<string, string> = {
|
||||
'api': 'a-pé-i',
|
||||
'cors': 'korss',
|
||||
'svg': 'ess-vé-gé',
|
||||
'cpu': 'cé-pé-u',
|
||||
'fps': 'eff-pé-ess',
|
||||
'tts': 'té-té-ess',
|
||||
'ui': 'u-i',
|
||||
'ai': 'a-i',
|
||||
'ia': 'i-a',
|
||||
'json': 'dji-zone',
|
||||
'url': 'u-err-el',
|
||||
'html': 'ach-té-em-el',
|
||||
'css': 'cé-ess-ess',
|
||||
'js': 'ji-ess',
|
||||
'typescript': 'taïpe-skript',
|
||||
'javascript': 'djava-skript',
|
||||
'electron': 'élèktron',
|
||||
'react': 'ri-akt',
|
||||
'github': 'gitte-ub',
|
||||
'git': 'gitte',
|
||||
'setup': 'sétup',
|
||||
'c++': 'cé-plus-plus',
|
||||
'node.js': 'node-j-s',
|
||||
'nodejs': 'node-j-s',
|
||||
'database': 'deïta-beïss',
|
||||
'token': 'tokène',
|
||||
'cli': 'cé-el-i',
|
||||
'npm': 'en-pé-em',
|
||||
'npx': 'en-pé-ix',
|
||||
'vite': 'vite',
|
||||
'vscode': 'vé-ess-code',
|
||||
'vs code': 'vé-ess-code',
|
||||
'windows': 'windoze',
|
||||
'macos': 'mak-os',
|
||||
'linux': 'linux',
|
||||
'docker': 'dokeur',
|
||||
'cyberpunk': 'saïbeur-pounk',
|
||||
'avatar': 'avatar',
|
||||
'chat': 'tchat',
|
||||
'prompt': 'prompte',
|
||||
'welcome': 'ouel-kome',
|
||||
'ipc': 'i-pé-cé'
|
||||
};
|
||||
|
||||
export class AudioService {
|
||||
public ttsSynth: SpeechSynthesis;
|
||||
private recognition: any = null;
|
||||
@@ -27,10 +73,52 @@ export class AudioService {
|
||||
}
|
||||
}
|
||||
|
||||
// Calculer un score de qualité pour trier les voix locales
|
||||
private _getVoiceScore(v: SpeechSynthesisVoice): number {
|
||||
const lang = v.lang.toLowerCase();
|
||||
const name = v.name.toLowerCase();
|
||||
|
||||
if (!lang.startsWith('fr')) return 0;
|
||||
|
||||
let score = 100; // Base français
|
||||
|
||||
if (name.includes('google') || name.includes('online')) {
|
||||
score += 50;
|
||||
}
|
||||
if (name.includes('natural') || name.includes('neural')) {
|
||||
score += 40;
|
||||
}
|
||||
if (name.includes('mobile')) {
|
||||
score += 30;
|
||||
}
|
||||
if (name.includes('julie') || name.includes('paul')) {
|
||||
score += 20;
|
||||
}
|
||||
if (name.includes('hortense')) {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
// Récupérer les voix françaises et globales disponibles
|
||||
public getVoices(): VoiceOption[] {
|
||||
if (!this.ttsSynth) return [];
|
||||
return this.ttsSynth.getVoices().map(v => ({
|
||||
|
||||
const allVoices = this.ttsSynth.getVoices();
|
||||
|
||||
// Trier pour mettre les meilleures voix françaises en haut
|
||||
const sorted = [...allVoices].sort((a, b) => {
|
||||
const scoreA = this._getVoiceScore(a);
|
||||
const scoreB = this._getVoiceScore(b);
|
||||
|
||||
if (scoreA !== scoreB) {
|
||||
return scoreB - scoreA;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return sorted.map(v => ({
|
||||
name: v.name,
|
||||
lang: v.lang,
|
||||
voice: v
|
||||
@@ -40,34 +128,58 @@ export class AudioService {
|
||||
private _resolveVoice(voiceName?: string): SpeechSynthesisVoice | undefined {
|
||||
const voices = this.ttsSynth.getVoices();
|
||||
if (voiceName) return voices.find(v => v.name === voiceName);
|
||||
return voices.find(v => v.lang.startsWith('fr') && v.name.toLowerCase().includes('google'))
|
||||
|| voices.find(v => v.lang.startsWith('fr'));
|
||||
|
||||
const frVoices = voices.filter(v => v.lang.toLowerCase().startsWith('fr'));
|
||||
if (frVoices.length === 0) return voices.find(v => v.lang.startsWith('en'));
|
||||
|
||||
// Sélectionner la voix française locale avec le meilleur score
|
||||
return frVoices.sort((a, b) => this._getVoiceScore(b) - this._getVoiceScore(a))[0];
|
||||
}
|
||||
|
||||
private _applyPhoneticCorrections(text: string): string {
|
||||
let corrected = text;
|
||||
for (const [key, replacement] of Object.entries(PHONETIC_DICTIONARY)) {
|
||||
const escapedKey = key.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
// Regex recherchant le mot technique avec des limites Unicode ou de ponctuations
|
||||
const regex = new RegExp(`(?<=^|\\s|\\p{P})${escapedKey}(?=$|\\s|\\p{P})`, 'giu');
|
||||
corrected = corrected.replace(regex, replacement);
|
||||
}
|
||||
return corrected;
|
||||
}
|
||||
|
||||
private _cleanForTTS(raw: string): string {
|
||||
// 1. Extraire le texte des liens Markdown [Texte](url). Gère aussi les parenthèses dans les URLs
|
||||
let cleaned = raw.replace(/\[([^\]]+)\]\((?:[^)(]+|\([^)(]*\))*\)/g, '$1');
|
||||
// 1. Améliorer la ponctuation Markdown pour forcer des pauses de diction naturelles
|
||||
// Remplacer les puces de liste (ex: - item, * item) par une pause douce (virgule)
|
||||
let formatted = raw.replace(/(?:\r?\n)\s*[-*+]\s+/g, ', ');
|
||||
|
||||
// Remplacer les deux-points suivis d'un saut de ligne par un point
|
||||
formatted = formatted.replace(/:\s*(?:\r?\n)/g, '. ');
|
||||
|
||||
// Remplacer les doubles sauts de ligne par des points (pauses de paragraphes)
|
||||
formatted = formatted.replace(/(?:\r?\n){2,}/g, '. ');
|
||||
|
||||
// Remplacer les sauts de ligne simples restant par des espaces
|
||||
formatted = formatted.replace(/(?:\r?\n)/g, ' ');
|
||||
|
||||
// 2. Supprimer toutes les URLs nues (http, https, ftp, file) complexes ou simples
|
||||
// 2. Extraire le texte des liens Markdown [Texte](url)
|
||||
let cleaned = formatted.replace(/\[([^\]]+)\]\((?:[^)(]+|\([^)(]*\))*\)/g, '$1');
|
||||
|
||||
// 3. Supprimer toutes les URLs nues (http, https, ftp, file)
|
||||
cleaned = cleaned.replace(/(?:https?|ftp|file):\/\/\S+/gi, '');
|
||||
|
||||
// 3. Supprimer les adresses commençant par www.
|
||||
// 4. Supprimer les adresses www.
|
||||
cleaned = cleaned.replace(/www\.\S+/gi, '');
|
||||
|
||||
// 4. Supprimer tout mot technique contenant des chemins de fichiers, adresses IP ou segments techniques
|
||||
// (ex: C:\chemin, localhost:8080, v1/chat/completions)
|
||||
// 5. Filtrer les mots techniques complexes (chemins de fichiers, adresses IP)
|
||||
const words = cleaned.split(/\s+/);
|
||||
cleaned = words
|
||||
.filter(word => {
|
||||
// Exclure les mots qui ressemblent à des chemins ou contiennent des slashes / backslashes
|
||||
if (word.includes('/') || word.includes('\\')) {
|
||||
return false;
|
||||
}
|
||||
// Exclure les hôtes avec port (ex: localhost:8080, 127.0.0.1:8642)
|
||||
if (/(?:[a-zA-Z0-9-]+\.[a-zA-Z]{2,6}|localhost|[\d.]+):\d+/.test(word)) {
|
||||
return false;
|
||||
}
|
||||
// Exclure les adresses IP nues
|
||||
if (/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/.test(word)) {
|
||||
return false;
|
||||
}
|
||||
@@ -75,7 +187,8 @@ export class AudioService {
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return cleaned
|
||||
// 6. Nettoyer les caractères Markdown, Emojis et symboles non prononçables
|
||||
cleaned = cleaned
|
||||
// Emojis et symboles Unicode
|
||||
.replace(/[\u{1F000}-\u{1FFFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FEFF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA9F}]/gu, '')
|
||||
// Markdown restant : **, *, _, __, #, `, ~
|
||||
@@ -89,6 +202,9 @@ export class AudioService {
|
||||
// Espaces multiples
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim();
|
||||
|
||||
// 7. Appliquer les corrections phonétiques locales
|
||||
return this._applyPhoneticCorrections(cleaned);
|
||||
}
|
||||
|
||||
private _makeUtterance(text: string, voiceName?: string, rate = 1.0, pitch = 1.1): SpeechSynthesisUtterance {
|
||||
|
||||
Reference in new issue
Block a user