@meechi-ai/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +624 -0
- package/README.md +59 -0
- package/dist/components/CalendarView.d.ts +3 -0
- package/dist/components/CalendarView.js +72 -0
- package/dist/components/ChatInterface.d.ts +6 -0
- package/dist/components/ChatInterface.js +105 -0
- package/dist/components/FileExplorer.d.ts +9 -0
- package/dist/components/FileExplorer.js +757 -0
- package/dist/components/Icon.d.ts +9 -0
- package/dist/components/Icon.js +44 -0
- package/dist/components/SourceEditor.d.ts +13 -0
- package/dist/components/SourceEditor.js +50 -0
- package/dist/components/ThemeProvider.d.ts +5 -0
- package/dist/components/ThemeProvider.js +105 -0
- package/dist/components/ThemeSwitcher.d.ts +1 -0
- package/dist/components/ThemeSwitcher.js +16 -0
- package/dist/components/voice/VoiceInputArea.d.ts +14 -0
- package/dist/components/voice/VoiceInputArea.js +190 -0
- package/dist/components/voice/VoiceOverlay.d.ts +7 -0
- package/dist/components/voice/VoiceOverlay.js +71 -0
- package/dist/hooks/useMeechi.d.ts +16 -0
- package/dist/hooks/useMeechi.js +461 -0
- package/dist/hooks/useSync.d.ts +8 -0
- package/dist/hooks/useSync.js +87 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +22 -0
- package/dist/lib/ai/embeddings.d.ts +15 -0
- package/dist/lib/ai/embeddings.js +128 -0
- package/dist/lib/ai/gpu-lock.d.ts +19 -0
- package/dist/lib/ai/gpu-lock.js +43 -0
- package/dist/lib/ai/llm.worker.d.ts +1 -0
- package/dist/lib/ai/llm.worker.js +7 -0
- package/dist/lib/ai/local-llm.d.ts +30 -0
- package/dist/lib/ai/local-llm.js +211 -0
- package/dist/lib/ai/manager.d.ts +20 -0
- package/dist/lib/ai/manager.js +51 -0
- package/dist/lib/ai/parsing.d.ts +12 -0
- package/dist/lib/ai/parsing.js +56 -0
- package/dist/lib/ai/prompts.d.ts +2 -0
- package/dist/lib/ai/prompts.js +2 -0
- package/dist/lib/ai/providers/gemini.d.ts +6 -0
- package/dist/lib/ai/providers/gemini.js +88 -0
- package/dist/lib/ai/providers/groq.d.ts +6 -0
- package/dist/lib/ai/providers/groq.js +42 -0
- package/dist/lib/ai/registry.d.ts +29 -0
- package/dist/lib/ai/registry.js +52 -0
- package/dist/lib/ai/tools.d.ts +2 -0
- package/dist/lib/ai/tools.js +106 -0
- package/dist/lib/ai/types.d.ts +22 -0
- package/dist/lib/ai/types.js +1 -0
- package/dist/lib/ai/worker.d.ts +1 -0
- package/dist/lib/ai/worker.js +60 -0
- package/dist/lib/audio/input.d.ts +13 -0
- package/dist/lib/audio/input.js +121 -0
- package/dist/lib/audio/stt.d.ts +13 -0
- package/dist/lib/audio/stt.js +119 -0
- package/dist/lib/audio/tts.d.ts +12 -0
- package/dist/lib/audio/tts.js +128 -0
- package/dist/lib/audio/vad.d.ts +18 -0
- package/dist/lib/audio/vad.js +117 -0
- package/dist/lib/colors.d.ts +16 -0
- package/dist/lib/colors.js +67 -0
- package/dist/lib/extensions.d.ts +35 -0
- package/dist/lib/extensions.js +24 -0
- package/dist/lib/hooks/use-voice-loop.d.ts +13 -0
- package/dist/lib/hooks/use-voice-loop.js +313 -0
- package/dist/lib/mcp/McpClient.d.ts +19 -0
- package/dist/lib/mcp/McpClient.js +42 -0
- package/dist/lib/mcp/McpRegistry.d.ts +47 -0
- package/dist/lib/mcp/McpRegistry.js +117 -0
- package/dist/lib/mcp/native/GroqVoiceNative.d.ts +21 -0
- package/dist/lib/mcp/native/GroqVoiceNative.js +29 -0
- package/dist/lib/mcp/native/LocalSyncNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalSyncNative.js +26 -0
- package/dist/lib/mcp/native/LocalVoiceNative.d.ts +19 -0
- package/dist/lib/mcp/native/LocalVoiceNative.js +27 -0
- package/dist/lib/mcp/native/MeechiNativeCore.d.ts +25 -0
- package/dist/lib/mcp/native/MeechiNativeCore.js +209 -0
- package/dist/lib/mcp/native/index.d.ts +10 -0
- package/dist/lib/mcp/native/index.js +10 -0
- package/dist/lib/mcp/types.d.ts +35 -0
- package/dist/lib/mcp/types.js +1 -0
- package/dist/lib/pdf.d.ts +10 -0
- package/dist/lib/pdf.js +142 -0
- package/dist/lib/settings.d.ts +48 -0
- package/dist/lib/settings.js +87 -0
- package/dist/lib/storage/db.d.ts +57 -0
- package/dist/lib/storage/db.js +45 -0
- package/dist/lib/storage/local.d.ts +28 -0
- package/dist/lib/storage/local.js +534 -0
- package/dist/lib/storage/migrate.d.ts +3 -0
- package/dist/lib/storage/migrate.js +122 -0
- package/dist/lib/storage/types.d.ts +66 -0
- package/dist/lib/storage/types.js +1 -0
- package/dist/lib/sync/client-drive.d.ts +9 -0
- package/dist/lib/sync/client-drive.js +69 -0
- package/dist/lib/sync/engine.d.ts +18 -0
- package/dist/lib/sync/engine.js +517 -0
- package/dist/lib/sync/google-drive.d.ts +52 -0
- package/dist/lib/sync/google-drive.js +183 -0
- package/dist/lib/sync/merge.d.ts +1 -0
- package/dist/lib/sync/merge.js +68 -0
- package/dist/lib/yjs/YjsProvider.d.ts +11 -0
- package/dist/lib/yjs/YjsProvider.js +33 -0
- package/dist/lib/yjs/graph.d.ts +11 -0
- package/dist/lib/yjs/graph.js +7 -0
- package/dist/lib/yjs/hooks.d.ts +7 -0
- package/dist/lib/yjs/hooks.js +37 -0
- package/dist/lib/yjs/store.d.ts +4 -0
- package/dist/lib/yjs/store.js +19 -0
- package/dist/lib/yjs/syncGraph.d.ts +1 -0
- package/dist/lib/yjs/syncGraph.js +38 -0
- package/dist/providers/theme-provider.d.ts +3 -0
- package/dist/providers/theme-provider.js +18 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// NOTE: @xenova/transformers is imported dynamically to prevent SWC parse errors at build time.
|
|
2
|
+
export class TranscriberService {
|
|
3
|
+
// Allow injecting key from outside (e.g. settings)
|
|
4
|
+
static setCloudProvider(apiKey) {
|
|
5
|
+
this.cloudKey = apiKey;
|
|
6
|
+
}
|
|
7
|
+
static async getInstance() {
|
|
8
|
+
if (!this.instance) {
|
|
9
|
+
console.log(`Loading Metadata for ${this.modelId}...`);
|
|
10
|
+
// Dynamically import @xenova/transformers
|
|
11
|
+
const { pipeline, env } = await import('@xenova/transformers');
|
|
12
|
+
env.allowLocalModels = false;
|
|
13
|
+
env.useBrowserCache = true;
|
|
14
|
+
this.instance = await pipeline('automatic-speech-recognition', this.modelId, {
|
|
15
|
+
// quantized: true, // Disable quantization to debug "empty text" issue
|
|
16
|
+
});
|
|
17
|
+
console.log("Transcriber Model Loaded");
|
|
18
|
+
}
|
|
19
|
+
return this.instance;
|
|
20
|
+
}
|
|
21
|
+
static async transcribe(audio) {
|
|
22
|
+
if (audio.length === 0)
|
|
23
|
+
return "";
|
|
24
|
+
// Pass 1: Find Peak
|
|
25
|
+
let max = 0;
|
|
26
|
+
for (let i = 0; i < audio.length; i++) {
|
|
27
|
+
const abs = Math.abs(audio[i]);
|
|
28
|
+
if (abs > max)
|
|
29
|
+
max = abs;
|
|
30
|
+
}
|
|
31
|
+
// Pass 2: Normalize to 0.95 (Boost volume)
|
|
32
|
+
const normalized = new Float32Array(audio.length);
|
|
33
|
+
const scale = max > 0 ? 0.95 / max : 1;
|
|
34
|
+
console.log(`[STT] Normalizing Audio. Peak: ${max.toFixed(4)} -> Scale: ${scale.toFixed(4)}`);
|
|
35
|
+
for (let i = 0; i < audio.length; i++) {
|
|
36
|
+
normalized[i] = audio[i] * scale;
|
|
37
|
+
}
|
|
38
|
+
// CLOUD FAST PATH (GROQ)
|
|
39
|
+
// If we have a cloud key, use Groq Whisper for 50x speedup
|
|
40
|
+
if (this.cloudKey || process.env.NEXT_PUBLIC_GROQ_API_KEY) {
|
|
41
|
+
const key = this.cloudKey || process.env.NEXT_PUBLIC_GROQ_API_KEY;
|
|
42
|
+
console.log("[STT] Using Cloud Whisper (Groq) for speed...");
|
|
43
|
+
try {
|
|
44
|
+
// Convert Float32Array to WAV Blob
|
|
45
|
+
const wavBlob = await this.encodeWAV(normalized);
|
|
46
|
+
const formData = new FormData();
|
|
47
|
+
formData.append('file', wavBlob, 'audio.wav');
|
|
48
|
+
formData.append('model', 'whisper-large-v3'); // Groq supports large-v3
|
|
49
|
+
formData.append('response_format', 'json');
|
|
50
|
+
const res = await fetch('https://api.groq.com/openai/v1/audio/transcriptions', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Authorization': `Bearer ${key}`
|
|
54
|
+
},
|
|
55
|
+
body: formData
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok)
|
|
58
|
+
throw new Error(`Groq STT Error: ${res.status}`);
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
console.log("[STT-Cloud] Result:", data.text);
|
|
61
|
+
return data.text.trim();
|
|
62
|
+
}
|
|
63
|
+
catch (cloudErr) {
|
|
64
|
+
console.warn("[STT] Cloud failed, falling back to local:", cloudErr);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// FALLBACK: Local Xenova
|
|
68
|
+
try {
|
|
69
|
+
const transcriber = await this.getInstance();
|
|
70
|
+
// Simplified options to reduce potential conflicts
|
|
71
|
+
const output = await transcriber(normalized, {
|
|
72
|
+
language: 'english',
|
|
73
|
+
task: 'transcribe',
|
|
74
|
+
});
|
|
75
|
+
console.log("[STT-Local] Raw Output:", JSON.stringify(output, null, 2));
|
|
76
|
+
return output.text.trim();
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
console.error("Transcription Error:", e);
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Helper: Encode Float32 to WAV for API upload
|
|
84
|
+
static encodeWAV(samples, sampleRate = 16000) {
|
|
85
|
+
const buffer = new ArrayBuffer(44 + samples.length * 2);
|
|
86
|
+
const view = new DataView(buffer);
|
|
87
|
+
const writeString = (view, offset, string) => {
|
|
88
|
+
for (let i = 0; i < string.length; i++) {
|
|
89
|
+
view.setUint8(offset + i, string.charCodeAt(i));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
// RIFF chunk descriptor
|
|
93
|
+
writeString(view, 0, 'RIFF');
|
|
94
|
+
view.setUint32(4, 36 + samples.length * 2, true);
|
|
95
|
+
writeString(view, 8, 'WAVE');
|
|
96
|
+
writeString(view, 12, 'fmt ');
|
|
97
|
+
view.setUint32(16, 16, true);
|
|
98
|
+
view.setUint16(20, 1, true); // PCM
|
|
99
|
+
view.setUint16(22, 1, true); // Mono
|
|
100
|
+
view.setUint32(24, sampleRate, true);
|
|
101
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
102
|
+
view.setUint16(32, 2, true);
|
|
103
|
+
view.setUint16(34, 16, true);
|
|
104
|
+
writeString(view, 36, 'data');
|
|
105
|
+
view.setUint32(40, samples.length * 2, true);
|
|
106
|
+
// PCM Samples
|
|
107
|
+
let offset = 44;
|
|
108
|
+
for (let i = 0; i < samples.length; i++) {
|
|
109
|
+
let s = Math.max(-1, Math.min(1, samples[i]));
|
|
110
|
+
view.setInt16(offset, s * 0x7FFF, true);
|
|
111
|
+
offset += 2;
|
|
112
|
+
}
|
|
113
|
+
return new Blob([buffer], { type: 'audio/wav' });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
TranscriberService.instance = null;
|
|
117
|
+
// Upgrade to base.en for better accuracy/robustness than tiny
|
|
118
|
+
TranscriberService.modelId = 'Xenova/whisper-base.en';
|
|
119
|
+
TranscriberService.cloudKey = null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class SynthesizerService {
|
|
2
|
+
private static tts;
|
|
3
|
+
private static modelId;
|
|
4
|
+
private static dtype;
|
|
5
|
+
private static cloudKey;
|
|
6
|
+
static setCloudProvider(apiKey: string | null): void;
|
|
7
|
+
static init(): Promise<void>;
|
|
8
|
+
static speak(text: string): Promise<{
|
|
9
|
+
audio: Float32Array;
|
|
10
|
+
sampling_rate: number;
|
|
11
|
+
} | null>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// NOTE: KokoroTTS uses ONNX internally and @huggingface/transformers for env.
|
|
2
|
+
// We dynamically import from static vendor file to prevent SWC parse errors at build time.
|
|
3
|
+
export class SynthesizerService {
|
|
4
|
+
static setCloudProvider(apiKey) {
|
|
5
|
+
this.cloudKey = apiKey;
|
|
6
|
+
}
|
|
7
|
+
static async init() {
|
|
8
|
+
// If we have an OpenAI Key (or cloud key), we don't strictly NEED to load local TTS immediately.
|
|
9
|
+
// But we might want it as fallback.
|
|
10
|
+
// Let's lazy load: If speak() fails on cloud, it initializes local.
|
|
11
|
+
// So init() can be a no-op if cloud is ready, or run in background.
|
|
12
|
+
const hasCloud = !!(this.cloudKey || process.env.NEXT_PUBLIC_OPENAI_API_KEY);
|
|
13
|
+
if (!hasCloud) {
|
|
14
|
+
console.log("Initializing Local Kokoro TTS...");
|
|
15
|
+
if (!this.tts) {
|
|
16
|
+
// Dynamically import kokoro-js and configure env
|
|
17
|
+
const { KokoroTTS } = await import('kokoro-js');
|
|
18
|
+
// @ts-ignore - Load transformers from static vendor file
|
|
19
|
+
let transformers = globalThis.transformers;
|
|
20
|
+
if (!transformers) {
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
transformers = await import(/* webpackIgnore: true */ '/vendor/transformers.js');
|
|
23
|
+
}
|
|
24
|
+
const { env } = transformers;
|
|
25
|
+
env.allowLocalModels = false;
|
|
26
|
+
env.useBrowserCache = true;
|
|
27
|
+
this.tts = await KokoroTTS.from_pretrained(this.modelId, {
|
|
28
|
+
dtype: this.dtype,
|
|
29
|
+
});
|
|
30
|
+
console.log("Kokoro TTS Initialized");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log("Cloud TTS Key detected. Local TTS deferred.");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
static async speak(text) {
|
|
38
|
+
// 1. CLOUD TURBO MODE (OpenAI / Groq)
|
|
39
|
+
// Check for specific keys in order: Injected -> OpenAI Env -> Groq Env
|
|
40
|
+
const key = this.cloudKey || process.env.NEXT_PUBLIC_OPENAI_API_KEY || process.env.NEXT_PUBLIC_GROQ_API_KEY;
|
|
41
|
+
if (key) {
|
|
42
|
+
const isGroq = key.startsWith('gsk_');
|
|
43
|
+
const url = isGroq
|
|
44
|
+
? "https://api.groq.com/openai/v1/audio/speech"
|
|
45
|
+
: "https://api.openai.com/v1/audio/speech";
|
|
46
|
+
// Groq requires specific model names.
|
|
47
|
+
// 'playai-tts' is decommissioned. New default is 'canopylabs/orpheus-v1-english'.
|
|
48
|
+
const model = isGroq ? "canopylabs/orpheus-v1-english" : "tts-1";
|
|
49
|
+
console.log(`[TTS] Using Cloud TTS (${isGroq ? 'Groq' : 'OpenAI'})... Model: ${model}`);
|
|
50
|
+
// Groq (Orpheus) supports specific voices: autumn, diana, hannah, austin, daniel, troy
|
|
51
|
+
// OpenAI supports: alloy, echo, fable, etc.
|
|
52
|
+
const voice = isGroq ? "diana" : "alloy";
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(url, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
"Authorization": `Bearer ${key}`,
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
model: model,
|
|
62
|
+
input: text,
|
|
63
|
+
voice: voice,
|
|
64
|
+
response_format: isGroq ? "wav" : "mp3",
|
|
65
|
+
speed: 1.15
|
|
66
|
+
})
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const errText = await res.text();
|
|
70
|
+
throw new Error(`Cloud TTS Error (${res.status}): ${errText}`);
|
|
71
|
+
}
|
|
72
|
+
const blob = await res.blob();
|
|
73
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
74
|
+
// Decode MP3 to Float32
|
|
75
|
+
const ctx = new AudioContext();
|
|
76
|
+
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
|
77
|
+
const pcm = audioBuffer.getChannelData(0); // Mono
|
|
78
|
+
// Copy to new float array
|
|
79
|
+
const pcmData = new Float32Array(pcm);
|
|
80
|
+
const sampleRate = audioBuffer.sampleRate;
|
|
81
|
+
ctx.close();
|
|
82
|
+
return {
|
|
83
|
+
audio: pcmData,
|
|
84
|
+
sampling_rate: sampleRate
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.warn("[TTS] Cloud failed, attempting local fallback:", err);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// 2. LOCAL FALLBACK
|
|
92
|
+
if (!this.tts) {
|
|
93
|
+
console.log("Initializing Local Kokoro TTS (Fallback)...");
|
|
94
|
+
// Dynamically import kokoro-js
|
|
95
|
+
const { KokoroTTS } = await import('kokoro-js');
|
|
96
|
+
// @ts-ignore - Load transformers from static vendor file
|
|
97
|
+
let transformers = globalThis.transformers;
|
|
98
|
+
if (!transformers) {
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
transformers = await import(/* webpackIgnore: true */ '/vendor/transformers.js');
|
|
101
|
+
}
|
|
102
|
+
const { env } = transformers;
|
|
103
|
+
env.allowLocalModels = false;
|
|
104
|
+
env.useBrowserCache = true;
|
|
105
|
+
this.tts = await KokoroTTS.from_pretrained(this.modelId, {
|
|
106
|
+
dtype: this.dtype,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (!this.tts)
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
// Default voice: af_bella
|
|
113
|
+
const audio = await this.tts.generate(text, {
|
|
114
|
+
voice: "af_bella",
|
|
115
|
+
});
|
|
116
|
+
return audio;
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
console.error("TTS Synthesis Error:", e);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
SynthesizerService.tts = null; // KokoroTTS
|
|
125
|
+
SynthesizerService.modelId = "onnx-community/Kokoro-82M-ONNX";
|
|
126
|
+
SynthesizerService.dtype = "fp32";
|
|
127
|
+
// Cloud API Key (Optional)
|
|
128
|
+
SynthesizerService.cloudKey = null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class VADService {
|
|
2
|
+
private session;
|
|
3
|
+
private h;
|
|
4
|
+
private c;
|
|
5
|
+
private sr;
|
|
6
|
+
private readonly SAMPLE_RATE;
|
|
7
|
+
private readonly WINDOW_SIZE_SAMPLES;
|
|
8
|
+
private readonly THRESHOLD_START;
|
|
9
|
+
private readonly THRESHOLD_END;
|
|
10
|
+
private isSpeaking;
|
|
11
|
+
constructor();
|
|
12
|
+
init(modelPath?: string): Promise<void>;
|
|
13
|
+
process(audioFrame: Float32Array): Promise<{
|
|
14
|
+
isSpeech: boolean;
|
|
15
|
+
probability: number;
|
|
16
|
+
}>;
|
|
17
|
+
reset(): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
let ort = null;
|
|
2
|
+
export class VADService {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.session = null; // ort.InferenceSession
|
|
5
|
+
this.h = null; // ort.Tensor
|
|
6
|
+
this.c = null; // ort.Tensor
|
|
7
|
+
this.sr = null; // ort.Tensor
|
|
8
|
+
this.SAMPLE_RATE = 16000;
|
|
9
|
+
this.WINDOW_SIZE_SAMPLES = 512; // 32ms at 16kHz
|
|
10
|
+
// Thresholds
|
|
11
|
+
this.THRESHOLD_START = 0.3; // Increased to avoid trigger on static
|
|
12
|
+
this.THRESHOLD_END = 0.15;
|
|
13
|
+
// State
|
|
14
|
+
this.isSpeaking = false;
|
|
15
|
+
}
|
|
16
|
+
async init(modelPath = '/models/silero_vad.onnx') {
|
|
17
|
+
try {
|
|
18
|
+
// Dynamically import onnxruntime-web to prevent SWC from parsing it at build time.
|
|
19
|
+
if (!ort) {
|
|
20
|
+
ort = await import('onnxruntime-web');
|
|
21
|
+
ort.env.logLevel = 'error'; // Silence benign warnings
|
|
22
|
+
}
|
|
23
|
+
// Initialize ONNX session
|
|
24
|
+
this.session = await ort.InferenceSession.create(modelPath, {
|
|
25
|
+
executionProviders: ['wasm'], // Force WASM for consistency
|
|
26
|
+
logSeverityLevel: 3, // 0:Verbose, 1:Info, 2:Warning, 3:Error, 4:Fatal
|
|
27
|
+
});
|
|
28
|
+
// Initialize states (2, 1, 64) float32
|
|
29
|
+
const dims = [2, 1, 64];
|
|
30
|
+
const size = 128;
|
|
31
|
+
this.h = new ort.Tensor('float32', new Float32Array(size).fill(0), dims);
|
|
32
|
+
this.c = new ort.Tensor('float32', new Float32Array(size).fill(0), dims);
|
|
33
|
+
// Sample rate tensor (Must be int64)
|
|
34
|
+
this.sr = new ort.Tensor('int64', BigInt64Array.from([BigInt(this.SAMPLE_RATE)]), [1]);
|
|
35
|
+
console.log("VADService initialized");
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
console.error("Failed to initialize VAD:", e);
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async process(audioFrame) {
|
|
43
|
+
if (!this.session || !this.h || !this.c || !this.sr || !ort) {
|
|
44
|
+
return { isSpeech: false, probability: 0 };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
// Create Input Tensor: (1, N)
|
|
48
|
+
const input = new ort.Tensor('float32', audioFrame, [1, audioFrame.length]);
|
|
49
|
+
// Check input names to support both v4 and v5
|
|
50
|
+
const inputNames = this.session.inputNames;
|
|
51
|
+
const feeds = {
|
|
52
|
+
input: input,
|
|
53
|
+
sr: this.sr,
|
|
54
|
+
};
|
|
55
|
+
if (inputNames.includes('state')) {
|
|
56
|
+
// Silero v5: Combined state [2, 1, 128]
|
|
57
|
+
const targetShape = [2, 1, 128];
|
|
58
|
+
const targetSize = 256;
|
|
59
|
+
// 1. Validate 'h' existence and size
|
|
60
|
+
if (!this.h || this.h.data.length !== targetSize) {
|
|
61
|
+
this.h = new ort.Tensor('float32', new Float32Array(targetSize).fill(0), targetShape);
|
|
62
|
+
}
|
|
63
|
+
// 2. Validate 'h' dimensions (Reshape if needed)
|
|
64
|
+
else if (this.h.dims.length !== 3 || this.h.dims[0] !== 2 || this.h.dims[1] !== 1 || this.h.dims[2] !== 128) {
|
|
65
|
+
// Reshape keeping data
|
|
66
|
+
this.h = new ort.Tensor('float32', this.h.data, targetShape);
|
|
67
|
+
}
|
|
68
|
+
feeds['state'] = this.h;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// ... v4 logic ...
|
|
72
|
+
feeds['h'] = this.h;
|
|
73
|
+
feeds['c'] = this.c;
|
|
74
|
+
}
|
|
75
|
+
const results = await this.session.run(feeds);
|
|
76
|
+
const probability = results.output.data[0];
|
|
77
|
+
// Update states
|
|
78
|
+
if (inputNames.includes('state')) {
|
|
79
|
+
const rawState = results.stateN || results.state;
|
|
80
|
+
// Bug Fix: Silero V5 might Output weird shapes (e.g. {1,1,1,128,8}) in some envs
|
|
81
|
+
// We force reshape back to [2, 1, 128] for consistency in the next loop.
|
|
82
|
+
if (rawState) {
|
|
83
|
+
const targetShape = [2, 1, 128];
|
|
84
|
+
if (rawState.dims.length !== 3 || rawState.dims[0] !== 2) {
|
|
85
|
+
this.h = new ort.Tensor('float32', rawState.data, targetShape);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.h = rawState;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.h = results.hn;
|
|
94
|
+
this.c = results.cn;
|
|
95
|
+
}
|
|
96
|
+
// Trigger logic
|
|
97
|
+
if (probability > this.THRESHOLD_START && !this.isSpeaking) {
|
|
98
|
+
this.isSpeaking = true;
|
|
99
|
+
}
|
|
100
|
+
else if (probability < this.THRESHOLD_END && this.isSpeaking) {
|
|
101
|
+
this.isSpeaking = false;
|
|
102
|
+
}
|
|
103
|
+
return { isSpeech: this.isSpeaking, probability };
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error("VAD Inference Error:", e);
|
|
107
|
+
return { isSpeech: false, probability: 0 };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
reset() {
|
|
111
|
+
// Reset states
|
|
112
|
+
// We set to null so they re-init on next process() call with correct v4/v5 logic
|
|
113
|
+
this.h = null;
|
|
114
|
+
this.c = null;
|
|
115
|
+
this.isSpeaking = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Utilities for Meechi
|
|
3
|
+
* Implements Hex -> OKLCH conversion for the dynamic theme engine.
|
|
4
|
+
*
|
|
5
|
+
* Flow: Hex -> RGB -> Linear RGB -> OKLAB -> OKLCH
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Main function: Converts Hex to OKLCH string
|
|
9
|
+
* Returns an object with the components for CSS variables
|
|
10
|
+
*/
|
|
11
|
+
export declare function getOklch(hex: string): {
|
|
12
|
+
l: string;
|
|
13
|
+
c: string;
|
|
14
|
+
h: string;
|
|
15
|
+
cssValue: string;
|
|
16
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color Utilities for Meechi
|
|
3
|
+
* Implements Hex -> OKLCH conversion for the dynamic theme engine.
|
|
4
|
+
*
|
|
5
|
+
* Flow: Hex -> RGB -> Linear RGB -> OKLAB -> OKLCH
|
|
6
|
+
*/
|
|
7
|
+
// 1. Hex to sRGB (0-1)
|
|
8
|
+
function hexToRgb(hex) {
|
|
9
|
+
hex = hex.replace(/^#/, '');
|
|
10
|
+
if (hex.length === 3)
|
|
11
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
12
|
+
const bigint = parseInt(hex, 16);
|
|
13
|
+
const r = ((bigint >> 16) & 255) / 255;
|
|
14
|
+
const g = ((bigint >> 8) & 255) / 255;
|
|
15
|
+
const b = (bigint & 255) / 255;
|
|
16
|
+
return { r, g, b };
|
|
17
|
+
}
|
|
18
|
+
// 2. sRGB to Linear RGB
|
|
19
|
+
// sRGB transfer function removal
|
|
20
|
+
function linearize(c) {
|
|
21
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
22
|
+
}
|
|
23
|
+
// 3. Linear RGB to OKLAB
|
|
24
|
+
// Approximate matrices from Okhwb/Okyz specifications
|
|
25
|
+
function rgbToOklab(rLin, gLin, bLin) {
|
|
26
|
+
const l = 0.4122214708 * rLin + 0.5363325363 * gLin + 0.0514459929 * bLin;
|
|
27
|
+
const m = 0.2119034982 * rLin + 0.6806995451 * gLin + 0.1073969566 * bLin;
|
|
28
|
+
const s = 0.0883024619 * rLin + 0.2817188376 * gLin + 0.6299787005 * bLin;
|
|
29
|
+
const l_ = Math.cbrt(l);
|
|
30
|
+
const m_ = Math.cbrt(m);
|
|
31
|
+
const s_ = Math.cbrt(s);
|
|
32
|
+
const L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
|
|
33
|
+
const a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
|
|
34
|
+
const b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
|
|
35
|
+
return { L, a, b };
|
|
36
|
+
}
|
|
37
|
+
// 4. OKLAB to OKLCH
|
|
38
|
+
function oklabToOklch(L, a, b) {
|
|
39
|
+
const C = Math.sqrt(a * a + b * b);
|
|
40
|
+
let h = Math.atan2(b, a) * (180 / Math.PI);
|
|
41
|
+
if (h < 0)
|
|
42
|
+
h += 360;
|
|
43
|
+
return { l: L, c: C, h };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Main function: Converts Hex to OKLCH string
|
|
47
|
+
* Returns an object with the components for CSS variables
|
|
48
|
+
*/
|
|
49
|
+
export function getOklch(hex) {
|
|
50
|
+
const { r, g, b } = hexToRgb(hex);
|
|
51
|
+
const rLin = linearize(r);
|
|
52
|
+
const gLin = linearize(g);
|
|
53
|
+
const bLin = linearize(b);
|
|
54
|
+
const { L, a, b: bLab } = rgbToOklab(rLin, gLin, bLin);
|
|
55
|
+
const { l, c, h } = oklabToOklch(L, a, bLab);
|
|
56
|
+
// Format for CSS
|
|
57
|
+
// Round for clean output
|
|
58
|
+
const lStr = l.toFixed(3);
|
|
59
|
+
const cStr = c.toFixed(3);
|
|
60
|
+
const hStr = h.toFixed(2);
|
|
61
|
+
return {
|
|
62
|
+
l: lStr,
|
|
63
|
+
c: cStr,
|
|
64
|
+
h: hStr,
|
|
65
|
+
cssValue: `oklch(${lStr} ${cStr} ${hStr})`
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { LucideIcon } from 'lucide-react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* MEECHI EXTENSION SYSTEM (Core)
|
|
5
|
+
* ----------------------------
|
|
6
|
+
* This file provides the generic slots for the application.
|
|
7
|
+
* In Core, these registries are empty.
|
|
8
|
+
* Parent applications can populate these registries at runtime/init.
|
|
9
|
+
*/
|
|
10
|
+
export interface SettingsTab {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
icon: LucideIcon;
|
|
14
|
+
component: React.ComponentType;
|
|
15
|
+
}
|
|
16
|
+
export interface FileContext {
|
|
17
|
+
storage: any;
|
|
18
|
+
}
|
|
19
|
+
export interface FileAction {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
icon?: LucideIcon;
|
|
23
|
+
handler: (file: any, context: FileContext) => Promise<void>;
|
|
24
|
+
shouldShow?: (file: any) => boolean;
|
|
25
|
+
}
|
|
26
|
+
declare class ExtensionRegistry {
|
|
27
|
+
private settingsTabs;
|
|
28
|
+
private fileActions;
|
|
29
|
+
registerSettingsTab(tab: SettingsTab): void;
|
|
30
|
+
getSettingsTabs(): SettingsTab[];
|
|
31
|
+
registerFileAction(action: FileAction): void;
|
|
32
|
+
getFileActions(): FileAction[];
|
|
33
|
+
}
|
|
34
|
+
export declare const extensions: ExtensionRegistry;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class ExtensionRegistry {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.settingsTabs = [];
|
|
4
|
+
this.fileActions = [];
|
|
5
|
+
}
|
|
6
|
+
registerSettingsTab(tab) {
|
|
7
|
+
if (!this.settingsTabs.find(t => t.id === tab.id)) {
|
|
8
|
+
this.settingsTabs.push(tab);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
getSettingsTabs() {
|
|
12
|
+
return [...this.settingsTabs];
|
|
13
|
+
}
|
|
14
|
+
// File Explorer Slots
|
|
15
|
+
registerFileAction(action) {
|
|
16
|
+
if (!this.fileActions.find(a => a.id === action.id)) {
|
|
17
|
+
this.fileActions.push(action);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
getFileActions() {
|
|
21
|
+
return [...this.fileActions];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const extensions = new ExtensionRegistry();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type VoiceState = 'idle' | 'listening' | 'processing' | 'speaking';
|
|
2
|
+
export declare function useVoiceLoop(sendMessage: (text: string, onToken?: (chunk: string) => void) => Promise<string | void>): {
|
|
3
|
+
start: () => Promise<void>;
|
|
4
|
+
stop: () => void;
|
|
5
|
+
stopPlayback: () => void;
|
|
6
|
+
state: VoiceState;
|
|
7
|
+
vadProb: number;
|
|
8
|
+
transcript: string;
|
|
9
|
+
playResponse: (text: string) => Promise<void>;
|
|
10
|
+
isPlaying: boolean;
|
|
11
|
+
getAnalyser: () => AnalyserNode | null;
|
|
12
|
+
};
|
|
13
|
+
export {};
|