style: lint
This commit is contained in:
@@ -21,24 +21,19 @@ const audioTestAPI = createAudioTestAPI<
|
||||
{ audio: AudioInputSession },
|
||||
AudioInputPreflightContext
|
||||
>({
|
||||
preflight: definition => definition.preflight,
|
||||
createPlans(name, testCase) {
|
||||
const task = createAudioTestTask(name, testCase)
|
||||
return [{
|
||||
name: task.name,
|
||||
definition: task,
|
||||
metadata: {
|
||||
input: fileURLToPath(task.input),
|
||||
runtime: inject('fakemicRuntime').name,
|
||||
},
|
||||
name: task.name,
|
||||
}]
|
||||
},
|
||||
async execute({ plan, task, invokeHandler, runPreflight }) {
|
||||
async execute({ invokeHandler, plan, runPreflight, task }) {
|
||||
await runAudioTestSession({
|
||||
start() {
|
||||
const microphoneInput = fileURLToPath(plan.definition.input)
|
||||
return startFakemicRuntime<AudioInputSession>(microphoneInput)
|
||||
},
|
||||
async execute(session) {
|
||||
await runPreflight({
|
||||
env,
|
||||
@@ -62,8 +57,13 @@ const audioTestAPI = createAudioTestAPI<
|
||||
contentType: 'application/json',
|
||||
})
|
||||
},
|
||||
start() {
|
||||
const microphoneInput = fileURLToPath(plan.definition.input)
|
||||
return startFakemicRuntime<AudioInputSession>(microphoneInput)
|
||||
},
|
||||
})
|
||||
},
|
||||
preflight: definition => definition.preflight,
|
||||
})
|
||||
|
||||
/** Groups AIRI audio-input tests in the Vitest task tree. */
|
||||
|
||||
@@ -28,16 +28,26 @@ describe('audio input matchers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
function actionEvent(storeId: string, actionName: string): PiniaActionEvent {
|
||||
return {
|
||||
actionName,
|
||||
invocationId: `${storeId}:${actionName}`,
|
||||
status: 'completed',
|
||||
storeId,
|
||||
timestamp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function createAudioInputSession(
|
||||
transcriptions: string[],
|
||||
actions: PiniaActionEvent[] = [],
|
||||
): AudioInputObservations {
|
||||
return {
|
||||
capturedTranscriptionAudio: async () => [],
|
||||
streamingTranscriptionUpdates: async () => [],
|
||||
transcriptionResults: async () => transcriptions,
|
||||
completedSpans: async () => [],
|
||||
piniaActionEvents: async () => actions,
|
||||
streamingTranscriptionUpdates: async () => [],
|
||||
transcriptionResults: async () => transcriptions,
|
||||
waitForPiniaAction: async () => {
|
||||
throw new Error('This matcher fixture does not observe Pinia actions.')
|
||||
},
|
||||
@@ -48,13 +58,3 @@ function createAudioInputSession(
|
||||
waitForVadReady: async () => {},
|
||||
}
|
||||
}
|
||||
|
||||
function actionEvent(storeId: string, actionName: string): PiniaActionEvent {
|
||||
return {
|
||||
actionName,
|
||||
invocationId: `${storeId}:${actionName}`,
|
||||
status: 'completed',
|
||||
storeId,
|
||||
timestamp: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,51 +7,37 @@ export interface CapturedTranscriptionAudioExpectation {
|
||||
minimumBytes: number
|
||||
}
|
||||
|
||||
export interface TranscriptionExpectationOptions {
|
||||
/** @default 'exact' */
|
||||
match?: 'exact' | 'contains'
|
||||
}
|
||||
|
||||
/** Sets the wait limit for a transcription action assertion. */
|
||||
export interface TranscriptionActionExpectationOptions {
|
||||
/** Maximum time to wait for a completed ASR action. @default 60000 */
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export interface TranscriptionExpectationOptions {
|
||||
/** @default 'exact' */
|
||||
match?: 'contains' | 'exact'
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
interface Assertion<T> {
|
||||
toHaveCapturedTranscriptionAudio: T extends AudioInputObservations
|
||||
? (expected: CapturedTranscriptionAudioExpectation) => Promise<void>
|
||||
: never
|
||||
toHaveCompletedTranscription: T extends AudioInputObservations
|
||||
? (options?: TranscriptionActionExpectationOptions) => Promise<void>
|
||||
: never
|
||||
toHaveTranscriptions: T extends AudioInputObservations
|
||||
? (
|
||||
expected: ReadonlyArray<ReadonlyArray<string>>,
|
||||
options?: TranscriptionExpectationOptions,
|
||||
) => Promise<void>
|
||||
: never
|
||||
toHaveCompletedTranscription: T extends AudioInputObservations
|
||||
? (options?: TranscriptionActionExpectationOptions) => Promise<void>
|
||||
: never
|
||||
}
|
||||
}
|
||||
|
||||
/** Vitest expect with AIRI audio-input matcher types. */
|
||||
export const expect = vitestExpect
|
||||
|
||||
/**
|
||||
* Normalizes speech text for transcript comparison.
|
||||
*
|
||||
* @example
|
||||
* normalizeSpeechText(' Hello, AIRI! ')
|
||||
* // => 'helloairi'
|
||||
*/
|
||||
function normalizeSpeechText(value: string): string {
|
||||
return value
|
||||
.normalize('NFKC')
|
||||
.toLocaleLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '')
|
||||
}
|
||||
|
||||
/** Installs asynchronous matchers for AIRI audio-input observations. */
|
||||
export function installAudioInputMatchers(): void {
|
||||
vitestExpect.extend({
|
||||
@@ -62,8 +48,8 @@ export function installAudioInputMatchers(): void {
|
||||
const format = session.transcriptionCaptureFormat
|
||||
if (!format) {
|
||||
return {
|
||||
pass: false,
|
||||
message: () => 'The active transcription Provider does not expose uploaded audio.',
|
||||
pass: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,10 +62,22 @@ export function installAudioInputMatchers(): void {
|
||||
const pass = captures.length === expected.count && !invalidCapture
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () => pass
|
||||
? 'Expected the session not to contain valid transcription audio.'
|
||||
: `Expected ${expected.count} ${format} capture(s) with at least ${expected.minimumBytes} bytes.`,
|
||||
pass,
|
||||
}
|
||||
},
|
||||
async toHaveCompletedTranscription(
|
||||
session: AudioInputObservations,
|
||||
options: TranscriptionActionExpectationOptions = {},
|
||||
) {
|
||||
const result = await waitForTranscription(session, options.timeout ?? 60_000)
|
||||
return {
|
||||
message: () => result.complete
|
||||
? 'Expected the transcription action not to complete.'
|
||||
: `Expected the transcription action to complete, but ${result.summary}.`,
|
||||
pass: result.complete,
|
||||
}
|
||||
},
|
||||
async toHaveTranscriptions(
|
||||
@@ -99,46 +97,34 @@ export function installAudioInputMatchers(): void {
|
||||
))
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () => pass
|
||||
? 'Expected the session not to contain the specified transcriptions.'
|
||||
: `Expected transcriptions ${JSON.stringify(expected)}, but received ${JSON.stringify(actual)}.`,
|
||||
}
|
||||
},
|
||||
async toHaveCompletedTranscription(
|
||||
session: AudioInputObservations,
|
||||
options: TranscriptionActionExpectationOptions = {},
|
||||
) {
|
||||
const result = await waitForTranscription(session, options.timeout ?? 60_000)
|
||||
return {
|
||||
pass: result.complete,
|
||||
message: () => result.complete
|
||||
? 'Expected the transcription action not to complete.'
|
||||
: `Expected the transcription action to complete, but ${result.summary}.`,
|
||||
pass,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const transcriptionActions = [
|
||||
{ storeId: 'modules:hearing:speech:audio-input-pipeline', actionName: 'transcribeForRecording' },
|
||||
{ storeId: 'modules:hearing:speech:audio-input-pipeline', actionName: 'transcribeForMediaStream' },
|
||||
]
|
||||
|
||||
async function waitForTranscription(
|
||||
session: AudioInputObservations,
|
||||
timeout: number,
|
||||
): Promise<{ complete: boolean, failed: boolean, summary: string }> {
|
||||
const deadline = Date.now() + timeout
|
||||
let result = transcriptionResult(await session.piniaActionEvents())
|
||||
|
||||
while (!result.complete && !result.failed && Date.now() < deadline) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
result = transcriptionResult(await session.piniaActionEvents())
|
||||
}
|
||||
return result
|
||||
/**
|
||||
* Normalizes speech text for transcript comparison.
|
||||
*
|
||||
* @example
|
||||
* normalizeSpeechText(' Hello, AIRI! ')
|
||||
* // => 'helloairi'
|
||||
*/
|
||||
function normalizeSpeechText(value: string): string {
|
||||
return value
|
||||
.normalize('NFKC')
|
||||
.toLocaleLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '')
|
||||
}
|
||||
|
||||
const transcriptionActions = [
|
||||
{ actionName: 'transcribeForRecording', storeId: 'modules:hearing:speech:audio-input-pipeline' },
|
||||
{ actionName: 'transcribeForMediaStream', storeId: 'modules:hearing:speech:audio-input-pipeline' },
|
||||
]
|
||||
|
||||
function transcriptionResult(
|
||||
events: Awaited<ReturnType<AudioInputObservations['piniaActionEvents']>>,
|
||||
): { complete: boolean, failed: boolean, summary: string } {
|
||||
@@ -159,3 +145,17 @@ function transcriptionResult(
|
||||
summary: 'ASR did not complete',
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForTranscription(
|
||||
session: AudioInputObservations,
|
||||
timeout: number,
|
||||
): Promise<{ complete: boolean, failed: boolean, summary: string }> {
|
||||
const deadline = Date.now() + timeout
|
||||
let result = transcriptionResult(await session.piniaActionEvents())
|
||||
|
||||
while (!result.complete && !result.failed && Date.now() < deadline) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
result = transcriptionResult(await session.piniaActionEvents())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ export default async function prepareElectronRuntime(context: FakemicElectronPre
|
||||
await page.evaluate(stubForBrowser, piniaActionTracingChannelName)
|
||||
|
||||
return createSession({
|
||||
close: context.close,
|
||||
electronApp: context.app,
|
||||
page,
|
||||
target: 'electron',
|
||||
close: context.close,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ export default async function prepareWebRuntime(context: FakemicWebPrepareContex
|
||||
await page.goto(context.runtime.url)
|
||||
|
||||
return createSession({
|
||||
close: context.close,
|
||||
page,
|
||||
target: 'web',
|
||||
close: context.close,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { SerializedIOSpan } from '@proj-airi/stage-shared/types/io-trace'
|
||||
import type { PiniaActionEvent } from '@proj-airi/stage-shared/types/pinia-action-event'
|
||||
|
||||
/** Returns the completed browser spans that match the optional span name. */
|
||||
export function readCompletedSpans(name?: string) {
|
||||
const spans = window.__airiAudioInputE2E?.spans ?? []
|
||||
return name ? spans.filter(span => span.name === name) : spans
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs passive browser probes before the application starts.
|
||||
*
|
||||
@@ -134,7 +140,16 @@ export function stubForBrowser(piniaActionChannelName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
|
||||
close(code?: number, reason?: string): void {
|
||||
const audioChunks = audioChunksBySocket.get(this)
|
||||
if (capturesAliyunNlsBySocket.get(this) && audioChunks && !capturedSockets.has(this) && audioChunks.length) {
|
||||
capturePcmAudio(this)
|
||||
}
|
||||
|
||||
super.close(code, reason)
|
||||
}
|
||||
|
||||
send(data: ArrayBufferLike | ArrayBufferView | Blob | string): void {
|
||||
const audioChunks = audioChunksBySocket.get(this)
|
||||
if (capturesAliyunNlsBySocket.get(this) && audioChunks && typeof data !== 'string') {
|
||||
if (data instanceof Blob) {
|
||||
@@ -151,7 +166,7 @@ export function stubForBrowser(piniaActionChannelName: string) {
|
||||
capturePcmAudio(this)
|
||||
}
|
||||
|
||||
let outboundData: string | Blob | BufferSource
|
||||
let outboundData: Blob | BufferSource | string
|
||||
if (typeof data === 'string' || data instanceof Blob || data instanceof ArrayBuffer) {
|
||||
outboundData = data
|
||||
}
|
||||
@@ -166,15 +181,6 @@ export function stubForBrowser(piniaActionChannelName: string) {
|
||||
|
||||
super.send(outboundData)
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string): void {
|
||||
const audioChunks = audioChunksBySocket.get(this)
|
||||
if (capturesAliyunNlsBySocket.get(this) && audioChunks && !capturedSockets.has(this) && audioChunks.length) {
|
||||
capturePcmAudio(this)
|
||||
}
|
||||
|
||||
super.close(code, reason)
|
||||
}
|
||||
}
|
||||
|
||||
window.WebSocket = CaptureAliyunNlsWebSocket
|
||||
@@ -203,9 +209,3 @@ export function stubForBrowser(piniaActionChannelName: string) {
|
||||
|
||||
channel.addEventListener('message', captureSpan)
|
||||
}
|
||||
|
||||
/** Returns the completed browser spans that match the optional span name. */
|
||||
export function readCompletedSpans(name?: string) {
|
||||
const spans = window.__airiAudioInputE2E?.spans ?? []
|
||||
return name ? spans.filter(span => span.name === name) : spans
|
||||
}
|
||||
|
||||
@@ -13,18 +13,13 @@ import { readCompletedSpans } from './browser-probe'
|
||||
|
||||
/** Creates the runtime session for one audio test. */
|
||||
export function createSession(options: {
|
||||
close: () => Promise<void>
|
||||
electronApp?: ElectronApplication
|
||||
page: Page
|
||||
target: AudioInputTarget
|
||||
close: () => Promise<void>
|
||||
transcriptionCaptureFormat?: AudioCapture['format']
|
||||
}): AudioInputSession {
|
||||
const session: AudioInputSession = {
|
||||
electronApp: options.electronApp,
|
||||
page: options.page,
|
||||
runtimePage: options.page,
|
||||
target: options.target,
|
||||
transcriptionCaptureFormat: options.transcriptionCaptureFormat,
|
||||
activatePage(page) {
|
||||
session.page = page
|
||||
},
|
||||
@@ -56,17 +51,11 @@ export function createSession(options: {
|
||||
}
|
||||
const capturedAudio = await options.page.evaluate(() => window.__airiAudioInputE2E?.transcriptionAudio ?? [])
|
||||
return capturedAudio.map(audio => ({
|
||||
format: audio.format,
|
||||
data: Buffer.from(audio.base64, 'base64'),
|
||||
format: audio.format,
|
||||
}))
|
||||
},
|
||||
streamingTranscriptionUpdates: () => session.page.evaluate(() => window.__airiAudioInputE2E?.streamingTranscriptionUpdates ?? []),
|
||||
async transcriptionResults(count) {
|
||||
await options.page.waitForFunction(expectedCount => (
|
||||
(window.__airiAudioInputE2E?.transcriptionResults.length ?? 0) >= expectedCount
|
||||
), count, { timeout: 60_000 })
|
||||
return options.page.evaluate(() => window.__airiAudioInputE2E?.transcriptionResults ?? [])
|
||||
},
|
||||
close: options.close,
|
||||
async completedSpans(name) {
|
||||
const runtimeSpans = await options.page.evaluate(readCompletedSpans, name)
|
||||
if (session.page === options.page)
|
||||
@@ -75,28 +64,30 @@ export function createSession(options: {
|
||||
const interactionSpans = await session.page.evaluate(readCompletedSpans, name)
|
||||
return [...runtimeSpans, ...interactionSpans]
|
||||
},
|
||||
async waitForTurn(waitOptions = {}) {
|
||||
const timeout = waitOptions.timeout ?? 60_000
|
||||
const deadline = Date.now() + timeout
|
||||
let turn = createTurnObservation(await session.completedSpans())
|
||||
|
||||
while (!turn && Date.now() < deadline) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
turn = createTurnObservation(await session.completedSpans())
|
||||
}
|
||||
|
||||
if (!turn)
|
||||
throw new Error('Timed out waiting for a completed LLM and speech turn.')
|
||||
|
||||
turn.chat.messages = await readChatMessages(session.page)
|
||||
return turn
|
||||
},
|
||||
electronApp: options.electronApp,
|
||||
page: options.page,
|
||||
piniaActionEvents: () => options.page.evaluate(() => window.__airiAudioInputE2E?.piniaActionEvents ?? []),
|
||||
async waitForVadReady() {
|
||||
await options.page.waitForFunction(() => window.__airiAudioInputE2E?.vadReady === true, undefined, { timeout: 30_000 })
|
||||
runtimePage: options.page,
|
||||
async snapshot() {
|
||||
const runtimeState = await options.page.evaluate(() => window.__airiAudioInputE2E)
|
||||
const interactionState = session.page === options.page
|
||||
? runtimeState
|
||||
: await session.page.evaluate(() => window.__airiAudioInputE2E)
|
||||
return {
|
||||
piniaActionEvents: runtimeState?.piniaActionEvents ?? [],
|
||||
spans: runtimeState?.spans ?? [],
|
||||
streamingTranscriptionUpdates: interactionState?.streamingTranscriptionUpdates ?? [],
|
||||
transcriptionResults: runtimeState?.transcriptionResults ?? [],
|
||||
}
|
||||
},
|
||||
async waitForStreamingTranscriptionReady() {
|
||||
await options.page.waitForFunction(() => window.__airiAudioInputE2E?.streamingTranscriptionReady === true, undefined, { timeout: 30_000 })
|
||||
streamingTranscriptionUpdates: () => session.page.evaluate(() => window.__airiAudioInputE2E?.streamingTranscriptionUpdates ?? []),
|
||||
target: options.target,
|
||||
transcriptionCaptureFormat: options.transcriptionCaptureFormat,
|
||||
async transcriptionResults(count) {
|
||||
await options.page.waitForFunction(expectedCount => (
|
||||
(window.__airiAudioInputE2E?.transcriptionResults.length ?? 0) >= expectedCount
|
||||
), count, { timeout: 60_000 })
|
||||
return options.page.evaluate(() => window.__airiAudioInputE2E?.transcriptionResults ?? [])
|
||||
},
|
||||
async waitForPiniaAction(waitOptions) {
|
||||
return options.page.evaluate(({ actionName, channelName, status, storeId, timeout }) => new Promise<PiniaActionEvent>((resolve, reject) => {
|
||||
@@ -123,19 +114,28 @@ export function createSession(options: {
|
||||
timeout: waitOptions.timeout ?? 60_000,
|
||||
})
|
||||
},
|
||||
async snapshot() {
|
||||
const runtimeState = await options.page.evaluate(() => window.__airiAudioInputE2E)
|
||||
const interactionState = session.page === options.page
|
||||
? runtimeState
|
||||
: await session.page.evaluate(() => window.__airiAudioInputE2E)
|
||||
return {
|
||||
piniaActionEvents: runtimeState?.piniaActionEvents ?? [],
|
||||
spans: runtimeState?.spans ?? [],
|
||||
streamingTranscriptionUpdates: interactionState?.streamingTranscriptionUpdates ?? [],
|
||||
transcriptionResults: runtimeState?.transcriptionResults ?? [],
|
||||
async waitForStreamingTranscriptionReady() {
|
||||
await options.page.waitForFunction(() => window.__airiAudioInputE2E?.streamingTranscriptionReady === true, undefined, { timeout: 30_000 })
|
||||
},
|
||||
async waitForTurn(waitOptions = {}) {
|
||||
const timeout = waitOptions.timeout ?? 60_000
|
||||
const deadline = Date.now() + timeout
|
||||
let turn = createTurnObservation(await session.completedSpans())
|
||||
|
||||
while (!turn && Date.now() < deadline) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
turn = createTurnObservation(await session.completedSpans())
|
||||
}
|
||||
|
||||
if (!turn)
|
||||
throw new Error('Timed out waiting for a completed LLM and speech turn.')
|
||||
|
||||
turn.chat.messages = await readChatMessages(session.page)
|
||||
return turn
|
||||
},
|
||||
async waitForVadReady() {
|
||||
await options.page.waitForFunction(() => window.__airiAudioInputE2E?.vadReady === true, undefined, { timeout: 30_000 })
|
||||
},
|
||||
close: options.close,
|
||||
}
|
||||
|
||||
return session
|
||||
@@ -168,8 +168,8 @@ function createTurnObservation(spans: Awaited<ReturnType<AudioInputSession['comp
|
||||
}))
|
||||
|
||||
return {
|
||||
id: turnId,
|
||||
chat: { messages: [] },
|
||||
id: turnId,
|
||||
llm: {
|
||||
inputMessages: inputMessageRoles.map(role => ({ role })),
|
||||
outputCharacters: numberAttribute(llmSpan, IOAttributes.LLMTextLength),
|
||||
@@ -179,6 +179,16 @@ function createTurnObservation(spans: Awaited<ReturnType<AudioInputSession['comp
|
||||
}
|
||||
}
|
||||
|
||||
function numberArrayAttribute(span: Awaited<ReturnType<AudioInputSession['completedSpans']>>[number], name: string): number[] {
|
||||
const value = span.attributes[name]
|
||||
return Array.isArray(value) ? value.filter(item => typeof item === 'number') : []
|
||||
}
|
||||
|
||||
function numberAttribute(span: Awaited<ReturnType<AudioInputSession['completedSpans']>>[number], name: string): number {
|
||||
const value = span.attributes[name]
|
||||
return typeof value === 'number' ? value : 0
|
||||
}
|
||||
|
||||
async function readChatMessages(page: Page): Promise<AudioInputChatMessage[]> {
|
||||
return page.locator('[data-chat-message-role]').evaluateAll(elements => elements.flatMap((element) => {
|
||||
const role = element.getAttribute('data-chat-message-role')
|
||||
@@ -189,22 +199,12 @@ async function readChatMessages(page: Page): Promise<AudioInputChatMessage[]> {
|
||||
}))
|
||||
}
|
||||
|
||||
function numberAttribute(span: Awaited<ReturnType<AudioInputSession['completedSpans']>>[number], name: string): number {
|
||||
function stringArrayAttribute(span: Awaited<ReturnType<AudioInputSession['completedSpans']>>[number], name: string): string[] {
|
||||
const value = span.attributes[name]
|
||||
return typeof value === 'number' ? value : 0
|
||||
}
|
||||
|
||||
function numberArrayAttribute(span: Awaited<ReturnType<AudioInputSession['completedSpans']>>[number], name: string): number[] {
|
||||
const value = span.attributes[name]
|
||||
return Array.isArray(value) ? value.filter(item => typeof item === 'number') : []
|
||||
return Array.isArray(value) ? value.filter(item => typeof item === 'string') : []
|
||||
}
|
||||
|
||||
function stringAttribute(span: Awaited<ReturnType<AudioInputSession['completedSpans']>>[number], name: string): string {
|
||||
const value = span.attributes[name]
|
||||
return typeof value === 'string' ? value : ''
|
||||
}
|
||||
|
||||
function stringArrayAttribute(span: Awaited<ReturnType<AudioInputSession['completedSpans']>>[number], name: string): string[] {
|
||||
const value = span.attributes[name]
|
||||
return Array.isArray(value) ? value.filter(item => typeof item === 'string') : []
|
||||
}
|
||||
|
||||
@@ -5,32 +5,6 @@ import type { ElectronApplication, Page } from 'playwright'
|
||||
|
||||
export const audioInputTargets = ['web', 'electron'] as const
|
||||
|
||||
export type AudioInputTarget = (typeof audioInputTargets)[number]
|
||||
|
||||
/** Values available when one case resolves its preflight callbacks. */
|
||||
export interface AudioInputPreflightContext {
|
||||
/** Environment variables loaded for this case. */
|
||||
env: Readonly<NodeJS.ProcessEnv>
|
||||
/** Clean runtime that the configuration callback can prepare. */
|
||||
runtime: AudioInputSession
|
||||
/** Skips this case when its environment does not satisfy a case constraint. */
|
||||
skip: (condition: unknown, note?: string) => void
|
||||
}
|
||||
|
||||
/** One optional case callback that resolves configuration from its environment. */
|
||||
export type AudioInputPreflightCallback = AudioTestPreflightCallback<AudioInputPreflightContext>
|
||||
|
||||
/** One AIRI audio-input test definition. */
|
||||
export type AudioInputTestCase = AudioTestCase<AudioInputPreflightContext>
|
||||
|
||||
/** Snapshot of the observable AIRI audio pipeline state. */
|
||||
export interface AudioInputSnapshot {
|
||||
piniaActionEvents: PiniaActionEvent[]
|
||||
spans: SerializedIOSpan[]
|
||||
streamingTranscriptionUpdates: string[]
|
||||
transcriptionResults: string[]
|
||||
}
|
||||
|
||||
/** One rendered chat message from the completed turn. */
|
||||
export interface AudioInputChatMessage {
|
||||
role: 'assistant' | 'user'
|
||||
@@ -47,47 +21,16 @@ export interface AudioInputLLMOutputChunk {
|
||||
characters: number
|
||||
}
|
||||
|
||||
/** One synthesized audio segment routed from the LLM turn. */
|
||||
export interface AudioInputTTSSegment {
|
||||
durationMs: number
|
||||
text: string
|
||||
}
|
||||
|
||||
/** Structured observations for one completed voice interaction turn. */
|
||||
export interface AudioInputTurn {
|
||||
id: string
|
||||
chat: {
|
||||
messages: AudioInputChatMessage[]
|
||||
}
|
||||
llm: {
|
||||
inputMessages: AudioInputLLMMessage[]
|
||||
outputCharacters: number
|
||||
outputChunks: AudioInputLLMOutputChunk[]
|
||||
}
|
||||
tts: {
|
||||
audioSegments: AudioInputTTSSegment[]
|
||||
}
|
||||
}
|
||||
|
||||
/** Observable audio values used by AIRI matchers. */
|
||||
export interface AudioInputObservations {
|
||||
/** Capture format used by the active transcription Provider. */
|
||||
transcriptionCaptureFormat?: AudioCaptureFormat
|
||||
capturedTranscriptionAudio: (count: number) => Promise<AudioCapture[]>
|
||||
streamingTranscriptionUpdates: () => Promise<string[]>
|
||||
transcriptionResults: (count: number) => Promise<string[]>
|
||||
completedSpans: (name?: string) => Promise<SerializedIOSpan[]>
|
||||
/** Waits until all speech work for the latest LLM turn is complete. */
|
||||
waitForTurn: (options?: {
|
||||
/** @default 60000 */
|
||||
timeout?: number
|
||||
}) => Promise<AudioInputTurn>
|
||||
/** Returns the Pinia action events collected by the runtime probe. */
|
||||
piniaActionEvents: () => Promise<PiniaActionEvent[]>
|
||||
/** Waits until the VAD audio graph is connected to the microphone stream. */
|
||||
waitForVadReady: () => Promise<void>
|
||||
/** Waits until a streaming transcription transport accepts microphone audio. */
|
||||
waitForStreamingTranscriptionReady: () => Promise<void>
|
||||
streamingTranscriptionUpdates: () => Promise<string[]>
|
||||
/** Capture format used by the active transcription Provider. */
|
||||
transcriptionCaptureFormat?: AudioCaptureFormat
|
||||
transcriptionResults: (count: number) => Promise<string[]>
|
||||
/**
|
||||
* Waits for the next matching Pinia action event after this method is called.
|
||||
*
|
||||
@@ -100,26 +43,83 @@ export interface AudioInputObservations {
|
||||
* await completed
|
||||
*/
|
||||
waitForPiniaAction: (options: {
|
||||
storeId: string
|
||||
actionName: string
|
||||
/** @default 'completed' */
|
||||
status?: PiniaActionEventStatus
|
||||
storeId: string
|
||||
/** @default 60000 */
|
||||
timeout?: number
|
||||
}) => Promise<PiniaActionEvent>
|
||||
/** Waits until a streaming transcription transport accepts microphone audio. */
|
||||
waitForStreamingTranscriptionReady: () => Promise<void>
|
||||
/** Waits until all speech work for the latest LLM turn is complete. */
|
||||
waitForTurn: (options?: {
|
||||
/** @default 60000 */
|
||||
timeout?: number
|
||||
}) => Promise<AudioInputTurn>
|
||||
/** Waits until the VAD audio graph is connected to the microphone stream. */
|
||||
waitForVadReady: () => Promise<void>
|
||||
}
|
||||
|
||||
/** One optional case callback that resolves configuration from its environment. */
|
||||
export type AudioInputPreflightCallback = AudioTestPreflightCallback<AudioInputPreflightContext>
|
||||
|
||||
/** Values available when one case resolves its preflight callbacks. */
|
||||
export interface AudioInputPreflightContext {
|
||||
/** Environment variables loaded for this case. */
|
||||
env: Readonly<NodeJS.ProcessEnv>
|
||||
/** Clean runtime that the configuration callback can prepare. */
|
||||
runtime: AudioInputSession
|
||||
/** Skips this case when its environment does not satisfy a case constraint. */
|
||||
skip: (condition: unknown, note?: string) => void
|
||||
}
|
||||
|
||||
/** Runtime handle for one AIRI audio-input test. */
|
||||
export interface AudioInputSession extends AudioInputObservations, AudioTestSession {
|
||||
/** Selects the page used by subsequent case interactions. */
|
||||
activatePage: (page: Page) => void
|
||||
/** Electron application for Electron tasks. */
|
||||
electronApp?: ElectronApplication
|
||||
/** Page used by case interactions. */
|
||||
page: Page
|
||||
/** Page that owns the audio-input pipeline. */
|
||||
runtimePage: Page
|
||||
snapshot: () => Promise<AudioInputSnapshot>
|
||||
/** Runtime selected for this concrete task. */
|
||||
target: AudioInputTarget
|
||||
/** Selects the page used by subsequent case interactions. */
|
||||
activatePage: (page: Page) => void
|
||||
snapshot: () => Promise<AudioInputSnapshot>
|
||||
}
|
||||
|
||||
/** Snapshot of the observable AIRI audio pipeline state. */
|
||||
export interface AudioInputSnapshot {
|
||||
piniaActionEvents: PiniaActionEvent[]
|
||||
spans: SerializedIOSpan[]
|
||||
streamingTranscriptionUpdates: string[]
|
||||
transcriptionResults: string[]
|
||||
}
|
||||
|
||||
export type AudioInputTarget = (typeof audioInputTargets)[number]
|
||||
|
||||
/** One AIRI audio-input test definition. */
|
||||
export type AudioInputTestCase = AudioTestCase<AudioInputPreflightContext>
|
||||
|
||||
/** One synthesized audio segment routed from the LLM turn. */
|
||||
export interface AudioInputTTSSegment {
|
||||
durationMs: number
|
||||
text: string
|
||||
}
|
||||
|
||||
/** Structured observations for one completed voice interaction turn. */
|
||||
export interface AudioInputTurn {
|
||||
chat: {
|
||||
messages: AudioInputChatMessage[]
|
||||
}
|
||||
id: string
|
||||
llm: {
|
||||
inputMessages: AudioInputLLMMessage[]
|
||||
outputCharacters: number
|
||||
outputChunks: AudioInputLLMOutputChunk[]
|
||||
}
|
||||
tts: {
|
||||
audioSegments: AudioInputTTSSegment[]
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -12,10 +12,6 @@ declare global {
|
||||
vadReady: boolean
|
||||
}
|
||||
|
||||
interface Window {
|
||||
__airiAudioInputE2E?: BrowserAudioInputState
|
||||
}
|
||||
|
||||
// NOTICE:
|
||||
// TypeScript 5.9 does not declare the Baseline 2025 Uint8Array Base64 methods.
|
||||
// The test runs in current Playwright and Electron Chromium runtimes that implement this API.
|
||||
@@ -27,6 +23,10 @@ declare global {
|
||||
omitPadding?: boolean
|
||||
}) => string
|
||||
}
|
||||
|
||||
interface Window {
|
||||
__airiAudioInputE2E?: BrowserAudioInputState
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
|
||||
Reference in New Issue
Block a user