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 +21 -0
- package/README.md +192 -2
- package/api.d.ts +60 -0
- package/api.mjs +217 -0
- package/atomicFile.d.ts +14 -0
- package/atomicFile.mjs +53 -0
- package/client/client.js +1 -0
- package/constants.d.ts +46 -0
- package/constants.mjs +75 -0
- package/contracts.d.ts +51 -0
- package/cordis.patch.yml +7 -0
- package/declaration.d.ts +4 -0
- package/fence.d.ts +100 -0
- package/fence.mjs +224 -0
- package/groq.d.ts +85 -0
- package/groq.mjs +176 -0
- package/index.d.ts +39 -0
- package/index.mjs +73 -0
- package/locale/en.json +5 -0
- package/package.json +65 -3
- package/polish.d.ts +40 -0
- package/polish.mjs +96 -0
- package/provider.d.ts +97 -0
- package/provider.mjs +160 -0
- package/settings.d.ts +89 -0
- package/settings.mjs +192 -0
- package/uninstall.d.ts +56 -0
- package/uninstall.mjs +124 -0
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
|
-
#
|
|
1
|
+
# dsh-groq-speech
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|
package/atomicFile.d.ts
ADDED
|
@@ -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 };
|
package/client/client.js
ADDED
|
@@ -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>;
|