Setting the file. One moment.
Analyze Speech SDK Log · Azure AI · microsoft/azure-skills · Skills Docs
ContentsBack to the top of the page — line 165
This file
Number 22.24
Position 24 of 26
Type JavaScript
Size 18 KB
Lines 376 scripts/ analyze-speech-sdk-log.mjs
JavaScript · 376 lines · 18 KB
;
10 const MAX_BYTES = 256 * 1024 * 1024 ;
11 const MAX_RECORD_CHARS = 256 * 1024 ;
12 const ID = "(?:[a-fA-F0-9]{32}|[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12})" ;
13 const POINTER = "(?:0x)?[a-fA-F0-9]{4,16}" ;
14 const HEADER = / ^ \[ ( \d {1,6} ) \] : \s * ( \d {1,13} )ms \s + (SPX_ [A-Z_] + ): \s + (?:AZ_LOG_ [A-Z] + : \s + ) ? ( \S + ):( \d + ) \s + ( . * ) $ / ;
15 const numeric = ( value ) => / ^ \d {1,10}$ / . test (value) ? Number (value) : undefined ;
16 const choice = ( ... values ) => ( value ) => values. includes (value) ? value : undefined ;
17 const locale = ( value ) => / ^ [a-z] {2,3} (?:- [A-Za-z0-9] {2,8} ) {0,3}$ / . test (value) ? value : undefined ;
18 const object = ( value ) => value !== null && typeof value === "object" && ! Array. isArray (value);
19 const pointer = ( value ) => value. toLowerCase (). replace ( / ^ 0x/ , "" ). replace ( / ^ 0 + (?= . )/ , "" );
20 const identifier = ( value ) => typeof value === "string" && new RegExp ( `^${ ID }$` ). test (value) ? value : undefined ;
21 const PROPERTIES = new Map ([
22 [ "SPEECH-RecoLanguage" , locale],
23 [ "SPEECH-SynthLanguage" , locale],
24 [ "SPEECH-RecoMode" , choice ( "INTERACTIVE" , "CONVERSATION" , "DICTATION" , "Interactive" , "Conversation" , "Dictation" )],
25 [ "SPEECH-LanguageIdMode" , choice ( "AtStart" , "Continuous" , "AtStartHighAccuracy" )],
26 [ "SPEECH-SegmentationStrategy" , choice ( "Default" , "Time" , "Semantic" )],
27 [ "SPEECH-SegmentationSilenceTimeoutMs" , numeric],
28 [ "SPEECH-SegmentationMaximumTimeMs" , numeric],
29 [ "SPEECH-InitialSilenceTimeoutMs" , numeric],
30 [ "SPEECH-EndSilenceTimeoutMs" , numeric],
31 [ "AudioConfig_NumberOfChannelsForCapture" , numeric],
32 [ "AudioConfig_SampleRateForCapture" , numeric],
33 [ "AudioConfig_BitsPerSampleForCapture" , numeric],
34 [ "AudioConfig_AudioSource" , choice ( "Microphones" , "Stream" , "File" )],
35 [ "AZAC-SDK-PROGRAMMING-LANGUAGE" , choice ( "C++" , "C#" , "Java" , "Python" , "Objective-C" , "Swift" , "Go" )],
36 [ "SpeechServiceResponse_RequestWordLevelTimestamps" , choice ( "true" , "false" , "1" , "0" )],
37 [ "SpeechServiceResponse_DiarizeIntermediateResults" , choice ( "true" , "false" , "1" , "0" )],
38 [ "PronunciationAssessment_EnableMiscue" , choice ( "true" , "false" , "1" , "0" )],
39 [ "PronunciationAssessment_EnableProsodyAssessment" , choice ( "true" , "false" , "1" , "0" )],
40 [ "PronunciationAssessment_Granularity" , choice ( "FullText" , "Word" , "Phoneme" )],
41 [ "PronunciationAssessment_GradingSystem" , choice ( "FivePoint" , "HundredMark" )],
42 [ "RESULT-RecognitionLatencyMs" , numeric],
43 [ "RESULT-SynthesisFirstByteLatencyMs" , numeric],
44 [ "RESULT-SynthesisFinishLatencyMs" , numeric],
45 [ "RESULT-SynthesisUnderrunTimeMs" , numeric],
46 [ "RESULT-SynthesisConnectionLatencyMs" , numeric],
47 [ "RESULT-SynthesisNetworkLatencyMs" , numeric],
48 [ "RESULT-SynthesisServiceLatencyMs" , numeric],
49 ]);
50 // Presence only: these fields can carry free text or account-specific identifiers.
51 const PRESENCE_PROPERTIES = new Set ([
52 "SPEECH-SynthVoice" , "SPEECH-SynthOutputFormat" , "SPEECH-ModelId" ,
53 "SPEECH-Endpoint" , "SPEECH-Region" , "Auto-Detect-Source-Languages" ,
54 "PronunciationAssessment_ReferenceText" , "PronunciationAssessment_Params" ,
55 ]);
56
57 function contextFeatures ( context ) {
58 const features = {};
59 const detection = object (context.phraseDetection) ? context.phraseDetection : {};
60 if ( object (context.languageId)) {
61 features.languageIdentification = { present: true };
62 const lid = features.languageIdentification;
63 const mode = choice ( "DetectContinuous" , "DetectSegments" , "DetectAtAudioStart" )(context.languageId.mode);
64 if (mode !== undefined ) lid.mode = mode;
65 if (Array. isArray (context.languageId.languages)) {
66 lid.candidateCount = context.languageId.languages. length ;
67 lid.languages = context.languageId.languages. filter (( item ) => typeof item === "string" && locale (item)). slice ( 0 , 100 );
68 lid.omittedCandidates = lid.candidateCount - lid.languages. length ;
69 }
70 }
71 if ( object (detection.enrichment?.pronunciationAssessment)) {
72 features.pronunciationAssessment = { present: true };
73 }
74 if ( object (detection.speakerDiarization)) {
75 features.speakerDiarization = { present: true };
76 const mode = choice ( "Anonymous" )(detection.speakerDiarization.mode);
77 if (mode !== undefined ) features.speakerDiarization.mode = mode;
78 const diarize = choice ( "True" , "False" )(detection.speakerDiarization.diarizeIntermediates);
79 if (diarize !== undefined ) features.speakerDiarization.diarizeIntermediates = diarize;
80 }
81 for ( const mode of [ "INTERACTIVE" , "CONVERSATION" , "DICTATION" ]) {
82 const segmentation = detection[mode]?.segmentation;
83 if ( ! object (segmentation)) continue ;
84 const entry = { recognitionMode: mode };
85 const strategy = choice ( "Normal" , "Custom" , "Semantic" )(segmentation.mode);
86 if (strategy !== undefined ) entry.mode = strategy;
87 for ( const key of [ "segmentationSilenceTimeoutMs" , "segmentationForcedTimeoutMs" ]) {
88 if (Number. isSafeInteger (segmentation[key]) && segmentation[key] >= 0 ) entry[key] = segmentation[key];
89 }
90 (features.segmentation ??= []). push (entry);
91 }
92 if ( object (context.phraseOutput)) {
93 features.output = {};
94 const format = choice ( "Simple" , "Detailed" )(context.phraseOutput.format);
95 if (format !== undefined ) features.output.format = format;
96 const options = context.phraseOutput.detailed?.options;
97 if (Array. isArray (options)) {
98 features.output.options = options. filter (( item ) => [ "WordTimings" , "WordConfidence" , "SNR" ]. includes (item)). slice ( 0 , 10 );
99 }
100 }
101 if (Array. isArray (context.dgi?.Groups)) {
102 let count = 0 ;
103 for ( const group of context.dgi.Groups) {
104 if (Array. isArray (group?.Items)) count += group.Items. length ;
105 }
106 features.phraseList = { itemCount: count };
107 }
108 return features;
109 }
110
111 async function* lines ( file ) {
112 const stream = createReadStream (file, { encoding: "utf8" });
113 let pending = "" ;
114 let oversized = false ;
115 let bytes = 0 ;
116 for await ( const chunk of stream) {
117 bytes += Buffer. byteLength (chunk);
118 if (bytes > MAX_BYTES ) throw new Error ( "INPUT_TOO_LARGE" );
119 let start = 0 ;
120 for ( let index = chunk. indexOf ( " \n " ); index !== - 1 ; index = chunk. indexOf ( " \n " , start)) {
121 const part = chunk. slice (start, index);
122 yield oversized || pending. length + part. length > MAX_RECORD_CHARS ? null : (pending + part). replace ( / \r $ / , "" );
123 pending = "" ;
124 oversized = false ;
125 start = index + 1 ;
126 }
127 const tail = chunk. slice (start);
128 if ( ! oversized && pending. length + tail. length <= MAX_RECORD_CHARS ) pending += tail;
129 else {
130 oversized = true ;
131 pending = "" ;
132 }
133 }
134 if (oversized) yield null ;
135 else if (pending) yield pending. replace ( / \r $ / , "" );
136 }
137
138 function jsonPayload ( message , label ) {
139 const match = message. match ( new RegExp ( `^${ label }(?:='([ \\ s \\ S]*)'| \\ s+([ \\ s \\ S]*))$` ));
140 if ( ! match) return undefined ;
141 return JSON . parse (match[ 1 ] ?? match[ 2 ]);
142 }
143
144 export async function analyze ( file , maxObservations = 1000 ) {
145 if ( ! Number. isInteger (maxObservations) || maxObservations < 1 || maxObservations > 10000 ) throw new Error ( "INVALID_LIMIT" );
146 const info = await stat (file);
147 if ( ! info. isFile ()) throw new Error ( "NOT_A_FILE" );
148 if (info.size > MAX_BYTES ) throw new Error ( "INPUT_TOO_LARGE" );
149 const coverage = {
150 format: "unrecognized" , lines: 0 , nativeRecords: 0 , unrecognizedLines: 0 ,
151 oversizedRecords: 0 , malformedJsonRecords: 0 , unhandledNativeRecords: 0 ,
152 omittedObservations: 0 , relativeClockRegressions: 0 , observedVersions: [], omittedVersions: 0 ,
153 sourceCompatibility: "unknown" , utcMapping: "not_established" ,
154 sessionAttribution: "explicit_ids_only" ,
155 captureCompleteness: "unknown" ,
156 };
157 const observations = [];
158 const versions = new Set ();
159 let record;
160 let previousTime;
161 function emit ( entry ) {
162 if (observations. length < maxObservations) observations. push (entry);
163 else coverage.omittedObservations ++ ;
164 }
165 function consume ( current ) {
166 if ( ! current) return ;
167 if (current.oversized) {
168 coverage.oversizedRecords ++ ;
169 return ;
170 }
171 const { message , ... anchor } = current;
172 const add = ( kind , fields ) => emit ({ kind, ... anchor, ... fields });
173 const source = current.codeFile;
174 let match;
175 let headerObserved = false ;
176 if ([ "web_socket.cpp" , "usp_connection.cpp" , "uws_client.c" ]. includes (source)) {
177 // Only standalone protocol headers, never GUID-like strings in free text.
178 for ( const header of message. matchAll ( new RegExp ( `^X-ConnectionId: (${ ID }) \\ r?$` , "gm" ))) {
179 add ( "connection_header" , { connectionId: header[ 1 ] });
180 headerObserved = true ;
181 }
182 }
183 if (source === "named_properties.h" || source === "property_bag_impl.cpp" ) {
184 // The SDK does not escape apostrophes in logged values, so take everything up to the final delimiter.
185 match = message. match ( new RegExp ( `^ISpx(?:NamedProperties|PropertyBagImpl)::(GetStringValue|SetStringValue): this=(${ POINTER }); name='([^']+)'; value='([ \\ s \\ S]*)'$` ));
186 if (match) {
187 const [, method , address , name , raw ] = match;
188 const fields = { access: method === "SetStringValue" ? "write" : "read" , object: pointer (address), name };
189 if (name === "SessionId" && identifier (raw)) {
190 add ( "session_property" , { ... fields, sessionId: raw });
191 } else if (name === "RESULT-RequestId" && identifier (raw)) {
192 add ( "request_property" , { ... fields, protocolRequestId: raw });
193 } else if ( PRESENCE_PROPERTIES . has (name)) {
194 add ( "property" , { ... fields, value: "present_value_withheld" });
195 } else if ( PROPERTIES . has (name)) {
196 const value = PROPERTIES . get (name)(raw);
197 add ( "property" , { ... fields, value: value ?? "value_not_allowlisted" });
198 }
199 return ;
200 }
201 }
202 if (source === "usp_reco_engine_adapter.cpp" || source === "usp_tts_engine_adapter.cpp" ) {
203 for ( const label of [ "speech.config" , "speech.context" ]) {
204 if ( ! message. startsWith (label)) continue ;
205 try {
206 const payload = jsonPayload (message, label. replace ( "." , " \\ ." ));
207 if ( ! object (payload)) {
208 coverage.malformedJsonRecords ++ ;
209 return ;
210 }
211 if (label === "speech.config" ) {
212 const version = payload.context?.system?.version;
213 if ( typeof version === "string" && / ^ \d {1,3} \. \d {1,3} \. \d {1,3}$ / . test (version)) {
214 if (versions.size < 100 || versions. has (version)) versions. add (version);
215 else coverage.omittedVersions ++ ;
216 add ( "sdk_version" , { version });
217 }
218 } else {
219 const fields = { features: contextFeatures (payload), evidence: "generated_context_only" };
220 const session = identifier (payload.phraseDetection?.speakerDiarization?.audioSessionId);
221 if (session) fields.sessionId = session;
222 add ( "request_context" , fields);
223 }
224 } catch (error) {
225 if ( ! (error instanceof SyntaxError )) throw error;
226 coverage.malformedJsonRecords ++ ;
227 }
228 return ;
229 }
230 }
231 if (source === "audio_stream_session.cpp" ) {
232 match = message. match ( new RegExp ( `^ \\ [(${ POINTER }) \\ ]CSpxAudioStreamSession::FireSession(Started|Stopped)Event: Firing Session(?:Started|Stopped) event: SessionId: (${ ID }) \\ s*$` ));
233 if (match) {
234 add ( "session_event" , { object: pointer (match[ 1 ]), event: match[ 2 ] === "Started" ? "started" : "stopped" , sessionId: match[ 3 ] });
235 return ;
236 }
237 }
238 if (source === "recognition_result.cpp" ) {
239 // These traces log __FUNCTION__: MSVC includes the class prefix, GCC/Clang do not.
240 match = message. match ( new RegExp ( `^(?:CSpxRecognitionResult::)?Init(Intermediate|Final)Result: resultId=(${ ID })(?: \\ s|$)` ));
241 if (match) {
242 const fields = { stage: match[ 1 ]. toLowerCase (), resultId: match[ 2 ] };
243 const reason = message. match ( new RegExp ( `^(?:CSpxRecognitionResult::)?InitFinalResult: resultId=${ ID } reason=( \\ d{1,6}),` ));
244 if (reason) fields.reasonCode = Number (reason[ 1 ]);
245 add ( "recognition_result" , fields);
246 return ;
247 }
248 }
249 if (source === "synthesizer.cpp" ) {
250 match = message. match ( new RegExp ( `^(?:CSpxSynthesizer::)?ExecuteSynthesis: synthesis (started|done), request id: (${ ID });` ));
251 if (match) {
252 add ( "synthesis_event" , { event: match[ 1 ], synthesisRequestId: match[ 2 ] });
253 return ;
254 }
255 }
256 if (source === "usp_connection.cpp" ) {
257 match = message. match ( new RegExp ( `^CSpxUspConnection::SetConfiguration Set UspConnection Configuration with SessionId: (${ ID }) \\ s*$` ));
258 if (match) {
259 add ( "connection_configuration" , { sessionId: match[ 1 ] });
260 return ;
261 }
262 }
263 if (source === "uws_client.c" ) {
264 match = message. match ( / ^ Received WebSocket upgrade response with status code ( \d {3} )(?: \r ? \n |$ )/ );
265 if (match) {
266 add ( "websocket_upgrade" , { statusCode: Number (match[ 1 ]) });
267 return ;
268 }
269 }
270 if (source === "web_socket.cpp" ) {
271 match = message. match ( new RegExp ( `^WS \\ [(${ POINTER }) \\ ] open operation failed with result=( \\ d{1,6}) \\ ([A-Z_]+ \\ ), code=(-? \\ d{1,10})(?: \\ [|,)` ));
272 if (match) {
273 add ( "websocket_open_error" , { object: pointer (match[ 1 ]), resultCode: Number (match[ 2 ]), errorCode: Number (match[ 3 ]) });
274 return ;
275 }
276 }
277 if ( / ^ socketio_(?:win32 | berkeley) \. c $ / . test (source)) {
278 match = message. match ( / ^ Failure: getaddrinfo failure (- ? \d {1,10} )(?: \s |$ )/ );
279 if (match) {
280 add ( "dns_error" , { errorCode: Number (match[ 1 ]) });
281 return ;
282 }
283 }
284 if ( ! headerObserved) coverage.unhandledNativeRecords ++ ;
285 }
286 for await ( let line of lines (file)) {
287 coverage.lines ++ ;
288 if (coverage.lines === 1 && line !== null ) line = line. replace ( / ^\uFEFF / , "" );
289 if (line === null ) {
290 consume (record);
291 record = undefined ;
292 coverage.oversizedRecords ++ ;
293 continue ;
294 }
295 const match = line. match ( HEADER );
296 if (match) {
297 consume (record);
298 const time = Number (match[ 2 ]);
299 if (previousTime !== undefined && time < previousTime) coverage.relativeClockRegressions ++ ;
300 previousTime = time;
301 coverage.nativeRecords ++ ;
302 record = {
303 lineStart: coverage.lines, lineEnd: coverage.lines,
304 relativeMs: time, threadMarker: Number (match[ 1 ]),
305 codeFile: match[ 4 ]. split ( / [ \\ /] / ). at ( - 1 ), codeLine: Number (match[ 5 ]),
306 message: match[ 6 ],
307 };
308 } else if (record) {
309 coverage.unrecognizedLines ++ ;
310 record.lineEnd = coverage.lines;
311 if (record.message. length + line. length + 1 <= MAX_RECORD_CHARS && ! record.oversized) record.message += ` \n ${ line }` ;
312 else record.oversized = true ;
313 } else {
314 coverage.unrecognizedLines ++ ;
315 }
316 }
317 consume (record);
318 coverage.format = coverage.nativeRecords ? "native_text" : "unrecognized" ;
319 coverage.observedVersions = [ ... versions]. sort ();
320 coverage.sourceCompatibility = versions.size === 0 ? "unknown"
321 : versions.size === 1 && versions. has ( "1.51.2" ) ? "matches_reviewed_version" : "unreviewed_or_mixed_versions" ;
322 if (coverage.omittedVersions) coverage.sourceCompatibility = "unreviewed_or_mixed_versions" ;
323 const ids = { sessions: [], results: [], synthesisRequests: [], protocolRequests: [], connections: [] };
324 for ( const entry of observations) {
325 entry.scope = entry.sessionId ? "explicit_session" : entry.synthesisRequestId ? "explicit_synthesis_request"
326 : entry.resultId ? "explicit_result" : entry.object ? "object_only" : "unassociated" ;
327 for ( const [ key , list ] of [[ "sessionId" , "sessions" ], [ "resultId" , "results" ], [ "synthesisRequestId" , "synthesisRequests" ], [ "protocolRequestId" , "protocolRequests" ], [ "connectionId" , "connections" ]]) {
328 if (entry[key] && ! ids[list]. includes (entry[key])) ids[list]. push (entry[key]);
329 }
330 }
331 return {
332 schemaVersion: 1 ,
333 source: { sdkVersion: "1.51.2" , commit: SOURCE_COMMIT },
334 coverage, ids, observations,
335 limits: [
336 "Allowlist summary, not a sanitized copy of the input." ,
337 "IDs and observations are unverified log claims; review before external sharing." ,
338 "No thread/time-only session attribution. Missing observations do not mean disabled or healthy." ,
339 "Relative time is not UTC. No automatic root cause or service-health conclusion." ,
340 ],
341 };
342 }
343
344 async function main ( args ) {
345 if (args. length === 1 && (args[ 0 ] === "--help" || args[ 0 ] === "-h" )) {
346 console. log ( "Usage: node analyze-speech-sdk-log.mjs --file <UTF-8-log> [--max-observations <1-10000>]" );
347 return ;
348 }
349 let file;
350 let limit = 1000 ;
351 const seen = new Set ();
352 for ( let index = 0 ; index < args. length ; index += 2 ) {
353 const flag = args[index];
354 const value = args[index + 1 ];
355 if ( ! [ "--file" , "--max-observations" ]. includes (flag) || seen. has (flag) || ! value) throw new Error ( "INVALID_ARGUMENTS" );
356 seen. add (flag);
357 if (flag === "--file" ) file = value;
358 else {
359 if ( ! / ^ \d {1,5}$ / . test (value)) throw new Error ( "INVALID_LIMIT" );
360 limit = Number (value);
361 }
362 }
363 if ( ! file) throw new Error ( "MISSING_FILE" );
364 console. log ( JSON . stringify ( await analyze (file, limit), null , 2 ));
365 }
366
367 if (process.argv[ 1 ] && realpathSync ( fileURLToPath ( import . meta .url)) === realpathSync ( resolve (process.argv[ 1 ]))) {
368 main (process.argv. slice ( 2 )). catch (( error ) => {
369 const usageErrors = new Set ([ "INVALID_ARGUMENTS" , "INVALID_LIMIT" , "MISSING_FILE" ]);
370 const known = new Set ([ ... usageErrors, "NOT_A_FILE" , "INPUT_TOO_LARGE" ]);
371 const code = known. has (error.message) ? error.message
372 : [ "ENOENT" , "EACCES" , "EPERM" , "EISDIR" ]. includes (error.code) ? error.code : "READ_OR_ANALYSIS_FAILED" ;
373 console. error ( JSON . stringify ({ error: code, help: "Use --help. Input contents and paths are withheld." }));
374 process.exitCode = usageErrors. has (code) ? 2 : 1 ;
375 });
376 }