mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-17 20:28:43 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5217eee53a |
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.7)
|
[](https://github.com/R0m1k3/EveFlow/releases/tag/v1.0.8)
|
||||||
[](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.7.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.8.exe`) et la build décompressée seront générés dans le dossier `./out/`.*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -275,6 +275,13 @@ app.whenReady().then(() => {
|
|||||||
callback(false);
|
callback(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
session.defaultSession.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||||
|
if (permission === 'media') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
startWebhookServer();
|
startWebhookServer();
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "eveflow",
|
"name": "eveflow",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "eveflow",
|
"name": "eveflow",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"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.7",
|
"version": "1.0.8",
|
||||||
"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": {
|
||||||
|
|||||||
+21
@@ -2543,6 +2543,27 @@ export const App: React.FC = () => {
|
|||||||
<div ref={compactMessagesEndRef} />
|
<div ref={compactMessagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: 'rgba(239, 68, 68, 0.12)',
|
||||||
|
border: '1px solid rgba(239, 68, 68, 0.35)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#ff6b6b',
|
||||||
|
padding: '6px 10px',
|
||||||
|
fontSize: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '6px',
|
||||||
|
alignItems: 'center',
|
||||||
|
boxShadow: '0 0 10px rgba(239, 68, 68, 0.05)'
|
||||||
|
}}>
|
||||||
|
<AlertCircle style={{ width: '13px', height: '13px', flexShrink: 0, color: '#ef4444' }} />
|
||||||
|
<div style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={errorMessage}>
|
||||||
|
<strong style={{ textTransform: 'uppercase', marginRight: '4px' }}>DÉFAILLANCE:</strong>
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Barre de saisie compacte */}
|
{/* Barre de saisie compacte */}
|
||||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', width: '100%', paddingBottom: '2px' }}>
|
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', width: '100%', paddingBottom: '2px' }}>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -113,6 +113,19 @@ export class AudioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private logInfo(tag: string, message: string, data?: any) {
|
||||||
|
console.log(`[${tag}] ${message}`, data);
|
||||||
|
if ((window as any).electronAPI?.writeLog) {
|
||||||
|
(window as any).electronAPI.writeLog({
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
level: 'INFO',
|
||||||
|
tag,
|
||||||
|
message,
|
||||||
|
data: data ? { message: data.message, stack: data.stack, ...data } : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.ttsSynth = window.speechSynthesis;
|
this.ttsSynth = window.speechSynthesis;
|
||||||
this.initSTT();
|
this.initSTT();
|
||||||
@@ -622,7 +635,18 @@ export class AudioService {
|
|||||||
|
|
||||||
const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
|
const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
|
||||||
this.audioContext = new AudioCtx();
|
this.audioContext = new AudioCtx();
|
||||||
|
|
||||||
|
// Assurer que l'AudioContext est actif (non suspendu)
|
||||||
|
if (this.audioContext.state === 'suspended') {
|
||||||
|
this.logWarn("STT", "AudioContext initialisé à l'état suspendu. Tentative de reprise...");
|
||||||
|
await this.audioContext.resume();
|
||||||
|
}
|
||||||
|
|
||||||
this.sampleRate = this.audioContext.sampleRate;
|
this.sampleRate = this.audioContext.sampleRate;
|
||||||
|
this.logInfo("STT", "AudioContext démarré avec succès", {
|
||||||
|
state: this.audioContext.state,
|
||||||
|
sampleRate: this.sampleRate
|
||||||
|
});
|
||||||
|
|
||||||
const source = this.audioContext.createMediaStreamSource(stream);
|
const source = this.audioContext.createMediaStreamSource(stream);
|
||||||
// bufferSize de 2048, 1 canal d'entrée, 1 de sortie
|
// bufferSize de 2048, 1 canal d'entrée, 1 de sortie
|
||||||
@@ -687,6 +711,12 @@ export class AudioService {
|
|||||||
// Exporter le tampon audio en fichier WAV
|
// Exporter le tampon audio en fichier WAV
|
||||||
const wavBlob = this.exportWAV(this.leftChannel, this.recordingLength, this.sampleRate);
|
const wavBlob = this.exportWAV(this.leftChannel, this.recordingLength, this.sampleRate);
|
||||||
|
|
||||||
|
this.logInfo("STT", "Arrêt de l'écoute, exportation WAV...", {
|
||||||
|
recordingLength: this.recordingLength,
|
||||||
|
sampleRate: this.sampleRate,
|
||||||
|
wavSize: wavBlob.size
|
||||||
|
});
|
||||||
|
|
||||||
this.transcribeAudioWithAPI(wavBlob, lang)
|
this.transcribeAudioWithAPI(wavBlob, lang)
|
||||||
.then(text => {
|
.then(text => {
|
||||||
if (onResultCallback) onResultCallback(text);
|
if (onResultCallback) onResultCallback(text);
|
||||||
@@ -699,14 +729,19 @@ export class AudioService {
|
|||||||
|
|
||||||
// Fonctions utilitaires pour générer le conteneur WAV PCM 16-bit
|
// Fonctions utilitaires pour générer le conteneur WAV PCM 16-bit
|
||||||
private exportWAV(channelData: Float32Array[], recordingLength: number, sampleRate: number): Blob {
|
private exportWAV(channelData: Float32Array[], recordingLength: number, sampleRate: number): Blob {
|
||||||
const buffer = this.mergeBuffers(channelData, recordingLength);
|
const originalBuffer = this.mergeBuffers(channelData, recordingLength);
|
||||||
const wavBuffer = new ArrayBuffer(44 + buffer.length * 2);
|
|
||||||
|
// Rééchantillonner à 16000 Hz pour la compatibilité avec l'API ASR (Whisper / Qwen3-ASR)
|
||||||
|
const targetSampleRate = 16000;
|
||||||
|
const resampledBuffer = this.downsampleBuffer(originalBuffer, sampleRate, targetSampleRate);
|
||||||
|
|
||||||
|
const wavBuffer = new ArrayBuffer(44 + resampledBuffer.length * 2);
|
||||||
const view = new DataView(wavBuffer);
|
const view = new DataView(wavBuffer);
|
||||||
|
|
||||||
/* Identifiant RIFF */
|
/* Identifiant RIFF */
|
||||||
this.writeString(view, 0, 'RIFF');
|
this.writeString(view, 0, 'RIFF');
|
||||||
/* Taille du fichier */
|
/* Taille du fichier */
|
||||||
view.setUint32(4, 36 + buffer.length * 2, true);
|
view.setUint32(4, 36 + resampledBuffer.length * 2, true);
|
||||||
/* Type RIFF */
|
/* Type RIFF */
|
||||||
this.writeString(view, 8, 'WAVE');
|
this.writeString(view, 8, 'WAVE');
|
||||||
/* Identifiant format fmt */
|
/* Identifiant format fmt */
|
||||||
@@ -718,9 +753,9 @@ export class AudioService {
|
|||||||
/* Nombre de canaux (1 = Mono) */
|
/* Nombre de canaux (1 = Mono) */
|
||||||
view.setUint16(22, 1, true);
|
view.setUint16(22, 1, true);
|
||||||
/* Fréquence d'échantillonnage */
|
/* Fréquence d'échantillonnage */
|
||||||
view.setUint32(24, sampleRate, true);
|
view.setUint32(24, targetSampleRate, true);
|
||||||
/* Débit d'octets (sampleRate * blockAlign) */
|
/* Débit d'octets (targetSampleRate * blockAlign) */
|
||||||
view.setUint32(28, sampleRate * 2, true);
|
view.setUint32(28, targetSampleRate * 2, true);
|
||||||
/* Block align (1 canal * 2 octets par échantillon) */
|
/* Block align (1 canal * 2 octets par échantillon) */
|
||||||
view.setUint16(32, 2, true);
|
view.setUint16(32, 2, true);
|
||||||
/* Bits par échantillon (16 bits) */
|
/* Bits par échantillon (16 bits) */
|
||||||
@@ -728,13 +763,49 @@ export class AudioService {
|
|||||||
/* Identifiant de données data */
|
/* Identifiant de données data */
|
||||||
this.writeString(view, 36, 'data');
|
this.writeString(view, 36, 'data');
|
||||||
/* Longueur des données audio */
|
/* Longueur des données audio */
|
||||||
view.setUint32(40, buffer.length * 2, true);
|
view.setUint32(40, resampledBuffer.length * 2, true);
|
||||||
|
|
||||||
this.floatTo16BitPCM(view, 44, buffer);
|
this.floatTo16BitPCM(view, 44, resampledBuffer);
|
||||||
|
|
||||||
return new Blob([view], { type: 'audio/wav' });
|
return new Blob([view], { type: 'audio/wav' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private downsampleBuffer(buffer: Float32Array, originalSampleRate: number, targetSampleRate: number): Float32Array {
|
||||||
|
if (originalSampleRate === targetSampleRate) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sampleRateRatio = originalSampleRate / targetSampleRate;
|
||||||
|
const newLength = Math.round(buffer.length / sampleRateRatio);
|
||||||
|
const result = new Float32Array(newLength);
|
||||||
|
|
||||||
|
let offsetResult = 0;
|
||||||
|
let offsetBuffer = 0;
|
||||||
|
|
||||||
|
while (offsetResult < result.length) {
|
||||||
|
const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
|
||||||
|
|
||||||
|
// Moyennage simple (box filter) pour éviter l'aliasing
|
||||||
|
let accum = 0;
|
||||||
|
let count = 0;
|
||||||
|
for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
|
||||||
|
accum += buffer[i];
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
result[offsetResult] = accum / count;
|
||||||
|
} else {
|
||||||
|
result[offsetResult] = buffer[Math.min(offsetBuffer, buffer.length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetResult++;
|
||||||
|
offsetBuffer = nextOffsetBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private mergeBuffers(channelData: Float32Array[], recordingLength: number): Float32Array {
|
private mergeBuffers(channelData: Float32Array[], recordingLength: number): Float32Array {
|
||||||
const result = new Float32Array(recordingLength);
|
const result = new Float32Array(recordingLength);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|||||||
Reference in new issue
Block a user