friend-assistant 0.1.43 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/BUILD.json +2 -2
- package/dist/friend.mjs +129 -20
- package/dist/web/assets/{index-CXlA46SS.js → index-D6qxQkPK.js} +4 -4
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ Connectors (Gmail, Chrome control, any MCP service) are managed inside the app,
|
|
|
138
138
|
| `ollama.url`, `ollama.model`, `ollama.vision`, `ollama.contextTokens` | The local model: server address, model name, whether it can see pictures, and how much it remembers at once. |
|
|
139
139
|
| `openai.url`, `openai.model`, `openai.apiKey`, `openai.vision` | An OpenAI-compatible server: its address (ending in `/v1`), the model name it knows, its API key (or empty), and whether the model can see pictures. For OmniRoute (https://github.com/diegosouzapw/OmniRoute) run `npm install -g omniroute` and `omniroute`, then `friend config set provider openai` (the defaults are OmniRoute's: `http://localhost:20128/v1` and model `auto`). Also works with LM Studio, LiteLLM, vLLM and OpenRouter. |
|
|
140
140
|
| `voice.hindiVoice`, `voice.marathiVoice` | The voices that read Hindi and Marathi (Devanagari) text: `hi_IN-priyamvada-medium`, `hi_IN-pratham-medium`, `hi_IN-rohan-medium`, `mr_IN-google-medium`. Empty makes the main voice read that language. See "Hindi, Marathi and English together". |
|
|
141
|
-
| `voice.enabled`, `voice.sttModel`, `voice.ttsVoice`, `voice.language`, `voice.speakTyped`, `voice.accept`, `voice.challenge` | Speech on or off, the listening model, the speaking voice, a forced language, speaking typed replies, and the two thresholds for recognising your voice. |
|
|
141
|
+
| `voice.enabled`, `voice.sttModel`, `voice.ttsVoice`, `voice.language`, `voice.speakTyped`, `voice.duck`, `voice.duckPercent`, `voice.accept`, `voice.challenge` | Speech on or off, the listening model, the speaking voice, a forced language, speaking typed replies, turning music and video down while you talk and while Friend speaks (and how far, in percent), and the two thresholds for recognising your voice. |
|
|
142
142
|
| `bind`, `port` | Where Friend listens (easier: `friend lan`). |
|
|
143
143
|
| `https` | `true` serves a secure `https://` address with a certificate Friend makes for itself, so a phone may use the microphone (easier: `friend lan https on`). |
|
|
144
144
|
| `publicUrl`, `allowedOrigins`, `allowedHosts`, `trustProxy` | For running behind your own domain or reverse proxy (see "Using your own domain later"). |
|
package/dist/BUILD.json
CHANGED
package/dist/friend.mjs
CHANGED
|
@@ -312,6 +312,8 @@ var ServerMsg = z3.discriminatedUnion("type", [
|
|
|
312
312
|
data: z3.string()
|
|
313
313
|
}),
|
|
314
314
|
z3.object({ type: z3.literal("audio.stop") }),
|
|
315
|
+
/** How loud music and video should be while the owner talks or Friend speaks: 1 is as it is, 0.25 a quarter. */
|
|
316
|
+
z3.object({ type: z3.literal("audio.duck"), level: z3.number().min(0).max(1) }),
|
|
315
317
|
/** The owner approved a look through the camera: take one picture and upload it. */
|
|
316
318
|
z3.object({ type: z3.literal("camera.request"), id: z3.string() }),
|
|
317
319
|
/** Stop: the request was answered elsewhere, timed out, or the emergency lock fired. */
|
|
@@ -335,6 +337,8 @@ var ClientMsg = z3.discriminatedUnion("type", [
|
|
|
335
337
|
z3.object({ type: z3.literal("stage.event"), objectId: z3.string(), event: z3.string().max(64) }),
|
|
336
338
|
ApprovalRespond,
|
|
337
339
|
z3.object({ type: z3.literal("voice.interrupt") }),
|
|
340
|
+
/** This screen is listening to the owner, or Friend is speaking on it (true), or neither (false). */
|
|
341
|
+
z3.object({ type: z3.literal("audio.activity"), active: z3.boolean() }),
|
|
338
342
|
z3.object({ type: z3.literal("voice.confirm"), id: z3.string(), pin: z3.string().max(32) }),
|
|
339
343
|
z3.object({ type: z3.literal("voice.cancel"), id: z3.string() }),
|
|
340
344
|
z3.object({ type: z3.literal("camera.denied"), id: z3.string(), reason: z3.enum(["off", "no-camera", "permission", "busy"]) }),
|
|
@@ -3352,12 +3356,16 @@ var DEFAULT_VOICE = {
|
|
|
3352
3356
|
accept: 0.72,
|
|
3353
3357
|
challenge: 0.58,
|
|
3354
3358
|
hindiVoice: DEFAULT_HINDI_VOICE,
|
|
3355
|
-
marathiVoice: DEFAULT_MARATHI_VOICE
|
|
3359
|
+
marathiVoice: DEFAULT_MARATHI_VOICE,
|
|
3360
|
+
duck: true,
|
|
3361
|
+
duckPercent: 25
|
|
3356
3362
|
};
|
|
3357
3363
|
function sanitizeVoice(v) {
|
|
3358
3364
|
const ok = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 1;
|
|
3359
|
-
|
|
3360
|
-
|
|
3365
|
+
const percent2 = typeof v.duckPercent === "number" && Number.isFinite(v.duckPercent) ? Math.max(0, Math.min(100, Math.round(v.duckPercent))) : DEFAULT_VOICE.duckPercent;
|
|
3366
|
+
const base = { ...v, duck: typeof v.duck === "boolean" ? v.duck : DEFAULT_VOICE.duck, duckPercent: percent2 };
|
|
3367
|
+
if (ok(v.accept) && ok(v.challenge) && v.accept > v.challenge) return base;
|
|
3368
|
+
return { ...base, accept: DEFAULT_VOICE.accept, challenge: DEFAULT_VOICE.challenge };
|
|
3361
3369
|
}
|
|
3362
3370
|
var DEFAULT_DESTRUCTIVE_PATTERNS = [
|
|
3363
3371
|
String.raw`\brm\s+(-[a-z]*r|-[a-z]*f)`,
|
|
@@ -5292,8 +5300,8 @@ var Mpv = class {
|
|
|
5292
5300
|
seek(seconds) {
|
|
5293
5301
|
return this.cmd(["seek", seconds, "absolute"]);
|
|
5294
5302
|
}
|
|
5295
|
-
volume(
|
|
5296
|
-
return this.cmd(["set_property", "volume",
|
|
5303
|
+
volume(percent2) {
|
|
5304
|
+
return this.cmd(["set_property", "volume", percent2]);
|
|
5297
5305
|
}
|
|
5298
5306
|
mute(on) {
|
|
5299
5307
|
return this.cmd(["set_property", "mute", on]);
|
|
@@ -5384,8 +5392,8 @@ var VideoHost = class {
|
|
|
5384
5392
|
seek(seconds) {
|
|
5385
5393
|
return this.mpv.seek(seconds);
|
|
5386
5394
|
}
|
|
5387
|
-
volume(
|
|
5388
|
-
return this.mpv.volume(
|
|
5395
|
+
volume(percent2) {
|
|
5396
|
+
return this.mpv.volume(percent2);
|
|
5389
5397
|
}
|
|
5390
5398
|
stop() {
|
|
5391
5399
|
return this.mpv.stop();
|
|
@@ -5989,10 +5997,10 @@ var Provisioner = class {
|
|
|
5989
5997
|
const list2 = await this.text(plan.sums.url);
|
|
5990
5998
|
const want = checksumFor(list2, plan.sums.name);
|
|
5991
5999
|
if (!want) return fail(`Friend could not check the ${name} download: no published checksum was found for it.`);
|
|
5992
|
-
let got = await this.file(plan.url, part, (
|
|
6000
|
+
let got = await this.file(plan.url, part, (percent2) => this.set(name, { phase: "downloading", percent: percent2 }));
|
|
5993
6001
|
if (got.error === "failed") {
|
|
5994
6002
|
rmSync4(part, { force: true });
|
|
5995
|
-
got = await this.file(plan.url, part, (
|
|
6003
|
+
got = await this.file(plan.url, part, (percent2) => this.set(name, { phase: "downloading", percent: percent2 }));
|
|
5996
6004
|
}
|
|
5997
6005
|
if (got.error) {
|
|
5998
6006
|
this.d.onError?.(name, got.error === "big" ? "too big" : got.why ?? "download failed");
|
|
@@ -6083,10 +6091,10 @@ var Provisioner = class {
|
|
|
6083
6091
|
}
|
|
6084
6092
|
hash.update(value);
|
|
6085
6093
|
if (!out.write(value)) await new Promise((r) => out.once("drain", () => r()));
|
|
6086
|
-
const
|
|
6087
|
-
if (
|
|
6088
|
-
lastPercent =
|
|
6089
|
-
progress(
|
|
6094
|
+
const percent2 = total > 0 ? Math.min(99, Math.floor(received / total * 100)) : null;
|
|
6095
|
+
if (percent2 !== lastPercent) {
|
|
6096
|
+
lastPercent = percent2 ?? -1;
|
|
6097
|
+
progress(percent2);
|
|
6090
6098
|
}
|
|
6091
6099
|
}
|
|
6092
6100
|
await new Promise((resolve13, reject) => {
|
|
@@ -6346,6 +6354,10 @@ var VideoHub = class {
|
|
|
6346
6354
|
tokens = /* @__PURE__ */ new Map();
|
|
6347
6355
|
streams = [];
|
|
6348
6356
|
cached = null;
|
|
6357
|
+
/** The volume the owner chose for the PC's player (mpv keeps it from one video to the next). */
|
|
6358
|
+
hostVolume = 100;
|
|
6359
|
+
/** How loud it is while someone talks: 1 is the owner's volume as it is. */
|
|
6360
|
+
duckLevel = 1;
|
|
6349
6361
|
now = null;
|
|
6350
6362
|
tickets = /* @__PURE__ */ new Map();
|
|
6351
6363
|
clock() {
|
|
@@ -6490,6 +6502,8 @@ var VideoHub = class {
|
|
|
6490
6502
|
const opened = await this.d.host.open(info.webpage || args.url || "", { start: start2, maxHeight: cfg.maxHeight });
|
|
6491
6503
|
if (!opened.ok) return { ok: false, text: opened.error };
|
|
6492
6504
|
this.now = { output, title: info.title, uploader: info.uploader, info, via: "mpv" };
|
|
6505
|
+
if (this.duckLevel < 1) await this.d.host.volume(this.hostLevel()).catch(() => {
|
|
6506
|
+
});
|
|
6493
6507
|
return { ok: true, text: `Playing ${what} in a window on this PC.` };
|
|
6494
6508
|
}
|
|
6495
6509
|
if (browserWindow) {
|
|
@@ -6534,6 +6548,17 @@ var VideoHub = class {
|
|
|
6534
6548
|
});
|
|
6535
6549
|
this.now = null;
|
|
6536
6550
|
}
|
|
6551
|
+
hostLevel() {
|
|
6552
|
+
return Math.round(this.hostVolume * this.duckLevel);
|
|
6553
|
+
}
|
|
6554
|
+
/** Turns the video in the PC's own player down (0.25 is a quarter) while someone talks, and back up (1) after. A video on the screen is turned down by the page. */
|
|
6555
|
+
async duck(level) {
|
|
6556
|
+
this.duckLevel = Math.max(0, Math.min(1, level));
|
|
6557
|
+
const now = this.now;
|
|
6558
|
+
if (now?.output !== "host" || now.via !== "mpv" || !this.d.host) return;
|
|
6559
|
+
await this.d.host.volume(this.hostLevel()).catch(() => {
|
|
6560
|
+
});
|
|
6561
|
+
}
|
|
6537
6562
|
async control(args) {
|
|
6538
6563
|
if (args.action === "stop") {
|
|
6539
6564
|
const wasScreen = this.now?.output === "screen";
|
|
@@ -6549,7 +6574,10 @@ var VideoHub = class {
|
|
|
6549
6574
|
if (args.action === "pause") await host.pause(true);
|
|
6550
6575
|
else if (args.action === "resume") await host.pause(false);
|
|
6551
6576
|
else if (args.action === "seek") await host.seek(Math.max(0, args.seconds ?? 0));
|
|
6552
|
-
else
|
|
6577
|
+
else {
|
|
6578
|
+
this.hostVolume = Math.max(0, Math.min(100, args.level ?? 50));
|
|
6579
|
+
await host.volume(this.hostLevel());
|
|
6580
|
+
}
|
|
6553
6581
|
} catch (e) {
|
|
6554
6582
|
return { ok: false, text: e instanceof Error ? e.message : "The player did not answer." };
|
|
6555
6583
|
}
|
|
@@ -8907,6 +8935,8 @@ var HostPlayer = class {
|
|
|
8907
8935
|
chain = Promise.resolve();
|
|
8908
8936
|
off;
|
|
8909
8937
|
disposed = false;
|
|
8938
|
+
/** How loud it is while someone talks: 1 is the owner's volume as it is. */
|
|
8939
|
+
duckLevel = 1;
|
|
8910
8940
|
now = () => (this.d.clock ?? Date.now)();
|
|
8911
8941
|
available() {
|
|
8912
8942
|
const problem = programProblem(this.d.settings().hostPlayer.trim());
|
|
@@ -9090,14 +9120,24 @@ var HostPlayer = class {
|
|
|
9090
9120
|
if (this.engine === "folder") await this.d.mpv.seek(seconds);
|
|
9091
9121
|
else if (this.engine === "spotify" && this.deviceId) await this.d.spotify.control(this.deviceId, { action: "seek", ms: Math.round(seconds * 1e3) });
|
|
9092
9122
|
}
|
|
9123
|
+
/** The owner's volume, turned down while someone is talking. */
|
|
9124
|
+
level() {
|
|
9125
|
+
return Math.round(this.s.volume * this.duckLevel);
|
|
9126
|
+
}
|
|
9093
9127
|
async applyVolume() {
|
|
9094
9128
|
if (this.engine === "folder") {
|
|
9095
|
-
await this.d.mpv.volume(this.
|
|
9129
|
+
await this.d.mpv.volume(this.level());
|
|
9096
9130
|
await this.d.mpv.mute(this.s.muted);
|
|
9097
9131
|
} else if (this.engine === "spotify" && this.deviceId) {
|
|
9098
|
-
await this.d.spotify.control(this.deviceId, { action: "volume", percent: this.s.muted ? 0 : this.
|
|
9132
|
+
await this.d.spotify.control(this.deviceId, { action: "volume", percent: this.s.muted ? 0 : this.level() });
|
|
9099
9133
|
}
|
|
9100
9134
|
}
|
|
9135
|
+
/** Turns the sound down (0.25 is a quarter) while the owner talks to Friend or Friend speaks, and back up (1) after. The owner's own volume is not changed. */
|
|
9136
|
+
async duck(level) {
|
|
9137
|
+
this.duckLevel = Math.max(0, Math.min(1, level));
|
|
9138
|
+
if (this.disposed || !this.engine) return;
|
|
9139
|
+
await this.run(() => this.applyVolume());
|
|
9140
|
+
}
|
|
9101
9141
|
// ---- what the engines tell us ----
|
|
9102
9142
|
onMpv(e) {
|
|
9103
9143
|
if (this.disposed) return;
|
|
@@ -12956,7 +12996,9 @@ var SettingsPatch = z11.object({
|
|
|
12956
12996
|
/** A Piper voice name; empty makes the main voice read Hindi too. */
|
|
12957
12997
|
hindiVoice: z11.string().trim().max(80).regex(/^[A-Za-z0-9_-]*$/, "A Piper voice name such as hi_IN-priyamvada-medium, or empty"),
|
|
12958
12998
|
marathiVoice: z11.string().trim().max(80).regex(/^[A-Za-z0-9_-]*$/, "A Piper voice name such as mr_IN-google-medium, or empty"),
|
|
12959
|
-
sttModel: z11.string().trim().min(1).max(80).regex(/^[\w./-]+$/)
|
|
12999
|
+
sttModel: z11.string().trim().min(1).max(80).regex(/^[\w./-]+$/),
|
|
13000
|
+
duck: z11.boolean(),
|
|
13001
|
+
duckPercent: z11.number().int().min(0).max(100)
|
|
12960
13002
|
}).partial().strict()
|
|
12961
13003
|
}).partial().strict();
|
|
12962
13004
|
function settingsView(c) {
|
|
@@ -13017,7 +13059,9 @@ function settingsView(c) {
|
|
|
13017
13059
|
ttsVoice: c.voice.ttsVoice,
|
|
13018
13060
|
sttModel: c.voice.sttModel,
|
|
13019
13061
|
hindiVoice: c.voice.hindiVoice,
|
|
13020
|
-
marathiVoice: c.voice.marathiVoice
|
|
13062
|
+
marathiVoice: c.voice.marathiVoice,
|
|
13063
|
+
duck: c.voice.duck,
|
|
13064
|
+
duckPercent: c.voice.duckPercent
|
|
13021
13065
|
}
|
|
13022
13066
|
};
|
|
13023
13067
|
}
|
|
@@ -13609,6 +13653,42 @@ async function ensureTlsCert(dataDir, extraHosts = [], ifaces, now = Date.now) {
|
|
|
13609
13653
|
return { key: pems.private, cert: pems.cert };
|
|
13610
13654
|
}
|
|
13611
13655
|
|
|
13656
|
+
// ../server/src/audio/duck.ts
|
|
13657
|
+
var FALLBACK_PERCENT = 25;
|
|
13658
|
+
var DuckController = class {
|
|
13659
|
+
constructor(d) {
|
|
13660
|
+
this.d = d;
|
|
13661
|
+
}
|
|
13662
|
+
d;
|
|
13663
|
+
active = /* @__PURE__ */ new Set();
|
|
13664
|
+
current = 1;
|
|
13665
|
+
/** How loud things should be now: 1 as normal. */
|
|
13666
|
+
get level() {
|
|
13667
|
+
return this.current;
|
|
13668
|
+
}
|
|
13669
|
+
set(source, active) {
|
|
13670
|
+
if (active) this.active.add(source);
|
|
13671
|
+
else this.active.delete(source);
|
|
13672
|
+
this.update();
|
|
13673
|
+
}
|
|
13674
|
+
/** A screen went away. */
|
|
13675
|
+
drop(source) {
|
|
13676
|
+
if (this.active.delete(source)) this.update();
|
|
13677
|
+
}
|
|
13678
|
+
/** The owner changed the settings. */
|
|
13679
|
+
refresh() {
|
|
13680
|
+
this.update();
|
|
13681
|
+
}
|
|
13682
|
+
update() {
|
|
13683
|
+
const raw = this.d.percent();
|
|
13684
|
+
const percent2 = Number.isFinite(raw) ? Math.max(0, Math.min(100, raw)) : FALLBACK_PERCENT;
|
|
13685
|
+
const next = this.d.enabled() && this.active.size > 0 ? percent2 / 100 : 1;
|
|
13686
|
+
if (next === this.current) return;
|
|
13687
|
+
this.current = next;
|
|
13688
|
+
this.d.apply(next);
|
|
13689
|
+
}
|
|
13690
|
+
};
|
|
13691
|
+
|
|
13612
13692
|
// ../server/src/voice/challenge.ts
|
|
13613
13693
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
13614
13694
|
var Challenges = class {
|
|
@@ -14484,6 +14564,7 @@ async function createApp(opts) {
|
|
|
14484
14564
|
else swap();
|
|
14485
14565
|
}
|
|
14486
14566
|
if (applied.changed.includes("voice.ttsVoice")) void warmVoice(config.voice.ttsVoice, voiceBefore);
|
|
14567
|
+
if (applied.changed.some((k) => k === "voice.duck" || k === "voice.duckPercent")) ducker.refresh();
|
|
14487
14568
|
if (applied.changed.includes("voice.sttModel")) void warmListening(config.voice.sttModel, sttBefore);
|
|
14488
14569
|
if (applied.changed.includes("voice.hindiVoice")) void warmLanguageVoice(config.voice.hindiVoice);
|
|
14489
14570
|
if (applied.changed.includes("voice.marathiVoice")) void warmLanguageVoice(config.voice.marathiVoice);
|
|
@@ -14925,6 +15006,20 @@ async function createApp(opts) {
|
|
|
14925
15006
|
return key;
|
|
14926
15007
|
}
|
|
14927
15008
|
};
|
|
15009
|
+
const duckIds = /* @__PURE__ */ new WeakMap();
|
|
15010
|
+
let duckSeq = 0;
|
|
15011
|
+
const duckId = (conn) => duckIds.get(conn) ?? (duckIds.set(conn, `screen-${++duckSeq}`), duckIds.get(conn));
|
|
15012
|
+
const ducker = new DuckController({
|
|
15013
|
+
enabled: () => config.voice.duck,
|
|
15014
|
+
percent: () => config.voice.duckPercent,
|
|
15015
|
+
apply: (level) => {
|
|
15016
|
+
broadcast({ type: "audio.duck", level });
|
|
15017
|
+
void hostPlayer.duck(level).catch(() => {
|
|
15018
|
+
});
|
|
15019
|
+
void videoHub.duck(level).catch(() => {
|
|
15020
|
+
});
|
|
15021
|
+
}
|
|
15022
|
+
});
|
|
14928
15023
|
const tls = config.https ? await ensureTlsCert(opts.dataDir, [...config.allowedHosts, ...config.publicUrl ? [new URL(config.publicUrl).hostname] : []]) : void 0;
|
|
14929
15024
|
const gateway = createGateway({
|
|
14930
15025
|
...tls ? { tls } : {},
|
|
@@ -15338,11 +15433,13 @@ ${url}`);
|
|
|
15338
15433
|
conn.send(statusMsg());
|
|
15339
15434
|
conn.send(uiMsg());
|
|
15340
15435
|
conn.send({ type: "music.host", state: hostPlayer.state() });
|
|
15436
|
+
if (ducker.level < 1) conn.send({ type: "audio.duck", level: ducker.level });
|
|
15341
15437
|
const pending = challenges.current();
|
|
15342
15438
|
if (pending) conn.send({ type: "voice.challenge", id: pending.id, text: pending.text });
|
|
15343
15439
|
return () => {
|
|
15344
15440
|
clients.delete(conn);
|
|
15345
15441
|
musicHub.detach(conn);
|
|
15442
|
+
ducker.drop(duckId(conn));
|
|
15346
15443
|
};
|
|
15347
15444
|
},
|
|
15348
15445
|
onClientMessage: (_session, msg, conn) => {
|
|
@@ -15360,6 +15457,9 @@ ${url}`);
|
|
|
15360
15457
|
case "voice.interrupt":
|
|
15361
15458
|
speaker?.interrupt(broadcast);
|
|
15362
15459
|
break;
|
|
15460
|
+
case "audio.activity":
|
|
15461
|
+
ducker.set(duckId(conn), msg.active);
|
|
15462
|
+
break;
|
|
15363
15463
|
case "voice.confirm":
|
|
15364
15464
|
void confirmChallenge(msg.id, msg.pin, conn).catch((e) => audit.log("system", "voice.confirm_error", { message: e.message }));
|
|
15365
15465
|
break;
|
|
@@ -15658,6 +15758,11 @@ var unit2 = (v) => {
|
|
|
15658
15758
|
if (v.trim() === "" || !Number.isFinite(n) || n < 0 || n > 1) throw new Error("Expected a number between 0 and 1");
|
|
15659
15759
|
return n;
|
|
15660
15760
|
};
|
|
15761
|
+
var percent = (v) => {
|
|
15762
|
+
const n = Number(v);
|
|
15763
|
+
if (v.trim() === "" || !Number.isFinite(n) || n < 0 || n > 100) throw new Error("Expected a number between 0 and 100");
|
|
15764
|
+
return Math.round(n);
|
|
15765
|
+
};
|
|
15661
15766
|
var httpUrl = (v) => {
|
|
15662
15767
|
let u;
|
|
15663
15768
|
try {
|
|
@@ -15716,6 +15821,8 @@ var GROUPS = {
|
|
|
15716
15821
|
},
|
|
15717
15822
|
language: (v) => v.trim() === "" ? null : v.trim(),
|
|
15718
15823
|
speakTyped: bool,
|
|
15824
|
+
duck: bool,
|
|
15825
|
+
duckPercent: percent,
|
|
15719
15826
|
accept: unit2,
|
|
15720
15827
|
challenge: unit2
|
|
15721
15828
|
},
|
|
@@ -15780,6 +15887,8 @@ var KEY_HELP = {
|
|
|
15780
15887
|
"voice.hindiVoice": "the voice that reads Hindi (Devanagari) text: hi_IN-priyamvada-medium, hi_IN-pratham-medium or hi_IN-rohan-medium; empty uses the main voice",
|
|
15781
15888
|
"voice.marathiVoice": "the voice that reads Marathi (Devanagari) text: mr_IN-google-medium; empty uses the main voice",
|
|
15782
15889
|
"voice.speakTyped": "true or false: also speak the replies to typed messages",
|
|
15890
|
+
"voice.duck": "true or false: turn music and video down while you talk to Friend and while it speaks",
|
|
15891
|
+
"voice.duckPercent": "0 to 100: how loud music and video are then, as a percent of how loud they were (25 is a quarter)",
|
|
15783
15892
|
"voice.accept": "0 to 1: a voice at or above this match is you",
|
|
15784
15893
|
"voice.challenge": "0 to 1: a voice between this and accept must be confirmed with your PIN",
|
|
15785
15894
|
"ollama.url": "address of the Ollama server, for example http://127.0.0.1:11434",
|
|
@@ -17943,7 +18052,7 @@ async function daemonCommand(args, io2, deps = realDeps) {
|
|
|
17943
18052
|
import crossSpawn5 from "cross-spawn";
|
|
17944
18053
|
var PACKAGE = "friend-assistant";
|
|
17945
18054
|
var real = {
|
|
17946
|
-
current: () => true ? "0.1.
|
|
18055
|
+
current: () => true ? "0.1.44" : "development",
|
|
17947
18056
|
latest: () => new Promise((resolve13) => {
|
|
17948
18057
|
const child = crossSpawn5("npm", ["view", PACKAGE, "version", "--prefer-online"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
17949
18058
|
let out = "";
|
|
@@ -18231,7 +18340,7 @@ async function run(argv, io2) {
|
|
|
18231
18340
|
case "version":
|
|
18232
18341
|
case "--version":
|
|
18233
18342
|
case "-v":
|
|
18234
|
-
io2.out(true ? "0.1.
|
|
18343
|
+
io2.out(true ? "0.1.44" : "development");
|
|
18235
18344
|
return 0;
|
|
18236
18345
|
case "lan":
|
|
18237
18346
|
return await lanCommand(rest, io2);
|