@nebutra/voice-realtime 0.1.2 → 0.1.4
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 +10 -0
- package/package.json +19 -8
- package/.nebutra/debug/voice-realtime.jsonl +0 -5
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/examples/enroll.ts +0 -5
- package/examples/narration.ts +0 -9
- package/examples/session.ts +0 -5
- package/src/cli.ts +0 -32
- package/src/index.test.ts +0 -33
- package/src/index.ts +0 -158
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @nebutra/voice-realtime
|
|
2
2
|
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies []:
|
|
8
|
+
- @nebutra/capability-kit@2.0.0
|
|
9
|
+
- @nebutra/errors@2.0.0
|
|
10
|
+
- @nebutra/audio-pipeline@0.1.4
|
|
11
|
+
- @nebutra/generation-context@0.1.4
|
|
12
|
+
|
|
3
13
|
## 0.1.2
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/voice-realtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Voice session lifecycle, narration synthesis, and enrollment surface",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"nebutra": {
|
|
9
|
+
"graph": "labs",
|
|
9
10
|
"status": "wip",
|
|
10
11
|
"productionReady": false,
|
|
11
12
|
"surface": "generation-capability",
|
|
@@ -23,16 +24,20 @@
|
|
|
23
24
|
"category": "ai",
|
|
24
25
|
"summary": "Realtime voice session and narration synthesis surface"
|
|
25
26
|
},
|
|
26
|
-
"main": "./
|
|
27
|
-
"types": "./
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
28
29
|
"exports": {
|
|
29
|
-
".":
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
}
|
|
30
35
|
},
|
|
31
36
|
"dependencies": {
|
|
32
|
-
"@nebutra/audio-pipeline": "0.1.
|
|
33
|
-
"@nebutra/
|
|
34
|
-
"@nebutra/
|
|
35
|
-
"@nebutra/
|
|
37
|
+
"@nebutra/audio-pipeline": "0.1.4",
|
|
38
|
+
"@nebutra/capability-kit": "2.0.0",
|
|
39
|
+
"@nebutra/errors": "2.0.0",
|
|
40
|
+
"@nebutra/generation-context": "0.1.4"
|
|
36
41
|
},
|
|
37
42
|
"devDependencies": {
|
|
38
43
|
"@types/node": "^25.9.1",
|
|
@@ -53,6 +58,12 @@
|
|
|
53
58
|
"publishConfig": {
|
|
54
59
|
"access": "public"
|
|
55
60
|
},
|
|
61
|
+
"files": [
|
|
62
|
+
"dist",
|
|
63
|
+
"README.md",
|
|
64
|
+
"LICENSE",
|
|
65
|
+
"CHANGELOG.md"
|
|
66
|
+
],
|
|
56
67
|
"scripts": {
|
|
57
68
|
"build": "tsup",
|
|
58
69
|
"test": "vitest run",
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
{"at":"2026-05-18T04:49:12.879Z","type":"session","session":{"id":"voice_session_thread_1_mpaq5ewf","tenantId":"tenant_a","threadId":"thread_1","room":"voice_tenant_a_thread_1","state":"listening"}}
|
|
2
|
-
{"at":"2026-05-18T04:52:55.989Z","type":"session","session":{"id":"voice_session_thread_1_mpaqa71w","tenantId":"tenant_a","threadId":"thread_1","room":"voice_tenant_a_thread_1","state":"listening"}}
|
|
3
|
-
{"at":"2026-05-18T05:20:20.680Z","type":"session","session":{"id":"voice_session_thread_1_mpar9g3s","tenantId":"tenant_a","threadId":"thread_1","room":"voice_tenant_a_thread_1","state":"listening"}}
|
|
4
|
-
{"at":"2026-05-30T21:06:23.065Z","type":"session","session":{"id":"voice_session_thread_1_mpsuca8p","tenantId":"tenant_a","threadId":"thread_1","room":"voice_tenant_a_thread_1","state":"listening"}}
|
|
5
|
-
{"at":"2026-07-01T01:38:46.615Z","type":"session","session":{"id":"voice_session_thread_1_mr1epzns","tenantId":"tenant_a","threadId":"thread_1","room":"voice_tenant_a_thread_1","state":"listening"}}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/voice-realtime@0.1.2 build /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/voice-realtime
|
|
3
|
-
> tsup
|
|
4
|
-
|
|
5
|
-
CLI Building entry: src/cli.ts, src/index.ts
|
|
6
|
-
CLI Using tsconfig: tsconfig.json
|
|
7
|
-
CLI tsup v8.5.1
|
|
8
|
-
CLI Using tsup config: /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/voice-realtime/tsup.config.ts
|
|
9
|
-
CLI Target: es2022
|
|
10
|
-
CLI Cleaning output folder
|
|
11
|
-
ESM Build start
|
|
12
|
-
ESM dist/cli.js 1.32 KB
|
|
13
|
-
ESM dist/index.js 152.00 B
|
|
14
|
-
ESM dist/chunk-GARLTI7J.js 3.28 KB
|
|
15
|
-
ESM dist/cli.js.map 2.34 KB
|
|
16
|
-
ESM dist/index.js.map 71.00 B
|
|
17
|
-
ESM dist/chunk-GARLTI7J.js.map 6.94 KB
|
|
18
|
-
ESM ⚡️ Build success in 38ms
|
|
19
|
-
DTS Build start
|
|
20
|
-
DTS ⚡️ Build success in 2771ms
|
|
21
|
-
DTS dist/cli.d.ts 13.00 B
|
|
22
|
-
DTS dist/index.d.ts 1.74 KB
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/voice-realtime@0.1.1 test /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/voice-realtime
|
|
3
|
-
> vitest run
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
RUN v4.1.4 /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/ai/voice-realtime
|
|
7
|
-
|
|
8
|
-
✓ src/index.test.ts (2 tests) 69ms
|
|
9
|
-
|
|
10
|
-
Test Files 1 passed (1)
|
|
11
|
-
Tests 2 passed (2)
|
|
12
|
-
Start at 09:38:45
|
|
13
|
-
Duration 1.14s (transform 146ms, setup 0ms, import 184ms, tests 69ms, environment 0ms)
|
|
14
|
-
|
package/examples/enroll.ts
DELETED
package/examples/narration.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
-
import { VoiceRealtime } from "../src/index";
|
|
3
|
-
|
|
4
|
-
const asset = await new VoiceRealtime().synthesizeNarration(
|
|
5
|
-
{ script: "Loop makes debugging visible.", targetDurationS: 3 },
|
|
6
|
-
createDemoBrandContext(),
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
process.stdout.write(`${asset.path}\n`);
|
package/examples/session.ts
DELETED
package/src/cli.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
2
|
-
import { readVoiceDebug, VoiceRealtime } from "./index";
|
|
3
|
-
|
|
4
|
-
const command = process.argv[2] ?? "doctor";
|
|
5
|
-
const voice = new VoiceRealtime();
|
|
6
|
-
|
|
7
|
-
if (command === "doctor") {
|
|
8
|
-
process.stdout.write(
|
|
9
|
-
`${JSON.stringify({ capability: "voice-realtime", results: await voice.doctor() }, null, 2)}\n`,
|
|
10
|
-
);
|
|
11
|
-
} else if (command === "debug") {
|
|
12
|
-
process.stdout.write(
|
|
13
|
-
`${JSON.stringify({ capability: "voice-realtime", entries: await readVoiceDebug() }, null, 2)}\n`,
|
|
14
|
-
);
|
|
15
|
-
} else if (command === "enroll") {
|
|
16
|
-
process.stdout.write(
|
|
17
|
-
`${JSON.stringify({ capability: "voice-realtime", profile: await voice.enroll({ tenantId: "local" }) }, null, 2)}\n`,
|
|
18
|
-
);
|
|
19
|
-
} else if (command === "test-mic") {
|
|
20
|
-
process.stdout.write(
|
|
21
|
-
`${JSON.stringify({ capability: "voice-realtime", mic: await voice.testMic() }, null, 2)}\n`,
|
|
22
|
-
);
|
|
23
|
-
} else if (command === "narrate") {
|
|
24
|
-
const asset = await voice.synthesizeNarration(
|
|
25
|
-
{ script: "Loop helps indie developers debug the work that matters.", targetDurationS: 3 },
|
|
26
|
-
createDemoBrandContext(),
|
|
27
|
-
);
|
|
28
|
-
process.stdout.write(`${JSON.stringify({ capability: "voice-realtime", asset }, null, 2)}\n`);
|
|
29
|
-
} else {
|
|
30
|
-
process.stderr.write(`Unknown voice-realtime command: ${command}\n`);
|
|
31
|
-
process.exitCode = 1;
|
|
32
|
-
}
|
package/src/index.test.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { createDemoBrandContext } from "@nebutra/generation-context";
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
-
import { readVoiceDebug, VoiceRealtime } from "./index";
|
|
7
|
-
|
|
8
|
-
let root: string | undefined;
|
|
9
|
-
|
|
10
|
-
afterEach(async () => {
|
|
11
|
-
if (root) await rm(root, { recursive: true, force: true });
|
|
12
|
-
root = undefined;
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe("VoiceRealtime", () => {
|
|
16
|
-
it("starts a thread-bound voice session without importing the runtime", async () => {
|
|
17
|
-
const session = await new VoiceRealtime().startSession({
|
|
18
|
-
tenantId: "tenant_a",
|
|
19
|
-
threadId: "thread_1",
|
|
20
|
-
});
|
|
21
|
-
expect(session.state).toBe("listening");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("synthesizes narration with BrandContext", async () => {
|
|
25
|
-
root = await mkdtemp(join(tmpdir(), "voice-realtime-"));
|
|
26
|
-
const asset = await new VoiceRealtime({ root }).synthesizeNarration(
|
|
27
|
-
{ script: "Ship the story with the founder voice.", targetDurationS: 2 },
|
|
28
|
-
createDemoBrandContext(),
|
|
29
|
-
);
|
|
30
|
-
expect((await readFile(asset.path)).subarray(0, 4).toString()).toBe("RIFF");
|
|
31
|
-
expect(await readVoiceDebug(root)).toHaveLength(1);
|
|
32
|
-
});
|
|
33
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { createToneWav } from "@nebutra/audio-pipeline";
|
|
4
|
-
import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
|
|
5
|
-
import {
|
|
6
|
-
assetId,
|
|
7
|
-
type BrandContext,
|
|
8
|
-
type GeneratedAsset,
|
|
9
|
-
requireBrandContext,
|
|
10
|
-
} from "@nebutra/generation-context";
|
|
11
|
-
|
|
12
|
-
export type VoiceState = "listening" | "thinking" | "speaking" | "paused" | "closed";
|
|
13
|
-
|
|
14
|
-
export interface VoiceSession {
|
|
15
|
-
readonly id: string;
|
|
16
|
-
readonly tenantId: string;
|
|
17
|
-
readonly threadId: string;
|
|
18
|
-
readonly room: string;
|
|
19
|
-
readonly state: VoiceState;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface NarrationRequest {
|
|
23
|
-
readonly script: string;
|
|
24
|
-
readonly voiceProfileId?: string;
|
|
25
|
-
readonly targetDurationS?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface VoiceAsset extends GeneratedAsset {
|
|
29
|
-
readonly kind: "voice";
|
|
30
|
-
readonly transcript: string;
|
|
31
|
-
readonly durationS: number;
|
|
32
|
-
readonly format: "wav";
|
|
33
|
-
readonly voiceProfileId: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface VoiceProfile {
|
|
37
|
-
readonly id: string;
|
|
38
|
-
readonly tenantId: string;
|
|
39
|
-
readonly consentRecordedAt: string;
|
|
40
|
-
readonly sampleCount: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface VoiceHealth {
|
|
44
|
-
readonly provider: string;
|
|
45
|
-
readonly ok: boolean;
|
|
46
|
-
readonly suggestion?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface VoiceRealtimeOptions {
|
|
50
|
-
readonly root?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function readVoiceDebug(root = process.cwd(), limit = 10): Promise<unknown[]> {
|
|
54
|
-
return readCapabilityDebug("voice-realtime", { root, limit });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export class VoiceRealtime {
|
|
58
|
-
readonly #root: string;
|
|
59
|
-
|
|
60
|
-
constructor(options: VoiceRealtimeOptions = {}) {
|
|
61
|
-
this.#root = options.root ?? process.cwd();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async startSession(input: { tenantId: string; threadId: string }): Promise<VoiceSession> {
|
|
65
|
-
const session: VoiceSession = {
|
|
66
|
-
id: assetId("voice_session", input.threadId),
|
|
67
|
-
tenantId: input.tenantId,
|
|
68
|
-
threadId: input.threadId,
|
|
69
|
-
room: `voice_${input.tenantId}_${input.threadId}`,
|
|
70
|
-
state: "listening",
|
|
71
|
-
};
|
|
72
|
-
await appendCapabilityDebug(
|
|
73
|
-
"voice-realtime",
|
|
74
|
-
{ type: "session", session },
|
|
75
|
-
{ root: this.#root },
|
|
76
|
-
);
|
|
77
|
-
return session;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async synthesizeNarration(
|
|
81
|
-
request: NarrationRequest,
|
|
82
|
-
brandInput: BrandContext | undefined,
|
|
83
|
-
): Promise<VoiceAsset> {
|
|
84
|
-
const brand = requireBrandContext(brandInput, "voice-realtime");
|
|
85
|
-
const durationS = Math.max(
|
|
86
|
-
1,
|
|
87
|
-
Math.min(request.targetDurationS ?? Math.ceil(request.script.length / 24), 20),
|
|
88
|
-
);
|
|
89
|
-
const id = assetId("voice", `${brand.brandId}_${request.script}`);
|
|
90
|
-
const path = join(this.#root, ".nebutra", "generated", "voice-realtime", `${id}.wav`);
|
|
91
|
-
await mkdir(dirname(path), { recursive: true });
|
|
92
|
-
await writeFile(path, createToneWav(durationS, 330));
|
|
93
|
-
const asset: VoiceAsset = {
|
|
94
|
-
id,
|
|
95
|
-
tenantId: brand.tenantId,
|
|
96
|
-
kind: "voice",
|
|
97
|
-
path,
|
|
98
|
-
brandId: brand.brandId,
|
|
99
|
-
provider: "tone-local",
|
|
100
|
-
model: "brand-context-narration-v1",
|
|
101
|
-
createdAt: new Date().toISOString(),
|
|
102
|
-
license: { status: "commercial-ok", source: "deterministic local renderer" },
|
|
103
|
-
transcript: request.script,
|
|
104
|
-
durationS,
|
|
105
|
-
format: "wav",
|
|
106
|
-
voiceProfileId: request.voiceProfileId ?? "default",
|
|
107
|
-
metadata: { brandSource: brand.sourcePath },
|
|
108
|
-
};
|
|
109
|
-
await appendCapabilityDebug(
|
|
110
|
-
"voice-realtime",
|
|
111
|
-
{ type: "narration", asset },
|
|
112
|
-
{ root: this.#root },
|
|
113
|
-
);
|
|
114
|
-
return asset;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async enroll(input: {
|
|
118
|
-
tenantId: string;
|
|
119
|
-
samplePaths?: readonly string[];
|
|
120
|
-
}): Promise<VoiceProfile> {
|
|
121
|
-
const profile: VoiceProfile = {
|
|
122
|
-
id: assetId("voice_profile", input.tenantId),
|
|
123
|
-
tenantId: input.tenantId,
|
|
124
|
-
consentRecordedAt: new Date().toISOString(),
|
|
125
|
-
sampleCount: input.samplePaths?.length ?? 0,
|
|
126
|
-
};
|
|
127
|
-
await appendCapabilityDebug(
|
|
128
|
-
"voice-realtime",
|
|
129
|
-
{ type: "enroll", profile },
|
|
130
|
-
{ root: this.#root },
|
|
131
|
-
);
|
|
132
|
-
return profile;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async testMic(): Promise<VoiceHealth> {
|
|
136
|
-
return {
|
|
137
|
-
provider: "local-mic",
|
|
138
|
-
ok: Boolean(process.env.VOICE_MIC_PERMISSION === "granted"),
|
|
139
|
-
suggestion: "Grant microphone access in the desktop app before realtime sessions.",
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async doctor(): Promise<VoiceHealth[]> {
|
|
144
|
-
return [
|
|
145
|
-
await this.testMic(),
|
|
146
|
-
{
|
|
147
|
-
provider: "voice-sidecar",
|
|
148
|
-
ok: Boolean(process.env.VOICE_SIDECAR_URL),
|
|
149
|
-
suggestion: "Set VOICE_SIDECAR_URL to enable realtime WebRTC/STT/TTS.",
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
provider: "remote-tts",
|
|
153
|
-
ok: Boolean(process.env.VOICE_REMOTE_API_KEY),
|
|
154
|
-
suggestion: "Set VOICE_REMOTE_API_KEY to enable remote voice fallback.",
|
|
155
|
-
},
|
|
156
|
-
];
|
|
157
|
-
}
|
|
158
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"target": "esnext",
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"incremental": false
|
|
9
|
-
},
|
|
10
|
-
"include": ["src", "examples"],
|
|
11
|
-
"exclude": ["node_modules", "dist"]
|
|
12
|
-
}
|
package/tsup.config.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "tsup";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
entry: ["src/index.ts", "src/cli.ts"],
|
|
5
|
-
format: ["esm"],
|
|
6
|
-
dts: true,
|
|
7
|
-
sourcemap: true,
|
|
8
|
-
clean: true,
|
|
9
|
-
target: "es2022",
|
|
10
|
-
external: [
|
|
11
|
-
"@nebutra/audio-pipeline",
|
|
12
|
-
"@nebutra/capability-kit/debug",
|
|
13
|
-
"@nebutra/errors",
|
|
14
|
-
"@nebutra/generation-context",
|
|
15
|
-
],
|
|
16
|
-
});
|