@moolam/bindings-speech 1.0.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/LICENSE +202 -0
  2. package/README.md +41 -0
  3. package/bin/speech-cert.mjs +11 -0
  4. package/certification/README.md +18 -0
  5. package/certification/device-profiles/mid-range-android.json +12 -0
  6. package/certification/profile.json +33 -0
  7. package/certification/prove-speech-cert.mjs +11 -0
  8. package/certification/reports/prove.green1.speech.cert.json +92 -0
  9. package/certification/reports/prove.seeded-red.speech.cert.json +70 -0
  10. package/certification/reports/speech.cert.json +92 -0
  11. package/certification/run-speech-cert.mjs +11 -0
  12. package/certification/schemas/speech.cert.report.schema.json +149 -0
  13. package/certification/voice-rtt.baseline.json +12 -0
  14. package/dist/edge_stt_harness.d.ts +86 -0
  15. package/dist/edge_stt_harness.d.ts.map +1 -0
  16. package/dist/edge_stt_harness.js +311 -0
  17. package/dist/edge_stt_harness.js.map +1 -0
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +10 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/speech_certification.d.ts +149 -0
  23. package/dist/speech_certification.d.ts.map +1 -0
  24. package/dist/speech_certification.js +490 -0
  25. package/dist/speech_certification.js.map +1 -0
  26. package/dist/stt_binding.d.ts +148 -0
  27. package/dist/stt_binding.d.ts.map +1 -0
  28. package/dist/stt_binding.js +400 -0
  29. package/dist/stt_binding.js.map +1 -0
  30. package/dist/tts_binding.d.ts +187 -0
  31. package/dist/tts_binding.d.ts.map +1 -0
  32. package/dist/tts_binding.js +565 -0
  33. package/dist/tts_binding.js.map +1 -0
  34. package/dist/voice_rtt_proof.d.ts +112 -0
  35. package/dist/voice_rtt_proof.d.ts.map +1 -0
  36. package/dist/voice_rtt_proof.js +241 -0
  37. package/dist/voice_rtt_proof.js.map +1 -0
  38. package/dist/whisper_ffi.d.ts +44 -0
  39. package/dist/whisper_ffi.d.ts.map +1 -0
  40. package/dist/whisper_ffi.js +104 -0
  41. package/dist/whisper_ffi.js.map +1 -0
  42. package/fixtures/indic/audio/en-classroom.pcm +0 -0
  43. package/fixtures/indic/audio/hi-classroom-noise.pcm +0 -0
  44. package/fixtures/indic/audio/hi-en-codeswitch.pcm +0 -0
  45. package/fixtures/indic/audio/hi-greeting.pcm +0 -0
  46. package/fixtures/indic/audio/short-hi.pcm +0 -0
  47. package/fixtures/indic/audio/ta-greeting.pcm +0 -0
  48. package/fixtures/indic/catalog.json +75 -0
  49. package/fixtures/indic/supported-languages.json +8 -0
  50. package/fixtures/tts/tts-voices.model-card.json +41 -0
  51. package/fixtures/tts/voices.json +24 -0
  52. package/package.json +55 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Speech certification: CK-05 against STT + TTS factories in one job,
3
+ * plus Indic utterance fixture partial-streaming gate.
4
+ *
5
+ * Failures print SPEECH CERT FAIL DIFF with obligation id / fixture id —
6
+ * never raw utterance or synthesis text.
7
+ */
8
+ import { type SpeechConformanceHarness } from "@moolam/contract-conformance";
9
+ import { type VoiceRttProofResult } from "./voice_rtt_proof.js";
10
+ export declare const SPEECH_PACKAGE_ROOT: string;
11
+ export declare const DEFAULT_SPEECH_CERT_PROFILE: string;
12
+ export declare const DEFAULT_SPEECH_CERT_REPORT: string;
13
+ export declare const SPEECH_CERT_REPORT_SCHEMA_VERSION: "bindings-speech.speech-cert.report.v1";
14
+ export declare const SPEECH_CERT_OBLIGATION_IDS: readonly ["CK-05.1", "CK-05.2"];
15
+ export type SpeechCertBindingId = "stt" | "tts";
16
+ export type SpeechCertProfile = {
17
+ schemaVersion: string;
18
+ profileId: string;
19
+ subjectId: string;
20
+ deviceId: string;
21
+ locality: "on-device" | "self-hosted";
22
+ obligationIds: string[];
23
+ bindings: SpeechCertBindingId[];
24
+ indicFixtures: {
25
+ requiredUtteranceIds: string[];
26
+ requirePartialBeforeFinal: boolean;
27
+ };
28
+ voiceRtt?: {
29
+ enabled: boolean;
30
+ nfrId: "NFR-07";
31
+ deviceProfileRelpath: string;
32
+ baselineRelpath: string;
33
+ utteranceId: string;
34
+ language: string;
35
+ };
36
+ reportArtifact: {
37
+ schemaVersion: string;
38
+ defaultRelpath: string;
39
+ };
40
+ description?: string;
41
+ };
42
+ export type SpeechCertBindingResult = {
43
+ binding: SpeechCertBindingId;
44
+ engine: string;
45
+ outcome: "pass" | "fail";
46
+ exitCode: 0 | 1;
47
+ passed: number;
48
+ failed: number;
49
+ verdicts: Array<{
50
+ obligationId: string;
51
+ outcome: string;
52
+ mustText: string;
53
+ message?: string;
54
+ }>;
55
+ };
56
+ export type SpeechCertReport = {
57
+ schemaVersion: typeof SPEECH_CERT_REPORT_SCHEMA_VERSION;
58
+ profileId: string;
59
+ outcome: "pass" | "fail";
60
+ subjectId: string;
61
+ deviceId: string;
62
+ exitCode: 0 | 1;
63
+ bindings: SpeechCertBindingResult[];
64
+ indicFixtures: {
65
+ outcome: "pass" | "fail";
66
+ utteranceCount: number;
67
+ failures: string[];
68
+ };
69
+ voiceRtt: VoiceRttProofResult | null;
70
+ failures: string[];
71
+ telemetry: {
72
+ event: "bindings_speech.speech_cert";
73
+ subjectId: string;
74
+ deviceId: string;
75
+ outcome: "pass" | "fail";
76
+ bindingCount: number;
77
+ fixtureCount: number;
78
+ voiceRttOk?: boolean;
79
+ };
80
+ };
81
+ export type SpeechCertIo = {
82
+ stdout: {
83
+ write(chunk: string): void;
84
+ };
85
+ stderr: {
86
+ write(chunk: string): void;
87
+ };
88
+ };
89
+ export type RunSpeechCertificationOptions = {
90
+ profilePath?: string;
91
+ reportOutPath?: string;
92
+ /** Inject factories (prove / unit tests). */
93
+ sttFactory?: (ctx?: {
94
+ subjectId?: string;
95
+ deviceId?: string;
96
+ }) => SpeechConformanceHarness | Promise<SpeechConformanceHarness>;
97
+ ttsFactory?: (ctx?: {
98
+ subjectId?: string;
99
+ deviceId?: string;
100
+ }) => SpeechConformanceHarness | Promise<SpeechConformanceHarness>;
101
+ /** Skip Indic PCM gate (prove seeded binding-only failures). */
102
+ skipIndicFixtures?: boolean;
103
+ /** Skip NFR-07 voice RTT (CK-05-only prove). */
104
+ skipVoiceRtt?: boolean;
105
+ /** Inject real first-audio delay for NFR-07 seeded red. */
106
+ voiceRttInjectFirstAudioDelayMs?: number;
107
+ /** Override NFR-07 absolute budget (tests). */
108
+ voiceRttBudgetP95Ms?: number;
109
+ };
110
+ export declare function loadSpeechCertProfile(profilePath?: string): SpeechCertProfile;
111
+ export declare function runIndicFixtureGate(args: {
112
+ subjectId: string;
113
+ deviceId: string;
114
+ utteranceIds: readonly string[];
115
+ requirePartialBeforeFinal: boolean;
116
+ }): Promise<{
117
+ outcome: "pass" | "fail";
118
+ utteranceCount: number;
119
+ failures: string[];
120
+ }>;
121
+ /**
122
+ * Run STT + TTS CK-05 and Indic fixture gate; write report; loud DIFF on fail.
123
+ */
124
+ export declare function runSpeechCertification(io: SpeechCertIo, options?: RunSpeechCertificationOptions): Promise<{
125
+ exitCode: 0 | 1;
126
+ report: SpeechCertReport;
127
+ }>;
128
+ export type ProveSpeechCertResult = {
129
+ exitCode: 0 | 1;
130
+ phases: Array<{
131
+ phase: string;
132
+ status: number;
133
+ }>;
134
+ };
135
+ /**
136
+ * Green → seeded CK-05 red → seeded NFR-07 red → green again.
137
+ * Does not mutate the working tree.
138
+ */
139
+ export declare function proveSpeechCertificationGate(io?: SpeechCertIo): Promise<ProveSpeechCertResult>;
140
+ export type ParsedSpeechCertCli = {
141
+ help: boolean;
142
+ prove: boolean;
143
+ reportOut?: string;
144
+ profilePath?: string;
145
+ errors: string[];
146
+ };
147
+ export declare function parseSpeechCertArgv(argv: readonly string[]): ParsedSpeechCertCli;
148
+ export declare function runSpeechCertCli(argv: readonly string[], io?: SpeechCertIo): Promise<number>;
149
+ //# sourceMappingURL=speech_certification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech_certification.d.ts","sourceRoot":"","sources":["../src/speech_certification.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Speech certification: CK-05 against STT + TTS factories in one job,
3
+ * plus Indic utterance fixture partial-streaming gate.
4
+ *
5
+ * Failures print SPEECH CERT FAIL DIFF with obligation id / fixture id —
6
+ * never raw utterance or synthesis text.
7
+ */
8
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { collectTranscriptSegments, createFinalOnlySpeechHarnessFactory, createNoFallbackSpeechHarnessFactory, createSpeechObligationsRegistry, formatHumanReport, hasPartialBeforeFinal, runConformance, } from "@moolam/contract-conformance";
12
+ import { WHISPER_CPP_CLASS_ENGINE, createWhisperCppSpeechHarnessFactory, indicFixtureAsAudioStream, loadIndicUtteranceFixture, loadWhisperCppSpeech, } from "./stt_binding.js";
13
+ import { LOCAL_TTS_ENGINE, createLocalTtsSpeechHarnessFactory, } from "./tts_binding.js";
14
+ import { DEFAULT_VOICE_RTT_BASELINE, DEFAULT_VOICE_RTT_DEVICE_PROFILE, runVoiceRttProof, } from "./voice_rtt_proof.js";
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ export const SPEECH_PACKAGE_ROOT = path.resolve(__dirname, "..");
17
+ export const DEFAULT_SPEECH_CERT_PROFILE = path.join(SPEECH_PACKAGE_ROOT, "certification", "profile.json");
18
+ export const DEFAULT_SPEECH_CERT_REPORT = path.join(SPEECH_PACKAGE_ROOT, "certification", "reports", "speech.cert.json");
19
+ export const SPEECH_CERT_REPORT_SCHEMA_VERSION = "bindings-speech.speech-cert.report.v1";
20
+ export const SPEECH_CERT_OBLIGATION_IDS = ["CK-05.1", "CK-05.2"];
21
+ function emitTelemetry(io, event) {
22
+ io.stdout.write(`${JSON.stringify({ event: "bindings_speech.speech_cert", ...event })}\n`);
23
+ }
24
+ export function loadSpeechCertProfile(profilePath = DEFAULT_SPEECH_CERT_PROFILE) {
25
+ if (!existsSync(profilePath)) {
26
+ throw new Error(`speech cert profile missing at ${profilePath}`);
27
+ }
28
+ const raw = JSON.parse(readFileSync(profilePath, "utf8"));
29
+ if (typeof raw.schemaVersion !== "string" ||
30
+ typeof raw.profileId !== "string" ||
31
+ typeof raw.subjectId !== "string" ||
32
+ !raw.subjectId.trim() ||
33
+ typeof raw.deviceId !== "string" ||
34
+ !raw.deviceId.trim() ||
35
+ (raw.locality !== "on-device" && raw.locality !== "self-hosted") ||
36
+ !Array.isArray(raw.obligationIds) ||
37
+ raw.obligationIds.length === 0 ||
38
+ !Array.isArray(raw.bindings) ||
39
+ raw.bindings.length === 0 ||
40
+ !Array.isArray(raw.indicFixtures?.requiredUtteranceIds) ||
41
+ raw.indicFixtures.requiredUtteranceIds.length === 0 ||
42
+ typeof raw.reportArtifact?.schemaVersion !== "string") {
43
+ throw new Error("speech cert profile missing required fields (subjectId, obligationIds, bindings, indicFixtures)");
44
+ }
45
+ for (const id of raw.obligationIds) {
46
+ if (!SPEECH_CERT_OBLIGATION_IDS.includes(id)) {
47
+ throw new Error(`speech cert profile unknown obligationId: ${id}`);
48
+ }
49
+ }
50
+ for (const b of raw.bindings) {
51
+ if (b !== "stt" && b !== "tts") {
52
+ throw new Error(`speech cert profile unknown binding: ${String(b)}`);
53
+ }
54
+ }
55
+ return {
56
+ schemaVersion: raw.schemaVersion,
57
+ profileId: raw.profileId,
58
+ subjectId: raw.subjectId.trim(),
59
+ deviceId: raw.deviceId.trim(),
60
+ locality: raw.locality,
61
+ obligationIds: [...raw.obligationIds],
62
+ bindings: [...raw.bindings],
63
+ indicFixtures: {
64
+ requiredUtteranceIds: [...raw.indicFixtures.requiredUtteranceIds],
65
+ requirePartialBeforeFinal: raw.indicFixtures.requirePartialBeforeFinal !== false,
66
+ },
67
+ ...(raw.voiceRtt &&
68
+ raw.voiceRtt.nfrId === "NFR-07" &&
69
+ typeof raw.voiceRtt.deviceProfileRelpath === "string" &&
70
+ typeof raw.voiceRtt.baselineRelpath === "string"
71
+ ? {
72
+ voiceRtt: {
73
+ enabled: raw.voiceRtt.enabled !== false,
74
+ nfrId: "NFR-07",
75
+ deviceProfileRelpath: raw.voiceRtt.deviceProfileRelpath,
76
+ baselineRelpath: raw.voiceRtt.baselineRelpath,
77
+ utteranceId: typeof raw.voiceRtt.utteranceId === "string" &&
78
+ raw.voiceRtt.utteranceId.trim()
79
+ ? raw.voiceRtt.utteranceId.trim()
80
+ : "hi-greeting",
81
+ language: typeof raw.voiceRtt.language === "string" &&
82
+ raw.voiceRtt.language.trim()
83
+ ? raw.voiceRtt.language.trim()
84
+ : "hi-IN",
85
+ },
86
+ }
87
+ : {}),
88
+ reportArtifact: {
89
+ schemaVersion: raw.reportArtifact.schemaVersion,
90
+ defaultRelpath: typeof raw.reportArtifact.defaultRelpath === "string"
91
+ ? raw.reportArtifact.defaultRelpath
92
+ : "certification/reports/speech.cert.json",
93
+ },
94
+ ...(typeof raw.description === "string"
95
+ ? { description: raw.description }
96
+ : {}),
97
+ };
98
+ }
99
+ function summarizeBinding(binding, engine, report) {
100
+ return {
101
+ binding,
102
+ engine,
103
+ outcome: report.exitCode === 0 ? "pass" : "fail",
104
+ exitCode: report.exitCode,
105
+ passed: report.passed,
106
+ failed: report.failed + report.timedOut + report.errored,
107
+ verdicts: report.verdicts.map((v) => ({
108
+ obligationId: v.obligationId,
109
+ outcome: v.outcome,
110
+ mustText: v.mustText,
111
+ ...(v.message ? { message: v.message.slice(0, 240) } : {}),
112
+ })),
113
+ };
114
+ }
115
+ function printBindingDiff(io, result, human) {
116
+ if (result.outcome === "pass")
117
+ return;
118
+ io.stderr.write(`\nSPEECH CERT FAIL DIFF — binding=${result.binding} engine=${result.engine}\n`);
119
+ for (const v of result.verdicts) {
120
+ if (v.outcome === "pass")
121
+ continue;
122
+ io.stderr.write(` FAIL ${v.obligationId}: ${v.message ?? "obligation failed"}\n`);
123
+ io.stderr.write(` MUST: ${v.mustText}\n`);
124
+ }
125
+ io.stderr.write(`${human}\n`);
126
+ }
127
+ export async function runIndicFixtureGate(args) {
128
+ const failures = [];
129
+ const speech = await loadWhisperCppSpeech({
130
+ subjectId: args.subjectId,
131
+ deviceId: args.deviceId,
132
+ });
133
+ try {
134
+ for (const id of args.utteranceIds) {
135
+ let fixture;
136
+ try {
137
+ fixture = loadIndicUtteranceFixture(id);
138
+ }
139
+ catch (err) {
140
+ failures.push(`fixture ${id}: load failed (${err instanceof Error ? err.message : String(err)})`);
141
+ continue;
142
+ }
143
+ try {
144
+ const segs = await collectTranscriptSegments(speech.transcribe(indicFixtureAsAudioStream(fixture)));
145
+ if (segs.length === 0) {
146
+ failures.push(`fixture ${id}: no transcript segments`);
147
+ continue;
148
+ }
149
+ if (args.requirePartialBeforeFinal &&
150
+ !hasPartialBeforeFinal(segs)) {
151
+ failures.push(`fixture ${id}: missing isFinal:false partial before final (CK-05.1)`);
152
+ }
153
+ const finals = segs.filter((s) => s.isFinal);
154
+ if (finals.length === 0) {
155
+ failures.push(`fixture ${id}: missing final segment`);
156
+ }
157
+ }
158
+ catch (err) {
159
+ failures.push(`fixture ${id}: transcribe threw (${err instanceof Error ? err.message : String(err)})`);
160
+ }
161
+ }
162
+ }
163
+ finally {
164
+ await speech.unload();
165
+ }
166
+ return {
167
+ outcome: failures.length === 0 ? "pass" : "fail",
168
+ utteranceCount: args.utteranceIds.length,
169
+ failures,
170
+ };
171
+ }
172
+ /**
173
+ * Run STT + TTS CK-05 and Indic fixture gate; write report; loud DIFF on fail.
174
+ */
175
+ export async function runSpeechCertification(io, options = {}) {
176
+ const profile = loadSpeechCertProfile(options.profilePath);
177
+ const failures = [];
178
+ const bindingResults = [];
179
+ emitTelemetry(io, {
180
+ op: "start",
181
+ outcome: "ok",
182
+ subjectId: profile.subjectId,
183
+ deviceId: profile.deviceId,
184
+ profileId: profile.profileId,
185
+ });
186
+ const registry = createSpeechObligationsRegistry();
187
+ if (profile.bindings.includes("stt")) {
188
+ const factory = options.sttFactory ??
189
+ createWhisperCppSpeechHarnessFactory({
190
+ subjectId: profile.subjectId,
191
+ deviceId: profile.deviceId,
192
+ });
193
+ const report = await runConformance({
194
+ registry,
195
+ factory,
196
+ subjectId: `${profile.subjectId}.stt`,
197
+ deviceId: profile.deviceId,
198
+ obligationIds: profile.obligationIds,
199
+ });
200
+ const summarized = summarizeBinding("stt", WHISPER_CPP_CLASS_ENGINE, report);
201
+ bindingResults.push(summarized);
202
+ printBindingDiff(io, summarized, formatHumanReport(report));
203
+ if (summarized.outcome === "fail") {
204
+ failures.push(...summarized.verdicts
205
+ .filter((v) => v.outcome !== "pass")
206
+ .map((v) => `stt ${v.obligationId}: ${v.message ?? v.outcome}`));
207
+ }
208
+ }
209
+ if (profile.bindings.includes("tts")) {
210
+ const factory = options.ttsFactory ??
211
+ createLocalTtsSpeechHarnessFactory({
212
+ subjectId: profile.subjectId,
213
+ deviceId: profile.deviceId,
214
+ });
215
+ const report = await runConformance({
216
+ registry,
217
+ factory,
218
+ subjectId: `${profile.subjectId}.tts`,
219
+ deviceId: profile.deviceId,
220
+ obligationIds: profile.obligationIds,
221
+ });
222
+ const summarized = summarizeBinding("tts", LOCAL_TTS_ENGINE, report);
223
+ bindingResults.push(summarized);
224
+ printBindingDiff(io, summarized, formatHumanReport(report));
225
+ if (summarized.outcome === "fail") {
226
+ failures.push(...summarized.verdicts
227
+ .filter((v) => v.outcome !== "pass")
228
+ .map((v) => `tts ${v.obligationId}: ${v.message ?? v.outcome}`));
229
+ }
230
+ }
231
+ let indicFixtures = {
232
+ outcome: "pass",
233
+ utteranceCount: 0,
234
+ failures: [],
235
+ };
236
+ if (!options.skipIndicFixtures) {
237
+ indicFixtures = await runIndicFixtureGate({
238
+ subjectId: `${profile.subjectId}.fixtures`,
239
+ deviceId: profile.deviceId,
240
+ utteranceIds: profile.indicFixtures.requiredUtteranceIds,
241
+ requirePartialBeforeFinal: profile.indicFixtures.requirePartialBeforeFinal,
242
+ });
243
+ if (indicFixtures.outcome === "fail") {
244
+ io.stderr.write(`\nSPEECH CERT FAIL DIFF — indicFixtures utteranceCount=${indicFixtures.utteranceCount}\n`);
245
+ for (const f of indicFixtures.failures) {
246
+ io.stderr.write(` FAIL ${f}\n`);
247
+ }
248
+ failures.push(...indicFixtures.failures);
249
+ }
250
+ }
251
+ let voiceRtt = null;
252
+ const voiceRttEnabled = !options.skipVoiceRtt && profile.voiceRtt?.enabled !== false;
253
+ if (voiceRttEnabled && profile.voiceRtt) {
254
+ const deviceProfilePath = path.join(SPEECH_PACKAGE_ROOT, profile.voiceRtt.deviceProfileRelpath);
255
+ const baselinePath = path.join(SPEECH_PACKAGE_ROOT, profile.voiceRtt.baselineRelpath);
256
+ voiceRtt = await runVoiceRttProof({
257
+ subjectId: `${profile.subjectId}.voiceRtt`,
258
+ deviceId: profile.deviceId,
259
+ deviceProfilePath: existsSync(deviceProfilePath)
260
+ ? deviceProfilePath
261
+ : DEFAULT_VOICE_RTT_DEVICE_PROFILE,
262
+ baselinePath: existsSync(baselinePath)
263
+ ? baselinePath
264
+ : DEFAULT_VOICE_RTT_BASELINE,
265
+ utteranceId: profile.voiceRtt.utteranceId,
266
+ language: profile.voiceRtt.language,
267
+ ...(options.voiceRttInjectFirstAudioDelayMs !== undefined
268
+ ? {
269
+ injectFirstAudioDelayMs: options.voiceRttInjectFirstAudioDelayMs,
270
+ }
271
+ : {}),
272
+ ...(options.voiceRttBudgetP95Ms !== undefined
273
+ ? { budgetP95Ms: options.voiceRttBudgetP95Ms }
274
+ : {}),
275
+ });
276
+ if (!voiceRtt.ok) {
277
+ io.stderr.write(`\nSPEECH CERT FAIL DIFF — NFR-07 voiceRtt measuredP95=${voiceRtt.measuredP95Ms.toFixed(3)}ms budget=${voiceRtt.budgetP95Ms}ms allowedRelative=${voiceRtt.allowedRelativeP95Ms.toFixed(3)}ms clock=${voiceRtt.clock}\n`);
278
+ for (const f of voiceRtt.failures) {
279
+ io.stderr.write(` FAIL ${f}\n`);
280
+ }
281
+ failures.push(...voiceRtt.failures);
282
+ }
283
+ else {
284
+ io.stdout.write(`SPEECH CERT NFR-07 PASS — p95 ${voiceRtt.measuredP95Ms.toFixed(3)}ms ≤ budget ${voiceRtt.budgetP95Ms}ms (relative ≤ ${voiceRtt.allowedRelativeP95Ms.toFixed(3)}ms)\n`);
285
+ }
286
+ }
287
+ const exitCode = failures.length === 0 ? 0 : 1;
288
+ const outcome = exitCode === 0 ? "pass" : "fail";
289
+ const report = {
290
+ schemaVersion: SPEECH_CERT_REPORT_SCHEMA_VERSION,
291
+ profileId: profile.profileId,
292
+ outcome,
293
+ subjectId: profile.subjectId,
294
+ deviceId: profile.deviceId,
295
+ exitCode,
296
+ bindings: bindingResults,
297
+ indicFixtures,
298
+ voiceRtt,
299
+ failures,
300
+ telemetry: {
301
+ event: "bindings_speech.speech_cert",
302
+ subjectId: profile.subjectId,
303
+ deviceId: profile.deviceId,
304
+ outcome,
305
+ bindingCount: bindingResults.length,
306
+ fixtureCount: indicFixtures.utteranceCount,
307
+ ...(voiceRtt ? { voiceRttOk: voiceRtt.ok } : {}),
308
+ },
309
+ };
310
+ const reportPath = options.reportOutPath ??
311
+ path.join(SPEECH_PACKAGE_ROOT, profile.reportArtifact.defaultRelpath);
312
+ mkdirSync(path.dirname(reportPath), { recursive: true });
313
+ writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
314
+ emitTelemetry(io, {
315
+ op: "complete",
316
+ outcome,
317
+ subjectId: profile.subjectId,
318
+ deviceId: profile.deviceId,
319
+ exitCode,
320
+ bindingCount: bindingResults.length,
321
+ fixtureCount: indicFixtures.utteranceCount,
322
+ failureCount: failures.length,
323
+ reportPath,
324
+ });
325
+ if (exitCode !== 0) {
326
+ io.stderr.write(`SPEECH CERT FAIL — ${failures.length} failure(s); see DIFF above and ${reportPath}\n`);
327
+ }
328
+ else {
329
+ io.stdout.write(`SPEECH CERT PASS — STT+TTS CK-05 + ${indicFixtures.utteranceCount} Indic fixtures` +
330
+ (voiceRtt ? ` + NFR-07 p95 ${voiceRtt.measuredP95Ms.toFixed(1)}ms` : "") +
331
+ `\n`);
332
+ }
333
+ return { exitCode, report };
334
+ }
335
+ /**
336
+ * Green → seeded CK-05 red → seeded NFR-07 red → green again.
337
+ * Does not mutate the working tree.
338
+ */
339
+ export async function proveSpeechCertificationGate(io = {
340
+ stdout: process.stdout,
341
+ stderr: process.stderr,
342
+ }) {
343
+ const phases = [];
344
+ const tmpRoot = path.join(SPEECH_PACKAGE_ROOT, "certification", "reports");
345
+ mkdirSync(tmpRoot, { recursive: true });
346
+ const green1 = await runSpeechCertification(io, {
347
+ reportOutPath: path.join(tmpRoot, "prove.green1.speech.cert.json"),
348
+ });
349
+ phases.push({ phase: "baseline.green", status: green1.exitCode });
350
+ if (green1.exitCode !== 0) {
351
+ throw new Error(`SPEECH_CERT_PROVE_BASELINE_RED:\n${JSON.stringify(green1.report.failures).slice(0, 2000)}`);
352
+ }
353
+ if (!green1.report.voiceRtt?.ok) {
354
+ throw new Error("SPEECH_CERT_PROVE_BASELINE_MISSING_NFR07: voice RTT must pass");
355
+ }
356
+ const seeded = await runSpeechCertification(io, {
357
+ skipIndicFixtures: true,
358
+ skipVoiceRtt: true,
359
+ sttFactory: createFinalOnlySpeechHarnessFactory(),
360
+ ttsFactory: createNoFallbackSpeechHarnessFactory(),
361
+ reportOutPath: path.join(tmpRoot, "prove.seeded-red.speech.cert.json"),
362
+ });
363
+ phases.push({ phase: "seeded.red.ck05", status: seeded.exitCode });
364
+ if (seeded.exitCode === 0) {
365
+ throw new Error("SPEECH_CERT_PROVE_SEED_UNEXPECTED_GREEN: final-only STT + no-fallback TTS must fail CK-05");
366
+ }
367
+ const redText = JSON.stringify(seeded.report);
368
+ if (!redText.includes("CK-05.1") && !redText.includes("CK-05.2")) {
369
+ throw new Error(`SPEECH_CERT_PROVE_SEED_MISSING_OBLIGATION_ID:\n${redText.slice(0, 2000)}`);
370
+ }
371
+ const hasMust = /MUST/i.test(redText) ||
372
+ seeded.report.bindings.some((b) => b.verdicts.some((v) => v.outcome !== "pass" && /MUST/i.test(v.mustText)));
373
+ if (!hasMust) {
374
+ throw new Error(`SPEECH_CERT_PROVE_SEED_MISSING_MUST:\n${redText.slice(0, 2000)}`);
375
+ }
376
+ io.stdout.write(`\n--- speech-cert seeded-red CK-05 (failures) ---\n${seeded.report.failures.join("\n")}\n---\n`);
377
+ // Real timer delay before first audio — breaches absolute NFR-07 (2500ms).
378
+ const nfr07Red = await runVoiceRttProof({
379
+ subjectId: "cert.speech.prove.nfr07",
380
+ deviceId: "ci-speech-ubuntu",
381
+ injectFirstAudioDelayMs: 3_200,
382
+ sampleCount: 3,
383
+ warmupCount: 0,
384
+ });
385
+ phases.push({
386
+ phase: "seeded.red.nfr07",
387
+ status: nfr07Red.ok ? 0 : 1,
388
+ });
389
+ if (nfr07Red.ok) {
390
+ throw new Error("SPEECH_CERT_PROVE_NFR07_UNEXPECTED_GREEN: 3200ms first-audio delay must fail NFR-07");
391
+ }
392
+ if (!nfr07Red.failures.some((f) => /NFR-07/.test(f))) {
393
+ throw new Error(`SPEECH_CERT_PROVE_NFR07_MISSING_DIFF:\n${nfr07Red.failures.join("\n")}`);
394
+ }
395
+ io.stderr.write(`\nSPEECH CERT FAIL DIFF — NFR-07 seeded delay measuredP95=${nfr07Red.measuredP95Ms.toFixed(3)}ms budget=${nfr07Red.budgetP95Ms}ms\n`);
396
+ for (const f of nfr07Red.failures) {
397
+ io.stderr.write(` FAIL ${f}\n`);
398
+ }
399
+ io.stdout.write(`\n--- speech-cert seeded-red NFR-07 (failures) ---\n${nfr07Red.failures.join("\n")}\n---\n`);
400
+ const green2 = await runSpeechCertification(io, {
401
+ reportOutPath: DEFAULT_SPEECH_CERT_REPORT,
402
+ });
403
+ phases.push({ phase: "restore.green", status: green2.exitCode });
404
+ if (green2.exitCode !== 0) {
405
+ throw new Error(`SPEECH_CERT_PROVE_RESTORE_STILL_RED:\n${JSON.stringify(green2.report.failures).slice(0, 2000)}`);
406
+ }
407
+ if (!green2.report.voiceRtt?.ok) {
408
+ throw new Error("SPEECH_CERT_PROVE_RESTORE_MISSING_NFR07: voice RTT must pass");
409
+ }
410
+ emitTelemetry(io, {
411
+ op: "prove",
412
+ outcome: "ok",
413
+ subjectId: green2.report.subjectId,
414
+ deviceId: green2.report.deviceId,
415
+ phases: phases.map((p) => p.phase),
416
+ });
417
+ return { exitCode: 0, phases };
418
+ }
419
+ export function parseSpeechCertArgv(argv) {
420
+ const out = { help: false, prove: false, errors: [] };
421
+ for (let i = 0; i < argv.length; i++) {
422
+ const a = argv[i];
423
+ if (a === "-h" || a === "--help") {
424
+ out.help = true;
425
+ continue;
426
+ }
427
+ if (a === "--prove") {
428
+ out.prove = true;
429
+ continue;
430
+ }
431
+ if (a === "--report-out") {
432
+ const v = argv[++i];
433
+ if (!v)
434
+ out.errors.push("--report-out requires a value");
435
+ else
436
+ out.reportOut = v;
437
+ continue;
438
+ }
439
+ if (a === "--profile") {
440
+ const v = argv[++i];
441
+ if (!v)
442
+ out.errors.push("--profile requires a value");
443
+ else
444
+ out.profilePath = v;
445
+ continue;
446
+ }
447
+ out.errors.push(`unknown argument: ${a}`);
448
+ }
449
+ return out;
450
+ }
451
+ const SPEECH_CERT_HELP = `Usage: speech-cert [options]
452
+
453
+ Indic speech certification (CK-05 STT+TTS + Indic fixtures).
454
+
455
+ Options:
456
+ --prove Green → seeded red → green (CI prove gate)
457
+ --report-out <path> Write speech.cert.json to path
458
+ --profile <path> Cert profile JSON (default: certification/profile.json)
459
+ -h, --help Show help
460
+ `;
461
+ export async function runSpeechCertCli(argv, io = {
462
+ stdout: process.stdout,
463
+ stderr: process.stderr,
464
+ }) {
465
+ const args = parseSpeechCertArgv(argv);
466
+ if (args.help) {
467
+ io.stdout.write(SPEECH_CERT_HELP);
468
+ return 0;
469
+ }
470
+ if (args.errors.length) {
471
+ io.stderr.write(`${args.errors.join("\n")}\n`);
472
+ return 1;
473
+ }
474
+ if (args.prove) {
475
+ try {
476
+ const result = await proveSpeechCertificationGate(io);
477
+ return result.exitCode;
478
+ }
479
+ catch (err) {
480
+ io.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
481
+ return 1;
482
+ }
483
+ }
484
+ const { exitCode } = await runSpeechCertification(io, {
485
+ ...(args.reportOut ? { reportOutPath: args.reportOut } : {}),
486
+ ...(args.profilePath ? { profilePath: args.profilePath } : {}),
487
+ });
488
+ return exitCode;
489
+ }
490
+ //# sourceMappingURL=speech_certification.js.map