From 7fc3bdb5b4e81a974f0a7798ae9c9a156fe5b42d Mon Sep 17 00:00:00 2001 From: Krzysztof Palikowski Date: Sat, 20 Dec 2025 15:24:40 +0100 Subject: [PATCH 1/8] added salad transcription API as 4th option in plugin settings --- manifest.json | 4 +- package-lock.json | 4 +- src/adapters/AIAdapter.ts | 5 + src/adapters/SaladAdapter.ts | 246 ++++++++++++++++++ src/main.ts | 23 +- src/settings/Settings.ts | 4 + .../accordions/ModelHookupAccordion.ts | 61 +++++ src/settings/accordions/RecordingAccordion.ts | 10 +- .../transcription/TranscriptionService.ts | 12 + 9 files changed, 363 insertions(+), 6 deletions(-) create mode 100644 src/adapters/SaladAdapter.ts diff --git a/manifest.json b/manifest.json index bd5d46f..3450373 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { - "id": "neurovox", - "name": "NeuroVox", + "id": "neurovoxsalad", + "name": "NeuroVoxSalad", "version": "1.0.4", "minAppVersion": "0.15.0", "description": "Enhances your note-taking with voice transcription and AI capabilities", diff --git a/package-lock.json b/package-lock.json index 0e15e93..051b64d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "neurovox", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "neurovox", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "dependencies": { "@deepgram/sdk": "^4.2.0", diff --git a/src/adapters/AIAdapter.ts b/src/adapters/AIAdapter.ts index 4797b84..6414b10 100644 --- a/src/adapters/AIAdapter.ts +++ b/src/adapters/AIAdapter.ts @@ -5,6 +5,7 @@ export enum AIProvider { OpenAI = 'openai', Groq = 'groq', Deepgram = 'deepgram', + Salad = 'salad', } export interface AIModel { @@ -41,6 +42,10 @@ export const AIModels: Record = { { id: 'nova-2', name: 'Nova-2', category: 'transcription' }, { id: 'nova-3', name: 'Nova-3', category: 'transcription' }, ], + [AIProvider.Salad]: [ + { id: 'transcribe', name: 'Salad Transcription', category: 'transcription' }, + { id: 'transcription-lite', name: 'Transcription Lite', category: 'transcription' }, + ], }; export function getModelInfo(modelId: string): AIModel | undefined { diff --git a/src/adapters/SaladAdapter.ts b/src/adapters/SaladAdapter.ts new file mode 100644 index 0000000..8eed3df --- /dev/null +++ b/src/adapters/SaladAdapter.ts @@ -0,0 +1,246 @@ +import { requestUrl } from 'obsidian'; +import { AIAdapter, AIProvider } from './AIAdapter'; +import { NeuroVoxSettings } from '../settings/Settings'; + +export class SaladAdapter extends AIAdapter { + private apiKey: string = ''; + private organization: string = ''; + + constructor(settings: NeuroVoxSettings) { + super(settings, AIProvider.Salad); + } + + getApiKey(): string { + return this.apiKey; + } + + getOrganization(): string { + return this.organization; + } + + setOrganization(org: string): void { + this.organization = org; + } + + protected setApiKeyInternal(key: string): void { + this.apiKey = key; + } + + protected getApiBaseUrl(): string { + return 'https://api.salad.com/api/public'; + } + + protected getStorageBaseUrl(): string { + return 'https://storage-api.salad.com'; + } + + protected getTextGenerationEndpoint(): string { + return ''; + } + + protected getTranscriptionEndpoint(): string { + return `/organizations/${this.organization}/inference-endpoints`; + } + + protected async validateApiKeyImpl(): Promise { + if (!this.apiKey || !this.organization) { + return false; + } + + try { + const response = await this.makeAPIRequest( + `${this.getStorageBaseUrl()}/organizations/${this.organization}/files`, + 'GET', + {}, + null + ); + return response && Array.isArray(response.files); + } catch (error) { + return false; + } + } + + protected parseTextGenerationResponse(response: any): string { + throw new Error('Text generation not supported by Salad'); + } + + protected parseTranscriptionResponse(response: any): string { + if (response?.output?.text) { + return response.output.text; + } + throw new Error('Invalid transcription response format from Salad'); + } + + public async transcribeAudio(audioArrayBuffer: ArrayBuffer, model: string): Promise { + try { + if (!this.organization) { + throw new Error('Salad organization name is not configured'); + } + + const audioUrl = await this.uploadToS4Storage(audioArrayBuffer); + + const jobId = await this.submitTranscriptionJob(audioUrl, model); + + const result = await this.pollForResult(jobId, model); + + await this.deleteFromS4Storage(audioUrl); + + return this.parseTranscriptionResponse(result); + } catch (error) { + const message = this.getErrorMessage(error); + throw new Error(`Failed to transcribe audio with Salad: ${message}`); + } + } + + private async uploadToS4Storage(audioArrayBuffer: ArrayBuffer): Promise { + const fileName = `audio/neurovox_${Date.now()}.wav`; + const uploadUrl = `${this.getStorageBaseUrl()}/organizations/${this.organization}/files/${fileName}`; + + const boundary = 'saladuploadboundary'; + const encoder = new TextEncoder(); + + const parts: Uint8Array[] = []; + + parts.push(encoder.encode(`--${boundary}\r\n`)); + parts.push(encoder.encode('Content-Disposition: form-data; name="mimeType"\r\n\r\n')); + parts.push(encoder.encode('audio/wav')); + parts.push(encoder.encode('\r\n')); + + parts.push(encoder.encode(`--${boundary}\r\n`)); + parts.push(encoder.encode('Content-Disposition: form-data; name="sign"\r\n\r\n')); + parts.push(encoder.encode('true')); + parts.push(encoder.encode('\r\n')); + + parts.push(encoder.encode(`--${boundary}\r\n`)); + parts.push(encoder.encode('Content-Disposition: form-data; name="signatureExp"\r\n\r\n')); + parts.push(encoder.encode('86400')); + parts.push(encoder.encode('\r\n')); + + parts.push(encoder.encode(`--${boundary}\r\n`)); + parts.push(encoder.encode(`Content-Disposition: form-data; name="file"; filename="audio.wav"\r\n`)); + parts.push(encoder.encode('Content-Type: audio/wav\r\n\r\n')); + parts.push(new Uint8Array(audioArrayBuffer)); + parts.push(encoder.encode('\r\n')); + + parts.push(encoder.encode(`--${boundary}--\r\n`)); + + const totalLength = parts.reduce((acc, part) => acc + part.length, 0); + const finalBuffer = new Uint8Array(totalLength); + let offset = 0; + + for (const part of parts) { + finalBuffer.set(part, offset); + offset += part.length; + } + + const response = await requestUrl({ + url: uploadUrl, + method: 'PUT', + headers: { + 'Salad-Api-Key': this.apiKey, + 'Content-Type': `multipart/form-data; boundary=${boundary}` + }, + body: finalBuffer.buffer, + throw: true + }); + + if (!response.json?.url) { + throw new Error('Failed to get signed URL from S4 storage'); + } + + return response.json.url; + } + + private async submitTranscriptionJob(audioUrl: string, model: string): Promise { + const endpoint = `${this.getApiBaseUrl()}/organizations/${this.organization}/inference-endpoints/${model}/jobs`; + + const body = { + input: { + url: audioUrl, + language_code: 'auto', + return_as_file: false, + sentence_level_timestamps: false, + word_level_timestamps: false, + diarization: false, + srt: false + } + }; + + const response = await this.makeAPIRequest( + endpoint, + 'POST', + { 'Content-Type': 'application/json' }, + JSON.stringify(body) + ); + + if (!response?.id) { + throw new Error('Failed to submit transcription job'); + } + + return response.id; + } + + private async pollForResult(jobId: string, model: string, maxAttempts: number = 120, intervalMs: number = 2000): Promise { + const endpoint = `${this.getApiBaseUrl()}/organizations/${this.organization}/inference-endpoints/${model}/jobs/${jobId}`; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const response = await this.makeAPIRequest(endpoint, 'GET', {}, null); + + if (response?.status === 'succeeded') { + return response; + } else if (response?.status === 'failed') { + throw new Error(`Transcription job failed: ${response?.error || 'Unknown error'}`); + } + + await new Promise(resolve => setTimeout(resolve, intervalMs)); + } + + throw new Error('Transcription job timed out'); + } + + private async deleteFromS4Storage(signedUrl: string): Promise { + try { + const urlWithoutToken = signedUrl.split('?')[0]; + + await requestUrl({ + url: urlWithoutToken, + method: 'DELETE', + headers: { + 'Salad-Api-Key': this.apiKey + }, + throw: false + }); + } catch (error) { + } + } + + protected async makeAPIRequest( + endpoint: string, + method: string, + headers: Record, + body: string | ArrayBuffer | null + ): Promise { + try { + const requestHeaders: Record = { + 'Salad-Api-Key': this.apiKey, + ...headers + }; + + const response = await requestUrl({ + url: endpoint, + method, + headers: requestHeaders, + body: body || undefined, + throw: true + }); + + if (!response.json) { + throw new Error('Invalid response format'); + } + + return response.json; + } catch (error: any) { + throw error; + } + } +} diff --git a/src/main.ts b/src/main.ts index 0b4077d..de61a11 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,6 +20,7 @@ import { TimerModal } from './modals/TimerModal'; import { OpenAIAdapter } from './adapters/OpenAIAdapter'; import { GroqAdapter } from './adapters/GroqAdapter'; import { DeepgramAdapter } from './adapters/DeepgramAdapter'; +import { SaladAdapter } from './adapters/SaladAdapter'; import { AIProvider, AIAdapter } from './adapters/AIAdapter'; import { PluginData } from './types'; import { RecordingProcessor } from './utils/RecordingProcessor'; @@ -189,6 +190,10 @@ export default class NeuroVoxPlugin extends Plugin { public async saveSettings(): Promise { try { await this.saveData(this.settings); + + // Validate API keys after settings change to ensure adapters are ready + await this.validateApiKeys(); + this.initializeUI(); // Trigger the floating button setting changed event to ensure UI is in sync @@ -219,6 +224,15 @@ export default class NeuroVoxPlugin extends Plugin { await deepgramAdapter.validateApiKey(); } + const saladAdapter = this.aiAdapters.get(AIProvider.Salad); + if (saladAdapter) { + saladAdapter.setApiKey(this.settings.saladApiKey); + if ('setOrganization' in saladAdapter) { + (saladAdapter as SaladAdapter).setOrganization(this.settings.saladOrganization); + } + await saladAdapter.validateApiKey(); + } + // Only show notice if validation fails if (openaiAdapter && !openaiAdapter.isReady() && this.settings.openaiApiKey) { new Notice('❌ OpenAI API key validation failed'); @@ -229,15 +243,22 @@ export default class NeuroVoxPlugin extends Plugin { if (deepgramAdapter && !deepgramAdapter.isReady() && this.settings.deepgramApiKey) { new Notice('❌ Deepgram API key validation failed'); } + if (saladAdapter && !saladAdapter.isReady() && this.settings.saladApiKey) { + new Notice('❌ Salad API key validation failed'); + } } catch (error) { // Silent fail for API key validation } }public initializeAIAdapters(): void { try { + const saladAdapter = new SaladAdapter(this.settings); + saladAdapter.setOrganization(this.settings.saladOrganization); + const adapters: Array<[AIProvider, AIAdapter]> = [ [AIProvider.OpenAI, new OpenAIAdapter(this.settings)], [AIProvider.Groq, new GroqAdapter(this.settings)], - [AIProvider.Deepgram, new DeepgramAdapter(this.settings)] + [AIProvider.Deepgram, new DeepgramAdapter(this.settings)], + [AIProvider.Salad, saladAdapter] ]; this.aiAdapters = new Map(adapters); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 18a1282..4ae0f2d 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -13,6 +13,8 @@ export type NeuroVoxSettings = { openaiApiKey: string; groqApiKey: string; deepgramApiKey: string; + saladApiKey: string; + saladOrganization: string; // Recording audioQuality: AudioQuality; @@ -54,6 +56,8 @@ export const DEFAULT_SETTINGS: NeuroVoxSettings = { openaiApiKey: '', groqApiKey: '', deepgramApiKey: '', + saladApiKey: '', + saladOrganization: '', // Recording audioQuality: AudioQuality.Medium, diff --git a/src/settings/accordions/ModelHookupAccordion.ts b/src/settings/accordions/ModelHookupAccordion.ts index ab4d403..8312c25 100644 --- a/src/settings/accordions/ModelHookupAccordion.ts +++ b/src/settings/accordions/ModelHookupAccordion.ts @@ -139,5 +139,66 @@ export class ModelHookupAccordion extends BaseAccordion { } }); }); + + // Salad Organization Name + new Setting(this.contentEl) + .setName("Salad Organization") + .setDesc("Enter your SaladCloud organization name") + .addText(text => { + text + .setPlaceholder("my-organization") + .setValue(this.settings.saladOrganization); + text.onChange(async (value: string) => { + const trimmedValue = value.trim(); + this.settings.saladOrganization = trimmedValue; + await this.plugin.saveSettings(); + + const adapter = this.getAdapter(AIProvider.Salad); + if (adapter && 'setOrganization' in adapter) { + (adapter as any).setOrganization(trimmedValue); + } + }); + }); + + // Salad API Key + const saladSetting = new Setting(this.contentEl) + .setName("Salad API Key") + .setDesc("Enter your SaladCloud API key") + .addText(text => { + text + .setPlaceholder("Enter your Salad API key...") + .setValue(this.settings.saladApiKey); + text.inputEl.type = "password"; + text.onChange(async (value: string) => { + const trimmedValue = value.trim(); + this.settings.saladApiKey = trimmedValue; + await this.plugin.saveSettings(); + + const adapter = this.getAdapter(AIProvider.Salad); + if (!adapter) { + return; + } + + adapter.setApiKey(trimmedValue); + + // Also set organization if available + if ('setOrganization' in adapter) { + (adapter as any).setOrganization(this.settings.saladOrganization); + } + + const isValid = await adapter.validateApiKey(); + + if (isValid) { + saladSetting.setDesc("✅ API key validated successfully"); + try { + await this.refreshAccordions(); + } catch (error) { + saladSetting.setDesc("✅ API key valid, but failed to update model lists"); + } + } else { + saladSetting.setDesc("❌ Invalid API key or organization. Please check your credentials."); + } + }); + }); } } diff --git a/src/settings/accordions/RecordingAccordion.ts b/src/settings/accordions/RecordingAccordion.ts index 0eded05..ef9e634 100644 --- a/src/settings/accordions/RecordingAccordion.ts +++ b/src/settings/accordions/RecordingAccordion.ts @@ -195,9 +195,17 @@ export class RecordingAccordion extends BaseAccordion { dropdown.onChange(async (value) => { this.settings.transcriptionModel = value; const provider = this.getProviderFromModel(value); + console.log('NeuroVox Debug - Model changed:', { + model: value, + detectedProvider: provider + }); if (provider) { this.settings.transcriptionProvider = provider; await this.plugin.saveSettings(); + console.log('NeuroVox Debug - Settings saved:', { + transcriptionProvider: this.settings.transcriptionProvider, + transcriptionModel: this.settings.transcriptionModel + }); } }); }); @@ -205,7 +213,7 @@ export class RecordingAccordion extends BaseAccordion { dropdown.selectEl.empty(); let hasValidProvider = false; - for (const provider of [AIProvider.OpenAI, AIProvider.Groq, AIProvider.Deepgram]) { + for (const provider of [AIProvider.OpenAI, AIProvider.Groq, AIProvider.Deepgram, AIProvider.Salad]) { const apiKey = this.settings[`${provider}ApiKey` as keyof NeuroVoxSettings]; if (apiKey) { const adapter = this.getAdapter(provider); diff --git a/src/utils/transcription/TranscriptionService.ts b/src/utils/transcription/TranscriptionService.ts index ff68a53..3a56dae 100644 --- a/src/utils/transcription/TranscriptionService.ts +++ b/src/utils/transcription/TranscriptionService.ts @@ -45,6 +45,11 @@ export class TranscriptionService { * Transcribes audio using the configured AI adapter */ private async transcribeAudio(audioBuffer: ArrayBuffer): Promise { + console.log('NeuroVox Debug - Transcription Settings:', { + provider: this.plugin.settings.transcriptionProvider, + model: this.plugin.settings.transcriptionModel + }); + const adapter = this.getAdapter( this.plugin.settings.transcriptionProvider, 'transcription' @@ -86,6 +91,13 @@ export class TranscriptionService { throw new Error(`${provider} adapter not found`); } + console.log('NeuroVox Debug - Adapter check:', { + provider, + category, + isReady: adapter.isReady(category), + apiKey: adapter.getApiKey() ? 'present' : 'missing' + }); + if (!adapter.isReady(category)) { const apiKey = adapter.getApiKey(); if (!apiKey) { From ec2a939f86c04ad46c3ab921d11f1f7db31a6308 Mon Sep 17 00:00:00 2001 From: Krzysztof Palikowski Date: Sat, 20 Dec 2025 16:03:31 +0100 Subject: [PATCH 2/8] perplexity sonar --- src/adapters/AIAdapter.ts | 5 ++ src/adapters/PerplexityAdapter.ts | 65 +++++++++++++++++++ src/main.ts | 13 +++- src/settings/Settings.ts | 2 + .../accordions/ModelHookupAccordion.ts | 35 ++++++++++ .../accordions/PostProcessingAccordion.ts | 2 +- 6 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 src/adapters/PerplexityAdapter.ts diff --git a/src/adapters/AIAdapter.ts b/src/adapters/AIAdapter.ts index 6414b10..b9d725a 100644 --- a/src/adapters/AIAdapter.ts +++ b/src/adapters/AIAdapter.ts @@ -6,6 +6,7 @@ export enum AIProvider { Groq = 'groq', Deepgram = 'deepgram', Salad = 'salad', + Perplexity = 'perplexity', } export interface AIModel { @@ -46,6 +47,10 @@ export const AIModels: Record = { { id: 'transcribe', name: 'Salad Transcription', category: 'transcription' }, { id: 'transcription-lite', name: 'Transcription Lite', category: 'transcription' }, ], + [AIProvider.Perplexity]: [ + { id: 'sonar', name: 'Sonar', category: 'language', maxTokens: 127072 }, + { id: 'sonar-pro', name: 'Sonar Pro', category: 'language', maxTokens: 127072 }, + ], }; export function getModelInfo(modelId: string): AIModel | undefined { diff --git a/src/adapters/PerplexityAdapter.ts b/src/adapters/PerplexityAdapter.ts new file mode 100644 index 0000000..f279367 --- /dev/null +++ b/src/adapters/PerplexityAdapter.ts @@ -0,0 +1,65 @@ +import { AIAdapter, AIProvider } from './AIAdapter'; +import { NeuroVoxSettings } from '../settings/Settings'; + +export class PerplexityAdapter extends AIAdapter { + private apiKey: string = ''; + + constructor(settings: NeuroVoxSettings) { + super(settings, AIProvider.Perplexity); + } + + getApiKey(): string { + return this.apiKey; + } + + protected setApiKeyInternal(key: string): void { + this.apiKey = key; + } + + protected getApiBaseUrl(): string { + return 'https://api.perplexity.ai'; + } + + protected getTextGenerationEndpoint(): string { + return '/chat/completions'; + } + + protected getTranscriptionEndpoint(): string { + throw new Error('Transcription not supported by Perplexity'); + } + + protected async validateApiKeyImpl(): Promise { + if (!this.apiKey) { + return false; + } + + try { + await this.makeAPIRequest( + `${this.getApiBaseUrl()}/chat/completions`, + 'POST', + { + 'Content-Type': 'application/json' + }, + JSON.stringify({ + model: 'sonar', + messages: [{ role: 'user', content: 'test' }], + max_tokens: 1 + }) + ); + return true; + } catch (error) { + return false; + } + } + + protected parseTextGenerationResponse(response: any): string { + if (response?.choices?.[0]?.message?.content) { + return response.choices[0].message.content; + } + throw new Error('Invalid response format from Perplexity'); + } + + protected parseTranscriptionResponse(response: any): string { + throw new Error('Transcription not supported by Perplexity'); + } +} diff --git a/src/main.ts b/src/main.ts index de61a11..6e807a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,6 +21,7 @@ import { OpenAIAdapter } from './adapters/OpenAIAdapter'; import { GroqAdapter } from './adapters/GroqAdapter'; import { DeepgramAdapter } from './adapters/DeepgramAdapter'; import { SaladAdapter } from './adapters/SaladAdapter'; +import { PerplexityAdapter } from './adapters/PerplexityAdapter'; import { AIProvider, AIAdapter } from './adapters/AIAdapter'; import { PluginData } from './types'; import { RecordingProcessor } from './utils/RecordingProcessor'; @@ -233,6 +234,12 @@ export default class NeuroVoxPlugin extends Plugin { await saladAdapter.validateApiKey(); } + const perplexityAdapter = this.aiAdapters.get(AIProvider.Perplexity); + if (perplexityAdapter) { + perplexityAdapter.setApiKey(this.settings.perplexityApiKey); + await perplexityAdapter.validateApiKey(); + } + // Only show notice if validation fails if (openaiAdapter && !openaiAdapter.isReady() && this.settings.openaiApiKey) { new Notice('❌ OpenAI API key validation failed'); @@ -246,6 +253,9 @@ export default class NeuroVoxPlugin extends Plugin { if (saladAdapter && !saladAdapter.isReady() && this.settings.saladApiKey) { new Notice('❌ Salad API key validation failed'); } + if (perplexityAdapter && !perplexityAdapter.isReady() && this.settings.perplexityApiKey) { + new Notice('❌ Perplexity API key validation failed'); + } } catch (error) { // Silent fail for API key validation } @@ -258,7 +268,8 @@ export default class NeuroVoxPlugin extends Plugin { [AIProvider.OpenAI, new OpenAIAdapter(this.settings)], [AIProvider.Groq, new GroqAdapter(this.settings)], [AIProvider.Deepgram, new DeepgramAdapter(this.settings)], - [AIProvider.Salad, saladAdapter] + [AIProvider.Salad, saladAdapter], + [AIProvider.Perplexity, new PerplexityAdapter(this.settings)] ]; this.aiAdapters = new Map(adapters); diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 4ae0f2d..f1cd3b2 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -15,6 +15,7 @@ export type NeuroVoxSettings = { deepgramApiKey: string; saladApiKey: string; saladOrganization: string; + perplexityApiKey: string; // Recording audioQuality: AudioQuality; @@ -58,6 +59,7 @@ export const DEFAULT_SETTINGS: NeuroVoxSettings = { deepgramApiKey: '', saladApiKey: '', saladOrganization: '', + perplexityApiKey: '', // Recording audioQuality: AudioQuality.Medium, diff --git a/src/settings/accordions/ModelHookupAccordion.ts b/src/settings/accordions/ModelHookupAccordion.ts index 8312c25..946f598 100644 --- a/src/settings/accordions/ModelHookupAccordion.ts +++ b/src/settings/accordions/ModelHookupAccordion.ts @@ -200,5 +200,40 @@ export class ModelHookupAccordion extends BaseAccordion { } }); }); + + // Perplexity API Key + const perplexitySetting = new Setting(this.contentEl) + .setName("Perplexity API Key") + .setDesc("Enter your Perplexity API key") + .addText(text => { + text + .setPlaceholder("pplx-...") + .setValue(this.settings.perplexityApiKey); + text.inputEl.type = "password"; + text.onChange(async (value: string) => { + const trimmedValue = value.trim(); + this.settings.perplexityApiKey = trimmedValue; + await this.plugin.saveSettings(); + + const adapter = this.getAdapter(AIProvider.Perplexity); + if (!adapter) { + return; + } + + adapter.setApiKey(trimmedValue); + const isValid = await adapter.validateApiKey(); + + if (isValid) { + perplexitySetting.setDesc("✅ API key validated successfully"); + try { + await this.refreshAccordions(); + } catch (error) { + perplexitySetting.setDesc("✅ API key valid, but failed to update model lists"); + } + } else { + perplexitySetting.setDesc("❌ Invalid API key. Please check your credentials."); + } + }); + }); } } diff --git a/src/settings/accordions/PostProcessingAccordion.ts b/src/settings/accordions/PostProcessingAccordion.ts index 5f36c04..539d192 100644 --- a/src/settings/accordions/PostProcessingAccordion.ts +++ b/src/settings/accordions/PostProcessingAccordion.ts @@ -95,7 +95,7 @@ export class PostProcessingAccordion extends BaseAccordion { dropdown.selectEl.empty(); let hasValidProvider = false; - for (const provider of [AIProvider.OpenAI, AIProvider.Groq]) { + for (const provider of [AIProvider.OpenAI, AIProvider.Groq, AIProvider.Perplexity]) { const apiKey = this.settings[`${provider}ApiKey` as keyof NeuroVoxSettings]; if (apiKey) { const models = AIModels[provider].filter(model => model.category === 'language'); From 13cd5161fe6def1badd5a81f65f0d2ce92d9ef1b Mon Sep 17 00:00:00 2001 From: Krzysztof Palikowski Date: Sat, 20 Dec 2025 16:30:19 +0100 Subject: [PATCH 3/8] add used model info in transcription and postprocessing text --- src/utils/RecordingProcessor.ts | 12 ++++++++++-- src/utils/document/DocumentInserter.ts | 18 ++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/utils/RecordingProcessor.ts b/src/utils/RecordingProcessor.ts index f8b4711..3e3a2b8 100644 --- a/src/utils/RecordingProcessor.ts +++ b/src/utils/RecordingProcessor.ts @@ -87,7 +87,11 @@ export class RecordingProcessor { { transcription: result.transcription, postProcessing: result.postProcessing, - audioFilePath: audioResult.finalPath + audioFilePath: audioResult.finalPath, + transcriptionProvider: this.plugin.settings.transcriptionProvider, + transcriptionModel: this.plugin.settings.transcriptionModel, + postProcessingProvider: this.plugin.settings.postProcessingProvider, + postProcessingModel: this.plugin.settings.postProcessingModel }, activeFile, cursorPosition @@ -137,8 +141,12 @@ export class RecordingProcessor { await this.documentInserter.insertContent( { transcription: transcriptionResult, - postProcessing + postProcessing, // No audioFilePath for streaming mode + transcriptionProvider: this.plugin.settings.transcriptionProvider, + transcriptionModel: this.plugin.settings.transcriptionModel, + postProcessingProvider: this.plugin.settings.postProcessingProvider, + postProcessingModel: this.plugin.settings.postProcessingModel }, activeFile, cursorPosition diff --git a/src/utils/document/DocumentInserter.ts b/src/utils/document/DocumentInserter.ts index 1101876..66ef610 100644 --- a/src/utils/document/DocumentInserter.ts +++ b/src/utils/document/DocumentInserter.ts @@ -8,6 +8,10 @@ export interface InsertContent { transcription: string; postProcessing?: string; audioFilePath?: string; + transcriptionProvider?: string; + transcriptionModel?: string; + postProcessingProvider?: string; + postProcessingModel?: string; } /** @@ -74,10 +78,15 @@ export class DocumentInserter { .replace('{audioPath}', ''); // Fallback for any other format } + // Add model info to transcription + const transcriptionWithModelInfo = content.transcriptionProvider && content.transcriptionModel + ? `*Transcribed with ${content.transcriptionProvider} (${content.transcriptionModel})*\n\n${content.transcription}` + : content.transcription; + // Format transcription content let formattedContent = format .replace('{audioPath}', content.audioFilePath || '') - .replace('{transcription}', content.transcription); + .replace('{transcription}', transcriptionWithModelInfo); // Only use callout formatting if the format includes callout syntax const useTranscriptionCallout = this.isCalloutFormat(format); @@ -88,8 +97,13 @@ export class DocumentInserter { const postFormat = this.plugin.settings.postProcessingCalloutFormat; const usePostCallout = this.isCalloutFormat(postFormat); + // Add model info to post-processing + const postProcessingWithModelInfo = content.postProcessingProvider && content.postProcessingModel + ? `*Generated with ${content.postProcessingProvider} (${content.postProcessingModel})*\n\n${content.postProcessing}` + : content.postProcessing; + let postContent = postFormat - .replace('{postProcessing}', content.postProcessing); + .replace('{postProcessing}', postProcessingWithModelInfo); postContent = this.formatLines(postContent, usePostCallout); formattedContent += '\n---\n' + postContent + '\n\n'; From 5093f9849a962160e10994e6f96775edd5467b56 Mon Sep 17 00:00:00 2001 From: Krzysztof Palikowski Date: Sat, 20 Dec 2025 16:58:22 +0100 Subject: [PATCH 4/8] limity --- src/utils/DeviceDetection.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/DeviceDetection.ts b/src/utils/DeviceDetection.ts index 85bad17..e4e92ea 100644 --- a/src/utils/DeviceDetection.ts +++ b/src/utils/DeviceDetection.ts @@ -64,19 +64,19 @@ export class DeviceDetection { if (isMobile || availableMemory < 1024 * 1024 * 1024) { // Mobile or < 1GB return { - chunkDuration: 5, // 5 second chunks - maxQueueSize: 3, // Max 3 chunks in memory + chunkDuration: 10, // 10 second chunks + maxQueueSize: 20, // Max 8 chunks in memory bitrate: 16000, // 16kbps processingMode: 'streaming', - memoryLimit: 100 // 100MB limit + memoryLimit: 950 // 250MB limit }; } else { return { chunkDuration: 10, // 10 second chunks - maxQueueSize: 5, // Max 5 chunks in memory + maxQueueSize: 20, // Max 10 chunks in memory bitrate: 48000, // 48kbps processingMode: 'streaming', - memoryLimit: 300 // 300MB limit + memoryLimit: 1400 // 400MB limit }; } } From 8f6bcb7dac0c1ce212d3440954479cbe2ef9b13e Mon Sep 17 00:00:00 2001 From: Krzysztof Palikowski Date: Sat, 20 Dec 2025 20:40:48 +0100 Subject: [PATCH 5/8] debug mode setting and audio file fix for mobile devices --- src/main.ts | 13 +- src/modals/TimerModal.ts | 13 +- src/settings/Settings.ts | 6 + src/settings/accordions/RecordingAccordion.ts | 17 +++ src/ui/ToolbarButton.ts | 12 +- src/utils/DebugLogger.ts | 133 ++++++++++++++++++ src/utils/RecordingProcessor.ts | 6 +- 7 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 src/utils/DebugLogger.ts diff --git a/src/main.ts b/src/main.ts index 6e807a9..412224f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -541,14 +541,23 @@ export default class NeuroVoxPlugin extends Plugin { if (this.modalInstance) return; this.modalInstance = new TimerModal(this); - this.modalInstance.onStop = async (result: Blob | string) => { + this.modalInstance.onStop = async (result: Blob | string, audioBlob?: Blob) => { try { if (typeof result === 'string') { // Streaming mode - transcription already done + // If we have audio blob, save it first + let audioFilePath: string | undefined; + if (audioBlob) { + const AudioFileManager = (await import('./utils/audio/AudioFileManager')).AudioFileManager; + const audioFileManager = new AudioFileManager(this); + audioFilePath = await audioFileManager.saveAudioFile(audioBlob); + } + await this.recordingProcessor.processStreamingResult( result, activeFile, - activeView.editor.getCursor() + activeView.editor.getCursor(), + audioFilePath ); } else { // Legacy mode - need to transcribe diff --git a/src/modals/TimerModal.ts b/src/modals/TimerModal.ts index 7f659bc..ae46b62 100644 --- a/src/modals/TimerModal.ts +++ b/src/modals/TimerModal.ts @@ -32,7 +32,7 @@ export class TimerModal extends Modal { private readonly CONFIG: TimerConfig; - public onStop: (result: Blob | string) => void; + public onStop: (result: Blob | string, audioBlob?: Blob) => void; constructor(private plugin: NeuroVoxPlugin) { super(plugin.app); @@ -301,6 +301,7 @@ export class TimerModal extends Modal { const finalBlob = await this.recordingManager.stop(); let result: Blob | string; + let audioBlob: Blob | undefined; if (this.useStreaming && this.streamingService) { // Streaming mode - get transcription result @@ -310,6 +311,9 @@ export class TimerModal extends Modal { if (!result || result.trim().length === 0) { throw new Error('No transcription result received'); } + + // Store audio blob for saving even in streaming mode + audioBlob = finalBlob || undefined; } else { // Legacy mode - return audio blob if (!finalBlob) { @@ -324,7 +328,12 @@ export class TimerModal extends Modal { // Always save the recording if (this.onStop) { - await this.onStop(result); + // Pass both result and audio blob if in streaming mode + if (this.useStreaming && audioBlob) { + await this.onStop(result, audioBlob); + } else { + await this.onStop(result); + } } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index f1cd3b2..3544d1a 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -41,6 +41,9 @@ export type NeuroVoxSettings = { postProcessingTemperature: number; postProcessingCalloutFormat: string; + // Debug + debugMode: boolean; + // Current Provider currentProvider: AIProvider; @@ -84,6 +87,9 @@ export const DEFAULT_SETTINGS: NeuroVoxSettings = { postProcessingProvider: AIProvider.OpenAI, postProcessingTemperature: 0.7, postProcessingCalloutFormat: '>[!note]- Post-Processing\n>{postProcessing}', + + // Debug + debugMode: false, // Current Provider currentProvider: AIProvider.OpenAI, diff --git a/src/settings/accordions/RecordingAccordion.ts b/src/settings/accordions/RecordingAccordion.ts index ef9e634..6e9d412 100644 --- a/src/settings/accordions/RecordingAccordion.ts +++ b/src/settings/accordions/RecordingAccordion.ts @@ -38,6 +38,9 @@ export class RecordingAccordion extends BaseAccordion { // Mic Button Color this.createMicButtonColorSetting(); + // Debug Mode Toggle + this.createDebugModeSetting(); + // Add this before createTranscriptionModelSetting this.createTranscriptionFormatSetting(); @@ -164,6 +167,20 @@ export class RecordingAccordion extends BaseAccordion { }); } + private createDebugModeSetting(): void { + new Setting(this.contentEl) + .setName("Debug mode") + .setDesc("Enable detailed logging of operations (chunks, API calls, file operations, timing). Debug info will be added to transcription notes.") + .addToggle(toggle => { + toggle + .setValue(this.settings.debugMode) + .onChange(async (value) => { + this.settings.debugMode = value; + await this.plugin.saveSettings(); + }); + }); + } + public createTranscriptionFormatSetting(): void { new Setting(this.contentEl) .setName("Transcription format") diff --git a/src/ui/ToolbarButton.ts b/src/ui/ToolbarButton.ts index 3811ecb..ea6361c 100644 --- a/src/ui/ToolbarButton.ts +++ b/src/ui/ToolbarButton.ts @@ -47,11 +47,19 @@ export class ToolbarButton { const cursorPosition = editor.getCursor(); const modal = new TimerModal(this.plugin); - modal.onStop = async (result: Blob | string) => { + modal.onStop = async (result: Blob | string, audioBlob?: Blob) => { // Handle both streaming (string) and legacy (Blob) results if (typeof result === 'string') { // Streaming mode - transcription already done - await this.plugin.recordingProcessor.processStreamingResult(result, activeFile, cursorPosition); + // If we have audio blob, save it first + let audioFilePath: string | undefined; + if (audioBlob) { + const { AudioFileManager } = await import('../utils/audio/AudioFileManager'); + const audioFileManager = new AudioFileManager(this.plugin); + audioFilePath = await audioFileManager.saveAudioFile(audioBlob); + } + + await this.plugin.recordingProcessor.processStreamingResult(result, activeFile, cursorPosition, audioFilePath); } else { // Legacy mode - need to transcribe await this.plugin.recordingProcessor.processRecording(result, activeFile, cursorPosition); diff --git a/src/utils/DebugLogger.ts b/src/utils/DebugLogger.ts new file mode 100644 index 0000000..dd6f309 --- /dev/null +++ b/src/utils/DebugLogger.ts @@ -0,0 +1,133 @@ +import NeuroVoxPlugin from '../main'; + +export interface DebugLogEntry { + timestamp: number; + category: 'audio' | 'api' | 'file' | 'chunk' | 'general'; + operation: string; + details: any; + duration?: number; +} + +export class DebugLogger { + private logs: DebugLogEntry[] = []; + private operationTimers: Map = new Map(); + + constructor(private plugin: NeuroVoxPlugin) {} + + isEnabled(): boolean { + return this.plugin.settings.debugMode; + } + + log(category: DebugLogEntry['category'], operation: string, details: any): void { + if (!this.isEnabled()) return; + + const entry: DebugLogEntry = { + timestamp: Date.now(), + category, + operation, + details + }; + + this.logs.push(entry); + console.log(`[NeuroVox Debug] ${category.toUpperCase()} - ${operation}:`, details); + } + + startTimer(operationId: string): void { + if (!this.isEnabled()) return; + this.operationTimers.set(operationId, Date.now()); + } + + endTimer(operationId: string, category: DebugLogEntry['category'], operation: string, details: any = {}): void { + if (!this.isEnabled()) return; + + const startTime = this.operationTimers.get(operationId); + if (startTime) { + const duration = Date.now() - startTime; + this.operationTimers.delete(operationId); + + const entry: DebugLogEntry = { + timestamp: Date.now(), + category, + operation, + details, + duration + }; + + this.logs.push(entry); + console.log(`[NeuroVox Debug] ${category.toUpperCase()} - ${operation} (${duration}ms):`, details); + } + } + + getLogs(): DebugLogEntry[] { + return [...this.logs]; + } + + getFormattedLogs(): string { + if (this.logs.length === 0) { + return 'No debug logs available.'; + } + + const lines: string[] = ['## 🐛 Debug Log\n']; + + // Group by category + const categories = ['audio', 'chunk', 'api', 'file', 'general'] as const; + + for (const category of categories) { + const categoryLogs = this.logs.filter(log => log.category === category); + if (categoryLogs.length === 0) continue; + + lines.push(`### ${this.getCategoryIcon(category)} ${category.toUpperCase()}\n`); + + for (const log of categoryLogs) { + const time = new Date(log.timestamp).toISOString().split('T')[1].split('.')[0]; + const durationStr = log.duration ? ` (${log.duration}ms)` : ''; + lines.push(`- **${time}** - ${log.operation}${durationStr}`); + + // Format details + if (log.details && Object.keys(log.details).length > 0) { + const detailsStr = Object.entries(log.details) + .map(([key, value]) => { + if (typeof value === 'object') { + return ` - ${key}: ${JSON.stringify(value, null, 2)}`; + } + return ` - ${key}: ${value}`; + }) + .join('\n'); + lines.push(detailsStr); + } + } + lines.push(''); + } + + // Add summary + lines.push('### 📊 Summary\n'); + const totalDuration = this.logs + .filter(log => log.duration) + .reduce((sum, log) => sum + (log.duration || 0), 0); + + lines.push(`- Total operations: ${this.logs.length}`); + lines.push(`- Total time: ${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)`); + lines.push(`- Audio operations: ${this.logs.filter(l => l.category === 'audio').length}`); + lines.push(`- API calls: ${this.logs.filter(l => l.category === 'api').length}`); + lines.push(`- File operations: ${this.logs.filter(l => l.category === 'file').length}`); + lines.push(`- Chunks processed: ${this.logs.filter(l => l.category === 'chunk').length}`); + + return lines.join('\n'); + } + + private getCategoryIcon(category: DebugLogEntry['category']): string { + const icons = { + audio: '🎵', + api: '🌐', + file: '📁', + chunk: '🧩', + general: '📝' + }; + return icons[category] || '📝'; + } + + clear(): void { + this.logs = []; + this.operationTimers.clear(); + } +} diff --git a/src/utils/RecordingProcessor.ts b/src/utils/RecordingProcessor.ts index 3e3a2b8..ad08568 100644 --- a/src/utils/RecordingProcessor.ts +++ b/src/utils/RecordingProcessor.ts @@ -4,6 +4,7 @@ import { AudioProcessor } from './audio/AudioProcessor'; import { TranscriptionService, TranscriptionResult } from './transcription/TranscriptionService'; import { DocumentInserter } from './document/DocumentInserter'; import { ProcessingState } from './state/ProcessingState'; +import { DebugLogger } from './DebugLogger'; /** * Configuration for the processing pipeline @@ -113,7 +114,8 @@ export class RecordingProcessor { public async processStreamingResult( transcriptionResult: string, activeFile: TFile, - cursorPosition: EditorPosition + cursorPosition: EditorPosition, + audioFilePath?: string ): Promise { if (this.processingState.getIsProcessing()) { throw new Error('Recording is already in progress.'); @@ -142,7 +144,7 @@ export class RecordingProcessor { { transcription: transcriptionResult, postProcessing, - // No audioFilePath for streaming mode + audioFilePath, transcriptionProvider: this.plugin.settings.transcriptionProvider, transcriptionModel: this.plugin.settings.transcriptionModel, postProcessingProvider: this.plugin.settings.postProcessingProvider, From 7824e6826816034ba7ca626c312a21dc5620947c Mon Sep 17 00:00:00 2001 From: Krzysztof Palikowski Date: Sat, 20 Dec 2025 23:26:55 +0100 Subject: [PATCH 6/8] fixing audio and transcription on mobile --- src/modals/TimerModal.ts | 79 +++++++++++++------ src/ui/RecordingUI.ts | 35 +++++++- src/utils/RecordingProcessor.ts | 21 ++++- src/utils/document/DocumentInserter.ts | 6 ++ .../StreamingTranscriptionService.ts | 36 ++++++--- 5 files changed, 140 insertions(+), 37 deletions(-) diff --git a/src/modals/TimerModal.ts b/src/modals/TimerModal.ts index ae46b62..ce21fbb 100644 --- a/src/modals/TimerModal.ts +++ b/src/modals/TimerModal.ts @@ -300,39 +300,63 @@ export class TimerModal extends Modal { try { const finalBlob = await this.recordingManager.stop(); - let result: Blob | string; - let audioBlob: Blob | undefined; - if (this.useStreaming && this.streamingService) { - // Streaming mode - get transcription result - new Notice('Finishing transcription...'); - result = await this.streamingService.finishProcessing(); + // Streaming mode - keep modal open and show processing status + const audioBlob = finalBlob || undefined; + + // Update UI to show processing state + this.ui?.updateState('stopped'); + this.ui?.updateTimer('Processing...'); + this.ui?.hideDebugInfo(); + + // Get stats before processing + const stats = this.streamingService.getStats(); - if (!result || result.trim().length === 0) { - throw new Error('No transcription result received'); + // Show notice with chunk status if debug mode is enabled + if (this.plugin.settings.debugMode) { + new Notice(`Processing ${stats.queueStats.queueSize} remaining chunks...`); } - // Store audio blob for saving even in streaming mode - audioBlob = finalBlob || undefined; + // Continue processing with modal still open + new Notice('Finishing transcription...'); + + try { + const result = await this.streamingService.finishProcessing(); + + if (!result || result.trim().length === 0) { + throw new Error('No transcription result received'); + } + + // Now close modal after successful processing + this.cleanup(); + super.close(); + + // Call onStop handler with results + if (this.onStop) { + await this.onStop(result, audioBlob); + } + } catch (processingError) { + // Close modal on error too + this.cleanup(); + super.close(); + + const errorMsg = processingError instanceof Error ? processingError.message : 'Unknown error'; + new Notice(`❌ Transcription failed: ${errorMsg}`); + throw processingError; + } } else { // Legacy mode - return audio blob if (!finalBlob) { throw new Error('No audio data received from recorder'); } - result = finalBlob; - } + + // Close recording modal first + this.cleanup(); + super.close(); - // Close recording modal first - this.cleanup(); - super.close(); - - // Always save the recording - if (this.onStop) { - // Pass both result and audio blob if in streaming mode - if (this.useStreaming && audioBlob) { - await this.onStop(result, audioBlob); - } else { - await this.onStop(result); + // Always save the recording + if (this.onStop) { + await this.onStop(finalBlob); } } } catch (error) { @@ -363,11 +387,20 @@ export class TimerModal extends Modal { * Updates the timer display */ private updateTimerDisplay(): void { + // Update timer this.ui.updateTimer( this.seconds, this.CONFIG.maxDuration, this.CONFIG.warningThreshold ); + + // In debug mode with streaming, show chunk queue status on separate line + if (this.plugin.settings.debugMode && this.useStreaming && this.streamingService) { + const stats = this.streamingService.getStats(); + this.ui.updateDebugInfo(stats.queueStats.queueSize, stats.processedChunks); + } else { + this.ui.hideDebugInfo(); + } } /** diff --git a/src/ui/RecordingUI.ts b/src/ui/RecordingUI.ts index cec669a..d6a3782 100644 --- a/src/ui/RecordingUI.ts +++ b/src/ui/RecordingUI.ts @@ -14,6 +14,7 @@ export interface RecordingUIHandlers { */ export class RecordingUI { private timerText: HTMLElement; + private debugText: HTMLElement | null = null; private pauseButton: TouchableButton; private stopButton: TouchableButton; private waveContainer: HTMLElement; @@ -70,6 +71,13 @@ export class RecordingUI { cls: 'neurovox-timer-display', text: '00:00' }); + + // Create debug text element (hidden by default) + this.debugText = this.container.createDiv({ + cls: 'neurovox-debug-display', + text: '' + }); + this.debugText.style.display = 'none'; } private createControls(): void { @@ -110,14 +118,35 @@ export class RecordingUI { } } - public updateTimer(seconds: number, maxDuration: number, warningThreshold: number): void { + public updateTimer(seconds: number | string, maxDuration?: number, warningThreshold?: number): void { + if (typeof seconds === 'string') { + // Allow custom text (e.g., "Processing...") + this.timerText.setText(seconds); + return; + } + const minutes = Math.floor(seconds / 60).toString().padStart(2, '0'); const remainingSeconds = (seconds % 60).toString().padStart(2, '0'); this.timerText.setText(`${minutes}:${remainingSeconds}`); - const timeLeft = maxDuration - seconds; - this.timerText.toggleClass('is-warning', timeLeft <= warningThreshold); + if (maxDuration !== undefined && warningThreshold !== undefined) { + const timeLeft = maxDuration - seconds; + this.timerText.toggleClass('is-warning', timeLeft <= warningThreshold); + } + } + + public updateDebugInfo(queueSize: number, processedCount: number): void { + if (this.debugText) { + this.debugText.setText(`Queue: ${queueSize} | Processed: ${processedCount}`); + this.debugText.style.display = 'block'; + } + } + + public hideDebugInfo(): void { + if (this.debugText) { + this.debugText.style.display = 'none'; + } } public updateState(state: RecordingState): void { diff --git a/src/utils/RecordingProcessor.ts b/src/utils/RecordingProcessor.ts index ad08568..e582639 100644 --- a/src/utils/RecordingProcessor.ts +++ b/src/utils/RecordingProcessor.ts @@ -32,12 +32,15 @@ export class RecordingProcessor { maxRetries: 3, retryDelay: 1000 }; + + private debugLogger: DebugLogger; private constructor(private plugin: NeuroVoxPlugin) { this.processingState = new ProcessingState(); this.audioProcessor = new AudioProcessor(plugin); this.transcriptionService = new TranscriptionService(plugin); this.documentInserter = new DocumentInserter(plugin); + this.debugLogger = new DebugLogger(plugin); } public static getInstance(plugin: NeuroVoxPlugin): RecordingProcessor { @@ -124,6 +127,12 @@ export class RecordingProcessor { try { this.processingState.setIsProcessing(true); this.processingState.reset(); + this.debugLogger.clear(); + + this.debugLogger.log('general', 'Streaming result processing started', { + transcriptionLength: transcriptionResult.length, + hasAudioFile: !!audioFilePath + }); // Skip audio processing since we already have the transcription this.processingState.startStep('Content Processing'); @@ -132,9 +141,15 @@ export class RecordingProcessor { let postProcessing: string | undefined; if (this.plugin.settings.generatePostProcessing) { this.processingState.startStep('Post-processing'); + this.debugLogger.startTimer('post-processing'); postProcessing = await this.executeWithRetry(() => this.generatePostProcessing(transcriptionResult) ); + this.debugLogger.endTimer('post-processing', 'api', 'Post-processing generation', { + provider: this.plugin.settings.postProcessingProvider, + model: this.plugin.settings.postProcessingModel, + resultLength: postProcessing?.length || 0 + }); this.processingState.completeStep(); } @@ -148,7 +163,8 @@ export class RecordingProcessor { transcriptionProvider: this.plugin.settings.transcriptionProvider, transcriptionModel: this.plugin.settings.transcriptionModel, postProcessingProvider: this.plugin.settings.postProcessingProvider, - postProcessingModel: this.plugin.settings.postProcessingModel + postProcessingModel: this.plugin.settings.postProcessingModel, + debugLogs: this.plugin.settings.debugMode ? this.debugLogger.getFormattedLogs() : undefined }, activeFile, cursorPosition @@ -156,6 +172,9 @@ export class RecordingProcessor { this.processingState.completeStep(); } catch (error) { + this.debugLogger.log('general', 'Processing error', { + error: error instanceof Error ? error.message : 'Unknown error' + }); this.handleError('Processing failed', error); this.processingState.setError(error as Error); throw error; diff --git a/src/utils/document/DocumentInserter.ts b/src/utils/document/DocumentInserter.ts index 66ef610..f6d81e6 100644 --- a/src/utils/document/DocumentInserter.ts +++ b/src/utils/document/DocumentInserter.ts @@ -12,6 +12,7 @@ export interface InsertContent { transcriptionModel?: string; postProcessingProvider?: string; postProcessingModel?: string; + debugLogs?: string; } /** @@ -109,6 +110,11 @@ export class DocumentInserter { formattedContent += '\n---\n' + postContent + '\n\n'; } + // Add debug logs if available + if (content.debugLogs) { + formattedContent += '\n---\n' + content.debugLogs + '\n\n'; + } + return formattedContent + '\n'; } diff --git a/src/utils/transcription/StreamingTranscriptionService.ts b/src/utils/transcription/StreamingTranscriptionService.ts index 4d7bb87..cfacbda 100644 --- a/src/utils/transcription/StreamingTranscriptionService.ts +++ b/src/utils/transcription/StreamingTranscriptionService.ts @@ -114,37 +114,53 @@ export class StreamingTranscriptionService { } async finishProcessing(): Promise { - // Stop accepting new chunks - this.isProcessing = false; + const initialQueueSize = this.chunkQueue.size(); + + if (this.plugin.settings.debugMode) { + console.log(`[NeuroVox Debug] Starting finishProcessing with ${initialQueueSize} chunks in queue, ${this.processedChunks.size} already processed`); + } - // Wait for queue to be processed + // Keep processing running until queue is empty let attempts = 0; - const maxAttempts = 300; // 30 seconds timeout + const maxAttempts = 600; // 60 seconds timeout while (this.chunkQueue.size() > 0 && attempts < maxAttempts) { + if (this.plugin.settings.debugMode && attempts % 10 === 0) { + console.log(`[NeuroVox Debug] Waiting for chunks: ${this.chunkQueue.size()} remaining, ${this.processedChunks.size} processed`); + } await this.sleep(100); attempts++; } - // Abort if still processing after timeout - if (this.abortController) { - this.abortController.abort(); + // Now stop accepting new chunks and stop processing + this.isProcessing = false; + + if (this.plugin.settings.debugMode) { + console.log(`[NeuroVox Debug] Queue processing complete. Processed: ${this.processedChunks.size}, Remaining: ${this.chunkQueue.size()}`); } - // Wait for processing to complete + // Wait for processing loop to complete if (this.processingPromise) { try { await this.processingPromise; } catch (error) { - // Silent fail + if (this.plugin.settings.debugMode) { + console.error('[NeuroVox Debug] Error in processing promise:', error); + } } } // Get final result - return this.resultCompiler.getFinalResult( + const result = this.resultCompiler.getFinalResult( this.plugin.settings.includeTimestamps || false, true // Include metadata ); + + if (this.plugin.settings.debugMode) { + console.log(`[NeuroVox Debug] Final result length: ${result.length} characters, segments: ${this.resultCompiler.getSegmentCount()}`); + } + + return result; } getPartialResult(): string { From b55ae644156044b0d601ce114ca4d9384cef48e9 Mon Sep 17 00:00:00 2001 From: Krzysztof Palikowski Date: Sun, 21 Dec 2025 00:43:15 +0100 Subject: [PATCH 7/8] readme info --- README.md | 46 +++++++++++++++------------------------------- manifest.json | 8 ++++---- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 07f1dc1..d9723e2 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ # NeuroVox -NeuroVox is an Obsidian plugin that enhances your note-taking with voice transcription and AI capabilities. Record your voice, transcribe it, and apply custom AI prompts to the transcription. +NeuroVoxSalad is a fork of NeuroVox 1.0.4 (https://github.com/Synaptic-Labs-AI/NeuroVox) Obsidian plugin that enhances your note-taking with voice transcription and AI capabilities. Record your voice, transcribe it, and apply custom AI prompts to the transcription. -## Features +## Features (new in this fork) -- **Voice Recording**: A mic icon will appear in your note, which you can press to record. -- **Transcription**: Automatically transcribes your voice recordings using the [OpenAI Whisper API](https://openai.com/index/whisper/) along with Groq. -- **Custom Prompts**: Apply custom prompts to the transcription to summarize, extract to-dos, or other actions. -- **Audio Playback**: Embeds the audio file in your note for easy access. -- **Embedded Output**: Transcriptions and AI-generated outputs are embedded in your notes as callouts wherever your cursor is. +- Ability to enter Salad Transcription API key and use it +- Abillity to add Perplexity Sonar as post-processing +- Debug mode for mobiles +- Fixed a bug causing audio file was not created on mobiles ## Installation -1. Download the NeuroVox plugin from Community Plugins. -2. Enable the plugin from the Obsidian settings by toggling it on. -3. Input your OpenAI, Deepgram and/or Groq API Key (instructions below). -4. Choose a folder to save the Recordings. -5. Turn on the Floating Button Mic (optional), or otherwise use the toolbar icon or command pallette to start a recording. +Manual: +- Create neurovoxsalad folder in your .obsidian/plugins +- copy 3 files into this folder: +-- main.js +-- manifest.json +-- styles.css +- restart obsidian +- activate plugin ## API Key Setup @@ -24,25 +26,7 @@ If you need to obtain an OpenAI API key, follow the steps below: ### Steps to Get an API Key -1. **Create an Account**: - - Visit the [OpenAI website](https://platform.openai.com) and sign up for an account. - - Visit the [Groq website](https://console.groq.com/) to get an account there. - - Visit the [Deepgram website](https://console.deepgram.com/signup) to create an account. - -2. **Access API Keys**: - - Log in to your account. - - **OpenAI**: Click on the ⚙️ in the top right corner and select "API Keys" from the dropdown menu. - - **Groq**: Click "API Keys" on the left sidebar. - - **Deepgram**: Click the "Free API key button in the top right. - -3. **Create a New Key**: - - On the API Keys page, click "Create new secret key." - -4. **Secure Your API Key**: - - Copy the newly generated API key into the `🔑 Api Keys` accordion in the Neurovox Settings. Treat this key like a password and do not share it with anyone. - -5. **Billing Information**: - - You need to add billing information to your account to make API calls. +same as in original plugin (but you can add Salad Transcription API and Preplexity Sonar, yay!) ## Contribution diff --git a/manifest.json b/manifest.json index 3450373..9e5c494 100644 --- a/manifest.json +++ b/manifest.json @@ -1,12 +1,12 @@ { "id": "neurovoxsalad", "name": "NeuroVoxSalad", - "version": "1.0.4", + "version": "1.0.5", "minAppVersion": "0.15.0", "description": "Enhances your note-taking with voice transcription and AI capabilities", - "author": "Synaptic Labs", - "authorUrl": "https://www.synapticlabs.ai", - "fundingUrl": "https://donate.stripe.com/bIY4gsgDo2mJ5kkfZ6", + "author": "Synaptic Labs + some new features by palik.it", + "authorUrl": "https://palik.it", + "fundingUrl": "https://buycoffee.to/palik", "isDesktopOnly": false, "settings": { "showFloatingButton": true From 1b32af0ab89a4c5f84738ec700bdba13c0bbaacc Mon Sep 17 00:00:00 2001 From: Krzysztof Palikowski Date: Sun, 21 Dec 2025 00:50:52 +0100 Subject: [PATCH 8/8] Revert README.md and manifest.json to upstream state --- README.md | 51 ++++++++++++++++++++++++++++++++++++--------------- manifest.json | 12 ++++++------ 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index d9723e2..573bd2b 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,22 @@ # NeuroVox -NeuroVoxSalad is a fork of NeuroVox 1.0.4 (https://github.com/Synaptic-Labs-AI/NeuroVox) Obsidian plugin that enhances your note-taking with voice transcription and AI capabilities. Record your voice, transcribe it, and apply custom AI prompts to the transcription. +NeuroVox is an Obsidian plugin that enhances your note-taking with voice transcription and AI capabilities. Record your voice, transcribe it, and apply custom AI prompts to the transcription. -## Features (new in this fork) +## Features -- Ability to enter Salad Transcription API key and use it -- Abillity to add Perplexity Sonar as post-processing -- Debug mode for mobiles -- Fixed a bug causing audio file was not created on mobiles +- **Voice Recording**: A mic icon will appear in your note, which you can press to record. +- **Transcription**: Automatically transcribes your voice recordings using the [OpenAI Whisper API](https://openai.com/index/whisper/) along with Groq. +- **Custom Prompts**: Apply custom prompts to the transcription to summarize, extract to-dos, or other actions. +- **Audio Playback**: Embeds the audio file in your note for easy access. +- **Embedded Output**: Transcriptions and AI-generated outputs are embedded in your notes as callouts wherever your cursor is. ## Installation -Manual: -- Create neurovoxsalad folder in your .obsidian/plugins -- copy 3 files into this folder: --- main.js --- manifest.json --- styles.css -- restart obsidian -- activate plugin +1. Download the NeuroVox plugin from Community Plugins. +2. Enable the plugin from the Obsidian settings by toggling it on. +3. Input your OpenAI, Deepgram and/or Groq API Key (instructions below). +4. Choose a folder to save the Recordings. +5. Turn on the Floating Button Mic (optional), or otherwise use the toolbar icon or command pallette to start a recording. ## API Key Setup @@ -26,7 +24,30 @@ If you need to obtain an OpenAI API key, follow the steps below: ### Steps to Get an API Key -same as in original plugin (but you can add Salad Transcription API and Preplexity Sonar, yay!) +1. **Create an Account**: + - Visit the [OpenAI website](https://platform.openai.com) and sign up for an account. + - Visit the [Groq website](https://console.groq.com/) to get an account there. + - Visit the [Deepgram website](https://console.deepgram.com/signup) to create an account. + - Visit the [Salad Portal](https://portal.salad.com/) to create an account. + + - Visit [Perplexity](https://www.perplexity.ai/) to create an account. + +2. **Access API Keys**: + - Log in to your account. + - **OpenAI**: Click on the ⚙️ in the top right corner and select "API Keys" from the dropdown menu. + - **Groq**: Click "API Keys" on the left sidebar. + - **Deepgram**: Click the "Free API key button in the top right. + - **Salad**: go to "API Access" in menu + - **Perplexity**: click on profile icon and select API + +3. **Create a New Key**: + - On the API Keys page, click "Create new secret key." + +4. **Secure Your API Key**: + - Copy the newly generated API key into the `🔑 Api Keys` accordion in the Neurovox Settings. Treat this key like a password and do not share it with anyone. + +5. **Billing Information**: + - You need to add billing information to your account to make API calls. ## Contribution diff --git a/manifest.json b/manifest.json index 9e5c494..bd5d46f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,12 +1,12 @@ { - "id": "neurovoxsalad", - "name": "NeuroVoxSalad", - "version": "1.0.5", + "id": "neurovox", + "name": "NeuroVox", + "version": "1.0.4", "minAppVersion": "0.15.0", "description": "Enhances your note-taking with voice transcription and AI capabilities", - "author": "Synaptic Labs + some new features by palik.it", - "authorUrl": "https://palik.it", - "fundingUrl": "https://buycoffee.to/palik", + "author": "Synaptic Labs", + "authorUrl": "https://www.synapticlabs.ai", + "fundingUrl": "https://donate.stripe.com/bIY4gsgDo2mJ5kkfZ6", "isDesktopOnly": false, "settings": { "showFloatingButton": true