@livekit/agents-plugin-livekit 0.1.3 → 1.0.0-next.1

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.
Files changed (91) hide show
  1. package/dist/hf_utils.cjs +272 -0
  2. package/dist/hf_utils.cjs.map +1 -0
  3. package/dist/hf_utils.d.cts +40 -0
  4. package/dist/hf_utils.d.ts +40 -0
  5. package/dist/hf_utils.d.ts.map +1 -0
  6. package/dist/hf_utils.js +237 -0
  7. package/dist/hf_utils.js.map +1 -0
  8. package/dist/hf_utils.test.cjs +330 -0
  9. package/dist/hf_utils.test.cjs.map +1 -0
  10. package/dist/hf_utils.test.d.cts +2 -0
  11. package/dist/hf_utils.test.d.ts +2 -0
  12. package/dist/hf_utils.test.d.ts.map +1 -0
  13. package/dist/hf_utils.test.js +307 -0
  14. package/dist/hf_utils.test.js.map +1 -0
  15. package/dist/index.cjs +27 -10
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +2 -2
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +24 -6
  21. package/dist/index.js.map +1 -1
  22. package/dist/turn_detector/base.cjs +202 -0
  23. package/dist/turn_detector/base.cjs.map +1 -0
  24. package/dist/turn_detector/base.d.cts +52 -0
  25. package/dist/turn_detector/base.d.ts +52 -0
  26. package/dist/turn_detector/base.d.ts.map +1 -0
  27. package/dist/turn_detector/base.js +172 -0
  28. package/dist/turn_detector/base.js.map +1 -0
  29. package/dist/turn_detector/constants.cjs +44 -0
  30. package/dist/turn_detector/constants.cjs.map +1 -0
  31. package/dist/turn_detector/constants.d.cts +7 -0
  32. package/dist/turn_detector/constants.d.ts +7 -0
  33. package/dist/turn_detector/constants.d.ts.map +1 -0
  34. package/dist/turn_detector/constants.js +16 -0
  35. package/dist/turn_detector/constants.js.map +1 -0
  36. package/dist/turn_detector/english.cjs +52 -0
  37. package/dist/turn_detector/english.cjs.map +1 -0
  38. package/dist/turn_detector/english.d.cts +11 -0
  39. package/dist/turn_detector/english.d.ts +11 -0
  40. package/dist/turn_detector/english.d.ts.map +1 -0
  41. package/dist/turn_detector/english.js +26 -0
  42. package/dist/turn_detector/english.js.map +1 -0
  43. package/dist/turn_detector/index.cjs +53 -0
  44. package/dist/turn_detector/index.cjs.map +1 -0
  45. package/dist/turn_detector/index.d.cts +5 -0
  46. package/dist/turn_detector/index.d.ts +5 -0
  47. package/dist/turn_detector/index.d.ts.map +1 -0
  48. package/dist/turn_detector/index.js +23 -0
  49. package/dist/turn_detector/index.js.map +1 -0
  50. package/dist/turn_detector/multilingual.cjs +144 -0
  51. package/dist/turn_detector/multilingual.cjs.map +1 -0
  52. package/dist/turn_detector/multilingual.d.cts +15 -0
  53. package/dist/turn_detector/multilingual.d.ts +15 -0
  54. package/dist/turn_detector/multilingual.d.ts.map +1 -0
  55. package/dist/turn_detector/multilingual.js +118 -0
  56. package/dist/turn_detector/multilingual.js.map +1 -0
  57. package/dist/turn_detector/utils.cjs +54 -0
  58. package/dist/turn_detector/utils.cjs.map +1 -0
  59. package/dist/turn_detector/utils.d.cts +35 -0
  60. package/dist/turn_detector/utils.d.ts +35 -0
  61. package/dist/turn_detector/utils.d.ts.map +1 -0
  62. package/dist/turn_detector/utils.js +29 -0
  63. package/dist/turn_detector/utils.js.map +1 -0
  64. package/dist/turn_detector/utils.test.cjs +196 -0
  65. package/dist/turn_detector/utils.test.cjs.map +1 -0
  66. package/dist/turn_detector/utils.test.d.cts +2 -0
  67. package/dist/turn_detector/utils.test.d.ts +2 -0
  68. package/dist/turn_detector/utils.test.d.ts.map +1 -0
  69. package/dist/turn_detector/utils.test.js +195 -0
  70. package/dist/turn_detector/utils.test.js.map +1 -0
  71. package/package.json +5 -4
  72. package/src/hf_utils.test.ts +392 -0
  73. package/src/hf_utils.ts +365 -0
  74. package/src/index.ts +32 -9
  75. package/src/turn_detector/base.ts +238 -0
  76. package/src/turn_detector/constants.ts +16 -0
  77. package/src/turn_detector/english.ts +27 -0
  78. package/src/turn_detector/index.ts +21 -0
  79. package/src/turn_detector/multilingual.ts +145 -0
  80. package/src/turn_detector/utils.test.ts +231 -0
  81. package/src/turn_detector/utils.ts +76 -0
  82. package/dist/turn_detector.cjs +0 -129
  83. package/dist/turn_detector.cjs.map +0 -1
  84. package/dist/turn_detector.d.cts +0 -22
  85. package/dist/turn_detector.d.ts +0 -22
  86. package/dist/turn_detector.d.ts.map +0 -1
  87. package/dist/turn_detector.js +0 -102
  88. package/dist/turn_detector.js.map +0 -1
  89. package/dist/turn_detector.onnx +0 -0
  90. package/src/turn_detector.onnx +0 -0
  91. package/src/turn_detector.ts +0 -121
@@ -0,0 +1,21 @@
1
+ // SPDX-FileCopyrightText: 2024 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { InferenceRunner } from '@livekit/agents';
5
+ import { INFERENCE_METHOD_EN } from './english.js';
6
+ import { INFERENCE_METHOD_MULTILINGUAL } from './multilingual.js';
7
+
8
+ export { EOUModel } from './base.js';
9
+ export { EnglishModel } from './english.js';
10
+ export { MultilingualModel } from './multilingual.js';
11
+ export { getUnicodeCategory, normalizeText } from './utils.js';
12
+
13
+ InferenceRunner.registerRunner(
14
+ INFERENCE_METHOD_EN,
15
+ new URL('./english.js', import.meta.url).toString(),
16
+ );
17
+
18
+ InferenceRunner.registerRunner(
19
+ INFERENCE_METHOD_MULTILINGUAL,
20
+ new URL('./multilingual.js', import.meta.url).toString(),
21
+ );
@@ -0,0 +1,145 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import type { llm } from '@livekit/agents';
5
+ import { getJobContext, log } from '@livekit/agents';
6
+ import { EOUModel, EOURunnerBase } from './base.js';
7
+ import { MAX_HISTORY_TURNS } from './constants.js';
8
+
9
+ const REMOTE_INFERENCE_TIMEOUT = 2000;
10
+
11
+ export const INFERENCE_METHOD_MULTILINGUAL = 'lk_end_of_utterance_multilingual';
12
+
13
+ export class EUORunnerMultilingual extends EOURunnerBase {
14
+ constructor() {
15
+ super('multilingual');
16
+ }
17
+ }
18
+
19
+ export class MultilingualModel extends EOUModel {
20
+ #logger = log();
21
+
22
+ constructor(unlikelyThreshold?: number) {
23
+ super({
24
+ modelType: 'multilingual',
25
+ unlikelyThreshold,
26
+ });
27
+ }
28
+
29
+ inferenceMethod(): string {
30
+ return INFERENCE_METHOD_MULTILINGUAL;
31
+ }
32
+
33
+ async unlikelyThreshold(language?: string): Promise<number | undefined> {
34
+ if (!language) {
35
+ return undefined;
36
+ }
37
+
38
+ let threshold = await super.unlikelyThreshold(language);
39
+ if (threshold === undefined) {
40
+ const url = remoteInferenceUrl();
41
+ if (!url) return undefined;
42
+
43
+ const resp = await fetch(url, {
44
+ method: 'POST',
45
+ body: JSON.stringify({
46
+ language,
47
+ }),
48
+ headers: {
49
+ 'Content-Type': 'application/json',
50
+ },
51
+ signal: AbortSignal.timeout(REMOTE_INFERENCE_TIMEOUT),
52
+ });
53
+
54
+ if (!resp.ok) {
55
+ throw new Error(`Failed to fetch threshold: ${resp.statusText}`);
56
+ }
57
+
58
+ const data = (await resp.json()) as { threshold: number | undefined };
59
+ threshold = data.threshold;
60
+ if (threshold) {
61
+ const languages = await this.languagesFuture.await;
62
+ languages[language] = { threshold };
63
+ }
64
+ }
65
+
66
+ return threshold;
67
+ }
68
+
69
+ async predictEndOfTurn(chatCtx: llm.ChatContext, timeout: number = 3): Promise<number> {
70
+ const url = remoteInferenceUrl();
71
+ if (!url) {
72
+ return await super.predictEndOfTurn(chatCtx, timeout);
73
+ }
74
+
75
+ // Copy and process chat context similar to Python implementation
76
+ const messages = chatCtx
77
+ .copy({
78
+ excludeFunctionCall: true,
79
+ excludeInstructions: true,
80
+ excludeEmptyMessage: true,
81
+ })
82
+ .truncate(MAX_HISTORY_TURNS);
83
+
84
+ // Get job context and build request
85
+ const ctx = getJobContext();
86
+ const request: any = {
87
+ ...messages.toJSON({
88
+ excludeImage: true,
89
+ excludeAudio: true,
90
+ excludeTimestamp: true,
91
+ }),
92
+ jobId: ctx.job.id,
93
+ workerId: ctx.workerId,
94
+ };
95
+
96
+ // Add agentId from environment variable if available
97
+ const agentId = process.env.LIVEKIT_AGENT_ID;
98
+ if (agentId) {
99
+ request.agentId = agentId;
100
+ }
101
+
102
+ const startedAt = performance.now();
103
+
104
+ this.#logger.debug({ url, request }, '=== remote EOU inference');
105
+
106
+ const resp = await fetch(url, {
107
+ method: 'POST',
108
+ body: JSON.stringify(request),
109
+ headers: {
110
+ 'Content-Type': 'application/json',
111
+ },
112
+ signal: AbortSignal.timeout(REMOTE_INFERENCE_TIMEOUT),
113
+ });
114
+
115
+ if (!resp.ok) {
116
+ throw new Error(`Failed to predict end of turn: ${resp.statusText}`);
117
+ }
118
+
119
+ const data = await resp.json();
120
+ const probability = data.probability;
121
+ if (typeof probability === 'number' && probability >= 0) {
122
+ this.#logger.debug(
123
+ {
124
+ eouProbability: probability,
125
+ duration: (performance.now() - startedAt) / 1000,
126
+ },
127
+ 'eou prediction',
128
+ );
129
+ return probability;
130
+ }
131
+
132
+ // default to indicate no prediction
133
+ return 1;
134
+ }
135
+ }
136
+
137
+ function remoteInferenceUrl() {
138
+ const urlBase = process.env.LIVEKIT_REMOTE_EOT_URL;
139
+ if (!urlBase) {
140
+ return undefined;
141
+ }
142
+ return `${urlBase}/eot/multi`;
143
+ }
144
+
145
+ export default EUORunnerMultilingual;
@@ -0,0 +1,231 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { describe, expect, it } from 'vitest';
5
+ import { getUnicodeCategory, normalizeText } from './utils.js';
6
+
7
+ describe('getUnicodeCategory', () => {
8
+ it('should identify basic ASCII punctuation', () => {
9
+ expect(getUnicodeCategory('!')).toBe('P');
10
+ expect(getUnicodeCategory('"')).toBe('P');
11
+ expect(getUnicodeCategory('#')).toBe('P');
12
+ expect(getUnicodeCategory('$')).toBe('P');
13
+ expect(getUnicodeCategory('%')).toBe('P');
14
+ expect(getUnicodeCategory('&')).toBe('P');
15
+ expect(getUnicodeCategory("'")).toBe('P');
16
+ expect(getUnicodeCategory('(')).toBe('P');
17
+ expect(getUnicodeCategory(')')).toBe('P');
18
+ expect(getUnicodeCategory('*')).toBe('P');
19
+ expect(getUnicodeCategory('+')).toBe('P');
20
+ expect(getUnicodeCategory(',')).toBe('P');
21
+ expect(getUnicodeCategory('-')).toBe('P');
22
+ expect(getUnicodeCategory('.')).toBe('P');
23
+ expect(getUnicodeCategory('/')).toBe('P');
24
+ });
25
+
26
+ it('should identify colon/semicolon punctuation', () => {
27
+ expect(getUnicodeCategory(':')).toBe('P');
28
+ expect(getUnicodeCategory(';')).toBe('P');
29
+ expect(getUnicodeCategory('<')).toBe('P');
30
+ expect(getUnicodeCategory('=')).toBe('P');
31
+ expect(getUnicodeCategory('>')).toBe('P');
32
+ expect(getUnicodeCategory('?')).toBe('P');
33
+ expect(getUnicodeCategory('@')).toBe('P');
34
+ });
35
+
36
+ it('should identify bracket punctuation', () => {
37
+ expect(getUnicodeCategory('[')).toBe('P');
38
+ expect(getUnicodeCategory('\\')).toBe('P');
39
+ expect(getUnicodeCategory(']')).toBe('P');
40
+ expect(getUnicodeCategory('^')).toBe('P');
41
+ expect(getUnicodeCategory('_')).toBe('P');
42
+ expect(getUnicodeCategory('`')).toBe('P');
43
+ });
44
+
45
+ it('should identify brace punctuation', () => {
46
+ expect(getUnicodeCategory('{')).toBe('P');
47
+ expect(getUnicodeCategory('|')).toBe('P');
48
+ expect(getUnicodeCategory('}')).toBe('P');
49
+ expect(getUnicodeCategory('~')).toBe('P');
50
+ });
51
+
52
+ it('should identify extended punctuation', () => {
53
+ expect(getUnicodeCategory('¡')).toBe('P');
54
+ expect(getUnicodeCategory('¿')).toBe('P');
55
+ expect(getUnicodeCategory('«')).toBe('P');
56
+ expect(getUnicodeCategory('»')).toBe('P');
57
+ });
58
+
59
+ it('should not identify letters as punctuation', () => {
60
+ expect(getUnicodeCategory('a')).toBe('');
61
+ expect(getUnicodeCategory('A')).toBe('');
62
+ expect(getUnicodeCategory('z')).toBe('');
63
+ expect(getUnicodeCategory('Z')).toBe('');
64
+ });
65
+
66
+ it('should not identify numbers as punctuation', () => {
67
+ expect(getUnicodeCategory('0')).toBe('');
68
+ expect(getUnicodeCategory('1')).toBe('');
69
+ expect(getUnicodeCategory('9')).toBe('');
70
+ });
71
+
72
+ it('should not identify whitespace as punctuation', () => {
73
+ expect(getUnicodeCategory(' ')).toBe('');
74
+ expect(getUnicodeCategory('\t')).toBe('');
75
+ expect(getUnicodeCategory('\n')).toBe('');
76
+ });
77
+
78
+ it('should handle empty string', () => {
79
+ expect(getUnicodeCategory('')).toBe('');
80
+ });
81
+
82
+ it('should handle unicode characters', () => {
83
+ expect(getUnicodeCategory('é')).toBe('');
84
+ expect(getUnicodeCategory('ñ')).toBe('');
85
+ expect(getUnicodeCategory('ç')).toBe('');
86
+ });
87
+ });
88
+
89
+ describe('normalizeText', () => {
90
+ describe('basic functionality', () => {
91
+ it('should convert to lowercase', () => {
92
+ expect(normalizeText('HELLO')).toBe('hello');
93
+ expect(normalizeText('HeLLo')).toBe('hello');
94
+ expect(normalizeText('WORLD')).toBe('world');
95
+ });
96
+
97
+ it('should remove basic punctuation', () => {
98
+ expect(normalizeText('Hello!')).toBe('hello');
99
+ expect(normalizeText('Hello?')).toBe('hello');
100
+ expect(normalizeText('Hello.')).toBe('hello');
101
+ expect(normalizeText('Hello,')).toBe('hello');
102
+ });
103
+
104
+ it('should preserve apostrophes', () => {
105
+ expect(normalizeText("I'm happy")).toBe("i'm happy");
106
+ expect(normalizeText("don't worry")).toBe("don't worry");
107
+ expect(normalizeText("it's great")).toBe("it's great");
108
+ });
109
+
110
+ it('should preserve hyphens', () => {
111
+ expect(normalizeText('well-trained')).toBe('well-trained');
112
+ expect(normalizeText('state-of-the-art')).toBe('state-of-the-art');
113
+ expect(normalizeText('co-worker')).toBe('co-worker');
114
+ });
115
+
116
+ it('should collapse multiple whitespace', () => {
117
+ expect(normalizeText('hello world')).toBe('hello world');
118
+ expect(normalizeText('multiple spaces here')).toBe('multiple spaces here');
119
+ expect(normalizeText('tab\t\tspaces')).toBe('tab spaces');
120
+ expect(normalizeText('newline\n\nspaces')).toBe('newline spaces');
121
+ });
122
+
123
+ it('should trim leading and trailing whitespace', () => {
124
+ expect(normalizeText(' hello ')).toBe('hello');
125
+ expect(normalizeText('\t\nhello\t\n')).toBe('hello');
126
+ expect(normalizeText(' hello world ')).toBe('hello world');
127
+ });
128
+ });
129
+
130
+ describe('comprehensive test cases', () => {
131
+ it('should handle the basic greeting case', () => {
132
+ expect(normalizeText('Hi, how can I help you today?')).toBe('hi how can i help you today');
133
+ });
134
+
135
+ it('should handle contractions and hyphens', () => {
136
+ expect(normalizeText("I'm a well-trained assistant!")).toBe("i'm a well-trained assistant");
137
+ });
138
+
139
+ it('should remove various punctuation types', () => {
140
+ expect(normalizeText('Hello!!! What??? Price: $19.99 (20% off).')).toBe(
141
+ 'hello what price 1999 20 off',
142
+ );
143
+ });
144
+
145
+ it('should handle multiple spaces', () => {
146
+ expect(normalizeText('Multiple spaces here')).toBe('multiple spaces here');
147
+ });
148
+
149
+ it('should handle unicode characters', () => {
150
+ expect(normalizeText('Café entrées naïve résumé')).toBe('café entrées naïve résumé');
151
+ });
152
+
153
+ it('should handle mixed punctuation and unicode', () => {
154
+ expect(normalizeText('¿Cómo estás? ¡Muy bien!')).toBe('cómo estás muy bien');
155
+ });
156
+ });
157
+
158
+ describe('edge cases', () => {
159
+ it('should handle empty string', () => {
160
+ expect(normalizeText('')).toBe('');
161
+ });
162
+
163
+ it('should handle whitespace-only string', () => {
164
+ expect(normalizeText(' ')).toBe('');
165
+ expect(normalizeText('\t\n ')).toBe('');
166
+ });
167
+
168
+ it('should handle punctuation-only string', () => {
169
+ expect(normalizeText('!!!')).toBe('');
170
+ expect(normalizeText('???')).toBe('');
171
+ expect(normalizeText('...')).toBe('');
172
+ });
173
+
174
+ it('should handle mixed punctuation and preserved characters', () => {
175
+ expect(normalizeText('!@#$%^&*()_+-={}[]|\\:;"\'<>?,./')).toBe("-'");
176
+ });
177
+
178
+ it('should handle numbers with punctuation', () => {
179
+ expect(normalizeText('$19.99')).toBe('1999');
180
+ expect(normalizeText('(555) 123-4567')).toBe('555 123-4567');
181
+ });
182
+
183
+ it('should handle special unicode punctuation', () => {
184
+ expect(normalizeText('Hello… world!')).toBe('hello world');
185
+ expect(normalizeText('"Quoted text"')).toBe('quoted text');
186
+ expect(normalizeText("'Single quotes'")).toBe("'single quotes'");
187
+ });
188
+ });
189
+
190
+ describe('unicode normalization', () => {
191
+ it('should apply NFKC normalization', () => {
192
+ expect(normalizeText('café')).toBe('café');
193
+ expect(normalizeText('naïve')).toBe('naïve');
194
+ });
195
+
196
+ it('should handle combining characters', () => {
197
+ const eWithCombiningAcute = 'caf\u0065\u0301';
198
+ const precomposedE = 'café';
199
+ expect(normalizeText(eWithCombiningAcute)).toBe(normalizeText(precomposedE));
200
+ });
201
+ });
202
+
203
+ describe('real-world examples', () => {
204
+ it('should handle typical assistant responses', () => {
205
+ expect(normalizeText('Hello! How can I assist you today?')).toBe(
206
+ 'hello how can i assist you today',
207
+ );
208
+ expect(normalizeText("I'm here to help with any questions you might have.")).toBe(
209
+ "i'm here to help with any questions you might have",
210
+ );
211
+ });
212
+
213
+ it('should handle typical user queries', () => {
214
+ expect(normalizeText("What's the weather like?")).toBe("what's the weather like");
215
+ expect(normalizeText('Can you help me with my order?')).toBe('can you help me with my order');
216
+ expect(normalizeText('I need assistance, please!')).toBe('i need assistance please');
217
+ });
218
+
219
+ it('should handle incomplete sentences', () => {
220
+ expect(normalizeText('What is the weather in')).toBe('what is the weather in');
221
+ expect(normalizeText('I am looking for')).toBe('i am looking for');
222
+ expect(normalizeText('Could you please')).toBe('could you please');
223
+ });
224
+
225
+ it('should handle multilingual text', () => {
226
+ expect(normalizeText('Bonjour! Comment ça va?')).toBe('bonjour comment ça va');
227
+ expect(normalizeText('¡Hola! ¿Cómo estás?')).toBe('hola cómo estás');
228
+ expect(normalizeText('Guten Tag! Wie geht es Ihnen?')).toBe('guten tag wie geht es ihnen');
229
+ });
230
+ });
231
+ });
@@ -0,0 +1,76 @@
1
+ // SPDX-FileCopyrightText: 2025 LiveKit, Inc.
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ /**
6
+ * Text normalization utilities for EOU turn detector
7
+ */
8
+
9
+ /**
10
+ * Simple unicode category detection for punctuation
11
+ * Mimics Python's unicodedata.category() for punctuation detection
12
+ */
13
+ export function getUnicodeCategory(char: string): string {
14
+ const code = char.codePointAt(0);
15
+ if (!code) return '';
16
+
17
+ // Basic punctuation ranges (simplified version to match Python unicodedata.category)
18
+ if (
19
+ (code >= 0x21 && code <= 0x2f) || // !"#$%&'()*+,-./
20
+ (code >= 0x3a && code <= 0x40) || // :;<=>?@
21
+ (code >= 0x5b && code <= 0x60) || // [\]^_`
22
+ (code >= 0x7b && code <= 0x7e) || // {|}~
23
+ (code >= 0xa0 && code <= 0xbf) || // Latin-1 punctuation
24
+ (code >= 0x2000 && code <= 0x206f) || // General punctuation
25
+ (code >= 0x3000 && code <= 0x303f)
26
+ ) {
27
+ // CJK symbols and punctuation
28
+ return 'P';
29
+ }
30
+ return '';
31
+ }
32
+
33
+ /**
34
+ * Normalizes text to match the training data format used by the EOU model
35
+ *
36
+ * This function applies the following transformations:
37
+ * 1. Converts to lowercase
38
+ * 2. Applies Unicode NFKC normalization
39
+ * 3. Removes all punctuation except apostrophes (') and hyphens (-)
40
+ * 4. Collapses multiple whitespace characters into single spaces
41
+ * 5. Trims leading and trailing whitespace
42
+ *
43
+ * @param text - The input text to normalize
44
+ * @returns The normalized text
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * normalizeText("Hi, how can I help you today?")
49
+ * // Returns: "hi how can i help you today"
50
+ *
51
+ * normalizeText("I'm a well-trained assistant!")
52
+ * // Returns: "i'm a well-trained assistant"
53
+ *
54
+ * normalizeText("Price: $19.99 (20% off).")
55
+ * // Returns: "price 1999 20 off"
56
+ * ```
57
+ */
58
+ export function normalizeText(text: string): string {
59
+ if (!text) return '';
60
+
61
+ let normalized = text.toLowerCase().normalize('NFKC');
62
+
63
+ // Remove punctuation except apostrophes and hyphens
64
+ // Using character-by-character approach to match Python logic
65
+ normalized = Array.from(normalized)
66
+ .filter((ch) => {
67
+ const category = getUnicodeCategory(ch);
68
+ return !(category.startsWith('P') && ch !== "'" && ch !== '-');
69
+ })
70
+ .join('');
71
+
72
+ // Collapse whitespace and trim
73
+ normalized = normalized.replace(/\s+/g, ' ').trim();
74
+
75
+ return normalized;
76
+ }
@@ -1,129 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
- var turn_detector_exports = {};
20
- __export(turn_detector_exports, {
21
- EOUModel: () => EOUModel,
22
- EOURunner: () => EOURunner,
23
- default: () => turn_detector_default
24
- });
25
- module.exports = __toCommonJS(turn_detector_exports);
26
- var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
27
- var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
28
- var import_transformers = require("@huggingface/transformers");
29
- var import_agents = require("@livekit/agents");
30
- var import_node_url = require("node:url");
31
- var import_onnxruntime_node = require("onnxruntime-node");
32
- const MAX_HISTORY = 4;
33
- class EOURunner extends import_agents.InferenceRunner {
34
- static INFERENCE_METHOD = "lk_end_of_utterance";
35
- #tokenizerPromise;
36
- #session;
37
- #tokenizer;
38
- #logger = (0, import_agents.log)();
39
- constructor() {
40
- super();
41
- this.#tokenizerPromise = import_transformers.AutoTokenizer.from_pretrained("livekit/turn-detector", {
42
- revision: "v1.2.0"
43
- // local_files_only: true, // TODO(nbsp): can't find it
44
- });
45
- this.#session = import_onnxruntime_node.InferenceSession.create(
46
- (0, import_node_url.fileURLToPath)(new URL("turn_detector.onnx", importMetaUrl).href),
47
- {
48
- executionProviders: [{ name: "cpu" }]
49
- }
50
- );
51
- }
52
- async initialize() {
53
- this.#tokenizer = await this.#tokenizerPromise;
54
- }
55
- async run(data) {
56
- const text = this.#formatChatContext(data);
57
- const startTime = Date.now();
58
- const inputs = this.#tokenizer.encode(text, { add_special_tokens: false });
59
- const outputs = await this.#session.then(
60
- (session) => session.run({ input_ids: new import_onnxruntime_node.Tensor("int64", inputs, [1, inputs.length]) }, ["prob"])
61
- );
62
- const endTime = Date.now();
63
- const logits = outputs.prob;
64
- const eouProbability = logits.data[0];
65
- this.#logger.child({ eouProbability, input: text, duration: endTime - startTime }).debug("eou prediction");
66
- return eouProbability;
67
- }
68
- #formatChatContext(ctx) {
69
- const newCtx = [];
70
- for (const msg of ctx) {
71
- if (!msg.content) continue;
72
- newCtx.push(msg);
73
- }
74
- const convoText = this.#tokenizer.apply_chat_template(newCtx, {
75
- add_generation_prompt: false,
76
- tokenize: false
77
- });
78
- return convoText.slice(0, convoText.lastIndexOf("<|im_end|>"));
79
- }
80
- async close() {
81
- await this.#session.then((session) => session.release());
82
- }
83
- }
84
- class EOUModel {
85
- unlikelyThreshold;
86
- #executor;
87
- constructor(unlikelyThreshold = 0.15) {
88
- this.unlikelyThreshold = unlikelyThreshold;
89
- this.#executor = import_agents.CurrentJobContext.getCurrent().inferenceExecutor;
90
- }
91
- supportsLanguage(language) {
92
- if (!language) return false;
93
- const parts = language.toLowerCase().split("-");
94
- return parts[0] === "en" || parts[0] === "english";
95
- }
96
- async predictEndOfTurn(chatCtx) {
97
- let messages = [];
98
- for (const msg of chatCtx.messages) {
99
- if (msg.role !== import_agents.llm.ChatRole.ASSISTANT && msg.role !== import_agents.llm.ChatRole.USER) {
100
- continue;
101
- }
102
- if (typeof msg.content === "string") {
103
- messages.push({
104
- role: msg.role === import_agents.llm.ChatRole.ASSISTANT ? "assistant" : "user",
105
- content: msg.content
106
- });
107
- } else if (Array.isArray(msg.content)) {
108
- for (const content of msg.content) {
109
- if (typeof content === "string") {
110
- messages.push({
111
- role: msg.role === import_agents.llm.ChatRole.ASSISTANT ? "assistant" : "user",
112
- content
113
- });
114
- }
115
- }
116
- }
117
- }
118
- messages = messages.slice(-MAX_HISTORY);
119
- const result = await this.#executor.doInference(EOURunner.INFERENCE_METHOD, messages);
120
- return result;
121
- }
122
- }
123
- var turn_detector_default = EOURunner;
124
- // Annotate the CommonJS export names for ESM import in node:
125
- 0 && (module.exports = {
126
- EOUModel,
127
- EOURunner
128
- });
129
- //# sourceMappingURL=turn_detector.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/turn_detector.ts","../../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.43.7_@types+node@22.5.5__postcss@8.4.38_tsx@4.19.2_typescript@5.4.5/node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2024 LiveKit, Inc.\n//\n// SPDX-License-Identifier: Apache-2.0\nimport type { PreTrainedTokenizer } from '@huggingface/transformers';\nimport { AutoTokenizer } from '@huggingface/transformers';\nimport type { ipc } from '@livekit/agents';\nimport { CurrentJobContext, InferenceRunner, llm, log } from '@livekit/agents';\nimport { fileURLToPath } from 'node:url';\nimport { InferenceSession, Tensor } from 'onnxruntime-node';\n\nconst MAX_HISTORY = 4;\n\ntype RawChatContext = { role: string; content: string }[];\n\nexport class EOURunner extends InferenceRunner {\n static INFERENCE_METHOD = 'lk_end_of_utterance';\n #tokenizerPromise: Promise<PreTrainedTokenizer>;\n #session: Promise<InferenceSession>;\n #tokenizer?: PreTrainedTokenizer;\n #logger = log();\n\n constructor() {\n super();\n this.#tokenizerPromise = AutoTokenizer.from_pretrained('livekit/turn-detector', {\n revision: 'v1.2.0',\n // local_files_only: true, // TODO(nbsp): can't find it\n });\n this.#session = InferenceSession.create(\n fileURLToPath(new URL('turn_detector.onnx', import.meta.url).href),\n {\n executionProviders: [{ name: 'cpu' }],\n },\n );\n }\n\n async initialize() {\n this.#tokenizer = await this.#tokenizerPromise;\n }\n\n async run(data: RawChatContext): Promise<number | undefined> {\n const text = this.#formatChatContext(data);\n const startTime = Date.now();\n const inputs = this.#tokenizer!.encode(text, { add_special_tokens: false });\n const outputs = await this.#session.then((session) =>\n session.run({ input_ids: new Tensor('int64', inputs, [1, inputs.length]) }, ['prob']),\n );\n const endTime = Date.now();\n const logits = outputs.prob!;\n const eouProbability = logits.data[0] as number;\n this.#logger\n .child({ eouProbability, input: text, duration: endTime - startTime })\n .debug('eou prediction');\n return eouProbability;\n }\n\n #formatChatContext(ctx: RawChatContext): string {\n const newCtx: RawChatContext = [];\n for (const msg of ctx) {\n if (!msg.content) continue;\n newCtx.push(msg);\n }\n\n const convoText = this.#tokenizer!.apply_chat_template(newCtx, {\n add_generation_prompt: false,\n tokenize: false,\n }) as string;\n // remove EOU token from current utterance\n return convoText.slice(0, convoText.lastIndexOf('<|im_end|>'));\n }\n\n async close() {\n await this.#session.then((session) => session.release());\n }\n}\n\nexport class EOUModel {\n readonly unlikelyThreshold: number;\n #executor: ipc.InferenceExecutor;\n\n constructor(unlikelyThreshold = 0.15) {\n this.unlikelyThreshold = unlikelyThreshold;\n this.#executor = CurrentJobContext.getCurrent().inferenceExecutor;\n }\n\n supportsLanguage(language?: string) {\n if (!language) return false;\n const parts = language.toLowerCase().split('-');\n return parts[0] === 'en' || parts[0] === 'english';\n }\n\n async predictEndOfTurn(chatCtx: llm.ChatContext): Promise<number> {\n let messages: RawChatContext = [];\n\n for (const msg of chatCtx.messages) {\n if (msg.role !== llm.ChatRole.ASSISTANT && msg.role !== llm.ChatRole.USER) {\n continue;\n }\n\n if (typeof msg.content === 'string') {\n messages.push({\n role: msg.role === llm.ChatRole.ASSISTANT ? 'assistant' : 'user',\n content: msg.content,\n });\n } else if (Array.isArray(msg.content)) {\n for (const content of msg.content) {\n if (typeof content === 'string') {\n messages.push({\n role: msg.role === llm.ChatRole.ASSISTANT ? 'assistant' : 'user',\n content: content,\n });\n }\n }\n }\n }\n messages = messages.slice(-MAX_HISTORY);\n const result = await this.#executor.doInference(EOURunner.INFERENCE_METHOD, messages);\n return result as any;\n }\n}\n\nexport default EOURunner;\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":"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","names":[]}
@@ -1,22 +0,0 @@
1
- import { InferenceRunner, llm } from '@livekit/agents';
2
- type RawChatContext = {
3
- role: string;
4
- content: string;
5
- }[];
6
- export declare class EOURunner extends InferenceRunner {
7
- #private;
8
- static INFERENCE_METHOD: string;
9
- constructor();
10
- initialize(): Promise<void>;
11
- run(data: RawChatContext): Promise<number | undefined>;
12
- close(): Promise<void>;
13
- }
14
- export declare class EOUModel {
15
- #private;
16
- readonly unlikelyThreshold: number;
17
- constructor(unlikelyThreshold?: number);
18
- supportsLanguage(language?: string): boolean;
19
- predictEndOfTurn(chatCtx: llm.ChatContext): Promise<number>;
20
- }
21
- export default EOURunner;
22
- //# sourceMappingURL=turn_detector.d.ts.map
@@ -1,22 +0,0 @@
1
- import { InferenceRunner, llm } from '@livekit/agents';
2
- type RawChatContext = {
3
- role: string;
4
- content: string;
5
- }[];
6
- export declare class EOURunner extends InferenceRunner {
7
- #private;
8
- static INFERENCE_METHOD: string;
9
- constructor();
10
- initialize(): Promise<void>;
11
- run(data: RawChatContext): Promise<number | undefined>;
12
- close(): Promise<void>;
13
- }
14
- export declare class EOUModel {
15
- #private;
16
- readonly unlikelyThreshold: number;
17
- constructor(unlikelyThreshold?: number);
18
- supportsLanguage(language?: string): boolean;
19
- predictEndOfTurn(chatCtx: llm.ChatContext): Promise<number>;
20
- }
21
- export default EOURunner;
22
- //# sourceMappingURL=turn_detector.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"turn_detector.d.ts","sourceRoot":"","sources":["../src/turn_detector.ts"],"names":[],"mappings":"AAMA,OAAO,EAAqB,eAAe,EAAE,GAAG,EAAO,MAAM,iBAAiB,CAAC;AAM/E,KAAK,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,CAAC;AAE1D,qBAAa,SAAU,SAAQ,eAAe;;IAC5C,MAAM,CAAC,gBAAgB,SAAyB;;IAoB1C,UAAU;IAIV,GAAG,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IA+BtD,KAAK;CAGZ;AAED,qBAAa,QAAQ;;IACnB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;gBAGvB,iBAAiB,SAAO;IAKpC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM;IAM5B,gBAAgB,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC;CA4BlE;AAED,eAAe,SAAS,CAAC"}