refactor(settings): remove avatar selector and minimax integration, bump version to 1.0.2

This commit is contained in:
Michael committed 2026-05-20 22:21:49 +02:00
1 parent bf247e7e5f
commit ba859aa78c
5 files changed
+2701 -302

No files matched your search

+2537
View File
File diff suppressed because it is too large. Load diff
+28 -2
View File
@@ -1,6 +1,6 @@
{
"name": "eveflow",
"version": "1.0.0",
"version": "1.0.2",
"description": "Retro-futuristic Windows companion with 3D Eve bot, local TTS/STT, and multi-agent connectivity",
"main": "main.js",
"scripts": {
@@ -8,7 +8,32 @@
"build": "tsc && vite build",
"preview": "vite preview",
"electron": "electron .",
"start": "concurrently -k \"npm run dev -- --host 127.0.0.1 --port 5173 --strictPort\" \"wait-on http://localhost:5173 && npm run electron\""
"start": "concurrently -k \"npm run dev -- --host 127.0.0.1 --port 5173 --strictPort\" \"wait-on http://localhost:5173 && npm run electron\"",
"dist": "electron-builder --win"
},
"build": {
"appId": "com.romik3.eveflow",
"productName": "EveFlow",
"copyright": "Copyright © 2026 R0m1k3",
"directories": {
"output": "out"
},
"files": [
"main.js",
"preload.js",
"dist/**/*"
],
"win": {
"target": "nsis",
"requestedExecutionLevel": "asInvoker"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "EveFlow"
}
},
"dependencies": {
"@react-three/drei": "^9.105.0",
@@ -27,6 +52,7 @@
"@vitejs/plugin-react": "^4.2.1",
"concurrently": "^8.2.2",
"electron": "^30.0.1",
"electron-builder": "^26.8.1",
"typescript": "^5.4.2",
"vite": "^5.2.2",
"wait-on": "^7.2.0"
+6 -178
View File
@@ -11,6 +11,7 @@ import { AgentService, AgentConfig, AgentProvider } from './services/agentServic
import { EmotionService, EveAvatar, EveEmotion } from './services/emotionService';
import { PollingService } from './services/pollingService';
import { AppCallbacks } from './services/toolRegistry';
import { version } from '../package.json';
interface Message {
id: string;
@@ -395,10 +396,7 @@ export const App: React.FC = () => {
hermesUrl: 'http://127.0.0.1:8642/v1',
hermesModel: 'hermes-agent',
hermesApiKey: '',
hermesSessionKey: 'eveflow-user-session',
minimaxApiKey: '',
minimaxGroupId: '',
minimaxModel: 'abab6.5-chat'
hermesSessionKey: 'eveflow-user-session'
});
const [availableVoices, setAvailableVoices] = useState<any[]>([]);
@@ -407,19 +405,7 @@ export const App: React.FC = () => {
const [sttLang, setSttLang] = useState<string>('fr-FR');
const activeAvatar = AVATAR_PROFILES[avatarId];
// Minimax dynamic models states
const [minimaxModelsList, setMinimaxModelsList] = useState<string[]>([
'MiniMax-Text-01',
'MiniMax-M2.7',
'MiniMax-M2.5',
'MiniMax-M2.1',
'MiniMax-M2',
'MiniMax-M2-her',
'abab6.5-chat'
]);
const [isLoadingMinimaxModels, setIsLoadingMinimaxModels] = useState(false);
const [minimaxModelsError, setMinimaxModelsError] = useState<string | null>(null);
const [lastLoadedMinimaxCredentials, setLastLoadedMinimaxCredentials] = useState<{ apiKey: string, groupId: string } | null>(null);
// Session Hermes persistante — garantit la continuité mémoire/skills entre redémarrages
const [hermesSessionId, setHermesSessionId] = useState<string>('');
@@ -570,22 +556,7 @@ export const App: React.FC = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, showSettings]);
// Chargement automatique des modèles Minimax lorsque les identifiants requis sont saisis et que le fournisseur est Minimax
useEffect(() => {
const hasCredentialsChanged = !lastLoadedMinimaxCredentials ||
lastLoadedMinimaxCredentials.apiKey !== agentConfig.minimaxApiKey ||
lastLoadedMinimaxCredentials.groupId !== agentConfig.minimaxGroupId;
if (
agentConfig.provider === 'minimax' &&
agentConfig.minimaxApiKey &&
agentConfig.minimaxGroupId &&
hasCredentialsChanged &&
!isLoadingMinimaxModels
) {
handleLoadMinimaxModels();
}
}, [agentConfig.provider, agentConfig.minimaxApiKey, agentConfig.minimaxGroupId, lastLoadedMinimaxCredentials]);
// Polling Hermes Jobs
useEffect(() => {
@@ -654,16 +625,7 @@ export const App: React.FC = () => {
persistWrite('eveflow_agent_config', JSON.stringify(newConfig));
};
const handleAvatarChange = (nextAvatarId: EveAvatar) => {
setAvatarId(nextAvatarId);
setCurrentEmotion('neutral');
persistWrite('eveflow_avatar_id', nextAvatarId);
setMessages(prev => {
const canReplaceWelcome = prev.length === 0 || (prev.length === 1 && prev[0].id === 'welcome');
return canReplaceWelcome ? [createWelcomeMessage(nextAvatarId)] : prev;
});
};
const handleWindowControl = (action: 'minimize' | 'maximize' | 'close') => {
if (window.electronAPI) {
@@ -947,40 +909,7 @@ export const App: React.FC = () => {
}
};
const handleLoadMinimaxModels = async () => {
if (!agentConfig.minimaxApiKey) {
setMinimaxModelsError("Veuillez saisir votre clé API Minimax.");
return;
}
setIsLoadingMinimaxModels(true);
setMinimaxModelsError(null);
try {
const models = await AgentService.getMinimaxModels(
agentConfig.minimaxApiKey,
agentConfig.minimaxGroupId
);
if (models && models.length > 0) {
setMinimaxModelsList(models);
setLastLoadedMinimaxCredentials({
apiKey: agentConfig.minimaxApiKey,
groupId: agentConfig.minimaxGroupId
});
// Si le modèle configuré n'est pas dans la liste des modèles disponibles, on bascule vers le premier
if (!models.includes(agentConfig.minimaxModel)) {
saveConfig({ ...agentConfig, minimaxModel: models[0] });
}
} else {
throw new Error("Aucun modèle retourné par l'API.");
}
} catch (err: any) {
console.error("Échec de chargement des modèles Minimax:", err);
setMinimaxModelsError(err.message || "Impossible de charger les modèles.");
} finally {
setIsLoadingMinimaxModels(false);
}
};
return (
<div className={`app-container ${isFloatingMode ? 'transparent-bg' : ''}`}>
@@ -993,7 +922,7 @@ export const App: React.FC = () => {
<h1 className="neon-title" style={{ fontFamily: 'var(--font-title)', letterSpacing: '2px', color: 'var(--text-primary)', fontSize: '18px', fontWeight: 800, display: 'flex', alignItems: 'center', gap: '10px', lineHeight: 1 }}>
EVEFLOW
<span className="header-subtitle" style={{ color: 'var(--text-secondary)', fontWeight: 500, fontSize: '14px' }}>// ANALOG SPACE OS</span>
<span className="sys-ready-badge" style={{ fontSize: '9px', padding: '3px 10px', backgroundColor: 'var(--accent-light)', color: 'var(--text-neon)', border: '1px solid rgba(0,212,245,0.4)', borderRadius: '20px', fontWeight: 'bold', fontFamily: 'var(--font-mono)' }}>SYS_READY_V74</span>
<span className="sys-ready-badge" style={{ fontSize: '9px', padding: '3px 10px', backgroundColor: 'var(--accent-light)', color: 'var(--text-neon)', border: '1px solid rgba(0,212,245,0.4)', borderRadius: '20px', fontWeight: 'bold', fontFamily: 'var(--font-mono)' }}>SYS_READY_V{version}</span>
</h1>
</div>
@@ -1332,52 +1261,7 @@ export const App: React.FC = () => {
</button>
</div>
{/* Avatar Selector */}
<div className="settings-card">
<h3 className="label-title" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Sparkles style={{ width: '14px', height: '14px' }} /> Selection d'Avatar
</h3>
<div className="grid-3">
{(Object.keys(AVATAR_PROFILES) as EveAvatar[]).map((id) => {
const profile = AVATAR_PROFILES[id];
const isActive = avatarId === id;
return (
<button
key={id}
onClick={() => handleAvatarChange(id)}
className="glow-input"
style={{
cursor: 'pointer',
minHeight: '112px',
padding: '14px',
textAlign: 'left',
borderColor: isActive ? profile.accent : 'var(--text-primary)',
backgroundColor: isActive ? 'var(--bg-glass-active)' : 'var(--bg-secondary)',
boxShadow: isActive ? `0 0 18px ${profile.glow}` : 'none'
}}
>
<span style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: profile.accent, boxShadow: `0 0 12px ${profile.accent}` }} />
<span style={{ fontFamily: 'var(--font-title)', fontWeight: 900, fontSize: '13px', textTransform: 'uppercase' }}>{profile.name}</span>
</span>
<span style={{ display: 'block', fontSize: '10px', lineHeight: 1.4, color: 'var(--text-secondary)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase' }}>
{profile.role}
</span>
{profile.assetPath && (
<span style={{ display: 'block', marginTop: '10px', fontSize: '9px', lineHeight: 1.35, color: isActive ? profile.accent : 'var(--text-secondary)', fontFamily: 'var(--font-mono)', textTransform: 'uppercase', fontWeight: 800 }}>
Asset requis: {profile.assetPath}
</span>
)}
</button>
);
})}
</div>
{avatarId !== 'eve' && activeAvatar.assetPath && (
<div style={{ marginTop: '12px', padding: '10px 12px', border: '1px solid rgba(0,212,245,0.2)', borderRadius: 'var(--radius-sm)', backgroundColor: 'rgba(0,212,245,0.06)', fontSize: '10px', lineHeight: 1.45, color: 'var(--text-secondary)', fontFamily: 'var(--font-mono)', fontWeight: 700 }}>
Si le fichier GLB premium est absent, EveFlow utilise Eve comme fallback visuel. Installe l'asset dans {activeAvatar.assetPath} pour activer le vrai modele {activeAvatar.name}.
</div>
)}
</div>
{/* A. Agents */}
<div className="settings-card">
@@ -1386,7 +1270,7 @@ export const App: React.FC = () => {
</h3>
<div className="grid-3">
{(['ollama', 'hermes', 'minimax'] as AgentProvider[]).map((prov) => (
{(['ollama', 'hermes'] as AgentProvider[]).map((prov) => (
<button
key={prov}
onClick={() => saveConfig({ ...agentConfig, provider: prov })}
@@ -1487,63 +1371,7 @@ export const App: React.FC = () => {
</div>
)}
{agentConfig.provider === 'minimax' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label className="label-title">Clé API Minimax</label>
<input
type="password"
value={agentConfig.minimaxApiKey}
onChange={(e) => saveConfig({ ...agentConfig, minimaxApiKey: e.target.value })}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px' }}
placeholder="Ex: MM-..."
/>
</div>
<div className="grid-2">
<div>
<label className="label-title">Minimax Group ID</label>
<input
type="text"
value={agentConfig.minimaxGroupId}
onChange={(e) => saveConfig({ ...agentConfig, minimaxGroupId: e.target.value })}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px' }}
placeholder="Ex: 1234567"
/>
</div>
<div>
<label className="label-title">Modèle Actuel</label>
<select
value={agentConfig.minimaxModel}
onChange={(e) => saveConfig({ ...agentConfig, minimaxModel: e.target.value })}
className="glow-input"
style={{ padding: '8px 12px', fontSize: '12px', width: '100%' }}
>
{minimaxModelsList.map((m) => (
<option key={m} value={m}>{m}</option>
))}
</select>
</div>
</div>
<div style={{ marginTop: '4px' }}>
<button
onClick={handleLoadMinimaxModels}
className="neon-btn neon-btn-secondary"
style={{ width: '100%', padding: '8px 16px', fontSize: '11px', gap: '6px', cursor: 'pointer' }}
disabled={isLoadingMinimaxModels}
>
{isLoadingMinimaxModels ? "Chargement des modèles..." : "Charger les modèles disponibles"}
</button>
{minimaxModelsError && (
<span style={{ fontSize: '10px', color: '#ef4444', display: 'block', marginTop: '6px', fontWeight: 'bold' }}>
⚠️ {minimaxModelsError} (Utilisation des modèles par défaut)
</span>
)}
</div>
</div>
)}
</div>
{/* B. TTS / STT */}
+1 -109
View File
@@ -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
View File
@@ -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, ', ');
// 2. Supprimer toutes les URLs nues (http, https, ftp, file) complexes ou simples
// 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. 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 {