mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-11 17:29:03 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
221bb7062c | ||
|
|
57690962e1 | ||
|
|
cf52a610d4 | ||
|
|
3a52cf3ae0 |
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
# 🚀 EveFlow — Compagnon de Bureau Rétro-Futuriste 3D
|
# 🚀 EveFlow — Compagnon de Bureau Rétro-Futuriste 3D
|
||||||
|
|
||||||
[](https://github.com/R0m1k3/EveFlow)
|
[](https://github.com/R0m1k3/EveFlow)
|
||||||
[](https://github.com/R0m1k3/EveFlow/releases/tag/v1.0.5)
|
[](https://github.com/R0m1k3/EveFlow/releases/tag/v1.0.7)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
**EveFlow** est un compagnon de bureau Windows immersif haut de gamme, combinant une esthétique cyberpunk rétro-futuriste soignée et des technologies d'intelligence artificielle avancées. Il intègre un assistant virtuel en 3D nommé **Eve**, animé en temps réel avec des expressions émotionnelles dynamiques et synchronisé avec des services de synthèse vocale (TTS/STT) locaux et des architectures multi-agents.
|
**EveFlow** est un compagnon de bureau Windows immersif haut de gamme, combinant une esthétique cyberpunk rétro-futuriste soignée et des technologies d'intelligence artificielle avancées. Il intègre un assistant virtuel en 3D nommé **Eve**, animé en temps réel avec des expressions émotionnelles dynamiques et synchronisé avec des services de synthèse vocale (TTS/STT) locaux et des architectures multi-agents.
|
||||||
@@ -74,7 +74,7 @@ Pour générer l'exécutable d'installation Windows (`.exe`) autonome :
|
|||||||
```bash
|
```bash
|
||||||
npm run dist
|
npm run dist
|
||||||
```
|
```
|
||||||
*L'exécutable d'installation NSIS (`EveFlow Setup 1.0.5.exe`) et la build décompressée seront générés dans le dossier `./out/`.*
|
*L'exécutable d'installation NSIS (`EveFlow Setup 1.0.7.exe`) et la build décompressée seront générés dans le dossier `./out/`.*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "eveflow",
|
"name": "eveflow",
|
||||||
"version": "1.0.5",
|
"version": "1.0.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "eveflow",
|
"name": "eveflow",
|
||||||
"version": "1.0.5",
|
"version": "1.0.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-three/drei": "^9.105.0",
|
"@react-three/drei": "^9.105.0",
|
||||||
"@react-three/fiber": "^8.16.0",
|
"@react-three/fiber": "^8.16.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "eveflow",
|
"name": "eveflow",
|
||||||
"version": "1.0.5",
|
"version": "1.0.7",
|
||||||
"description": "Retro-futuristic Windows companion with 3D Eve bot, local TTS/STT, and multi-agent connectivity",
|
"description": "Retro-futuristic Windows companion with 3D Eve bot, local TTS/STT, and multi-agent connectivity",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+9
-2
@@ -898,7 +898,7 @@ export const App: React.FC = () => {
|
|||||||
const [sttProvider, setSttProvider] = useState<'browser' | 'qwen3-asr'>('browser');
|
const [sttProvider, setSttProvider] = useState<'browser' | 'qwen3-asr'>('browser');
|
||||||
const [sttApiUrl, setSttApiUrl] = useState<string>('http://127.0.0.1:8000/v1');
|
const [sttApiUrl, setSttApiUrl] = useState<string>('http://127.0.0.1:8000/v1');
|
||||||
const [sttApiKey, setSttApiKey] = useState<string>('');
|
const [sttApiKey, setSttApiKey] = useState<string>('');
|
||||||
const [sttModel, setSttModel] = useState<string>('qwen3-asr');
|
const [sttModel, setSttModel] = useState<string>('Qwen/Qwen3-ASR-0.6B');
|
||||||
|
|
||||||
const [ttsApiUrl, setTtsApiUrl] = useState<string>('http://127.0.0.1:8000/v1');
|
const [ttsApiUrl, setTtsApiUrl] = useState<string>('http://127.0.0.1:8000/v1');
|
||||||
const [ttsApiKey, setTtsApiKey] = useState<string>('');
|
const [ttsApiKey, setTtsApiKey] = useState<string>('');
|
||||||
@@ -1187,7 +1187,14 @@ export const App: React.FC = () => {
|
|||||||
if (val) setSttApiKey(val);
|
if (val) setSttApiKey(val);
|
||||||
});
|
});
|
||||||
persistRead('eveflow_stt_model').then(val => {
|
persistRead('eveflow_stt_model').then(val => {
|
||||||
if (val) setSttModel(val);
|
if (val) {
|
||||||
|
if (val === 'qwen3-asr') {
|
||||||
|
setSttModel('Qwen/Qwen3-ASR-0.6B');
|
||||||
|
persistWrite('eveflow_stt_model', 'Qwen/Qwen3-ASR-0.6B');
|
||||||
|
} else {
|
||||||
|
setSttModel(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
persistRead('eveflow_tts_api_url').then(val => {
|
persistRead('eveflow_tts_api_url').then(val => {
|
||||||
if (val) setTtsApiUrl(val);
|
if (val) setTtsApiUrl(val);
|
||||||
|
|||||||
+158
-53
@@ -63,9 +63,21 @@ export class AudioService {
|
|||||||
private sttProvider: 'browser' | 'qwen3-asr' = 'browser';
|
private sttProvider: 'browser' | 'qwen3-asr' = 'browser';
|
||||||
private sttApiUrl = 'http://127.0.0.1:8000/v1';
|
private sttApiUrl = 'http://127.0.0.1:8000/v1';
|
||||||
private sttApiKey = '';
|
private sttApiKey = '';
|
||||||
private sttModel = 'qwen3-asr';
|
private sttModel = 'Qwen/Qwen3-ASR-0.6B';
|
||||||
private mediaRecorder: MediaRecorder | null = null;
|
|
||||||
private audioChunks: Blob[] = [];
|
// Propriétés pour l'enregistrement WAV direct
|
||||||
|
private audioContext: AudioContext | null = null;
|
||||||
|
private scriptProcessor: ScriptProcessorNode | null = null;
|
||||||
|
private micStream: MediaStream | null = null;
|
||||||
|
private leftChannel: Float32Array[] = [];
|
||||||
|
private recordingLength = 0;
|
||||||
|
private sampleRate = 44100;
|
||||||
|
|
||||||
|
// Callbacks pour l'écoute
|
||||||
|
private activeOnResult: ((text: string) => void) | null = null;
|
||||||
|
private activeOnEnd: (() => void) | null = null;
|
||||||
|
private activeOnError: ((err: string) => void) | null = null;
|
||||||
|
private activeLang = 'fr-FR';
|
||||||
|
|
||||||
private ttsApiUrl = 'http://127.0.0.1:8000/v1';
|
private ttsApiUrl = 'http://127.0.0.1:8000/v1';
|
||||||
private ttsApiKey = '';
|
private ttsApiKey = '';
|
||||||
@@ -584,7 +596,7 @@ export class AudioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Démarrer l'écoute vocale API via MediaRecorder
|
// Démarrer l'écoute vocale API via AudioContext (WAV Recorder)
|
||||||
private async startListeningAPI(
|
private async startListeningAPI(
|
||||||
onResult: (text: string) => void,
|
onResult: (text: string) => void,
|
||||||
onStart: () => void,
|
onStart: () => void,
|
||||||
@@ -596,59 +608,156 @@ export class AudioService {
|
|||||||
this.stopListening();
|
this.stopListening();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assigner les callbacks pour y avoir accès à l'arrêt
|
||||||
|
this.activeOnResult = onResult;
|
||||||
|
this.activeOnEnd = onEnd;
|
||||||
|
this.activeOnError = onError;
|
||||||
|
this.activeLang = lang;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
this.audioChunks = [];
|
this.micStream = stream;
|
||||||
|
this.leftChannel = [];
|
||||||
|
this.recordingLength = 0;
|
||||||
|
|
||||||
let options = {};
|
const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
|
||||||
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
|
this.audioContext = new AudioCtx();
|
||||||
options = { mimeType: 'audio/webm;codecs=opus' };
|
this.sampleRate = this.audioContext.sampleRate;
|
||||||
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
|
|
||||||
options = { mimeType: 'audio/webm' };
|
const source = this.audioContext.createMediaStreamSource(stream);
|
||||||
}
|
// bufferSize de 2048, 1 canal d'entrée, 1 de sortie
|
||||||
|
this.scriptProcessor = this.audioContext.createScriptProcessor(2048, 1, 1);
|
||||||
|
|
||||||
|
this.scriptProcessor.onaudioprocess = (e) => {
|
||||||
|
if (!this.isListeningActive) return;
|
||||||
|
const inputData = e.inputBuffer.getChannelData(0);
|
||||||
|
// Cloner les échantillons
|
||||||
|
this.leftChannel.push(new Float32Array(inputData));
|
||||||
|
this.recordingLength += inputData.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
source.connect(this.scriptProcessor);
|
||||||
|
this.scriptProcessor.connect(this.audioContext.destination);
|
||||||
|
|
||||||
this.mediaRecorder = new MediaRecorder(stream, options);
|
|
||||||
this.isListeningActive = true;
|
this.isListeningActive = true;
|
||||||
|
onStart();
|
||||||
this.mediaRecorder.ondataavailable = (event) => {
|
|
||||||
if (event.data && event.data.size > 0) {
|
|
||||||
this.audioChunks.push(event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.mediaRecorder.onstart = () => {
|
|
||||||
onStart();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.mediaRecorder.onstop = async () => {
|
|
||||||
this.isListeningActive = false;
|
|
||||||
onEnd();
|
|
||||||
|
|
||||||
// Stopper toutes les pistes pour éteindre le voyant du microphone
|
|
||||||
stream.getTracks().forEach(track => track.stop());
|
|
||||||
|
|
||||||
if (this.audioChunks.length === 0) {
|
|
||||||
onError("Aucune donnée audio capturée");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioBlob = new Blob(this.audioChunks, { type: this.mediaRecorder?.mimeType || 'audio/webm' });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const text = await this.transcribeAudioWithAPI(audioBlob, lang);
|
|
||||||
onResult(text);
|
|
||||||
} catch (err: any) {
|
|
||||||
onError(err.message || "Erreur de transcription de l'API");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.mediaRecorder.start();
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.isListeningActive = false;
|
this.isListeningActive = false;
|
||||||
|
this.logError("STT", "Impossible d'accéder au microphone", err);
|
||||||
onError(err.message || "Impossible d'accéder au microphone.");
|
onError(err.message || "Impossible d'accéder au microphone.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Arrêter proprement la capture AudioContext et exporter en WAV
|
||||||
|
private stopListeningAPI() {
|
||||||
|
if (!this.isListeningActive) return;
|
||||||
|
this.isListeningActive = false;
|
||||||
|
|
||||||
|
// Déconnecter le processeur
|
||||||
|
if (this.scriptProcessor) {
|
||||||
|
this.scriptProcessor.disconnect();
|
||||||
|
this.scriptProcessor.onaudioprocess = null;
|
||||||
|
this.scriptProcessor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrêter le flux micro
|
||||||
|
if (this.micStream) {
|
||||||
|
this.micStream.getTracks().forEach(track => track.stop());
|
||||||
|
this.micStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fermer l'audio context
|
||||||
|
if (this.audioContext) {
|
||||||
|
this.audioContext.close();
|
||||||
|
this.audioContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEndCallback = this.activeOnEnd;
|
||||||
|
const onResultCallback = this.activeOnResult;
|
||||||
|
const onErrorCallback = this.activeOnError;
|
||||||
|
const lang = this.activeLang;
|
||||||
|
|
||||||
|
if (onEndCallback) onEndCallback();
|
||||||
|
|
||||||
|
if (this.recordingLength === 0) {
|
||||||
|
if (onErrorCallback) onErrorCallback("Aucune donnée audio capturée");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporter le tampon audio en fichier WAV
|
||||||
|
const wavBlob = this.exportWAV(this.leftChannel, this.recordingLength, this.sampleRate);
|
||||||
|
|
||||||
|
this.transcribeAudioWithAPI(wavBlob, lang)
|
||||||
|
.then(text => {
|
||||||
|
if (onResultCallback) onResultCallback(text);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.logError("STT", "Erreur de transcription", err);
|
||||||
|
if (onErrorCallback) onErrorCallback(err.message || "Erreur de transcription de l'API");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonctions utilitaires pour générer le conteneur WAV PCM 16-bit
|
||||||
|
private exportWAV(channelData: Float32Array[], recordingLength: number, sampleRate: number): Blob {
|
||||||
|
const buffer = this.mergeBuffers(channelData, recordingLength);
|
||||||
|
const wavBuffer = new ArrayBuffer(44 + buffer.length * 2);
|
||||||
|
const view = new DataView(wavBuffer);
|
||||||
|
|
||||||
|
/* Identifiant RIFF */
|
||||||
|
this.writeString(view, 0, 'RIFF');
|
||||||
|
/* Taille du fichier */
|
||||||
|
view.setUint32(4, 36 + buffer.length * 2, true);
|
||||||
|
/* Type RIFF */
|
||||||
|
this.writeString(view, 8, 'WAVE');
|
||||||
|
/* Identifiant format fmt */
|
||||||
|
this.writeString(view, 12, 'fmt ');
|
||||||
|
/* Longueur du chunk format */
|
||||||
|
view.setUint32(16, 16, true);
|
||||||
|
/* Format d'encodage (1 = PCM non compressé) */
|
||||||
|
view.setUint16(20, 1, true);
|
||||||
|
/* Nombre de canaux (1 = Mono) */
|
||||||
|
view.setUint16(22, 1, true);
|
||||||
|
/* Fréquence d'échantillonnage */
|
||||||
|
view.setUint32(24, sampleRate, true);
|
||||||
|
/* Débit d'octets (sampleRate * blockAlign) */
|
||||||
|
view.setUint32(28, sampleRate * 2, true);
|
||||||
|
/* Block align (1 canal * 2 octets par échantillon) */
|
||||||
|
view.setUint16(32, 2, true);
|
||||||
|
/* Bits par échantillon (16 bits) */
|
||||||
|
view.setUint16(34, 16, true);
|
||||||
|
/* Identifiant de données data */
|
||||||
|
this.writeString(view, 36, 'data');
|
||||||
|
/* Longueur des données audio */
|
||||||
|
view.setUint32(40, buffer.length * 2, true);
|
||||||
|
|
||||||
|
this.floatTo16BitPCM(view, 44, buffer);
|
||||||
|
|
||||||
|
return new Blob([view], { type: 'audio/wav' });
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeBuffers(channelData: Float32Array[], recordingLength: number): Float32Array {
|
||||||
|
const result = new Float32Array(recordingLength);
|
||||||
|
let offset = 0;
|
||||||
|
for (let i = 0; i < channelData.length; i++) {
|
||||||
|
result.set(channelData[i], offset);
|
||||||
|
offset += channelData[i].length;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeString(view: DataView, offset: number, string: string) {
|
||||||
|
for (let i = 0; i < string.length; i++) {
|
||||||
|
view.setUint8(offset + i, string.charCodeAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private floatTo16BitPCM(output: DataView, offset: number, input: Float32Array) {
|
||||||
|
for (let i = 0; i < input.length; i++, offset += 2) {
|
||||||
|
const s = Math.max(-1, Math.min(1, input[i]));
|
||||||
|
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Requête API Speech-To-Text compatible OpenAI /v1/audio/transcriptions
|
// Requête API Speech-To-Text compatible OpenAI /v1/audio/transcriptions
|
||||||
private async transcribeAudioWithAPI(audioBlob: Blob, lang: string): Promise<string> {
|
private async transcribeAudioWithAPI(audioBlob: Blob, lang: string): Promise<string> {
|
||||||
if (!this.sttApiUrl) {
|
if (!this.sttApiUrl) {
|
||||||
@@ -666,9 +775,8 @@ export class AudioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const ext = audioBlob.type.includes('wav') ? 'wav' : 'webm';
|
formData.append('file', audioBlob, 'audio.wav');
|
||||||
formData.append('file', audioBlob, `audio.${ext}`);
|
formData.append('model', this.sttModel || 'Qwen/Qwen3-ASR-0.6B');
|
||||||
formData.append('model', this.sttModel || 'qwen3-asr');
|
|
||||||
|
|
||||||
if (lang) {
|
if (lang) {
|
||||||
const isoLang = lang.split('-')[0];
|
const isoLang = lang.split('-')[0];
|
||||||
@@ -756,10 +864,7 @@ export class AudioService {
|
|||||||
// Arrêter l'écoute vocale
|
// Arrêter l'écoute vocale
|
||||||
public stopListening() {
|
public stopListening() {
|
||||||
if (this.sttProvider === 'qwen3-asr') {
|
if (this.sttProvider === 'qwen3-asr') {
|
||||||
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
this.stopListeningAPI();
|
||||||
this.mediaRecorder.stop();
|
|
||||||
this.isListeningActive = false;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (this.recognition && this.isListeningActive) {
|
if (this.recognition && this.isListeningActive) {
|
||||||
this.recognition.stop();
|
this.recognition.stop();
|
||||||
|
|||||||
Reference in new issue
Block a user