@wirecat/cli-core 0.551.0 → 0.552.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/CHANGELOG.md +59 -0
- package/README.md +1 -1
- package/dist/background/schedules.d.ts +12 -1
- package/dist/background/schedules.js +65 -24
- package/dist/background/units.d.ts +16 -9
- package/dist/background/units.js +26 -11
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/meeting-preparation-answer.d.ts +3 -0
- package/dist/cli/meeting-preparation-projects.d.ts +3 -0
- package/dist/cli/meeting-preparation-read.d.ts +3 -0
- package/dist/cli/messenger/search-answers.js +8 -1
- package/dist/cli/messenger/serve-catch-up.d.ts +43 -0
- package/dist/cli/messenger/serve-catch-up.js +90 -0
- package/dist/cli/messenger/serve-command.js +54 -1
- package/dist/cli/messenger/server-command.d.ts +4 -3
- package/dist/cli/messenger/server-command.js +30 -12
- package/dist/cli/program.js +9 -4
- package/dist/cli/schedule/schedule-command.js +5 -1
- package/dist/entities/hit.d.ts +5 -0
- package/dist/entities/hit.js +5 -0
- package/dist/entities/index.d.ts +27 -0
- package/dist/entities/listener.d.ts +6 -0
- package/dist/entities/listener.js +1 -0
- package/dist/entities/meeting-preparation.d.ts +3 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/mcp/meeting-preparation.d.ts +3 -0
- package/dist/meetings/answers.d.ts +5 -0
- package/dist/meetings/answers.js +8 -0
- package/dist/meetings/cli/index.js +14 -0
- package/dist/meetings/index.d.ts +2 -1
- package/dist/meetings/index.js +1 -0
- package/dist/meetings/ingest.js +2 -1
- package/dist/meetings/language.d.ts +5 -0
- package/dist/meetings/language.js +57 -0
- package/dist/meetings/mcp/search.d.ts +3 -0
- package/dist/meetings/project-evidence-answer.d.ts +3 -0
- package/dist/meetings/result-items.d.ts +9 -0
- package/dist/meetings/result-items.js +7 -0
- package/dist/meetings/store.d.ts +22 -0
- package/dist/meetings/testing/memory.js +3 -0
- package/dist/meetings/transcription.d.ts +2 -0
- package/dist/meetings/transcription.js +5 -1
- package/dist/search/lucene/types.d.ts +7 -0
- package/dist/search/lucene/types.js +11 -1
- package/dist/services/messages-archive-search.js +3 -2
- package/dist/services/messages-combined.js +2 -2
- package/dist/services/messages-discovery.js +2 -2
- package/dist/services/notes-search.js +4 -5
- package/dist/services/search-all-meetings.d.ts +3 -0
- package/dist/services/search-all-meetings.js +6 -1
- package/dist/speech/local.d.ts +2 -0
- package/dist/speech/local.js +8 -1
- package/dist/speech/models.d.ts +2 -0
- package/dist/speech/models.js +2 -0
- package/dist/store/driver.d.ts +11 -0
- package/dist/store/driver.js +13 -0
- package/dist/store/drivers/node-sqlite.d.ts +1 -1
- package/dist/store/drivers/node-sqlite.js +3 -0
- package/dist/store/sqlite/lucene.d.ts +1 -1
- package/dist/store/sqlite/lucene.js +33 -7
- package/dist/store/sqlite/meeting-hit-search.js +10 -3
- package/dist/store/sqlite/meeting-languages.d.ts +4 -0
- package/dist/store/sqlite/meeting-languages.js +36 -0
- package/dist/store/sqlite/meetings.d.ts +2 -1
- package/dist/store/sqlite/meetings.js +9 -3
- package/dist/store/sqlite/note-search.js +2 -2
- package/package.json +1 -1
|
@@ -116,10 +116,17 @@ const embedding = v.union([
|
|
|
116
116
|
}),
|
|
117
117
|
}),
|
|
118
118
|
]);
|
|
119
|
+
const repair = v.strictObject({
|
|
120
|
+
account_id: identifier,
|
|
121
|
+
applied: flag,
|
|
122
|
+
transcripts: count,
|
|
123
|
+
languages: v.array(v.strictObject({ language: nullableText, transcripts: count })),
|
|
124
|
+
});
|
|
119
125
|
export const MEETING_RESULT_ENTITIES = {
|
|
120
126
|
meeting_ingestion: run,
|
|
121
127
|
meeting_export: exported,
|
|
122
128
|
meeting_association: meetingChange,
|
|
123
129
|
meeting_transcription: transcription,
|
|
124
130
|
meeting_embedding: embedding,
|
|
131
|
+
meeting_repair: repair,
|
|
125
132
|
};
|
package/dist/meetings/store.d.ts
CHANGED
|
@@ -90,6 +90,11 @@ export interface SearchHit {
|
|
|
90
90
|
startMs: number | null;
|
|
91
91
|
/** Who spoke a transcript hit's cue, as the transcript names them; absent for chat and summary hits. */
|
|
92
92
|
speakerName?: string | null;
|
|
93
|
+
/** A transcript hit's transcript language (`ru`, `en`…), `null` when unknown; absent for chat and summary hits. */
|
|
94
|
+
language?: string | null;
|
|
95
|
+
/** A transcript hit's line, from 1, of `linesTotal` lines in its transcript: where in the meeting it was said. */
|
|
96
|
+
line?: number;
|
|
97
|
+
linesTotal?: number;
|
|
93
98
|
/** `corrected` when a query word matched only as a stored word recognition may have garbled. */
|
|
94
99
|
match?: "words" | "corrected";
|
|
95
100
|
/** For a `corrected` hit, the stored words each such query word matched in this passage. */
|
|
@@ -126,4 +131,21 @@ export interface MeetingStore {
|
|
|
126
131
|
reviewMeetingLink?(meetingId: number, eventIds: number[], now: number): Promise<void>;
|
|
127
132
|
cursor(accountId: number): Promise<string | null>;
|
|
128
133
|
setCursor(accountId: number, value: string, now: number): Promise<void>;
|
|
134
|
+
/** Detects the language of the account's transcripts that have none; stores it only with `apply`. */
|
|
135
|
+
fillTranscriptLanguages?(input: {
|
|
136
|
+
accountId: number;
|
|
137
|
+
apply: boolean;
|
|
138
|
+
now: number;
|
|
139
|
+
}): Promise<TranscriptLanguageFill>;
|
|
140
|
+
}
|
|
141
|
+
export interface TranscriptLanguageFill {
|
|
142
|
+
accountId: number;
|
|
143
|
+
applied: boolean;
|
|
144
|
+
/** Transcripts that had no language. */
|
|
145
|
+
transcripts: number;
|
|
146
|
+
/** How many of them each language was found in; `null` where the text did not show one. */
|
|
147
|
+
languages: {
|
|
148
|
+
language: string | null;
|
|
149
|
+
transcripts: number;
|
|
150
|
+
}[];
|
|
129
151
|
}
|
|
@@ -121,6 +121,9 @@ export const memoryMeetingStore = () => {
|
|
|
121
121
|
text: r.text,
|
|
122
122
|
startMs: r.startMs,
|
|
123
123
|
speakerName: r.speakerName,
|
|
124
|
+
language: t.transcript.language,
|
|
125
|
+
line: r.position + 1,
|
|
126
|
+
linesTotal: t.rows.length,
|
|
124
127
|
})));
|
|
125
128
|
rows.push(...d.chat.map((r) => ({
|
|
126
129
|
meetingId: d.meeting.id,
|
|
@@ -13,6 +13,8 @@ export interface MeetingMediaDescriptor {
|
|
|
13
13
|
model: string;
|
|
14
14
|
version?: string;
|
|
15
15
|
timing?: "fixed-windows" | "recognition-segments";
|
|
16
|
+
/** The one language the model writes (`ru`); without it the language is read from the recognized text. */
|
|
17
|
+
language?: string;
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
export interface MeetingRecognitionSegment {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { contentHash } from "./ingest.js";
|
|
3
|
+
import { transcriptLanguage } from "./language.js";
|
|
3
4
|
import { dispatchMeetingMutation } from "./meeting-management.js";
|
|
4
5
|
const bounded = (value, max, name) => {
|
|
5
6
|
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
@@ -46,6 +47,9 @@ export const transcribeMeeting = async (source, store, target, options = {}) =>
|
|
|
46
47
|
const timing = recognition.timing ?? "recognition-segments";
|
|
47
48
|
if (timing !== "fixed-windows" && timing !== "recognition-segments")
|
|
48
49
|
throw new CliError("invalid_response", "unsupported recognition timing");
|
|
50
|
+
const modelLanguage = recognition.language;
|
|
51
|
+
if (modelLanguage !== undefined && (typeof modelLanguage !== "string" || !/^[a-z]{2,3}$/.test(modelLanguage)))
|
|
52
|
+
throw new CliError("invalid_response", "recognition language must be a two- or three-letter code");
|
|
49
53
|
const media = Object.freeze({
|
|
50
54
|
id: identifier(raw.id),
|
|
51
55
|
contentHash: raw.contentHash,
|
|
@@ -137,7 +141,7 @@ export const transcribeMeeting = async (source, store, target, options = {}) =>
|
|
|
137
141
|
source: `local/${encodeURIComponent(media.recognition.engine)}/${encodeURIComponent(media.recognition.model)}`,
|
|
138
142
|
format: "local-transcription",
|
|
139
143
|
}),
|
|
140
|
-
language:
|
|
144
|
+
language: modelLanguage ?? transcriptLanguage(rows.map((row) => row.text)),
|
|
141
145
|
contentHash: hash,
|
|
142
146
|
externalCreatedAt: null,
|
|
143
147
|
metadata: provenance,
|
|
@@ -22,4 +22,11 @@ export declare const MESSAGE_PARSING: {
|
|
|
22
22
|
/** Further apart than this, two words are rarely about one thing, even in a long message. */
|
|
23
23
|
export declare const MOST_WORDS_APART = 50;
|
|
24
24
|
export declare function searchError(reason: string, span: Span, alternative?: string): never;
|
|
25
|
+
/**
|
|
26
|
+
* The search ran out of time, not the query out of bounds: a valid query over a large store. `timeout`, so a caller
|
|
27
|
+
* does not tell the person to fix what they typed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function outOfTime(limitMs?: number): never;
|
|
30
|
+
/** A search limit that leaves the answer without a part, such as its total, rather than failing it. */
|
|
31
|
+
export declare const isQueryLimit: (error: unknown) => boolean;
|
|
25
32
|
export declare function exhausted(budget: string): never;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliError } from "@wirecat/cli-base";
|
|
1
|
+
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
2
2
|
import { QUERY_LIMITS as PARSER_LIMITS, queryError } from "@wirecat/cli-base/query";
|
|
3
3
|
export const QUERY_VERSION = 1;
|
|
4
4
|
export const QUERY_LIMITS = {
|
|
@@ -22,6 +22,16 @@ export const MOST_WORDS_APART = 50;
|
|
|
22
22
|
export function searchError(reason, span, alternative) {
|
|
23
23
|
queryError(reason, span, alternative, SEARCH_QUERY);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The search ran out of time, not the query out of bounds: a valid query over a large store. `timeout`, so a caller
|
|
27
|
+
* does not tell the person to fix what they typed.
|
|
28
|
+
*/
|
|
29
|
+
export function outOfTime(limitMs = QUERY_LIMITS.milliseconds) {
|
|
30
|
+
throw new CliError("timeout", `search stopped at its time limit of ${limitMs / 1000}s — the query is valid; ` +
|
|
31
|
+
"add chat: or date: to search less of the store", { reason: "query.time-limit", budget: "time", limit_ms: limitMs, complete: false });
|
|
32
|
+
}
|
|
33
|
+
/** A search limit that leaves the answer without a part, such as its total, rather than failing it. */
|
|
34
|
+
export const isQueryLimit = (error) => isCliError(error) && (error.details.reason === "query.too-large" || error.details.reason === "query.time-limit");
|
|
25
35
|
export function exhausted(budget) {
|
|
26
36
|
throw new CliError("validation_error", `search exceeded its ${budget} budget — narrow the chat or date range`, {
|
|
27
37
|
reason: "query.too-large",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { CliError
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { parseLocator } from "../domain/locator.js";
|
|
3
|
+
import { isQueryLimit } from "../search/lucene/types.js";
|
|
3
4
|
import { TOTAL_CAP, totalOf } from "../search/total.js";
|
|
4
5
|
import { discoveryRequest, searchDiscovery } from "./messages-discovery.js";
|
|
5
6
|
import { countLucene, searchLucene, statsLucene } from "./messages-search.js";
|
|
@@ -53,7 +54,7 @@ export const searchStore = async (store, account, request, messenger = {}, meani
|
|
|
53
54
|
const total = discovery
|
|
54
55
|
? totalOf(discovery.candidates, found.items.length, found.hasMore, !discovery.truncated)
|
|
55
56
|
: totalOf(await countLucene(store, account, { ...(request.discover ? discoveryRequest(request) : request), limit: request.limit + offset }, messenger, TOTAL_CAP).catch((error) => {
|
|
56
|
-
if (
|
|
57
|
+
if (isQueryLimit(error))
|
|
57
58
|
return undefined;
|
|
58
59
|
throw error;
|
|
59
60
|
}), found.items.length, found.hasMore);
|
|
@@ -4,7 +4,7 @@ import { parseLocator } from "../domain/locator.js";
|
|
|
4
4
|
import { correctWords } from "../search/correct.js";
|
|
5
5
|
import { PRESET_VERSION } from "../search/lucene/presets.js";
|
|
6
6
|
import { FIELD_VERSION } from "../search/lucene/registry.js";
|
|
7
|
-
import { exhausted, QUERY_LIMITS } from "../search/lucene/types.js";
|
|
7
|
+
import { exhausted, outOfTime, QUERY_LIMITS } from "../search/lucene/types.js";
|
|
8
8
|
import { normalize } from "../store/normalize.js";
|
|
9
9
|
import { withExcerpt } from "./excerpt.js";
|
|
10
10
|
import { coverageOf, prepareLucene } from "./messages-search.js";
|
|
@@ -31,7 +31,7 @@ export const searchCombined = async (store, account, request, messenger = {}, op
|
|
|
31
31
|
if (request.signal?.aborted)
|
|
32
32
|
throw new CliError("validation_error", "search was aborted", { reason: "query.aborted", complete: false });
|
|
33
33
|
if (performance.now() - started > QUERY_LIMITS.milliseconds)
|
|
34
|
-
|
|
34
|
+
outOfTime();
|
|
35
35
|
};
|
|
36
36
|
check();
|
|
37
37
|
const spans = request.ast === undefined && !request.exact && request.text !== undefined
|
|
@@ -3,7 +3,7 @@ import { implicitTextTerms, parseLucene, walkQuery } from "@wirecat/cli-base/que
|
|
|
3
3
|
import { parseLocator } from "../domain/locator.js";
|
|
4
4
|
import { fieldsOfQuery, meaningWords } from "../search/lucene/meaning-parts.js";
|
|
5
5
|
import { validateFields } from "../search/lucene/registry.js";
|
|
6
|
-
import { exhausted, MESSAGE_PARSING, QUERY_LIMITS } from "../search/lucene/types.js";
|
|
6
|
+
import { exhausted, MESSAGE_PARSING, outOfTime, QUERY_LIMITS } from "../search/lucene/types.js";
|
|
7
7
|
import { planQuestion, words } from "../search/question-plan.js";
|
|
8
8
|
import { searchCombined } from "./messages-combined.js";
|
|
9
9
|
import { prepareLucene, searchLucene } from "./messages-search.js";
|
|
@@ -101,7 +101,7 @@ export const searchDiscovery = async (store, account, asked, messenger = {}, mea
|
|
|
101
101
|
if (request.signal?.aborted)
|
|
102
102
|
throw new CliError("validation_error", "search was aborted", { reason: "query.aborted", complete: false });
|
|
103
103
|
if (performance.now() - started > QUERY_LIMITS.milliseconds)
|
|
104
|
-
|
|
104
|
+
outOfTime();
|
|
105
105
|
};
|
|
106
106
|
const union = new Map();
|
|
107
107
|
let truncated = false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliError
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { dateRange, parseLucene, timezoneOf } from "@wirecat/cli-base/query";
|
|
3
3
|
import { INTERNAL_NOTES } from "../domain/providers.js";
|
|
4
4
|
import { normalizeTag } from "../domain/tags.js";
|
|
@@ -9,7 +9,7 @@ import { entriesOfList, fitting } from "../search/account-list.js";
|
|
|
9
9
|
import { fieldsOfQuery as fieldsOf, meaningWords } from "../search/lucene/meaning-parts.js";
|
|
10
10
|
import { validateFields } from "../search/lucene/registry.js";
|
|
11
11
|
import { hasStems } from "../search/lucene/resolved.js";
|
|
12
|
-
import { SEARCH_QUERY } from "../search/lucene/types.js";
|
|
12
|
+
import { isQueryLimit, SEARCH_QUERY } from "../search/lucene/types.js";
|
|
13
13
|
import { createStemmer, DEFAULT_STEMMERS } from "../search/stem.js";
|
|
14
14
|
import { TOTAL_CAP, totalOf } from "../search/total.js";
|
|
15
15
|
import { openPictureEmbedderFor } from "./document-pictures.js";
|
|
@@ -286,12 +286,11 @@ const filterRefs = async (store, narrowing, scope) => {
|
|
|
286
286
|
return found.hasMore ? undefined : found.refs;
|
|
287
287
|
}
|
|
288
288
|
catch (error) {
|
|
289
|
-
if (
|
|
289
|
+
if (isQueryLimit(error))
|
|
290
290
|
return undefined;
|
|
291
291
|
throw error;
|
|
292
292
|
}
|
|
293
293
|
};
|
|
294
|
-
const tooLarge = (error) => isCliError(error) && error.details.reason === "query.too-large";
|
|
295
294
|
/**
|
|
296
295
|
* The notes the words find, with those found by meaning: every note the search would list, paged to the end. Never
|
|
297
296
|
* below what the page already showed; a count the store refuses as too costly is that lower bound.
|
|
@@ -313,7 +312,7 @@ const notesTotal = async (store, text, { exact, scope, shown, hasMore, }, others
|
|
|
313
312
|
});
|
|
314
313
|
}
|
|
315
314
|
catch (error) {
|
|
316
|
-
if (
|
|
315
|
+
if (isQueryLimit(error))
|
|
317
316
|
return totalOf(undefined, shown, hasMore);
|
|
318
317
|
throw error;
|
|
319
318
|
}
|
|
@@ -22,6 +22,9 @@ export interface MeetingSearchItem {
|
|
|
22
22
|
/** A transcript hit's cue, `meeting:<account>/<meeting>/<transcript>/<cue>`, which `meetings passage` opens. */
|
|
23
23
|
cueRef?: string;
|
|
24
24
|
speakerName?: string | null;
|
|
25
|
+
language?: string | null;
|
|
26
|
+
line?: number;
|
|
27
|
+
linesTotal?: number;
|
|
25
28
|
}
|
|
26
29
|
export interface MeetingSearchCursor {
|
|
27
30
|
accountId: number;
|
|
@@ -105,7 +105,12 @@ const collectMeetings = async (store, account, request, query) => {
|
|
|
105
105
|
id: hit.id,
|
|
106
106
|
startMs: hit.startMs,
|
|
107
107
|
...(hit.scope === "transcript" && hit.ref !== undefined
|
|
108
|
-
? {
|
|
108
|
+
? {
|
|
109
|
+
cueRef: hit.ref,
|
|
110
|
+
speakerName: hit.speakerName ?? null,
|
|
111
|
+
language: hit.language ?? null,
|
|
112
|
+
...(hit.line === undefined ? {} : { line: hit.line, linesTotal: hit.linesTotal }),
|
|
113
|
+
}
|
|
109
114
|
: {}),
|
|
110
115
|
},
|
|
111
116
|
});
|
package/dist/speech/local.d.ts
CHANGED
package/dist/speech/local.js
CHANGED
|
@@ -23,7 +23,14 @@ export const localSpeechRecognitionModel = (id) => {
|
|
|
23
23
|
featureDim: model.featureDim,
|
|
24
24
|
}))
|
|
25
25
|
.digest("hex");
|
|
26
|
-
return Object.freeze({
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
engine: "sherpa-onnx",
|
|
28
|
+
model: model.id,
|
|
29
|
+
version,
|
|
30
|
+
...(model.language === undefined ? {} : { language: model.language }),
|
|
31
|
+
sampleRate: 16000,
|
|
32
|
+
maxChunkSamples: 480000,
|
|
33
|
+
});
|
|
27
34
|
};
|
|
28
35
|
const notInstalled = (id) => new CliError("not_found", `speech model ${id} is not installed; download it explicitly with a WireCat messenger's models download command`, { reason: "model.not-downloaded", model: id });
|
|
29
36
|
/**
|
package/dist/speech/models.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface SpeechModel {
|
|
|
18
18
|
id: string;
|
|
19
19
|
title: string;
|
|
20
20
|
languages: string;
|
|
21
|
+
/** The one language the model writes, so its transcripts need no guessing; absent for multilingual models. */
|
|
22
|
+
language?: string;
|
|
21
23
|
files: ModelFile[];
|
|
22
24
|
/** The part of sherpa-onnx's `modelConfig` that names the files, given where they are. */
|
|
23
25
|
config: (path: (name: string) => string) => Record<string, unknown>;
|
package/dist/speech/models.js
CHANGED
|
@@ -8,6 +8,7 @@ export const MODELS = [
|
|
|
8
8
|
id: "gigaam-v3",
|
|
9
9
|
title: "GigaAM v3, with punctuation",
|
|
10
10
|
languages: "Russian — the best of the three for Russian",
|
|
11
|
+
language: "ru",
|
|
11
12
|
featureDim: 64,
|
|
12
13
|
files: [
|
|
13
14
|
{
|
|
@@ -45,6 +46,7 @@ export const MODELS = [
|
|
|
45
46
|
id: "gigaam-v3-ctc",
|
|
46
47
|
title: "GigaAM v3 CTC, with punctuation — a little faster",
|
|
47
48
|
languages: "Russian — a little faster, rougher with capital letters",
|
|
49
|
+
language: "ru",
|
|
48
50
|
featureDim: 64,
|
|
49
51
|
files: [
|
|
50
52
|
{
|
package/dist/store/driver.d.ts
CHANGED
|
@@ -23,7 +23,18 @@ export interface CacheDatabase {
|
|
|
23
23
|
exec(sql: string): void;
|
|
24
24
|
prepare(sql: string): CacheStatement;
|
|
25
25
|
close(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Set when the connection has `TIME_LIMIT_FUNCTION`. A statement runs to its end before any timer can fire, so a
|
|
28
|
+
* long one is stopped from inside: `WHERE … AND wc_time_limit(<epoch ms>)` throws `TimeLimitReached` once past it.
|
|
29
|
+
*/
|
|
30
|
+
timeLimit?: true;
|
|
26
31
|
}
|
|
32
|
+
export declare const TIME_LIMIT_FUNCTION = "wc_time_limit";
|
|
33
|
+
export declare class TimeLimitReached extends Error {
|
|
34
|
+
constructor();
|
|
35
|
+
}
|
|
36
|
+
/** The SQL function's body: 1 until `until` (epoch milliseconds) has passed. */
|
|
37
|
+
export declare const beforeTimeLimit: (until: unknown) => number;
|
|
27
38
|
export type OpenDatabase = (path: string) => CacheDatabase;
|
|
28
39
|
/**
|
|
29
40
|
* **Write-ahead mode, and a timeout rather than an instant failure.**
|
package/dist/store/driver.js
CHANGED
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
* The surface is deliberately tiny. Everything above it is ordinary SQL, so replacing the driver —
|
|
10
10
|
* or adding a third — is writing thirty lines, not learning an abstraction.
|
|
11
11
|
*/
|
|
12
|
+
export const TIME_LIMIT_FUNCTION = "wc_time_limit";
|
|
13
|
+
export class TimeLimitReached extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("the statement ran past its time limit");
|
|
16
|
+
this.name = "TimeLimitReached";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The SQL function's body: 1 until `until` (epoch milliseconds) has passed. */
|
|
20
|
+
export const beforeTimeLimit = (until) => {
|
|
21
|
+
if (Date.now() > Number(until))
|
|
22
|
+
throw new TimeLimitReached();
|
|
23
|
+
return 1;
|
|
24
|
+
};
|
|
12
25
|
/**
|
|
13
26
|
* **Write-ahead mode, and a timeout rather than an instant failure.**
|
|
14
27
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import type
|
|
2
|
+
import { type CacheDatabase } from "../driver.js";
|
|
3
3
|
/**
|
|
4
4
|
* Import this module only through a dynamic import that follows `quietSqliteWarning()`: a static import loads
|
|
5
5
|
* `node:sqlite`, and on Node 22 its ExperimentalWarning, before the filter can run.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// biome-ignore-all lint/plugin/no-sqlite-driver: cli-core owns the store, so it opens the driver itself
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { beforeTimeLimit, TIME_LIMIT_FUNCTION, } from "../driver.js";
|
|
3
4
|
import { loadVectorExtension, vectorExtension } from "../vector-extension.js";
|
|
4
5
|
/**
|
|
5
6
|
* Import this module only through a dynamic import that follows `quietSqliteWarning()`: a static import loads
|
|
@@ -29,6 +30,7 @@ export const nodeClient = async (path, { readOnly = false } = {}) => {
|
|
|
29
30
|
export const STATEMENT_CACHE_SIZE = 1000;
|
|
30
31
|
export const cacheOverNodeSqlite = (database, cacheSize = STATEMENT_CACHE_SIZE) => {
|
|
31
32
|
const statements = new Map();
|
|
33
|
+
database.function(TIME_LIMIT_FUNCTION, { deterministic: false, directOnly: true }, beforeTimeLimit);
|
|
32
34
|
const prepare = (sql) => {
|
|
33
35
|
const statement = database.prepare(sql);
|
|
34
36
|
return {
|
|
@@ -38,6 +40,7 @@ export const cacheOverNodeSqlite = (database, cacheSize = STATEMENT_CACHE_SIZE)
|
|
|
38
40
|
};
|
|
39
41
|
};
|
|
40
42
|
return {
|
|
43
|
+
timeLimit: true,
|
|
41
44
|
exec: (sql) => database.exec(sql),
|
|
42
45
|
prepare: (sql) => {
|
|
43
46
|
const cached = statements.get(sql);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Page } from "../../domain/models.js";
|
|
2
2
|
import type { QueryExecution } from "../../search/lucene/resolved.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type SqlValue } from "../driver.js";
|
|
4
4
|
import type { StoreContext } from "./open.js";
|
|
5
5
|
import type { ScoredHit } from "./words.js";
|
|
6
6
|
export { type Corpus, prefixOf } from "./lucene-fields.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { compileAutomaton } from "../../search/lucene/automaton.js";
|
|
3
3
|
import { isStemmed } from "../../search/lucene/resolved.js";
|
|
4
|
-
import { exhausted, QUERY_LIMITS, searchError } from "../../search/lucene/types.js";
|
|
4
|
+
import { exhausted, outOfTime, QUERY_LIMITS, searchError } from "../../search/lucene/types.js";
|
|
5
|
+
import { TIME_LIMIT_FUNCTION, TimeLimitReached } from "../driver.js";
|
|
5
6
|
import { emailHitsByPk } from "./emails.js";
|
|
6
7
|
import { placeHits } from "./hit-places.js";
|
|
7
8
|
import { combine, FIELD_COMPILERS, ftsMatch, SPEECH, } from "./lucene-fields.js";
|
|
@@ -54,7 +55,7 @@ export const countMatches = (context, execution, cap) => matchingPks(context, ex
|
|
|
54
55
|
* The keys of the messages that match, no more than `cap + 1`: one SQL query, unranked. `undefined` when a leaf is
|
|
55
56
|
* checked row by row (a preset, a pattern), where it would cost as much as the search itself.
|
|
56
57
|
*/
|
|
57
|
-
export const matchingPks = (context, execution, cap) => {
|
|
58
|
+
export const matchingPks = (context, execution, cap) => inTime(() => {
|
|
58
59
|
const { database, check, where, from, withRanking, ranked, leaves } = compileQuery(context, {
|
|
59
60
|
...execution,
|
|
60
61
|
newest: true,
|
|
@@ -66,6 +67,19 @@ export const matchingPks = (context, execution, cap) => {
|
|
|
66
67
|
.all(...ranked.params, ...where.params, cap + 1);
|
|
67
68
|
check();
|
|
68
69
|
return rows.map(({ id }) => Number(id));
|
|
70
|
+
});
|
|
71
|
+
const timeLimited = (error) => {
|
|
72
|
+
if (error instanceof TimeLimitReached)
|
|
73
|
+
outOfTime();
|
|
74
|
+
throw error;
|
|
75
|
+
};
|
|
76
|
+
const inTime = (work) => {
|
|
77
|
+
try {
|
|
78
|
+
return work();
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return timeLimited(error);
|
|
82
|
+
}
|
|
69
83
|
};
|
|
70
84
|
const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
71
85
|
const { database } = context;
|
|
@@ -74,6 +88,7 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
|
74
88
|
if (mail && (execution.conversationIds || execution.conversationSince !== undefined))
|
|
75
89
|
throw new CliError("validation_error", "mail has no conversations to search within");
|
|
76
90
|
const started = context.now();
|
|
91
|
+
const until = Date.now() + QUERY_LIMITS.milliseconds;
|
|
77
92
|
const budget = { work: 0 };
|
|
78
93
|
let states = 0, expansions = 0;
|
|
79
94
|
const patterns = new Map();
|
|
@@ -82,7 +97,7 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
|
82
97
|
if (execution.signal?.aborted)
|
|
83
98
|
throw new CliError("validation_error", "search was aborted", { reason: "query.aborted", complete: false });
|
|
84
99
|
if (context.now() - started > QUERY_LIMITS.milliseconds)
|
|
85
|
-
|
|
100
|
+
outOfTime();
|
|
86
101
|
};
|
|
87
102
|
const patternOf = (pattern, node) => {
|
|
88
103
|
check();
|
|
@@ -197,7 +212,10 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
|
197
212
|
exact: true,
|
|
198
213
|
})), "OR"),
|
|
199
214
|
], "AND");
|
|
200
|
-
|
|
215
|
+
const timeLimit = database.timeLimit
|
|
216
|
+
? [{ sql: `${TIME_LIMIT_FUNCTION}(?)`, params: [until], exact: true }]
|
|
217
|
+
: [];
|
|
218
|
+
let where = combine([{ sql: "m.deleted_at IS NULL", params: [], exact: true }, scope, expression, ...timeLimit], "AND");
|
|
201
219
|
if (execution.conversationIds !== undefined || execution.conversationSince !== undefined) {
|
|
202
220
|
const ids = execution.conversationIds;
|
|
203
221
|
where = combine([
|
|
@@ -404,7 +422,7 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
|
404
422
|
/** The matcher and its consumer run synchronously in one read snapshot; exact keys stay in SQL. */
|
|
405
423
|
export const withQuerySelection = (context, execution, consume) => {
|
|
406
424
|
const { database } = context;
|
|
407
|
-
return read(database, () => {
|
|
425
|
+
return inTime(() => read(database, () => {
|
|
408
426
|
const query = compileQuery(context, execution, true);
|
|
409
427
|
const { check, leaves, where, from, withRanking, ranked } = query;
|
|
410
428
|
check();
|
|
@@ -448,9 +466,17 @@ export const withQuerySelection = (context, execution, consume) => {
|
|
|
448
466
|
throw new Error("query selection consumers must run synchronously");
|
|
449
467
|
check();
|
|
450
468
|
return result;
|
|
451
|
-
});
|
|
469
|
+
}));
|
|
470
|
+
};
|
|
471
|
+
const runQuery = (context, execution, by) => {
|
|
472
|
+
try {
|
|
473
|
+
return runCompiled(context, execution, by).catch(timeLimited);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
return timeLimited(error);
|
|
477
|
+
}
|
|
452
478
|
};
|
|
453
|
-
const
|
|
479
|
+
const runCompiled = async (context, execution, by) => {
|
|
454
480
|
const { corpus, rowsFrom, hits, database, check, grouped, where, from, withRanking, ranked, leaves, stemmed, exactTier, driver, baseFrom, filtered, ranking, relevance, rankedFrom, exactColumn, order, placeTier, projection, projectionParams, joinedFrom, evaluate, } = compileQuery(context, execution);
|
|
455
481
|
if (by === "pks" && leaves.every(({ test }) => !test)) {
|
|
456
482
|
const rows = database
|
|
@@ -121,9 +121,9 @@ export function meetingHitSearchOver({ database }) {
|
|
|
121
121
|
if (after)
|
|
122
122
|
params.push(after.tier, after.tier, after.started, after.started, after.meeting, after.meeting, after.kind, after.kind, after.id);
|
|
123
123
|
const sql = `WITH matches AS (${matching}), matching AS (SELECT rid,tier FROM matches h WHERE NOT EXISTS(SELECT 1 FROM meeting_index_pending p WHERE p.id=h.rid/4 AND p.indexable_type=CASE h.rid%4 WHEN 1 THEN 'meeting_transcript_row' WHEN 2 THEN 'meeting_chat_message' ELSE 'meeting_summary' END)), selected AS (SELECT m.id,coalesce(m.started_at,0) AS started FROM meetings m WHERE ${clauses.join(" AND ")}), hits AS (
|
|
124
|
-
SELECT h.tier,m.started,m.id AS meeting_id,0 AS kind,r.id,r.start_ms,${bytes("r", ["text"])} AS bytes,t.id AS transcript_id,r.position FROM matching h JOIN meeting_transcript_rows r ON h.rid%4=1 AND r.id=h.rid/4 JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id AND t.superseded_at IS NULL AND t.deleted_at IS NULL JOIN selected m ON m.id=t.meeting_id
|
|
125
|
-
UNION ALL SELECT h.tier,m.started,m.id,1,c.id,NULL,${bytes("c", ["text"])},NULL,NULL FROM matching h JOIN meeting_chat_messages c ON h.rid%4=2 AND c.id=h.rid/4 JOIN selected m ON m.id=c.meeting_id
|
|
126
|
-
UNION ALL SELECT h.tier,m.started,m.id,2,s.id,NULL,${bytes("s", fields)},NULL,NULL FROM matching h JOIN meeting_summaries s ON h.rid%4=3 AND s.id=h.rid/4 JOIN selected m ON m.id=s.meeting_id)
|
|
124
|
+
SELECT h.tier,m.started,m.id AS meeting_id,0 AS kind,r.id,r.start_ms,${bytes("r", ["text"])} AS bytes,t.id AS transcript_id,r.position,t.language,(SELECT count(*) FROM meeting_transcript_rows n WHERE n.meeting_transcript_id=t.id) AS lines_total FROM matching h JOIN meeting_transcript_rows r ON h.rid%4=1 AND r.id=h.rid/4 JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id AND t.superseded_at IS NULL AND t.deleted_at IS NULL JOIN selected m ON m.id=t.meeting_id
|
|
125
|
+
UNION ALL SELECT h.tier,m.started,m.id,1,c.id,NULL,${bytes("c", ["text"])},NULL,NULL,NULL,NULL FROM matching h JOIN meeting_chat_messages c ON h.rid%4=2 AND c.id=h.rid/4 JOIN selected m ON m.id=c.meeting_id
|
|
126
|
+
UNION ALL SELECT h.tier,m.started,m.id,2,s.id,NULL,${bytes("s", fields)},NULL,NULL,NULL,NULL FROM matching h JOIN meeting_summaries s ON h.rid%4=3 AND s.id=h.rid/4 JOIN selected m ON m.id=s.meeting_id)
|
|
127
127
|
SELECT h.*,${bytes("m", ["title"])}+${bytes("s", ["doc_url"])} AS context_bytes FROM hits h
|
|
128
128
|
JOIN meetings m ON m.id=h.meeting_id LEFT JOIN meeting_summaries s ON h.kind=2 AND s.id=h.id
|
|
129
129
|
${seek} ORDER BY h.tier,h.started DESC,h.meeting_id,h.kind,h.id LIMIT ?`;
|
|
@@ -176,6 +176,13 @@ export function meetingHitSearchOver({ database }) {
|
|
|
176
176
|
id: Number(row.id),
|
|
177
177
|
text,
|
|
178
178
|
startMs: row.start_ms === null ? null : Number(row.start_ms),
|
|
179
|
+
...(kind === 0
|
|
180
|
+
? {
|
|
181
|
+
language: row.language === null ? null : String(row.language),
|
|
182
|
+
line: Number(row.position) + 1,
|
|
183
|
+
linesTotal: Number(row.lines_total),
|
|
184
|
+
}
|
|
185
|
+
: {}),
|
|
179
186
|
};
|
|
180
187
|
});
|
|
181
188
|
const fresh = items.flatMap((item) => {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type MeetingStore } from "../../meetings/index.js";
|
|
2
|
+
import type { StoreContext } from "./open.js";
|
|
3
|
+
export type MeetingLanguageStore = Required<Pick<MeetingStore, "fillTranscriptLanguages">>;
|
|
4
|
+
export declare const meetingLanguagesOver: ({ database }: StoreContext) => MeetingLanguageStore;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { transcriptLanguage } from "../../meetings/index.js";
|
|
2
|
+
import { positiveReadInteger } from "./meeting-reads.js";
|
|
3
|
+
import { read, writeBatch } from "./transaction.js";
|
|
4
|
+
export const meetingLanguagesOver = ({ database }) => ({
|
|
5
|
+
async fillTranscriptLanguages({ accountId, apply, now }) {
|
|
6
|
+
positiveReadInteger(accountId, "accountId");
|
|
7
|
+
const found = read(database, () => {
|
|
8
|
+
const rows = database.prepare("SELECT text FROM meeting_transcript_rows WHERE meeting_transcript_id = ?");
|
|
9
|
+
return database
|
|
10
|
+
.prepare(`SELECT t.id FROM meeting_transcripts t JOIN meetings m ON m.id = t.meeting_id
|
|
11
|
+
WHERE m.account_id = ? AND t.language IS NULL AND t.deleted_at IS NULL ORDER BY t.id`)
|
|
12
|
+
.all(accountId)
|
|
13
|
+
.map(({ id }) => ({
|
|
14
|
+
id: Number(id),
|
|
15
|
+
language: transcriptLanguage(rows.all(Number(id)).map((row) => String(row.text))),
|
|
16
|
+
}));
|
|
17
|
+
});
|
|
18
|
+
if (apply) {
|
|
19
|
+
const update = database.prepare("UPDATE meeting_transcripts SET language = ?, updated_at = ? WHERE id = ? AND language IS NULL");
|
|
20
|
+
writeBatch(database, () => {
|
|
21
|
+
for (const { id, language } of found)
|
|
22
|
+
if (language !== null)
|
|
23
|
+
update.run(language, now, id);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const counts = new Map();
|
|
27
|
+
for (const { language } of found)
|
|
28
|
+
counts.set(language, (counts.get(language) ?? 0) + 1);
|
|
29
|
+
return {
|
|
30
|
+
accountId,
|
|
31
|
+
applied: apply,
|
|
32
|
+
transcripts: found.length,
|
|
33
|
+
languages: [...counts].map(([language, transcripts]) => ({ language, transcripts })),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Attachment, MeetingChatLinesStore, MeetingContextStore, MeetingParticipantSearchStore, MeetingPreparationContextStore, MeetingPreparationKnowledgeStore, MeetingPreparationNotesStore, MeetingRelatedHistoryStore, MeetingReviewStore, MeetingSearchIndexStore, MeetingSearchStore, MeetingSeriesHistoryStore, MeetingStatsStore, MeetingStore, MeetingTranscriptReceiptStore, MeetingTranscriptStore, NewRecord } from "../../meetings/index.js";
|
|
2
2
|
import type { CacheDatabase } from "../driver.js";
|
|
3
|
+
import { type MeetingLanguageStore } from "./meeting-languages.js";
|
|
3
4
|
import { type MeetingContextReadCapabilities, type MeetingReadCapabilities } from "./meeting-reads.js";
|
|
4
5
|
import type { StoreContext } from "./open.js";
|
|
5
6
|
type Row = Record<string, unknown>;
|
|
@@ -11,7 +12,7 @@ export declare const saveAttachment: (database: CacheDatabase, type: string, par
|
|
|
11
12
|
export declare const drainMeetingIndex: (database: CacheDatabase, batch?: number) => number;
|
|
12
13
|
/** Each word of the query, as a prefix, all required. */
|
|
13
14
|
export declare const wordsQuery: (query: string) => string | null;
|
|
14
|
-
export type StoredMeetings = MeetingStore & MeetingPreparationContextStore & MeetingPreparationKnowledgeStore & MeetingPreparationNotesStore & MeetingRelatedHistoryStore & MeetingSeriesHistoryStore & MeetingTranscriptStore & MeetingReadCapabilities & MeetingContextReadCapabilities & MeetingParticipantSearchStore & MeetingSearchStore & MeetingTranscriptReceiptStore & MeetingContextStore & MeetingSearchIndexStore & MeetingStatsStore & MeetingReviewStore & MeetingChatLinesStore;
|
|
15
|
+
export type StoredMeetings = MeetingStore & MeetingPreparationContextStore & MeetingPreparationKnowledgeStore & MeetingPreparationNotesStore & MeetingRelatedHistoryStore & MeetingSeriesHistoryStore & MeetingTranscriptStore & MeetingReadCapabilities & MeetingContextReadCapabilities & MeetingParticipantSearchStore & MeetingSearchStore & MeetingTranscriptReceiptStore & MeetingContextStore & MeetingSearchIndexStore & MeetingStatsStore & MeetingReviewStore & MeetingChatLinesStore & MeetingLanguageStore;
|
|
15
16
|
/** The `MeetingStore` of `src/meetings/` over the shared store. */
|
|
16
17
|
export declare const meetingStoreOver: (context: StoreContext) => StoredMeetings;
|
|
17
18
|
export {};
|
|
@@ -11,6 +11,7 @@ import { meetingContextLinksOver } from "./meeting-context-links.js";
|
|
|
11
11
|
import { meetingHitIndexOver } from "./meeting-hit-index.js";
|
|
12
12
|
import { meetingHitSearchOver } from "./meeting-hit-search.js";
|
|
13
13
|
import { INDEX_CODES, indexedText, meetingRowid, summaryOf } from "./meeting-index-values.js";
|
|
14
|
+
import { meetingLanguagesOver } from "./meeting-languages.js";
|
|
14
15
|
import { meetingParticipantSearchOver } from "./meeting-participant-search.js";
|
|
15
16
|
import { meetingPreparationContextOver } from "./meeting-preparation-context.js";
|
|
16
17
|
import { meetingPreparationKnowledgeOver } from "./meeting-preparation-knowledge.js";
|
|
@@ -516,17 +517,18 @@ const search = (database, query, filter) => {
|
|
|
516
517
|
.prepare(`WITH hit AS (SELECT rowid AS r FROM meeting_words WHERE meeting_words MATCH ?),
|
|
517
518
|
picked AS (SELECT id, row_number() OVER (ORDER BY coalesce(started_at, 0) DESC, id) AS n FROM (${picked.sql}))
|
|
518
519
|
SELECT p.n, 0 AS kind, t.id AS a, r.position AS b, r.id, t.meeting_id, r.start_ms, coalesce(length(CAST(r.text AS BLOB)),0) AS bytes,
|
|
519
|
-
r.speaker_name AS speaker, m.account_id
|
|
520
|
+
r.speaker_name AS speaker, m.account_id, t.language,
|
|
521
|
+
(SELECT count(*) FROM meeting_transcript_rows n WHERE n.meeting_transcript_id = t.id) AS lines_total
|
|
520
522
|
FROM hit h JOIN meeting_transcript_rows r ON h.r % 4 = 1 AND r.id = h.r / 4
|
|
521
523
|
JOIN meeting_transcripts t ON t.id = r.meeting_transcript_id
|
|
522
524
|
AND t.superseded_at IS NULL AND t.deleted_at IS NULL
|
|
523
525
|
JOIN picked p ON p.id = t.meeting_id JOIN meetings m ON m.id = t.meeting_id
|
|
524
526
|
UNION ALL
|
|
525
|
-
SELECT p.n, 1, c.id, 0, c.id, c.meeting_id, NULL, coalesce(length(CAST(c.text AS BLOB)),0), NULL, NULL
|
|
527
|
+
SELECT p.n, 1, c.id, 0, c.id, c.meeting_id, NULL, coalesce(length(CAST(c.text AS BLOB)),0), NULL, NULL, NULL, NULL
|
|
526
528
|
FROM hit h JOIN meeting_chat_messages c ON h.r % 4 = 2 AND c.id = h.r / 4
|
|
527
529
|
JOIN picked p ON p.id = c.meeting_id
|
|
528
530
|
UNION ALL
|
|
529
|
-
SELECT p.n, 2, s.id, 0, s.id, s.meeting_id, NULL, coalesce(length(CAST(s.title AS BLOB)),0)+coalesce(length(CAST(s.overview AS BLOB)),0)+coalesce(length(CAST(s.sections AS BLOB)),0)+coalesce(length(CAST(s.next_steps AS BLOB)),0)+coalesce(length(CAST(s.content AS BLOB)),0), NULL, NULL
|
|
531
|
+
SELECT p.n, 2, s.id, 0, s.id, s.meeting_id, NULL, coalesce(length(CAST(s.title AS BLOB)),0)+coalesce(length(CAST(s.overview AS BLOB)),0)+coalesce(length(CAST(s.sections AS BLOB)),0)+coalesce(length(CAST(s.next_steps AS BLOB)),0)+coalesce(length(CAST(s.content AS BLOB)),0), NULL, NULL, NULL, NULL
|
|
530
532
|
FROM hit h JOIN meeting_summaries s ON h.r % 4 = 3 AND s.id = h.r / 4
|
|
531
533
|
JOIN picked p ON p.id = s.meeting_id
|
|
532
534
|
ORDER BY 1, 2, 3, 4 LIMIT 10001`)
|
|
@@ -551,6 +553,9 @@ const search = (database, query, filter) => {
|
|
|
551
553
|
cuePosition: Number(row.b),
|
|
552
554
|
}),
|
|
553
555
|
speakerName: row.speaker === null ? null : String(row.speaker),
|
|
556
|
+
language: row.language === null ? null : String(row.language),
|
|
557
|
+
line: Number(row.b) + 1,
|
|
558
|
+
linesTotal: Number(row.lines_total),
|
|
554
559
|
}
|
|
555
560
|
: {};
|
|
556
561
|
return {
|
|
@@ -596,6 +601,7 @@ export const meetingStoreOver = (context) => {
|
|
|
596
601
|
...meetingParticipantSearchOver(context),
|
|
597
602
|
...meetingHitIndexOver(context),
|
|
598
603
|
...meetingContextLinksOver(context),
|
|
604
|
+
...meetingLanguagesOver(context),
|
|
599
605
|
async appendTranscriptsWithReceipt(input) {
|
|
600
606
|
try {
|
|
601
607
|
return append(context, input);
|