@capsiynau/intelligence 0.2.0 → 0.4.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.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Total milliseconds the utterances cover WITHIN the transcript span
3
+ * `[spanStart, spanEnd]`, counted as a UNION so overlapping utterances don't
4
+ * double-count. Each utterance is first clipped to the span (speech entirely
5
+ * outside the transcript window contributes 0), then the clipped intervals are
6
+ * merged and their lengths summed. This is what makes coverage measure the
7
+ * fraction of the transcript actually overlapped by the speaker timeline,
8
+ * rather than total diarisation duration (which can sit outside the window
9
+ * when diarisation ran over the full audio but the transcript is a clip).
10
+ *
11
+ * @param {Array<{start?:number,end?:number}>} utts
12
+ * @param {number} spanStart inclusive lower bound (ms)
13
+ * @param {number} spanEnd inclusive upper bound (ms)
14
+ * @returns {number} unioned, clipped duration in ms (>= 0)
15
+ */
16
+ export function clampedUnionMs(utts: Array<{
17
+ start?: number;
18
+ end?: number;
19
+ }>, spanStart: number, spanEnd: number): number;
20
+ /**
21
+ * Judge whether a diarisation timeline is reliable enough to label segments.
22
+ *
23
+ * @param {Array<{start:number,end:number,speaker:string}>} utterances
24
+ * diarisation timeline in milliseconds (AssemblyAI `utterances` shape).
25
+ * @param {Array<{start_ms:number,end_ms:number}>} segments
26
+ * transcript segments in milliseconds (used to measure coverage).
27
+ * @param {Partial<typeof DIARISATION_GATE_DEFAULTS>} [opts] threshold overrides.
28
+ * @returns {{ reliable: boolean, speakerCount: number, coverage: number,
29
+ * durationMs: number, reason: string }}
30
+ */
31
+ export function assessDiarisation(utterances: Array<{
32
+ start: number;
33
+ end: number;
34
+ speaker: string;
35
+ }>, segments: Array<{
36
+ start_ms: number;
37
+ end_ms: number;
38
+ }>, opts?: Partial<typeof DIARISATION_GATE_DEFAULTS>): {
39
+ reliable: boolean;
40
+ speakerCount: number;
41
+ coverage: number;
42
+ durationMs: number;
43
+ reason: string;
44
+ };
45
+ /**
46
+ * Map acoustic diarisation speakers ("Speaker 1/2/3") to REAL names from a
47
+ * labelled reference timeline (platform transcript / roster), by greatest
48
+ * total time-overlap. Upgrades anonymous acoustic labels to real names when a
49
+ * labelled source exists (speaker-ID ladder rung A over C). Pure.
50
+ *
51
+ * Only confident matches are returned: the winning name must cover
52
+ * >= minOverlapFraction of the acoustic speaker's speaking time AND beat the
53
+ * runner-up by >= dominanceRatio. Ambiguous / unmatched acoustic speakers are
54
+ * listed in `unmapped`, so the caller keeps their anonymous label.
55
+ *
56
+ * Note: two acoustic speakers can map to the same real name (acoustic over-
57
+ * split one person). That is acceptable - downstream can merge. 1:1 dedup is a
58
+ * future refinement.
59
+ *
60
+ * @param {Array<{start:number,end:number,speaker:string}>} acoustic utterances (ms)
61
+ * @param {Array<{start:number,end:number,name:string}>} labelled real-name turns (ms)
62
+ * @param {Partial<typeof SPEAKER_MAP_DEFAULTS>} [opts]
63
+ * @returns {{ map: Record<string,string>, unmapped: string[] }}
64
+ */
65
+ export function mapSpeakersToNames(acoustic: Array<{
66
+ start: number;
67
+ end: number;
68
+ speaker: string;
69
+ }>, labelled: Array<{
70
+ start: number;
71
+ end: number;
72
+ name: string;
73
+ }>, opts?: Partial<typeof SPEAKER_MAP_DEFAULTS>): {
74
+ map: Record<string, string>;
75
+ unmapped: string[];
76
+ };
77
+ export namespace DIARISATION_GATE_DEFAULTS {
78
+ let soleSpeakerSuspectAboveMs: number;
79
+ let minCoverage: number;
80
+ let trustAtSpeakers: number;
81
+ }
82
+ export namespace SPEAKER_MAP_DEFAULTS {
83
+ let minOverlapFraction: number;
84
+ let dominanceRatio: number;
85
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Check a single word against the supplied dictionary + accept-list.
3
+ *
4
+ * @param {string} word - The word to check (no punctuation, single token).
5
+ * @param {object} opts
6
+ * @param {object} opts.nspell - An nspell instance (server side, built
7
+ * from dictionary-cy or dictionary-en-gb).
8
+ * @param {Set<string>} [opts.accept] - Lowercased proper nouns + glossary
9
+ * + word_boost_approved terms that
10
+ * must never be flagged.
11
+ * @param {boolean} [opts.welsh] - When true, additionally try mutation
12
+ * reversal before declaring the word
13
+ * misspelled. Pass false for English.
14
+ * @returns {boolean} true if the word is considered correctly spelled.
15
+ */
16
+ export function checkWord(word: string, { nspell, accept, welsh }: {
17
+ nspell: object;
18
+ accept?: Set<string>;
19
+ welsh?: boolean;
20
+ }): boolean;
21
+ /**
22
+ * Tokenise a string into checkable words + their positions, so a UI
23
+ * can underline only the wrong tokens without re-tokenising itself.
24
+ *
25
+ * Position is the offset in the original string. Tokens are word-like
26
+ * runs of letters/marks/apostrophes/hyphens; punctuation is skipped.
27
+ *
28
+ * @param {string} text
29
+ * @returns {{ word: string, start: number, end: number }[]}
30
+ */
31
+ export function tokenise(text: string): {
32
+ word: string;
33
+ start: number;
34
+ end: number;
35
+ }[];
36
+ /**
37
+ * Check an entire string and return the misspelled tokens (if any).
38
+ * Convenience wrapper around tokenise + checkWord.
39
+ *
40
+ * @param {string} text
41
+ * @param {object} opts - same shape as checkWord opts
42
+ * @param {object} opts.nspell - An nspell instance.
43
+ * @param {Set<string>} [opts.accept] - Terms that must never be flagged.
44
+ * @param {boolean} [opts.welsh] - Try mutation reversal before flagging.
45
+ * @returns {{ word: string, start: number, end: number }[]}
46
+ */
47
+ export function checkSentence(text: string, opts: {
48
+ nspell: object;
49
+ accept?: Set<string>;
50
+ welsh?: boolean;
51
+ }): {
52
+ word: string;
53
+ start: number;
54
+ end: number;
55
+ }[];
56
+ /**
57
+ * Generate up to N suggestions for a misspelled word. Wraps
58
+ * nspell.suggest() but biases toward `accept` (proper nouns are
59
+ * surfaced first when they're close in edit distance).
60
+ *
61
+ * @param {string} word
62
+ * @param {object} opts
63
+ * @param {object} opts.nspell
64
+ * @param {Set<string>} [opts.accept]
65
+ * @param {boolean} [opts.welsh] - Bias suggestions for Welsh mutation forms.
66
+ * @param {number} [opts.max] - default 6
67
+ * @returns {string[]}
68
+ */
69
+ export function suggest(word: string, { nspell, accept, welsh, max }: {
70
+ nspell: object;
71
+ accept?: Set<string>;
72
+ welsh?: boolean;
73
+ max?: number;
74
+ }): string[];
@@ -0,0 +1 @@
1
+ export { checkWord, checkSentence, tokenise, suggest } from "./core.js";
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Split a Welsh word into its constituent letters, treating digraphs
3
+ * as single units. Case-preserving.
4
+ *
5
+ * @example
6
+ * splitIntoLetters('llwybr') // ['ll', 'w', 'y', 'b', 'r']
7
+ * splitIntoLetters('CHWARAE') // ['CH', 'W', 'A', 'R', 'A', 'E']
8
+ * splitIntoLetters('rhag-') // ['rh', 'a', 'g', '-']
9
+ *
10
+ * @param {string} word
11
+ * @returns {string[]}
12
+ */
13
+ export function splitIntoLetters(word: string): string[];
14
+ /**
15
+ * Welsh-aware character count: a digraph counts as ONE letter.
16
+ *
17
+ * @example
18
+ * welshLength('llwybr') // 5 (ll-w-y-b-r), NOT 6
19
+ * welshLength('chwarae') // 6 (ch-w-a-r-a-e)
20
+ * welshLength('Eisteddfod') // 9 (e-i-s-t-e-dd-f-o-d), NOT 10
21
+ *
22
+ * @param {string} word
23
+ * @returns {number}
24
+ */
25
+ export function welshLength(word: string): number;
26
+ /**
27
+ * Reverse a Welsh word preserving digraphs (used by anagram /
28
+ * caption-style transforms that mirror text).
29
+ *
30
+ * @example
31
+ * welshReverse('llwybr') // 'rbywll' (NOT 'rbywll' broken to 'rbyw ll')
32
+ *
33
+ * @param {string} word
34
+ * @returns {string}
35
+ */
36
+ export function welshReverse(word: string): string;
37
+ export function welshCompare(a: any, b: any): number;
38
+ /**
39
+ * Check if a string position lies on a digraph boundary. Useful for
40
+ * cursor / selection logic: when the cursor sits after `l` in `llwybr`,
41
+ * a single backspace should remove BOTH letters of `ll`, not just one.
42
+ *
43
+ * Returns `true` if the character at `index` is the second half of a
44
+ * digraph (i.e. the digraph started at `index - 1`).
45
+ *
46
+ * @example
47
+ * isDigraphContinuation('llwybr', 1) // true ('l' completing 'll')
48
+ * isDigraphContinuation('llwybr', 2) // false ('w')
49
+ * isDigraphContinuation('cath', 1) // false ('a')
50
+ *
51
+ * @param {string} word
52
+ * @param {number} index
53
+ * @returns {boolean}
54
+ */
55
+ export function isDigraphContinuation(word: string, index: number): boolean;
56
+ /**
57
+ * @capsiynau/intelligence/welsh/digraphs
58
+ *
59
+ * Welsh digraph handling. In the Welsh alphabet, these eight digraphs
60
+ * are SINGLE letters and must be treated as one unit for:
61
+ * - character iteration
62
+ * - cursor movement
63
+ * - word length
64
+ * - alphabetical sort
65
+ * - reversal (for anagrams etc.)
66
+ *
67
+ * The eight digraphs (canonical order in the Welsh alphabet):
68
+ * ch dd ff ng ll ph rh th
69
+ *
70
+ * Note: `ng` is the most context-sensitive. In some words it represents
71
+ * a single sound (canu's "ng" is /ŋ/) but in compound words it can be
72
+ * `n` + `g` separately (e.g. "Bangor" is BAN+GOR phonetically but
73
+ * conventionally still written without splitting). For purely
74
+ * orthographic operations (length, indexing, anagrams), we treat every
75
+ * occurrence as a single digraph. Callers needing phonetic accuracy
76
+ * must layer their own lexicon lookup on top.
77
+ *
78
+ * Pure JS, no external deps.
79
+ */
80
+ /**
81
+ * The eight Welsh digraphs in their canonical alphabetical order.
82
+ * @type {readonly string[]}
83
+ */
84
+ export const WELSH_DIGRAPHS: readonly string[];
85
+ /**
86
+ * Map of digraph -> alphabetical position (0-indexed within the digraph set).
87
+ * For full Welsh-alphabet collation, layer this on top of the single-letter
88
+ * position map in `WELSH_ALPHABET_ORDER` below.
89
+ */
90
+ export const DIGRAPH_INDEX: Readonly<{
91
+ [k: string]: number;
92
+ }>;
93
+ /**
94
+ * The canonical Welsh alphabet in collation order. Note the digraphs
95
+ * follow their leading letter: `c` then `ch`, `d` then `dd`, etc.
96
+ * Letters not in Welsh (`k`, `q`, `v`, `x`, `z`) are omitted; loan words
97
+ * containing them are conventionally sorted as if they were English.
98
+ *
99
+ * Source: Geiriadur Prifysgol Cymru collation rules.
100
+ * @type {readonly string[]}
101
+ */
102
+ export const WELSH_ALPHABET_ORDER: readonly string[];
@@ -0,0 +1,3 @@
1
+ export * from "./normaliser.js";
2
+ export * from "./digraphs.js";
3
+ export * from "./mutations.js";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Apply a mutation to a Welsh word. Returns the mutated form.
3
+ * Preserves the case of the first character.
4
+ *
5
+ * Soft mutation of `g` is DELETION — the word loses its initial `g`.
6
+ * e.g. 'gardd' -> soft -> 'ardd' (the garden, after 'yr' for feminine).
7
+ *
8
+ * If the word's initial consonant is not subject to the given mutation,
9
+ * returns the word unchanged.
10
+ *
11
+ * @param {string} word
12
+ * @param {'soft'|'nasal'|'aspirate'} kind
13
+ * @returns {string}
14
+ *
15
+ * @example
16
+ * applyMutation('cath', 'soft') // 'gath'
17
+ * applyMutation('Tad', 'soft') // 'Dad' (case preserved)
18
+ * applyMutation('ci', 'nasal') // 'nghi'
19
+ * applyMutation('Pen', 'aspirate') // 'Phen'
20
+ * applyMutation('gardd', 'soft') // 'ardd' (g-deletion)
21
+ * applyMutation('llaw', 'soft') // 'law'
22
+ * applyMutation('siop', 'soft') // 'siop' (s not subject)
23
+ */
24
+ export function applyMutation(word: string, kind: "soft" | "nasal" | "aspirate"): string;
25
+ /**
26
+ * Reverse a mutation: given an observed (possibly mutated) word,
27
+ * return all candidate root forms. The caller can intersect this
28
+ * with a dictionary lookup to pick the real root.
29
+ *
30
+ * If `kind` is provided, only candidates from that mutation are
31
+ * returned. If omitted, candidates from all three mutations.
32
+ *
33
+ * The observed word itself is ALWAYS included as the first candidate
34
+ * (it might not actually be mutated).
35
+ *
36
+ * @param {string} word
37
+ * @param {'soft'|'nasal'|'aspirate'} [kind]
38
+ * @returns {{ root: string, kind: string | null }[]}
39
+ *
40
+ * @example
41
+ * possibleRoots('gath') // [
42
+ * // { root: 'gath', kind: null }, // as-is
43
+ * // { root: 'cath', kind: 'soft' }, // c -> g
44
+ * // ]
45
+ * possibleRoots('phen', 'aspirate') // [
46
+ * // { root: 'phen', kind: null },
47
+ * // { root: 'pen', kind: 'aspirate' },
48
+ * // ]
49
+ */
50
+ export function possibleRoots(word: string, kind?: "soft" | "nasal" | "aspirate"): {
51
+ root: string;
52
+ kind: string | null;
53
+ }[];
54
+ /**
55
+ * Best-effort classification: does this word LOOK mutated?
56
+ *
57
+ * Returns the mutation kind if the word starts with a sequence
58
+ * characteristic of one specific mutation (e.g. 'ph' / 'th' / 'ch' are
59
+ * aspirate-only; 'mh' / 'nh' / 'ngh' are nasal-only). Returns null if
60
+ * the word either isn't mutated or its surface form is ambiguous.
61
+ *
62
+ * NOT a reliable mutation detector on its own. Use with caller context.
63
+ *
64
+ * @param {string} word
65
+ * @returns {'soft'|'nasal'|'aspirate'|null}
66
+ */
67
+ export function classifyMutation(word: string): "soft" | "nasal" | "aspirate" | null;
68
+ /**
69
+ * The three mutation kinds as a const enum-ish object.
70
+ * @type {Readonly<{soft:'soft', nasal:'nasal', aspirate:'aspirate'}>}
71
+ */
72
+ export const MUTATION: Readonly<{
73
+ soft: "soft";
74
+ nasal: "nasal";
75
+ aspirate: "aspirate";
76
+ }>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Normalise raw Welsh/English transcript text.
3
+ * Repairs broken contractions, fixes encoding, removes zero-width chars.
4
+ * Safe to apply to English too — repairs are Welsh-specific patterns unlikely
5
+ * to match English text.
6
+ */
7
+ export function normaliseWelsh(text: any): any;
8
+ /**
9
+ * Get a dialect-specific system prompt fragment for AI calls.
10
+ * @param {'broadcast'|'north'|'south'|'formal'} dialect
11
+ * @returns {string}
12
+ */
13
+ export function getDialectSystemPrompt(dialect: "broadcast" | "north" | "south" | "formal"): string;