mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-19 05:09:30 +02:00
fix: liaison « dégradé », réponses vides, transcriptions parasites, voix masculine (v2.4.0.1) (#15)
- Liaison Hermes : l'échec de l'API des crons (/api/jobs absente ou refusée) ne fait plus passer la liaison en « dégradé » ; l'onglet Crons affiche la raison. Les états « healthy / ready / ok » sont reconnus et un état « degraded » détaille les contrôles en échec. - Chat completions : une réponse sans fragment n'affiche plus « … » ; le corps est relu (JSON non streamé, erreur renvoyée en HTTP 200) et sinon l'erreur explicite « Réponse vide de Hermes » est affichée avec le début du corps. - Voix : transcriptions parasites de Whisper (« (cliquant) », « *Claire* », « [Musique] », génériques de sous-titres) ignorées au lieu d'être envoyées. - Préférence de voix masculine / féminine (Paramètres → Voix), appliquée à tous les moteurs : Piper Tom / Pierre en local (téléchargement automatique), onyx / nova en API OpenAI, Paul / Hortense en voix Windows. Genre des voix du catalogue déduit des libellés. Tests : 42 tests unitaires (santé, récupération de réponse, filtre de bruit, préférence de voix) ; e2e Electron inchangé et vert. Claude-Session: https://claude.ai/code/session_017Wn5VX9HNbJ7N54hR24u9Y Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
6fc38d335b
commit
6d3073b083
19 files changed
+350
-14
No files matched your search
+36
-6
@@ -38,6 +38,8 @@ interface HermesStore {
|
||||
sessions: HermesSession[];
|
||||
jobs: HermesJob[];
|
||||
jobRuns: JobRun[];
|
||||
/** Last cron sync failure (the chat link stays independent of it). */
|
||||
jobsError: string | null;
|
||||
transport: ResolvedTransport;
|
||||
lastSyncAt: number | null;
|
||||
webhook: WebhookStatus | null;
|
||||
@@ -88,6 +90,7 @@ export const useHermes = create<HermesStore>((set, get) => ({
|
||||
sessions: [],
|
||||
jobs: [],
|
||||
jobRuns: [],
|
||||
jobsError: null,
|
||||
transport: 'completions',
|
||||
lastSyncAt: null,
|
||||
webhook: null,
|
||||
@@ -119,8 +122,8 @@ export const useHermes = create<HermesStore>((set, get) => ({
|
||||
Log.warn('hermes', `capabilities unavailable: ${(err as Error).message}`);
|
||||
}
|
||||
const transport = resolveTransport(config, capabilities);
|
||||
const degraded = String(health.status ?? 'ok').toLowerCase() !== 'ok';
|
||||
set({ health, capabilities, transport, link: degraded ? 'degraded' : 'online', linkDetail: degraded ? `état ${health.status}` : '' });
|
||||
const degraded = !isHealthyStatus(health.status);
|
||||
set({ health, capabilities, transport, link: degraded ? 'degraded' : 'online', linkDetail: degraded ? describeHealth(health) : '' });
|
||||
Log.info('hermes', `connected (${transport})`, { status: health.status, model: capabilities?.model });
|
||||
void get().refreshCatalog();
|
||||
void get().refreshJobs();
|
||||
@@ -165,7 +168,7 @@ export const useHermes = create<HermesStore>((set, get) => ({
|
||||
const merged = [...incoming, ...previous.filter((r) => !incoming.some((i) => i.id === r.id))]
|
||||
.sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime())
|
||||
.slice(0, 100);
|
||||
set({ jobs, jobRuns: merged, lastSyncAt: Date.now(), link: get().link === 'offline' || get().link === 'degraded' ? 'online' : get().link });
|
||||
set({ jobs, jobRuns: merged, jobsError: null, lastSyncAt: Date.now(), link: get().link === 'offline' ? 'online' : get().link });
|
||||
const snapshot = JSON.stringify({ jobs, jobRuns: merged });
|
||||
if (snapshot !== lastCacheSnapshot) {
|
||||
lastCacheSnapshot = snapshot;
|
||||
@@ -174,9 +177,8 @@ export const useHermes = create<HermesStore>((set, get) => ({
|
||||
} catch (err) {
|
||||
const message = (err as Error).message;
|
||||
Log.warn('hermes', `jobs sync failed: ${message}`);
|
||||
if (/HTTP 404/.test(message)) return; // jobs API disabled on this server
|
||||
// A failing jobs poll does not mean chat is down: degrade, and let the next health probe decide.
|
||||
set({ linkDetail: message, link: get().link === 'online' ? 'degraded' : get().link });
|
||||
// The cron API can be absent or restricted on a given Hermes: the chat link is unaffected.
|
||||
set({ jobsError: /HTTP 404/.test(message) ? 'API des crons absente sur ce serveur Hermes' : message });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -233,3 +235,31 @@ export const useHermes = create<HermesStore>((set, get) => ({
|
||||
}));
|
||||
|
||||
export { jobStatus };
|
||||
|
||||
const HEALTHY = new Set(['ok', 'healthy', 'up', 'alive', 'pass', 'ready', 'running', 'online', 'true']);
|
||||
|
||||
/** Hermes /health reports "ok"; /health/detailed may report "healthy", "degraded" or "unhealthy". */
|
||||
export function isHealthyStatus(status: unknown): boolean {
|
||||
if (status === undefined || status === null || status === '') return true;
|
||||
return HEALTHY.has(String(status).toLowerCase());
|
||||
}
|
||||
|
||||
/** Human summary of a degraded health payload: failing checks by name. */
|
||||
export function describeHealth(health: Record<string, unknown>): string {
|
||||
const failing: string[] = [];
|
||||
const visit = (node: unknown, prefix: string, depth: number) => {
|
||||
if (!node || typeof node !== 'object' || depth > 3) return;
|
||||
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
||||
if (key === 'status') continue;
|
||||
if (value && typeof value === 'object') {
|
||||
const rec = value as Record<string, unknown>;
|
||||
const st = rec.status ?? rec.ok ?? rec.healthy;
|
||||
if (st !== undefined && !isHealthyStatus(st)) failing.push(prefix + key);
|
||||
else visit(value, `${prefix}${key}.`, depth + 1);
|
||||
} else if (typeof value === 'boolean' && !value && /ok|healthy|ready|connected|available/i.test(key)) failing.push(prefix + key);
|
||||
}
|
||||
};
|
||||
visit(health, '', 0);
|
||||
const base = `état ${String(health.status)}`;
|
||||
return failing.length ? `${base} · ${failing.slice(0, 4).join(', ')}` : base;
|
||||
}
|
||||
@@ -125,6 +125,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
volume: 1,
|
||||
localModel: 'kokoro-v1',
|
||||
localSpeaker: 30,
|
||||
voiceGender: 'male',
|
||||
autoSpeak: true,
|
||||
speakIncoming: true
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user