@capsiynau/intelligence 0.3.0 → 0.5.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 +98 -0
- package/README.md +35 -4
- package/package.json +41 -10
- package/src/corrections/index.js +11 -1
- package/src/corrections/pure.js +133 -0
- package/src/corrections/tracker.js +36 -116
- package/src/dialect/index.js +240 -0
- package/src/speakers/index.js +204 -0
- package/src/spellcheck/core.js +73 -6
- package/src/welsh/normaliser.js +11 -0
- package/types/corrections/index.d.ts +2 -0
- package/types/corrections/pure.d.ts +21 -0
- package/types/corrections/tracker.d.ts +36 -0
- package/types/dialect/index.d.ts +69 -0
- package/types/glossary/import.d.ts +55 -0
- package/types/glossary/index.d.ts +6 -0
- package/types/glossary/parse.d.ts +63 -0
- package/types/glossary/persist.d.ts +66 -0
- package/types/glossary/prompts.d.ts +10 -0
- package/types/glossary/tools.d.ts +4 -0
- package/types/glossary/url.d.ts +9 -0
- package/types/index.d.ts +2 -0
- package/types/speakers/index.d.ts +85 -0
- package/types/spellcheck/core.d.ts +74 -0
- package/types/spellcheck/index.d.ts +1 -0
- package/types/welsh/digraphs.d.ts +102 -0
- package/types/welsh/index.d.ts +3 -0
- package/types/welsh/mutations.d.ts +76 -0
- package/types/welsh/normaliser.d.ts +13 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lexicon as a reviewable list. This is what a "your preferred terms"
|
|
3
|
+
* management screen renders, and what a workspace exports for sign-off.
|
|
4
|
+
*
|
|
5
|
+
* @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
|
|
6
|
+
* @returns {Array<{id:string, gloss:string, preferred:string, alternatives:string[]}>}
|
|
7
|
+
*/
|
|
8
|
+
export function listDialectTerms(region: "north" | "south" | "neutral" | "broadcast" | "formal"): Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
gloss: string;
|
|
11
|
+
preferred: string;
|
|
12
|
+
alternatives: string[];
|
|
13
|
+
}>;
|
|
14
|
+
/**
|
|
15
|
+
* Report off-region terms WITHOUT changing the text. This is the validator
|
|
16
|
+
* half: it produces evidence a reviewer can act on, which is what makes the
|
|
17
|
+
* lexical layer auditable rather than merely applied.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} text
|
|
20
|
+
* @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
|
|
21
|
+
* @returns {Array<{id:string, found:string, expected:string, index:number}>}
|
|
22
|
+
* Findings in document order. Empty array for empty input.
|
|
23
|
+
*/
|
|
24
|
+
export function checkDialect(text: string, region: "north" | "south" | "neutral" | "broadcast" | "formal"): Array<{
|
|
25
|
+
id: string;
|
|
26
|
+
found: string;
|
|
27
|
+
expected: string;
|
|
28
|
+
index: number;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Rewrite off-region terms to the target region's form. Deterministic and
|
|
32
|
+
* case-preserving; returns the input unchanged when there is nothing to do.
|
|
33
|
+
*
|
|
34
|
+
* Substitution is whole-term only and mutation-blind, so it is safe to run on
|
|
35
|
+
* mixed Welsh/English text: no entry in the table is an English word.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} text
|
|
38
|
+
* @param {'north'|'south'|'neutral'|'broadcast'|'formal'} region
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function applyDialect(text: string, region: "north" | "south" | "neutral" | "broadcast" | "formal"): string;
|
|
42
|
+
export namespace DIALECT_TERMS_STATUS {
|
|
43
|
+
let reviewed: boolean;
|
|
44
|
+
let reviewedBy: any;
|
|
45
|
+
let seededOn: string;
|
|
46
|
+
let note: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Seed lexicon. `neutral` is the form a broadcast or formal register would
|
|
50
|
+
* use; `null` means BOTH forms are regionally marked and there is no neutral
|
|
51
|
+
* standard, so a neutral target leaves the text alone rather than inventing
|
|
52
|
+
* one. Saying "no neutral form exists" is more useful than picking a side.
|
|
53
|
+
*/
|
|
54
|
+
export const DIALECT_TERMS: {
|
|
55
|
+
id: string;
|
|
56
|
+
north: string;
|
|
57
|
+
south: string;
|
|
58
|
+
neutral: string;
|
|
59
|
+
gloss: string;
|
|
60
|
+
}[];
|
|
61
|
+
/**
|
|
62
|
+
* Deliberately NOT in the table, with the reason. Kept in code because the
|
|
63
|
+
* next person to extend this will reach for exactly these, and the reason
|
|
64
|
+
* they are absent is not self-evident.
|
|
65
|
+
*/
|
|
66
|
+
export const EXCLUDED_TERMS: {
|
|
67
|
+
id: string;
|
|
68
|
+
reason: string;
|
|
69
|
+
}[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a CSV-ish string. Accepts (with or without header row):
|
|
3
|
+
* term,definition
|
|
4
|
+
* term,definition,category
|
|
5
|
+
* term,category,definition
|
|
6
|
+
* Header row (detected by token match) maps columns by name and
|
|
7
|
+
* tolerates aliases (name, meaning, tag). RFC-4180-lite: quoted
|
|
8
|
+
* fields + escaped quotes on a single line. Multi-line quoted values
|
|
9
|
+
* are out of scope - report and skip.
|
|
10
|
+
*/
|
|
11
|
+
export function parseImportedCsv(raw: any): {
|
|
12
|
+
rows: {
|
|
13
|
+
source_term: string;
|
|
14
|
+
preferred_term: string;
|
|
15
|
+
definition: string;
|
|
16
|
+
category: any;
|
|
17
|
+
}[];
|
|
18
|
+
errors: {
|
|
19
|
+
line: number;
|
|
20
|
+
reason: string;
|
|
21
|
+
}[];
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Parse plain text. One term per line. Separator tried in order: tab,
|
|
25
|
+
* `:` followed by space, ` - ` (hyphen with spaces). Three columns
|
|
26
|
+
* (term, definition, optional category) match our copy-as-text output
|
|
27
|
+
* so users can round-trip cleanly.
|
|
28
|
+
*/
|
|
29
|
+
export function parseImportedText(raw: any): {
|
|
30
|
+
rows: {
|
|
31
|
+
source_term: string;
|
|
32
|
+
preferred_term: string;
|
|
33
|
+
definition: string;
|
|
34
|
+
category: string;
|
|
35
|
+
}[];
|
|
36
|
+
errors: {
|
|
37
|
+
line: number;
|
|
38
|
+
reason: string;
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Split incoming rows against the existing set. Matches case-insensitive
|
|
43
|
+
* on trimmed source_term. Returns {additions, conflicts} where conflicts
|
|
44
|
+
* is an array of {existing, incoming} pairs the UI surfaces for
|
|
45
|
+
* resolution. Dedup within the import itself happens against additions
|
|
46
|
+
* (the second occurrence of a duplicated import term becomes a conflict
|
|
47
|
+
* with the first occurrence).
|
|
48
|
+
*/
|
|
49
|
+
export function splitImportAgainstExisting(existing: any, incoming: any): {
|
|
50
|
+
additions: any[];
|
|
51
|
+
conflicts: {
|
|
52
|
+
existing: any;
|
|
53
|
+
incoming: any;
|
|
54
|
+
}[];
|
|
55
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { WEB_SEARCH_TOOL } from "./tools.js";
|
|
2
|
+
export { normaliseUrl, isHttpUrl } from "./url.js";
|
|
3
|
+
export { extractText, parseTermsJson, normaliseTermRow, flagDiacriticStrip } from "./parse.js";
|
|
4
|
+
export { RESEARCH_SYSTEM, buildGenerationSystemPrompt, buildExpandSystemPrompt } from "./prompts.js";
|
|
5
|
+
export { parseImportedCsv, parseImportedText, splitImportAgainstExisting } from "./import.js";
|
|
6
|
+
export { suggestGlossaryName, languagePairFor, expandRowsForDb, buildGlossaryRow, suggestVersionedName } from "./persist.js";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function extractText(message: any): any;
|
|
2
|
+
/**
|
|
3
|
+
* Defensive JSON parse for the generation phase. The model is told to
|
|
4
|
+
* reply with raw JSON only, but markdown fences and conversational
|
|
5
|
+
* preamble do leak through. Strip what we can, then fall back to the
|
|
6
|
+
* first [...] match. Return null on total failure - never throw.
|
|
7
|
+
*/
|
|
8
|
+
export function parseTermsJson(raw: any): any[];
|
|
9
|
+
/**
|
|
10
|
+
* Heuristic guard: any Welsh text longer than 25 chars with ZERO
|
|
11
|
+
* diacritics is suspicious - the model probably stripped them.
|
|
12
|
+
*
|
|
13
|
+
* Not a perfect check (some short Welsh text genuinely has none) but
|
|
14
|
+
* catches the common "model normalised everything to ASCII" failure
|
|
15
|
+
* mode. Substitution detection (ŵ → w, ŷ → y in specific positions)
|
|
16
|
+
* requires a dictionary and is deferred to a future Tier-3 spellcheck
|
|
17
|
+
* integration; not in scope for the wizard.
|
|
18
|
+
*/
|
|
19
|
+
export function flagDiacriticStrip(text: any): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Normalise one row from the model's JSON output.
|
|
22
|
+
*
|
|
23
|
+
* In 'en' mode: tolerate naming wobbles ({term} vs {source_term}; etc).
|
|
24
|
+
* In 'cy' mode: same shape, but route Welsh text through normaliseWelsh().
|
|
25
|
+
* In 'bilingual' mode: expect {term_en, term_cy, definition_en,
|
|
26
|
+
* definition_cy, category}; route the *_cy fields through normaliseWelsh().
|
|
27
|
+
*
|
|
28
|
+
* Returns null for entries missing the required fields so the caller
|
|
29
|
+
* can `.map(normaliseTermRow).filter(Boolean)`. Adds a transient
|
|
30
|
+
* `_diacritic_warning` flag the handler can aggregate; it does NOT
|
|
31
|
+
* persist to the DB.
|
|
32
|
+
*/
|
|
33
|
+
export function normaliseTermRow(t: any, opts?: {}): {
|
|
34
|
+
source_term: any;
|
|
35
|
+
preferred_term: any;
|
|
36
|
+
definition: any;
|
|
37
|
+
term_en: any;
|
|
38
|
+
term_cy: any;
|
|
39
|
+
definition_en: any;
|
|
40
|
+
definition_cy: any;
|
|
41
|
+
category: string;
|
|
42
|
+
_diacritic_warning: boolean;
|
|
43
|
+
} | {
|
|
44
|
+
source_term: any;
|
|
45
|
+
preferred_term: any;
|
|
46
|
+
definition: any;
|
|
47
|
+
category: string;
|
|
48
|
+
_diacritic_warning: boolean;
|
|
49
|
+
term_en?: undefined;
|
|
50
|
+
term_cy?: undefined;
|
|
51
|
+
definition_en?: undefined;
|
|
52
|
+
definition_cy?: undefined;
|
|
53
|
+
} | {
|
|
54
|
+
source_term: string;
|
|
55
|
+
preferred_term: string;
|
|
56
|
+
definition: string;
|
|
57
|
+
category: string;
|
|
58
|
+
term_en?: undefined;
|
|
59
|
+
term_cy?: undefined;
|
|
60
|
+
definition_en?: undefined;
|
|
61
|
+
definition_cy?: undefined;
|
|
62
|
+
_diacritic_warning?: undefined;
|
|
63
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for converting Glossary Builder wizard rows into the
|
|
3
|
+
* shapes the `glossaries` + `terms` Supabase tables expect.
|
|
4
|
+
*
|
|
5
|
+
* No Supabase calls here - those live in the view layer
|
|
6
|
+
* (src/components/glossaryBuilder/savePersist.js) so this module
|
|
7
|
+
* stays unit-testable and side-effect-free, mirroring how
|
|
8
|
+
* @capsiynau/intelligence/welsh/normaliser splits pure logic from
|
|
9
|
+
* the API handler.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Suggest a glossary name based on the source URL. Strips
|
|
13
|
+
* protocol + leading `www.` so the result reads naturally
|
|
14
|
+
* ("Site terms - example.com").
|
|
15
|
+
*/
|
|
16
|
+
export function suggestGlossaryName(url: any): string;
|
|
17
|
+
/**
|
|
18
|
+
* Map wizard languageMode to glossaries.language_pair. Single-language
|
|
19
|
+
* modes get 'en-GB' or 'cy-GB'; bilingual is 'en-GB,cy-GB' (the
|
|
20
|
+
* existing convention in the schema for multi-language glossaries).
|
|
21
|
+
*/
|
|
22
|
+
export function languagePairFor(languageMode: any): "cy-GB" | "en-GB,cy-GB" | "en-GB";
|
|
23
|
+
/**
|
|
24
|
+
* Expand wizard rows into terms-table inserts.
|
|
25
|
+
*
|
|
26
|
+
* Single-language modes produce one row per wizard row.
|
|
27
|
+
* Bilingual produces TWO rows per pair (EN + CY), linked by sharing
|
|
28
|
+
* glossary_id and category, with source_language differing. This uses
|
|
29
|
+
* the schema as designed and keeps the live-relay engine able to
|
|
30
|
+
* query by language without a JOIN.
|
|
31
|
+
*
|
|
32
|
+
* Drops rows missing term or definition silently (the wizard
|
|
33
|
+
* shouldn't let them through, but defence in depth at persistence
|
|
34
|
+
* time means a partial bilingual row contributes only its complete
|
|
35
|
+
* leg rather than aborting the whole save).
|
|
36
|
+
*/
|
|
37
|
+
export function expandRowsForDb(wizardRows: any, opts?: {}): any[];
|
|
38
|
+
/**
|
|
39
|
+
* Build the glossaries-table insert payload for a wizard save.
|
|
40
|
+
* Defaults to visibility='shared' (org-wide, not just creator) - the
|
|
41
|
+
* wizard is collaborative tooling, not personal scratch space.
|
|
42
|
+
*/
|
|
43
|
+
export function buildGlossaryRow({ name, url, languageMode, organisationId, createdBy, visibility, }: {
|
|
44
|
+
name: any;
|
|
45
|
+
url: any;
|
|
46
|
+
languageMode: any;
|
|
47
|
+
organisationId: any;
|
|
48
|
+
createdBy: any;
|
|
49
|
+
visibility?: string;
|
|
50
|
+
}): {
|
|
51
|
+
name: any;
|
|
52
|
+
description: string;
|
|
53
|
+
organisation_id: any;
|
|
54
|
+
created_by: any;
|
|
55
|
+
visibility: string;
|
|
56
|
+
language_pair: string;
|
|
57
|
+
source_url: any;
|
|
58
|
+
is_default: boolean;
|
|
59
|
+
is_system: boolean;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Suggest a versioned name when "Create new version" is chosen on
|
|
63
|
+
* conflict. Bumps an existing (vN) suffix or appends (v2) to a name
|
|
64
|
+
* that lacks one.
|
|
65
|
+
*/
|
|
66
|
+
export function suggestVersionedName(existingName: any): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function buildGenerationSystemPrompt({ audience, topics, languageMode, dialect, }?: {
|
|
2
|
+
languageMode?: string;
|
|
3
|
+
dialect?: string;
|
|
4
|
+
}): string;
|
|
5
|
+
export function buildExpandSystemPrompt({ audience, topics, languageMode, dialect, existingTerms, }?: {
|
|
6
|
+
languageMode?: string;
|
|
7
|
+
dialect?: string;
|
|
8
|
+
existingTerms?: any[];
|
|
9
|
+
}): string;
|
|
10
|
+
export const RESEARCH_SYSTEM: "You are helping a SaaS user build a starter glossary for their organisation. You have a web_search tool - use it to look up the URL the user provides and identify the industry, the products / services they offer, and the likely audience. Reply conversationally in 2-3 sentences confirming what you found, then ask who the glossary is for (e.g. \"clients\", \"internal team\", \"general public\", \"broadcasters\"). Keep the tone warm and direct. Do NOT produce a glossary yet - that comes in a follow-up step.";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL helpers for the Glossary Builder engine.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic's web_search expects a full URL; a bare 'example.com'
|
|
5
|
+
* returns "no relevant pages" and the whole flow degrades silently.
|
|
6
|
+
* normaliseUrl prepends https:// when the caller forgot a scheme.
|
|
7
|
+
*/
|
|
8
|
+
export function normaliseUrl(raw: any): string;
|
|
9
|
+
export function isHttpUrl(url: any): boolean;
|
package/types/index.d.ts
ADDED
|
@@ -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,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;
|