@scotthuang/engram 0.6.5 → 0.6.7

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,49 @@
1
+ /**
2
+ * Voice Identify — 腾讯云 ASR 说话人分离 + 本地声纹匹配
3
+ *
4
+ * 流程:
5
+ * 1. 音频 base64 → 腾讯云 ASR(SpeakerDiarization=1)→ 转写 + speaker_0/1/2...
6
+ * 2. 按 SpeakerId 分组,取每人最长段的时间戳
7
+ * 3. 从原始 WAV 按时间戳切片
8
+ * 4. 切片 → 本地 /api/voice/extract → 192d embedding
9
+ * 5. embedding → voice-store 匹配 → 人名
10
+ * 6. 合并输出:[{speaker: "Scott", text: "...", startMs, endMs}, ...]
11
+ */
12
+ import { VoiceStore } from "./voice-store.js";
13
+ export interface SpeakerSegment {
14
+ speakerId: number;
15
+ speakerName: string;
16
+ text: string;
17
+ startMs: number;
18
+ endMs: number;
19
+ confidence: number;
20
+ }
21
+ export interface VoiceIdentifyResult {
22
+ segments: SpeakerSegment[];
23
+ fullText: string;
24
+ speakerCount: number;
25
+ elapsedMs: number;
26
+ }
27
+ export interface TencentASRConfig {
28
+ secretId: string;
29
+ secretKey: string;
30
+ region?: string;
31
+ engineType?: string;
32
+ }
33
+ export declare class VoiceIdentifier {
34
+ private asrConfig;
35
+ private voiceStore;
36
+ private serviceUrl;
37
+ constructor(asrConfig: TencentASRConfig, voiceStore: VoiceStore, serviceUrl?: string);
38
+ /**
39
+ * 完整识别流程:ASR + 说话人分离 + 声纹匹配
40
+ *
41
+ * @param audioPath WAV 文件路径(16kHz mono 16-bit)
42
+ * @returns 每段话的说话人 + 文本 + 时间戳
43
+ */
44
+ identify(audioPath: string): Promise<VoiceIdentifyResult>;
45
+ /**
46
+ * 从 Base64 音频识别(先保存为临时文件再处理)
47
+ */
48
+ identifyFromBase64(audioBase64: string, format?: string): Promise<VoiceIdentifyResult>;
49
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Voice Identify — 腾讯云 ASR 说话人分离 + 本地声纹匹配
3
+ *
4
+ * 流程:
5
+ * 1. 音频 base64 → 腾讯云 ASR(SpeakerDiarization=1)→ 转写 + speaker_0/1/2...
6
+ * 2. 按 SpeakerId 分组,取每人最长段的时间戳
7
+ * 3. 从原始 WAV 按时间戳切片
8
+ * 4. 切片 → 本地 /api/voice/extract → 192d embedding
9
+ * 5. embedding → voice-store 匹配 → 人名
10
+ * 6. 合并输出:[{speaker: "Scott", text: "...", startMs, endMs}, ...]
11
+ */
12
+ import { promises as fs } from "node:fs";
13
+ import { join } from "node:path";
14
+ import * as crypto from "node:crypto";
15
+ import { logger } from "../../src/logger.js";
16
+ // ---- 腾讯云 API 签名 ----
17
+ function sha256(data) {
18
+ return crypto.createHash("sha256").update(data).digest("hex");
19
+ }
20
+ function hmacSha256(key, data) {
21
+ return crypto.createHmac("sha256", key).update(data).digest();
22
+ }
23
+ function buildTencentSignature(secretId, secretKey, timestamp, payload) {
24
+ const date = new Date(timestamp * 1000).toISOString().split("T")[0];
25
+ const service = "asr";
26
+ const canonicalRequest = [
27
+ "POST",
28
+ "/",
29
+ "",
30
+ "content-type:application/json",
31
+ "host:asr.tencentcloudapi.com",
32
+ "",
33
+ "content-type;host",
34
+ sha256(payload),
35
+ ].join("\n");
36
+ const credentialScope = `${date}/${service}/tc3_request`;
37
+ const stringToSign = [
38
+ "TC3-HMAC-SHA256",
39
+ String(timestamp),
40
+ credentialScope,
41
+ sha256(canonicalRequest),
42
+ ].join("\n");
43
+ const secretDate = hmacSha256(`TC3${secretKey}`, date);
44
+ const secretService = hmacSha256(secretDate, service);
45
+ const secretSigning = hmacSha256(secretService, "tc3_request");
46
+ const signature = hmacSha256(secretSigning, stringToSign).toString("hex");
47
+ return `TC3-HMAC-SHA256 Credential=${secretId}/${credentialScope}, SignedHeaders=content-type;host, Signature=${signature}`;
48
+ }
49
+ // ---- 腾讯云 ASR 调用 ----
50
+ async function callTencentASR(config, audioBase64) {
51
+ const payload = JSON.stringify({
52
+ EngineModelType: config.engineType || "16k_zh",
53
+ ChannelNum: 1,
54
+ SourceType: 1,
55
+ Data: audioBase64,
56
+ ResTextFormat: 2,
57
+ SpeakerDiarization: 1,
58
+ SpeakerNumber: 0,
59
+ });
60
+ const timestamp = Math.floor(Date.now() / 1000);
61
+ const authorization = buildTencentSignature(config.secretId, config.secretKey, timestamp, payload);
62
+ const res = await fetch("https://asr.tencentcloudapi.com", {
63
+ method: "POST",
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ "Host": "asr.tencentcloudapi.com",
67
+ "X-TC-Action": "CreateRecTask",
68
+ "X-TC-Version": "2019-06-14",
69
+ "X-TC-Timestamp": String(timestamp),
70
+ "X-TC-Region": config.region || "ap-guangzhou",
71
+ "Authorization": authorization,
72
+ },
73
+ body: payload,
74
+ });
75
+ const data = await res.json();
76
+ if (data.Response?.Error) {
77
+ throw new Error(`Tencent ASR error: ${data.Response.Error.Code} - ${data.Response.Error.Message}`);
78
+ }
79
+ return { taskId: data.Response.Data.TaskId };
80
+ }
81
+ async function pollTencentResult(config, taskId, maxWaitMs = 120000) {
82
+ const startTime = Date.now();
83
+ while (Date.now() - startTime < maxWaitMs) {
84
+ await new Promise(r => setTimeout(r, 2000));
85
+ const payload = JSON.stringify({ TaskId: taskId });
86
+ const timestamp = Math.floor(Date.now() / 1000);
87
+ const authorization = buildTencentSignature(config.secretId, config.secretKey, timestamp, payload);
88
+ const res = await fetch("https://asr.tencentcloudapi.com", {
89
+ method: "POST",
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ "Host": "asr.tencentcloudapi.com",
93
+ "X-TC-Action": "DescribeTaskStatus",
94
+ "X-TC-Version": "2019-06-14",
95
+ "X-TC-Timestamp": String(timestamp),
96
+ "X-TC-Region": config.region || "ap-guangzhou",
97
+ "Authorization": authorization,
98
+ },
99
+ body: payload,
100
+ });
101
+ const data = await res.json();
102
+ if (data.Response?.Error) {
103
+ throw new Error(`Tencent poll error: ${data.Response.Error.Code}`);
104
+ }
105
+ const status = data.Response.Data.StatusStr;
106
+ if (status === "success") {
107
+ return data.Response.Data;
108
+ }
109
+ else if (status === "failed") {
110
+ throw new Error(`ASR task failed: ${data.Response.Data.ErrorMsg}`);
111
+ }
112
+ // still doing, continue polling
113
+ }
114
+ throw new Error("ASR task timed out");
115
+ }
116
+ // ---- WAV 切片 ----
117
+ /**
118
+ * 从 WAV buffer 中按时间戳切出一段
119
+ * @returns WAV buffer(含 header)
120
+ */
121
+ function sliceWav(wavBuffer, startMs, endMs) {
122
+ // WAV header: 44 bytes, 16kHz, 16-bit, mono → 32000 bytes/sec
123
+ const sampleRate = 16000;
124
+ const bytesPerSample = 2;
125
+ const bytesPerSec = sampleRate * bytesPerSample;
126
+ const headerSize = 44;
127
+ const startByte = headerSize + Math.floor(startMs / 1000 * bytesPerSec);
128
+ const endByte = headerSize + Math.ceil(endMs / 1000 * bytesPerSec);
129
+ const clampedStart = Math.max(headerSize, Math.min(startByte, wavBuffer.length));
130
+ const clampedEnd = Math.max(clampedStart, Math.min(endByte, wavBuffer.length));
131
+ const dataLen = clampedEnd - clampedStart;
132
+ const fileLen = headerSize + dataLen;
133
+ // Build new WAV header
134
+ const header = Buffer.alloc(headerSize);
135
+ header.write("RIFF", 0);
136
+ header.writeUInt32LE(fileLen - 8, 4);
137
+ header.write("WAVE", 8);
138
+ header.write("fmt ", 12);
139
+ header.writeUInt32LE(16, 16); // chunk size
140
+ header.writeUInt16LE(1, 20); // PCM
141
+ header.writeUInt16LE(1, 22); // mono
142
+ header.writeUInt32LE(sampleRate, 24);
143
+ header.writeUInt32LE(bytesPerSec, 28);
144
+ header.writeUInt16LE(bytesPerSample, 32);
145
+ header.writeUInt16LE(16, 34); // bits per sample
146
+ header.write("data", 36);
147
+ header.writeUInt32LE(dataLen, 40);
148
+ return Buffer.concat([header, wavBuffer.subarray(clampedStart, clampedEnd)]);
149
+ }
150
+ // ---- 主流程 ----
151
+ export class VoiceIdentifier {
152
+ asrConfig;
153
+ voiceStore;
154
+ serviceUrl;
155
+ constructor(asrConfig, voiceStore, serviceUrl = "http://localhost:8765") {
156
+ this.asrConfig = asrConfig;
157
+ this.voiceStore = voiceStore;
158
+ this.serviceUrl = serviceUrl;
159
+ }
160
+ /**
161
+ * 完整识别流程:ASR + 说话人分离 + 声纹匹配
162
+ *
163
+ * @param audioPath WAV 文件路径(16kHz mono 16-bit)
164
+ * @returns 每段话的说话人 + 文本 + 时间戳
165
+ */
166
+ async identify(audioPath) {
167
+ const start = Date.now();
168
+ // 1. 读取音频 base64
169
+ const audioBuffer = await fs.readFile(audioPath);
170
+ const audioBase64 = audioBuffer.toString("base64");
171
+ logger.info(`[engram:voice-identify] Audio: ${audioPath} (${(audioBuffer.length / 1024).toFixed(0)} KB)`);
172
+ // 2. 提交腾讯云 ASR
173
+ logger.info(`[engram:voice-identify] Submitting to Tencent ASR...`);
174
+ const { taskId } = await callTencentASR(this.asrConfig, audioBase64);
175
+ logger.info(`[engram:voice-identify] TaskId: ${taskId}, polling...`);
176
+ // 3. 轮询结果
177
+ const asrResult = await pollTencentResult(this.asrConfig, taskId);
178
+ const resultDetail = asrResult.ResultDetail;
179
+ if (!resultDetail || !Array.isArray(resultDetail) || resultDetail.length === 0) {
180
+ return { segments: [], fullText: asrResult.Result || "", speakerCount: 0, elapsedMs: Date.now() - start };
181
+ }
182
+ logger.info(`[engram:voice-identify] ASR returned ${resultDetail.length} segment(s)`);
183
+ // 4. 按 SpeakerId 分组,取每人最长的一段用于声纹匹配
184
+ const speakerSegments = new Map();
185
+ for (const seg of resultDetail) {
186
+ const sid = seg.SpeakerId ?? 0;
187
+ if (!speakerSegments.has(sid))
188
+ speakerSegments.set(sid, []);
189
+ speakerSegments.get(sid).push(seg);
190
+ }
191
+ // 5. 对每个 speaker 提取声纹并匹配
192
+ const speakerNameMap = new Map();
193
+ const tempDir = join(require("os").tmpdir(), `engram-voice-${Date.now()}`);
194
+ await fs.mkdir(tempDir, { recursive: true });
195
+ try {
196
+ for (const [speakerId, segs] of speakerSegments) {
197
+ // 找最长的一段
198
+ const longestSeg = segs.reduce((a, b) => (b.EndMs - b.StartMs) > (a.EndMs - a.StartMs) ? b : a);
199
+ const duration = longestSeg.EndMs - longestSeg.StartMs;
200
+ if (duration < 1500) {
201
+ // 太短(<1.5s),声纹不可靠
202
+ logger.info(`[engram:voice-identify] Speaker ${speakerId}: segment too short (${duration}ms), skipping voice match`);
203
+ speakerNameMap.set(speakerId, { name: `speaker_${speakerId}`, confidence: 0 });
204
+ continue;
205
+ }
206
+ // 切片 WAV
207
+ const slicedWav = sliceWav(audioBuffer, longestSeg.StartMs, longestSeg.EndMs);
208
+ const slicePath = join(tempDir, `speaker_${speakerId}.wav`);
209
+ await fs.writeFile(slicePath, slicedWav);
210
+ // 提取声纹并匹配
211
+ const match = await this.voiceStore.extractAndMatch(slicePath);
212
+ if (match && match.status === "matched") {
213
+ speakerNameMap.set(speakerId, { name: match.name, confidence: match.confidence });
214
+ logger.info(`[engram:voice-identify] Speaker ${speakerId} → ${match.name} (${match.confidence})`);
215
+ }
216
+ else if (match && match.status === "uncertain") {
217
+ speakerNameMap.set(speakerId, { name: match.name, confidence: match.confidence });
218
+ logger.info(`[engram:voice-identify] Speaker ${speakerId} → ${match.name} (uncertain, ${match.confidence})`);
219
+ }
220
+ else {
221
+ speakerNameMap.set(speakerId, { name: `speaker_${speakerId}`, confidence: 0 });
222
+ logger.info(`[engram:voice-identify] Speaker ${speakerId} → unknown`);
223
+ }
224
+ }
225
+ }
226
+ finally {
227
+ // 清理临时文件
228
+ try {
229
+ await fs.rm(tempDir, { recursive: true, force: true });
230
+ }
231
+ catch { }
232
+ }
233
+ // 6. 构建最终结果
234
+ const segments = resultDetail.map((seg) => {
235
+ const sid = seg.SpeakerId ?? 0;
236
+ const matched = speakerNameMap.get(sid) || { name: `speaker_${sid}`, confidence: 0 };
237
+ return {
238
+ speakerId: sid,
239
+ speakerName: matched.name,
240
+ text: seg.FinalSentence || "",
241
+ startMs: seg.StartMs || 0,
242
+ endMs: seg.EndMs || 0,
243
+ confidence: matched.confidence,
244
+ };
245
+ });
246
+ const fullText = segments.map(s => `[${s.speakerName}] ${s.text}`).join("\n");
247
+ const elapsedMs = Date.now() - start;
248
+ logger.info(`[engram:voice-identify] Done: ${segments.length} segments, ${speakerSegments.size} speakers, ${elapsedMs}ms`);
249
+ return {
250
+ segments,
251
+ fullText,
252
+ speakerCount: speakerSegments.size,
253
+ elapsedMs,
254
+ };
255
+ }
256
+ /**
257
+ * 从 Base64 音频识别(先保存为临时文件再处理)
258
+ */
259
+ async identifyFromBase64(audioBase64, format = "wav") {
260
+ const tempPath = join(require("os").tmpdir(), `engram-voice-${Date.now()}.${format}`);
261
+ await fs.writeFile(tempPath, Buffer.from(audioBase64, "base64"));
262
+ try {
263
+ return await this.identify(tempPath);
264
+ }
265
+ finally {
266
+ try {
267
+ await fs.unlink(tempPath);
268
+ }
269
+ catch { }
270
+ }
271
+ }
272
+ }
273
+ //# sourceMappingURL=voice-identify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice-identify.js","sourceRoot":"","sources":["../../../face/src/voice-identify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Voice Store — 声纹注册 + 匹配逻辑
3
+ *
4
+ * 与 face-store 共享 registry.json 中的 person,扩展 voice embedding 字段。
5
+ *
6
+ * 存储结构(在现有 faces/ 目录下扩展):
7
+ * memory-engram/faces/
8
+ * ├── registry.json ← 统一注册表(person 包含 face + voice)
9
+ * ├── embeddings/ ← 人脸 512d embedding(已有)
10
+ * ├── voice-embeddings/ ← 声纹 192d embedding(新增)
11
+ * │ ├── person_001.json
12
+ * │ └── person_002.json
13
+ * └── voice-samples/ ← 声纹音频样本(新增)
14
+ * ├── person_001/
15
+ * │ ├── 001.wav
16
+ * │ └── 002.wav
17
+ * └── person_002/
18
+ * └── 001.wav
19
+ */
20
+ export interface VoiceMatchResult {
21
+ personId: string | null;
22
+ name: string;
23
+ confidence: number;
24
+ status: "matched" | "uncertain" | "unknown";
25
+ }
26
+ export declare class VoiceStore {
27
+ private workspaceDir;
28
+ private apiClient;
29
+ /** 已注册声纹的 embedding 缓存(personId → embedding) */
30
+ private embeddingCache;
31
+ constructor(workspaceDir: string, serviceUrl?: string);
32
+ private get facesDir();
33
+ private get registryPath();
34
+ private get voiceEmbeddingsDir();
35
+ private get voiceSamplesDir();
36
+ private loadRegistry;
37
+ private saveRegistry;
38
+ private loadVoiceEmbedding;
39
+ private saveVoiceEmbedding;
40
+ isServiceAvailable(): Promise<boolean>;
41
+ /**
42
+ * 为已注册的 person 添加声纹
43
+ *
44
+ * @param personId 已注册人物 ID(必须先在 face-store 中注册)
45
+ * @param audioPaths 多段音频路径(建议 3~5 段不同语境)
46
+ */
47
+ registerVoice(personId: string, audioPaths: string[]): Promise<{
48
+ success: boolean;
49
+ sampleCount: number;
50
+ error?: string;
51
+ }>;
52
+ /**
53
+ * 为已注册的 person 添加声纹(通过 Base64 音频)
54
+ */
55
+ registerVoiceBase64(personId: string, audioBase64Samples: Array<{
56
+ base64: string;
57
+ format?: string;
58
+ }>): Promise<{
59
+ success: boolean;
60
+ sampleCount: number;
61
+ error?: string;
62
+ }>;
63
+ /**
64
+ * 将一个 voice embedding 与所有已注册声纹匹配
65
+ *
66
+ * @param embedding 待匹配的 192d embedding
67
+ * @returns 最佳匹配结果
68
+ */
69
+ matchEmbedding(embedding: number[]): Promise<VoiceMatchResult>;
70
+ /**
71
+ * 从音频文件提取 embedding 并匹配
72
+ */
73
+ extractAndMatch(audioPath: string): Promise<VoiceMatchResult | null>;
74
+ /**
75
+ * 列出所有有声纹注册的人物
76
+ */
77
+ listVoicePersons(): Promise<Array<{
78
+ id: string;
79
+ name: string;
80
+ voiceSampleCount: number;
81
+ }>>;
82
+ }