whspr 1.0.14 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -25
- package/dist/index.d.ts +3 -0
- package/dist/index.js +110 -16
- package/dist/postprocess.js +2 -1
- package/dist/ui.js +11 -5
- package/dist/utils/pricing.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,11 @@ whspr
|
|
|
43
43
|
|
|
44
44
|
# With verbose output
|
|
45
45
|
whspr --verbose
|
|
46
|
+
|
|
47
|
+
# Pipe output to another command (instead of clipboard)
|
|
48
|
+
whspr --pipe "pbcopy" # Explicit clipboard
|
|
49
|
+
whspr --pipe "claude" # Pipe directly to Claude Code
|
|
50
|
+
whspr -p "cat >> notes.txt" # Append to a file
|
|
46
51
|
```
|
|
47
52
|
|
|
48
53
|
Press **Enter** to stop recording.
|
|
@@ -53,9 +58,12 @@ Press **Enter** to stop recording.
|
|
|
53
58
|
- 15-minute max recording time
|
|
54
59
|
- Transcription via Groq Whisper API
|
|
55
60
|
- AI-powered post-processing to fix transcription errors
|
|
61
|
+
- Progress bar during post-processing
|
|
62
|
+
- Cost tracking for Anthropic models
|
|
56
63
|
- Custom vocabulary support via `WHSPR.md` (global and local)
|
|
57
64
|
- Configurable settings via `~/.whspr/settings.json`
|
|
58
|
-
- Automatic clipboard copy
|
|
65
|
+
- Automatic clipboard copy (or pipe to any command with `--pipe`)
|
|
66
|
+
- Optional auto-save for transcriptions and audio files
|
|
59
67
|
|
|
60
68
|
## Settings
|
|
61
69
|
|
|
@@ -70,40 +78,46 @@ Create `~/.whspr/settings.json` to customize whspr's behavior:
|
|
|
70
78
|
"model": "groq:openai/gpt-oss-120b",
|
|
71
79
|
"systemPrompt": "Your task is to clean up transcribed text...",
|
|
72
80
|
"customPromptPrefix": "Here's my custom user prompt:",
|
|
73
|
-
"transcriptionPrefix": "Here's my raw transcription output:"
|
|
81
|
+
"transcriptionPrefix": "Here's my raw transcription output:",
|
|
82
|
+
"alwaysSaveTranscriptions": false,
|
|
83
|
+
"alwaysSaveAudio": false,
|
|
84
|
+
"saveTranscriptionsToCwd": false
|
|
74
85
|
}
|
|
75
86
|
```
|
|
76
87
|
|
|
77
|
-
| Option
|
|
78
|
-
|
|
79
|
-
| `verbose`
|
|
80
|
-
| `suffix`
|
|
81
|
-
| `transcriptionModel`
|
|
82
|
-
| `language`
|
|
83
|
-
| `model`
|
|
84
|
-
| `systemPrompt`
|
|
85
|
-
| `customPromptPrefix`
|
|
86
|
-
| `transcriptionPrefix`
|
|
88
|
+
| Option | Type | Default | Description |
|
|
89
|
+
| -------------------------- | ------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
90
|
+
| `verbose` | boolean | `false` | Enable verbose output |
|
|
91
|
+
| `suffix` | string | none | Text appended to all transcriptions |
|
|
92
|
+
| `transcriptionModel` | string | `"whisper-large-v3-turbo"` | Whisper model (`"whisper-large-v3"` or `"whisper-large-v3-turbo"`) |
|
|
93
|
+
| `language` | string | `"en"` | ISO 639-1 language code (e.g., `"en"`, `"zh"`, `"es"`) |
|
|
94
|
+
| `model` | string | `"groq:openai/gpt-oss-120b"` | Post-processing model in `provider:model-name` format (see below) |
|
|
95
|
+
| `systemPrompt` | string | (built-in) | System prompt for AI post-processing |
|
|
96
|
+
| `customPromptPrefix` | string | `"Here's my custom user prompt:"` | Prefix before custom prompt content |
|
|
97
|
+
| `transcriptionPrefix` | string | `"Here's my raw transcription output that I need you to edit:"` | Prefix before raw transcription |
|
|
98
|
+
| `alwaysSaveTranscriptions` | boolean | `false` | Always save transcription text files to `~/.whspr/transcriptions/` |
|
|
99
|
+
| `alwaysSaveAudio` | boolean | `false` | Always save audio MP3 files to `~/.whspr/recordings/` |
|
|
100
|
+
| `saveTranscriptionsToCwd` | boolean | `false` | Save transcriptions to current directory instead of `~/.whspr/transcriptions/` |
|
|
87
101
|
|
|
88
102
|
### Supported Providers
|
|
89
103
|
|
|
90
104
|
The `model` setting uses a `provider:model-name` format. Supported providers:
|
|
91
105
|
|
|
92
|
-
| Provider
|
|
93
|
-
|
|
94
|
-
| `groq`
|
|
106
|
+
| Provider | API Key Required |
|
|
107
|
+
| ----------- | ------------------- |
|
|
108
|
+
| `groq` | `GROQ_API_KEY` |
|
|
95
109
|
| `anthropic` | `ANTHROPIC_API_KEY` |
|
|
96
110
|
|
|
97
111
|
### Common Models
|
|
98
112
|
|
|
99
|
-
| Provider
|
|
100
|
-
|
|
101
|
-
| `anthropic` | `claude-sonnet-4-5`
|
|
102
|
-
| `anthropic` | `claude-haiku-4-5`
|
|
103
|
-
| `anthropic` | `claude-opus-4-5`
|
|
104
|
-
| `groq`
|
|
105
|
-
| `groq`
|
|
106
|
-
| `groq`
|
|
113
|
+
| Provider | Model | Description |
|
|
114
|
+
| ----------- | ---------------------------------- | ---------------------------------------- |
|
|
115
|
+
| `anthropic` | `claude-sonnet-4-5` | Balanced speed and quality (recommended) |
|
|
116
|
+
| `anthropic` | `claude-haiku-4-5` | Fastest responses, smaller model |
|
|
117
|
+
| `anthropic` | `claude-opus-4-5` | Best quality, slower and more expensive |
|
|
118
|
+
| `groq` | `openai/gpt-oss-120b` | Default model |
|
|
119
|
+
| `groq` | `llama-3.3-70b-versatile` | Fast, versatile Llama model |
|
|
120
|
+
| `groq` | `moonshotai/kimi-k2-instruct-0905` | Moonshot Kimi model |
|
|
107
121
|
|
|
108
122
|
> **Note:** Model names are set by the providers and may change at any time. Check [Groq Models](https://console.groq.com/docs/models) and [Anthropic Models](https://docs.anthropic.com/en/docs/about-claude/models) for the latest available models.
|
|
109
123
|
|
|
@@ -116,6 +130,32 @@ The `model` setting uses a `provider:model-name` format. Supported providers:
|
|
|
116
130
|
}
|
|
117
131
|
```
|
|
118
132
|
|
|
133
|
+
### Example: Auto-save Transcriptions to Current Directory
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"alwaysSaveTranscriptions": true,
|
|
138
|
+
"saveTranscriptionsToCwd": true
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Pipe Output
|
|
143
|
+
|
|
144
|
+
Use `--pipe` (or `-p`) to send the transcription to any command instead of the clipboard:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Pipe to Claude Code for further processing
|
|
148
|
+
whspr --pipe "claude"
|
|
149
|
+
|
|
150
|
+
# Append to a file
|
|
151
|
+
whspr --pipe "cat >> meeting-notes.txt"
|
|
152
|
+
|
|
153
|
+
# Send via curl
|
|
154
|
+
whspr --pipe "xargs -I {} curl -X POST -d 'text={}' https://api.example.com"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
If the pipe command fails, whspr falls back to copying to the clipboard.
|
|
158
|
+
|
|
119
159
|
## Custom Vocabulary
|
|
120
160
|
|
|
121
161
|
Create a `WHSPR.md` (or `WHISPER.md`) file to provide custom vocabulary, names, or instructions for the AI post-processor.
|
|
@@ -152,9 +192,11 @@ When both exist, they are combined (global first, then local).
|
|
|
152
192
|
3. Converts the recording to MP3
|
|
153
193
|
4. Sends audio to Groq's Whisper API for transcription
|
|
154
194
|
5. Loads custom prompts from `~/.whspr/WHSPR.md` and/or `./WHSPR.md`
|
|
155
|
-
6. Sends transcription + custom vocabulary to AI for post-processing
|
|
195
|
+
6. Sends transcription + custom vocabulary to AI for post-processing (with progress bar)
|
|
156
196
|
7. Applies suffix (if configured)
|
|
157
|
-
8.
|
|
197
|
+
8. Displays result with word count, character count, and cost estimate
|
|
198
|
+
9. Pipes to command (`--pipe`) or copies to clipboard
|
|
199
|
+
10. Saves transcription/audio files (if configured)
|
|
158
200
|
|
|
159
201
|
If transcription fails, the recording is saved to `~/.whspr/recordings/` for manual recovery.
|
|
160
202
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -4,10 +4,11 @@ import { transcribe } from "./transcribe.js";
|
|
|
4
4
|
import { postprocess } from "./postprocess.js";
|
|
5
5
|
import { copyToClipboard } from "./utils/clipboard.js";
|
|
6
6
|
import { calculateCost, formatCost } from "./utils/pricing.js";
|
|
7
|
-
import { renderStartupHeader, formatCompactStats, formatStatus, colors, BOX } from "./ui.js";
|
|
7
|
+
import { renderStartupHeader, formatCompactStats, formatStatus, colors, BOX, } from "./ui.js";
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import os from "os";
|
|
11
|
+
import { spawn } from "child_process";
|
|
11
12
|
// Default prompts (can be overridden in settings.json)
|
|
12
13
|
export const DEFAULTS = {
|
|
13
14
|
transcriptionModel: "whisper-large-v3-turbo",
|
|
@@ -27,6 +28,13 @@ const DEFAULT_SETTINGS = {
|
|
|
27
28
|
};
|
|
28
29
|
const WHSPR_DIR = path.join(os.homedir(), ".whspr");
|
|
29
30
|
const SETTINGS_PATH = path.join(WHSPR_DIR, "settings.json");
|
|
31
|
+
const TRANSCRIPTIONS_DIR = path.join(WHSPR_DIR, "transcriptions");
|
|
32
|
+
const RECORDINGS_DIR = path.join(WHSPR_DIR, "recordings");
|
|
33
|
+
function generateTimestampedFilename(extension) {
|
|
34
|
+
const now = new Date();
|
|
35
|
+
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
36
|
+
return `transcription-${timestamp}${extension}`;
|
|
37
|
+
}
|
|
30
38
|
function parseModelProvider(model) {
|
|
31
39
|
const colonIndex = model.indexOf(":");
|
|
32
40
|
if (colonIndex === -1) {
|
|
@@ -98,7 +106,40 @@ function loadCustomPrompt(verbose) {
|
|
|
98
106
|
return { prompt: combinedPrompt, sources };
|
|
99
107
|
}
|
|
100
108
|
const settings = loadSettings();
|
|
101
|
-
const verbose = settings.verbose ||
|
|
109
|
+
const verbose = settings.verbose ||
|
|
110
|
+
process.argv.includes("--verbose") ||
|
|
111
|
+
process.argv.includes("-v");
|
|
112
|
+
// Parse --pipe flag
|
|
113
|
+
function getPipeCommand() {
|
|
114
|
+
const pipeIndex = process.argv.findIndex((arg) => arg === "--pipe" || arg === "-p");
|
|
115
|
+
if (pipeIndex !== -1 && process.argv[pipeIndex + 1]) {
|
|
116
|
+
return process.argv[pipeIndex + 1];
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const pipeCommand = getPipeCommand();
|
|
121
|
+
// Execute a command with text piped to stdin
|
|
122
|
+
function pipeToCommand(text, command) {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const child = spawn(command, [], {
|
|
125
|
+
shell: true,
|
|
126
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
127
|
+
});
|
|
128
|
+
child.on("error", (err) => {
|
|
129
|
+
reject(new Error(`Failed to execute pipe command: ${err.message}`));
|
|
130
|
+
});
|
|
131
|
+
child.on("close", (code) => {
|
|
132
|
+
if (code === 0) {
|
|
133
|
+
resolve();
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
reject(new Error(`Pipe command exited with code ${code}`));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
child.stdin.write(text);
|
|
140
|
+
child.stdin.end();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
102
143
|
function status(message) {
|
|
103
144
|
process.stdout.write(`\x1b[2K\r${formatStatus(message)}`);
|
|
104
145
|
}
|
|
@@ -122,14 +163,14 @@ async function main() {
|
|
|
122
163
|
if (!process.env.GROQ_API_KEY) {
|
|
123
164
|
console.error(colors.error("Error: GROQ_API_KEY environment variable is not set"));
|
|
124
165
|
console.log(colors.metadata("Get your API key at https://console.groq.com/keys"));
|
|
125
|
-
console.log(colors.metadata(
|
|
166
|
+
console.log(colors.metadata('Then run: export GROQ_API_KEY="your-api-key"'));
|
|
126
167
|
process.exit(1);
|
|
127
168
|
}
|
|
128
169
|
// Check for provider-specific API key for post-processing
|
|
129
170
|
if (provider === "anthropic" && !process.env.ANTHROPIC_API_KEY) {
|
|
130
171
|
console.error(colors.error("Error: ANTHROPIC_API_KEY environment variable is not set"));
|
|
131
172
|
console.log(colors.metadata("Get your API key at https://console.anthropic.com/settings/keys"));
|
|
132
|
-
console.log(colors.metadata(
|
|
173
|
+
console.log(colors.metadata('Then run: export ANTHROPIC_API_KEY="your-api-key"'));
|
|
133
174
|
process.exit(1);
|
|
134
175
|
}
|
|
135
176
|
// Load custom prompt early to show in startup header
|
|
@@ -180,7 +221,10 @@ async function main() {
|
|
|
180
221
|
// 6. Output and copy
|
|
181
222
|
clearStatus();
|
|
182
223
|
const processTime = ((Date.now() - processStart) / 1000).toFixed(1);
|
|
183
|
-
const wordCount = fixedText
|
|
224
|
+
const wordCount = fixedText
|
|
225
|
+
.trim()
|
|
226
|
+
.split(/\s+/)
|
|
227
|
+
.filter((w) => w.length > 0).length;
|
|
184
228
|
const charCount = fixedText.length;
|
|
185
229
|
// Calculate cost if usage info is available
|
|
186
230
|
let costString;
|
|
@@ -192,7 +236,9 @@ async function main() {
|
|
|
192
236
|
const termWidth = Math.min(process.stdout.columns || 60, 80);
|
|
193
237
|
const lineWidth = termWidth - 2;
|
|
194
238
|
const label = " TRANSCRIPT ";
|
|
195
|
-
console.log(colors.dim(BOX.topLeft + BOX.horizontal) +
|
|
239
|
+
console.log(colors.dim(BOX.topLeft + BOX.horizontal) +
|
|
240
|
+
colors.header.bold(label) +
|
|
241
|
+
colors.dim(BOX.horizontal.repeat(lineWidth - label.length - 1) + BOX.topRight));
|
|
196
242
|
const lines = fixedText.split("\n");
|
|
197
243
|
for (const line of lines) {
|
|
198
244
|
// Wrap long lines
|
|
@@ -200,31 +246,79 @@ async function main() {
|
|
|
200
246
|
while (remaining.length > 0) {
|
|
201
247
|
const chunk = remaining.slice(0, lineWidth - 2);
|
|
202
248
|
remaining = remaining.slice(lineWidth - 2);
|
|
203
|
-
console.log(colors.dim(BOX.vertical + " ") +
|
|
249
|
+
console.log(colors.dim(BOX.vertical + " ") +
|
|
250
|
+
colors.white(chunk.padEnd(lineWidth - 2)) +
|
|
251
|
+
colors.dim(" " + BOX.vertical));
|
|
204
252
|
}
|
|
205
253
|
if (line.length === 0) {
|
|
206
|
-
console.log(colors.dim(BOX.vertical +
|
|
254
|
+
console.log(colors.dim(BOX.vertical +
|
|
255
|
+
" " +
|
|
256
|
+
" ".repeat(lineWidth - 2) +
|
|
257
|
+
" " +
|
|
258
|
+
BOX.vertical));
|
|
207
259
|
}
|
|
208
260
|
}
|
|
209
261
|
const stats = ` ${wordCount} words \u2022 ${charCount} chars `;
|
|
210
262
|
const bottomLine = BOX.horizontal.repeat(lineWidth - stats.length - 1) + " ";
|
|
211
|
-
console.log(colors.dim(BOX.bottomLeft + bottomLine) +
|
|
212
|
-
|
|
263
|
+
console.log(colors.dim(BOX.bottomLeft + bottomLine) +
|
|
264
|
+
colors.metadata(stats) +
|
|
265
|
+
colors.dim(BOX.bottomRight));
|
|
213
266
|
console.log(formatCompactStats({
|
|
214
267
|
audioDuration: formatDuration(recording.durationSeconds),
|
|
215
268
|
processingTime: processTime + "s",
|
|
216
269
|
cost: costString,
|
|
217
270
|
}));
|
|
218
|
-
|
|
219
|
-
|
|
271
|
+
// Either pipe to command or copy to clipboard
|
|
272
|
+
if (pipeCommand) {
|
|
273
|
+
try {
|
|
274
|
+
await pipeToCommand(fixedText, pipeCommand);
|
|
275
|
+
console.log(colors.success("\u2713") +
|
|
276
|
+
colors.metadata(` Piped to: ${pipeCommand}`));
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
console.error(colors.error(`Pipe failed: ${err}`));
|
|
280
|
+
// Fall back to clipboard
|
|
281
|
+
await copyToClipboard(fixedText);
|
|
282
|
+
console.log(colors.success("\u2713") +
|
|
283
|
+
colors.metadata(" Copied to clipboard (pipe failed)"));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
await copyToClipboard(fixedText);
|
|
288
|
+
console.log(colors.success("\u2713") + colors.metadata(" Copied to clipboard"));
|
|
289
|
+
}
|
|
290
|
+
// 7. Save transcription if configured
|
|
291
|
+
if (settings.alwaysSaveTranscriptions) {
|
|
292
|
+
const filename = generateTimestampedFilename(".txt");
|
|
293
|
+
let savePath;
|
|
294
|
+
if (settings.saveTranscriptionsToCwd) {
|
|
295
|
+
savePath = path.join(process.cwd(), filename);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
fs.mkdirSync(TRANSCRIPTIONS_DIR, { recursive: true });
|
|
299
|
+
savePath = path.join(TRANSCRIPTIONS_DIR, filename);
|
|
300
|
+
}
|
|
301
|
+
fs.writeFileSync(savePath, fixedText, "utf-8");
|
|
302
|
+
console.log(colors.success("\u2713") +
|
|
303
|
+
colors.metadata(` Saved transcription to: ${savePath}`));
|
|
304
|
+
}
|
|
305
|
+
// 8. Save audio if configured
|
|
306
|
+
if (settings.alwaysSaveAudio) {
|
|
307
|
+
fs.mkdirSync(RECORDINGS_DIR, { recursive: true });
|
|
308
|
+
const audioFilename = generateTimestampedFilename(".mp3");
|
|
309
|
+
const audioSavePath = path.join(RECORDINGS_DIR, audioFilename);
|
|
310
|
+
fs.copyFileSync(mp3Path, audioSavePath);
|
|
311
|
+
console.log(colors.success("\u2713") +
|
|
312
|
+
colors.metadata(` Saved audio to: ${audioSavePath}`));
|
|
313
|
+
}
|
|
314
|
+
// 9. Clean up
|
|
220
315
|
fs.unlinkSync(mp3Path);
|
|
221
316
|
}
|
|
222
317
|
catch (error) {
|
|
223
318
|
clearStatus();
|
|
224
|
-
// Save recording on failure
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const backupPath = path.join(backupDir, `recording-${Date.now()}.mp3`);
|
|
319
|
+
// Save recording on failure (post-processing failed, save audio only)
|
|
320
|
+
fs.mkdirSync(RECORDINGS_DIR, { recursive: true });
|
|
321
|
+
const backupPath = path.join(RECORDINGS_DIR, `recording-${Date.now()}.mp3`);
|
|
228
322
|
fs.renameSync(mp3Path, backupPath);
|
|
229
323
|
console.error(colors.error(`Error: ${error}`));
|
|
230
324
|
console.log(colors.info(`Recording saved to: ${backupPath}`));
|
package/dist/postprocess.js
CHANGED
|
@@ -10,7 +10,8 @@ export async function postprocess(rawTranscription, customPrompt, options) {
|
|
|
10
10
|
messages: [
|
|
11
11
|
{
|
|
12
12
|
role: "system",
|
|
13
|
-
content: systemPrompt +
|
|
13
|
+
content: systemPrompt +
|
|
14
|
+
"\n\nIMPORTANT: Output ONLY the corrected transcription text. Do not wrap it in JSON, markdown code blocks, or any other formatting. Just output the fixed text directly.",
|
|
14
15
|
},
|
|
15
16
|
{
|
|
16
17
|
role: "user",
|
package/dist/ui.js
CHANGED
|
@@ -25,7 +25,9 @@ export function renderStartupHeader(config) {
|
|
|
25
25
|
const termWidth = Math.min(process.stdout.columns || 60, 66);
|
|
26
26
|
const innerWidth = termWidth - 4; // Account for "│ " and " │"
|
|
27
27
|
const headerLabel = " WHSPR ";
|
|
28
|
-
const topLine = BOX.topLeft +
|
|
28
|
+
const topLine = BOX.topLeft +
|
|
29
|
+
BOX.horizontal +
|
|
30
|
+
colors.header.bold(headerLabel) +
|
|
29
31
|
colors.dim(BOX.horizontal.repeat(termWidth - headerLabel.length - 3) + BOX.topRight);
|
|
30
32
|
console.log(topLine);
|
|
31
33
|
// Model line
|
|
@@ -33,7 +35,8 @@ export function renderStartupHeader(config) {
|
|
|
33
35
|
const modelValue = config.model;
|
|
34
36
|
const modelLine = `${modelLabel}${modelValue}`;
|
|
35
37
|
console.log(colors.dim(BOX.vertical + " ") +
|
|
36
|
-
colors.metadata(modelLabel) +
|
|
38
|
+
colors.metadata(modelLabel) +
|
|
39
|
+
colors.white(modelValue) +
|
|
37
40
|
" ".repeat(Math.max(0, innerWidth - modelLine.length)) +
|
|
38
41
|
colors.dim(" " + BOX.vertical));
|
|
39
42
|
// Vocab line (only show if sources exist)
|
|
@@ -42,7 +45,8 @@ export function renderStartupHeader(config) {
|
|
|
42
45
|
const vocabValue = config.vocabSources.join(" + ");
|
|
43
46
|
const vocabLine = `${vocabLabel}${vocabValue}`;
|
|
44
47
|
console.log(colors.dim(BOX.vertical + " ") +
|
|
45
|
-
colors.metadata(vocabLabel) +
|
|
48
|
+
colors.metadata(vocabLabel) +
|
|
49
|
+
colors.info(vocabValue) +
|
|
46
50
|
" ".repeat(Math.max(0, innerWidth - vocabLine.length)) +
|
|
47
51
|
colors.dim(" " + BOX.vertical));
|
|
48
52
|
}
|
|
@@ -51,8 +55,10 @@ export function renderStartupHeader(config) {
|
|
|
51
55
|
console.log(); // Empty line after header
|
|
52
56
|
}
|
|
53
57
|
export function formatCompactStats(stats) {
|
|
54
|
-
let result = colors.metadata("Audio: ") +
|
|
55
|
-
colors.
|
|
58
|
+
let result = colors.metadata("Audio: ") +
|
|
59
|
+
colors.white(stats.audioDuration) +
|
|
60
|
+
colors.metadata(" \u2022 Processing: ") +
|
|
61
|
+
colors.white(stats.processingTime);
|
|
56
62
|
if (stats.cost) {
|
|
57
63
|
result += colors.metadata(" \u2022 Cost: ") + colors.white(stats.cost);
|
|
58
64
|
}
|
package/dist/utils/pricing.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const MODEL_PRICING = {
|
|
2
2
|
// Groq models
|
|
3
|
-
"openai/gpt-oss-120b": { input: 0.
|
|
3
|
+
"openai/gpt-oss-120b": { input: 0.0, output: 0.0 }, // Free tier pricing
|
|
4
4
|
// Anthropic models
|
|
5
|
-
"claude-sonnet-4-5": { input: 3.
|
|
6
|
-
"claude-haiku-4-5": { input: 0.
|
|
7
|
-
"claude-opus-4-5": { input: 15.
|
|
5
|
+
"claude-sonnet-4-5": { input: 3.0, output: 15.0 },
|
|
6
|
+
"claude-haiku-4-5": { input: 0.8, output: 4.0 },
|
|
7
|
+
"claude-opus-4-5": { input: 15.0, output: 75.0 },
|
|
8
8
|
};
|
|
9
9
|
export function calculateCost(modelName, usage) {
|
|
10
10
|
const pricing = MODEL_PRICING[modelName];
|