@agentprojectcontext/apx 1.42.2 → 1.44.0
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/package.json +1 -1
- package/src/core/config/index.js +2 -0
- package/src/core/config/redact.js +2 -0
- package/src/core/desktop/process.js +126 -0
- package/src/core/voice/stt-hardware.js +87 -0
- package/src/core/voice/stt-models.js +97 -0
- package/src/core/voice/transcription.js +147 -16
- package/src/host/daemon/api/desktop.js +54 -8
- package/src/host/daemon/api/transcribe.js +40 -1
- package/src/host/daemon/whisper-server.js +18 -8
- package/src/host/daemon/whisper-server.py +71 -44
- package/src/interfaces/cli/commands/desktop.js +13 -68
- package/src/interfaces/desktop/main.js +32 -4
- package/src/interfaces/desktop/renderer.js +26 -5
- package/src/interfaces/web/dist/assets/index-BAKk7d_M.css +1 -0
- package/src/interfaces/web/dist/assets/index-Cjj_d3SA.js +656 -0
- package/src/interfaces/web/dist/assets/index-Cjj_d3SA.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/Pager.tsx +88 -0
- package/src/interfaces/web/src/components/ShortcutInput.tsx +156 -0
- package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +101 -5
- package/src/interfaces/web/src/i18n/en.ts +33 -2
- package/src/interfaces/web/src/i18n/es.ts +33 -2
- package/src/interfaces/web/src/lib/api/desktop.ts +28 -0
- package/src/interfaces/web/src/lib/api/voice.ts +26 -2
- package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +4 -1
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +4 -1
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +55 -3
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +98 -36
- package/src/interfaces/web/src/screens/project/TasksTab.tsx +4 -1
- package/src/interfaces/web/dist/assets/index-BReF4_xV.js +0 -646
- package/src/interfaces/web/dist/assets/index-BReF4_xV.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-wrEbTJbc.css +0 -1
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
<link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
|
|
19
19
|
<link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
|
|
20
20
|
<link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
|
|
21
|
-
<script type="module" crossorigin src="/assets/index-
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-Cjj_d3SA.js"></script>
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BAKk7d_M.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-background text-foreground antialiased">
|
|
25
25
|
<div id="root"></div>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
|
+
import { Button } from "./ui";
|
|
4
|
+
import { UiSelect } from "./UiSelect";
|
|
5
|
+
import { t } from "../i18n";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
8
|
+
export const PAGE_SIZES = [10, 20, 50, 100];
|
|
9
|
+
|
|
10
|
+
// Client-side pagination over an already-fetched array. The list endpoints
|
|
11
|
+
// return the full set (sessions/tasks are bounded), so we page in the browser
|
|
12
|
+
// rather than round-trip the daemon. Pass `resetKey` (e.g. the active filter)
|
|
13
|
+
// to jump back to page 1 whenever the source set changes; the window is also
|
|
14
|
+
// clamped so a shrinking list never strands the user on an empty page.
|
|
15
|
+
export function usePaged<T>(items: T[], resetKey?: unknown, initialPageSize = DEFAULT_PAGE_SIZE) {
|
|
16
|
+
const [page, setPage] = useState(1);
|
|
17
|
+
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
18
|
+
|
|
19
|
+
useEffect(() => { setPage(1); }, [resetKey]);
|
|
20
|
+
|
|
21
|
+
const pageCount = Math.max(1, Math.ceil(items.length / pageSize));
|
|
22
|
+
const safePage = Math.min(page, pageCount);
|
|
23
|
+
useEffect(() => { if (page !== safePage) setPage(safePage); }, [page, safePage]);
|
|
24
|
+
|
|
25
|
+
const start = (safePage - 1) * pageSize;
|
|
26
|
+
const end = Math.min(start + pageSize, items.length);
|
|
27
|
+
return {
|
|
28
|
+
slice: items.slice(start, end),
|
|
29
|
+
page: safePage,
|
|
30
|
+
pageCount,
|
|
31
|
+
total: items.length,
|
|
32
|
+
start,
|
|
33
|
+
end,
|
|
34
|
+
pageSize,
|
|
35
|
+
setPage,
|
|
36
|
+
// Changing the page size keeps things predictable by returning to page 1.
|
|
37
|
+
setPageSize: (n: number) => { setPageSize(n); setPage(1); },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function Pager({
|
|
42
|
+
page,
|
|
43
|
+
pageCount,
|
|
44
|
+
total,
|
|
45
|
+
start,
|
|
46
|
+
end,
|
|
47
|
+
pageSize,
|
|
48
|
+
onPage,
|
|
49
|
+
onPageSize,
|
|
50
|
+
}: {
|
|
51
|
+
page: number;
|
|
52
|
+
pageCount: number;
|
|
53
|
+
total: number;
|
|
54
|
+
start: number;
|
|
55
|
+
end: number;
|
|
56
|
+
pageSize: number;
|
|
57
|
+
onPage: (p: number) => void;
|
|
58
|
+
onPageSize: (n: number) => void;
|
|
59
|
+
}) {
|
|
60
|
+
// Nothing to page when the whole set fits in the smallest page size.
|
|
61
|
+
if (total <= PAGE_SIZES[0]) return null;
|
|
62
|
+
return (
|
|
63
|
+
<div className="mt-3 flex flex-wrap items-center justify-between gap-3 text-xs text-muted-fg">
|
|
64
|
+
<div className="flex items-center gap-3">
|
|
65
|
+
<span className="tabular-nums">{t("common.pager_range", { from: start + 1, to: end, total })}</span>
|
|
66
|
+
<span className="flex items-center gap-1.5">
|
|
67
|
+
<span>{t("common.pager_per_page")}</span>
|
|
68
|
+
<div className="w-[4.5rem]">
|
|
69
|
+
<UiSelect
|
|
70
|
+
value={String(pageSize)}
|
|
71
|
+
onChange={(v) => onPageSize(Number(v))}
|
|
72
|
+
options={PAGE_SIZES.map((n) => ({ value: String(n), label: String(n) }))}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="flex items-center gap-1">
|
|
78
|
+
<Button size="sm" variant="ghost" disabled={page <= 1} onClick={() => onPage(page - 1)} aria-label={t("common.pager_prev")}>
|
|
79
|
+
<ChevronLeft size={14} />
|
|
80
|
+
</Button>
|
|
81
|
+
<span className="px-1 tabular-nums">{t("common.pager_page", { page, total: pageCount })}</span>
|
|
82
|
+
<Button size="sm" variant="ghost" disabled={page >= pageCount} onClick={() => onPage(page + 1)} aria-label={t("common.pager_next")}>
|
|
83
|
+
<ChevronRight size={14} />
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import { Kbd } from "./ui/kbd";
|
|
4
|
+
import { t } from "../i18n";
|
|
5
|
+
|
|
6
|
+
// Capture-style field for an Electron global-shortcut accelerator. Instead of
|
|
7
|
+
// showing the raw "CommandOrControl+Shift+G" string in a text box (which reads
|
|
8
|
+
// like gibberish), it renders the combo as readable key badges (⌘ Cmd + G) and
|
|
9
|
+
// lets the user re-record it by clicking and pressing the keys. The stored
|
|
10
|
+
// value stays the canonical Electron accelerator string so main.js /
|
|
11
|
+
// globalShortcut keep working unchanged.
|
|
12
|
+
|
|
13
|
+
const isMac = typeof navigator !== "undefined" && /mac/i.test(navigator.platform || "");
|
|
14
|
+
|
|
15
|
+
// Readable label for one accelerator segment. Modifiers get a glyph + word
|
|
16
|
+
// (a bare "⌘"/"Ctrl" alone reads oddly), other keys (G, Space, Up, F5) show
|
|
17
|
+
// verbatim.
|
|
18
|
+
function tokenLabel(seg: string): string {
|
|
19
|
+
const macMap: Record<string, string> = {
|
|
20
|
+
CommandOrControl: "⌘ Cmd", CmdOrCtrl: "⌘ Cmd", Command: "⌘ Cmd", Cmd: "⌘ Cmd", Super: "⌘ Cmd", Meta: "⌘ Cmd",
|
|
21
|
+
Control: "⌃ Ctrl", Ctrl: "⌃ Ctrl", Option: "⌥ Opt", Alt: "⌥ Opt", Shift: "⇧ Shift",
|
|
22
|
+
};
|
|
23
|
+
const winMap: Record<string, string> = {
|
|
24
|
+
CommandOrControl: "Ctrl", CmdOrCtrl: "Ctrl", Control: "Ctrl", Ctrl: "Ctrl",
|
|
25
|
+
Command: "Win", Cmd: "Win", Super: "Win", Meta: "Win", Option: "Alt", Alt: "Alt", Shift: "Shift",
|
|
26
|
+
};
|
|
27
|
+
const map = isMac ? macMap : winMap;
|
|
28
|
+
return map[seg] ?? seg;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function splitAccelerator(value: string): string[] {
|
|
32
|
+
return (value || "").split("+").map((s) => s.trim()).filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Layout-independent main-key name from a KeyboardEvent.code (so Option-dead
|
|
36
|
+
// keys on mac don't corrupt the letter). Returns null for pure modifier keys.
|
|
37
|
+
function codeToKey(e: KeyboardEvent): string | null {
|
|
38
|
+
const code = e.code;
|
|
39
|
+
if (/^(Shift|Control|Alt|Meta|OS)(Left|Right)?$/.test(code)) return null;
|
|
40
|
+
let m;
|
|
41
|
+
if ((m = code.match(/^Key([A-Z])$/))) return m[1];
|
|
42
|
+
if ((m = code.match(/^Digit(\d)$/))) return m[1];
|
|
43
|
+
if ((m = code.match(/^Numpad(\d)$/))) return m[1];
|
|
44
|
+
if ((m = code.match(/^(F\d{1,2})$/))) return m[1];
|
|
45
|
+
if ((m = code.match(/^Arrow(Up|Down|Left|Right)$/))) return m[1];
|
|
46
|
+
const named: Record<string, string> = {
|
|
47
|
+
Space: "Space", Enter: "Enter", Tab: "Tab", Backspace: "Backspace", Delete: "Delete",
|
|
48
|
+
Home: "Home", End: "End", PageUp: "PageUp", PageDown: "PageDown", Insert: "Insert",
|
|
49
|
+
Minus: "-", Equal: "=", BracketLeft: "[", BracketRight: "]", Backslash: "\\",
|
|
50
|
+
Semicolon: ";", Quote: "'", Comma: ",", Period: ".", Slash: "/", Backquote: "`",
|
|
51
|
+
};
|
|
52
|
+
if (named[code]) return named[code];
|
|
53
|
+
// Fallback: a single printable char from e.key.
|
|
54
|
+
if (e.key && e.key.length === 1) return e.key.toUpperCase();
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build a canonical Electron accelerator from a keydown. Returns null until a
|
|
59
|
+
// non-modifier key is pressed alongside ≥1 modifier (or a function key on its
|
|
60
|
+
// own), so a lone ⇧ doesn't get committed.
|
|
61
|
+
function eventToAccelerator(e: KeyboardEvent): string | null {
|
|
62
|
+
const key = codeToKey(e);
|
|
63
|
+
if (!key) return null;
|
|
64
|
+
const parts: string[] = [];
|
|
65
|
+
if (isMac ? e.metaKey : e.ctrlKey) parts.push("CommandOrControl");
|
|
66
|
+
if (isMac && e.ctrlKey) parts.push("Control");
|
|
67
|
+
if (e.altKey) parts.push("Alt");
|
|
68
|
+
if (e.shiftKey) parts.push("Shift");
|
|
69
|
+
const hasMod = parts.length > 0;
|
|
70
|
+
const isFn = /^F\d{1,2}$/.test(key);
|
|
71
|
+
if (!hasMod && !isFn) return null; // need a modifier for letter/digit keys
|
|
72
|
+
parts.push(key);
|
|
73
|
+
return parts.join("+");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function ShortcutInput({
|
|
77
|
+
value,
|
|
78
|
+
onChange,
|
|
79
|
+
disabled,
|
|
80
|
+
className,
|
|
81
|
+
trailing,
|
|
82
|
+
}: {
|
|
83
|
+
value: string;
|
|
84
|
+
onChange: (accelerator: string) => void;
|
|
85
|
+
disabled?: boolean;
|
|
86
|
+
className?: string;
|
|
87
|
+
/** Rendered at the end of the field — e.g. a Save button (input-group style). */
|
|
88
|
+
trailing?: ReactNode;
|
|
89
|
+
}) {
|
|
90
|
+
const [recording, setRecording] = useState(false);
|
|
91
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!recording) return;
|
|
95
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
if (e.key === "Escape") { setRecording(false); return; }
|
|
99
|
+
const accel = eventToAccelerator(e);
|
|
100
|
+
if (accel) { onChange(accel); setRecording(false); }
|
|
101
|
+
};
|
|
102
|
+
window.addEventListener("keydown", onKeyDown, true);
|
|
103
|
+
return () => window.removeEventListener("keydown", onKeyDown, true);
|
|
104
|
+
}, [recording, onChange]);
|
|
105
|
+
|
|
106
|
+
const segments = splitAccelerator(value);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
className={cn(
|
|
111
|
+
"flex h-9 w-full max-w-md items-center rounded-md border border-border bg-transparent transition",
|
|
112
|
+
recording && "ring-2 ring-ring",
|
|
113
|
+
disabled && "opacity-50",
|
|
114
|
+
className,
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{/* Capture area (click → record). Kept borderless so the wrapper reads as
|
|
118
|
+
one input; the trailing slot sits flush at the end. */}
|
|
119
|
+
<button
|
|
120
|
+
ref={ref}
|
|
121
|
+
type="button"
|
|
122
|
+
disabled={disabled}
|
|
123
|
+
onClick={() => setRecording((r) => !r)}
|
|
124
|
+
onBlur={() => setRecording(false)}
|
|
125
|
+
aria-label={t("modules_ui.desktop_shortcut_record")}
|
|
126
|
+
className="flex h-full min-w-0 flex-1 items-center gap-1.5 rounded-l-md px-3 text-sm outline-none hover:bg-muted/40 focus-visible:bg-muted/40"
|
|
127
|
+
>
|
|
128
|
+
{recording ? (
|
|
129
|
+
<span className="text-muted-fg">{t("modules_ui.desktop_shortcut_recording")}</span>
|
|
130
|
+
) : segments.length ? (
|
|
131
|
+
<span className="flex min-w-0 flex-wrap items-center gap-1">
|
|
132
|
+
{segments.map((seg, i) => (
|
|
133
|
+
<span key={i} className="flex items-center gap-1">
|
|
134
|
+
{i > 0 && <span className="text-xs text-muted-fg">+</span>}
|
|
135
|
+
<Kbd className="h-6 min-w-6 border border-border bg-muted px-1.5 text-[13px] font-semibold text-fg shadow-sm">
|
|
136
|
+
{tokenLabel(seg)}
|
|
137
|
+
</Kbd>
|
|
138
|
+
</span>
|
|
139
|
+
))}
|
|
140
|
+
</span>
|
|
141
|
+
) : (
|
|
142
|
+
<span className="text-muted-fg">{t("modules_ui.desktop_shortcut_record")}</span>
|
|
143
|
+
)}
|
|
144
|
+
<span className="ml-auto whitespace-nowrap pl-2 text-xs text-muted-fg">
|
|
145
|
+
{recording ? t("modules_ui.desktop_shortcut_esc") : t("modules_ui.desktop_shortcut_change")}
|
|
146
|
+
</span>
|
|
147
|
+
</button>
|
|
148
|
+
|
|
149
|
+
{trailing ? (
|
|
150
|
+
<div className="flex h-full items-center border-l border-border px-1">
|
|
151
|
+
{trailing}
|
|
152
|
+
</div>
|
|
153
|
+
) : null}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { Field } from "../ui";
|
|
1
|
+
import { Field, Input } from "../ui";
|
|
2
2
|
import { UiSelect } from "../UiSelect";
|
|
3
3
|
import { WHISPER_MODELS, type TranscriptionConfig } from "../../lib/api/voice";
|
|
4
|
+
import { isSecretMarker, secretSuffix } from "../../lib/secrets";
|
|
4
5
|
import { t } from "../../i18n";
|
|
5
6
|
|
|
6
7
|
// STT (speech-to-text) configuration. Persisted under config.transcription.
|
|
7
|
-
// The actual capture happens in the
|
|
8
|
-
// owner
|
|
8
|
+
// The actual capture happens in the desktop window / Telegram / CLI; here the
|
|
9
|
+
// owner picks the engine and configures it:
|
|
10
|
+
// local — embedded faster-whisper (offline; model + language)
|
|
11
|
+
// openai — OpenAI cloud Whisper (api_key + model)
|
|
12
|
+
// custom — any OpenAI-compatible server: mlx-audio on this Mac's Metal GPU,
|
|
13
|
+
// a Radeon/NVIDIA box on the LAN, or a remote endpoint (base_url).
|
|
9
14
|
|
|
10
15
|
interface Props {
|
|
11
16
|
config: TranscriptionConfig;
|
|
@@ -17,6 +22,7 @@ const providerOptions = () => [
|
|
|
17
22
|
{ value: "auto", label: t("voice_ui.stt_provider_auto") },
|
|
18
23
|
{ value: "local", label: t("voice_ui.stt_provider_local") },
|
|
19
24
|
{ value: "openai", label: t("voice_ui.stt_provider_openai") },
|
|
25
|
+
{ value: "custom", label: t("voice_ui.stt_provider_custom") },
|
|
20
26
|
];
|
|
21
27
|
|
|
22
28
|
const langOptions = () => [
|
|
@@ -32,9 +38,30 @@ const langOptions = () => [
|
|
|
32
38
|
export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
33
39
|
const provider = config.provider || "auto";
|
|
34
40
|
const local = config.local || {};
|
|
41
|
+
const openai = config.openai || {};
|
|
42
|
+
const custom = config.custom || {};
|
|
35
43
|
const model = local.model || "small";
|
|
36
44
|
const language = local.language || "auto";
|
|
37
|
-
|
|
45
|
+
// "auto" tries local first, so its tuning shares the local block.
|
|
46
|
+
const showLocal = provider === "auto" || provider === "local";
|
|
47
|
+
|
|
48
|
+
// Text fields patch on blur (not every keystroke), and only when changed.
|
|
49
|
+
const patchText = (key: string, prev: string | undefined, value: string) => {
|
|
50
|
+
const next = value.trim();
|
|
51
|
+
if (next === (prev || "").trim()) return;
|
|
52
|
+
onPatch({ [key]: next });
|
|
53
|
+
};
|
|
54
|
+
// Secrets: a blank field keeps the stored key; the daemon ignores redacted
|
|
55
|
+
// "*** set ***" markers, so we never echo one back as a real value.
|
|
56
|
+
const patchKey = (key: string, value: string) => {
|
|
57
|
+
const next = value.trim();
|
|
58
|
+
if (!next || isSecretMarker(next)) return;
|
|
59
|
+
onPatch({ [key]: next });
|
|
60
|
+
};
|
|
61
|
+
const keyPlaceholder = (marker: unknown) =>
|
|
62
|
+
isSecretMarker(marker)
|
|
63
|
+
? t("voice_ui.api_key_set", { suffix: secretSuffix(marker) ?? "" })
|
|
64
|
+
: t("voice_ui.api_key_label");
|
|
38
65
|
|
|
39
66
|
return (
|
|
40
67
|
<div className="space-y-3">
|
|
@@ -48,7 +75,7 @@ export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
|
48
75
|
/>
|
|
49
76
|
</Field>
|
|
50
77
|
|
|
51
|
-
{
|
|
78
|
+
{showLocal && (
|
|
52
79
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
53
80
|
<Field label={t("voice_ui.stt_model_label")} hint={t("voice_ui.stt_model_hint")}>
|
|
54
81
|
<UiSelect
|
|
@@ -68,6 +95,75 @@ export function VoiceSttCard({ config, onPatch, busy }: Props) {
|
|
|
68
95
|
</Field>
|
|
69
96
|
</div>
|
|
70
97
|
)}
|
|
98
|
+
|
|
99
|
+
{provider === "openai" && (
|
|
100
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
101
|
+
<Field
|
|
102
|
+
label={t("voice_ui.api_key_label")}
|
|
103
|
+
hint={isSecretMarker(openai.api_key)
|
|
104
|
+
? t("voice_ui.api_key_keep_hint")
|
|
105
|
+
: t("voice_ui.api_key_reuse_hint", { engine: "engines.openai.api_key", env: "OPENAI_API_KEY" })}
|
|
106
|
+
>
|
|
107
|
+
<Input
|
|
108
|
+
type="password"
|
|
109
|
+
autoComplete="new-password"
|
|
110
|
+
defaultValue=""
|
|
111
|
+
placeholder={keyPlaceholder(openai.api_key)}
|
|
112
|
+
onBlur={(e) => patchKey("transcription.openai.api_key", e.target.value)}
|
|
113
|
+
disabled={busy}
|
|
114
|
+
/>
|
|
115
|
+
</Field>
|
|
116
|
+
<Field label={t("voice_ui.stt_openai_model_label")} hint={t("voice_ui.stt_openai_model_hint")}>
|
|
117
|
+
<Input
|
|
118
|
+
defaultValue={openai.model || ""}
|
|
119
|
+
placeholder="whisper-1"
|
|
120
|
+
onBlur={(e) => patchText("transcription.openai.model", openai.model, e.target.value)}
|
|
121
|
+
disabled={busy}
|
|
122
|
+
/>
|
|
123
|
+
</Field>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{provider === "custom" && (
|
|
128
|
+
<div className="space-y-3">
|
|
129
|
+
<Field label={t("voice_ui.stt_custom_baseurl_label")} hint={t("voice_ui.stt_custom_baseurl_hint")}>
|
|
130
|
+
<Input
|
|
131
|
+
defaultValue={custom.base_url || ""}
|
|
132
|
+
placeholder="http://localhost:8000/v1"
|
|
133
|
+
onBlur={(e) => patchText("transcription.custom.base_url", custom.base_url, e.target.value)}
|
|
134
|
+
disabled={busy}
|
|
135
|
+
/>
|
|
136
|
+
</Field>
|
|
137
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
138
|
+
<Field label={t("voice_ui.stt_custom_model_label")} hint={t("voice_ui.stt_custom_model_hint")}>
|
|
139
|
+
<Input
|
|
140
|
+
defaultValue={custom.model || ""}
|
|
141
|
+
placeholder="mlx-community/whisper-large-v3-turbo"
|
|
142
|
+
onBlur={(e) => patchText("transcription.custom.model", custom.model, e.target.value)}
|
|
143
|
+
disabled={busy}
|
|
144
|
+
/>
|
|
145
|
+
</Field>
|
|
146
|
+
<Field label={t("voice_ui.stt_language_label")} hint={t("voice_ui.stt_language_hint")}>
|
|
147
|
+
<UiSelect
|
|
148
|
+
value={custom.language || "auto"}
|
|
149
|
+
onChange={(v) => onPatch({ "transcription.custom.language": v })}
|
|
150
|
+
options={langOptions()}
|
|
151
|
+
disabled={busy}
|
|
152
|
+
/>
|
|
153
|
+
</Field>
|
|
154
|
+
</div>
|
|
155
|
+
<Field label={t("voice_ui.api_key_label")} hint={t("voice_ui.stt_custom_key_hint")}>
|
|
156
|
+
<Input
|
|
157
|
+
type="password"
|
|
158
|
+
autoComplete="new-password"
|
|
159
|
+
defaultValue=""
|
|
160
|
+
placeholder={keyPlaceholder(custom.api_key)}
|
|
161
|
+
onBlur={(e) => patchKey("transcription.custom.api_key", e.target.value)}
|
|
162
|
+
disabled={busy}
|
|
163
|
+
/>
|
|
164
|
+
</Field>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
71
167
|
</div>
|
|
72
168
|
);
|
|
73
169
|
}
|
|
@@ -32,6 +32,11 @@ export const en = {
|
|
|
32
32
|
hide: "Hide",
|
|
33
33
|
copy: "Copy",
|
|
34
34
|
run: "Run",
|
|
35
|
+
pager_prev: "Previous",
|
|
36
|
+
pager_next: "Next",
|
|
37
|
+
pager_page: "Page {page} of {total}",
|
|
38
|
+
pager_range: "{from}–{to} of {total}",
|
|
39
|
+
pager_per_page:"Per page",
|
|
35
40
|
},
|
|
36
41
|
daemon: {
|
|
37
42
|
connecting: "Connecting to the daemon…",
|
|
@@ -940,6 +945,9 @@ export const en = {
|
|
|
940
945
|
reload_manifest: "Reload manifest",
|
|
941
946
|
widget_native: "Native APX widget",
|
|
942
947
|
widget_external: "External widget",
|
|
948
|
+
preview_badge: "Preview",
|
|
949
|
+
preview_title: "Deck — Coming soon",
|
|
950
|
+
preview_body: "The Deck module is still in development and not released yet. We'll re-enable it once Deck ships in a stable release. For now everything here is read-only and no changes will be saved.",
|
|
943
951
|
},
|
|
944
952
|
|
|
945
953
|
memory_panel: {
|
|
@@ -1150,9 +1158,17 @@ export const en = {
|
|
|
1150
1158
|
stt_model_hint: "Bigger = more accurate and slower.",
|
|
1151
1159
|
stt_language_label: "Language",
|
|
1152
1160
|
stt_language_hint: "For Spanish, setting \"Spanish\" improves accuracy.",
|
|
1153
|
-
stt_provider_auto: "Automatic (local, then
|
|
1161
|
+
stt_provider_auto: "Automatic (local, then remote)",
|
|
1154
1162
|
stt_provider_local: "Local — faster-whisper (offline)",
|
|
1155
1163
|
stt_provider_openai: "OpenAI — Whisper-1 (cloud)",
|
|
1164
|
+
stt_provider_custom: "Custom — OpenAI-compatible server",
|
|
1165
|
+
stt_openai_model_label: "OpenAI model",
|
|
1166
|
+
stt_openai_model_hint: "Defaults to whisper-1.",
|
|
1167
|
+
stt_custom_baseurl_label: "Base URL (OpenAI-compatible)",
|
|
1168
|
+
stt_custom_baseurl_hint: "e.g. http://localhost:8000/v1 (mlx-audio on Metal) or http://192.168.1.50:9000/v1 (Radeon/NVIDIA on the LAN).",
|
|
1169
|
+
stt_custom_model_label: "Model",
|
|
1170
|
+
stt_custom_model_hint: "e.g. mlx-community/whisper-large-v3-turbo or large-v3.",
|
|
1171
|
+
stt_custom_key_hint: "Optional — most local servers need no key.",
|
|
1156
1172
|
lang_auto: "Auto-detect",
|
|
1157
1173
|
lang_es: "Spanish",
|
|
1158
1174
|
lang_en: "English",
|
|
@@ -1302,18 +1318,33 @@ export const en = {
|
|
|
1302
1318
|
desktop_pos_left: "Left",
|
|
1303
1319
|
desktop_pos_center: "Center",
|
|
1304
1320
|
desktop_pos_right: "Right",
|
|
1321
|
+
desktop_theme_system: "System",
|
|
1305
1322
|
desktop_theme_light: "Light",
|
|
1306
1323
|
desktop_theme_dark: "Dark",
|
|
1307
1324
|
desktop_status_desc: "The window launches from the terminal or via autostart.",
|
|
1308
1325
|
desktop_running: "Running",
|
|
1309
1326
|
desktop_stopped: "Stopped",
|
|
1310
1327
|
desktop_refresh: "refresh",
|
|
1328
|
+
desktop_start: "Start",
|
|
1329
|
+
desktop_stop: "Stop",
|
|
1330
|
+
desktop_restart: "Restart",
|
|
1331
|
+
desktop_restart_hint: "Reload the open window so config changes (theme, position) apply now.",
|
|
1332
|
+
desktop_restart_done: "Restarting the window — applying the latest config.",
|
|
1333
|
+
desktop_restart_none: "No desktop window is connected.",
|
|
1334
|
+
desktop_start_done: "Desktop window launched.",
|
|
1335
|
+
desktop_start_already: "Desktop window is already running.",
|
|
1336
|
+
desktop_stop_done: "Desktop window stopped.",
|
|
1337
|
+
desktop_stop_none: "No desktop window was running.",
|
|
1311
1338
|
desktop_from_terminal: "From terminal:",
|
|
1312
1339
|
desktop_autostart_desc: "Launches the window at user login. Equivalent to `apx desktop install` (no sudo required).",
|
|
1313
1340
|
desktop_platform: "platform: {platform}",
|
|
1314
1341
|
desktop_shortcut_desc: "Global hotkey that shows/hides the window and starts listening.",
|
|
1315
1342
|
desktop_accelerator: "Accelerator",
|
|
1316
|
-
desktop_accelerator_hint:"
|
|
1343
|
+
desktop_accelerator_hint:"Click the field and press your key combo. Restart the window to apply.",
|
|
1344
|
+
desktop_shortcut_record: "Click to set a shortcut",
|
|
1345
|
+
desktop_shortcut_recording: "Press your combo…",
|
|
1346
|
+
desktop_shortcut_change: "click to change",
|
|
1347
|
+
desktop_shortcut_esc: "Esc to cancel",
|
|
1317
1348
|
desktop_shortcut_saved: "Shortcut saved. Restart the window (apx desktop stop && start) to apply it.",
|
|
1318
1349
|
desktop_autostart_on: "Autostart enabled for the next login.",
|
|
1319
1350
|
desktop_autostart_off: "Autostart disabled.",
|
|
@@ -33,6 +33,11 @@ export const es = {
|
|
|
33
33
|
hide: "Ocultar",
|
|
34
34
|
copy: "Copiar",
|
|
35
35
|
run: "Ejecutar",
|
|
36
|
+
pager_prev: "Anterior",
|
|
37
|
+
pager_next: "Siguiente",
|
|
38
|
+
pager_page: "Página {page} de {total}",
|
|
39
|
+
pager_range: "{from}–{to} de {total}",
|
|
40
|
+
pager_per_page:"Por página",
|
|
36
41
|
},
|
|
37
42
|
daemon: {
|
|
38
43
|
connecting: "Conectando con el daemon…",
|
|
@@ -938,6 +943,9 @@ export const es = {
|
|
|
938
943
|
reload_manifest: "Recargar manifest",
|
|
939
944
|
widget_native: "Widget nativo APX",
|
|
940
945
|
widget_external: "Widget externo",
|
|
946
|
+
preview_badge: "Vista previa",
|
|
947
|
+
preview_title: "Deck — Próximamente",
|
|
948
|
+
preview_body: "El módulo Deck todavía está en desarrollo y no fue lanzado aún. Lo volveremos a activar cuando Deck salga en una versión estable. Por ahora todo acá es de solo lectura y no se guardará ningún cambio.",
|
|
941
949
|
},
|
|
942
950
|
|
|
943
951
|
memory_panel: {
|
|
@@ -1148,9 +1156,17 @@ export const es = {
|
|
|
1148
1156
|
stt_model_hint: "Más grande = más preciso y más lento.",
|
|
1149
1157
|
stt_language_label: "Idioma",
|
|
1150
1158
|
stt_language_hint: "Para español, elegir \"Español\" mejora la precisión.",
|
|
1151
|
-
stt_provider_auto: "Automático (local, después
|
|
1159
|
+
stt_provider_auto: "Automático (local, después remoto)",
|
|
1152
1160
|
stt_provider_local: "Local — faster-whisper (offline)",
|
|
1153
1161
|
stt_provider_openai: "OpenAI — Whisper-1 (cloud)",
|
|
1162
|
+
stt_provider_custom: "Custom — server OpenAI-compatible",
|
|
1163
|
+
stt_openai_model_label: "Modelo OpenAI",
|
|
1164
|
+
stt_openai_model_hint: "Por defecto whisper-1.",
|
|
1165
|
+
stt_custom_baseurl_label: "URL base (OpenAI-compatible)",
|
|
1166
|
+
stt_custom_baseurl_hint: "Ej: http://localhost:8000/v1 (mlx-audio en Metal) o http://192.168.1.50:9000/v1 (Radeon/NVIDIA en la red).",
|
|
1167
|
+
stt_custom_model_label: "Modelo",
|
|
1168
|
+
stt_custom_model_hint: "Ej: mlx-community/whisper-large-v3-turbo o large-v3.",
|
|
1169
|
+
stt_custom_key_hint: "Opcional — la mayoría de los servers locales no requieren key.",
|
|
1154
1170
|
lang_auto: "Detección automática",
|
|
1155
1171
|
lang_es: "Español",
|
|
1156
1172
|
lang_en: "Inglés",
|
|
@@ -1300,18 +1316,33 @@ export const es = {
|
|
|
1300
1316
|
desktop_pos_left: "Izquierda",
|
|
1301
1317
|
desktop_pos_center: "Centro",
|
|
1302
1318
|
desktop_pos_right: "Derecha",
|
|
1319
|
+
desktop_theme_system: "Sistema",
|
|
1303
1320
|
desktop_theme_light: "Claro",
|
|
1304
1321
|
desktop_theme_dark: "Oscuro",
|
|
1305
1322
|
desktop_status_desc: "La ventana se abre desde la terminal o por arranque automático.",
|
|
1306
1323
|
desktop_running: "Corriendo",
|
|
1307
1324
|
desktop_stopped: "Detenida",
|
|
1308
1325
|
desktop_refresh: "refrescar",
|
|
1326
|
+
desktop_start: "Iniciar",
|
|
1327
|
+
desktop_stop: "Detener",
|
|
1328
|
+
desktop_restart: "Reiniciar",
|
|
1329
|
+
desktop_restart_hint: "Recarga la ventana abierta para aplicar ya los cambios de config (tema, posición).",
|
|
1330
|
+
desktop_restart_done: "Reiniciando la ventana — aplicando la última config.",
|
|
1331
|
+
desktop_restart_none: "No hay ninguna ventana de desktop conectada.",
|
|
1332
|
+
desktop_start_done: "Ventana de desktop iniciada.",
|
|
1333
|
+
desktop_start_already: "La ventana de desktop ya estaba corriendo.",
|
|
1334
|
+
desktop_stop_done: "Ventana de desktop detenida.",
|
|
1335
|
+
desktop_stop_none: "No había ninguna ventana de desktop corriendo.",
|
|
1309
1336
|
desktop_from_terminal: "Desde la terminal:",
|
|
1310
1337
|
desktop_autostart_desc: "Abre la ventana al iniciar sesión. Equivale a `apx desktop install` (no requiere sudo).",
|
|
1311
1338
|
desktop_platform: "plataforma: {platform}",
|
|
1312
1339
|
desktop_shortcut_desc: "Atajo global que muestra/oculta la ventana y empieza a escuchar.",
|
|
1313
1340
|
desktop_accelerator: "Acelerador",
|
|
1314
|
-
desktop_accelerator_hint:"
|
|
1341
|
+
desktop_accelerator_hint:"Hacé clic en el campo y apretá tu combinación de teclas. Reiniciá la ventana para aplicar.",
|
|
1342
|
+
desktop_shortcut_record: "Hacé clic para definir un atajo",
|
|
1343
|
+
desktop_shortcut_recording: "Apretá tu combinación…",
|
|
1344
|
+
desktop_shortcut_change: "clic para cambiar",
|
|
1345
|
+
desktop_shortcut_esc: "Esc para cancelar",
|
|
1315
1346
|
desktop_shortcut_saved: "Atajo guardado. Reiniciá la ventana (apx desktop stop && start) para aplicarlo.",
|
|
1316
1347
|
desktop_autostart_on: "Arranque automático habilitado para el próximo inicio de sesión.",
|
|
1317
1348
|
desktop_autostart_off: "Arranque automático deshabilitado.",
|
|
@@ -16,6 +16,25 @@ export interface DesktopStatus {
|
|
|
16
16
|
running: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface DesktopRestartResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
/** How many connected windows were told to reload (0 = none connected). */
|
|
22
|
+
reloaded: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DesktopStartResult {
|
|
26
|
+
ok: boolean;
|
|
27
|
+
pid?: number;
|
|
28
|
+
/** True when a window was already running (start was a no-op). */
|
|
29
|
+
already?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DesktopStopResult {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
/** False when nothing was running. */
|
|
35
|
+
stopped?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export interface AutostartStatus {
|
|
20
39
|
ok: boolean;
|
|
21
40
|
enabled: boolean;
|
|
@@ -26,6 +45,15 @@ export const Desktop = {
|
|
|
26
45
|
/** GET /desktop/status — connected window count + running flag (live probe). */
|
|
27
46
|
status: () => http.get<DesktopStatus>("/desktop/status"),
|
|
28
47
|
|
|
48
|
+
/** POST /desktop/start — launch the floating window (detached Electron). */
|
|
49
|
+
start: () => http.post<DesktopStartResult>("/desktop/start", {}),
|
|
50
|
+
|
|
51
|
+
/** POST /desktop/stop — terminate the running window. */
|
|
52
|
+
stop: () => http.post<DesktopStopResult>("/desktop/stop", {}),
|
|
53
|
+
|
|
54
|
+
/** POST /desktop/restart — tell every live window to reload + re-read config. */
|
|
55
|
+
restart: () => http.post<DesktopRestartResult>("/desktop/restart", {}),
|
|
56
|
+
|
|
29
57
|
/** GET /desktop/autostart — current login-item state for this platform. */
|
|
30
58
|
autostartGet: () => http.get<AutostartStatus>("/desktop/autostart"),
|
|
31
59
|
|
|
@@ -99,9 +99,33 @@ export interface TranscriptionLocalConfig {
|
|
|
99
99
|
beam_size?: number;
|
|
100
100
|
idle_minutes?: number;
|
|
101
101
|
}
|
|
102
|
+
export interface TranscriptionOpenAIConfig {
|
|
103
|
+
base_url?: string; // defaults to https://api.openai.com/v1
|
|
104
|
+
api_key?: string; // may carry a redacted "*** set ***" marker
|
|
105
|
+
model?: string; // defaults to whisper-1
|
|
106
|
+
}
|
|
107
|
+
export interface TranscriptionCustomConfig {
|
|
108
|
+
base_url?: string; // OpenAI-compatible server, e.g. http://localhost:8000/v1
|
|
109
|
+
api_key?: string; // optional; may carry a redacted marker
|
|
110
|
+
model?: string; // e.g. mlx-community/whisper-large-v3-turbo
|
|
111
|
+
language?: string; // ISO code or "auto"
|
|
112
|
+
}
|
|
102
113
|
export interface TranscriptionConfig {
|
|
103
|
-
provider?: string; // "auto" | "local" | "openai"
|
|
114
|
+
provider?: string; // "auto" | "local" | "openai" | "custom"
|
|
104
115
|
local?: TranscriptionLocalConfig;
|
|
116
|
+
openai?: TranscriptionOpenAIConfig;
|
|
117
|
+
custom?: TranscriptionCustomConfig;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One STT engine entry as reported by GET /transcribe/providers. */
|
|
121
|
+
export interface SttProviderEntry {
|
|
122
|
+
id: string; // "local" | "openai" | "custom"
|
|
123
|
+
available: boolean;
|
|
124
|
+
configured: boolean;
|
|
125
|
+
}
|
|
126
|
+
export interface SttProvidersResponse {
|
|
127
|
+
configured_provider: string;
|
|
128
|
+
engines: SttProviderEntry[];
|
|
105
129
|
}
|
|
106
130
|
|
|
107
131
|
// Known engine voice presets used to fill selects without a daemon round-trip.
|
|
@@ -109,7 +133,7 @@ export const OPENAI_TTS_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shi
|
|
|
109
133
|
export const GEMINI_TTS_VOICES = ["Kore", "Puck", "Charon", "Fenrir", "Aoede"];
|
|
110
134
|
export const ELEVENLABS_MODELS = ["eleven_multilingual_v2", "eleven_turbo_v2_5", "eleven_flash_v2_5"];
|
|
111
135
|
export const OPENAI_TTS_MODELS = ["tts-1", "tts-1-hd"];
|
|
112
|
-
export const WHISPER_MODELS = ["tiny", "base", "small", "medium", "large-v2", "large-v3"];
|
|
136
|
+
export const WHISPER_MODELS = ["tiny", "base", "small", "medium", "large-v2", "large-v3", "large-v3-turbo"];
|
|
113
137
|
|
|
114
138
|
// Friendly labels + ordering for the provider list. The daemon is the source
|
|
115
139
|
// of truth for availability; this only adds display names + a stable order.
|