@memberjunction/ai-segmentation 0.0.0 → 5.50.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/README.md +177 -43
- package/dist/generic/AdaptiveBoundarySegmenter.d.ts +98 -0
- package/dist/generic/AdaptiveBoundarySegmenter.d.ts.map +1 -0
- package/dist/generic/AdaptiveBoundarySegmenter.js +177 -0
- package/dist/generic/AdaptiveBoundarySegmenter.js.map +1 -0
- package/dist/generic/BaseContentCleaner.d.ts +101 -0
- package/dist/generic/BaseContentCleaner.d.ts.map +1 -0
- package/dist/generic/BaseContentCleaner.js +114 -0
- package/dist/generic/BaseContentCleaner.js.map +1 -0
- package/dist/generic/BaseSegmenter.d.ts +106 -0
- package/dist/generic/BaseSegmenter.d.ts.map +1 -0
- package/dist/generic/BaseSegmenter.js +260 -0
- package/dist/generic/BaseSegmenter.js.map +1 -0
- package/dist/generic/FixedWindowSegmenter.d.ts +49 -0
- package/dist/generic/FixedWindowSegmenter.d.ts.map +1 -0
- package/dist/generic/FixedWindowSegmenter.js +106 -0
- package/dist/generic/FixedWindowSegmenter.js.map +1 -0
- package/dist/generic/HtmlContentCleaner.d.ts +61 -0
- package/dist/generic/HtmlContentCleaner.d.ts.map +1 -0
- package/dist/generic/HtmlContentCleaner.js +130 -0
- package/dist/generic/HtmlContentCleaner.js.map +1 -0
- package/dist/generic/PagedContentSegmenter.d.ts +55 -0
- package/dist/generic/PagedContentSegmenter.d.ts.map +1 -0
- package/dist/generic/PagedContentSegmenter.js +99 -0
- package/dist/generic/PagedContentSegmenter.js.map +1 -0
- package/dist/generic/PlainTextContentCleaner.d.ts +22 -0
- package/dist/generic/PlainTextContentCleaner.d.ts.map +1 -0
- package/dist/generic/PlainTextContentCleaner.js +37 -0
- package/dist/generic/PlainTextContentCleaner.js.map +1 -0
- package/dist/generic/Segmentation.types.d.ts +198 -0
- package/dist/generic/Segmentation.types.d.ts.map +1 -0
- package/dist/generic/Segmentation.types.js +19 -0
- package/dist/generic/Segmentation.types.js.map +1 -0
- package/dist/generic/SegmentationResolver.d.ts +43 -0
- package/dist/generic/SegmentationResolver.d.ts.map +1 -0
- package/dist/generic/SegmentationResolver.js +83 -0
- package/dist/generic/SegmentationResolver.js.map +1 -0
- package/dist/generic/SemanticTextSegmenter.d.ts +80 -0
- package/dist/generic/SemanticTextSegmenter.d.ts.map +1 -0
- package/dist/generic/SemanticTextSegmenter.js +201 -0
- package/dist/generic/SemanticTextSegmenter.js.map +1 -0
- package/dist/generic/StructuralTextSegmenter.d.ts +63 -0
- package/dist/generic/StructuralTextSegmenter.d.ts.map +1 -0
- package/dist/generic/StructuralTextSegmenter.js +177 -0
- package/dist/generic/StructuralTextSegmenter.js.map +1 -0
- package/dist/generic/TranscriptSegmenter.d.ts +78 -0
- package/dist/generic/TranscriptSegmenter.d.ts.map +1 -0
- package/dist/generic/TranscriptSegmenter.js +194 -0
- package/dist/generic/TranscriptSegmenter.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -7
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pluggable content cleaning — the stage that runs before segmentation.
|
|
3
|
+
*
|
|
4
|
+
* @module @memberjunction/ai-segmentation
|
|
5
|
+
*/
|
|
6
|
+
import { LogError } from '@memberjunction/core';
|
|
7
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
8
|
+
/**
|
|
9
|
+
* Base class for content cleaning strategies.
|
|
10
|
+
*
|
|
11
|
+
* Cleaning is deliberately a **separate stage from segmentation**, and a separate
|
|
12
|
+
* plug-in point. The two answer different questions — cleaning asks *which text is
|
|
13
|
+
* actually content*, segmentation asks *where that content divides* — and they change for
|
|
14
|
+
* different reasons: a new CMS template needs new selectors, not a new chunking strategy.
|
|
15
|
+
* Splitting them also means the cleaning rules apply once and benefit every downstream
|
|
16
|
+
* consumer (embedding chunks, tagging chunks, full-text indexing) instead of being
|
|
17
|
+
* reimplemented per pipeline.
|
|
18
|
+
*
|
|
19
|
+
* Garbage that survives this stage is expensive: it gets embedded, stored, retrieved, and
|
|
20
|
+
* eventually shown to a user or an agent. Navigation chrome repeated across a thousand
|
|
21
|
+
* pages produces a thousand near-identical vectors that crowd out real answers.
|
|
22
|
+
*
|
|
23
|
+
* ```typescript
|
|
24
|
+
* @RegisterClass(BaseContentCleaner, 'MyCleaner')
|
|
25
|
+
* export class MyCleaner extends BaseContentCleaner {
|
|
26
|
+
* public get Key(): string { return 'MyCleaner'; }
|
|
27
|
+
* protected CleanCore(params: ContentCleaningParams): string { return strip(params.Content); }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class BaseContentCleaner {
|
|
32
|
+
/**
|
|
33
|
+
* Clean content ahead of segmentation.
|
|
34
|
+
*
|
|
35
|
+
* Never throws for content-shaped problems — inspect `Success`/`ErrorMessage`. On
|
|
36
|
+
* failure the ORIGINAL content is returned rather than an empty string, so a bad
|
|
37
|
+
* selector degrades to "not cleaned" instead of silently discarding the document.
|
|
38
|
+
*/
|
|
39
|
+
Clean(params) {
|
|
40
|
+
const warnings = [];
|
|
41
|
+
const original = params.Content ?? '';
|
|
42
|
+
if (original.trim().length === 0) {
|
|
43
|
+
return this.buildResult('', original, warnings);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const cleaned = this.CleanCore(params);
|
|
47
|
+
const finished = this.applyCommonRules(cleaned, params.Options, warnings);
|
|
48
|
+
if (finished.trim().length === 0 && original.trim().length > 0) {
|
|
49
|
+
warnings.push('Cleaning removed all content; falling back to the original text.');
|
|
50
|
+
return this.buildResult(original, original, warnings);
|
|
51
|
+
}
|
|
52
|
+
return this.buildResult(finished, original, warnings);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
56
|
+
LogError(`Content cleaner '${this.Key}' failed: ${message}`);
|
|
57
|
+
return {
|
|
58
|
+
Success: false,
|
|
59
|
+
Content: original,
|
|
60
|
+
CleanerKey: this.Key,
|
|
61
|
+
ErrorMessage: message,
|
|
62
|
+
Warnings: warnings,
|
|
63
|
+
CharactersRemoved: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a registered cleaner by key.
|
|
69
|
+
*
|
|
70
|
+
* Uses `TryCreateInstance` because `CreateInstance` never returns null for an unknown
|
|
71
|
+
* key — it silently yields a hollow base instance whose abstract members are undefined.
|
|
72
|
+
*/
|
|
73
|
+
static Resolve(key) {
|
|
74
|
+
if (!key || key.trim().length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const result = MJGlobal.Instance.ClassFactory.TryCreateInstance(BaseContentCleaner, key.trim());
|
|
78
|
+
return result.Resolved ? result.Instance : null;
|
|
79
|
+
}
|
|
80
|
+
/** Whitespace normalization and truncation, shared by every cleaner. */
|
|
81
|
+
applyCommonRules(content, options, warnings) {
|
|
82
|
+
let output = content;
|
|
83
|
+
if (options?.NormalizeWhitespace !== false) {
|
|
84
|
+
output = this.normalizeWhitespace(output);
|
|
85
|
+
}
|
|
86
|
+
if (options?.MaxLength && output.length > options.MaxLength) {
|
|
87
|
+
warnings?.push(`Truncated to ${options.MaxLength} characters (was ${output.length}).`);
|
|
88
|
+
output = output.slice(0, options.MaxLength);
|
|
89
|
+
}
|
|
90
|
+
return output.trim();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Collapse horizontal whitespace and runs of blank lines, while preserving the single
|
|
94
|
+
* blank line that marks a paragraph break — segmenters rely on it as a boundary signal.
|
|
95
|
+
*/
|
|
96
|
+
normalizeWhitespace(content) {
|
|
97
|
+
return content
|
|
98
|
+
.replace(/\r\n?/g, '\n')
|
|
99
|
+
.replace(/[ \t ]+/g, ' ')
|
|
100
|
+
.replace(/ *\n */g, '\n')
|
|
101
|
+
.replace(/\n{3,}/g, '\n\n');
|
|
102
|
+
}
|
|
103
|
+
/** Build a successful result with the removal delta computed. */
|
|
104
|
+
buildResult(content, original, warnings) {
|
|
105
|
+
return {
|
|
106
|
+
Success: true,
|
|
107
|
+
Content: content,
|
|
108
|
+
CleanerKey: this.Key,
|
|
109
|
+
Warnings: warnings,
|
|
110
|
+
CharactersRemoved: Math.max(original.length - content.length, 0),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=BaseContentCleaner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseContentCleaner.js","sourceRoot":"","sources":["../../src/generic/BaseContentCleaner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Abstract base for all content segmenters.
|
|
3
|
+
*
|
|
4
|
+
* @module @memberjunction/ai-segmentation
|
|
5
|
+
*/
|
|
6
|
+
import { ContentModality, RawSegment, SegmentationOptions, SegmentationParams, SegmentationResult } from './Segmentation.types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Base class for content segmentation strategies.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this exists
|
|
11
|
+
*
|
|
12
|
+
* Chunking used to be a private helper inside whichever pipeline needed it, which
|
|
13
|
+
* meant every new strategy (structure-aware, LLM topic boundaries, audio chapters)
|
|
14
|
+
* would have been another bespoke branch. `BaseSegmenter` turns "how do I split this
|
|
15
|
+
* content" into a **registered, swappable strategy** selected from metadata — the
|
|
16
|
+
* same pattern `BaseEmbeddings` and `VectorDBBase` already use for their providers.
|
|
17
|
+
*
|
|
18
|
+
* ## Adding a new strategy
|
|
19
|
+
*
|
|
20
|
+
* Implement {@link SegmentCore} and register the class. That is the whole contract —
|
|
21
|
+
* the base class handles validation, the token ceiling, small-segment merging,
|
|
22
|
+
* sequence numbering, parent/child remapping, and provenance stamping:
|
|
23
|
+
*
|
|
24
|
+
* ```typescript
|
|
25
|
+
* @RegisterClass(BaseSegmenter, 'MyStrategy')
|
|
26
|
+
* export class MySegmenter extends BaseSegmenter {
|
|
27
|
+
* public get Key(): string { return 'MyStrategy'; }
|
|
28
|
+
* public get SupportedModalities(): ContentModality[] { return ['text']; }
|
|
29
|
+
*
|
|
30
|
+
* protected async SegmentCore(params: SegmentationParams): Promise<RawSegment[]> {
|
|
31
|
+
* return myBoundaries(params.Text ?? '').map(b => ({
|
|
32
|
+
* Modality: 'text', Text: b.Text, StartOffset: b.Start, EndOffset: b.End
|
|
33
|
+
* }));
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Subclasses emit {@link RawSegment}s and never worry about numbering: `ParentIndex`
|
|
39
|
+
* refers to positions in the array they just returned, and the base class remaps it
|
|
40
|
+
* to real `Sequence` values after any oversized segment has been split.
|
|
41
|
+
*/
|
|
42
|
+
export declare abstract class BaseSegmenter {
|
|
43
|
+
/**
|
|
44
|
+
* The registration key for this segmenter. Must match the key passed to
|
|
45
|
+
* `@RegisterClass` so metadata-driven resolution round-trips.
|
|
46
|
+
*/
|
|
47
|
+
abstract get Key(): string;
|
|
48
|
+
/** Modalities this segmenter can produce. Used to validate configuration. */
|
|
49
|
+
abstract get SupportedModalities(): ContentModality[];
|
|
50
|
+
/**
|
|
51
|
+
* Produce the raw, un-normalized segments for this content.
|
|
52
|
+
*
|
|
53
|
+
* Implementations should focus purely on *where the boundaries are*; the base
|
|
54
|
+
* class enforces the token ceiling afterwards, so returning a segment that is
|
|
55
|
+
* too large is acceptable (it will be split, preserving `Title` and offsets).
|
|
56
|
+
*/
|
|
57
|
+
protected abstract SegmentCore(params: SegmentationParams): Promise<RawSegment[]> | RawSegment[];
|
|
58
|
+
/**
|
|
59
|
+
* Segment content into embeddable units.
|
|
60
|
+
*
|
|
61
|
+
* Never throws for content-shaped problems — inspect `Success`/`ErrorMessage`.
|
|
62
|
+
*/
|
|
63
|
+
Segment(params: SegmentationParams): Promise<SegmentationResult>;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a registered segmenter by key via the MJ class factory.
|
|
66
|
+
* Returns null when no segmenter is registered under that key.
|
|
67
|
+
*
|
|
68
|
+
* Uses `TryCreateInstance` rather than `CreateInstance` deliberately: the latter never returns
|
|
69
|
+
* null for an unregistered key — it falls back to `new BaseSegmenter()`, a hollow object whose
|
|
70
|
+
* abstract `Key`/`SegmentCore` are undefined. That failure stays invisible until something calls
|
|
71
|
+
* it, so an unresolvable key must be reported as such here.
|
|
72
|
+
*/
|
|
73
|
+
static Resolve(key: string): BaseSegmenter | null;
|
|
74
|
+
/** Apply the full normalization pipeline to raw segments. */
|
|
75
|
+
private normalize;
|
|
76
|
+
/**
|
|
77
|
+
* Merge adjacent text-only segments that fall below `MinSegmentTokens`, so a
|
|
78
|
+
* document with many one-line sections doesn't produce a spray of weak vectors.
|
|
79
|
+
*/
|
|
80
|
+
private mergeUndersized;
|
|
81
|
+
/** Two segments may merge only when both are plain text at the same place in the tree. */
|
|
82
|
+
private isMergeable;
|
|
83
|
+
/** Combine two adjacent text segments, widening offsets/timings to cover both. */
|
|
84
|
+
private mergePair;
|
|
85
|
+
/** Split any text segment exceeding the token ceiling, preserving metadata. */
|
|
86
|
+
private enforceTokenCeiling;
|
|
87
|
+
/** Return one piece when the segment fits, or N pieces via TextChunker when it doesn't. */
|
|
88
|
+
private splitIfNeeded;
|
|
89
|
+
/** Build a pre-sequencing segment from a raw segment plus (possibly split) text. */
|
|
90
|
+
private toSegment;
|
|
91
|
+
/** Assign Sequence/Depth and remap ParentIndex to the parent's first Sequence. */
|
|
92
|
+
private assignSequencing;
|
|
93
|
+
/** Walk each segment's parent chain to a depth, guarding against cycles. */
|
|
94
|
+
private computeDepths;
|
|
95
|
+
/** Fill in defaults for any option the caller left unset. */
|
|
96
|
+
protected resolveOptions(options?: SegmentationOptions): Required<SegmentationOptions>;
|
|
97
|
+
/** True when the params carry something segmentable. */
|
|
98
|
+
private hasPayload;
|
|
99
|
+
/** True when a raw segment carries text or media. */
|
|
100
|
+
private hasContent;
|
|
101
|
+
/** Estimated token count of a raw segment's text. */
|
|
102
|
+
protected tokensOf(segment: RawSegment): number;
|
|
103
|
+
/** Build a failed result. */
|
|
104
|
+
private failure;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=BaseSegmenter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseSegmenter.d.ts","sourceRoot":"","sources":["../../src/generic/BaseSegmenter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EACH,eAAe,EAGf,UAAU,EACV,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EACrB,MAAM,sBAAsB,CAAC;AAQ9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,8BAAsB,aAAa;IAC/B;;;OAGG;IACH,aAAoB,GAAG,IAAI,MAAM,CAAC;IAElC,6EAA6E;IAC7E,aAAoB,mBAAmB,IAAI,eAAe,EAAE,CAAC;IAE7D;;;;;;OAMG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,UAAU,EAAE;IAEhG;;;;OAIG;IACU,OAAO,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAoB7E;;;;;;;;OAQG;WACW,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAYxD,6DAA6D;IAC7D,OAAO,CAAC,SAAS;IAUjB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAiBvB,0FAA0F;IAC1F,OAAO,CAAC,WAAW;IAUnB,kFAAkF;IAClF,OAAO,CAAC,SAAS;IASjB,+EAA+E;IAC/E,OAAO,CAAC,mBAAmB;IAsB3B,2FAA2F;IAC3F,OAAO,CAAC,aAAa;IAuBrB,oFAAoF;IACpF,OAAO,CAAC,SAAS;IAsBjB,kFAAkF;IAClF,OAAO,CAAC,gBAAgB;IAoBxB,4EAA4E;IAC5E,OAAO,CAAC,aAAa;IAoBrB,6DAA6D;IAC7D,SAAS,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,QAAQ,CAAC,mBAAmB,CAAC;IAStF,wDAAwD;IACxD,OAAO,CAAC,UAAU;IAOlB,qDAAqD;IACrD,OAAO,CAAC,UAAU;IAIlB,qDAAqD;IACrD,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM;IAI/C,6BAA6B;IAC7B,OAAO,CAAC,OAAO;CAGlB"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Abstract base for all content segmenters.
|
|
3
|
+
*
|
|
4
|
+
* @module @memberjunction/ai-segmentation
|
|
5
|
+
*/
|
|
6
|
+
import { MJGlobal } from '@memberjunction/global';
|
|
7
|
+
import { LogError } from '@memberjunction/core';
|
|
8
|
+
import { TextChunker } from '@memberjunction/ai-vectors';
|
|
9
|
+
import { DEFAULT_MAX_SEGMENT_TOKENS, } from './Segmentation.types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Base class for content segmentation strategies.
|
|
12
|
+
*
|
|
13
|
+
* ## Why this exists
|
|
14
|
+
*
|
|
15
|
+
* Chunking used to be a private helper inside whichever pipeline needed it, which
|
|
16
|
+
* meant every new strategy (structure-aware, LLM topic boundaries, audio chapters)
|
|
17
|
+
* would have been another bespoke branch. `BaseSegmenter` turns "how do I split this
|
|
18
|
+
* content" into a **registered, swappable strategy** selected from metadata — the
|
|
19
|
+
* same pattern `BaseEmbeddings` and `VectorDBBase` already use for their providers.
|
|
20
|
+
*
|
|
21
|
+
* ## Adding a new strategy
|
|
22
|
+
*
|
|
23
|
+
* Implement {@link SegmentCore} and register the class. That is the whole contract —
|
|
24
|
+
* the base class handles validation, the token ceiling, small-segment merging,
|
|
25
|
+
* sequence numbering, parent/child remapping, and provenance stamping:
|
|
26
|
+
*
|
|
27
|
+
* ```typescript
|
|
28
|
+
* @RegisterClass(BaseSegmenter, 'MyStrategy')
|
|
29
|
+
* export class MySegmenter extends BaseSegmenter {
|
|
30
|
+
* public get Key(): string { return 'MyStrategy'; }
|
|
31
|
+
* public get SupportedModalities(): ContentModality[] { return ['text']; }
|
|
32
|
+
*
|
|
33
|
+
* protected async SegmentCore(params: SegmentationParams): Promise<RawSegment[]> {
|
|
34
|
+
* return myBoundaries(params.Text ?? '').map(b => ({
|
|
35
|
+
* Modality: 'text', Text: b.Text, StartOffset: b.Start, EndOffset: b.End
|
|
36
|
+
* }));
|
|
37
|
+
* }
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Subclasses emit {@link RawSegment}s and never worry about numbering: `ParentIndex`
|
|
42
|
+
* refers to positions in the array they just returned, and the base class remaps it
|
|
43
|
+
* to real `Sequence` values after any oversized segment has been split.
|
|
44
|
+
*/
|
|
45
|
+
export class BaseSegmenter {
|
|
46
|
+
/**
|
|
47
|
+
* Segment content into embeddable units.
|
|
48
|
+
*
|
|
49
|
+
* Never throws for content-shaped problems — inspect `Success`/`ErrorMessage`.
|
|
50
|
+
*/
|
|
51
|
+
async Segment(params) {
|
|
52
|
+
const warnings = [];
|
|
53
|
+
if (!this.hasPayload(params)) {
|
|
54
|
+
return this.failure('Segmentation requires Text, Media, or Cues.');
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const raw = await this.SegmentCore(params);
|
|
58
|
+
if (!raw || raw.length === 0) {
|
|
59
|
+
return { Success: true, Segments: [], SegmenterKey: this.Key, Warnings: warnings };
|
|
60
|
+
}
|
|
61
|
+
const segments = this.normalize(raw, this.resolveOptions(params.Options), warnings);
|
|
62
|
+
return { Success: true, Segments: segments, SegmenterKey: this.Key, Warnings: warnings };
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
66
|
+
LogError(`Segmenter '${this.Key}' failed: ${message}`);
|
|
67
|
+
return this.failure(message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a registered segmenter by key via the MJ class factory.
|
|
72
|
+
* Returns null when no segmenter is registered under that key.
|
|
73
|
+
*
|
|
74
|
+
* Uses `TryCreateInstance` rather than `CreateInstance` deliberately: the latter never returns
|
|
75
|
+
* null for an unregistered key — it falls back to `new BaseSegmenter()`, a hollow object whose
|
|
76
|
+
* abstract `Key`/`SegmentCore` are undefined. That failure stays invisible until something calls
|
|
77
|
+
* it, so an unresolvable key must be reported as such here.
|
|
78
|
+
*/
|
|
79
|
+
static Resolve(key) {
|
|
80
|
+
if (!key || key.trim().length === 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const result = MJGlobal.Instance.ClassFactory.TryCreateInstance(BaseSegmenter, key.trim());
|
|
84
|
+
return result.Resolved ? result.Instance : null;
|
|
85
|
+
}
|
|
86
|
+
// ─────────────────────────────────────────────
|
|
87
|
+
// Normalization pipeline
|
|
88
|
+
// ─────────────────────────────────────────────
|
|
89
|
+
/** Apply the full normalization pipeline to raw segments. */
|
|
90
|
+
normalize(raw, options, warnings) {
|
|
91
|
+
const populated = raw.filter((s) => this.hasContent(s));
|
|
92
|
+
if (populated.length < raw.length) {
|
|
93
|
+
warnings.push(`Dropped ${raw.length - populated.length} empty segment(s).`);
|
|
94
|
+
}
|
|
95
|
+
const merged = this.mergeUndersized(populated, options);
|
|
96
|
+
const emitted = this.enforceTokenCeiling(merged, options, warnings);
|
|
97
|
+
return this.assignSequencing(emitted, merged);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Merge adjacent text-only segments that fall below `MinSegmentTokens`, so a
|
|
101
|
+
* document with many one-line sections doesn't produce a spray of weak vectors.
|
|
102
|
+
*/
|
|
103
|
+
mergeUndersized(segments, options) {
|
|
104
|
+
if (options.MinSegmentTokens <= 0) {
|
|
105
|
+
return segments;
|
|
106
|
+
}
|
|
107
|
+
const result = [];
|
|
108
|
+
for (const segment of segments) {
|
|
109
|
+
const previous = result[result.length - 1];
|
|
110
|
+
const isSmall = this.tokensOf(segment) < options.MinSegmentTokens;
|
|
111
|
+
if (previous && isSmall && this.isMergeable(previous, segment, options)) {
|
|
112
|
+
result[result.length - 1] = this.mergePair(previous, segment);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
result.push(segment);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
/** Two segments may merge only when both are plain text at the same place in the tree. */
|
|
121
|
+
isMergeable(a, b, options) {
|
|
122
|
+
if (a.Media || b.Media || a.Modality !== 'text' || b.Modality !== 'text') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (a.ParentIndex !== b.ParentIndex) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
return this.tokensOf(a) + this.tokensOf(b) <= options.MaxSegmentTokens;
|
|
129
|
+
}
|
|
130
|
+
/** Combine two adjacent text segments, widening offsets/timings to cover both. */
|
|
131
|
+
mergePair(a, b) {
|
|
132
|
+
return {
|
|
133
|
+
...a,
|
|
134
|
+
Text: `${a.Text ?? ''}\n${b.Text ?? ''}`.trim(),
|
|
135
|
+
EndOffset: b.EndOffset ?? a.EndOffset,
|
|
136
|
+
EndMs: b.EndMs ?? a.EndMs,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Split any text segment exceeding the token ceiling, preserving metadata. */
|
|
140
|
+
enforceTokenCeiling(segments, options, warnings) {
|
|
141
|
+
const emitted = [];
|
|
142
|
+
let splitCount = 0;
|
|
143
|
+
segments.forEach((segment, rawIndex) => {
|
|
144
|
+
const pieces = this.splitIfNeeded(segment, options);
|
|
145
|
+
if (pieces.length > 1) {
|
|
146
|
+
splitCount++;
|
|
147
|
+
}
|
|
148
|
+
for (const piece of pieces) {
|
|
149
|
+
emitted.push({ RawIndex: rawIndex, Segment: piece });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (splitCount > 0) {
|
|
153
|
+
warnings.push(`Split ${splitCount} oversized segment(s) to fit ${options.MaxSegmentTokens} tokens.`);
|
|
154
|
+
}
|
|
155
|
+
return emitted;
|
|
156
|
+
}
|
|
157
|
+
/** Return one piece when the segment fits, or N pieces via TextChunker when it doesn't. */
|
|
158
|
+
splitIfNeeded(segment, options) {
|
|
159
|
+
const text = segment.Text ?? '';
|
|
160
|
+
if (!text || this.tokensOf(segment) <= options.MaxSegmentTokens) {
|
|
161
|
+
return [this.toSegment(segment, text)];
|
|
162
|
+
}
|
|
163
|
+
const chunks = TextChunker.ChunkText({
|
|
164
|
+
Text: text,
|
|
165
|
+
MaxChunkTokens: options.MaxSegmentTokens,
|
|
166
|
+
OverlapTokens: options.OverlapTokens,
|
|
167
|
+
Strategy: 'sentence',
|
|
168
|
+
});
|
|
169
|
+
if (chunks.length === 0) {
|
|
170
|
+
return [this.toSegment(segment, text)];
|
|
171
|
+
}
|
|
172
|
+
const base = segment.StartOffset ?? 0;
|
|
173
|
+
return chunks.map((chunk) => this.toSegment(segment, chunk.Text, base + chunk.StartOffset, base + chunk.EndOffset));
|
|
174
|
+
}
|
|
175
|
+
/** Build a pre-sequencing segment from a raw segment plus (possibly split) text. */
|
|
176
|
+
toSegment(segment, text, startOffset, endOffset) {
|
|
177
|
+
return {
|
|
178
|
+
Modality: segment.Modality,
|
|
179
|
+
Text: text.length > 0 ? text : undefined,
|
|
180
|
+
Media: segment.Media,
|
|
181
|
+
Title: segment.Title,
|
|
182
|
+
StartOffset: startOffset ?? segment.StartOffset,
|
|
183
|
+
EndOffset: endOffset ?? segment.EndOffset,
|
|
184
|
+
StartMs: segment.StartMs,
|
|
185
|
+
EndMs: segment.EndMs,
|
|
186
|
+
PageNumber: segment.PageNumber,
|
|
187
|
+
Speaker: segment.Speaker,
|
|
188
|
+
TokenEstimate: TextChunker.EstimateTokenCount(text),
|
|
189
|
+
SegmenterKey: this.Key,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** Assign Sequence/Depth and remap ParentIndex to the parent's first Sequence. */
|
|
193
|
+
assignSequencing(emitted, rawSegments) {
|
|
194
|
+
const firstSequenceByRaw = new Map();
|
|
195
|
+
emitted.forEach((item, sequence) => {
|
|
196
|
+
if (!firstSequenceByRaw.has(item.RawIndex)) {
|
|
197
|
+
firstSequenceByRaw.set(item.RawIndex, sequence);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
const depthByRaw = this.computeDepths(rawSegments);
|
|
201
|
+
return emitted.map((item, sequence) => {
|
|
202
|
+
const parentIndex = rawSegments[item.RawIndex]?.ParentIndex;
|
|
203
|
+
const parentSequence = parentIndex === undefined ? undefined : firstSequenceByRaw.get(parentIndex);
|
|
204
|
+
return {
|
|
205
|
+
...item.Segment,
|
|
206
|
+
Sequence: sequence,
|
|
207
|
+
Depth: depthByRaw.get(item.RawIndex) ?? 0,
|
|
208
|
+
ParentSequence: parentSequence,
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Walk each segment's parent chain to a depth, guarding against cycles. */
|
|
213
|
+
computeDepths(segments) {
|
|
214
|
+
const depths = new Map();
|
|
215
|
+
segments.forEach((_, index) => {
|
|
216
|
+
let depth = 0;
|
|
217
|
+
let cursor = segments[index]?.ParentIndex;
|
|
218
|
+
const visited = new Set([index]);
|
|
219
|
+
while (cursor !== undefined && !visited.has(cursor) && segments[cursor]) {
|
|
220
|
+
visited.add(cursor);
|
|
221
|
+
depth++;
|
|
222
|
+
cursor = segments[cursor].ParentIndex;
|
|
223
|
+
}
|
|
224
|
+
depths.set(index, depth);
|
|
225
|
+
});
|
|
226
|
+
return depths;
|
|
227
|
+
}
|
|
228
|
+
// ─────────────────────────────────────────────
|
|
229
|
+
// Helpers
|
|
230
|
+
// ─────────────────────────────────────────────
|
|
231
|
+
/** Fill in defaults for any option the caller left unset. */
|
|
232
|
+
resolveOptions(options) {
|
|
233
|
+
const max = options?.MaxSegmentTokens ?? DEFAULT_MAX_SEGMENT_TOKENS;
|
|
234
|
+
return {
|
|
235
|
+
MaxSegmentTokens: max,
|
|
236
|
+
OverlapTokens: options?.OverlapTokens ?? Math.floor(max * 0.1),
|
|
237
|
+
MinSegmentTokens: options?.MinSegmentTokens ?? 0,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/** True when the params carry something segmentable. */
|
|
241
|
+
hasPayload(params) {
|
|
242
|
+
const hasText = !!params.Text && params.Text.trim().length > 0;
|
|
243
|
+
const hasCues = !!params.Cues && params.Cues.length > 0;
|
|
244
|
+
const hasPages = !!params.Pages && params.Pages.length > 0;
|
|
245
|
+
return hasText || hasCues || hasPages || !!params.Media;
|
|
246
|
+
}
|
|
247
|
+
/** True when a raw segment carries text or media. */
|
|
248
|
+
hasContent(segment) {
|
|
249
|
+
return (!!segment.Text && segment.Text.trim().length > 0) || !!segment.Media;
|
|
250
|
+
}
|
|
251
|
+
/** Estimated token count of a raw segment's text. */
|
|
252
|
+
tokensOf(segment) {
|
|
253
|
+
return TextChunker.EstimateTokenCount(segment.Text ?? '');
|
|
254
|
+
}
|
|
255
|
+
/** Build a failed result. */
|
|
256
|
+
failure(message) {
|
|
257
|
+
return { Success: false, Segments: [], SegmenterKey: this.Key, ErrorMessage: message, Warnings: [] };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=BaseSegmenter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseSegmenter.js","sourceRoot":"","sources":["../../src/generic/BaseSegmenter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAGH,0BAA0B,GAK7B,MAAM,sBAAsB,CAAC;AAQ9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,OAAgB,aAAa;IAmB/B;;;;OAIG;IACI,KAAK,CAAC,OAAO,CAAC,MAA0B;QAC3C,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,6CAA6C,CAAC,CAAC;QACvE,CAAC;QAED,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAC3C,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;YACvF,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;YACpF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;QAC7F,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,MAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC3D,QAAQ,CAAC,cAAc,IAAI,CAAC,GAAG,aAAa,OAAO,EAAE,CAAC,CAAC;YACvD,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACjC,CAAC;IACL,CAAC;IAED;;;;;;;;OAQG;IACI,MAAM,CAAC,OAAO,CAAC,GAAW;QAC7B,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,iBAAiB,CAAgB,aAAa,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1G,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IACpD,CAAC;IAED,gDAAgD;IAChD,yBAAyB;IACzB,gDAAgD;IAEhD,6DAA6D;IACrD,SAAS,CAAC,GAAiB,EAAE,OAAsC,EAAE,QAAkB;QAC3F,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,IAAI,SAAS,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;YAChC,QAAQ,CAAC,IAAI,CAAC,WAAW,GAAG,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,oBAAoB,CAAC,CAAC;QAChF,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACxD,MAAM,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QACpE,OAAO,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAClD,CAAC;IAED;;;OAGG;IACK,eAAe,CAAC,QAAsB,EAAE,OAAsC;QAClF,IAAI,OAAO,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC;YAChC,OAAO,QAAQ,CAAC;QACpB,CAAC;QACD,MAAM,MAAM,GAAiB,EAAE,CAAC;QAChC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAClE,IAAI,QAAQ,IAAI,OAAO,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;gBACtE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAClE,CAAC;iBAAM,CAAC;gBACJ,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACzB,CAAC;QACL,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,0FAA0F;IAClF,WAAW,CAAC,CAAa,EAAE,CAAa,EAAE,OAAsC;QACpF,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,QAAQ,KAAK,MAAM,IAAI,CAAC,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;YACvE,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,IAAI,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;YAClC,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAC3E,CAAC;IAED,kFAAkF;IAC1E,SAAS,CAAC,CAAa,EAAE,CAAa;QAC1C,OAAO;YACH,GAAG,CAAC;YACJ,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,IAAI,EAAE,KAAK,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE;YAC/C,SAAS,EAAE,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS;YACrC,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK;SAC5B,CAAC;IACN,CAAC;IAED,+EAA+E;IACvE,mBAAmB,CACvB,QAAsB,EACtB,OAAsC,EACtC,QAAkB;QAElB,MAAM,OAAO,GAAqB,EAAE,CAAC;QACrC,IAAI,UAAU,GAAG,CAAC,CAAC;QACnB,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;YACnC,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACpD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpB,UAAU,EAAE,CAAC;YACjB,CAAC;YACD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBACzB,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;YACzD,CAAC;QACL,CAAC,CAAC,CAAC;QACH,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YACjB,QAAQ,CAAC,IAAI,CAAC,SAAS,UAAU,gCAAgC,OAAO,CAAC,gBAAgB,UAAU,CAAC,CAAC;QACzG,CAAC;QACD,OAAO,OAAO,CAAC;IACnB,CAAC;IAED,2FAA2F;IACnF,aAAa,CACjB,OAAmB,EACnB,OAAsC;QAEtC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;QAChC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAC9D,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,MAAM,GAAG,WAAW,CAAC,SAAS,CAAC;YACjC,IAAI,EAAE,IAAI;YACV,cAAc,EAAE,OAAO,CAAC,gBAAgB;YACxC,aAAa,EAAE,OAAO,CAAC,aAAa;YACpC,QAAQ,EAAE,UAAU;SACvB,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAC3C,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC;QACtC,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACxB,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,KAAK,CAAC,WAAW,EAAE,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,CACxF,CAAC;IACN,CAAC;IAED,oFAAoF;IAC5E,SAAS,CACb,OAAmB,EACnB,IAAY,EACZ,WAAoB,EACpB,SAAkB;QAElB,OAAO;YACH,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,IAAI,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;YACxC,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,WAAW,EAAE,WAAW,IAAI,OAAO,CAAC,WAAW;YAC/C,SAAS,EAAE,SAAS,IAAI,OAAO,CAAC,SAAS;YACzC,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,aAAa,EAAE,WAAW,CAAC,kBAAkB,CAAC,IAAI,CAAC;YACnD,YAAY,EAAE,IAAI,CAAC,GAAG;SACzB,CAAC;IACN,CAAC;IAED,kFAAkF;IAC1E,gBAAgB,CAAC,OAAyB,EAAE,WAAyB;QACzE,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAkB,CAAC;QACrD,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE;YAC/B,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACzC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YACpD,CAAC;QACL,CAAC,CAAC,CAAC;QACH,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;QACnD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE;YAClC,MAAM,WAAW,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;YAC5D,MAAM,cAAc,GAAG,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;YACnG,OAAO;gBACH,GAAG,IAAI,CAAC,OAAO;gBACf,QAAQ,EAAE,QAAQ;gBAClB,KAAK,EAAE,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;gBACzC,cAAc,EAAE,cAAc;aACjC,CAAC;QACN,CAAC,CAAC,CAAC;IACP,CAAC;IAED,4EAA4E;IACpE,aAAa,CAAC,QAAsB;QACxC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;QACzC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE;YAC1B,IAAI,KAAK,GAAG,CAAC,CAAC;YACd,IAAI,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;YAC1C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YACzC,OAAO,MAAM,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;gBACtE,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;gBACpB,KAAK,EAAE,CAAC;gBACR,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;YAC1C,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,gDAAgD;IAChD,UAAU;IACV,gDAAgD;IAEhD,6DAA6D;IACnD,cAAc,CAAC,OAA6B;QAClD,MAAM,GAAG,GAAG,OAAO,EAAE,gBAAgB,IAAI,0BAA0B,CAAC;QACpE,OAAO;YACH,gBAAgB,EAAE,GAAG;YACrB,aAAa,EAAE,OAAO,EAAE,aAAa,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC;YAC9D,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,IAAI,CAAC;SACnD,CAAC;IACN,CAAC;IAED,wDAAwD;IAChD,UAAU,CAAC,MAA0B;QACzC,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;QAC/D,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QACxD,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QAC3D,OAAO,OAAO,IAAI,OAAO,IAAI,QAAQ,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;IAC5D,CAAC;IAED,qDAAqD;IAC7C,UAAU,CAAC,OAAmB;QAClC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACjF,CAAC;IAED,qDAAqD;IAC3C,QAAQ,CAAC,OAAmB;QAClC,OAAO,WAAW,CAAC,kBAAkB,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,6BAA6B;IACrB,OAAO,CAAC,OAAe;QAC3B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IACzG,CAAC;CACJ"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Fixed-window segmenter — the universal fallback strategy.
|
|
3
|
+
*
|
|
4
|
+
* @module @memberjunction/ai-segmentation
|
|
5
|
+
*/
|
|
6
|
+
import { BaseSegmenter } from './BaseSegmenter.js';
|
|
7
|
+
import { ContentModality, RawSegment, SegmentationOptions, SegmentationParams } from './Segmentation.types.js';
|
|
8
|
+
/** Registration key for {@link FixedWindowSegmenter}. */
|
|
9
|
+
export declare const FIXED_WINDOW_SEGMENTER_KEY = "FixedWindow";
|
|
10
|
+
/** Options specific to {@link FixedWindowSegmenter}. */
|
|
11
|
+
export interface FixedWindowSegmentationOptions extends SegmentationOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Window length in ms for audio/video with no transcript.
|
|
14
|
+
*
|
|
15
|
+
* Defaults to 30000 (30s). Multimodal embedders sample a bounded number of
|
|
16
|
+
* frames per call, giving an effective window of roughly half a minute
|
|
17
|
+
* regardless of clip length — windows much longer than this are silently
|
|
18
|
+
* under-sampled, so the default deliberately stays under that ceiling.
|
|
19
|
+
*/
|
|
20
|
+
WindowMs?: number;
|
|
21
|
+
/** Overlap between consecutive AV windows, in ms. Default: 0. */
|
|
22
|
+
WindowOverlapMs?: number;
|
|
23
|
+
/** Text splitting strategy handed to `TextChunker`. Default: `'sentence'`. */
|
|
24
|
+
TextStrategy?: 'sentence' | 'paragraph' | 'fixed';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Splits content into uniform windows: token-bounded chunks for text, and
|
|
28
|
+
* fixed-duration windows for audio/video that has no transcript.
|
|
29
|
+
*
|
|
30
|
+
* This is the safety net, not the recommended default. It requires no LLM call,
|
|
31
|
+
* no transcript, and no document structure, so it always produces *something* —
|
|
32
|
+
* which makes it the right choice for logs, machine-generated text, and media
|
|
33
|
+
* that arrives without cues. Where structure or a transcript exists, prefer
|
|
34
|
+
* `StructuralText` or `Transcript`, both of which cut on real boundaries.
|
|
35
|
+
*/
|
|
36
|
+
export declare class FixedWindowSegmenter extends BaseSegmenter {
|
|
37
|
+
get Key(): string;
|
|
38
|
+
get SupportedModalities(): ContentModality[];
|
|
39
|
+
protected SegmentCore(params: SegmentationParams<FixedWindowSegmentationOptions>): RawSegment[];
|
|
40
|
+
/** Token-bounded text windows, offsets preserved. */
|
|
41
|
+
private segmentText;
|
|
42
|
+
/** Fixed-duration media windows, or a single segment for untimed media. */
|
|
43
|
+
private segmentMedia;
|
|
44
|
+
/** Walk the asset duration emitting one window per step. */
|
|
45
|
+
private buildTimeWindows;
|
|
46
|
+
/** Infer the media modality from its mime type. */
|
|
47
|
+
private resolveMediaModality;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=FixedWindowSegmenter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FixedWindowSegmenter.d.ts","sourceRoot":"","sources":["../../src/generic/FixedWindowSegmenter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE5G,yDAAyD;AACzD,eAAO,MAAM,0BAA0B,gBAAgB,CAAC;AAExD,wDAAwD;AACxD,MAAM,WAAW,8BAA+B,SAAQ,mBAAmB;IACvE;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,OAAO,CAAC;CACrD;AAED;;;;;;;;;GASG;AACH,qBACa,oBAAqB,SAAQ,aAAa;IACnD,IAAW,GAAG,IAAI,MAAM,CAEvB;IAED,IAAW,mBAAmB,IAAI,eAAe,EAAE,CAElD;IAED,SAAS,CAAC,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,8BAA8B,CAAC,GAAG,UAAU,EAAE;IAU/F,qDAAqD;IACrD,OAAO,CAAC,WAAW;IAgBnB,2EAA2E;IAC3E,OAAO,CAAC,YAAY;IAQpB,4DAA4D;IAC5D,OAAO,CAAC,gBAAgB;IAwBxB,mDAAmD;IACnD,OAAO,CAAC,oBAAoB;CAa/B"}
|