mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-11 17:29:03 +02:00
feat: voix Edge neuronales, Supertonic 3 en local et genre respecté par tous les moteurs (v2.5.0)
- Nouveau moteur « Microsoft Edge » (voix neuronales gratuites, sans clé) : Henri / Denise par défaut selon le genre, liste des voix fr-FR / fr-CA / fr-CH / fr-BE, WebSocket signé (Sec-MS-GEC) dans le processus principal, MP3 24 kHz. Moteur par défaut des nouvelles installations. - Supertonic 3 ajouté au catalogue local (31 langues, 5 voix masculines + 5 féminines, 44 kHz, 129 Mo) : langue transmise au worker, genres des voix vérifiés par mesure de F0. - Kokoro déclassé pour le français (une seule voix féminine, accent) ; le choix masculin/féminin bascule sur le meilleur modèle installé et conserve le genre quand on change de modèle ; Google Translate signalé comme voix féminine uniquement. - Deux préréglages JARVIS : en ligne (Edge Henri) et hors ligne (Supertonic 3). - Traitement du micro par Chromium (écho, bruit, gain) débrayable pour de meilleures transcriptions au casque. - Tests : protocole Edge (jeton, SSML, trames), sélection de voix ; README et feuille de route (mesures Supertonic, Parakeet vs Qwen3-ASR). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMpgFriwxiBgurUVb21oCE
This commit is contained in:
23 files changed
+735
-81
No files matched your search
@@ -13,6 +13,9 @@ import type {
|
||||
} from './ipc';
|
||||
import type { McpToolRequest, McpToolResponse } from './ipc';
|
||||
import type {
|
||||
EdgeSynthesizeRequest,
|
||||
EdgeSynthesizeResult,
|
||||
EdgeVoice,
|
||||
KwsDetection,
|
||||
KwsStartRequest,
|
||||
VadEvent,
|
||||
@@ -75,6 +78,9 @@ export interface EveFlowBridge {
|
||||
onProgress: (cb: (progress: VoiceDownloadProgress) => void) => Unsubscribe;
|
||||
transcribe: (req: TranscribeRequest) => Promise<TranscribeResult>;
|
||||
synthesize: (req: SynthesizeRequest) => Promise<SynthesizeResult>;
|
||||
/** Microsoft Edge neural voices (online, free): MP3 for one sentence. */
|
||||
edgeSynthesize: (req: EdgeSynthesizeRequest) => Promise<EdgeSynthesizeResult>;
|
||||
edgeVoices: () => Promise<EdgeVoice[]>;
|
||||
unload: (id?: string) => Promise<unknown>;
|
||||
kwsStart: (req: KwsStartRequest) => Promise<{ accepted: string[]; rejected: string[] }>;
|
||||
kwsStop: () => Promise<void>;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Pure helpers for the Microsoft Edge "Read aloud" speech service (the endpoint used by the
|
||||
* Edge browser, no API key): request signing input, SSML building, frame parsing and the
|
||||
* default French voices. No Node or DOM dependency so it is shared by main and tests.
|
||||
*/
|
||||
|
||||
export const EDGE_TRUSTED_CLIENT_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';
|
||||
export const EDGE_CHROMIUM_VERSION = '143.0.3650.75';
|
||||
export const EDGE_WSS_URL = `wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=${EDGE_TRUSTED_CLIENT_TOKEN}`;
|
||||
export const EDGE_VOICES_URL = `https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken=${EDGE_TRUSTED_CLIENT_TOKEN}`;
|
||||
export const EDGE_OUTPUT_FORMAT = 'audio-24khz-48kbitrate-mono-mp3';
|
||||
|
||||
const WIN_EPOCH_SEC = 11644473600;
|
||||
|
||||
/**
|
||||
* String whose SHA-256 (upper-case hex) is the `Sec-MS-GEC` value: Windows file time (100 ns
|
||||
* ticks since 1601) rounded down to 5 minutes, followed by the trusted client token.
|
||||
* `nowMs` is the client clock, `skewSec` the correction learned from the server's Date header.
|
||||
*/
|
||||
export function edgeTokenInput(nowMs: number, skewSec = 0): string {
|
||||
let seconds = Math.floor(nowMs / 1000 + skewSec) + WIN_EPOCH_SEC;
|
||||
seconds -= seconds % 300;
|
||||
// 10 million ticks per second; BigInt keeps the 18-digit value exact.
|
||||
const ticks = BigInt(seconds) * 10_000_000n;
|
||||
return `${ticks}${EDGE_TRUSTED_CLIENT_TOKEN}`;
|
||||
}
|
||||
|
||||
export function edgeHeaders(): Record<string, string> {
|
||||
const major = EDGE_CHROMIUM_VERSION.split('.')[0];
|
||||
return {
|
||||
'User-Agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36 Edg/${major}.0.0.0`,
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
Pragma: 'no-cache',
|
||||
'Cache-Control': 'no-cache',
|
||||
Origin: 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold'
|
||||
};
|
||||
}
|
||||
|
||||
export function escapeXml(text: string): string {
|
||||
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/** Prosody rate attribute for a playback speed multiplier (1 = "+0%", 1.25 = "+25%"). */
|
||||
export function edgeRate(speed: number): string {
|
||||
const pct = Math.round((Math.max(0.5, Math.min(2, speed || 1)) - 1) * 100);
|
||||
return `${pct >= 0 ? '+' : ''}${pct}%`;
|
||||
}
|
||||
|
||||
export function edgeSsml(text: string, voice: string, speed: number): string {
|
||||
const lang = voice.split('-').slice(0, 2).join('-') || 'fr-FR';
|
||||
return (
|
||||
`<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='${lang}'>` +
|
||||
`<voice name='${escapeXml(voice)}'><prosody pitch='+0Hz' rate='${edgeRate(speed)}' volume='+0%'>${escapeXml(text)}</prosody></voice></speak>`
|
||||
);
|
||||
}
|
||||
|
||||
/** Timestamp header the service expects ("JavaScript date string"). */
|
||||
export function edgeTimestamp(date = new Date()): string {
|
||||
return date.toUTCString().replace('GMT', 'GMT+0000 (Coordinated Universal Time)');
|
||||
}
|
||||
|
||||
export function edgeConfigMessage(date = new Date()): string {
|
||||
return (
|
||||
`X-Timestamp:${edgeTimestamp(date)}\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n` +
|
||||
`{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":"false","wordBoundaryEnabled":"false"},"outputFormat":"${EDGE_OUTPUT_FORMAT}"}}}}`
|
||||
);
|
||||
}
|
||||
|
||||
export function edgeSsmlMessage(requestId: string, ssml: string, date = new Date()): string {
|
||||
return `X-RequestId:${requestId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${edgeTimestamp(date)}Z\r\nPath:ssml\r\n\r\n${ssml}`;
|
||||
}
|
||||
|
||||
/** Split a binary frame into its text headers and payload (2-byte big-endian header length). */
|
||||
export function parseEdgeBinaryFrame(frame: Uint8Array): { path: string; payload: Uint8Array } {
|
||||
if (frame.byteLength < 2) return { path: '', payload: new Uint8Array(0) };
|
||||
const headerLength = (frame[0] << 8) | frame[1];
|
||||
const end = Math.min(frame.byteLength, 2 + headerLength);
|
||||
let header = '';
|
||||
for (let i = 2; i < end; i++) header += String.fromCharCode(frame[i]);
|
||||
const path = /Path:\s*([^\r\n]+)/i.exec(header)?.[1]?.trim() ?? '';
|
||||
return { path, payload: frame.subarray(end) };
|
||||
}
|
||||
|
||||
/** Path of a text frame ("turn.start", "response", "audio.metadata", "turn.end"). */
|
||||
export function edgeTextFramePath(message: string): string {
|
||||
return /Path:\s*([^\r\n]+)/i.exec(message)?.[1]?.trim() ?? '';
|
||||
}
|
||||
|
||||
export function edgeConnectionId(hex32: string): string {
|
||||
return hex32.replace(/-/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
/** Well-known voices per language so the choice works before the voice list is fetched. */
|
||||
export const EDGE_DEFAULT_VOICES: Record<string, { male: string; female: string }> = {
|
||||
fr: { male: 'fr-FR-HenriNeural', female: 'fr-FR-DeniseNeural' },
|
||||
en: { male: 'en-US-AndrewMultilingualNeural', female: 'en-US-AvaMultilingualNeural' },
|
||||
de: { male: 'de-DE-ConradNeural', female: 'de-DE-KatjaNeural' },
|
||||
es: { male: 'es-ES-AlvaroNeural', female: 'es-ES-ElviraNeural' },
|
||||
it: { male: 'it-IT-DiegoNeural', female: 'it-IT-ElsaNeural' },
|
||||
pt: { male: 'pt-BR-AntonioNeural', female: 'pt-BR-FranciscaNeural' }
|
||||
};
|
||||
|
||||
/** Default Edge voice for a language ("fr-FR", "fr", "en-GB") and gender; French when unknown. */
|
||||
export function defaultEdgeVoice(language: string, gender: 'male' | 'female'): string {
|
||||
const lang = (language || 'fr').toLowerCase().split(/[-_]/)[0];
|
||||
return (EDGE_DEFAULT_VOICES[lang] ?? EDGE_DEFAULT_VOICES.fr)[gender];
|
||||
}
|
||||
|
||||
/** Gender of an Edge voice from its short name, using the built-in table then common first names. */
|
||||
export function edgeVoiceGender(shortName: string): 'male' | 'female' | undefined {
|
||||
for (const pair of Object.values(EDGE_DEFAULT_VOICES)) {
|
||||
if (pair.male === shortName) return 'male';
|
||||
if (pair.female === shortName) return 'female';
|
||||
}
|
||||
const name = shortName.split('-')[2]?.replace(/(Multilingual)?Neural$/i, '') ?? '';
|
||||
if (/^(Henri|Remy|Rémy|Gerard|Antoine|Jean|Thierry|Fabrice|Claude|Andrew|Brian|Guy|Christopher|Eric|Roger|Steffan|Ryan|Thomas|Conrad|Alvaro|Diego|Antonio)$/i.test(name)) return 'male';
|
||||
if (/^(Denise|Eloise|Vivienne|Charline|Sylvie|Ariane|Ava|Emma|Jenny|Aria|Michelle|Ana|Sonia|Libby|Katja|Elvira|Elsa|Francisca)$/i.test(name)) return 'female';
|
||||
return undefined;
|
||||
}
|
||||
+28
-1
@@ -1,7 +1,7 @@
|
||||
/** Local voice engine contract (sherpa-onnx in a utility process). Shared by main and renderer. */
|
||||
|
||||
export type VoiceModelKind = 'stt' | 'tts' | 'kws' | 'vad';
|
||||
export type VoiceEngineKind = 'whisper' | 'sense-voice' | 'nemo-transducer' | 'kokoro' | 'piper' | 'kws-transducer' | 'silero';
|
||||
export type VoiceEngineKind = 'whisper' | 'sense-voice' | 'nemo-transducer' | 'kokoro' | 'piper' | 'supertonic' | 'kws-transducer' | 'silero';
|
||||
|
||||
export interface VoiceSpeaker {
|
||||
id: number;
|
||||
@@ -62,6 +62,31 @@ export interface SynthesizeRequest {
|
||||
text: string;
|
||||
speaker: number;
|
||||
speed: number;
|
||||
/** BCP-47 tag or 2-letter code of the text (multilingual engines such as Supertonic need it). */
|
||||
language?: string;
|
||||
}
|
||||
|
||||
/** Microsoft Edge "Read aloud" neural voices (online, no key). */
|
||||
export interface EdgeVoice {
|
||||
/** e.g. fr-FR-HenriNeural */
|
||||
shortName: string;
|
||||
/** Display name, e.g. Henri */
|
||||
name: string;
|
||||
locale: string;
|
||||
gender: 'm' | 'f';
|
||||
}
|
||||
|
||||
export interface EdgeSynthesizeRequest {
|
||||
text: string;
|
||||
voice: string;
|
||||
/** Playback speed multiplier (0.5 .. 2). */
|
||||
speed: number;
|
||||
}
|
||||
|
||||
export interface EdgeSynthesizeResult {
|
||||
/** MP3, 24 kHz mono 48 kbit/s. */
|
||||
mp3: Uint8Array;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface SynthesizeResult {
|
||||
@@ -115,6 +140,8 @@ export const VOICE_IPC = {
|
||||
modelsProgress: 'voice:models:progress',
|
||||
transcribe: 'voice:transcribe',
|
||||
synthesize: 'voice:synthesize',
|
||||
edgeSynthesize: 'voice:edge:synthesize',
|
||||
edgeVoices: 'voice:edge:voices',
|
||||
unload: 'voice:unload',
|
||||
kwsStart: 'voice:kws:start',
|
||||
kwsStop: 'voice:kws:stop',
|
||||
|
||||
Reference in new issue
Block a user