@openparachute/vault 0.6.5-rc.2 → 0.6.5-rc.9
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/core/src/do-param-cap.test.ts +161 -0
- package/core/src/links.ts +8 -13
- package/core/src/notes.ts +33 -15
- package/core/src/onboarding.ts +14 -296
- package/core/src/seed-packs.test.ts +191 -0
- package/core/src/seed-packs.ts +559 -0
- package/core/src/sql-in.test.ts +58 -0
- package/core/src/sql-in.ts +76 -0
- package/core/src/transcription/provider.ts +141 -0
- package/core/src/vault-projection.ts +1 -1
- package/core/src/wikilinks.ts +10 -4
- package/package.json +1 -1
- package/src/add-pack.test.ts +142 -0
- package/src/cli.ts +542 -7
- package/src/onboarding-seed.test.ts +126 -40
- package/src/onboarding-seed.ts +41 -46
- package/src/routes.ts +17 -0
- package/src/server.ts +77 -31
- package/src/transcription/build.test.ts +224 -0
- package/src/transcription/build.ts +252 -0
- package/src/transcription/capability.test.ts +93 -0
- package/src/transcription/capability.ts +71 -0
- package/src/transcription/install.test.ts +167 -0
- package/src/transcription/install.ts +296 -0
- package/src/transcription/providers/scribe-http.test.ts +195 -0
- package/src/transcription/providers/scribe-http.ts +144 -0
- package/src/transcription/providers/transcribe-cpp.test.ts +314 -0
- package/src/transcription/providers/transcribe-cpp.ts +293 -0
- package/src/transcription/select.test.ts +134 -0
- package/src/transcription/select.ts +164 -0
- package/src/transcription-worker.test.ts +44 -0
- package/src/transcription-worker.ts +57 -122
- package/src/vault-create.test.ts +38 -10
- package/src/vault.test.ts +48 -0
|
@@ -55,9 +55,14 @@ import { join, normalize } from "path";
|
|
|
55
55
|
import { existsSync, readFileSync, unlinkSync } from "fs";
|
|
56
56
|
import type { Store, Attachment, Note } from "../core/src/types.ts";
|
|
57
57
|
import type { HookRegistry } from "../core/src/hooks.ts";
|
|
58
|
-
import {
|
|
58
|
+
import { fetchContextEntries, type ContextPayload } from "./context.ts";
|
|
59
59
|
import type { TriggerIncludeContext } from "./config.ts";
|
|
60
60
|
import { upsertTranscriptNote } from "./transcript-note.ts";
|
|
61
|
+
import {
|
|
62
|
+
TranscriptionError,
|
|
63
|
+
type TranscriptionProvider,
|
|
64
|
+
} from "../core/src/transcription/provider.ts";
|
|
65
|
+
import { ScribeHttpProvider } from "./transcription/providers/scribe-http.ts";
|
|
61
66
|
|
|
62
67
|
/** Placeholder pattern written by the voice-memo capture stub. */
|
|
63
68
|
const TRANSCRIPT_PLACEHOLDER = /_Transcript pending\._/;
|
|
@@ -118,8 +123,12 @@ export interface TranscriptionWorkerOpts {
|
|
|
118
123
|
vaultList: () => string[];
|
|
119
124
|
/** Get a store for a vault name. */
|
|
120
125
|
getStore: (name: string) => Store;
|
|
121
|
-
/**
|
|
122
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Scribe base URL (no trailing slash). Only used to build the default
|
|
128
|
+
* `scribe-http` provider when no `provider` is injected — a `transcribe-cpp`
|
|
129
|
+
* (or any injected) provider needs no scribe URL, so this is optional.
|
|
130
|
+
*/
|
|
131
|
+
scribeUrl?: string;
|
|
123
132
|
/** Optional bearer token for scribe. */
|
|
124
133
|
scribeToken?: string;
|
|
125
134
|
/** Resolve the assets root for a vault name. */
|
|
@@ -138,6 +147,15 @@ export interface TranscriptionWorkerOpts {
|
|
|
138
147
|
maxAttempts?: number;
|
|
139
148
|
timeoutMs?: number;
|
|
140
149
|
fetchImpl?: typeof fetch;
|
|
150
|
+
/**
|
|
151
|
+
* Transcription provider (scribe-fold Phase 1). When omitted, a behavior-
|
|
152
|
+
* preserving `scribe-http` provider is built from `scribeUrl` / `scribeToken`
|
|
153
|
+
* / `timeoutMs` / `fetchImpl` — reproducing exactly the former `callScribe`.
|
|
154
|
+
* Phase 2+ (local-ASR backends, the cloud Workers-AI provider) inject one
|
|
155
|
+
* here; the worker's queue/backoff/retry/transcript-note/retention logic is
|
|
156
|
+
* unchanged regardless.
|
|
157
|
+
*/
|
|
158
|
+
provider?: TranscriptionProvider;
|
|
141
159
|
logger?: { info?: (...args: unknown[]) => void; error: (...args: unknown[]) => void };
|
|
142
160
|
}
|
|
143
161
|
|
|
@@ -180,25 +198,6 @@ interface PendingMeta {
|
|
|
180
198
|
[k: string]: unknown;
|
|
181
199
|
}
|
|
182
200
|
|
|
183
|
-
/**
|
|
184
|
-
* Structured error thrown when scribe returns a 4xx with a recognized
|
|
185
|
-
* `error_code` — we surface the code on the transcript note's frontmatter
|
|
186
|
-
* so callers can branch on stable strings instead of regex-matching message
|
|
187
|
-
* text. Today the canonical code is `missing_provider` (scribe#47).
|
|
188
|
-
*/
|
|
189
|
-
class ScribeApiError extends Error {
|
|
190
|
-
readonly errorCode?: string;
|
|
191
|
-
readonly httpStatus: number;
|
|
192
|
-
readonly retriable: boolean;
|
|
193
|
-
constructor(message: string, opts: { errorCode?: string; httpStatus: number; retriable: boolean }) {
|
|
194
|
-
super(message);
|
|
195
|
-
this.name = "ScribeApiError";
|
|
196
|
-
this.errorCode = opts.errorCode;
|
|
197
|
-
this.httpStatus = opts.httpStatus;
|
|
198
|
-
this.retriable = opts.retriable;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
201
|
/**
|
|
203
202
|
* Start the worker loop. Returns a handle with `stop()` + `tick()`.
|
|
204
203
|
* Tests should build the worker and call `tick()` directly; production
|
|
@@ -217,6 +216,18 @@ export function startTranscriptionWorker(opts: TranscriptionWorkerOpts): Transcr
|
|
|
217
216
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
218
217
|
const retentionFor = opts.getAudioRetention ?? (() => "keep" as const);
|
|
219
218
|
|
|
219
|
+
// Resolve the transcription provider (scribe-fold Phase 1). Default: the
|
|
220
|
+
// behavior-preserving `scribe-http` provider built from the same
|
|
221
|
+
// scribeUrl/token/timeout/fetch the worker was already constructed with —
|
|
222
|
+
// so existing installs transcribe byte-identically. Callers may inject a
|
|
223
|
+
// different provider without touching any of the queue/retry logic below.
|
|
224
|
+
const provider: TranscriptionProvider = opts.provider ?? new ScribeHttpProvider({
|
|
225
|
+
url: opts.scribeUrl,
|
|
226
|
+
token: opts.scribeToken,
|
|
227
|
+
timeoutMs,
|
|
228
|
+
fetchImpl,
|
|
229
|
+
});
|
|
230
|
+
|
|
220
231
|
let stopped = false;
|
|
221
232
|
let inflight: Promise<void> = Promise.resolve();
|
|
222
233
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -474,30 +485,35 @@ export function startTranscriptionWorker(opts: TranscriptionWorkerOpts): Transcr
|
|
|
474
485
|
|
|
475
486
|
let scribeResult: { text: string; durationMs: number };
|
|
476
487
|
try {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
488
|
+
// Read the audio + call the provider inside this try so a read failure
|
|
489
|
+
// (a race deleting the file between existsSync and read) is handled the
|
|
490
|
+
// same way as a transcription failure — as a retriable error — exactly
|
|
491
|
+
// as the former `callScribe` (which read the file internally) did. The
|
|
492
|
+
// worker owns the wall-clock timing (`durationMs`); the provider owns
|
|
493
|
+
// only the audio→text step.
|
|
494
|
+
const audio = readFileSync(filePath);
|
|
495
|
+
const startedAt = Date.now();
|
|
496
|
+
const result = await provider.transcribe({
|
|
497
|
+
audio,
|
|
481
498
|
filename: attachment.path.split("/").pop() ?? "audio",
|
|
482
499
|
mimeType: attachment.mimeType,
|
|
483
500
|
context,
|
|
484
|
-
timeoutMs,
|
|
485
|
-
fetchImpl,
|
|
486
501
|
});
|
|
502
|
+
scribeResult = { text: result.text, durationMs: Date.now() - startedAt };
|
|
487
503
|
} catch (err) {
|
|
488
504
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
489
|
-
const apiErr = err instanceof
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
505
|
+
const apiErr = err instanceof TranscriptionError ? err : null;
|
|
506
|
+
// A non-retriable provider error (a 4xx the operator must fix — no
|
|
507
|
+
// provider configured, bad auth) is terminal immediately. Re-POSTing the
|
|
508
|
+
// same audio would keep failing; retries don't help. This is the
|
|
509
|
+
// "graceful first-boot path" from design Q5.
|
|
494
510
|
const nonRetriable = apiErr !== null && !apiErr.retriable;
|
|
495
511
|
const nextAttempts = attempts + 1;
|
|
496
512
|
const terminal = nonRetriable || nextAttempts >= maxAttempts;
|
|
497
513
|
|
|
498
514
|
if (terminal) {
|
|
499
515
|
if (nonRetriable) {
|
|
500
|
-
logger.error(`[transcribe] non-retriable
|
|
516
|
+
logger.error(`[transcribe] non-retriable provider error on attachment ${attachment.id} (status ${apiErr!.httpStatus ?? "n/a"}${apiErr!.code ? `, ${apiErr!.code}` : ""}):`, errMsg);
|
|
501
517
|
} else {
|
|
502
518
|
logger.error(`[transcribe] giving up on attachment ${attachment.id} after ${nextAttempts} attempts:`, errMsg);
|
|
503
519
|
}
|
|
@@ -506,10 +522,10 @@ export function startTranscriptionWorker(opts: TranscriptionWorkerOpts): Transcr
|
|
|
506
522
|
transcribe_status: "failed",
|
|
507
523
|
transcribe_attempts: nextAttempts,
|
|
508
524
|
transcribe_error: errMsg,
|
|
509
|
-
...(apiErr?.
|
|
525
|
+
...(apiErr?.code ? { transcribe_error_code: apiErr.code } : {}),
|
|
510
526
|
});
|
|
511
527
|
if (isAutoOrigin) {
|
|
512
|
-
await writeFailureTranscriptNote(store, attachment, errMsg, apiErr?.
|
|
528
|
+
await writeFailureTranscriptNote(store, attachment, errMsg, apiErr?.code, undefined);
|
|
513
529
|
} else {
|
|
514
530
|
await applyFailureMarker(store, attachment.noteId);
|
|
515
531
|
}
|
|
@@ -744,90 +760,9 @@ export function registerTranscriptionHook(
|
|
|
744
760
|
}
|
|
745
761
|
|
|
746
762
|
/**
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
* Failure modes (encoded as throws):
|
|
753
|
-
* - 4xx with a JSON body carrying `error_code`: throws `ScribeApiError`
|
|
754
|
-
* with the code (`missing_provider` etc.). Treated as a non-retriable
|
|
755
|
-
* terminal failure — re-POSTing the same audio at the same broken scribe
|
|
756
|
-
* would just fail the same way; the operator has to act.
|
|
757
|
-
* - 4xx without `error_code` (auth, malformed multipart): throws
|
|
758
|
-
* `ScribeApiError` with the body text. Non-retriable.
|
|
759
|
-
* - 5xx, network error, or timeout: throws a plain `Error`. Retriable —
|
|
760
|
-
* the worker's backoff path picks it up.
|
|
761
|
-
* - 200 with missing/invalid `text` field: throws a plain `Error`.
|
|
762
|
-
* Retriable (could be a transient provider-output glitch).
|
|
763
|
-
*/
|
|
764
|
-
async function callScribe(args: {
|
|
765
|
-
url: string;
|
|
766
|
-
token?: string;
|
|
767
|
-
filePath: string;
|
|
768
|
-
filename: string;
|
|
769
|
-
mimeType: string;
|
|
770
|
-
context: ContextPayload | null;
|
|
771
|
-
timeoutMs: number;
|
|
772
|
-
fetchImpl: typeof fetch;
|
|
773
|
-
}): Promise<{ text: string; durationMs: number }> {
|
|
774
|
-
const controller = new AbortController();
|
|
775
|
-
const timer = setTimeout(() => controller.abort(), args.timeoutMs);
|
|
776
|
-
const startedAt = Date.now();
|
|
777
|
-
try {
|
|
778
|
-
const fileBuffer = readFileSync(args.filePath);
|
|
779
|
-
const file = new File([fileBuffer], args.filename, { type: args.mimeType });
|
|
780
|
-
const form = new FormData();
|
|
781
|
-
form.append("file", file);
|
|
782
|
-
if (args.context) appendContextPart(form, args.context);
|
|
783
|
-
|
|
784
|
-
const endpoint = `${args.url.replace(/\/$/, "")}/v1/audio/transcriptions`;
|
|
785
|
-
const headers: Record<string, string> = {};
|
|
786
|
-
if (args.token) headers["Authorization"] = `Bearer ${args.token}`;
|
|
787
|
-
|
|
788
|
-
const resp = await args.fetchImpl(endpoint, {
|
|
789
|
-
method: "POST",
|
|
790
|
-
headers,
|
|
791
|
-
body: form,
|
|
792
|
-
signal: controller.signal,
|
|
793
|
-
});
|
|
794
|
-
if (!resp.ok) {
|
|
795
|
-
const body = await resp.text().catch(() => "");
|
|
796
|
-
// Try to extract structured error_code from JSON body (scribe#47).
|
|
797
|
-
let errorCode: string | undefined;
|
|
798
|
-
let errorMessage: string | undefined;
|
|
799
|
-
try {
|
|
800
|
-
const parsed = JSON.parse(body) as { error?: string; error_code?: string; message?: string };
|
|
801
|
-
if (typeof parsed.error_code === "string") errorCode = parsed.error_code;
|
|
802
|
-
if (typeof parsed.error === "string") errorMessage = parsed.error;
|
|
803
|
-
else if (typeof parsed.message === "string") errorMessage = parsed.message;
|
|
804
|
-
} catch {
|
|
805
|
-
// Not JSON — leave errorCode undefined; the raw body becomes the message.
|
|
806
|
-
}
|
|
807
|
-
// 4xx is terminal (re-POSTing the same audio at the same broken scribe
|
|
808
|
-
// will just fail again). 5xx is retriable — provider hiccup, will likely
|
|
809
|
-
// succeed on backoff.
|
|
810
|
-
const retriable = resp.status >= 500;
|
|
811
|
-
const message = errorMessage
|
|
812
|
-
?? (errorCode ? `scribe ${errorCode}` : `scribe returned ${resp.status}: ${body}`);
|
|
813
|
-
throw new ScribeApiError(message, {
|
|
814
|
-
errorCode,
|
|
815
|
-
httpStatus: resp.status,
|
|
816
|
-
retriable,
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
const result = await resp.json() as { text?: string };
|
|
820
|
-
if (typeof result.text !== "string") {
|
|
821
|
-
throw new Error("scribe response missing text field");
|
|
822
|
-
}
|
|
823
|
-
return { text: result.text, durationMs: Date.now() - startedAt };
|
|
824
|
-
} finally {
|
|
825
|
-
clearTimeout(timer);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/**
|
|
830
|
-
* Re-export the structured error type so tests + callers can `instanceof`-check
|
|
831
|
-
* for terminal-failure semantics.
|
|
763
|
+
* Re-export the structured provider error so tests + callers can
|
|
764
|
+
* `instanceof`-check for terminal-failure semantics. The scribe HTTP call
|
|
765
|
+
* itself now lives in `src/transcription/providers/scribe-http.ts` behind the
|
|
766
|
+
* `TranscriptionProvider` seam (scribe-fold Phase 1).
|
|
832
767
|
*/
|
|
833
|
-
export {
|
|
768
|
+
export { TranscriptionError };
|
package/src/vault-create.test.ts
CHANGED
|
@@ -355,13 +355,16 @@ describe("vault create — services.json registration (#208)", () => {
|
|
|
355
355
|
});
|
|
356
356
|
|
|
357
357
|
/**
|
|
358
|
-
*
|
|
358
|
+
* Default-pack seeding on create (originally demo-prep Workstream A — A1/A3;
|
|
359
|
+
* reshaped for named seed packs).
|
|
359
360
|
*
|
|
360
|
-
* A freshly-created vault must contain the `
|
|
361
|
-
*
|
|
362
|
-
*
|
|
361
|
+
* A freshly-created vault must contain the `welcome` pack (three-note welcome
|
|
362
|
+
* web + the one capture tag Notes requires) and the `getting-started` guide — and
|
|
363
|
+
* NOT the `surface-starter` pack, which is opt-in via `add-pack` (ratified
|
|
364
|
+
* 2026-07-02). Idempotent + best-effort: the seed never fails a create and
|
|
365
|
+
* never clobbers an edited note.
|
|
363
366
|
*/
|
|
364
|
-
describe("vault create —
|
|
367
|
+
describe("vault create — default pack seeding (welcome + getting-started)", () => {
|
|
365
368
|
/** Read all (path, content) rows from a created vault's SQLite DB. */
|
|
366
369
|
function readNotes(name: string): { path: string | null; content: string }[] {
|
|
367
370
|
const dbPath = join(home, "vault", "data", name, "vault.db");
|
|
@@ -375,7 +378,20 @@ describe("vault create — onboarding guide seeding (A1/A3)", () => {
|
|
|
375
378
|
}
|
|
376
379
|
}
|
|
377
380
|
|
|
378
|
-
|
|
381
|
+
/** Read all tag names from a created vault's SQLite DB. */
|
|
382
|
+
function readTagNames(name: string): string[] {
|
|
383
|
+
const dbPath = join(home, "vault", "data", name, "vault.db");
|
|
384
|
+
const db = new Database(dbPath, { readonly: true });
|
|
385
|
+
try {
|
|
386
|
+
return (db.query("SELECT name FROM tags").all() as { name: string }[]).map(
|
|
387
|
+
(r) => r.name,
|
|
388
|
+
);
|
|
389
|
+
} finally {
|
|
390
|
+
db.close();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
test("create seeds the welcome web + Getting Started — and NOT Surface Starter", () => {
|
|
379
395
|
const { exitCode } = runCli(["create", "guided", "--json"], {
|
|
380
396
|
PARACHUTE_HOME: home,
|
|
381
397
|
});
|
|
@@ -383,12 +399,24 @@ describe("vault create — onboarding guide seeding (A1/A3)", () => {
|
|
|
383
399
|
|
|
384
400
|
const notes = readNotes("guided");
|
|
385
401
|
const gs = notes.find((n) => n.path === "Getting Started");
|
|
386
|
-
const
|
|
402
|
+
const welcome = notes.find((n) => n.path === "Welcome to your vault 🪂");
|
|
403
|
+
const tryLinking = notes.find((n) => n.path === "Try linking notes");
|
|
404
|
+
const connectAi = notes.find((n) => n.path === "Connect your AI");
|
|
387
405
|
expect(gs).toBeDefined();
|
|
388
|
-
expect(
|
|
406
|
+
expect(welcome).toBeDefined();
|
|
407
|
+
expect(tryLinking).toBeDefined();
|
|
408
|
+
expect(connectAi).toBeDefined();
|
|
389
409
|
expect(gs!.content).toContain("# Getting Started");
|
|
390
|
-
|
|
391
|
-
expect(
|
|
410
|
+
// Surface Starter is out of the default seed — no note, no dangling link.
|
|
411
|
+
expect(notes.find((n) => n.path === "Surface Starter")).toBeUndefined();
|
|
412
|
+
expect(gs!.content).not.toContain("[[Surface Starter]]");
|
|
413
|
+
expect(gs!.content).toContain("add-pack surface-starter");
|
|
414
|
+
|
|
415
|
+
// The ONE capture tag Notes requires arrives with the welcome pack —
|
|
416
|
+
// and nothing else (fresh-vault seed = 4 notes + 1 tag; the retired
|
|
417
|
+
// capture/text + capture/voice subtypes are no longer seeded).
|
|
418
|
+
expect(readTagNames("guided")).toEqual(["capture"]);
|
|
419
|
+
expect(notes).toHaveLength(4);
|
|
392
420
|
});
|
|
393
421
|
|
|
394
422
|
test("seeding doesn't break --json stdout (notes seeded silently)", () => {
|
package/src/vault.test.ts
CHANGED
|
@@ -6505,3 +6505,51 @@ describe("handleVault: auto_transcribe (per-vault)", async () => {
|
|
|
6505
6505
|
|
|
6506
6506
|
});
|
|
6507
6507
|
|
|
6508
|
+
describe("handleVault: transcription capability (scribe-fold Phase 1)", async () => {
|
|
6509
|
+
test("GET surfaces transcription: { enabled, provider } when a provider is available", async () => {
|
|
6510
|
+
const cfg = { name: "default" } as { name: string };
|
|
6511
|
+
const res = await handleVault(
|
|
6512
|
+
mkReq("GET", "/vault"),
|
|
6513
|
+
store,
|
|
6514
|
+
cfg as any,
|
|
6515
|
+
undefined,
|
|
6516
|
+
async () => ({ enabled: true, provider: "scribe-http" }),
|
|
6517
|
+
);
|
|
6518
|
+
expect(res.status).toBe(200);
|
|
6519
|
+
const body = await res.json() as any;
|
|
6520
|
+
expect(body.transcription).toEqual({ enabled: true, provider: "scribe-http" });
|
|
6521
|
+
});
|
|
6522
|
+
|
|
6523
|
+
test("GET surfaces transcription.enabled=false when no provider is available (no crash)", async () => {
|
|
6524
|
+
const cfg = { name: "default" } as { name: string };
|
|
6525
|
+
const res = await handleVault(
|
|
6526
|
+
mkReq("GET", "/vault"),
|
|
6527
|
+
store,
|
|
6528
|
+
cfg as any,
|
|
6529
|
+
undefined,
|
|
6530
|
+
async () => ({ enabled: false }),
|
|
6531
|
+
);
|
|
6532
|
+
expect(res.status).toBe(200);
|
|
6533
|
+
const body = await res.json() as any;
|
|
6534
|
+
expect(body.transcription).toEqual({ enabled: false });
|
|
6535
|
+
expect(body.transcription.provider).toBeUndefined();
|
|
6536
|
+
});
|
|
6537
|
+
|
|
6538
|
+
test("capability is a distinct axis from the auto_transcribe policy toggle", async () => {
|
|
6539
|
+
// A vault with auto_transcribe.enabled=true but NO provider available still
|
|
6540
|
+
// reports transcription.enabled=false — the mic should be gated on
|
|
6541
|
+
// capability, not policy.
|
|
6542
|
+
const cfg = { name: "default", auto_transcribe: { enabled: true } };
|
|
6543
|
+
const res = await handleVault(
|
|
6544
|
+
mkReq("GET", "/vault"),
|
|
6545
|
+
store,
|
|
6546
|
+
cfg as any,
|
|
6547
|
+
undefined,
|
|
6548
|
+
async () => ({ enabled: false }),
|
|
6549
|
+
);
|
|
6550
|
+
const body = await res.json() as any;
|
|
6551
|
+
expect(body.config.auto_transcribe.enabled).toBe(true);
|
|
6552
|
+
expect(body.transcription.enabled).toBe(false);
|
|
6553
|
+
});
|
|
6554
|
+
});
|
|
6555
|
+
|