dsh-groq-speech 0.0.0-stage → 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike Lysikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,193 @@
1
- # Temporary Holding Version
1
+ # dsh-groq-speech
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Groq Whisper speech recognition for DSH voice input: a cloud `groq-whisper`
4
+ provider in the microphone picker, optional LLM polishing of the transcript,
5
+ and a settings page that reuses the shared `GROQ_API_KEY` credential from
6
+ **Settings → Models**.
7
+
8
+ ## What it does
9
+
10
+ - Registers the `groq-whisper` provider with the official DSH voice input.
11
+ - Uploads the recording (16 kHz mono PCM16 WAV) to Groq:
12
+ `POST https://api.groq.com/openai/v1/audio/transcriptions`.
13
+ - Optionally polishes the transcript through any model configured in DSH
14
+ (**Settings → Models**) to fix punctuation and obvious recognition errors.
15
+ - Uses the shared Groq key from **Settings → Models** and never sends it to the
16
+ browser.
17
+
18
+ ## Requirements
19
+
20
+ - DSH `0.2.1-rc.2` or newer, Node.js 22.
21
+ - The official voice-input bundle
22
+ `@deepseek-ai/dsh-experimental-voice-input-bundle` enabled and listed in
23
+ `dsh.profile.bundles` for the `web` profile. It provides `ctx.speechToText`
24
+ and the microphone button; without it the plugin loads but the provider is
25
+ not offered.
26
+ - A Groq account and API key: <https://console.groq.com/keys>.
27
+
28
+ ## Installation
29
+
30
+ The publishable package is generated into `release/` by `npm run build`. Install
31
+ that directory (the repository root only contains sources and dev tooling):
32
+
33
+ ```sh
34
+ npm install
35
+ npm run build
36
+ dsh plugin --profile web add file:/site/dsh/dsh-groq-speech/release
37
+ ```
38
+
39
+ From npm:
40
+
41
+ ```sh
42
+ dsh plugin --profile web add dsh-groq-speech
43
+ ```
44
+
45
+ From git (build `release/` before installing, or publish from CI):
46
+
47
+ ```sh
48
+ npm install && npm run build
49
+ dsh plugin --profile web add file:/path/to/dsh-groq-speech/release
50
+ ```
51
+
52
+ Reload the Web GUI after installing. If the **Settings → Groq Speech** section
53
+ or the microphone provider is missing, restart `dsh web`.
54
+
55
+ ## Configuring the key
56
+
57
+ 1. Open **Settings → Models**.
58
+ 2. Add or select the **Groq** provider and enter its API key.
59
+ 3. Groq Speech uses the same `GROQ_API_KEY` credential automatically; there is
60
+ no separate key field in the plugin settings.
61
+ 4. On **Settings → Groq Speech**, the **Settings → Models** link opens that
62
+ section, and the key row shows whether the credential is configured.
63
+
64
+ The plugin stores no copy of the key and cannot set or clear it. The key from
65
+ Models is used both for Whisper transcription and for Groq models.
66
+
67
+ ## Recognition settings
68
+
69
+ - **Whisper model**: `whisper-large-v3-turbo` (default, fast),
70
+ `whisper-large-v3` (more accurate), or `distil-whisper-large-v3-en`
71
+ (English only, fastest).
72
+ - **Language hint**: `auto` (detect automatically) or one of the offered
73
+ languages.
74
+ - Model, language and polish changes are saved automatically.
75
+
76
+ ## Polishing
77
+
78
+ Polishing runs when a model is selected in **Polish model**. Choose
79
+ **No polishing** to keep the raw transcript. The list is built from the models
80
+ exposed by the DSH agent, so Groq appears automatically once it is configured in
81
+ **Settings → Models**. Polishing goes through the DSH model route and does not
82
+ use the Groq Speech key. A custom prompt can be provided; an empty field uses the
83
+ built-in default.
84
+
85
+ If the model is unavailable, the request fails, a timeout fires, or the stream
86
+ breaks, the provider returns the **raw transcript**. The user's words are never
87
+ lost.
88
+
89
+ ## Security
90
+
91
+ - The Groq key lives in **DSH credentials** (reference `GROQ_API_KEY`) and is
92
+ configured in **Settings → Models**; the plugin settings file never contains
93
+ it.
94
+ - Settings are stored at `$DSH_HOME/dsh-groq-speech/groq-speech.json` (or
95
+ `~/.dsh/dsh-groq-speech/groq-speech.json`). A 0.1.0 file at
96
+ `$DSH_HOME/groq-speech.json` is read as a one-time fallback and is never
97
+ modified.
98
+ - The browser receives only `{ configured, source, writable }` plus the
99
+ non-secret settings. The key, its prefix and its length are never returned.
100
+ - The plugin has no API actions to set or clear the key: the Groq Speech page
101
+ shows a **Configured** / **Not configured** badge and links to Models.
102
+ - The `GROQ_API_KEY` environment variable is a read-only fallback; a stored key
103
+ takes priority.
104
+ - The settings file is written atomically (temporary file + rename) with mode
105
+ `0600`, and every browser API request is fenced to loopback / trusted hosts,
106
+ same-origin and a live DSH browser session (when the `connection` service is
107
+ available).
108
+ - Audio is sent to Groq (`api.groq.com`). With polishing enabled, the transcript
109
+ is sent to the selected DSH provider. No other network requests are made.
110
+ - Groq errors are surfaced as fixed messages; response bodies and keys are never
111
+ included.
112
+ - Removing the bundle from the profile manifest lets the host delete the plugin
113
+ data directory after a short confirmation window; disabling or reloading the
114
+ plugin never deletes settings.
115
+
116
+ ## The provider in the UI
117
+
118
+ The voice-input microphone picker shows **Groq Whisper** marked `cloud`. There is
119
+ no preparation (model download) step; the provider is ready immediately.
120
+
121
+ ## Troubleshooting
122
+
123
+ | Symptom | What to do |
124
+ |---|---|
125
+ | `Groq API key is not configured` | Add a key for the Groq provider in Settings → Models. |
126
+ | `Groq rejected the API key` (401/403) | The key was revoked or copied incorrectly. Create a new one at console.groq.com/keys and update it in Settings → Models. |
127
+ | `Groq rate limit reached` (429) | The Groq quota is exhausted. Wait or check the plan and limits in the Groq console. |
128
+ | `Groq is unavailable` (5xx) | Groq-side failure; retry later. |
129
+ | `Could not reach Groq` | No network, DNS failure, or a proxy blocking `api.groq.com`. |
130
+ | Provider missing from the microphone picker | Enable the official voice-input bundle, make sure the plugin is added to the `web` profile, and restart DSH. |
131
+ | No Settings → Groq Speech section | Reload the page with the cache cleared; if that does not help, restart DSH. |
132
+ | Polishing does not run | Select a provider and model under Polish; settings save automatically. Any failure keeps the raw text. |
133
+
134
+ ## Tests
135
+
136
+ ```sh
137
+ npm test
138
+ ```
139
+
140
+ The suite is fully offline: no real keys are needed, Groq is never contacted,
141
+ and a local `http` server imitates the API. Tests run with Jest and ts-jest; the
142
+ `@deepseek-ai/dsh-llm` peer is mapped to a tiny local fixture. The tests do not
143
+ load DSH.
144
+
145
+ ## Build
146
+
147
+ ```sh
148
+ npm run build # theme → client bundle → release/
149
+ npm run build:client # only client/client.js
150
+ npm test # Jest
151
+ npm run lint # ESLint over src/
152
+ ```
153
+
154
+ `npm run build` writes:
155
+
156
+ - `client/client.js` — the browser half loaded by the shell's ModuleLoader;
157
+ - `release/` — the publishable package (`index.mjs`, host modules, types,
158
+ `cordis.patch.yml` and the client bundle).
159
+
160
+ ## Layout
161
+
162
+ ```
163
+ dsh-groq-speech/
164
+ ├── package.json plugin manifest and build scripts
165
+ ├── cordis.patch.yml adds the plugin to the web profile
166
+ ├── theme.scss settings-section styles (inlined at build time)
167
+ ├── buildEnv.ts compiles theme.scss into .env.theme
168
+ ├── src/
169
+ │ ├── constants.ts shared constants
170
+ │ ├── contracts.ts host/UI type shapes
171
+ │ ├── atomicFile.ts atomic 0600 writes
172
+ │ ├── settings.ts settings read, validation and persistence
173
+ │ ├── fence.ts same-origin guard and JSON responses
174
+ │ ├── groq.ts Groq multipart request and error mapping
175
+ │ ├── polish.ts optional LLM polishing with fallback
176
+ │ ├── provider.ts the `groq-whisper` SpeechProvider
177
+ │ ├── api.ts browser JSON API
178
+ │ ├── uninstall.ts post-uninstall data cleanup
179
+ │ ├── index.ts host plugin entry
180
+ │ ├── client.ts ModuleLoader registration
181
+ │ ├── components/ Settings → Groq Speech React section
182
+ │ ├── ui/ small React controls
183
+ │ └── utils/ API client, styling and form helpers
184
+ ├── test/ offline Jest suites and helpers
185
+ ├── client/client.js generated browser bundle
186
+ ├── release/ generated publishable package
187
+ ├── README.md
188
+ └── LICENSE
189
+ ```
190
+
191
+ ## License
192
+
193
+ MIT. See [LICENSE](LICENSE).
package/api.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Browser JSON API for the Groq Speech settings section.
3
+ *
4
+ * Every request is fenced to loopback / trusted hosts and same-origin, and no
5
+ * response contains key material: `status` reports only presence, source, and
6
+ * writability from `ctx.credentials.describe()`. Key management is owned by
7
+ * Settings → Models; this API has no action that can set or clear a key.
8
+ */
9
+ import type { IncomingMessage, ServerResponse } from 'node:http';
10
+ /**
11
+ * A browser-facing failure with an HTTP status and stable code.
12
+ * Messages here are fixed strings; callers never pass secrets in.
13
+ */
14
+ export declare class ApiFailure extends Error {
15
+ readonly status: number;
16
+ readonly code: string;
17
+ constructor(status: number, code: string, message: string);
18
+ }
19
+ export type ApiHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
20
+ export interface ApiResult {
21
+ status: number;
22
+ body: unknown;
23
+ }
24
+ export interface DispatchOptions {
25
+ /** Environment used for the key fallback (tests). */
26
+ env?: NodeJS.ProcessEnv;
27
+ }
28
+ export interface HandlerOptions {
29
+ trustedHosts?: string[];
30
+ env?: NodeJS.ProcessEnv;
31
+ }
32
+ /**
33
+ * Execute one parsed API action.
34
+ *
35
+ * Returns an HTTP status plus the exact response envelope; throws
36
+ * {@link ApiFailure} for a client-visible failure.
37
+ *
38
+ * @param ctx host context.
39
+ * @param payload parsed request body carrying `action`.
40
+ * @param options environment override.
41
+ * @returns the response envelope.
42
+ */
43
+ export declare function dispatchAction(ctx: unknown, payload: unknown, options?: DispatchOptions): Promise<ApiResult>;
44
+ /**
45
+ * Handle one HTTP request against the browser API.
46
+ *
47
+ * @param ctx host context.
48
+ * @param req incoming request.
49
+ * @param res response.
50
+ * @param options trusted hosts and environment override.
51
+ */
52
+ export declare function handleApiRequest(ctx: unknown, req: IncomingMessage, res: ServerResponse, options?: HandlerOptions): Promise<void>;
53
+ /**
54
+ * Build the `webServer` prefix handler.
55
+ *
56
+ * @param ctx host context.
57
+ * @param options trusted hosts and environment override.
58
+ * @returns a prefix-route handler.
59
+ */
60
+ export declare function createApiHandler(ctx: unknown, options?: HandlerOptions): ApiHandler;
package/api.mjs ADDED
@@ -0,0 +1,217 @@
1
+ import { KEY_REF, MAX_BODY_BYTES } from "./constants.mjs";
2
+ import { PAYLOAD_TOO_LARGE, isLoopbackRequest, isTrustedApiRequest, readJsonBody, safeErrorMessage, writeError, writeJson } from "./fence.mjs";
3
+ import { describeGroqKey } from "./provider.mjs";
4
+ import { mergeSettings, normalizeSettings, readSettings, writeSettings } from "./settings.mjs";
5
+ // src/api.ts
6
+ /**
7
+ * A browser-facing failure with an HTTP status and stable code.
8
+ * Messages here are fixed strings; callers never pass secrets in.
9
+ */
10
+ var ApiFailure = class extends Error {
11
+ constructor(status, code, message) {
12
+ super(message);
13
+ this.name = "ApiFailure";
14
+ this.status = status;
15
+ this.code = code;
16
+ }
17
+ };
18
+ function isPlainObject(value) {
19
+ return value !== null && typeof value === "object" && !Array.isArray(value);
20
+ }
21
+ /**
22
+ * Resolve the connection service for browser-session verification.
23
+ *
24
+ * @param ctx plugin context.
25
+ * @returns the service, or undefined when this profile has none.
26
+ */
27
+ function resolveConnection(ctx) {
28
+ if (ctx === null || typeof ctx !== "object") return void 0;
29
+ const candidate = ctx;
30
+ let direct;
31
+ try {
32
+ direct = candidate.connection;
33
+ } catch {}
34
+ if (direct && typeof direct.requestRejection === "function") return direct;
35
+ if (typeof candidate.get === "function") try {
36
+ const service = candidate.get("connection");
37
+ if (service && typeof service.requestRejection === "function") return service;
38
+ } catch {}
39
+ }
40
+ /**
41
+ * Require a live DSH browser session (or API credential) for a request.
42
+ *
43
+ * @param ctx plugin context.
44
+ * @param req incoming request.
45
+ * @returns undefined when admitted, otherwise the HTTP status to return.
46
+ */
47
+ function sessionRejection(ctx, req) {
48
+ const connection = resolveConnection(ctx);
49
+ if (!connection || typeof connection.requestRejection !== "function") return 403;
50
+ let rejection;
51
+ try {
52
+ rejection = connection.requestRejection(req);
53
+ } catch {
54
+ return 403;
55
+ }
56
+ if (rejection === void 0) return void 0;
57
+ return rejection === 401 ? 401 : 403;
58
+ }
59
+ function ok(value) {
60
+ return {
61
+ status: 200,
62
+ body: {
63
+ ok: true,
64
+ value
65
+ }
66
+ };
67
+ }
68
+ /**
69
+ * Execute one parsed API action.
70
+ *
71
+ * Returns an HTTP status plus the exact response envelope; throws
72
+ * {@link ApiFailure} for a client-visible failure.
73
+ *
74
+ * @param ctx host context.
75
+ * @param payload parsed request body carrying `action`.
76
+ * @param options environment override.
77
+ * @returns the response envelope.
78
+ */
79
+ async function dispatchAction(ctx, payload, options = {}) {
80
+ const env = options.env || process.env;
81
+ const action = isPlainObject(payload) ? payload.action : void 0;
82
+ if (action === "status") {
83
+ const key = await describeGroqKey(ctx, env);
84
+ return ok({
85
+ configured: key.configured,
86
+ source: key.source,
87
+ writable: key.writable,
88
+ envVar: KEY_REF,
89
+ settings: readSettings()
90
+ });
91
+ }
92
+ if (action === "get-settings") return ok({ settings: readSettings() });
93
+ if (action === "set-settings") {
94
+ if (!isPlainObject(payload) || !isPlainObject(payload.patch)) throw new ApiFailure(400, "bad-request", "patch must be a plain object");
95
+ const settings = mergeSettings(readSettings(), payload.patch);
96
+ try {
97
+ writeSettings(settings);
98
+ } catch {
99
+ throw new ApiFailure(500, "settings-write-failed", "Could not save settings");
100
+ }
101
+ return ok({ settings: normalizeSettings(settings) });
102
+ }
103
+ if (action === "list-models") return ok(await listModels(ctx));
104
+ throw new ApiFailure(404, "unknown-action", "Unknown action");
105
+ }
106
+ /**
107
+ * Flatten the DSH model routes into provider/model pairs for the polish picker.
108
+ *
109
+ * One broken adapter must not hide every other route, so a failing provider is
110
+ * skipped while the rest of the list is still returned.
111
+ *
112
+ * @param ctx host context carrying `llm`.
113
+ * @returns the selectable routes, possibly empty.
114
+ */
115
+ async function listModels(ctx) {
116
+ const routes = [];
117
+ const llm = ctx !== null && typeof ctx === "object" ? ctx.llm : void 0;
118
+ if (llm === null || typeof llm !== "object") return routes;
119
+ const service = llm;
120
+ if (typeof service.listProviders !== "function" || typeof service.listModels !== "function") return routes;
121
+ let providers = [];
122
+ try {
123
+ providers = service.listProviders();
124
+ } catch {
125
+ providers = [];
126
+ }
127
+ for (const provider of Array.isArray(providers) ? providers : []) {
128
+ if (!isPlainObject(provider) || typeof provider.id !== "string") continue;
129
+ let models = [];
130
+ try {
131
+ models = await service.listModels(provider.id);
132
+ } catch {
133
+ continue;
134
+ }
135
+ for (const model of Array.isArray(models) ? models : []) {
136
+ if (!isPlainObject(model) || typeof model.id !== "string") continue;
137
+ routes.push({
138
+ provider: provider.id,
139
+ providerName: typeof provider.name === "string" ? provider.name : provider.id,
140
+ model: model.id,
141
+ modelName: typeof model.name === "string" ? model.name : model.id
142
+ });
143
+ }
144
+ }
145
+ return routes;
146
+ }
147
+ function contentTypeOf(req) {
148
+ const value = req.headers["content-type"];
149
+ return typeof value === "string" ? value.toLowerCase() : "";
150
+ }
151
+ /**
152
+ * Handle one HTTP request against the browser API.
153
+ *
154
+ * @param ctx host context.
155
+ * @param req incoming request.
156
+ * @param res response.
157
+ * @param options trusted hosts and environment override.
158
+ */
159
+ async function handleApiRequest(ctx, req, res, options = {}) {
160
+ try {
161
+ if (!isTrustedApiRequest(req, options.trustedHosts || [])) {
162
+ writeError(res, 403, "forbidden", "Forbidden");
163
+ return;
164
+ }
165
+ if (resolveConnection(ctx) !== void 0 || !isLoopbackRequest(req)) {
166
+ const rejection = sessionRejection(ctx, req);
167
+ if (rejection !== void 0) {
168
+ writeError(res, rejection, rejection === 401 ? "authentication-required" : "forbidden", rejection === 401 ? "Authentication required: open the DSH Web UI from its printed URL." : "Untrusted request");
169
+ return;
170
+ }
171
+ }
172
+ if (req.method !== "POST") {
173
+ writeError(res, 405, "method-error", "Method not allowed");
174
+ return;
175
+ }
176
+ if (!contentTypeOf(req).startsWith("application/json")) {
177
+ writeError(res, 415, "unsupported-media-type", "Content-Type must be application/json");
178
+ return;
179
+ }
180
+ const declared = Number(req.headers["content-length"]);
181
+ if (Number.isFinite(declared) && declared > 65536) {
182
+ res.setHeader("connection", "close");
183
+ writeError(res, 413, "payload-too-large", "Request body too large");
184
+ return;
185
+ }
186
+ const payload = await readJsonBody(req, MAX_BODY_BYTES);
187
+ if (payload === PAYLOAD_TOO_LARGE) {
188
+ res.setHeader("connection", "close");
189
+ writeError(res, 413, "payload-too-large", "Request body too large");
190
+ return;
191
+ }
192
+ if (!isPlainObject(payload) || typeof payload.action !== "string") {
193
+ writeError(res, 400, "bad-request", "Request body must carry a string action");
194
+ return;
195
+ }
196
+ const result = await dispatchAction(ctx, payload, options);
197
+ writeJson(res, result.status, result.body);
198
+ } catch (error) {
199
+ if (error instanceof ApiFailure) {
200
+ writeError(res, error.status, error.code, error.message);
201
+ return;
202
+ }
203
+ console.error("[dsh-groq-speech] API error:", safeErrorMessage(error instanceof Error ? error.message : void 0));
204
+ writeError(res, 500, "internal", "Internal error");
205
+ }
206
+ }
207
+ /**
208
+ * Build the `webServer` prefix handler.
209
+ *
210
+ * @param ctx host context.
211
+ * @param options trusted hosts and environment override.
212
+ * @returns a prefix-route handler.
213
+ */
214
+ function createApiHandler(ctx, options = {}) {
215
+ return (req, res) => handleApiRequest(ctx, req, res, options);
216
+ }
217
+ export { ApiFailure, createApiHandler, dispatchAction, handleApiRequest };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Atomic private-file writes shared by settings storage.
3
+ *
4
+ * Files are written to a sibling temp file, fsynced, renamed into place and
5
+ * tightened to 0600, so a crash never leaves a half-written secret and an
6
+ * existing destination cannot keep looser permissions.
7
+ */
8
+ /**
9
+ * Write a secret file atomically with mode 0600.
10
+ *
11
+ * @param file destination path.
12
+ * @param content file contents.
13
+ */
14
+ export declare function writeSecretFile(file: string, content: string): void;
package/atomicFile.mjs ADDED
@@ -0,0 +1,53 @@
1
+ import { dirname } from "node:path";
2
+ import { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { randomBytes } from "node:crypto";
4
+ // src/atomicFile.ts
5
+ /**
6
+ * Atomic private-file writes shared by settings storage.
7
+ *
8
+ * Files are written to a sibling temp file, fsynced, renamed into place and
9
+ * tightened to 0600, so a crash never leaves a half-written secret and an
10
+ * existing destination cannot keep looser permissions.
11
+ */
12
+ /**
13
+ * Write a secret file atomically with mode 0600.
14
+ *
15
+ * @param file destination path.
16
+ * @param content file contents.
17
+ */
18
+ function writeSecretFile(file, content) {
19
+ const directory = dirname(file);
20
+ mkdirSync(directory, {
21
+ recursive: true,
22
+ mode: 448
23
+ });
24
+ const temp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
25
+ let handle;
26
+ try {
27
+ handle = openSync(temp, "w", 384);
28
+ writeFileSync(handle, content, "utf8");
29
+ fsyncSync(handle);
30
+ closeSync(handle);
31
+ handle = void 0;
32
+ } catch (error) {
33
+ if (handle !== void 0) try {
34
+ closeSync(handle);
35
+ } catch {}
36
+ try {
37
+ unlinkSync(temp);
38
+ } catch {}
39
+ throw error;
40
+ }
41
+ try {
42
+ renameSync(temp, file);
43
+ } catch (error) {
44
+ try {
45
+ unlinkSync(temp);
46
+ } catch {}
47
+ throw error;
48
+ }
49
+ try {
50
+ chmodSync(file, 384);
51
+ } catch {}
52
+ }
53
+ export { writeSecretFile };
@@ -0,0 +1 @@
1
+ window.__ModuleLoader__.load({id:`dsh-groq-speech`,factory(e){var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i=e("react/jsx-runtime"),a=`whisper-large-v3-turbo`,o=[{value:`whisper-large-v3-turbo`,label:`whisper-large-v3-turbo (fast, default)`},{value:`whisper-large-v3`,label:`whisper-large-v3 (most accurate)`},{value:`distil-whisper-large-v3-en`,label:`distil-whisper-large-v3-en (English only, fastest)`}],s=[`auto`,`ru`,`en`,`uk`,`de`,`fr`,`es`,`it`,`pt`,`nl`,`pl`,`tr`,`zh`,`ja`,`ko`];Object.freeze({model:a,language:`auto`,polish:Object.freeze({provider:``,model:``,prompt:``})});function c({children:e,variant:t=`primary`,compact:n,disabled:r,onClick:a}){let o=[`gqs-button`,`gqs-`+t];return n&&o.push(`gqs-compact`),/* @__PURE__ */ (0,i.jsx)(`button`,{type:`button`,className:o.join(` `),disabled:r,onClick:a,children:e})}function ee({href:e=`#`,onClick:t,children:n}){return/* @__PURE__ */ (0,i.jsx)(`a`,{children:n,href:e,className:`gqs-link`,onClick:t})}function te({kind:e,text:t}){return/* @__PURE__ */ (0,i.jsx)(`p`,{className:`gqs-notice gqs-notice-`+e,role:e===`error`?`alert`:`status`,"aria-live":`polite`,style:{marginTop:`16px`},children:t})}function l({title:e,description:t,control:n,badge:r,prompt:a,htmlFor:o}){return/* @__PURE__ */ (0,i.jsxs)(`div`,{className:a?`gqs-row gqs-row-prompt`:`gqs-row`,children:[/* @__PURE__ */ (0,i.jsxs)(`div`,{className:`gqs-text`,children:[/* @__PURE__ */ (0,i.jsxs)(`div`,{className:`gqs-titleRow`,children:[o?/* @__PURE__ */ (0,i.jsx)(`label`,{className:`gqs-title`,htmlFor:o,children:e}):/* @__PURE__ */ (0,i.jsx)(`span`,{className:`gqs-title`,children:e}),r||null]}),t?/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-desc`,children:t}):null]}),a?n:/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-control`,children:n})]})}function u(){return/* @__PURE__ */ (0,i.jsx)(`svg`,{className:`gqs-chevron`,viewBox:`0 0 16 16`,width:14,height:14,"aria-hidden":`true`,children:/* @__PURE__ */ (0,i.jsx)(`path`,{d:`M4 6l4 4 4-4`,fill:`none`,stroke:`currentColor`,strokeWidth:1.5,strokeLinecap:`round`,strokeLinejoin:`round`})})}function d({id:e,value:t,options:n,disabled:r,onChange:a}){return/* @__PURE__ */ (0,i.jsxs)(`span`,{className:`gqs-selectWrap`,children:[/* @__PURE__ */ (0,i.jsx)(`select`,{id:e,className:`gqs-select`,value:t,disabled:r,onChange:e=>a(e.target.value),children:n.map(e=>/* @__PURE__ */ (0,i.jsx)(`option`,{value:e.value,children:e.label},e.value))}),u()]})}function ne({state:e,label:t,hint:n}){return/* @__PURE__ */ (0,i.jsxs)(`span`,{className:`gqs-badge gqs-badge-${e}`,title:n,"aria-label":t,children:[/* @__PURE__ */ (0,i.jsx)(`span`,{className:`gqs-dot`,"aria-hidden":`true`}),t]})}function re({id:e,value:t,placeholder:n,rows:r=4,maxLength:a,disabled:o,onChange:s}){return/* @__PURE__ */ (0,i.jsx)(`textarea`,{id:e,className:`gqs-textarea`,value:t,placeholder:n,rows:r,maxLength:a,disabled:o,spellCheck:!0,onChange:e=>s(e.target.value)})}async function f(e,t){let n=Object.assign({action:e},t||{}),r=await fetch(`/groq-speech/api`,{method:`POST`,headers:{"content-type":`application/json`},credentials:`same-origin`,body:JSON.stringify(n)}),i=null;try{i=await r.json()}catch{i=null}let a=typeof i==`object`&&i?i:null;if(a&&a.ok===!1){let e=a.error,t=e&&typeof e.message==`string`?e.message:`Request failed`;throw Error(t)}if(!r.ok)throw Error(`Groq Speech request failed (HTTP `+r.status+`).`);if(!a)throw Error(`Groq Speech returned an unreadable response.`);return Object.prototype.hasOwnProperty.call(a,`value`)?a.value:a}function p(e){if(typeof document>`u`||!document.head||document.querySelector(`style[data-plugin="${e}"]`)!==null)return;let t=document.createElement(`style`);t.dataset.plugin=e,t.textContent=`.gqs-section{color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;font-size:14px;line-height:22px;width:100%}.gqs-row{align-items:center;border-bottom:.5px solid var(--dsw-alias-border-l2);display:flex;gap:8px;padding:16px 0}.gqs-row:last-of-type{border-bottom:none}.gqs-text{flex:1;flex-direction:column;gap:4px;padding-right:48px}.gqs-text,.gqs-titleRow{display:flex;min-width:0}.gqs-titleRow{align-items:center;flex-wrap:wrap;gap:8px}.gqs-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.gqs-badge{align-items:center;border:.5px solid transparent;border-radius:999px;display:inline-flex;flex:none;font-size:11px;font-weight:500;gap:5px;line-height:17px;padding:1px 8px;white-space:nowrap}.gqs-badge-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.gqs-badge-off,.gqs-badge-on{background:var(--dsw-alias-bg-module-platform)}.gqs-badge-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.gqs-dot{background:currentColor;border-radius:50%;flex:none;height:6px;width:6px}.gqs-desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px;white-space:pre-line}.gqs-desc code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px}.gqs-control{align-items:center;display:flex;flex:none;gap:8px;min-width:0}.gqs-link{color:var(--dsw-alias-link);cursor:pointer;font:inherit;text-decoration:none;white-space:nowrap}.gqs-link:hover{text-decoration:underline}.gqs-selectWrap{align-items:center;display:inline-flex;min-width:0;position:relative}.gqs-select{appearance:none;-webkit-appearance:none;background:var(--dsw-alias-bg-module-platform);border:none;border-radius:var(--dsw-radius-md);box-sizing:border-box;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;font-size:14px;height:36px;line-height:22px;max-width:320px;padding:0 36px 0 14px}.gqs-select:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.gqs-select:disabled{cursor:default;opacity:.5}.gqs-select:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}.gqs-chevron{color:var(--dsw-alias-label-caption);flex:none;pointer-events:none;position:absolute;right:12px}.gqs-textarea{background:var(--dsw-alias-bg-module-platform);border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);box-sizing:border-box;color:var(--dsw-alias-label-primary);font:inherit;font-size:14px;line-height:22px;min-height:96px;outline:none;padding:8px 12px;resize:vertical;width:100%}.gqs-textarea::placeholder{color:var(--dsw-alias-label-caption)}.gqs-textarea:focus{border-color:var(--dsw-alias-state-business-primary);box-shadow:inset 0 0 0 .5px var(--dsw-alias-state-business-primary)}.gqs-button{align-items:center;border:none;border-radius:var(--dsw-radius-md);box-sizing:border-box;cursor:pointer;display:inline-flex;font:inherit;font-size:14px;gap:6px;height:36px;justify-content:center;line-height:22px;padding:0 14px;white-space:nowrap}.gqs-button:disabled{cursor:default;opacity:.4}.gqs-button:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}.gqs-primary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font-weight:500}.gqs-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.gqs-secondary{background:transparent;border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-primary)}.gqs-secondary:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.gqs-compact{font-size:12px;height:28px;line-height:18px;padding:0 10px}.gqs-row-prompt{align-items:stretch;flex-direction:column;gap:10px}.gqs-row-prompt .gqs-text{padding-right:0}.gqs-footer{align-items:center;display:flex;gap:16px;padding:16px 0 0}.gqs-footer .gqs-notice{flex:1;min-width:0}.gqs-footer .gqs-button{margin-left:auto}.gqs-notice{font-size:12px;line-height:18px;margin:0}.gqs-notice-success{color:var(--dsw-alias-state-success-primary)}.gqs-notice-error{color:var(--dsw-alias-state-error-primary)}.gqs-loading{border-bottom:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding:16px 0}@media (max-width:640px){.gqs-row{flex-wrap:wrap}.gqs-text{flex-basis:100%;padding-right:0}.gqs-control{align-items:stretch;width:100%}.gqs-select,.gqs-selectWrap{max-width:none;width:100%}}`,document.head.appendChild(t)}function m(e,t){return e instanceof Error&&e.message.trim()!==``?e.message.trim():typeof e==`string`&&e.trim()!==``?e.trim():t}function h(e){let t=[];for(let n of Array.isArray(e)?e:[]){if(typeof n!=`object`||!n)continue;let e=n,r=typeof e.provider==`string`?e.provider:``,i=typeof e.model==`string`?e.model:``;r&&i&&t.push({provider:r,providerName:typeof e.providerName==`string`&&e.providerName?e.providerName:r,model:i,modelName:typeof e.modelName==`string`&&e.modelName?e.modelName:i})}return t}function g(e,t){return!e||!t?``:e+`\0`+t}function ie(e){let t=typeof e==`string`?e:``,n=t.indexOf(`\0`);return n<0?{provider:``,model:``}:{provider:t.slice(0,n),model:t.slice(n+1)}}function _(e){return e===`auto`?`Auto-detect`:String(e).toUpperCase()}function v(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function y(e){return e===`stored`||e===`env`?e:null}function ae(e){let t=v(e)?e:{},n=v(t.settings)?b(t.settings):null;return{configured:t.configured===!0,source:y(t.source),writable:t.writable!==!1,envVar:typeof t.envVar==`string`&&t.envVar?t.envVar:`GROQ_API_KEY`,settings:n}}function b(e){let t=v(e)?e:{},n=v(t.polish)?t.polish:{};return{model:typeof t.model==`string`&&t.model?t.model:a,language:typeof t.language==`string`&&t.language?t.language:`auto`,polish:{provider:typeof n.provider==`string`?n.provider:``,model:typeof n.model==`string`?n.model:``,prompt:typeof n.prompt==`string`?n.prompt:``}}}function x(e){let t=b(e);return JSON.stringify({model:t.model,language:t.language,polish:{provider:t.polish.provider,model:t.polish.model,prompt:t.polish.prompt}})}let S={model:`groq-speech-model`,language:`groq-speech-language`,polishModel:`groq-speech-polish-model`,prompt:`groq-speech-polish-prompt`};function C({openModels:e}){let[t,n]=(0,r.useState)(`loading`),[a,u]=(0,r.useState)(``),[p,v]=(0,r.useState)(null),[y,C]=(0,r.useState)(null),[w,T]=(0,r.useState)([]),[E,D]=(0,r.useState)(``),[O,k]=(0,r.useState)(!1),[A,j]=(0,r.useState)(!1),[M,N]=(0,r.useState)(null),P=(0,r.useRef)(null),F=(0,r.useRef)(!1),I=(0,r.useRef)(``),L=(0,r.useRef)(null),R=(0,r.useRef)(null),z=(0,r.useRef)(``),B=(0,r.useRef)(0),V=(0,r.useRef)(!0);function oe(e){R.current=e,z.current=x(e),C(e)}function H(e){B.current+=1,R.current=e,C(e),se(e)}function U(e){let t=R.current;t!==null&&H({...t,...e})}function W(e){let t=R.current;t!==null&&H({...t,polish:{...t.polish,...e}})}function se(e){P.current!==null&&clearTimeout(P.current),P.current=setTimeout(()=>{P.current=null,G(e)},500)}async function G(e){if(e===null)return;let t=x(e);if(F.current){t!==I.current&&(L.current=e);return}if(t===z.current)return;F.current=!0,I.current=t;let n=B.current;V.current&&j(!0);try{let t=await f(`set-settings`,{patch:{model:e.model,language:e.language,polish:{provider:e.polish.provider,model:e.polish.model,prompt:e.polish.prompt}}}),r=b(typeof t==`object`&&t&&t.settings?t.settings:e);z.current=x(r),V.current&&n===B.current&&(R.current=r,C(r),N({kind:`saved`,text:`Saved`}))}catch(e){V.current&&N({kind:`error`,text:m(e,`Could not save settings.`)})}finally{F.current=!1,I.current=``,V.current&&j(!1);let e=L.current;L.current=null,e!==null&&G(e)}}function ce(){if(P.current===null)return;clearTimeout(P.current),P.current=null;let e=R.current;e!==null&&x(e)!==z.current&&G(e)}async function K(){n(`loading`),u(``),D(``),N(null);let[e,t,r]=await Promise.allSettled([f(`status`),f(`get-settings`),f(`list-models`)]);if(!V.current)return;if(e.status!==`fulfilled`){n(`error`),u(m(e.reason,`Could not load Groq Speech status.`));return}let i=ae(e.value),a=b((t.status===`fulfilled`&&t.value?t.value.settings:void 0)??i.settings??null);v(i),oe(a),r.status===`fulfilled`?(T(h(r.value)),D(``)):(T([]),D(m(r.reason,`Could not load the model list.`))),n(`ready`)}async function le(){k(!0),D(``);try{T(h(await f(`list-models`)))}catch(e){T([]),D(m(e,`Could not load the model list.`))}finally{k(!1)}}(0,r.useEffect)(()=>(V.current=!0,K(),()=>{V.current=!1,ce()}),[]);let q=p!==null&&p.configured,J=p===null?`GROQ_API_KEY`:p.envVar;function ue(){return q?`Configured through the shared `+J+` credential. Groq Speech uses the same key as the Groq provider; manage it only in Settings → Models.`:`No Groq API key is configured. Add it in Settings → Models; Groq Speech will use the same `+J+` credential automatically.`}function de(t){t.preventDefault(),typeof e==`function`&&e()||N({kind:`error`,text:`Open Settings → Models from the settings sidebar to manage the key.`})}if(t===`loading`)return/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-section`,children:/* @__PURE__ */ (0,i.jsx)(`p`,{className:`gqs-loading`,role:`status`,children:`Loading Groq Speech settings…`})});if(t===`error`)return/* @__PURE__ */ (0,i.jsxs)(`div`,{className:`gqs-section`,children:[/* @__PURE__ */ (0,i.jsx)(`p`,{className:`gqs-notice gqs-notice-error`,role:`alert`,children:a||`Could not load Groq Speech settings.`}),/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-footer`,children:/* @__PURE__ */ (0,i.jsx)(c,{onClick:()=>{K()},children:`Retry`})})]});if(y===null)return null;let Y=o.map(e=>({value:e.value,label:e.label}));Y.some(e=>e.value===y.model)||Y.push({value:y.model,label:y.model});let X=s.map(e=>({value:e,label:_(e)}));X.some(e=>e.value===y.language)||X.push({value:y.language,label:_(y.language)});let Z=g(y.polish.provider,y.polish.model),Q=[{value:``,label:`No polishing`}];for(let e of w)Q.push({value:g(e.provider,e.model),label:e.providerName+` — `+e.modelName});Z&&!Q.some(e=>e.value===Z)&&Q.push({value:Z,label:y.polish.provider+` — `+y.polish.model});let fe=O||w.length===0&&!Z,pe=A?`Saving…`:M===null?``:M.text,$=M!==null&&M.kind===`error`?`error`:`success`;return/* @__PURE__ */ (0,i.jsxs)(`div`,{className:`gqs-section`,children:[/* @__PURE__ */ (0,i.jsx)(l,{title:`Groq API key`,badge:/* @__PURE__ */ (0,i.jsx)(ne,{state:q?`on`:`off`,label:q?`Configured`:`Not configured`,hint:q?`A shared `+J+` credential is available to Groq Speech.`:`No shared `+J+` credential is available yet. Add it in Settings → Models.`}),description:ue(),control:/* @__PURE__ */ (0,i.jsx)(ee,{onClick:de,children:`Settings → Models`})},`key`),/* @__PURE__ */ (0,i.jsx)(l,{title:`Whisper model`,description:`Model used to transcribe speech. Turbo is the fastest default.`,htmlFor:S.model,control:/* @__PURE__ */ (0,i.jsx)(d,{id:S.model,value:y.model,options:Y,onChange:e=>U({model:e})})},`model`),/* @__PURE__ */ (0,i.jsx)(l,{title:`Language hint`,description:`Auto-detect lets Whisper guess the language; pin one to improve accuracy on short recordings.`,htmlFor:S.language,control:/* @__PURE__ */ (0,i.jsx)(d,{id:S.language,value:y.language,options:X,onChange:e=>U({language:e})})},`language`),/* @__PURE__ */ (0,i.jsx)(l,{title:`Polish model`,description:`Choose any model exposed by DSH, including Groq. No polishing keeps the raw transcript; any failure also falls back to it.`,htmlFor:S.polishModel,control:/* @__PURE__ */ (0,i.jsx)(d,{id:S.polishModel,value:Z,options:Q,disabled:fe,onChange:e=>{let t=ie(e);W({provider:t.provider,model:t.model})}})},`polish-model`),E?/* @__PURE__ */ (0,i.jsxs)(`div`,{className:`gqs-row`,children:[/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-text`,children:/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-desc`,children:E})}),/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-control`,children:/* @__PURE__ */ (0,i.jsx)(c,{variant:`secondary`,compact:!0,disabled:O,onClick:()=>{le()},children:O?`Loading…`:`Retry`})})]},`models-error`):null,w.length===0&&!E?/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-row`,children:/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-text`,children:/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-desc`,children:`No polish models were found. Configure a provider in DSH Settings → Models, then retry.`})})},`models-empty`):null,/* @__PURE__ */ (0,i.jsx)(l,{title:`Polish prompt (optional)`,description:`Used only when a Polish model is selected. Leave empty to use the built-in default prompt.`,prompt:!0,htmlFor:S.prompt,control:/* @__PURE__ */ (0,i.jsx)(re,{id:S.prompt,value:y.polish.prompt,placeholder:`You are an editor of speech-to-text transcripts. Fix punctuation, capitalization, and obvious recognition errors, remove filler words and repetitions. Do not add new information, do not change the meaning, and do not translate the text. Return only the corrected text without explanations.`,maxLength:4e3,onChange:e=>W({prompt:e})})},`polish-prompt`),A||M?/* @__PURE__ */ (0,i.jsx)(`div`,{className:`gqs-footer`,children:/* @__PURE__ */ (0,i.jsx)(`p`,{className:`gqs-notice gqs-notice-`+$,role:!A&&$===`error`?`alert`:`status`,"aria-live":`polite`,children:pe})}):null,M!==null&&M.kind===`error`&&!A?/* @__PURE__ */ (0,i.jsx)(te,{kind:`error`,text:M.text}):null]})}let w={name:`settings.section`,id:`groq-speech`,order:30,label:`Groq Speech`},T=[`slots`];function E(e){if(typeof document>`u`)return!1;let t=document.querySelector(`[data-shortcut-modal="settings"]`);if(t===null)return!1;let n=-1;try{n=e.slots.entries(`settings.section`).map(e=>({id:e&&e.options?e.options.id:``,order:e&&e.options&&typeof e.options.order==`number`?e.options.order:0})).sort((e,t)=>e.order-t.order).findIndex(e=>e.id===`models`)}catch{n=-1}let r=t.querySelectorAll(`nav > div:last-child button`);return n<0||!r[n]?!1:(r[n].click(),!0)}function D(e){p(`dsh-groq-speech`);let t=e.slots;t.inject(`settings.section`,()=>t.register({...w,inject:()=>({openModels:()=>E(e)})},C))}return n.apply=D,n.inject=T,t.exports}});
package/constants.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ /** Shared host-side constants for dsh-groq-speech. */
2
+ import type { GroqSpeechSettings, WhisperModel } from './contracts';
3
+ /** Provider id registered into ctx.speechToText. */
4
+ export declare const PROVIDER_ID = "groq-whisper";
5
+ /** Provider display name shown in the official voice-input picker. */
6
+ export declare const PROVIDER_NAME = "Groq Whisper";
7
+ /** Same-origin browser API prefix. */
8
+ export declare const API_PREFIX = "/groq-speech/api";
9
+ /** Credential reference used for the Groq key (stored value outranks env). */
10
+ export declare const KEY_REF = "GROQ_API_KEY";
11
+ /** Groq's OpenAI-compatible base URL; tests override via GROQ_SPEECH_API_BASE. */
12
+ export declare const DEFAULT_API_BASE = "https://api.groq.com/openai/v1";
13
+ /** This package's name as the profile manifest declares it. */
14
+ export declare const PACKAGE_NAME = "dsh-groq-speech";
15
+ /** Plugin data directory name under the DSH home. */
16
+ export declare const PLUGIN_DIR_NAME = "dsh-groq-speech";
17
+ /** Settings file name inside the plugin directory. */
18
+ export declare const SETTINGS_FILENAME = "groq-speech.json";
19
+ /**
20
+ * Settings file name used by 0.1.0 directly inside `$DSH_HOME`. It is read
21
+ * when the plugin-directory file does not exist yet and is never written back.
22
+ */
23
+ export declare const LEGACY_SETTINGS_FILENAME = "groq-speech.json";
24
+ /** Selectable transcription models. */
25
+ export declare const MODELS: readonly WhisperModel[];
26
+ /** Default transcription model (fast + cheap on the free tier). */
27
+ export declare const DEFAULT_MODEL: WhisperModel;
28
+ /** Picker labels for {@link MODELS}, shared by the settings UI. */
29
+ export declare const MODEL_OPTIONS: ReadonlyArray<{
30
+ value: WhisperModel;
31
+ label: string;
32
+ }>;
33
+ /** Language hints advertised to the picker; `auto` lets Whisper detect. */
34
+ export declare const LANGUAGES: readonly string[];
35
+ /** Default polish prompt: fix the transcript, never rewrite its meaning. */
36
+ export declare const DEFAULT_POLISH_PROMPT: string;
37
+ /** Maximum length of a stored custom polish prompt. */
38
+ export declare const MAX_PROMPT_CHARS = 4000;
39
+ /** Maximum accepted size of one browser API request body. */
40
+ export declare const MAX_BODY_BYTES: number;
41
+ /** Per-request Groq timeout. */
42
+ export declare const GROQ_TIMEOUT_MS = 60000;
43
+ /** Per-request polish timeout. */
44
+ export declare const POLISH_TIMEOUT_MS = 45000;
45
+ /** Default empty settings document. */
46
+ export declare const DEFAULT_SETTINGS: Readonly<GroqSpeechSettings>;