mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-11 17:29:03 +02:00
Over the runs transport Hermes never reports the session it attached to a
run in the SSE stream, only in GET /v1/runs/{id}. EveFlow only read it in the
polling fallback, so the locally generated id was sent again and again and
each message opened a fresh Hermes session, losing the conversation context.
The client now adopts the run's session id after every run.
Long answers are now spoken as a digest: the first sentences (configurable,
4 by default) plus the closing question, followed by a short notice that the
full text is on screen. The default instructions ask Hermes to open long
replies with the essentials and to rely on the ongoing conversation.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ln4KL1feHtnV4nee4sZFhM
61 lines
2.9 KiB
TypeScript
61 lines
2.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { chunkForSpeech, cleanForSpeech, closingQuestion, extractSentences, isTranscriptNoise, preprocessMedia, spokenDigest } from '../src/lib/text';
|
|
|
|
describe('text', () => {
|
|
it('cleans markdown for speech', () => {
|
|
const out = cleanForSpeech('**Bonjour** ! Voir [le site](https://x.io) et `npm` 😀\n\n```js\nconsole.log(1)\n```\n- item un\n- item deux');
|
|
expect(out).not.toMatch(/\*|https|```|😀/);
|
|
expect(out).toContain('Bonjour');
|
|
expect(out).toContain('item un');
|
|
});
|
|
|
|
it('extracts complete sentences from a stream buffer', () => {
|
|
const { sentences, rest } = extractSentences('Première phrase complète. Deuxième phrase aussi ! Troisième en cours');
|
|
expect(sentences).toEqual(['Première phrase complète.', 'Deuxième phrase aussi !']);
|
|
expect(rest).toBe(' Troisième en cours');
|
|
});
|
|
|
|
it('does not split inside an open code fence', () => {
|
|
expect(extractSentences('Voici. ```js\nfoo(). bar\n').sentences).toEqual([]);
|
|
});
|
|
|
|
it('chunks long text', () => {
|
|
const chunks = chunkForSpeech('mot '.repeat(200), 100);
|
|
expect(chunks.every((c) => c.length <= 100)).toBe(true);
|
|
expect(preprocessMedia('MEDIA:/tmp/a.png')).toContain('');
|
|
});
|
|
});
|
|
|
|
describe('isTranscriptNoise', () => {
|
|
it('drops Whisper hallucinations on silence', () => {
|
|
expect(isTranscriptNoise('(cliquant)')).toBe(true);
|
|
expect(isTranscriptNoise('*Claire*')).toBe(true);
|
|
expect(isTranscriptNoise('[Musique]')).toBe(true);
|
|
expect(isTranscriptNoise('...')).toBe(true);
|
|
expect(isTranscriptNoise("Sous-titres réalisés par la communauté d'Amara.org")).toBe(true);
|
|
});
|
|
it('keeps real sentences', () => {
|
|
expect(isTranscriptNoise('Jarvis, allume la lumière du salon.')).toBe(false);
|
|
expect(isTranscriptNoise('(Jarvis) quelle heure est-il maintenant ?')).toBe(false);
|
|
expect(isTranscriptNoise('Oui')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('spokenDigest', () => {
|
|
const reply = 'Voici la réponse. Elle contient plusieurs phrases. Une troisième pour la forme. Et une quatrième.\n\n```js\nconsole.log(1)\n```\n\nTu veux que je continue ?';
|
|
it('speaks the first sentences and keeps the closing question', () => {
|
|
const digest = spokenDigest(reply, 2);
|
|
expect(digest.truncated).toBe(true);
|
|
expect(digest.text).toBe('Voici la réponse. Elle contient plusieurs phrases. Tu veux que je continue ?');
|
|
});
|
|
it('leaves short replies untouched', () => {
|
|
const digest = spokenDigest('Bonjour Michael. Tout va bien.', 4);
|
|
expect(digest).toEqual({ text: 'Bonjour Michael. Tout va bien.', truncated: false });
|
|
});
|
|
it('ignores code blocks when counting and only reports a real question', () => {
|
|
expect(spokenDigest('Une phrase.\n\n```\ncode\n```\n', 1).truncated).toBe(false);
|
|
expect(closingQuestion(reply)).toBe('Tu veux que je continue ?');
|
|
expect(closingQuestion('Aucune question ici.')).toBeNull();
|
|
});
|
|
});
|