Skip to content

Update whisper-dictation extension #19989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 26, 2025
Merged
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
10 changes: 9 additions & 1 deletion extensions/whisper-dictation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [0.1.1] - 2025-06-26

### Added
- Option to both copy and paste transcibed text automatically
- Added seperate commands for dictation and dictation with AI refinement
- This gives more flexibility and how and when each command is called
- Added shortcut to skip refinement for a sesssion during the prompt selection menu (if configured)

## [0.1.0] - 2025-06-05

### Added
@@ -8,4 +16,4 @@
- Download and manage Whisper models within Raycast
- AI-based refinement via Raycast AI or Ollama/OpenAI-compatible APIs
- Dictation history with browse, copy, and paste capabilities
- Configurable default actions (paste, copy, or manual)
- Configurable default actions (paste, copy, or manual)
40 changes: 20 additions & 20 deletions extensions/whisper-dictation/README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,17 @@
# 🎤 Whisper Dictation for Raycast

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Effortlessly convert your speech to text directly within Raycast using the power of [`whisper.cpp`](https://github.com/ggerganov/whisper.cpp). This extension provides a simple interface to record audio, transcribe and refine it locally, privately on your machine. Refine the text with custom prompts privately using ollama, or additionally with Raycast AI or any v1 (OpenAI) compatible API.
## ✨ Features

* **Local Transcription:** Uses `whisper.cpp` running locally on your machine through Raycast.
* **Refine with AI:** Optionally refine the transcribed text using Raycast AI or any OpenAI (v1) compatible API, such as Anthropic, OpenAI, a local Ollama server.
* **Download Models:** Download a variety of whisper models from right within the extension.
* **Dictation History:** Saves all transcriptions locally with timestamps which can be browsed, copied and pasted using the Dictation History command.
* **Refine with AI** Optionally refine the transcribed text using Raycast AI or any OpenAI (v1) compatible API, such as Anthropic, OpenAI, a local Ollama server.
**Download Models** Download a variety of whisper models from right within the extension.
**Dictation History** Saves all transcriptions locally with timestamps which can be browsed, copied and pasted using the Dictation History command.
* **Simple Interface:** Start recording, press Enter to stop, copy or paste directly into your active window.
* **Configurable Output:** Choose to choose, or automatically paste/copy to clipboard.

## 📚 Table of Contents

* [Features](#-features)
* [Requirements](#-requirements)
* [Installation](#-installation)
* [1. Prerequisites](#1-prerequisites)
* [2. Install the Extension](#2-install-the-extension)
* [Configuration](#️-configuration)
* [Usage](#-usage)
* [Refine with AI](#-refine-with-ai)
* [Models](#-models)
* [Troubleshooting](#-troubleshooting)
* [Contributing](#-contributing)
* [License](#-license)
* [Acknowledgements](#-acknowledgements)

## ⚠️ Requirements

Before installing the extension, you need the following installed and configured on your system:
@@ -41,6 +27,10 @@ Before installing the extension, you need the following installed and configured
* The easiest way to install it on macOS is with [Homebrew](https://brew.sh/): `brew install sox`
*The extension currently default for `sox` to be at `/opt/homebrew/bin/sox`. If yours is installed somewhere else, point the extension to it's executable in preferences.

## 🚀 Installation

**This ectension is now available to download from the [Raycast Store](https://www.raycast.com/finjo/whisper-dictation). However if you'd prefer to build from source see below**

## ⚙️ Configuration

After installing, you have to configure the extension preferences in Raycast, if you installed both SoX and whisper-cpp using homebrew, and download a model using the extension this should all be pre-configured for you, the extension will also confirm both SoX and whisper-cli path on first launch which will allow you to immediately start using simple dictation once configured:
@@ -161,4 +151,14 @@ The extension downloader currently supports the following whisper models, howeve
* **External API:** https://api.anthropic.com
* If using Raycast AI make sure that you have paid for Raycast Pro

---
## 📄 License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details (or state MIT directly if no file exists).

## ❤️ Acknowledgements

* [Georgi Gerganov](https://github.com/ggerganov) for the amazing [`whisper.cpp`](https://github.com/ggerganov/whisper.cpp) project.
* The [Raycast](https://raycast.com/) team for the fantastic platform and API.
* [SoX - Sound eXchange](https://github.com/chirlu/sox) developers.

---
23 changes: 17 additions & 6 deletions extensions/whisper-dictation/package.json
Original file line number Diff line number Diff line change
@@ -19,11 +19,18 @@
"transcribed.png"
],
"commands": [
{
"name": "dictate-simple",
"title": "Dictate",
"subtitle": "Whisper Dictation",
"description": "Convert speech to text using Whisper (no AI refinement)",
"mode": "view"
},
{
"name": "dictate",
"title": "Dictate Text",
"title": "Dictate with AI",
"subtitle": "Whisper Dictation",
"description": "Convert speech to text using Whisper",
"description": "Convert speech to text with AI refinement options",
"mode": "view"
},
{
@@ -84,16 +91,20 @@
"default": "none",
"data": [
{
"title": "Paste Text",
"value": "paste"
"title": "None (Show Options)",
"value": "none"
},
{
"title": "Copy to Clipboard",
"value": "copy"
},
{
"title": "None (Show Options)",
"value": "none"
"title": "Paste Text",
"value": "paste"
},
{
"title": "Copy & Paste Text",
"value": "copy_paste"
}
]
},
436 changes: 436 additions & 0 deletions extensions/whisper-dictation/src/dictate-simple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,436 @@
import {
ActionPanel,
Form,
Action,
showToast,
Toast,
closeMainWindow,
Icon,
Detail,
getPreferenceValues,
environment,
LocalStorage,
launchCommand,
LaunchType,
showHUD,
openExtensionPreferences,
PopToRootType,
} from "@raycast/api";
import { useState, useEffect, useRef, useCallback } from "react";
import type { ChildProcessWithoutNullStreams } from "child_process";
import path from "path";
import fs from "fs";
import crypto from "crypto";
import { useConfiguration } from "./hooks/useConfiguration";
import { useRecording } from "./hooks/useRecording";
import { useTranscription } from "./hooks/useTranscription";

interface TranscriptionHistoryItem {
id: string;
timestamp: number;
text: string;
}

const AUDIO_FILE_PATH = path.join(environment.supportPath, "raycast_dictate_audio.wav");
const HISTORY_STORAGE_KEY = "dictationHistory";

type CommandState =
| "configuring"
| "configured_waiting_selection"
| "selectingPrompt"
| "idle"
| "recording"
| "transcribing"
| "done"
| "error";

interface Config {
execPath: string;
modelPath: string;
soxPath: string;
}

export default function SimpleDictateCommand() {
const [state, setState] = useState<CommandState>("configuring");
const [transcribedText, setTranscribedText] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string>("");
const soxProcessRef = useRef<ChildProcessWithoutNullStreams | null>(null);
const [waveformSeed, setWaveformSeed] = useState<number>(0);
const [config, setConfig] = useState<Config | null>(null);

const preferences = getPreferenceValues<Preferences>();
const DEFAULT_ACTION = preferences.defaultAction || "none";

const cleanupAudioFile = useCallback(() => {
fs.promises
.unlink(AUDIO_FILE_PATH)
.then(() => console.log("Cleaned up audio file."))
.catch((err) => {
if (err.code !== "ENOENT") {
console.error("Error cleaning up audio file:", err.message);
}
});
}, []);

useConfiguration(setState, setConfig, setErrorMessage);

const saveTranscriptionToHistory = useCallback(async (text: string) => {
if (!text || text === "[BLANK_AUDIO]") return;

try {
const newItem: TranscriptionHistoryItem = {
id: crypto.randomUUID(),
timestamp: Date.now(),
text: text,
};

let history: TranscriptionHistoryItem[] = [];
const existingHistory = await LocalStorage.getItem<string>(HISTORY_STORAGE_KEY);
if (existingHistory) {
try {
history = JSON.parse(existingHistory);
} catch (parseError) {
console.error("Failed to parse history from LocalStorage:", parseError);
await showToast({
style: Toast.Style.Failure,
title: "Warning",
message: "Could not read previous dictation history. Clearing history.",
});
history = [];
}
}

history.unshift(newItem);

const MAX_HISTORY_ITEMS = 100;
if (history.length > MAX_HISTORY_ITEMS) {
history = history.slice(0, MAX_HISTORY_ITEMS);
}

await LocalStorage.setItem(HISTORY_STORAGE_KEY, JSON.stringify(history));
console.log("Saved transcription to history.");
} catch (error) {
console.error("Failed to save transcription to history:", error);
await showToast({
style: Toast.Style.Failure,
title: "Error",
message: "Failed to save transcription to history.",
});
}
}, []);

const { startTranscription, handlePasteAndCopy } = useTranscription({
config,
preferences,
setState,
setErrorMessage,
setTranscribedText,
refineText: async (text: string) => text, // No AI refinement
saveTranscriptionToHistory,
cleanupAudioFile,
aiErrorMessage: "",
skipAIForSession: true, // Always skip AI for simple dictate
});

// Function to stop recording and transcribe
const stopRecordingAndTranscribe = useCallback(async () => {
console.log(`stopRecordingAndTranscribe called. Current state: ${state}`);

if (state !== "recording") {
console.warn(`stopRecordingAndTranscribe: State is '${state}', expected 'recording'. Aborting.`);
return;
}

const processToStop = soxProcessRef.current;

if (processToStop) {
console.log(`Attempting to stop recording process PID: ${processToStop.pid}...`);
soxProcessRef.current = null;
console.log("Cleared sox process ref.");
try {
if (!processToStop.killed) {
process.kill(processToStop.pid!, "SIGTERM");
console.log(`Sent SIGTERM to PID ${processToStop.pid}`);
await new Promise((resolve) => setTimeout(resolve, 100));
} else {
console.log(`Process ${processToStop.pid} was already killed.`);
}
} catch (e) {
if (e instanceof Error && "code" in e && e.code !== "ESRCH") {
console.warn(`Error stopping sox process PID ${processToStop.pid}:`, e);
} else {
console.log(`Process ${processToStop.pid} likely already exited.`);
}
}
} else {
console.warn("stopRecordingAndTranscribe: No active sox process reference found to stop.");
}

await startTranscription();
}, [state, startTranscription]);

const { restartRecording } = useRecording(state, config, setState, setErrorMessage, soxProcessRef);

// Effect to automatically transition to idle after configuration (skip prompt selection)
useEffect(() => {
if (state === "configured_waiting_selection") {
setState("idle");
}
}, [state]);

useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
if (state === "recording") {
intervalId = setInterval(() => {
setWaveformSeed((prev) => prev + 1);
}, 150);
}
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [state]);

useEffect(() => {
if (state === "transcribing") {
const timer = setTimeout(() => {
startTranscription();
}, 100);
return () => clearTimeout(timer);
}
}, [state, startTranscription]);

useEffect(() => {
return () => {
const currentProcess = soxProcessRef.current;
if (currentProcess && !currentProcess.killed) {
console.log(`Cleanup: Killing process PID: ${currentProcess.pid}`);
try {
process.kill(currentProcess.pid!, "SIGKILL");
} catch (e) {
if (e instanceof Error && "code" in e && e.code !== "ESRCH") {
console.warn("Cleanup: Error sending SIGKILL:", e);
}
}
}
cleanupAudioFile();
};
}, [cleanupAudioFile]);

const generateWaveformMarkdown = useCallback(() => {
const waveformHeight = 18;
const waveformWidth = 105;
let waveform = "```\n";
waveform += "RECORDING AUDIO... PRESS ENTER TO STOP\n\n";

for (let y = 0; y < waveformHeight; y++) {
let line = "";
for (let x = 0; x < waveformWidth; x++) {
const baseAmplitude1 = Math.sin((x / waveformWidth) * Math.PI * 4) * 0.3;
const baseAmplitude2 = Math.sin((x / waveformWidth) * Math.PI * 8) * 0.15;
const baseAmplitude3 = Math.sin((x / waveformWidth) * Math.PI * 2) * 0.25;
const baseAmplitude = baseAmplitude1 + baseAmplitude2 + baseAmplitude3;
const randomFactor = Math.sin(x + waveformSeed * 0.3) * 0.2;
const amplitude = baseAmplitude + randomFactor;
const normalizedAmplitude = (amplitude + 0.7) * waveformHeight * 0.5;
const distFromCenter = Math.abs(y - waveformHeight / 2);
const shouldDraw = distFromCenter < normalizedAmplitude;

if (shouldDraw) {
const intensity = 1 - distFromCenter / normalizedAmplitude;
if (intensity > 0.8) line += "█";
else if (intensity > 0.6) line += "▓";
else if (intensity > 0.4) line += "▒";
else if (intensity > 0.2) line += "░";
else line += "·";
} else {
line += " ";
}
}
waveform += line + "\n";
}
waveform += "```";
return waveform;
}, [waveformSeed]);

const getActionPanel = useCallback(() => {
switch (state) {
case "recording":
return (
<ActionPanel>
<Action title="Stop and Transcribe" icon={Icon.Stop} onAction={stopRecordingAndTranscribe} />
<Action
title="Cancel Recording"
icon={Icon.XMarkCircle}
shortcut={{ modifiers: ["cmd"], key: "." }}
onAction={() => {
const processToStop = soxProcessRef.current;
if (processToStop && !processToStop.killed) {
try {
process.kill(processToStop.pid!, "SIGKILL");
console.log(`Cancel Recording: Sent SIGKILL to PID ${processToStop.pid}`);
} catch {
/* Ignore ESRCH */
}
soxProcessRef.current = null;
}
cleanupAudioFile();
closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate });
}}
/>
<Action
title="Retry Recording"
icon={Icon.ArrowClockwise}
shortcut={{ modifiers: ["cmd"], key: "r" }}
onAction={() => {
console.log("Retry Recording action triggered.");
cleanupAudioFile();
restartRecording();
}}
/>
</ActionPanel>
);
case "done":
return (
<ActionPanel>
<Action.CopyToClipboard
title={DEFAULT_ACTION === "copy" ? "Copy Text (Default)" : "Copy Text"}
content={transcribedText}
shortcut={{ modifiers: ["cmd"], key: "enter" }}
onCopy={() => closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate })}
/>
<Action.Paste
title={DEFAULT_ACTION === "paste" ? "Paste Text (Default)" : "Paste Text"}
content={transcribedText}
shortcut={{ modifiers: ["cmd", "shift"], key: "enter" }}
onPaste={() => closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate })}
/>
<Action
title={DEFAULT_ACTION === "copy_paste" ? "Copy & Paste Text (Default)" : "Copy & Paste Text"}
icon={Icon.Clipboard}
shortcut={{ modifiers: ["cmd", "opt"], key: "enter" }}
onAction={() => handlePasteAndCopy(transcribedText)}
/>
<Action
title="View History"
icon={Icon.List}
shortcut={{ modifiers: ["cmd"], key: "h" }}
onAction={async () => {
await launchCommand({ name: "dictation-history", type: LaunchType.UserInitiated });
}}
/>
<Action title="Close" icon={Icon.XMarkCircle} onAction={closeMainWindow} />
</ActionPanel>
);
case "transcribing":
return null;
case "error":
return (
<ActionPanel>
<Action
title="Open Extension Preferences"
icon={Icon.Gear}
onAction={() => {
openExtensionPreferences();
closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate });
}}
/>
<Action
title="Retry (Reopen Command)"
icon={Icon.ArrowClockwise}
onAction={() => {
showHUD("Please reopen the Simple Dictate command.");
closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate });
}}
/>
<Action
title="Download Model"
icon={Icon.Download}
onAction={async () => {
await launchCommand({ name: "download-model", type: LaunchType.UserInitiated });
}}
/>
<Action title="Close" icon={Icon.XMarkCircle} onAction={closeMainWindow} />
</ActionPanel>
);
default:
return (
<ActionPanel>
<Action title="Start Recording" icon={Icon.Microphone} onAction={() => setState("recording")} />
<Action
title="View History"
icon={Icon.List}
shortcut={{ modifiers: ["cmd"], key: "h" }}
onAction={async () => {
await launchCommand({ name: "dictation-history", type: LaunchType.UserInitiated });
}}
/>
<Action
title="Open Extension Preferences"
icon={Icon.Gear}
onAction={() => {
openExtensionPreferences();
closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate });
}}
/>
<Action title="Close" icon={Icon.XMarkCircle} onAction={closeMainWindow} />
</ActionPanel>
);
}
}, [
state,
stopRecordingAndTranscribe,
transcribedText,
cleanupAudioFile,
DEFAULT_ACTION,
handlePasteAndCopy,
restartRecording,
]);

if (state === "configuring") {
return <Detail isLoading={true} markdown={"Checking Whisper configuration..."} />;
}

if (state === "recording") {
const waveformMarkdown = generateWaveformMarkdown();
return <Detail markdown={waveformMarkdown} actions={getActionPanel()} />;
}

return (
<Form
isLoading={state === "transcribing"}
actions={getActionPanel()}
navigationTitle={
state === "transcribing"
? "Transcribing..."
: state === "done"
? "Transcription Result"
: state === "error"
? "Error"
: "Simple Dictation"
}
>
{state === "error" && <Form.Description title="Error" text={errorMessage} />}
{(state === "done" || state === "transcribing" || state === "idle") && (
<Form.TextArea
id="dictatedText"
title={state === "done" ? "Dictated Text" : ""}
placeholder={
state === "transcribing"
? "Transcribing audio..."
: state === "done"
? "Transcription result"
: "Press Enter to start recording..."
}
value={state === "done" ? transcribedText : ""}
onChange={setTranscribedText}
/>
)}
{state === "transcribing" && <Form.Description text="Processing audio, please wait..." />}
{state === "idle" && (
<Form.Description text="Simple dictation without AI refinement. Press Enter to start recording." />
)}
</Form>
);
}
36 changes: 33 additions & 3 deletions extensions/whisper-dictation/src/dictate.tsx
Original file line number Diff line number Diff line change
@@ -64,12 +64,13 @@ interface Config {
soxPath: string;
}

export default function Command() {
export default function DictateWithAICommand() {
const [state, setState] = useState<CommandState>("configuring");
const [transcribedText, setTranscribedText] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string>("");
const [aiErrorMessage, setAiErrorMessage] = useState<string>("");
const [selectedSessionPrompt, setSelectedSessionPrompt] = useState<AIPrompt | null>(null);
const [skipAIForSession, setSkipAIForSession] = useState<boolean>(false);
const soxProcessRef = useRef<ChildProcessWithoutNullStreams | null>(null);
const [waveformSeed, setWaveformSeed] = useState<number>(0);
const [config, setConfig] = useState<Config | null>(null);
@@ -144,6 +145,13 @@ export default function Command() {
closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate });
}, [cleanupAudioFile]);

// Handle skipping AI refinement entirely for current session
const handleSkipAIRefinement = useCallback(() => {
setSkipAIForSession(true);
setSelectedSessionPrompt(null);
setState("idle");
}, []);

// Handle skipping prompt selection, will use currently active prompt or first prompt
const handleSkipAndUseActivePrompt = useCallback(async () => {
try {
@@ -275,11 +283,12 @@ export default function Command() {
useEffect(() => {
if (preferences.aiRefinementMethod === "disabled") {
setSelectedSessionPrompt(null);
setSkipAIForSession(false);
}
}, [preferences.aiRefinementMethod]);

// Use transcription hook
const { startTranscription } = useTranscription({
const { startTranscription, handlePasteAndCopy } = useTranscription({
config,
preferences,
setState,
@@ -289,6 +298,7 @@ export default function Command() {
saveTranscriptionToHistory,
cleanupAudioFile,
aiErrorMessage,
skipAIForSession,
});

// Function to stop recording and transcribe via hook
@@ -421,6 +431,12 @@ export default function Command() {
shortcut={{ modifiers: ["cmd", "shift"], key: "enter" }}
onPaste={() => closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate })} // Close after paste
/>
<Action
title={DEFAULT_ACTION === "copy_paste" ? "Copy & Paste Text (Default)" : "Copy & Paste Text"}
icon={Icon.Clipboard}
shortcut={{ modifiers: ["cmd", "opt"], key: "enter" }}
onAction={() => handlePasteAndCopy(transcribedText)}
/>
<Action
title="View History"
icon={Icon.List}
@@ -502,6 +518,12 @@ export default function Command() {
onAction={handleSkipAndUseActivePrompt}
shortcut={{ modifiers: ["cmd"], key: "s" }}
/>
<Action
title="Skip AI Refinement"
icon={Icon.XMarkCircle}
onAction={handleSkipAIRefinement}
shortcut={{ modifiers: ["cmd", "shift"], key: "s" }}
/>
<Action title="Skip & Continue" icon={Icon.ArrowRight} onAction={handlePromptSelectionCancel} />
</ActionPanel>
}
@@ -530,6 +552,12 @@ export default function Command() {
onAction={handleSkipAndUseActivePrompt}
shortcut={{ modifiers: ["cmd"], key: "s" }}
/>
<Action
title="Skip AI Refinement"
icon={Icon.XMarkCircle}
onAction={handleSkipAIRefinement}
shortcut={{ modifiers: ["cmd", "shift"], key: "s" }}
/>
<Action
title="Configure AI"
icon={Icon.Gear}
@@ -557,7 +585,9 @@ export default function Command() {
if (state === "recording") {
let refinementSection = "";

if (selectedSessionPrompt) {
if (skipAIForSession) {
refinementSection = `**AI Refinement: Skipped for this session**\n\n`;
} else if (selectedSessionPrompt) {
refinementSection = `**AI Refinement: ${selectedSessionPrompt.name}**\n\n`;
} else if (isRefinementActive && currentRefinementPrompt) {
const activePrompt = prompts.find((p) => p.prompt === currentRefinementPrompt);
37 changes: 33 additions & 4 deletions extensions/whisper-dictation/src/hooks/useTranscription.ts
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ interface UseTranscriptionProps {
saveTranscriptionToHistory: (text: string) => Promise<void>;
cleanupAudioFile: () => void;
aiErrorMessage: string;
skipAIForSession: boolean;
}
/**
* Hook that manages audio transcription using Whisper CLI.
@@ -68,13 +69,37 @@ export function useTranscription({
saveTranscriptionToHistory,
cleanupAudioFile,
aiErrorMessage,
skipAIForSession,
}: UseTranscriptionProps) {
const handlePasteAndCopy = useCallback(
async (text: string) => {
try {
await Clipboard.copy(text);
await Clipboard.paste(text);
await showHUD("Copied and pasted transcribed text");
} catch (error) {
console.error("Error during copy and paste:", error);
showFailureToast(error, { title: "Failed to copy and paste text" });
}
await Promise.all([
cleanupAudioFile(),
closeMainWindow({ clearRootSearch: true, popToRootType: PopToRootType.Immediate }),
]);
},
[cleanupAudioFile],
);

const handleTranscriptionResult = useCallback(
async (rawText: string) => {
let finalText = rawText;

// Apply AI refinement if enabled and text is not empty
if (preferences.aiRefinementMethod !== "disabled" && rawText && rawText !== "[BLANK_AUDIO]") {
// Apply AI refinement if enabled and text is not empty and not skipped for session
if (
preferences.aiRefinementMethod !== "disabled" &&
!skipAIForSession &&
rawText &&
rawText !== "[BLANK_AUDIO]"
) {
try {
finalText = await refineText(rawText);
} catch (error) {
@@ -110,10 +135,12 @@ export function useTranscription({
await handleClipboardActionAndClose("paste", finalText);
} else if (DEFAULT_ACTION === "copy") {
await handleClipboardActionAndClose("copy", finalText);
} else if (DEFAULT_ACTION === "copy_paste") {
await handlePasteAndCopy(finalText);
} else {
// Action is "none", stay in "done" state
// Show success toast only if AI didn't fail (or wasn't used)
if (preferences.aiRefinementMethod === "disabled" || !aiErrorMessage) {
if (preferences.aiRefinementMethod === "disabled" || skipAIForSession || !aiErrorMessage) {
await showToast({ style: Toast.Style.Success, title: "Transcription complete" });
}
// Clean up file when staying in 'done' state
@@ -128,6 +155,8 @@ export function useTranscription({
setState,
cleanupAudioFile,
aiErrorMessage,
handlePasteAndCopy,
skipAIForSession,
],
);

@@ -229,5 +258,5 @@ export function useTranscription({
);
}, [config, setState, setErrorMessage, handleTranscriptionResult, cleanupAudioFile]);

return { startTranscription };
return { startTranscription, handlePasteAndCopy };
}