opencode-groq-dictate 1.1.3 → 1.1.4
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 +5 -2
- package/package.json +1 -1
- package/src/voice.test.ts +19 -2
- package/src/voice.ts +15 -7
- package/tui.tsx +5 -4
package/README.md
CHANGED
|
@@ -108,10 +108,13 @@ takes, spend in total.
|
|
|
108
108
|
|
|
109
109
|
## Cost and quota
|
|
110
110
|
|
|
111
|
-
- Cost is estimated on the audio length at **$0.04/hour**
|
|
111
|
+
- Cost is estimated on the audio length at the model's rate — **$0.04/hour** for
|
|
112
|
+
`whisper-large-v3-turbo`, **$0.111/hour** for `whisper-large-v3` — with Groq's 10-second
|
|
113
|
+
minimum billed length per request. It is appended to
|
|
112
114
|
`~/.cache/groq-dictate-costs.log`, the same file and the same four columns
|
|
113
115
|
(`date time seconds cost`) the older shell script wrote, so a `/dictate-cost` command
|
|
114
|
-
keeps working unchanged.
|
|
116
|
+
keeps working unchanged. The model name trails as an optional fifth column, so each
|
|
117
|
+
line stays attributable while readers of the first four columns ignore it.
|
|
115
118
|
- The **daily** request figure comes from the `x-ratelimit-*` headers of the real
|
|
116
119
|
transcription. No request is spent just to read the quota.
|
|
117
120
|
- The **per-minute** figure is tracked locally: timestamps of the last 60 seconds, cap
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-groq-dictate",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Groq dictation in the OpenCode v2 TUI: alt+v records, transcribes with whisper-large-v3-turbo, and pastes the text into the prompt.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
package/src/voice.test.ts
CHANGED
|
@@ -15,11 +15,17 @@ test("audio length comes from the bytes past the header", () => {
|
|
|
15
15
|
expect(audioSeconds(44 + 16_000 * 2 * 3)).toBe(3)
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
-
test("cost
|
|
19
|
-
expect(cost(3600)).toBeCloseTo(0.04, 6)
|
|
18
|
+
test("cost follows the model's hourly rate", () => {
|
|
19
|
+
expect(cost(3600, "whisper-large-v3-turbo")).toBeCloseTo(0.04, 6)
|
|
20
|
+
expect(cost(3600, "whisper-large-v3")).toBeCloseTo(0.111, 6)
|
|
20
21
|
expect(cost(0)).toBe(0)
|
|
21
22
|
})
|
|
22
23
|
|
|
24
|
+
test("a request bills at least ten seconds", () => {
|
|
25
|
+
expect(cost(4, "whisper-large-v3-turbo")).toBeCloseTo((10 / 3600) * 0.04, 9)
|
|
26
|
+
expect(cost(30, "whisper-large-v3")).toBeCloseTo((30 / 3600) * 0.111, 9)
|
|
27
|
+
})
|
|
28
|
+
|
|
23
29
|
test("the rpm window drops stamps older than a minute", () => {
|
|
24
30
|
const now = 100_000
|
|
25
31
|
expect(recentRpm([now - 61_000, now - 1_000, now], now)).toEqual([now - 1_000, now])
|
|
@@ -41,6 +47,17 @@ test("a config with the wrong types falls back to the defaults", () => {
|
|
|
41
47
|
|
|
42
48
|
test("the cost line keeps the shell script's four columns", () => {
|
|
43
49
|
expect(logLine(new Date(2026, 9, 19, 8, 5, 3), 12.34, 0.000137)).toBe("2026-10-19 08:05:03 12.3 0.000137")
|
|
50
|
+
expect(logLine(new Date(2026, 9, 19, 8, 5, 3), 12.34, 0.000137, "whisper-large-v3")).toBe(
|
|
51
|
+
"2026-10-19 08:05:03 12.3 0.000137 whisper-large-v3",
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("the totals ignore the optional model column", () => {
|
|
56
|
+
const now = new Date(2026, 9, 19, 12, 0, 0)
|
|
57
|
+
const log = ["2026-10-19 08:05:03 12.3 0.000137", "2026-10-19 09:00:00 30.0 0.000333 whisper-large-v3"].join("\n")
|
|
58
|
+
const totals = summarize(log, now)
|
|
59
|
+
expect(totals.today).toBeCloseTo(0.00047, 9)
|
|
60
|
+
expect(totals.todayCount).toBe(2)
|
|
44
61
|
})
|
|
45
62
|
|
|
46
63
|
test("the totals only count today's lines, and ignore junk", () => {
|
package/src/voice.ts
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
export const MODELS = ["whisper-large-v3-turbo", "whisper-large-v3"] as const
|
|
6
6
|
export type Model = (typeof MODELS)[number]
|
|
7
7
|
export const DEFAULT_MODEL: Model = "whisper-large-v3-turbo"
|
|
8
|
-
//
|
|
9
|
-
export const
|
|
8
|
+
// Groq bills per audio hour: large-v3 is ~2.8x turbo, not the same rate.
|
|
9
|
+
export const RATE_PER_HOUR: Record<Model, number> = {
|
|
10
|
+
"whisper-large-v3-turbo": 0.04,
|
|
11
|
+
"whisper-large-v3": 0.111,
|
|
12
|
+
}
|
|
13
|
+
// Groq bills a minimum of 10 seconds per request, however short the take.
|
|
14
|
+
export const MIN_BILLED_SECONDS = 10
|
|
10
15
|
export const RPM_LIMIT = 20
|
|
11
16
|
// A forgotten recording would upload silence for minutes; cap it.
|
|
12
17
|
export const MAX_SECONDS = 60
|
|
@@ -33,8 +38,9 @@ export function audioSeconds(bytes: number): number {
|
|
|
33
38
|
return Math.max(0, bytes - WAV_HEADER) / (SAMPLE_RATE * BYTES_PER_SAMPLE)
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
export function cost(seconds: number): number {
|
|
37
|
-
|
|
41
|
+
export function cost(seconds: number, model: Model = DEFAULT_MODEL): number {
|
|
42
|
+
const billable = seconds > 0 ? Math.max(seconds, MIN_BILLED_SECONDS) : 0
|
|
43
|
+
return (billable / 3600) * RATE_PER_HOUR[model]
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
export function recentRpm(stamps: number[], now: number): number[] {
|
|
@@ -42,12 +48,14 @@ export function recentRpm(stamps: number[], now: number): number[] {
|
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
// Same columns the shell script wrote: `date time seconds cost`, which is what
|
|
45
|
-
// the /dictate-cost command's awk still reads.
|
|
46
|
-
|
|
51
|
+
// the /dictate-cost command's awk still reads. The model is appended as a fifth
|
|
52
|
+
// column so a rate change stays attributable; older lines simply lack it.
|
|
53
|
+
export function logLine(at: Date, seconds: number, usd: number, model?: Model): string {
|
|
47
54
|
const pad = (n: number) => String(n).padStart(2, "0")
|
|
48
55
|
const date = `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`
|
|
49
56
|
const time = `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}`
|
|
50
|
-
|
|
57
|
+
const line = `${date} ${time} ${seconds.toFixed(1)} ${usd.toFixed(6)}`
|
|
58
|
+
return model ? `${line} ${model}` : line
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
export type Totals = { today: number; total: number; todayCount: number }
|
package/tui.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
recentRpm,
|
|
18
18
|
startTicker,
|
|
19
19
|
summarize,
|
|
20
|
+
type Model,
|
|
20
21
|
} from "./src/voice"
|
|
21
22
|
|
|
22
23
|
const WAV = "/tmp/opencode-groq-dictate.wav"
|
|
@@ -119,10 +120,10 @@ export default Plugin.define({
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
// Cost bookkeeping: the log line is best effort, the totals are in memory.
|
|
122
|
-
async function bookkeep(seconds: number) {
|
|
123
|
-
const usd = cost(seconds)
|
|
123
|
+
async function bookkeep(seconds: number, model: Model) {
|
|
124
|
+
const usd = cost(seconds, model)
|
|
124
125
|
try {
|
|
125
|
-
await appendFile(COST_LOG, logLine(new Date(), seconds, usd) + "\n")
|
|
126
|
+
await appendFile(COST_LOG, logLine(new Date(), seconds, usd, model) + "\n")
|
|
126
127
|
} catch (error) {
|
|
127
128
|
context.ui.toast.show({ message: `Cost log: ${String(error)}`, variant: "warning" })
|
|
128
129
|
}
|
|
@@ -155,7 +156,7 @@ export default Plugin.define({
|
|
|
155
156
|
// a copy of everything dictated on disk.
|
|
156
157
|
await rm(WAV, { force: true }).catch(() => {})
|
|
157
158
|
|
|
158
|
-
const usd = await bookkeep(seconds)
|
|
159
|
+
const usd = await bookkeep(seconds, model)
|
|
159
160
|
context.ui.toast.show({ message: `Dictated ${seconds.toFixed(1)}s · $${usd.toFixed(4)}`, variant: "success" })
|
|
160
161
|
}
|
|
161
162
|
|