@kuralle-syrinx/pipecat-smart-turn 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ export type SemanticCompletenessLabel = "complete" | "incomplete" | "backchannel";
2
+
3
+ export interface SemanticCompletenessScore {
4
+ readonly complete: boolean;
5
+ readonly label: SemanticCompletenessLabel;
6
+ readonly confidence: number;
7
+ }
8
+
9
+ export interface SemanticEndpointFusionConfig {
10
+ readonly enabled: boolean;
11
+ readonly finalizeDelayMs: number;
12
+ readonly semanticShortcutDelayMs: number;
13
+ readonly incompleteFallbackMs: number;
14
+ }
15
+
16
+ export interface EndpointFusionDecision {
17
+ readonly release: boolean;
18
+ readonly requestFinalize: boolean;
19
+ readonly finalizeDelayMs: number;
20
+ readonly deferReason?: "semantic_incomplete";
21
+ readonly shortcutReason?: "semantic_complete";
22
+ }
23
+
24
+ const TRAILING_INCOMPLETE =
25
+ /\b(and|but|or|so|because|if|when|while|although|though|since|unless|until|as|that|which|who|where|how|what|why|the|a|an|to|for|of|in|on|at|with|about|from|by|into|through|after|before|during|without|within|between|among|over|under|around|against|toward|towards|upon|like|than|not|please|just|also|then|well|um|uh|er|hmm|i|we|you|he|she|they|my|your|our|their|this|these|those)\s*$/i;
26
+
27
+ const EXACT_INCOMPLETE = [
28
+ /^i need to know$/i,
29
+ /^i want to know$/i,
30
+ /^can you tell me$/i,
31
+ /^how do i$/i,
32
+ /^what is the$/i,
33
+ ];
34
+
35
+ const INCOMPLETE_PREFIXES: readonly RegExp[] = [
36
+ /^i need to\b/i,
37
+ /^i want to\b/i,
38
+ /^can you tell me about\b/i,
39
+ /^what is the\b/i,
40
+ /^how do i\b/i,
41
+ /^i'm trying to\b/i,
42
+ /^i am trying to\b/i,
43
+ /^could you help me with\b/i,
44
+ /^tell me about the\b/i,
45
+ ];
46
+
47
+ const OPEN_ENDED_PREFIXES: readonly RegExp[] = [
48
+ /^i was wondering if\b/i,
49
+ ];
50
+
51
+ const BACKCHANNEL =
52
+ /^(yeah|yes|yep|yup|uh-?huh|mm-?hmm|mhm|right|okay|ok|sure|got it|thank you|thanks|no|nope|nah)\.?$/i;
53
+
54
+ const SENTENCE_END = /[.!?]["')]*\s*$/;
55
+
56
+ export function scoreSemanticCompleteness(text: string): SemanticCompletenessScore {
57
+ const normalized = text.trim().replace(/\s+/g, " ");
58
+ if (!normalized) {
59
+ return { complete: false, label: "incomplete", confidence: 0 };
60
+ }
61
+
62
+ if (BACKCHANNEL.test(normalized)) {
63
+ return { complete: true, label: "backchannel", confidence: 0.95 };
64
+ }
65
+
66
+ if (SENTENCE_END.test(normalized)) {
67
+ return { complete: true, label: "complete", confidence: 0.9 };
68
+ }
69
+
70
+ if (TRAILING_INCOMPLETE.test(normalized)) {
71
+ return { complete: false, label: "incomplete", confidence: 0.85 };
72
+ }
73
+
74
+ for (const phrase of EXACT_INCOMPLETE) {
75
+ if (phrase.test(normalized)) {
76
+ return { complete: false, label: "incomplete", confidence: 0.85 };
77
+ }
78
+ }
79
+
80
+ for (const prefix of INCOMPLETE_PREFIXES) {
81
+ if (!prefix.test(normalized)) continue;
82
+ const remainder = normalized.replace(prefix, "").trim();
83
+ if (!remainder) {
84
+ return { complete: false, label: "incomplete", confidence: 0.8 };
85
+ }
86
+ }
87
+
88
+ for (const prefix of OPEN_ENDED_PREFIXES) {
89
+ if (prefix.test(normalized)) {
90
+ return { complete: false, label: "incomplete", confidence: 0.75 };
91
+ }
92
+ }
93
+
94
+ if (/,\s*$/.test(normalized)) {
95
+ return { complete: false, label: "incomplete", confidence: 0.75 };
96
+ }
97
+
98
+ const words = normalized.split(/\s+/);
99
+ if (
100
+ /^(what|where|when|why|how|who|which|is|are|do|does|did|can|could|would|will|should|have|has|had)\b/i.test(
101
+ normalized,
102
+ ) &&
103
+ words.length >= 4
104
+ ) {
105
+ return { complete: true, label: "complete", confidence: 0.7 };
106
+ }
107
+
108
+ return { complete: true, label: "complete", confidence: 0.55 };
109
+ }
110
+
111
+ export function fuseEndpointDecision(
112
+ smartTurnComplete: boolean,
113
+ semantic: SemanticCompletenessScore,
114
+ config: SemanticEndpointFusionConfig,
115
+ ): EndpointFusionDecision {
116
+ if (!config.enabled) {
117
+ return {
118
+ release: smartTurnComplete,
119
+ requestFinalize: smartTurnComplete,
120
+ finalizeDelayMs: config.finalizeDelayMs,
121
+ };
122
+ }
123
+
124
+ if (smartTurnComplete && semantic.complete) {
125
+ return {
126
+ release: true,
127
+ requestFinalize: true,
128
+ finalizeDelayMs: config.finalizeDelayMs,
129
+ };
130
+ }
131
+
132
+ if (smartTurnComplete && !semantic.complete) {
133
+ return {
134
+ release: false,
135
+ requestFinalize: false,
136
+ finalizeDelayMs: config.finalizeDelayMs,
137
+ deferReason: "semantic_incomplete",
138
+ };
139
+ }
140
+
141
+ if (!smartTurnComplete && semantic.complete && semantic.confidence >= 0.85 && config.semanticShortcutDelayMs > 0) {
142
+ return {
143
+ release: true,
144
+ requestFinalize: true,
145
+ finalizeDelayMs: config.semanticShortcutDelayMs,
146
+ shortcutReason: "semantic_complete",
147
+ };
148
+ }
149
+
150
+ return {
151
+ release: false,
152
+ requestFinalize: false,
153
+ finalizeDelayMs: config.incompleteFallbackMs,
154
+ };
155
+ }
156
+
157
+ export function latestTranscript(finalSegments: readonly string[], interimText: string): string {
158
+ const finals = finalSegments.join(" ").replace(/\s+/g, " ").trim();
159
+ const interim = interimText.trim();
160
+ if (!interim) return finals;
161
+ if (!finals) return interim;
162
+ if (interim.startsWith(finals) || finals.startsWith(interim)) {
163
+ return interim.length >= finals.length ? interim : finals;
164
+ }
165
+ return `${finals} ${interim}`.replace(/\s+/g, " ").trim();
166
+ }
@@ -0,0 +1,20 @@
1
+ export interface SemanticLabeledUtterance {
2
+ readonly id: string;
3
+ readonly text: string;
4
+ readonly category: "complete" | "mid_thought_pause" | "backchannel";
5
+ }
6
+
7
+ export const SEMANTIC_LABELED_UTTERANCES: readonly SemanticLabeledUtterance[] = [
8
+ { id: "complete-question", text: "What are your office hours?", category: "complete" },
9
+ { id: "complete-statement", text: "I would like to schedule an appointment.", category: "complete" },
10
+ { id: "complete-imperative", text: "Please send me the enrollment form.", category: "complete" },
11
+ { id: "complete-multi-clause", text: "I applied last week, and I still have not heard back.", category: "complete" },
12
+ { id: "mid-thought-conjunction", text: "I need to know whether the petition is approved and", category: "mid_thought_pause" },
13
+ { id: "mid-thought-prefix", text: "I need to know", category: "mid_thought_pause" },
14
+ { id: "mid-thought-comma", text: "Before I submit the form,", category: "mid_thought_pause" },
15
+ { id: "mid-thought-article", text: "Can you tell me about the", category: "mid_thought_pause" },
16
+ { id: "backchannel-yeah", text: "yeah", category: "backchannel" },
17
+ { id: "backchannel-uh-huh", text: "uh-huh", category: "backchannel" },
18
+ { id: "backchannel-okay", text: "okay", category: "backchannel" },
19
+ { id: "backchannel-thanks", text: "thank you", category: "backchannel" },
20
+ ];
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "noImplicitReturns": true,
10
+ "declaration": true,
11
+ "declarationMap": true,
12
+ "sourceMap": true,
13
+ "outDir": "./dist",
14
+ "rootDir": "./src",
15
+ "esModuleInterop": true,
16
+ "skipLibCheck": true,
17
+ "forceConsistentCasingInFileNames": true
18
+ },
19
+ "include": ["src/**/*.ts"],
20
+ "exclude": ["node_modules", "dist"]
21
+ }