mirror of
https://github.com/wiseguru/ReWrite-Voice-Notes.git
synced 2026-07-22 07:49:19 +00:00
Update max token suggestions
This commit is contained in:
parent
85c549136c
commit
b0bc85eff0
5 changed files with 71 additions and 4 deletions
|
|
@ -261,6 +261,7 @@ Per [.editorconfig](.editorconfig): tabs (width 4), LF, UTF-8, final newline. Ma
|
|||
- **`safeStorage` is lazy-required inside a `Platform.isDesktop` guard** in [src/secrets.ts](src/secrets.ts). Importing `electron` at module top would crash on mobile load (it's marked `external` in esbuild). Any failure is treated as "encryption unavailable", which is also the mobile path and the Linux-without-keyring path. The settings tab surfaces the active backend via `safeStorage.getSelectedStorageBackend()` so users can see why it failed (e.g. `basic_text` is Chromium's last-resort backend and counts as unencrypted).
|
||||
- **`saveManyKeys` is a silent no-op when locked.** When `mode === 'passphrase'` and `unlockedKey === null`, `saveManyKeys` does nothing. This is deliberate: unrelated `plugin.saveSettings()` calls (e.g. user changes a model dropdown) would otherwise persist empty `apiKey` values for every profile, wiping the on-disk encrypted bag. The UI prevents this by disabling key fields and gating all pipeline entry points on `encryptionStatus.locked`. `saveKey` (single-key write) still throws so callers can react.
|
||||
- **Keep zxcvbn lazy and the strength API async.** [src/passphrase-strength.ts](src/passphrase-strength.ts) pulls `@zxcvbn-ts/*` via dynamic `import()` specifically so the ~1.6 MB of dictionaries are not parsed/constructed at plugin load (measured: ~9 ms startup vs ~47 ms with a static import). Do not "simplify" it back to a top-level static import or a synchronous `evaluatePassphrase` — that re-adds the cost to every Obsidian launch on every device. The async ripples (modal `updateStrength` race guard, `await isPassphraseAcceptable` in `changeEncryptionMode`) are intentional. `warmPassphraseStrength()` is fired on passphrase-modal open to hide the one-time build behind the modal animation.
|
||||
- **`maxTokens` has two settings-tab views over one value.** `LLMConfig.maxTokens` (per profile, default `2560`) is the single source of truth. The normal-area "Maximum note length" dropdown (`renderNoteLength` in [src/settings/tab.ts](src/settings/tab.ts)) frames it in minutes via `NOTE_LENGTH_PRESETS` at `TOKENS_PER_MIN = 256` (≈150 wpm × ~1.3 tokens/word, padded ~20% for headings/bullets/`Speaker X:` labels; ~10 min → 2560). The Advanced "LLM max tokens" text field edits the same number raw; a value not matching a preset surfaces in the dropdown as a "Custom (N tokens, ~M min)" option. The dropdown calls `this.display()` on change so Advanced reflects the new number; the raw text field does not redraw (focus preservation), so the dropdown updates on the next full render. The Anthropic adapter's `config.maxTokens > 0 ? ... : 2560` fallback ([src/llm/anthropic.ts](src/llm/anthropic.ts)) must stay in sync with the default. The cap is on **output** tokens, so it bounds note length, not input.
|
||||
- **No baked-in model defaults.** Both profiles ship with `model: ""`. The modal renders an inline setup card that blocks recording/paste until the active profile has a provider, model, key, and (for `openai-compatible`) base URL. If you add a provider, do not bake a default model string; surface it as placeholder hint text.
|
||||
- **`openai-compatible` base URL asymmetry** (literal interpretation of the spec): transcription appends `/v1/audio/transcriptions` to a *root* URL (`http://localhost:8080`); LLM appends `/chat/completions` to a URL that *already includes* `/v1` (`http://localhost:11434/v1`). The settings UI hint text and setup card both guide users; do not "normalize" one to match the other.
|
||||
- **`setHeading()` instead of manual `<h2>`** inside settings tabs. `obsidianmd/settings-tab/no-manual-html-headings` forbids manual headings. Same applies anywhere else inside a settings tab that needs a section header. In [src/settings/tab.ts](src/settings/tab.ts), section headings go through the `sectionHeading(parent, name, icon)` helper, which builds the `setHeading()` Setting and prepends a Lucide icon (via `setIcon`) to its `nameEl` styled by `.rewrite-heading-icon`. It returns the `Setting` so callers that attach a status badge (profile, shared core) still reach `nameEl`. Add new headings via this helper, not a bare `new Setting(...).setHeading()`, so they keep an icon.
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function createAnthropicLLM(): LLMProvider {
|
|||
if (!config.model) throw new Error('anthropic: model is not configured');
|
||||
const body = {
|
||||
model: config.model,
|
||||
max_tokens: config.maxTokens > 0 ? config.maxTokens : 2048,
|
||||
max_tokens: config.maxTokens > 0 ? config.maxTokens : 2560,
|
||||
system: systemPrompt,
|
||||
messages: [{ role: 'user', content: userMessage }],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const EMPTY_LLM_CONFIG: LLMConfig = {
|
|||
apiKey: '',
|
||||
baseUrl: '',
|
||||
model: '',
|
||||
maxTokens: 2048,
|
||||
maxTokens: 2560,
|
||||
};
|
||||
|
||||
const DESKTOP_DEFAULT_PROFILE: EnvironmentProfile = {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,19 @@ import { PassphraseModal } from '../ui/passphrase-modal';
|
|||
// Sentinel value for the "Custom..." entry in a model dropdown; never written to config.model.
|
||||
const CUSTOM_MODEL_OPTION = '__rewrite_custom__';
|
||||
|
||||
// Cleanup output runs ~256 tokens per minute of speech (≈150 wpm × ~1.3 tokens/word,
|
||||
// padded ~20% for headings, bullets, and Speaker labels in structured/diarized notes).
|
||||
// The "Maximum note length" dropdown maps these minute presets onto config.maxTokens;
|
||||
// the raw token count is editable in Advanced. ~10 min → 2560 is the default.
|
||||
const TOKENS_PER_MIN = 256;
|
||||
const NOTE_LENGTH_PRESETS: Array<{ minutes: number; tokens: number }> = [
|
||||
{ minutes: 5, tokens: 1280 },
|
||||
{ minutes: 10, tokens: 2560 },
|
||||
{ minutes: 20, tokens: 5120 },
|
||||
{ minutes: 30, tokens: 7680 },
|
||||
{ minutes: 60, tokens: 15360 },
|
||||
];
|
||||
|
||||
const TRANSCRIPTION_OPTIONS: Array<{ id: TranscriptionProviderID; label: string; desktopOnly?: boolean }> = [
|
||||
{ id: 'openai', label: 'OpenAI Whisper' },
|
||||
{ id: 'openai-compatible', label: 'OpenAI-compatible (local server)' },
|
||||
|
|
@ -468,11 +481,48 @@ export class ReWriteSettingTab extends PluginSettingTab {
|
|||
await this.commit();
|
||||
});
|
||||
});
|
||||
|
||||
this.renderNoteLength(body, profile);
|
||||
}
|
||||
|
||||
this.renderProfileAdvanced(body, profile);
|
||||
}
|
||||
|
||||
// Normal-area control for cleanup output length, framed in minutes rather than raw
|
||||
// tokens. Presets map onto config.maxTokens (the single source of truth); a non-preset
|
||||
// value (set via the Advanced "LLM max tokens" field) surfaces as a "Custom (...)" option.
|
||||
private renderNoteLength(parent: HTMLElement, profile: EnvironmentProfile): void {
|
||||
const current = profile.llmConfig.maxTokens;
|
||||
const matched = NOTE_LENGTH_PRESETS.some((p) => p.tokens === current);
|
||||
|
||||
const setting = new Setting(parent)
|
||||
.setName('Maximum note length')
|
||||
.setDesc(
|
||||
'How long a recording the cleaned note can hold before the response is cut off. ' +
|
||||
'Roughly 10 min of speech ≈ 2,560 tokens; structured or multi-speaker notes run longer. ' +
|
||||
'For an exact token count, use LLM max tokens under Advanced.',
|
||||
)
|
||||
.addDropdown((d) => {
|
||||
for (const p of NOTE_LENGTH_PRESETS) {
|
||||
d.addOption(String(p.tokens), `~${p.minutes} min (${p.tokens.toLocaleString()} tokens)`);
|
||||
}
|
||||
if (!matched) {
|
||||
const mins = Math.max(1, Math.round(current / TOKENS_PER_MIN));
|
||||
d.addOption(String(current), `Custom (${current.toLocaleString()} tokens, ~${mins} min)`);
|
||||
}
|
||||
d.setValue(String(current));
|
||||
d.onChange(async (v) => {
|
||||
const n = Number.parseInt(v, 10);
|
||||
if (!Number.isFinite(n) || n <= 0) return;
|
||||
profile.llmConfig.maxTokens = n;
|
||||
await this.commit();
|
||||
// Re-render so the Advanced raw-token field reflects the new value.
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
setting.settingEl.addClass('rewrite-note-length');
|
||||
}
|
||||
|
||||
private renderProfileAdvanced(parent: HTMLElement, profile: EnvironmentProfile): void {
|
||||
if (profile.transcriptionProvider === 'none' && profile.llmProvider === 'none') return;
|
||||
|
||||
|
|
@ -495,13 +545,16 @@ export class ReWriteSettingTab extends PluginSettingTab {
|
|||
if (profile.llmProvider !== 'none') {
|
||||
new Setting(details)
|
||||
.setName('LLM max tokens')
|
||||
.setDesc('Maximum tokens for the cleanup response. Default 2048.')
|
||||
.setDesc(
|
||||
'Exact token cap for the cleanup response, overriding the Maximum note length presets. ' +
|
||||
'~256 tokens ≈ 1 min of cleaned speech. Default 2560.',
|
||||
)
|
||||
.addText((t) => {
|
||||
t.inputEl.type = 'number';
|
||||
t.setValue(String(profile.llmConfig.maxTokens));
|
||||
t.onChange(async (v) => {
|
||||
const n = Number.parseInt(v, 10);
|
||||
profile.llmConfig.maxTokens = Number.isFinite(n) && n > 0 ? n : 2048;
|
||||
profile.llmConfig.maxTokens = Number.isFinite(n) && n > 0 ? n : 2560;
|
||||
await this.commit();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
13
styles.css
13
styles.css
|
|
@ -200,6 +200,19 @@
|
|||
margin-left: -15px;
|
||||
}
|
||||
|
||||
/* Give "Maximum note length" first-class emphasis vs the plainer fields around it. */
|
||||
.rewrite-settings .rewrite-note-length {
|
||||
margin-top: 8px;
|
||||
padding: 6px 12px;
|
||||
border-left: 3px solid var(--interactive-accent);
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.rewrite-settings .rewrite-note-length .setting-item-name {
|
||||
font-weight: var(--font-semibold, 600);
|
||||
}
|
||||
|
||||
.rewrite-settings .rewrite-profile-active-badge {
|
||||
display: inline-block;
|
||||
background-color: var(--interactive-accent);
|
||||
|
|
|
|||
Loading…
Reference in a new issue