@kuralle-syrinx/core 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.
Files changed (52) hide show
  1. package/README.md +41 -0
  2. package/package.json +22 -0
  3. package/src/audio/audio.test.ts +285 -0
  4. package/src/audio/index.ts +5 -0
  5. package/src/audio/mulaw.ts +42 -0
  6. package/src/audio/pcm.ts +43 -0
  7. package/src/audio/resample.ts +177 -0
  8. package/src/audio-envelope.test.ts +167 -0
  9. package/src/audio-envelope.ts +143 -0
  10. package/src/conversation-event.ts +62 -0
  11. package/src/error-handler.test.ts +56 -0
  12. package/src/error-handler.ts +149 -0
  13. package/src/idle-timeout.ts +210 -0
  14. package/src/index.ts +215 -0
  15. package/src/init-chain.ts +137 -0
  16. package/src/init-stage-order.ts +79 -0
  17. package/src/latency-filler-fixtures.ts +16 -0
  18. package/src/latency-filler.test.ts +62 -0
  19. package/src/latency-filler.ts +125 -0
  20. package/src/mode-switcher.ts +110 -0
  21. package/src/observability-observer.test.ts +245 -0
  22. package/src/observability-observer.ts +195 -0
  23. package/src/observability.test.ts +85 -0
  24. package/src/observability.ts +93 -0
  25. package/src/packet-factories.test.ts +34 -0
  26. package/src/packet-factories.ts +243 -0
  27. package/src/packets.ts +553 -0
  28. package/src/pipeline-bus.g10.test.ts +145 -0
  29. package/src/pipeline-bus.test.ts +197 -0
  30. package/src/pipeline-bus.ts +369 -0
  31. package/src/plugin-contract.ts +79 -0
  32. package/src/primary-speaker-fixtures.ts +45 -0
  33. package/src/primary-speaker-gate.test.ts +150 -0
  34. package/src/primary-speaker-gate.ts +186 -0
  35. package/src/provider-fallback.test.ts +87 -0
  36. package/src/provider-fallback.ts +88 -0
  37. package/src/reasoner.test.ts +69 -0
  38. package/src/reasoner.ts +54 -0
  39. package/src/retry.test.ts +83 -0
  40. package/src/retry.ts +106 -0
  41. package/src/scheduler.ts +28 -0
  42. package/src/tts-playout-clock.test.ts +125 -0
  43. package/src/tts-playout-clock.ts +116 -0
  44. package/src/turn-arbiter.characterization.test.ts +477 -0
  45. package/src/turn-arbiter.test.ts +567 -0
  46. package/src/turn-arbiter.ts +283 -0
  47. package/src/voice-agent-session-util.ts +240 -0
  48. package/src/voice-agent-session.test.ts +2487 -0
  49. package/src/voice-agent-session.ts +1175 -0
  50. package/src/voice-text.test.ts +81 -0
  51. package/src/voice-text.ts +102 -0
  52. package/tsconfig.json +21 -0
@@ -0,0 +1,81 @@
1
+ // SPDX-License-Identifier: MIT
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { takeCompleteVoiceText, isCompleteVoiceText, appendVoiceText } from "./voice-text.js";
5
+
6
+ describe("isCompleteVoiceText", () => {
7
+ it("treats terminal punctuation as complete", () => {
8
+ expect(isCompleteVoiceText("Hello there.")).toBe(true);
9
+ expect(isCompleteVoiceText("Really?!")).toBe(true);
10
+ });
11
+
12
+ it("treats an unterminated fragment as incomplete", () => {
13
+ expect(isCompleteVoiceText("Hello there")).toBe(false);
14
+ expect(isCompleteVoiceText("and then we")).toBe(false);
15
+ });
16
+
17
+ it("looks past trailing closing quotes/brackets to the terminator", () => {
18
+ expect(isCompleteVoiceText('She said "hi."')).toBe(true);
19
+ expect(isCompleteVoiceText("(a complete aside.)")).toBe(true);
20
+ expect(isCompleteVoiceText('an open quote "')).toBe(false);
21
+ });
22
+
23
+ it("recognizes non-English terminal punctuation", () => {
24
+ expect(isCompleteVoiceText("こんにちは。")).toBe(true); // Japanese full stop
25
+ expect(isCompleteVoiceText("مرحبا؟")).toBe(true); // Arabic question mark
26
+ expect(isCompleteVoiceText("नमस्ते।")).toBe(true); // Devanagari danda
27
+ });
28
+ });
29
+
30
+ describe("takeCompleteVoiceText", () => {
31
+ it("splits leading complete sentences from the incomplete remainder", () => {
32
+ const { text, remaining } = takeCompleteVoiceText("One. Two. Thre");
33
+ expect(text).toBe("One. Two.");
34
+ expect(remaining).toBe("Thre");
35
+ });
36
+
37
+ it("returns no text when nothing is complete yet", () => {
38
+ const { text, remaining } = takeCompleteVoiceText("still going");
39
+ expect(text).toBe("");
40
+ expect(remaining).toBe("still going");
41
+ });
42
+
43
+ it("emits multiple complete sentences and buffers only the trailing fragment", () => {
44
+ const { text, remaining } = takeCompleteVoiceText("Done. And more.");
45
+ expect(text).toBe("Done. And more.");
46
+ expect(remaining).toBe("");
47
+ });
48
+
49
+ it("buffers the trailing incomplete fragment after a complete sentence", () => {
50
+ // Capitalized continuation so the locale-aware segmenter treats it as a new
51
+ // (still-incomplete) sentence rather than one run-on.
52
+ const { text, remaining } = takeCompleteVoiceText("Done. And more");
53
+ expect(text).toBe("Done.");
54
+ expect(remaining).toBe("And more");
55
+ });
56
+ });
57
+
58
+ describe("appendVoiceText", () => {
59
+ it("seeds from empty and trims", () => {
60
+ expect(appendVoiceText("", " hi ")).toBe("hi");
61
+ });
62
+
63
+ it("joins with a single space when neither side has whitespace at the seam", () => {
64
+ expect(appendVoiceText("Hello", "there")).toBe("Hello there");
65
+ });
66
+
67
+ it("does not double-space when the existing side already ends in whitespace", () => {
68
+ expect(appendVoiceText("Hello ", "there")).toBe("Hello there");
69
+ });
70
+
71
+ it("trims a whitespace-led next fragment (the seam's space collapses)", () => {
72
+ // Current behavior: a leading-whitespace `next` is trimmed and concatenated
73
+ // directly. Reachable inputs (trimmed segment text) never hit this path; the
74
+ // test pins the documented behavior rather than the intuitive one.
75
+ expect(appendVoiceText("Hello", " there")).toBe("Hellothere");
76
+ });
77
+
78
+ it("returns the existing text unchanged when the next fragment is blank", () => {
79
+ expect(appendVoiceText("Hello", " ")).toBe("Hello");
80
+ });
81
+ });
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // Syrinx Kernel v2 — Voice Text Segmentation
4
+ //
5
+ // Pure helpers for turning a streaming LLM token feed into complete, speakable
6
+ // sentence segments. No bus, no state — extracted from VoiceAgentSession so the
7
+ // orchestrator owns wiring, not text rules.
8
+
9
+ interface SentenceSegment {
10
+ segment: string;
11
+ }
12
+
13
+ /**
14
+ * Split off the leading run of complete sentences from `text`, returning the
15
+ * speakable prefix and the still-incomplete remainder. A segment is "complete"
16
+ * when it ends in terminal punctuation (optionally followed by closing quotes).
17
+ */
18
+ export function takeCompleteVoiceText(text: string): { text: string; remaining: string } {
19
+ const segments = segmentSentences(text);
20
+ let emitted = "";
21
+ let remaining = "";
22
+ for (const segment of segments) {
23
+ if (remaining) {
24
+ remaining += segment;
25
+ continue;
26
+ }
27
+ if (isCompleteVoiceText(segment)) {
28
+ emitted += segment;
29
+ } else {
30
+ remaining = segment;
31
+ }
32
+ }
33
+ return { text: emitted.trimEnd(), remaining };
34
+ }
35
+
36
+ export function isCompleteVoiceText(text: string): boolean {
37
+ const trimmed = text.trim();
38
+ for (let index = trimmed.length - 1; index >= 0; index -= 1) {
39
+ const char = trimmed[index]!;
40
+ if (isClosingPunctuation(char)) continue;
41
+ return isTerminalPunctuation(char);
42
+ }
43
+ return false;
44
+ }
45
+
46
+ /** Join two voice-text fragments, normalizing whitespace at the seam. */
47
+ export function appendVoiceText(existing: string, next: string): string {
48
+ const normalizedNext = next.trim();
49
+ if (!existing) return normalizedNext;
50
+ if (!normalizedNext) return existing;
51
+ if (/\s$/.test(existing) || /^\s/.test(next)) return `${existing}${normalizedNext}`;
52
+ return `${existing} ${normalizedNext}`;
53
+ }
54
+
55
+ function isClosingPunctuation(char: string): boolean {
56
+ return char === ")" || char === "]" || char === "}" || char === "\"" || char === "'" || char === "”" || char === "’";
57
+ }
58
+
59
+ function isTerminalPunctuation(char: string): boolean {
60
+ return char === "." ||
61
+ char === "!" ||
62
+ char === "?" ||
63
+ char === "。" ||
64
+ char === "!" ||
65
+ char === "?" ||
66
+ char === "؟" ||
67
+ char === "।" ||
68
+ char === "॥";
69
+ }
70
+
71
+ function segmentSentences(text: string): string[] {
72
+ const segmenter = createSentenceSegmenter();
73
+ if (segmenter) {
74
+ return Array.from(segmenter.segment(text), (part) => part.segment);
75
+ }
76
+
77
+ const segments: string[] = [];
78
+ let start = 0;
79
+ for (let index = 0; index < text.length; index += 1) {
80
+ if (!isTerminalPunctuation(text[index]!)) continue;
81
+ let end = index + 1;
82
+ while (end < text.length && isClosingPunctuation(text[end]!)) end += 1;
83
+ if (end < text.length && !/\s/.test(text[end]!)) continue;
84
+ segments.push(text.slice(start, end));
85
+ start = end;
86
+ }
87
+ if (start < text.length) segments.push(text.slice(start));
88
+ return segments;
89
+ }
90
+
91
+ function createSentenceSegmenter(): { segment(text: string): Iterable<SentenceSegment> } | null {
92
+ const Segmenter = (Intl as unknown as { Segmenter?: new (
93
+ locale?: string | string[],
94
+ options?: { granularity: "sentence" },
95
+ ) => { segment(text: string): Iterable<SentenceSegment> } }).Segmenter;
96
+ if (!Segmenter) return null;
97
+ try {
98
+ return new Segmenter(undefined, { granularity: "sentence" });
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
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
+ }