Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
getAudioStorageUsage: () => ipcRenderer.invoke("get-audio-storage-usage"),
deleteAllAudio: () => ipcRenderer.invoke("delete-all-audio"),
retryTranscription: (id, settings) => ipcRenderer.invoke("retry-transcription", id, settings),
updateTranscriptionText: (id, text, rawText) =>
ipcRenderer.invoke("update-transcription-text", id, text, rawText),
updateTranscriptionText: (id, text, rawText, cleanupLevel) =>
ipcRenderer.invoke("update-transcription-text", id, text, rawText, cleanupLevel),
getTranscriptionById: (id) => ipcRenderer.invoke("get-transcription-by-id", id),

// Dictionary functions
Expand Down
3 changes: 2 additions & 1 deletion src/components/ControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,8 @@ export default function ControlPanel({ initialSettingsSection }: ControlPanelPro
const updated = await window.electronAPI.updateTranscriptionText(
id,
reasonedText,
rawText
rawText,
s.cleanupLevel === "none" ? null : s.cleanupLevel
);
if (updated.success && updated.transcription) {
finalTranscription = updated.transcription;
Expand Down
65 changes: 54 additions & 11 deletions src/components/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
FileAudio,
Wand2,
Upload,
Feather,
} from "lucide-react";
import { useAuth } from "../hooks/useAuth";
import { AUTH_URL, signOut, deleteAccount } from "../lib/auth";
Expand Down Expand Up @@ -76,6 +77,7 @@ import { ActivationModeSelector } from "./ui/ActivationModeSelector";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import LinuxPttSetupInfo from "./ui/LinuxPttSetupInfo";
import { Toggle } from "./ui/toggle";
import OptionCard from "./ui/OptionCard";
import DeveloperSection from "./DeveloperSection";
import ChatAgentSettings from "./settings/ChatAgentSettings";
import DictationAgentSettings from "./settings/DictationAgentSettings";
Expand All @@ -102,6 +104,7 @@ import { useSettingsStore } from "../stores/settingsStore";
import { canManageSystemAudioInApp } from "../utils/systemAudioAccess";
import WorkspaceSection from "./settings/WorkspaceSection";
import { WORKSPACES_ENABLED } from "../lib/features";
import type { CleanupLevel } from "../config/cleanupLevels";

const formatAmount = (cents: number, currency: string) =>
(cents / 100).toLocaleString(undefined, { style: "currency", currency });
Expand Down Expand Up @@ -395,7 +398,8 @@ function TranscriptionSection({

interface AiModelsSectionProps {
useCleanupModel: boolean;
setUseCleanupModel: (value: boolean) => void;
cleanupLevel: CleanupLevel;
setCleanupLevel: (level: CleanupLevel) => void;
toast: (opts: {
title: string;
description: string;
Expand Down Expand Up @@ -434,8 +438,24 @@ function NoteFormattingSettings() {
);
}

function AiModelsSection({ useCleanupModel, setUseCleanupModel, toast }: AiModelsSectionProps) {
const CLEANUP_LEVEL_OPTIONS: Array<{
id: CleanupLevel;
icon: typeof CircleX;
}> = [
{ id: "none", icon: CircleX },
{ id: "light", icon: Feather },
{ id: "medium", icon: Wand2 },
{ id: "high", icon: Sparkles },
];

function AiModelsSection({
useCleanupModel,
cleanupLevel,
setCleanupLevel,
toast,
}: AiModelsSectionProps) {
const { t } = useTranslation();
const hasCustomPrompt = useSettingsStore((s) => s.customPrompts.cleanup.trim().length > 0);

const handleCleanupModeChange = (mode: InferenceMode) => {
const toastKey = CLEANUP_MODE_TOAST_KEY[mode];
Expand All @@ -451,12 +471,33 @@ function AiModelsSection({ useCleanupModel, setUseCleanupModel, toast }: AiModel
<div className="space-y-4">
<SettingsPanel>
<SettingsPanelRow>
<SettingsRow
label={t("settingsPage.aiModels.enableTextCleanup")}
description={t("settingsPage.aiModels.enableTextCleanupDescription")}
>
<Toggle checked={useCleanupModel} onChange={setUseCleanupModel} />
</SettingsRow>
<div className="space-y-3">
<div>
<h3 className="text-sm font-medium text-foreground">
{t("settingsPage.aiModels.cleanupLevel.title")}
</h3>
<p className="text-xs text-muted-foreground mt-0.5">
{t("settingsPage.aiModels.cleanupLevel.description")}
</p>
</div>
<div className="grid grid-cols-2 gap-2">
{CLEANUP_LEVEL_OPTIONS.map(({ id, icon }) => (
<OptionCard
key={id}
icon={icon}
title={t(`settingsPage.aiModels.cleanupLevel.options.${id}.title`)}
description={t(`settingsPage.aiModels.cleanupLevel.options.${id}.description`)}
selected={cleanupLevel === id}
onSelect={() => setCleanupLevel(id)}
/>
))}
</div>
{hasCustomPrompt && cleanupLevel !== "none" && (
<p className="text-xs text-muted-foreground">
{t("settingsPage.aiModels.cleanupLevel.customPromptOverride")}
</p>
)}
</div>
</SettingsPanelRow>
</SettingsPanel>

Expand Down Expand Up @@ -702,6 +743,7 @@ export default function SettingsPage({
cloudTranscriptionModel,
cloudTranscriptionBaseUrl,
useCleanupModel,
cleanupLevel,
dictationKey,
activationMode,
setActivationMode,
Expand All @@ -717,7 +759,7 @@ export default function SettingsPage({
setCloudTranscriptionProvider,
setCloudTranscriptionModel,
setCloudTranscriptionBaseUrl,
setUseCleanupModel,
setCleanupLevel,
setDictationKey,
meetingKey,
setMeetingKey,
Expand Down Expand Up @@ -4028,8 +4070,9 @@ EOF`,
<div className="space-y-6">
<AiModelsSection
useCleanupModel={useCleanupModel}
setUseCleanupModel={(value) => {
updateCleanupSettings({ useCleanupModel: value });
cleanupLevel={cleanupLevel}
setCleanupLevel={(level) => {
updateCleanupSettings({ cleanupLevel: level });
}}
toast={toast}
/>
Expand Down
12 changes: 10 additions & 2 deletions src/components/ui/PromptStudio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ReasoningService from "../../services/ReasoningService";
import { getModelProvider } from "../../models/ModelRegistry";
import logger from "../../utils/logger";
import { getDefaultPromptText, type PromptKind } from "../../config/prompts";
import { applyCleanupLevel } from "../../config/cleanupLevels";
import { useSettingsStore, selectIsCloudCleanupMode } from "../../stores/settingsStore";

interface PromptStudioProps {
Expand Down Expand Up @@ -64,6 +65,7 @@ export default function PromptStudio({ className = "", kind = "cleanup" }: Promp
const isCloudMode = useSettingsStore(selectIsCloudCleanupMode);
const useCleanupModel = useSettingsStore((s) => s.useCleanupModel);
const cleanupModel = useSettingsStore((s) => s.cleanupModel);
const cleanupLevel = useSettingsStore((s) => s.cleanupLevel);

const customPrompt = useSettingsStore((s) => s.customPrompts[kind]);
const setCustomPrompt = useSettingsStore((s) => s.setCustomPrompt);
Expand Down Expand Up @@ -153,7 +155,11 @@ export default function PromptStudio({ className = "", kind = "cleanup" }: Promp
const modelToUse = isCloudMode ? cleanupModel || "auto" : cleanupModel;

const previous = customPrompt;
setCustomPrompt(kind, editedPrompt);
const promptToTest =
kind === "cleanup" && !customPrompt
? applyCleanupLevel(editedPrompt, cleanupLevel)
: editedPrompt;
setCustomPrompt(kind, promptToTest);
try {
const result = await ReasoningService.processText(testText, modelToUse, agentName, {
disableThinking: useSettingsStore.getState().cleanupDisableThinking,
Expand All @@ -178,7 +184,9 @@ export default function PromptStudio({ className = "", kind = "cleanup" }: Promp

const isAgentAddressed = testText.toLowerCase().includes(agentName.toLowerCase());
const isCustomPrompt = customPrompt.length > 0;
const currentPrompt = customPrompt || defaultPrompt;
const currentPrompt =
customPrompt ||
(kind === "cleanup" ? applyCleanupLevel(defaultPrompt, cleanupLevel) : defaultPrompt);

const tabs = [
{ id: "current" as const, label: t("promptStudio.tabs.view"), icon: Eye },
Expand Down
17 changes: 14 additions & 3 deletions src/components/ui/TranscriptionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,20 @@ export default function TranscriptionItem({
</span>
</div>
) : (
<p className="flex-1 min-w-0 text-foreground text-sm leading-normal wrap-break-word whitespace-pre-wrap">
{item.text}
</p>
<div className="flex-1 min-w-0">
{item.cleanup_level && (
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground mb-0.5">
{t("controlPanel.history.cleanupLevelApplied", {
level: t(
`settingsPage.aiModels.cleanupLevel.options.${item.cleanup_level}.title`
),
})}
</p>
)}
<p className="text-foreground text-sm leading-normal wrap-break-word whitespace-pre-wrap">
{item.text}
</p>
</div>
)}

<div
Expand Down
45 changes: 45 additions & 0 deletions src/config/cleanupLevels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export const CLEANUP_LEVELS = ["none", "light", "medium", "high"] as const;

export type CleanupLevel = (typeof CLEANUP_LEVELS)[number];

const CLEANUP_LEVEL_SET = new Set<string>(CLEANUP_LEVELS);

// Existing installs only have the legacy on/off setting. Medium preserves the
// established cleanup prompt; disabled users migrate to None without behavior change.
export function normalizeCleanupLevel(
value: string | null | undefined,
cleanupEnabled = true
): CleanupLevel {
if (value && CLEANUP_LEVEL_SET.has(value)) return value as CleanupLevel;
return cleanupEnabled ? "medium" : "none";
}

export function getCleanupLevelForEnabled(
enabled: boolean,
currentLevel: CleanupLevel
): CleanupLevel {
if (!enabled) return "none";
return currentLevel === "none" ? "medium" : currentLevel;
}

const LEVEL_INSTRUCTIONS: Record<Exclude<CleanupLevel, "none" | "medium">, string> = {
light:
"CLEANUP LEVEL: LIGHT\nMake only conservative edits: remove filler words and fix clear grammar, spelling, capitalization, and punctuation errors. Preserve the speaker's wording, length, order, and structure. Do not rephrase for style or concision.",
high: "CLEANUP LEVEL: HIGH\nRewrite for brevity and polish while preserving every fact, instruction, proper noun, technical term, and the speaker's intended tone. Remove redundancy, tighten phrasing, and improve structure. Never add new information or answer the transcript.",
};

export function applyCleanupLevel(prompt: string, level: CleanupLevel): string {
if (level === "none" || level === "medium") return prompt;
return `${prompt}\n\n${LEVEL_INSTRUCTIONS[level]}`;
}

export function getCleanupPromptOverride(
customPrompt: string,
level: CleanupLevel,
defaultPrompt: string
): string | undefined {
if (level === "none") return undefined;
if (customPrompt) return customPrompt;
if (level === "light" || level === "high") return applyCleanupLevel(defaultPrompt, level);
return undefined;
}
6 changes: 5 additions & 1 deletion src/config/prompts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import i18n, { normalizeUiLanguage } from "../../i18n";
import { useSettingsStore } from "../../stores/settingsStore";
import { en as enPrompts } from "../../locales/prompts";
import { getLanguageInstruction } from "../../utils/languageSupport";
import { applyCleanupLevel } from "../cleanupLevels";
import { PROMPT_KINDS, type PromptKind } from "./registry";

export { PROMPT_KINDS, PROMPT_KIND_LIST, type PromptKind } from "./registry";
Expand All @@ -15,7 +16,10 @@ export interface ResolvePromptOptions {

export function resolvePrompt(kind: PromptKind, opts: ResolvePromptOptions): string {
const custom = useSettingsStore.getState().customPrompts[kind];
const template = custom || getDefaultPromptText(kind, opts.uiLanguage);
let template = custom || getDefaultPromptText(kind, opts.uiLanguage);
if (kind === "cleanup" && !custom) {
template = applyCleanupLevel(template, useSettingsStore.getState().cleanupLevel);
}
return applySubstitutions(template, opts);
}

Expand Down
7 changes: 6 additions & 1 deletion src/helpers/audioManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2364,7 +2364,8 @@ registerProcessor("pcm-streaming-processor", PCMStreamingProcessor);
}

async saveTranscription(text, rawText = null, { clientTranscriptionId } = {}) {
if (!getSettings().dataRetentionEnabled) {
const settings = getSettings();
if (!settings.dataRetentionEnabled) {
logger.debug("Skipping transcription save — data retention disabled", {}, "audio");
this.lastAudioBlob = null;
this.lastAudioMetadata = null;
Expand All @@ -2374,6 +2375,10 @@ registerProcessor("pcm-streaming-processor", PCMStreamingProcessor);
try {
const result = await window.electronAPI.saveTranscription(text, rawText, {
clientTranscriptionId,
cleanupLevel:
rawText !== null && text !== rawText && settings.useCleanupModel
? settings.cleanupLevel
: null,
});
if (result?.id) syncService.debouncedPush("transcription", result.id);

Expand Down
31 changes: 26 additions & 5 deletions src/helpers/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ const { app } = require("electron");
// Server-enforced trigger cap (openwhispr-api); enforced here so one oversized
// trigger can't 400 the whole sync batch.
const MAX_SNIPPET_TRIGGER_LENGTH = 100;
const CLEANUP_LEVELS = new Set(["light", "medium", "high"]);

function normalizeCleanupLevel(value) {
return CLEANUP_LEVELS.has(value) ? value : null;
}

class DatabaseManager {
constructor() {
Expand Down Expand Up @@ -40,6 +45,11 @@ class DatabaseManager {
} catch (err) {
if (!err.message.includes("duplicate column")) throw err;
}
try {
this.db.exec("ALTER TABLE transcriptions ADD COLUMN cleanup_level TEXT");
} catch (err) {
if (!err.message.includes("duplicate column")) throw err;
}
try {
this.db.exec("ALTER TABLE transcriptions ADD COLUMN has_audio INTEGER NOT NULL DEFAULT 0");
} catch (err) {
Expand Down Expand Up @@ -640,22 +650,24 @@ class DatabaseManager {
errorMessage = null,
errorCode = null,
clientTranscriptionId = randomUUID(),
cleanupLevel = null,
} = {}
) {
try {
if (!this.db) {
throw new Error("Database not initialized");
}
const stmt = this.db.prepare(
"INSERT INTO transcriptions (text, raw_text, status, error_message, error_code, client_transcription_id) VALUES (?, ?, ?, ?, ?, ?)"
"INSERT INTO transcriptions (text, raw_text, status, error_message, error_code, client_transcription_id, cleanup_level) VALUES (?, ?, ?, ?, ?, ?, ?)"
);
const result = stmt.run(
text,
rawText,
status,
errorMessage,
errorCode,
clientTranscriptionId
clientTranscriptionId,
normalizeCleanupLevel(cleanupLevel)
);

const fetchStmt = this.db.prepare("SELECT * FROM transcriptions WHERE id = ?");
Expand Down Expand Up @@ -740,11 +752,20 @@ class DatabaseManager {
}
}

updateTranscriptionText(id, text, rawText) {
updateTranscriptionText(id, text, rawText, cleanupLevel) {
try {
if (!this.db) throw new Error("Database not initialized");
const stmt = this.db.prepare("UPDATE transcriptions SET text = ?, raw_text = ? WHERE id = ?");
stmt.run(text, rawText, id);
const hasCleanupLevel = cleanupLevel !== undefined;
const stmt = this.db.prepare(
hasCleanupLevel
? "UPDATE transcriptions SET text = ?, raw_text = ?, cleanup_level = ? WHERE id = ?"
: "UPDATE transcriptions SET text = ?, raw_text = ? WHERE id = ?"
);
if (hasCleanupLevel) {
stmt.run(text, rawText, normalizeCleanupLevel(cleanupLevel), id);
} else {
stmt.run(text, rawText, id);
}
return { success: true };
} catch (error) {
debugLogger.error("Error updating transcription text", { error: error.message }, "database");
Expand Down
4 changes: 2 additions & 2 deletions src/helpers/ipcHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6281,9 +6281,9 @@ class IPCHandlers {
return { success: true };
});

ipcMain.handle("update-transcription-text", async (_event, id, text, rawText) => {
ipcMain.handle("update-transcription-text", async (_event, id, text, rawText, cleanupLevel) => {
try {
this.databaseManager.updateTranscriptionText(id, text, rawText);
this.databaseManager.updateTranscriptionText(id, text, rawText, cleanupLevel);
const updated = this.databaseManager.getTranscriptionById(id);
return { success: true, transcription: updated };
} catch (error) {
Expand Down
Loading
Loading