pi-web-voice 0.1.1 → 0.1.3

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.
@@ -21,7 +21,16 @@ const hook = path.join(__dirname, "..", "hook.cjs");
21
21
  // `pi-web-voice doctor [file.wav]` checks the speech backend and exits.
22
22
  if (args[0] === "doctor") {
23
23
  const { doctor } = require("../lib/doctor.cjs");
24
- doctor(args.slice(1)).then((code) => process.exit(code));
24
+ doctor(args.slice(1)).then((code) => {
25
+ // Setting the code rather than calling process.exit lets the HTTP
26
+ // connection finish closing. Forcing an exit mid-teardown trips a libuv
27
+ // assertion on Windows: !(handle->flags & UV_HANDLE_CLOSING).
28
+ process.exitCode = code;
29
+ // Keep-alive sockets can still hold the loop open for a few seconds after
30
+ // the answer is in hand. This timer does not itself keep the process
31
+ // alive, and by the time it fires nothing is mid-close.
32
+ setTimeout(() => process.exit(code), 750).unref();
33
+ });
25
34
  return;
26
35
  }
27
36
 
package/lib/doctor.cjs CHANGED
@@ -68,6 +68,9 @@ function describe(config) {
68
68
 
69
69
  /** Turns provider failures into the thing that is actually wrong. */
70
70
  function diagnose(message) {
71
+ if (/cannot go in an HTTP header|ByteString/i.test(message)) {
72
+ return "the key in ~/.pi/agent/voice.env is not a real key yet";
73
+ }
71
74
  if (/\b401\b|Unauthorized|Access denied/i.test(message)) {
72
75
  return "the key is wrong, or it belongs to a different resource";
73
76
  }
package/lib/providers.cjs CHANGED
@@ -27,6 +27,22 @@ function audioBlob(audio) {
27
27
  return new Blob([audio], { type: "audio/wav" });
28
28
  }
29
29
 
30
+ /**
31
+ * Credentials travel in HTTP headers, which only carry bytes. A key that is
32
+ * still the placeholder, or that picked up a stray quote or newline, would
33
+ * otherwise surface as "Cannot convert argument to a ByteString", which says
34
+ * nothing about what to go and fix.
35
+ */
36
+ function checkKey(key, variable) {
37
+ if (!key) throw new Error(`${variable} is not set in ~/.pi/agent/voice.env`);
38
+ if (!/^[\x21-\x7e]+$/.test(key)) {
39
+ throw new Error(
40
+ `${variable} contains characters that cannot go in an HTTP header — ` +
41
+ `is the placeholder still in ~/.pi/agent/voice.env?`,
42
+ );
43
+ }
44
+ }
45
+
30
46
  /**
31
47
  * Azure AI Speech — fast transcription, including MAI-Transcribe-2.
32
48
  * Phrase list gives real decode-time keyword biasing, and leaving `locales`
@@ -34,7 +50,8 @@ function audioBlob(audio) {
34
50
  */
35
51
  async function azureSpeech(audio, config, terms) {
36
52
  const { endpoint, key, model, apiVersion, style } = config.azureSpeech;
37
- if (!endpoint || !key) throw new Error("azure-speech needs AZURE_SPEECH_ENDPOINT and AZURE_SPEECH_KEY");
53
+ if (!endpoint) throw new Error("azure-speech needs AZURE_SPEECH_ENDPOINT");
54
+ checkKey(key, "AZURE_SPEECH_KEY");
38
55
 
39
56
  const url = `${endpoint}/speechtotext/transcriptions:transcribe?api-version=${encodeURIComponent(apiVersion)}`;
40
57
 
@@ -102,7 +119,8 @@ function vocabularyPrompt(terms) {
102
119
  */
103
120
  async function azureOpenAI(audio, config, terms, languages) {
104
121
  const { endpoint, key, deployment, apiVersion } = config.azureOpenAI;
105
- if (!endpoint || !key) throw new Error("azure-openai needs AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY");
122
+ if (!endpoint) throw new Error("azure-openai needs AZURE_OPENAI_ENDPOINT");
123
+ checkKey(key, "AZURE_OPENAI_API_KEY");
106
124
 
107
125
  const explicit = /\/audio\/transcriptions/.test(endpoint);
108
126
  const url = explicit
@@ -144,6 +162,7 @@ async function azureOpenAI(audio, config, terms, languages) {
144
162
  async function openAICompatible(audio, config, terms) {
145
163
  const { baseUrl, key, model } = config.openai;
146
164
  if (!baseUrl) throw new Error("openai needs PI_VOICE_OPENAI_BASE_URL");
165
+ if (key) checkKey(key, "OPENAI_API_KEY");
147
166
 
148
167
  const form = new FormData();
149
168
  form.append("file", audioBlob(audio), "clip.wav");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-voice",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Voice input for pi-web. A NODE_OPTIONS hook that injects a microphone button into the chat composer and transcribes speech with Azure AI Speech, Azure OpenAI, or any OpenAI-compatible endpoint. No fork, no patching, no rebuild.",
5
5
  "keywords": [
6
6
  "pi",