Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf27867d20 | |||
| e2c4a5264c | |||
| c6a98d4050 | |||
| 9b7ec36828 |
@@ -75,11 +75,15 @@ Open **Settings** → **NibbleAI** and add at least one API key:
|
||||
|
||||
| Key | Used For |
|
||||
|-----|----------|
|
||||
| **Transcription API Key** | Whisper transcription (OpenAI, Groq, Azure) |
|
||||
| **OpenAI API Key** | Proofreading, title generation, property filling, post-processing *(recommended: OpenRouter)* |
|
||||
| **Anthropic API Key** | Post-processing with Claude models |
|
||||
| **Groq API Key** | Whisper transcription, and as a chat provider for proofreading/title/property/post-processing |
|
||||
| **OpenAI API Key** | Whisper transcription, and as a chat provider for proofreading/title/property/post-processing |
|
||||
| **Anthropic API Key** | Claude models for proofreading/title/property/post-processing |
|
||||
| **OpenRouter API Key** | Access to hundreds of models for proofreading/title/property/post-processing |
|
||||
| **Custom API Key + URL** | Any OpenAI-compatible endpoint (self-hosted, Azure, etc.) |
|
||||
|
||||
> **Tip:** For proofreading, title generation, and property filling, an [OpenRouter](https://openrouter.ai/keys) API key with a free model like `google/gemini-2.5-flash-lite` costs nothing and works great.
|
||||
Every LLM-backed feature (Proofreading, Title Generation, Property Filler, Post-Processing) shows the same provider dropdown — pick any provider that has a key configured above.
|
||||
|
||||
> **Tip:** An [OpenRouter](https://openrouter.ai/keys) API key with a free model like `google/gemini-2.5-flash-lite` costs nothing and works great across all four AI features.
|
||||
|
||||
### 2. Try the Features
|
||||
|
||||
@@ -172,13 +176,14 @@ Securely stored in your system keychain via Obsidian's SecretStorage. Never writ
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| Enable | Off | Clean up transcripts with an LLM |
|
||||
| Provider | Anthropic | Anthropic, OpenAI, or Custom |
|
||||
| Provider | Anthropic | Any configured provider: Anthropic, OpenAI, Groq, OpenRouter, or Custom |
|
||||
| Model | `claude-sonnet-4-20250514` | LLM for cleanup |
|
||||
| Auto-generate title | Off | Create filenames from content |
|
||||
|
||||
### Proofreading
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| Provider | OpenRouter | Any configured provider: Anthropic, OpenAI, Groq, OpenRouter, or Custom |
|
||||
| Model | `openai/gpt-4o-mini` | Model for proofreading |
|
||||
| Temperature | `0.2` | Lower = more consistent |
|
||||
| Max tokens | `2000` | Maximum response length |
|
||||
@@ -187,12 +192,16 @@ Securely stored in your system keychain via Obsidian's SecretStorage. Never writ
|
||||
### Title Generation
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| Provider | OpenRouter | Any configured provider: Anthropic, OpenAI, Groq, OpenRouter, or Custom |
|
||||
| Model | `openai/gpt-4o-mini` | Model for generating titles |
|
||||
| Mode | Rename file | Rename file or set frontmatter property |
|
||||
| Max title length | 8 words | Maximum words in generated title |
|
||||
|
||||
### Property Filler
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| Provider | OpenRouter | Any configured provider: Anthropic, OpenAI, Groq, OpenRouter, or Custom |
|
||||
| Model | `openai/gpt-4o-mini` | Model for generating property values |
|
||||
| Custom prompt | *(built-in)* | Template with `{property}` placeholder |
|
||||
| Overwrite existing | Off | Replace existing property values |
|
||||
|
||||
@@ -205,7 +214,8 @@ NibbleAI/
|
||||
├── main.ts # Plugin entry point, commands, events
|
||||
├── src/
|
||||
│ ├── SettingsManager.ts # Settings + SecretStorage management
|
||||
│ ├── AIService.ts # Unified AI client (OpenAI + Anthropic)
|
||||
│ ├── AIService.ts # Unified AI client (OpenAI-compatible + Anthropic-native)
|
||||
│ ├── ProviderResolver.ts # Shared provider → API key/endpoint resolution
|
||||
│ ├── ModelBrowser.ts # OpenRouter model suggest modal
|
||||
│ ├── PostProcessor.ts # LLM post-processing for transcripts
|
||||
│ ├── NibbleAISettingsTab.ts # Settings UI
|
||||
|
||||
@@ -89,6 +89,10 @@ export default class NibbleAI extends Plugin {
|
||||
new Notice("Already recording");
|
||||
return;
|
||||
}
|
||||
if (!this.audioHandler.getTranscriptionApiKey()) {
|
||||
new Notice("✘ Add your API key for the selected transcription provider in NibbleAI settings");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.recorder.startRecording();
|
||||
this.statusBar.updateStatus(RecordingStatus.Recording);
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "nibbleai",
|
||||
"name": "NibbleAI",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"minAppVersion": "1.12.7",
|
||||
"description": "AI-powered tools for your Obsidian vault: speech-to-text, proofreading, title generation, property filling, and more.",
|
||||
"author": "Olivier",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nibbleai",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"description": "AI-powered tools for your Obsidian vault",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ export interface ChatCompletionOptions {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
endpoint?: string;
|
||||
provider?: "anthropic" | "openai" | "openrouter" | "custom";
|
||||
provider?: "anthropic" | "openai" | "openrouter" | "groq" | "custom";
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
}
|
||||
|
||||
+143
-81
@@ -3,9 +3,8 @@ import { App, PluginSettingTab, Setting, setIcon } from "obsidian";
|
||||
import {
|
||||
SettingsManager,
|
||||
PostProcessingProvider,
|
||||
AIProvider,
|
||||
TranscriptionProvider,
|
||||
PROVIDER_URLS,
|
||||
CHAT_PROVIDERS,
|
||||
PROVIDER_DEFAULT_MODELS,
|
||||
} from "./SettingsManager";
|
||||
import { ModelSuggestModal } from "./ModelBrowser";
|
||||
@@ -106,10 +105,14 @@ export class NibbleAISettingsTab extends PluginSettingTab {
|
||||
text: "API keys are stored securely in your system keychain.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
containerEl.createEl("p", {
|
||||
text: "Each AI feature below (Proofreading, Title Generation, Property Filler, Post-Processing) lets you pick which of these providers to use, provided its key is set here.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
this.createApiKeySetting(containerEl,
|
||||
"Groq API Key",
|
||||
"For Whisper transcription via Groq (groq.com)",
|
||||
"For Whisper transcription and Groq chat models (groq.com)",
|
||||
"gsk_...",
|
||||
this.plugin.settings.groqApiKey,
|
||||
async (value) => {
|
||||
@@ -139,7 +142,7 @@ export class NibbleAISettingsTab extends PluginSettingTab {
|
||||
);
|
||||
this.createApiKeySetting(containerEl,
|
||||
"OpenRouter API Key",
|
||||
"For OpenRouter models — get one at openrouter.ai/keys. Used for proofreading, title generation, property filling, and post-processing.",
|
||||
"For OpenRouter models — get one at openrouter.ai/keys.",
|
||||
"sk-or-v1-...",
|
||||
this.plugin.settings.openRouterApiKey,
|
||||
async (value) => {
|
||||
@@ -147,6 +150,26 @@ export class NibbleAISettingsTab extends PluginSettingTab {
|
||||
await this.save();
|
||||
}
|
||||
);
|
||||
this.createApiKeySetting(containerEl,
|
||||
"Custom API Key",
|
||||
"For a self-hosted or OpenAI-compatible endpoint not listed above",
|
||||
"sk-...xxxx",
|
||||
this.plugin.settings.customApiKey,
|
||||
async (value) => {
|
||||
this.plugin.settings.customApiKey = value;
|
||||
await this.save();
|
||||
}
|
||||
);
|
||||
this.createTextSetting(containerEl,
|
||||
"Custom API URL",
|
||||
"Chat completions endpoint for the custom provider",
|
||||
"https://api.example.com/v1/chat/completions",
|
||||
this.plugin.settings.customApiUrl,
|
||||
async (value) => {
|
||||
this.plugin.settings.customApiUrl = value;
|
||||
await this.save();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ── Transcription Tab ──
|
||||
@@ -328,29 +351,22 @@ export class NibbleAISettingsTab extends PluginSettingTab {
|
||||
);
|
||||
|
||||
if (this.plugin.settings.postProcessing) {
|
||||
this.createPostProcessingProviderSetting(containerEl);
|
||||
this.createChatProviderSetting(containerEl,
|
||||
"Select the LLM provider for cleaning up transcriptions",
|
||||
this.plugin.settings.postProcessingProvider,
|
||||
async (provider) => {
|
||||
this.plugin.settings.postProcessingProvider = provider;
|
||||
if (provider !== "custom") {
|
||||
this.plugin.settings.postProcessingModel =
|
||||
PROVIDER_DEFAULT_MODELS[provider];
|
||||
}
|
||||
await this.save();
|
||||
this.display();
|
||||
}
|
||||
);
|
||||
|
||||
if (this.plugin.settings.postProcessingProvider === "custom") {
|
||||
this.createTextSetting(containerEl,
|
||||
"API URL",
|
||||
"Custom endpoint for chat completions",
|
||||
"https://api.example.com/v1/chat/completions",
|
||||
this.plugin.settings.postProcessingUrl,
|
||||
async (value) => {
|
||||
this.plugin.settings.postProcessingUrl = value;
|
||||
await this.save();
|
||||
}
|
||||
);
|
||||
this.createApiKeySetting(containerEl,
|
||||
"API Key",
|
||||
"API key for the custom endpoint",
|
||||
"sk-...xxxx",
|
||||
this.plugin.settings.customApiKey,
|
||||
async (value) => {
|
||||
this.plugin.settings.customApiKey = value;
|
||||
await this.save();
|
||||
}
|
||||
);
|
||||
this.createCustomProviderFields(containerEl);
|
||||
}
|
||||
|
||||
this.createModelSetting(containerEl,
|
||||
@@ -424,15 +440,23 @@ export class NibbleAISettingsTab extends PluginSettingTab {
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
this.createProviderSetting(containerEl,
|
||||
"Provider",
|
||||
this.createChatProviderSetting(containerEl,
|
||||
"Select the AI provider for proofreading",
|
||||
this.plugin.settings.proofreadingProvider,
|
||||
async (value) => {
|
||||
this.plugin.settings.proofreadingProvider = value as AIProvider;
|
||||
async (provider) => {
|
||||
this.plugin.settings.proofreadingProvider = provider;
|
||||
if (provider !== "custom") {
|
||||
this.plugin.settings.proofreadingModel =
|
||||
PROVIDER_DEFAULT_MODELS[provider];
|
||||
}
|
||||
await this.save();
|
||||
this.display();
|
||||
}
|
||||
);
|
||||
|
||||
if (this.plugin.settings.proofreadingProvider === "custom") {
|
||||
this.createCustomProviderFields(containerEl);
|
||||
}
|
||||
this.createModelSetting(containerEl,
|
||||
"Model",
|
||||
"Model for fixing punctuation and grammar",
|
||||
@@ -508,15 +532,23 @@ export class NibbleAISettingsTab extends PluginSettingTab {
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
this.createProviderSetting(containerEl,
|
||||
"Provider",
|
||||
this.createChatProviderSetting(containerEl,
|
||||
"Select the AI provider for title generation",
|
||||
this.plugin.settings.titleGenProvider,
|
||||
async (value) => {
|
||||
this.plugin.settings.titleGenProvider = value as AIProvider;
|
||||
async (provider) => {
|
||||
this.plugin.settings.titleGenProvider = provider;
|
||||
if (provider !== "custom") {
|
||||
this.plugin.settings.titleGenModel =
|
||||
PROVIDER_DEFAULT_MODELS[provider];
|
||||
}
|
||||
await this.save();
|
||||
this.display();
|
||||
}
|
||||
);
|
||||
|
||||
if (this.plugin.settings.titleGenProvider === "custom") {
|
||||
this.createCustomProviderFields(containerEl);
|
||||
}
|
||||
this.createModelSetting(containerEl,
|
||||
"Model",
|
||||
"Model for generating titles",
|
||||
@@ -577,15 +609,23 @@ export class NibbleAISettingsTab extends PluginSettingTab {
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
|
||||
this.createProviderSetting(containerEl,
|
||||
"Provider",
|
||||
this.createChatProviderSetting(containerEl,
|
||||
"Select the AI provider for property generation",
|
||||
this.plugin.settings.propertyGenProvider,
|
||||
async (value) => {
|
||||
this.plugin.settings.propertyGenProvider = value as AIProvider;
|
||||
async (provider) => {
|
||||
this.plugin.settings.propertyGenProvider = provider;
|
||||
if (provider !== "custom") {
|
||||
this.plugin.settings.propertyGenModel =
|
||||
PROVIDER_DEFAULT_MODELS[provider];
|
||||
}
|
||||
await this.save();
|
||||
this.display();
|
||||
}
|
||||
);
|
||||
|
||||
if (this.plugin.settings.propertyGenProvider === "custom") {
|
||||
this.createCustomProviderFields(containerEl);
|
||||
}
|
||||
this.createModelSetting(containerEl,
|
||||
"Model",
|
||||
"Model for generating property values",
|
||||
@@ -748,30 +788,53 @@ export class NibbleAISettingsTab extends PluginSettingTab {
|
||||
infoEl.style.flex = "1 1 100%";
|
||||
}
|
||||
|
||||
private createProviderSetting(
|
||||
/**
|
||||
* Shared provider dropdown for every LLM-backed feature (Post-Processing,
|
||||
* Proofreading, Title Generation, Property Filler). Reads from the single
|
||||
* CHAT_PROVIDERS registry so all four features always offer the exact
|
||||
* same providers, and disables any provider whose API key isn't set yet.
|
||||
*/
|
||||
private createChatProviderSetting(
|
||||
containerEl: HTMLElement,
|
||||
name: string,
|
||||
desc: string,
|
||||
value: string,
|
||||
onChange: (value: string) => Promise<void>
|
||||
currentProvider: PostProcessingProvider,
|
||||
onChange: (provider: PostProcessingProvider) => Promise<void>
|
||||
): void {
|
||||
const providers: Record<string, string> = {
|
||||
openrouter: "OpenRouter",
|
||||
openai: "OpenAI",
|
||||
anthropic: "Anthropic",
|
||||
custom: "Custom",
|
||||
};
|
||||
const configured = new Set(
|
||||
CHAT_PROVIDERS.filter((p) => !!this.plugin.settings[p.apiKeyField]).map(
|
||||
(p) => p.id
|
||||
)
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(name)
|
||||
.setName("Provider")
|
||||
.setDesc(desc)
|
||||
.addDropdown((dropdown) => {
|
||||
for (const [val, label] of Object.entries(providers)) {
|
||||
dropdown.addOption(val, label);
|
||||
for (const p of CHAT_PROVIDERS) {
|
||||
dropdown.addOption(
|
||||
p.id,
|
||||
configured.has(p.id) ? p.label : `${p.label} (no API key)`
|
||||
);
|
||||
}
|
||||
dropdown.setValue(value || "openrouter");
|
||||
dropdown.onChange(async (val) => await onChange(val));
|
||||
dropdown.setValue(currentProvider || "openrouter");
|
||||
dropdown.onChange(async (value) => {
|
||||
await onChange(value as PostProcessingProvider);
|
||||
});
|
||||
for (const p of CHAT_PROVIDERS) {
|
||||
if (configured.has(p.id)) continue;
|
||||
const optionEl = dropdown.selectEl.querySelector(
|
||||
`option[value="${p.id}"]`
|
||||
) as HTMLOptionElement | null;
|
||||
if (optionEl) optionEl.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (configured.size === 0) {
|
||||
containerEl.createEl("p", {
|
||||
text: "⚠️ No AI provider configured yet — add an API key in the API Keys tab.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private createModelSetting(
|
||||
@@ -860,36 +923,35 @@ export class NibbleAISettingsTab extends PluginSettingTab {
|
||||
);
|
||||
}
|
||||
|
||||
private createPostProcessingProviderSetting(containerEl: HTMLElement): void {
|
||||
const providers: Record<PostProcessingProvider, string> = {
|
||||
anthropic: "Anthropic",
|
||||
openai: "OpenAI",
|
||||
openrouter: "OpenRouter",
|
||||
custom: "Custom",
|
||||
};
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Provider")
|
||||
.setDesc("Select the LLM provider")
|
||||
.addDropdown((dropdown) => {
|
||||
for (const [value, label] of Object.entries(providers)) {
|
||||
dropdown.addOption(value, label);
|
||||
}
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.postProcessingProvider)
|
||||
.onChange(async (value) => {
|
||||
const provider = value as PostProcessingProvider;
|
||||
this.plugin.settings.postProcessingProvider = provider;
|
||||
if (provider !== "custom") {
|
||||
this.plugin.settings.postProcessingUrl =
|
||||
PROVIDER_URLS[provider];
|
||||
this.plugin.settings.postProcessingModel =
|
||||
PROVIDER_DEFAULT_MODELS[provider];
|
||||
}
|
||||
/**
|
||||
* Custom endpoint + key fields, shown whenever a feature's provider is
|
||||
* set to "Custom". This slot is shared across all four AI features.
|
||||
*/
|
||||
private createCustomProviderFields(containerEl: HTMLElement): void {
|
||||
containerEl.createEl("p", {
|
||||
text: "Custom provider connection \u2014 shared across Post-Processing, Proofreading, Title Generation, and Property Filler.",
|
||||
cls: "setting-item-description",
|
||||
});
|
||||
this.createTextSetting(containerEl,
|
||||
"Custom API URL",
|
||||
"OpenAI-compatible chat completions endpoint",
|
||||
"https://api.example.com/v1/chat/completions",
|
||||
this.plugin.settings.customApiUrl,
|
||||
async (value) => {
|
||||
this.plugin.settings.customApiUrl = value;
|
||||
await this.save();
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
this.createApiKeySetting(containerEl,
|
||||
"Custom API Key",
|
||||
"API key for the custom endpoint",
|
||||
"sk-...xxxx",
|
||||
this.plugin.settings.customApiKey,
|
||||
async (value) => {
|
||||
this.plugin.settings.customApiKey = value;
|
||||
await this.save();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async createAudioDeviceSetting(containerEl: HTMLElement): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CHAT_PROVIDERS, PROVIDER_URLS, PluginSettings, PostProcessingProvider } from "./SettingsManager";
|
||||
|
||||
export interface ResolvedChatProvider {
|
||||
apiKey: string;
|
||||
endpoint: string;
|
||||
provider: PostProcessingProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the API key + endpoint for a chat-completion provider selection.
|
||||
* Shared by every AI feature that talks to an LLM (Post-Processing,
|
||||
* Proofreading, Title Generation, Property Filler) so they all treat
|
||||
* providers — including "no key configured" — identically.
|
||||
*/
|
||||
export function resolveChatProvider(
|
||||
settings: PluginSettings,
|
||||
provider: PostProcessingProvider
|
||||
): ResolvedChatProvider | null {
|
||||
const info = CHAT_PROVIDERS.find((p) => p.id === provider);
|
||||
if (!info) return null;
|
||||
|
||||
const apiKey = settings[info.apiKeyField];
|
||||
const endpoint = provider === "custom" ? settings.customApiUrl : PROVIDER_URLS[provider];
|
||||
|
||||
if (!apiKey || !endpoint) return null;
|
||||
return { apiKey, endpoint, provider };
|
||||
}
|
||||
+47
-9
@@ -1,6 +1,6 @@
|
||||
import { Plugin } from "obsidian";
|
||||
|
||||
const SECRET_IDS: Record<keyof ApiKeysSettings, string> = {
|
||||
const SECRET_IDS: Partial<Record<keyof ApiKeysSettings, string>> = {
|
||||
groqApiKey: "groq-api-key",
|
||||
openAiApiKey: "openai-api-key",
|
||||
anthropicApiKey: "anthropic-api-key",
|
||||
@@ -8,7 +8,7 @@ const SECRET_IDS: Record<keyof ApiKeysSettings, string> = {
|
||||
openRouterApiKey: "openrouter-api-key",
|
||||
};
|
||||
|
||||
export type PostProcessingProvider = "anthropic" | "openai" | "openrouter" | "custom";
|
||||
export type PostProcessingProvider = "anthropic" | "openai" | "openrouter" | "groq" | "custom";
|
||||
|
||||
export type AIProvider = PostProcessingProvider;
|
||||
|
||||
@@ -16,6 +16,7 @@ export const PROVIDER_URLS: Record<PostProcessingProvider, string> = {
|
||||
anthropic: "https://api.anthropic.com/v1/messages",
|
||||
openai: "https://api.openai.com/v1/chat/completions",
|
||||
openrouter: "https://openrouter.ai/api/v1/chat/completions",
|
||||
groq: "https://api.groq.com/openai/v1/chat/completions",
|
||||
custom: "",
|
||||
};
|
||||
|
||||
@@ -23,9 +24,30 @@ export const PROVIDER_DEFAULT_MODELS: Record<PostProcessingProvider, string> = {
|
||||
anthropic: "claude-sonnet-4-20250514",
|
||||
openai: "gpt-4o-mini",
|
||||
openrouter: "openai/gpt-4o-mini",
|
||||
groq: "llama-3.3-70b-versatile",
|
||||
custom: "",
|
||||
};
|
||||
|
||||
export interface ChatProviderInfo {
|
||||
id: PostProcessingProvider;
|
||||
label: string;
|
||||
apiKeyField: keyof ApiKeysSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single source of truth for which chat-completion providers exist and which
|
||||
* API key each one uses. UI dropdowns and provider resolution both read from
|
||||
* this list so every AI feature (Post-Processing, Proofreading, Title
|
||||
* Generation, Property Filler) offers the exact same providers.
|
||||
*/
|
||||
export const CHAT_PROVIDERS: ChatProviderInfo[] = [
|
||||
{ id: "anthropic", label: "Anthropic", apiKeyField: "anthropicApiKey" },
|
||||
{ id: "openai", label: "OpenAI", apiKeyField: "openAiApiKey" },
|
||||
{ id: "groq", label: "Groq", apiKeyField: "groqApiKey" },
|
||||
{ id: "openrouter", label: "OpenRouter", apiKeyField: "openRouterApiKey" },
|
||||
{ id: "custom", label: "Custom", apiKeyField: "customApiKey" },
|
||||
];
|
||||
|
||||
export type TranscriptionProvider = "groq" | "openai" | "azure" | "custom";
|
||||
|
||||
export const TRANSCRIPTION_PROVIDER_URLS: Record<TranscriptionProvider, string> = {
|
||||
@@ -42,6 +64,7 @@ export interface ApiKeysSettings {
|
||||
openAiApiKey: string;
|
||||
anthropicApiKey: string;
|
||||
customApiKey: string;
|
||||
customApiUrl: string;
|
||||
openRouterApiKey: string;
|
||||
}
|
||||
|
||||
@@ -74,7 +97,6 @@ export interface OutputSettings {
|
||||
export interface PostProcessingSettings {
|
||||
postProcessing: boolean;
|
||||
postProcessingProvider: PostProcessingProvider;
|
||||
postProcessingUrl: string;
|
||||
postProcessingModel: string;
|
||||
postProcessingPrompt: string;
|
||||
autoGenerateTitle: boolean;
|
||||
@@ -128,6 +150,7 @@ export const DEFAULT_API_KEYS: ApiKeysSettings = {
|
||||
openAiApiKey: "",
|
||||
anthropicApiKey: "",
|
||||
customApiKey: "",
|
||||
customApiUrl: "",
|
||||
openRouterApiKey: "",
|
||||
};
|
||||
|
||||
@@ -160,7 +183,6 @@ export const DEFAULT_OUTPUT: OutputSettings = {
|
||||
export const DEFAULT_POST_PROCESSING: PostProcessingSettings = {
|
||||
postProcessing: false,
|
||||
postProcessingProvider: "anthropic",
|
||||
postProcessingUrl: "https://api.anthropic.com/v1/messages",
|
||||
postProcessingModel: "claude-sonnet-4-20250514",
|
||||
postProcessingPrompt:
|
||||
'You are a transcription editor. Clean up the following voice transcription: fix grammar, remove filler words (um, uh, like) and repetitions, and improve readability. Format the text in markdown. If there are action items or to-dos, format them as task lists with "[ ]". Preserve the original meaning and language. Return only the polished text, nothing else.',
|
||||
@@ -274,19 +296,31 @@ export class SettingsManager {
|
||||
|
||||
private migratePostProcessingProvider(settings: PluginSettings): boolean {
|
||||
if (settings.postProcessingProvider) return false;
|
||||
const legacyUrl = (settings as PluginSettings & { postProcessingUrl?: string })
|
||||
.postProcessingUrl;
|
||||
if (!legacyUrl) return false;
|
||||
|
||||
for (const [provider, url] of Object.entries(PROVIDER_URLS)) {
|
||||
if (url && settings.postProcessingUrl === url) {
|
||||
settings.postProcessingProvider =
|
||||
provider as PostProcessingProvider;
|
||||
if (url && legacyUrl === url) {
|
||||
settings.postProcessingProvider = provider as PostProcessingProvider;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (settings.postProcessingUrl) {
|
||||
settings.postProcessingProvider = "custom";
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
private migrateCustomApiUrl(settings: PluginSettings): boolean {
|
||||
// One-time migration: the old `postProcessingUrl` field is now the
|
||||
// shared `customApiUrl` used by every feature's "Custom" provider.
|
||||
if (settings.customApiUrl) return false;
|
||||
const legacyUrl = (settings as PluginSettings & { postProcessingUrl?: string })
|
||||
.postProcessingUrl;
|
||||
if (!legacyUrl) return false;
|
||||
if (Object.values(PROVIDER_URLS).includes(legacyUrl)) return false;
|
||||
|
||||
settings.customApiUrl = legacyUrl;
|
||||
return true;
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<PluginSettings> {
|
||||
@@ -300,6 +334,10 @@ export class SettingsManager {
|
||||
await this.plugin.saveData(settings);
|
||||
}
|
||||
|
||||
if (this.migrateCustomApiUrl(settings)) {
|
||||
await this.plugin.saveData(settings);
|
||||
}
|
||||
|
||||
if (this.migrateKeysFromDataJson(settings)) {
|
||||
await this.plugin.saveData(settings);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Editor, Notice } from "obsidian";
|
||||
import { AIService } from "../AIService";
|
||||
import { AIProvider, PROVIDER_URLS } from "../SettingsManager";
|
||||
import { resolveChatProvider } from "../ProviderResolver";
|
||||
import NibbleAI from "main";
|
||||
|
||||
export class Proofreader {
|
||||
@@ -10,33 +10,6 @@ export class Proofreader {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private resolveProvider(): { apiKey: string; endpoint: string; provider: AIProvider } | null {
|
||||
const provider = this.plugin.settings.proofreadingProvider || "openrouter";
|
||||
let apiKey = "";
|
||||
let endpoint = PROVIDER_URLS[provider] || "";
|
||||
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
apiKey = this.plugin.settings.openAiApiKey;
|
||||
break;
|
||||
case "anthropic":
|
||||
apiKey = this.plugin.settings.anthropicApiKey;
|
||||
break;
|
||||
case "openrouter":
|
||||
apiKey = this.plugin.settings.openRouterApiKey;
|
||||
break;
|
||||
case "custom":
|
||||
apiKey = this.plugin.settings.customApiKey;
|
||||
endpoint = this.plugin.settings.postProcessingUrl || "";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!apiKey) return null;
|
||||
if (!endpoint) return null;
|
||||
|
||||
return { apiKey, endpoint, provider };
|
||||
}
|
||||
|
||||
async fixSelection(editor: Editor): Promise<void> {
|
||||
const selection = editor.getSelection();
|
||||
if (!selection || selection.trim().length === 0) {
|
||||
@@ -44,7 +17,10 @@ export class Proofreader {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = this.resolveProvider();
|
||||
const config = resolveChatProvider(
|
||||
this.plugin.settings,
|
||||
this.plugin.settings.proofreadingProvider || "openrouter"
|
||||
);
|
||||
if (!config) {
|
||||
new Notice(
|
||||
"⚠️ No API key for selected provider. Check Settings \u2192 NibbleAI \u2192 API Keys."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Notice, MarkdownView, Modal, App, Setting, TFile } from "obsidian";
|
||||
import { AIService } from "../AIService";
|
||||
import { AIProvider, PROVIDER_URLS } from "../SettingsManager";
|
||||
import { resolveChatProvider } from "../ProviderResolver";
|
||||
import NibbleAI from "main";
|
||||
|
||||
class PropertyPromptModal extends Modal {
|
||||
@@ -80,31 +80,6 @@ export class PropertyFiller {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private resolveProvider(): { apiKey: string; endpoint: string; provider: AIProvider } | null {
|
||||
const provider = this.plugin.settings.propertyGenProvider || "openrouter";
|
||||
let apiKey = "";
|
||||
let endpoint = PROVIDER_URLS[provider] || "";
|
||||
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
apiKey = this.plugin.settings.openAiApiKey;
|
||||
break;
|
||||
case "anthropic":
|
||||
apiKey = this.plugin.settings.anthropicApiKey;
|
||||
break;
|
||||
case "openrouter":
|
||||
apiKey = this.plugin.settings.openRouterApiKey;
|
||||
break;
|
||||
case "custom":
|
||||
apiKey = this.plugin.settings.customApiKey;
|
||||
endpoint = this.plugin.settings.postProcessingUrl || "";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!apiKey || !endpoint) return null;
|
||||
return { apiKey, endpoint, provider };
|
||||
}
|
||||
|
||||
async fillProperty(): Promise<void> {
|
||||
new PropertyPromptModal(
|
||||
this.plugin.app,
|
||||
@@ -139,7 +114,10 @@ export class PropertyFiller {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = this.resolveProvider();
|
||||
const config = resolveChatProvider(
|
||||
this.plugin.settings,
|
||||
this.plugin.settings.propertyGenProvider || "openrouter"
|
||||
);
|
||||
if (!config) {
|
||||
new Notice(
|
||||
"⚠️ No API key for selected provider. Check Settings \u2192 NibbleAI \u2192 API Keys."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Notice, TFile, MarkdownView } from "obsidian";
|
||||
import { AIService } from "../AIService";
|
||||
import { AIProvider, PROVIDER_URLS } from "../SettingsManager";
|
||||
import { resolveChatProvider } from "../ProviderResolver";
|
||||
import NibbleAI from "main";
|
||||
|
||||
export class TitleGenerator {
|
||||
@@ -10,31 +10,6 @@ export class TitleGenerator {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private resolveProvider(): { apiKey: string; endpoint: string; provider: AIProvider } | null {
|
||||
const provider = this.plugin.settings.titleGenProvider || "openrouter";
|
||||
let apiKey = "";
|
||||
let endpoint = PROVIDER_URLS[provider] || "";
|
||||
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
apiKey = this.plugin.settings.openAiApiKey;
|
||||
break;
|
||||
case "anthropic":
|
||||
apiKey = this.plugin.settings.anthropicApiKey;
|
||||
break;
|
||||
case "openrouter":
|
||||
apiKey = this.plugin.settings.openRouterApiKey;
|
||||
break;
|
||||
case "custom":
|
||||
apiKey = this.plugin.settings.customApiKey;
|
||||
endpoint = this.plugin.settings.postProcessingUrl || "";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!apiKey || !endpoint) return null;
|
||||
return { apiKey, endpoint, provider };
|
||||
}
|
||||
|
||||
async generateTitle(): Promise<void> {
|
||||
const activeView =
|
||||
this.plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
@@ -55,7 +30,10 @@ export class TitleGenerator {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = this.resolveProvider();
|
||||
const config = resolveChatProvider(
|
||||
this.plugin.settings,
|
||||
this.plugin.settings.titleGenProvider || "openrouter"
|
||||
);
|
||||
if (!config) {
|
||||
new Notice(
|
||||
"⚠️ No API key for selected provider. Check Settings \u2192 NibbleAI \u2192 API Keys."
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resolveTemplate,
|
||||
} from "./utils";
|
||||
import { PostProcessor } from "../PostProcessor";
|
||||
import { resolveChatProvider } from "../ProviderResolver";
|
||||
|
||||
export class AudioHandler {
|
||||
private plugin: NibbleAI;
|
||||
@@ -16,19 +17,6 @@ export class AudioHandler {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
private getPostProcessingApiKey(): string {
|
||||
switch (this.plugin.settings.postProcessingProvider) {
|
||||
case "anthropic":
|
||||
return this.plugin.settings.anthropicApiKey;
|
||||
case "openai":
|
||||
return this.plugin.settings.openAiApiKey;
|
||||
case "openrouter":
|
||||
return this.plugin.settings.openRouterApiKey;
|
||||
case "custom":
|
||||
return this.plugin.settings.customApiKey;
|
||||
}
|
||||
}
|
||||
|
||||
private getTranscriptionUrl(): string {
|
||||
switch (this.plugin.settings.transcriptionProvider) {
|
||||
case "groq":
|
||||
@@ -42,7 +30,7 @@ export class AudioHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private getTranscriptionApiKey(): string {
|
||||
getTranscriptionApiKey(): string {
|
||||
switch (this.plugin.settings.transcriptionProvider) {
|
||||
case "groq":
|
||||
return this.plugin.settings.groqApiKey;
|
||||
@@ -173,8 +161,11 @@ export class AudioHandler {
|
||||
|
||||
// Post-process with LLM if enabled
|
||||
if (this.plugin.settings.postProcessing) {
|
||||
const ppApiKey = this.getPostProcessingApiKey();
|
||||
if (!ppApiKey) {
|
||||
const resolved = resolveChatProvider(
|
||||
this.plugin.settings,
|
||||
this.plugin.settings.postProcessingProvider
|
||||
);
|
||||
if (!resolved) {
|
||||
new Notice(
|
||||
"✘ Add your post-processing API key in settings"
|
||||
);
|
||||
@@ -185,10 +176,10 @@ export class AudioHandler {
|
||||
new Notice("Post-processing...");
|
||||
}
|
||||
const processor = new PostProcessor({
|
||||
apiKey: ppApiKey,
|
||||
apiKey: resolved.apiKey,
|
||||
model: this.plugin.settings.postProcessingModel,
|
||||
url: this.plugin.settings.postProcessingUrl,
|
||||
provider: this.plugin.settings.postProcessingProvider,
|
||||
url: resolved.endpoint,
|
||||
provider: resolved.provider,
|
||||
});
|
||||
finalText = await processor.process(
|
||||
originalText,
|
||||
@@ -209,14 +200,17 @@ export class AudioHandler {
|
||||
this.plugin.settings.autoGenerateTitle &&
|
||||
this.plugin.settings.createNoteFile
|
||||
) {
|
||||
const ppApiKey = this.getPostProcessingApiKey();
|
||||
if (ppApiKey) {
|
||||
const resolved = resolveChatProvider(
|
||||
this.plugin.settings,
|
||||
this.plugin.settings.postProcessingProvider
|
||||
);
|
||||
if (resolved) {
|
||||
try {
|
||||
const processor = new PostProcessor({
|
||||
apiKey: ppApiKey,
|
||||
apiKey: resolved.apiKey,
|
||||
model: this.plugin.settings.postProcessingModel,
|
||||
url: this.plugin.settings.postProcessingUrl,
|
||||
provider: this.plugin.settings.postProcessingProvider,
|
||||
url: resolved.endpoint,
|
||||
provider: resolved.provider,
|
||||
});
|
||||
const title = await processor.process(
|
||||
finalText,
|
||||
|
||||
Reference in New Issue
Block a user