diff --git a/README.md b/README.md index 411358d..000aa04 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,24 @@ A Tampermonkey/Violentmonkey userscript — a mic button in the OpenCode web UI. 3. Open the [script install link](https://raw.githubusercontent.com/slaid098/opencode-voice-dictation/dist/opencode-voice-dictation.user.js) — it installs into Tampermonkey 4. Tampermonkey menu → **Set Groq API Key** → paste `gsk_...` +### 🌐 Custom STT Endpoint + +Groq may block direct requests from some networks. Point the script at your own nginx proxy: + +1. Deploy an nginx reverse proxy that forwards to `api.groq.com`: + ```nginx + location /groq/ { + proxy_pass https://api.groq.com/; + proxy_set_header Host api.groq.com; + } + ``` +2. Tampermonkey menu → **Set STT Endpoint** → paste `https://your-domain.com/groq/openai/v1/audio/transcriptions` +3. Requests now go through your proxy. The userscript metadata uses `@connect *`, so any domain is allowed. + +### 🌡️ Temperature + +Whisper may hallucinate on silence/noise. **Set Temperature** (default `0` = deterministic, range `0`–`1`) reduces hallucinations. + --- ## 🇷🇺 Русский @@ -77,6 +95,24 @@ A Tampermonkey/Violentmonkey userscript — a mic button in the OpenCode web UI. 3. Открой [ссылку установки скрипта](https://raw.githubusercontent.com/slaid098/opencode-voice-dictation/dist/opencode-voice-dictation.user.js) — скрипт установится в Tampermonkey 4. Меню Tampermonkey → **Set Groq API Key** → вставь `gsk_...` +### 🌐 Кастомный STT endpoint + +Groq может блокировать прямые запросы из некоторых сетей. Направь скрипт на свой nginx-прокси: + +1. Разверни nginx reverse proxy, который форвардит на `api.groq.com`: + ```nginx + location /groq/ { + proxy_pass https://api.groq.com/; + proxy_set_header Host api.groq.com; + } + ``` +2. Меню Tampermonkey → **Set STT Endpoint** → вставь `https://your-domain.com/groq/openai/v1/audio/transcriptions` +3. Запросы пойдут через твой прокси. Метаблок юзерскрипта использует `@connect *`, поэтому разрешён любой домен. + +### 🌡️ Temperature + +Whisper может галлюцинировать на тишине/шуме. **Set Temperature** (по умолчанию `0` = детерминированный вывод, диапазон `0`–`1`) снижает галлюцинации. + --- ## 💬 Support and contacts / Поддержка и контакты diff --git a/src/config.ts b/src/config.ts index 35671e3..b9fb6c3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,7 +6,9 @@ export const DEFAULTS: AppConfig = { model: "whisper-large-v3", language: "", whisperPrompt: - "Software development discussion. Common terms: API, JSON, async, await, function, class, component, endpoint, deployment, refactoring, merge, commit, pull request, branch, repository, TypeScript, Python, Docker, Kubernetes, OpenCode, Whisper, Groq, contenteditable, SolidJS, Vite, Biome, Vitest, MutationObserver, FormData, MediaRecorder.", + "opencode, voice, dictation, transcribe, command, terminal, commit, branch, pull, push, merge, issue, prompt", + endpoint: "https://api.groq.com/openai/v1/audio/transcriptions", + temperature: 0, autoSubmit: false, }; @@ -16,6 +18,8 @@ export function getConfig(): AppConfig { model: GM_getValue("model", DEFAULTS.model), language: GM_getValue("language", DEFAULTS.language), whisperPrompt: GM_getValue("whisperPrompt", DEFAULTS.whisperPrompt), + endpoint: GM_getValue("endpoint", DEFAULTS.endpoint), + temperature: GM_getValue("temperature", DEFAULTS.temperature), autoSubmit: GM_getValue("autoSubmit", DEFAULTS.autoSubmit), }; } @@ -40,10 +44,14 @@ export function registerMenuCommands(callbacks: { onSetModel: () => void; onSetLanguage: () => void; onSetPrompt: () => void; + onSetEndpoint: () => void; + onSetTemperature: () => void; }): void { GM_registerMenuCommand("Set Groq API Key", callbacks.onSetKey); GM_registerMenuCommand("Toggle Auto-Submit", callbacks.onToggleAutoSubmit); GM_registerMenuCommand("Set Whisper Model", callbacks.onSetModel); GM_registerMenuCommand("Set Language", callbacks.onSetLanguage); GM_registerMenuCommand("Set Whisper Prompt", callbacks.onSetPrompt); + GM_registerMenuCommand("Set STT Endpoint", callbacks.onSetEndpoint); + GM_registerMenuCommand("Set Temperature", callbacks.onSetTemperature); } diff --git a/src/index.ts b/src/index.ts index 08d1049..8434f44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,14 @@ import { type AudioRecorder, createAudioRecorder } from "./audio.js"; import { + DEFAULTS, getConfig, isFirstRun, registerMenuCommands, setConfig, validateApiKey, } from "./config.js"; + +const DEFAULTS_ENDPOINT = DEFAULTS.endpoint; import { type InsertTarget, insertText, @@ -158,6 +161,34 @@ function promptForWhisperPrompt(): void { } } +function promptForEndpoint(): void { + const current = getConfig().endpoint; + const url = prompt("STT endpoint URL:", current); + if (url === null) { + return; + } + const trimmed = url.trim(); + const value = trimmed || DEFAULTS_ENDPOINT; + setConfig({ endpoint: value }); + ui?.toast(`Endpoint set to ${value}`); +} + +function promptForTemperature(): void { + const current = getConfig().temperature; + const input = prompt("Temperature (0-1):", String(current)); + if (input === null) { + return; + } + const parsed = Number(input); + if (Number.isNaN(parsed)) { + ui?.toast("Invalid temperature", true); + return; + } + const clamped = Math.min(1, Math.max(0, parsed)); + setConfig({ temperature: clamped }); + ui?.toast(`Temperature set to ${clamped}`); +} + function toggleAutoSubmit(): void { const config = getConfig(); setConfig({ autoSubmit: !config.autoSubmit }); @@ -199,6 +230,8 @@ function init(): void { onSetModel: promptForModel, onSetLanguage: promptForLanguage, onSetPrompt: promptForWhisperPrompt, + onSetEndpoint: promptForEndpoint, + onSetTemperature: promptForTemperature, }); checkFirstRun(); diff --git a/src/transcribe.ts b/src/transcribe.ts index f903e5d..3c26350 100644 --- a/src/transcribe.ts +++ b/src/transcribe.ts @@ -1,13 +1,12 @@ import { GM_xmlhttpRequest } from "$"; import type { AppConfig, TranscriptionResult } from "./types.js"; -const GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions"; - export function buildFormData(audioBlob: Blob, config: AppConfig): FormData { const formData = new FormData(); formData.append("file", audioBlob, "audio.webm"); formData.append("model", config.model); formData.append("response_format", "text"); + formData.append("temperature", String(config.temperature)); if (config.language) { formData.append("language", config.language); } @@ -51,7 +50,7 @@ export function transcribe(audioBlob: Blob, config: AppConfig): Promise { expect(DEFAULTS.whisperPrompt.length).toBeGreaterThan(50); }); + it("should have whisperPrompt under 120 characters (terms only, no sentences)", () => { + expect(DEFAULTS.whisperPrompt.length).toBeLessThan(120); + }); + it("should have autoSubmit disabled by default", () => { expect(DEFAULTS.autoSubmit).toBe(false); }); + + it("should have default Groq endpoint", () => { + expect(DEFAULTS.endpoint).toBe("https://api.groq.com/openai/v1/audio/transcriptions"); + }); + + it("should have temperature 0 by default", () => { + expect(DEFAULTS.temperature).toBe(0); + }); }); describe("validateApiKey", () => { @@ -108,8 +120,10 @@ describe("registerMenuCommands", () => { onSetModel: () => {}, onSetLanguage: () => {}, onSetPrompt: () => {}, + onSetEndpoint: () => {}, + onSetTemperature: () => {}, }; registerMenuCommands(callbacks); - expect(GM_registerMenuCommand).toHaveBeenCalledTimes(5); + expect(GM_registerMenuCommand).toHaveBeenCalledTimes(7); }); }); diff --git a/tests/transcribe.test.ts b/tests/transcribe.test.ts index 15d720f..abd933f 100644 --- a/tests/transcribe.test.ts +++ b/tests/transcribe.test.ts @@ -13,6 +13,8 @@ const mockConfig: AppConfig = { model: "whisper-large-v3", language: "ru", whisperPrompt: "Software development discussion.", + endpoint: "https://api.groq.com/openai/v1/audio/transcriptions", + temperature: 0, autoSubmit: false, }; @@ -31,6 +33,7 @@ describe("buildFormData", () => { expect(formData.get("response_format")).toBe("text"); expect(formData.get("language")).toBe("ru"); expect(formData.get("prompt")).toBe("Software development discussion."); + expect(formData.get("temperature")).toBe("0"); const file = formData.get("file") as File; expect(file).toBeInstanceOf(Blob); @@ -44,6 +47,15 @@ describe("buildFormData", () => { expect(formData.get("response_format")).toBe("text"); expect(formData.get("language")).toBeNull(); expect(formData.get("prompt")).toBeNull(); + expect(formData.get("temperature")).toBe("0"); + }); + + it("should send temperature as string", () => { + const blob = new Blob(["audio data"], { type: "audio/webm" }); + const config = { ...mockConfig, temperature: 0.5 }; + const formData = buildFormData(blob, config); + + expect(formData.get("temperature")).toBe("0.5"); }); }); diff --git a/vite.config.ts b/vite.config.ts index a6e5a4d..2f253b2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ author: "slaid098", match: ["*://*/*"], grant: ["GM_xmlhttpRequest", "GM_getValue", "GM_setValue", "GM_registerMenuCommand"], - connect: ["api.groq.com"], + connect: ["*"], "run-at": "document-idle", icon: "https://raw.githubusercontent.com/slaid098/opencode-voice-dictation/main/assets/icon.png", updateURL: