whspr 1.0.13 → 1.0.14
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/dist/index.js +48 -33
- package/dist/postprocess.d.ts +6 -1
- package/dist/postprocess.js +6 -1
- package/dist/ui.d.ts +33 -0
- package/dist/ui.js +66 -0
- package/dist/utils/pricing.d.ts +11 -0
- package/dist/utils/pricing.js +29 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import { record, convertToMp3 } from "./recorder.js";
|
|
|
3
3
|
import { transcribe } from "./transcribe.js";
|
|
4
4
|
import { postprocess } from "./postprocess.js";
|
|
5
5
|
import { copyToClipboard } from "./utils/clipboard.js";
|
|
6
|
-
import
|
|
6
|
+
import { calculateCost, formatCost } from "./utils/pricing.js";
|
|
7
|
+
import { renderStartupHeader, formatCompactStats, formatStatus, colors, BOX } from "./ui.js";
|
|
7
8
|
import fs from "fs";
|
|
8
9
|
import path from "path";
|
|
9
10
|
import os from "os";
|
|
@@ -99,7 +100,7 @@ function loadCustomPrompt(verbose) {
|
|
|
99
100
|
const settings = loadSettings();
|
|
100
101
|
const verbose = settings.verbose || process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
101
102
|
function status(message) {
|
|
102
|
-
process.stdout.write(`\x1b[2K\r${
|
|
103
|
+
process.stdout.write(`\x1b[2K\r${formatStatus(message)}`);
|
|
103
104
|
}
|
|
104
105
|
function clearStatus() {
|
|
105
106
|
process.stdout.write("\x1b[2K\r");
|
|
@@ -119,18 +120,25 @@ async function main() {
|
|
|
119
120
|
// Check for required API keys before recording
|
|
120
121
|
// Always need GROQ_API_KEY for Whisper transcription
|
|
121
122
|
if (!process.env.GROQ_API_KEY) {
|
|
122
|
-
console.error(
|
|
123
|
-
console.log(
|
|
124
|
-
console.log(
|
|
123
|
+
console.error(colors.error("Error: GROQ_API_KEY environment variable is not set"));
|
|
124
|
+
console.log(colors.metadata("Get your API key at https://console.groq.com/keys"));
|
|
125
|
+
console.log(colors.metadata("Then run: export GROQ_API_KEY=\"your-api-key\""));
|
|
125
126
|
process.exit(1);
|
|
126
127
|
}
|
|
127
128
|
// Check for provider-specific API key for post-processing
|
|
128
129
|
if (provider === "anthropic" && !process.env.ANTHROPIC_API_KEY) {
|
|
129
|
-
console.error(
|
|
130
|
-
console.log(
|
|
131
|
-
console.log(
|
|
130
|
+
console.error(colors.error("Error: ANTHROPIC_API_KEY environment variable is not set"));
|
|
131
|
+
console.log(colors.metadata("Get your API key at https://console.anthropic.com/settings/keys"));
|
|
132
|
+
console.log(colors.metadata("Then run: export ANTHROPIC_API_KEY=\"your-api-key\""));
|
|
132
133
|
process.exit(1);
|
|
133
134
|
}
|
|
135
|
+
// Load custom prompt early to show in startup header
|
|
136
|
+
const { prompt: customPrompt, sources: vocabSources } = loadCustomPrompt(verbose);
|
|
137
|
+
// Display startup header
|
|
138
|
+
renderStartupHeader({
|
|
139
|
+
model: modelConfig,
|
|
140
|
+
vocabSources,
|
|
141
|
+
});
|
|
134
142
|
try {
|
|
135
143
|
// 1. Record audio
|
|
136
144
|
const recording = await record(verbose);
|
|
@@ -144,16 +152,14 @@ async function main() {
|
|
|
144
152
|
const rawText = await transcribe(mp3Path, settings.transcriptionModel ?? DEFAULTS.transcriptionModel, settings.language ?? DEFAULTS.language);
|
|
145
153
|
if (verbose) {
|
|
146
154
|
clearStatus();
|
|
147
|
-
console.log(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (customPrompt && verbose) {
|
|
152
|
-
console.log(chalk.gray(`Using custom vocabulary from: ${vocabSources.join(" + ")}`));
|
|
155
|
+
console.log(colors.metadata(`Raw: ${rawText}`));
|
|
156
|
+
if (customPrompt) {
|
|
157
|
+
console.log(colors.metadata(`Using custom vocabulary from: ${vocabSources.join(" + ")}`));
|
|
158
|
+
}
|
|
153
159
|
}
|
|
154
|
-
//
|
|
160
|
+
// 4. Post-process with progress bar
|
|
155
161
|
status("Post-processing... 0%");
|
|
156
|
-
|
|
162
|
+
const postprocessResult = await postprocess(rawText, customPrompt, {
|
|
157
163
|
provider,
|
|
158
164
|
modelName,
|
|
159
165
|
systemPrompt: settings.systemPrompt ?? DEFAULTS.systemPrompt,
|
|
@@ -162,27 +168,31 @@ async function main() {
|
|
|
162
168
|
onProgress: (progress) => {
|
|
163
169
|
const barWidth = 20;
|
|
164
170
|
const filled = Math.round((progress / 100) * barWidth);
|
|
165
|
-
const bar = "
|
|
171
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
|
|
166
172
|
status(`Post-processing... [${bar}] ${progress}%`);
|
|
167
173
|
},
|
|
168
174
|
});
|
|
169
|
-
|
|
175
|
+
let fixedText = postprocessResult.text;
|
|
176
|
+
// 5. Apply suffix if configured
|
|
170
177
|
if (settings.suffix) {
|
|
171
178
|
fixedText = fixedText + settings.suffix;
|
|
172
179
|
}
|
|
173
|
-
//
|
|
180
|
+
// 6. Output and copy
|
|
174
181
|
clearStatus();
|
|
175
182
|
const processTime = ((Date.now() - processStart) / 1000).toFixed(1);
|
|
176
183
|
const wordCount = fixedText.trim().split(/\s+/).filter(w => w.length > 0).length;
|
|
177
184
|
const charCount = fixedText.length;
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
// Calculate cost if usage info is available
|
|
186
|
+
let costString;
|
|
187
|
+
if (postprocessResult.usage) {
|
|
188
|
+
const cost = calculateCost(modelName, postprocessResult.usage);
|
|
189
|
+
costString = formatCost(cost);
|
|
190
|
+
}
|
|
181
191
|
// Draw box
|
|
182
192
|
const termWidth = Math.min(process.stdout.columns || 60, 80);
|
|
183
193
|
const lineWidth = termWidth - 2;
|
|
184
194
|
const label = " TRANSCRIPT ";
|
|
185
|
-
console.log(
|
|
195
|
+
console.log(colors.dim(BOX.topLeft + BOX.horizontal) + colors.header.bold(label) + colors.dim(BOX.horizontal.repeat(lineWidth - label.length - 1) + BOX.topRight));
|
|
186
196
|
const lines = fixedText.split("\n");
|
|
187
197
|
for (const line of lines) {
|
|
188
198
|
// Wrap long lines
|
|
@@ -190,18 +200,23 @@ async function main() {
|
|
|
190
200
|
while (remaining.length > 0) {
|
|
191
201
|
const chunk = remaining.slice(0, lineWidth - 2);
|
|
192
202
|
remaining = remaining.slice(lineWidth - 2);
|
|
193
|
-
console.log(
|
|
203
|
+
console.log(colors.dim(BOX.vertical + " ") + colors.white(chunk.padEnd(lineWidth - 2)) + colors.dim(" " + BOX.vertical));
|
|
194
204
|
}
|
|
195
205
|
if (line.length === 0) {
|
|
196
|
-
console.log(
|
|
206
|
+
console.log(colors.dim(BOX.vertical + " " + " ".repeat(lineWidth - 2) + " " + BOX.vertical));
|
|
197
207
|
}
|
|
198
208
|
}
|
|
199
|
-
const stats = ` ${wordCount} words
|
|
200
|
-
const bottomLine =
|
|
201
|
-
console.log(
|
|
209
|
+
const stats = ` ${wordCount} words \u2022 ${charCount} chars `;
|
|
210
|
+
const bottomLine = BOX.horizontal.repeat(lineWidth - stats.length - 1) + " ";
|
|
211
|
+
console.log(colors.dim(BOX.bottomLeft + bottomLine) + colors.metadata(stats) + colors.dim(BOX.bottomRight));
|
|
202
212
|
await copyToClipboard(fixedText);
|
|
203
|
-
console.log(
|
|
204
|
-
|
|
213
|
+
console.log(formatCompactStats({
|
|
214
|
+
audioDuration: formatDuration(recording.durationSeconds),
|
|
215
|
+
processingTime: processTime + "s",
|
|
216
|
+
cost: costString,
|
|
217
|
+
}));
|
|
218
|
+
console.log(colors.success("\u2713") + colors.metadata(" Copied to clipboard"));
|
|
219
|
+
// 7. Clean up
|
|
205
220
|
fs.unlinkSync(mp3Path);
|
|
206
221
|
}
|
|
207
222
|
catch (error) {
|
|
@@ -211,8 +226,8 @@ async function main() {
|
|
|
211
226
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
212
227
|
const backupPath = path.join(backupDir, `recording-${Date.now()}.mp3`);
|
|
213
228
|
fs.renameSync(mp3Path, backupPath);
|
|
214
|
-
console.error(
|
|
215
|
-
console.log(
|
|
229
|
+
console.error(colors.error(`Error: ${error}`));
|
|
230
|
+
console.log(colors.info(`Recording saved to: ${backupPath}`));
|
|
216
231
|
process.exit(1);
|
|
217
232
|
}
|
|
218
233
|
}
|
|
@@ -222,7 +237,7 @@ async function main() {
|
|
|
222
237
|
if (error instanceof Error && error.message === "cancelled") {
|
|
223
238
|
process.exit(0);
|
|
224
239
|
}
|
|
225
|
-
console.error(
|
|
240
|
+
console.error(colors.error(`Recording error: ${error}`));
|
|
226
241
|
process.exit(1);
|
|
227
242
|
}
|
|
228
243
|
}
|
package/dist/postprocess.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ProviderType } from "./utils/providers.js";
|
|
2
|
+
import type { UsageInfo } from "./utils/pricing.js";
|
|
2
3
|
export interface PostprocessOptions {
|
|
3
4
|
provider: ProviderType;
|
|
4
5
|
modelName: string;
|
|
@@ -7,4 +8,8 @@ export interface PostprocessOptions {
|
|
|
7
8
|
transcriptionPrefix: string;
|
|
8
9
|
onProgress?: (progress: number) => void;
|
|
9
10
|
}
|
|
10
|
-
export
|
|
11
|
+
export interface PostprocessResult {
|
|
12
|
+
text: string;
|
|
13
|
+
usage?: UsageInfo;
|
|
14
|
+
}
|
|
15
|
+
export declare function postprocess(rawTranscription: string, customPrompt: string | null, options: PostprocessOptions): Promise<PostprocessResult>;
|
package/dist/postprocess.js
CHANGED
|
@@ -35,7 +35,12 @@ export async function postprocess(rawTranscription, customPrompt, options) {
|
|
|
35
35
|
onProgress(progress);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
// Capture usage info after stream completes
|
|
39
|
+
const usage = await textStream.usage;
|
|
40
|
+
const usageInfo = usage?.inputTokens !== undefined && usage?.outputTokens !== undefined
|
|
41
|
+
? { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens }
|
|
42
|
+
: undefined;
|
|
43
|
+
return { text: accumulated.trim(), usage: usageInfo };
|
|
39
44
|
}, 3, "postprocess");
|
|
40
45
|
return result;
|
|
41
46
|
}
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const BOX: {
|
|
2
|
+
readonly topLeft: "┌";
|
|
3
|
+
readonly topRight: "┐";
|
|
4
|
+
readonly bottomLeft: "└";
|
|
5
|
+
readonly bottomRight: "┘";
|
|
6
|
+
readonly horizontal: "─";
|
|
7
|
+
readonly vertical: "│";
|
|
8
|
+
readonly teeRight: "├";
|
|
9
|
+
readonly teeLeft: "┤";
|
|
10
|
+
};
|
|
11
|
+
export declare const colors: {
|
|
12
|
+
readonly header: import("chalk").ChalkInstance;
|
|
13
|
+
readonly action: import("chalk").ChalkInstance;
|
|
14
|
+
readonly info: import("chalk").ChalkInstance;
|
|
15
|
+
readonly metadata: import("chalk").ChalkInstance;
|
|
16
|
+
readonly success: import("chalk").ChalkInstance;
|
|
17
|
+
readonly error: import("chalk").ChalkInstance;
|
|
18
|
+
readonly dim: import("chalk").ChalkInstance;
|
|
19
|
+
readonly white: import("chalk").ChalkInstance;
|
|
20
|
+
};
|
|
21
|
+
export interface StartupConfig {
|
|
22
|
+
model: string;
|
|
23
|
+
vocabSources: string[];
|
|
24
|
+
}
|
|
25
|
+
export declare function renderStartupHeader(config: StartupConfig): void;
|
|
26
|
+
export interface CompactStats {
|
|
27
|
+
audioDuration: string;
|
|
28
|
+
processingTime: string;
|
|
29
|
+
cost?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function formatCompactStats(stats: CompactStats): string;
|
|
32
|
+
export declare function statusPrefix(): string;
|
|
33
|
+
export declare function formatStatus(message: string): string;
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
// Box-drawing characters
|
|
3
|
+
export const BOX = {
|
|
4
|
+
topLeft: "┌",
|
|
5
|
+
topRight: "┐",
|
|
6
|
+
bottomLeft: "└",
|
|
7
|
+
bottomRight: "┘",
|
|
8
|
+
horizontal: "─",
|
|
9
|
+
vertical: "│",
|
|
10
|
+
teeRight: "├",
|
|
11
|
+
teeLeft: "┤",
|
|
12
|
+
};
|
|
13
|
+
// Semantic colors following Research-Agent aesthetics
|
|
14
|
+
export const colors = {
|
|
15
|
+
header: chalk.blue,
|
|
16
|
+
action: chalk.cyan,
|
|
17
|
+
info: chalk.yellow.italic,
|
|
18
|
+
metadata: chalk.gray,
|
|
19
|
+
success: chalk.green,
|
|
20
|
+
error: chalk.red,
|
|
21
|
+
dim: chalk.dim,
|
|
22
|
+
white: chalk.white,
|
|
23
|
+
};
|
|
24
|
+
export function renderStartupHeader(config) {
|
|
25
|
+
const termWidth = Math.min(process.stdout.columns || 60, 66);
|
|
26
|
+
const innerWidth = termWidth - 4; // Account for "│ " and " │"
|
|
27
|
+
const headerLabel = " WHSPR ";
|
|
28
|
+
const topLine = BOX.topLeft + BOX.horizontal + colors.header.bold(headerLabel) +
|
|
29
|
+
colors.dim(BOX.horizontal.repeat(termWidth - headerLabel.length - 3) + BOX.topRight);
|
|
30
|
+
console.log(topLine);
|
|
31
|
+
// Model line
|
|
32
|
+
const modelLabel = "Model: ";
|
|
33
|
+
const modelValue = config.model;
|
|
34
|
+
const modelLine = `${modelLabel}${modelValue}`;
|
|
35
|
+
console.log(colors.dim(BOX.vertical + " ") +
|
|
36
|
+
colors.metadata(modelLabel) + colors.white(modelValue) +
|
|
37
|
+
" ".repeat(Math.max(0, innerWidth - modelLine.length)) +
|
|
38
|
+
colors.dim(" " + BOX.vertical));
|
|
39
|
+
// Vocab line (only show if sources exist)
|
|
40
|
+
if (config.vocabSources.length > 0) {
|
|
41
|
+
const vocabLabel = "Vocab: ";
|
|
42
|
+
const vocabValue = config.vocabSources.join(" + ");
|
|
43
|
+
const vocabLine = `${vocabLabel}${vocabValue}`;
|
|
44
|
+
console.log(colors.dim(BOX.vertical + " ") +
|
|
45
|
+
colors.metadata(vocabLabel) + colors.info(vocabValue) +
|
|
46
|
+
" ".repeat(Math.max(0, innerWidth - vocabLine.length)) +
|
|
47
|
+
colors.dim(" " + BOX.vertical));
|
|
48
|
+
}
|
|
49
|
+
// Bottom border
|
|
50
|
+
console.log(colors.dim(BOX.bottomLeft + BOX.horizontal.repeat(termWidth - 2) + BOX.bottomRight));
|
|
51
|
+
console.log(); // Empty line after header
|
|
52
|
+
}
|
|
53
|
+
export function formatCompactStats(stats) {
|
|
54
|
+
let result = colors.metadata("Audio: ") + colors.white(stats.audioDuration) +
|
|
55
|
+
colors.metadata(" \u2022 Processing: ") + colors.white(stats.processingTime);
|
|
56
|
+
if (stats.cost) {
|
|
57
|
+
result += colors.metadata(" \u2022 Cost: ") + colors.white(stats.cost);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
export function statusPrefix() {
|
|
62
|
+
return colors.dim(BOX.teeRight + BOX.horizontal + " ");
|
|
63
|
+
}
|
|
64
|
+
export function formatStatus(message) {
|
|
65
|
+
return statusPrefix() + colors.action(message);
|
|
66
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ModelPricing {
|
|
2
|
+
input: number;
|
|
3
|
+
output: number;
|
|
4
|
+
}
|
|
5
|
+
export declare const MODEL_PRICING: Record<string, ModelPricing>;
|
|
6
|
+
export interface UsageInfo {
|
|
7
|
+
inputTokens: number;
|
|
8
|
+
outputTokens: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function calculateCost(modelName: string, usage: UsageInfo): number;
|
|
11
|
+
export declare function formatCost(cost: number): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const MODEL_PRICING = {
|
|
2
|
+
// Groq models
|
|
3
|
+
"openai/gpt-oss-120b": { input: 0.00, output: 0.00 }, // Free tier pricing
|
|
4
|
+
// Anthropic models
|
|
5
|
+
"claude-sonnet-4-5": { input: 3.00, output: 15.00 },
|
|
6
|
+
"claude-haiku-4-5": { input: 0.80, output: 4.00 },
|
|
7
|
+
"claude-opus-4-5": { input: 15.00, output: 75.00 },
|
|
8
|
+
};
|
|
9
|
+
export function calculateCost(modelName, usage) {
|
|
10
|
+
const pricing = MODEL_PRICING[modelName];
|
|
11
|
+
if (!pricing) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
const inputCost = (usage.inputTokens / 1_000_000) * pricing.input;
|
|
15
|
+
const outputCost = (usage.outputTokens / 1_000_000) * pricing.output;
|
|
16
|
+
return inputCost + outputCost;
|
|
17
|
+
}
|
|
18
|
+
export function formatCost(cost) {
|
|
19
|
+
if (cost === 0) {
|
|
20
|
+
return "$0.00";
|
|
21
|
+
}
|
|
22
|
+
if (cost < 0.0001) {
|
|
23
|
+
return `$${cost.toFixed(6)}`;
|
|
24
|
+
}
|
|
25
|
+
if (cost < 0.01) {
|
|
26
|
+
return `$${cost.toFixed(4)}`;
|
|
27
|
+
}
|
|
28
|
+
return `$${cost.toFixed(2)}`;
|
|
29
|
+
}
|