diff --git a/apps/stage-tamagotchi/electron-builder.config.ts b/apps/stage-tamagotchi/electron-builder.config.ts index f8d55dd94..012778b1b 100644 --- a/apps/stage-tamagotchi/electron-builder.config.ts +++ b/apps/stage-tamagotchi/electron-builder.config.ts @@ -206,14 +206,11 @@ export default { // - Linux arm64 -> `latest-arm64-linux-arm64.yml` channel: 'latest-${arch}', }, - extendInfo: [ - { - NSMicrophoneUsageDescription: 'AIRI requires microphone access for voice interaction', - }, - { - NSCameraUsageDescription: 'AIRI requires camera access for vision understanding', - }, - ], + extendInfo: { + NSMicrophoneUsageDescription: 'AIRI requires microphone access for voice interaction', + NSSpeechRecognitionUsageDescription: 'AIRI uses Apple Speech to transcribe voice interactions on this device', + NSCameraUsageDescription: 'AIRI requires camera access for vision understanding', + }, // For self-publishing, testing, and distribution after modified the code without access to // an Apple Developer account, comment and uncomment the following 4 lines. // Later on when you obtained one, you can set up the necessary certificates and provisioning diff --git a/apps/stage-tamagotchi/electron.vite.config.ts b/apps/stage-tamagotchi/electron.vite.config.ts index b37474c62..578a60979 100644 --- a/apps/stage-tamagotchi/electron.vite.config.ts +++ b/apps/stage-tamagotchi/electron.vite.config.ts @@ -28,6 +28,7 @@ export default defineConfig({ // them into ESM and causing issues in runtime. 'electron-click-drag-plugin', 'uiohook-napi', + '@xsai-apple-speech/transcription-native', ], }, }, diff --git a/apps/stage-tamagotchi/package.json b/apps/stage-tamagotchi/package.json index 9e814cf43..35892d9f2 100644 --- a/apps/stage-tamagotchi/package.json +++ b/apps/stage-tamagotchi/package.json @@ -81,6 +81,9 @@ "@vueuse/core": "catalog:", "@vueuse/motion": "catalog:", "@vueuse/shared": "catalog:", + "@xsai-apple-speech/transcription": "catalog:", + "@xsai-apple-speech/transcription-electron-plugin": "catalog:", + "@xsai-apple-speech/transcription-native": "catalog:", "@xsai-ext/providers": "catalog:", "@xsai-transformers/embed": "catalog:", "@xsai-transformers/transcription": "catalog:", @@ -143,6 +146,10 @@ "xsschema": "catalog:", "zod": "catalog:" }, + "optionalDependencies": { + "@xsai-apple-speech/transcription-native-darwin-arm64": "catalog:", + "@xsai-apple-speech/transcription-native-darwin-x64": "catalog:" + }, "devDependencies": { "@electron-toolkit/preload": "catalog:", "@electron-toolkit/tsconfig": "catalog:", diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 96fa455f3..47f9437e9 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -28,6 +28,7 @@ import { createGlobalAppConfig } from './configs/global' import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed } from './libs/bootkit/lifecycle' import { setElectronMainDirname } from './libs/electron/location' import { createI18n } from './libs/i18n' +import { setupAppleSpeechTranscriptionService } from './services/airi/apple-speech-transcription' import { setupServerChannel } from './services/airi/channel-server' import { setupGodotStageManager } from './services/airi/godot-stage' import { setupBuiltInServer } from './services/airi/http-server' @@ -165,6 +166,11 @@ app.whenReady().then(async () => { build: async () => setupGodotStageManager(), }) + const appleSpeechTranscription = injeca.provide('modules:apple-speech-transcription', { + dependsOn: { lifecycle }, + build: ({ dependsOn }) => setupAppleSpeechTranscriptionService(dependsOn), + }) + const mcpStdioManager = injeca.provide('modules:mcp-stdio-manager', { build: async () => setupMcpStdioManager(), }) @@ -226,7 +232,7 @@ app.whenReady().then(async () => { }) const mainWindow = injeca.provide('windows:main', { - dependsOn: { editorWindow, settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync, autoUpdater, serverChannel, godotStageManager, mcpStdioManager, i18n, onboardingWindowManager }, + dependsOn: { editorWindow, settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync, autoUpdater, serverChannel, godotStageManager, mcpStdioManager, i18n, onboardingWindowManager, appleSpeechTranscription }, build: async ({ dependsOn }) => setupMainWindow({ ...dependsOn, onWindowCreated: (window) => { diff --git a/apps/stage-tamagotchi/src/main/services/airi/apple-speech-transcription/index.ts b/apps/stage-tamagotchi/src/main/services/airi/apple-speech-transcription/index.ts new file mode 100644 index 000000000..336b157c9 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/apple-speech-transcription/index.ts @@ -0,0 +1,46 @@ +import type { Lifecycle } from 'injeca' + +import { createContext } from '@moeru/eventa/adapters/electron/main' +import { setupAppleSpeechTranscription } from '@xsai-apple-speech/transcription-electron-plugin/main' +import { ipcMain } from 'electron' +import { isMacOS } from 'std-env' + +/** + * Registers the app-wide Apple Speech transport and its native Provider. + * + * The Electron main process owns native work. Renderer Providers communicate + * with it through the Eventa handlers registered by the xsAI plugin. + * Non-macOS hosts return an inactive service without loading the native package. + * + * Call stack: + * + * setupAppleSpeechTranscriptionService + * -> {@link createContext} + * -> {@link setupAppleSpeechTranscription} + * -> {@link createAppleSpeechProvider} + */ +export async function setupAppleSpeechTranscriptionService(options: { lifecycle: Lifecycle }) { + if (!isMacOS) + return { dispose: () => Promise.resolve() } + + const { createAppleSpeechProvider } = await import('@xsai-apple-speech/transcription-native') + const eventa = createContext(ipcMain) + const setup = setupAppleSpeechTranscription({ + context: eventa.context, + provider: createAppleSpeechProvider(), + }) + let disposal: Promise | undefined + + const dispose = () => { + disposal ??= (async () => { + // Stop accepting native work before the transport cancels remaining invokes. + await setup.dispose() + eventa.dispose() + })() + return disposal + } + + options.lifecycle.appHooks.onStop(dispose) + + return { dispose } +} diff --git a/packages/audio/src/encoding/wav.test.ts b/packages/audio/src/encoding/wav.test.ts index 3417b3bbc..149bc89ff 100644 --- a/packages/audio/src/encoding/wav.test.ts +++ b/packages/audio/src/encoding/wav.test.ts @@ -1,6 +1,46 @@ import { describe, expect, it } from 'vitest' -import { toWav, toWavFromPCM16 } from './wav' +import { toFloat32FromPCM16, toPCM16FromFloat32, toWav, toWavFromPCM16 } from './wav' + +describe('pcm sample encoding', () => { + it('converts normalized Float32 samples to little-endian PCM16 bytes', () => { + const samples = new Float32Array([-2, -1, -0.5, 0, 0.5, 1, 2]) + const pcmBytes = toPCM16FromFloat32(samples) + const view = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength) + + expect(view.getInt16(0, true)).toBe(-32768) + expect(view.getInt16(2, true)).toBe(-32768) + expect(view.getInt16(4, true)).toBe(-16384) + expect(view.getInt16(6, true)).toBe(0) + expect(view.getInt16(8, true)).toBe(16383) + expect(view.getInt16(10, true)).toBe(32767) + expect(view.getInt16(12, true)).toBe(32767) + }) + + it('converts little-endian PCM16 bytes to normalized Float32 samples', () => { + const pcmBytes = new Uint8Array(10) + const view = new DataView(pcmBytes.buffer) + view.setInt16(0, -32768, true) + view.setInt16(2, -16384, true) + view.setInt16(4, 0, true) + view.setInt16(6, 16384, true) + view.setInt16(8, 32767, true) + + expect([...toFloat32FromPCM16(pcmBytes)]).toEqual([ + -1, + -0.5, + 0, + 0.5, + 32767 / 32768, + ]) + }) + + it('rejects incomplete PCM16 samples', () => { + expect(() => toFloat32FromPCM16(new Uint8Array([0]))).toThrow( + 'PCM16 input must contain complete 16-bit samples.', + ) + }) +}) describe('toWav', () => { it('converts Float32 samples to PCM16 bytes by default', () => { diff --git a/packages/audio/src/encoding/wav.ts b/packages/audio/src/encoding/wav.ts index 6e1567d56..344254ae5 100644 --- a/packages/audio/src/encoding/wav.ts +++ b/packages/audio/src/encoding/wav.ts @@ -32,6 +32,47 @@ function createWavBuffer(dataSize: number, sampleRate: number, channel: number): return arrayBuffer } +/** + * Converts normalized Float32 PCM samples to little-endian signed PCM16 bytes. + * Values outside the normalized range are clamped. + * + * @example + * toPCM16FromFloat32(new Float32Array([-1, 0, 1])) + * // => Uint8Array([0, 128, 0, 0, 255, 127]) + */ +export function toPCM16FromFloat32(samples: Float32Array): Uint8Array { + const output = new Uint8Array(samples.length * Int16Array.BYTES_PER_ELEMENT) + const dataView = new DataView(output.buffer) + + for (let i = 0; i < samples.length; i++) { + const sample = Math.max(-1, Math.min(1, samples[i])) + const value = sample < 0 ? sample * 0x8000 : sample * 0x7FFF + dataView.setInt16(i * Int16Array.BYTES_PER_ELEMENT, value, true) + } + + return output +} + +/** + * Converts little-endian signed PCM16 bytes to normalized Float32 PCM samples. + * + * @example + * toFloat32FromPCM16(new Uint8Array([0, 128, 0, 0, 255, 127])) + * // => Float32Array([-1, 0, 0.999969482421875]) + */ +export function toFloat32FromPCM16(pcmBytes: Uint8Array): Float32Array { + if (pcmBytes.byteLength % Int16Array.BYTES_PER_ELEMENT !== 0) + throw new TypeError('PCM16 input must contain complete 16-bit samples.') + + const dataView = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength) + const output = new Float32Array(pcmBytes.byteLength / Int16Array.BYTES_PER_ELEMENT) + + for (let i = 0; i < output.length; i++) + output[i] = dataView.getInt16(i * Int16Array.BYTES_PER_ELEMENT, true) / 0x8000 + + return output +} + /** * Encodes Float32 samples as a WAV file. * @@ -41,16 +82,7 @@ function createWavBuffer(dataSize: number, sampleRate: number, channel: number): */ export function toWav(buffer: ArrayBufferLike, sampleRate: number, channel = 1): ArrayBuffer { const samples = new Float32Array(buffer) - const arrayBuffer = createWavBuffer(samples.length * 2, sampleRate, channel) - const dataView = new DataView(arrayBuffer) - - for (let i = 0; i < samples.length; i++) { - const sample = Math.max(-1, Math.min(1, samples[i])) - const value = sample < 0 ? sample * 0x8000 : sample * 0x7FFF - dataView.setInt16(44 + i * 2, value, true) - } - - return arrayBuffer + return toWavFromPCM16(toPCM16FromFloat32(samples), sampleRate, channel) } /** diff --git a/packages/electron-screen-capture/package.json b/packages/electron-screen-capture/package.json index b606b7ff8..497c27033 100644 --- a/packages/electron-screen-capture/package.json +++ b/packages/electron-screen-capture/package.json @@ -51,7 +51,7 @@ }, "inlinedDependencies": { "@electron-toolkit/preload": "3.0.2", - "@moeru/eventa": "1.0.0-beta.15", + "@moeru/eventa": "1.0.0", "async-mutex": "0.5.0", "nanoid": [ "5.1.11", diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index e3883e14a..3923a04a7 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -941,6 +941,7 @@ pages: config: loading: Loading provider settings... load-error: Failed to load provider settings. + save-error: Failed to save provider settings. retry: Retry common: fields: @@ -1082,6 +1083,14 @@ pages: OpenAI, Azure Speech description: LLMs, speech providers, etc. provider: + apple-speech-transcription: + title: Apple Speech + description: On-device speech recognition on macOS 26 or later. No API key is required. + fields: + locale: + label: Locale + description: Use an exact Apple Speech locale, such as en-US or zh-CN. + placeholder: en-US app-local-audio-transcription: title: App (Local) description: https://github.com/moeru-ai/xsai-transformers diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index ab2353603..ec918bfb2 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -903,6 +903,7 @@ pages: config: loading: 正在加载服务来源设置…… load-error: 无法加载服务来源设置。 + save-error: 无法保存服务来源设置。 retry: 重试 common: fields: @@ -1031,6 +1032,14 @@ pages: 转录(语音转文本)模型服务来源,例如 Whisper.cpp, OpenAI, Azure Speech description: LLM,语音合成,语音识别服务来源等 provider: + apple-speech-transcription: + title: Apple 语音识别 + description: 使用 macOS 26 或更高版本的设备端语音识别,无需 API 密钥。 + fields: + locale: + label: 语言区域 + description: 使用 Apple 语音识别支持的完整语言区域代码,例如 en-US 或 zh-CN。 + placeholder: zh-CN app-local-audio-transcription: title: 应用内(本地) description: https://github.com/moeru-ai/xsai-transformers diff --git a/packages/stage-pages/package.json b/packages/stage-pages/package.json index f808eaa74..08b5f673e 100644 --- a/packages/stage-pages/package.json +++ b/packages/stage-pages/package.json @@ -24,6 +24,7 @@ "dependencies": { "@moeru/eventa": "catalog:", "@moeru/std": "catalog:", + "@proj-airi/audio": "workspace:^", "@proj-airi/ccc": "workspace:*", "@proj-airi/i18n": "workspace:*", "@proj-airi/server-sdk": "workspace:*", diff --git a/packages/stage-pages/src/pages/devtools/providers-transcription-realtime-aliyun-nls.vue b/packages/stage-pages/src/pages/devtools/providers-transcription-realtime-aliyun-nls.vue index b7c159c71..4bc8ba348 100644 --- a/packages/stage-pages/src/pages/devtools/providers-transcription-realtime-aliyun-nls.vue +++ b/packages/stage-pages/src/pages/devtools/providers-transcription-realtime-aliyun-nls.vue @@ -3,6 +3,7 @@ import type { ServerEvent, ServerEvents } from '@proj-airi/stage-ui/libs/provide import vadWorkletUrl from '@proj-airi/stage-ui/workers/vad/process.worklet?worker&url' +import { toPCM16FromFloat32 } from '@proj-airi/audio/encoding' import { errorMessageFromValue } from '@proj-airi/stage-shared' import { createAliyunNLSProvider } from '@proj-airi/stage-ui/libs/providers/providers/aliyun-nls' import { streamTranscription } from '@proj-airi/stage-ui/libs/providers/stream-transcription' @@ -83,15 +84,6 @@ function appendLog(message: string, level: 'info' | 'error' = 'info') { }) } -function float32ToInt16(buffer: Float32Array) { - const output = new Int16Array(buffer.length) - for (let i = 0; i < buffer.length; i++) { - const value = Math.max(-1, Math.min(1, buffer[i])) - output[i] = value < 0 ? value * 0x8000 : value * 0x7FFF - } - return output -} - function resetRecordingCounters() { audioChunkCount = 0 lastChunkLogAt = 0 @@ -116,7 +108,7 @@ async function initializeAudioGraph(stream: MediaStream) { if (!buffer || !controller) return - const pcm16 = float32ToInt16(buffer) + const pcm16 = toPCM16FromFloat32(buffer) controller.enqueue(pcm16.buffer.slice(0)) audioChunkCount += 1 diff --git a/packages/stage-pages/src/pages/settings/modules/hearing.vue b/packages/stage-pages/src/pages/settings/modules/hearing.vue index bd8d11da2..cb740b663 100644 --- a/packages/stage-pages/src/pages/settings/modules/hearing.vue +++ b/packages/stage-pages/src/pages/settings/modules/hearing.vue @@ -2,6 +2,7 @@ import { errorMessageFrom } from '@moeru/std' import { Alert, ErrorContainer, LevelMeter, RadioCardManySelect, RadioCardSimple, TestDummyMarker, ThresholdMeter, TimeSeriesChart } from '@proj-airi/stage-ui/components' import { useAnalytics, useAudioAnalyzer, useHearingPlaygroundSegments, useVoiceInputSession } from '@proj-airi/stage-ui/composables' +import { hearingProviderViewContextKey } from '@proj-airi/stage-ui/libs' import { useAudioContext } from '@proj-airi/stage-ui/stores/audio' import { CONFIDENCE_THRESHOLD_DISABLED, useHearingSpeechInputPipeline, useHearingStore } from '@proj-airi/stage-ui/stores/modules/hearing' import { useProviderConfigStore } from '@proj-airi/stage-ui/stores/providers/config' @@ -9,7 +10,7 @@ import { useProviderStore } from '@proj-airi/stage-ui/stores/providers/provider' import { useSettingsAudioDevice } from '@proj-airi/stage-ui/stores/settings' import { Button, FieldCheckbox, FieldCombobox, FieldInput, FieldRange } from '@proj-airi/ui' import { storeToRefs } from 'pinia' -import { computed, onMounted, onUnmounted, shallowRef, watch } from 'vue' +import { computed, defineAsyncComponent, onMounted, onUnmounted, provide, shallowRef, watch } from 'vue' import { useI18n } from 'vue-i18n' import HearingPlaygroundTranscripts from './components/hearing-playground-transcripts.vue' @@ -59,6 +60,15 @@ let volumeSpeechEndTimer: ReturnType | undefined const error = shallowRef('') const isMonitoring = shallowRef(false) +const activeProviderConfig = computed(() => { + if (!activeTranscriptionProvider.value) + return undefined + return providerStore.providers[activeTranscriptionProvider.value]?.config +}) +const activeProviderHearingView = computed(() => { + const loadView = providersStore.findProviderDefinition(activeTranscriptionProvider.value)?.views?.hearing + return loadView ? defineAsyncComponent(loadView) : undefined +}) const { current: currentTranscription, @@ -251,6 +261,50 @@ function updateCustomModelName(value: string | undefined) { activeTranscriptionModel.value = modelValue } +async function updateActiveProviderConfig(patch: Record) { + const providerId = activeTranscriptionProvider.value + if (!providerId) + throw new Error('No transcription Provider is active.') + + const shouldRestartMonitoring = isMonitoring.value + + try { + await providersStore.initializeProvider(providerId) + const provider = providerStore.getProvider(providerId) + if (!provider) + throw new Error('The transcription Provider configuration is unavailable.') + + const update = providerStore.updateProviderConfig( + providerId, + { ...provider.config, ...patch }, + 'configured', + ) + + if (shouldRestartMonitoring) { + isMonitoring.value = false + await stopAudioMonitoring(providerId) + } + + await update + await providersStore.disposeProviderInstance(providerId) + clearPlaygroundSegments() + + // The selected Provider can change while a remote configuration save is pending. + // Only restart the monitoring session for the Provider that requested the save. + if (shouldRestartMonitoring && activeTranscriptionProvider.value === providerId) + isMonitoring.value = await setupAudioMonitoring() + } + catch (cause) { + error.value = errorMessageFrom(cause) ?? t('settings.pages.providers.catalog.edit.config.save-error') + throw cause + } +} + +provide(hearingProviderViewContextKey, { + providerConfig: activeProviderConfig, + updateProviderConfig: updateActiveProviderConfig, +}) + // Sync OpenAI Compatible model from provider config function syncOpenAICompatibleSettings() { if (activeTranscriptionProvider.value !== 'openai-compatible-audio-transcription') @@ -417,6 +471,11 @@ onUnmounted(() => { + +
diff --git a/packages/stage-pages/src/pages/settings/providers/transcription/aliyun-nls-transcription.vue b/packages/stage-pages/src/pages/settings/providers/transcription/aliyun-nls-transcription.vue index b7c5e09d3..18aa34ca6 100644 --- a/packages/stage-pages/src/pages/settings/providers/transcription/aliyun-nls-transcription.vue +++ b/packages/stage-pages/src/pages/settings/providers/transcription/aliyun-nls-transcription.vue @@ -6,6 +6,7 @@ import type { TranscriptionProviderWithExtraOptions } from '@xsai-ext/providers/ import vadWorkletUrl from '@proj-airi/stage-ui/workers/vad/process.worklet?worker&url' +import { toPCM16FromFloat32 } from '@proj-airi/audio/encoding' import { errorMessageFromValue } from '@proj-airi/stage-shared' import { Alert, @@ -123,15 +124,6 @@ const { forceValid, } = useProviderValidation(providerId) -function float32ToInt16(buffer: Float32Array) { - const output = new Int16Array(buffer.length) - for (let i = 0; i < buffer.length; i++) { - const value = Math.max(-1, Math.min(1, buffer[i])) - output[i] = value < 0 ? value * 0x8000 : value * 0x7FFF - } - return output -} - async function initializeAudioGraph(stream: MediaStream) { const context = new AudioContext({ sampleRate: SAMPLE_RATE, @@ -146,7 +138,7 @@ async function initializeAudioGraph(stream: MediaStream) { if (!buffer || !controller) return - const pcm16 = float32ToInt16(buffer) + const pcm16 = toPCM16FromFloat32(buffer) controller.enqueue(pcm16.buffer.slice(0)) } diff --git a/packages/stage-pages/src/pages/v2/settings/providers/edit/[providerId]/index.vue b/packages/stage-pages/src/pages/v2/settings/providers/edit/[providerId]/index.vue index 89e4b1fde..5d7e7a4ce 100644 --- a/packages/stage-pages/src/pages/v2/settings/providers/edit/[providerId]/index.vue +++ b/packages/stage-pages/src/pages/v2/settings/providers/edit/[providerId]/index.vue @@ -526,6 +526,15 @@ function handleDeleteProvider() { :required="field.required" @update:model-value="setFieldValue(field.key, $event)" /> + > | undefined> + /** Saves a partial configuration as configured and refreshes the monitoring session. */ + updateProviderConfig: (patch: Record) => Promise +} + +export const hearingProviderViewContextKey: InjectionKey + = Symbol('hearing-provider-view-context') + +/** Returns the Hearing module APIs available to a registered Provider view. */ +export function useHearingProviderViewContext() { + const context = inject(hearingProviderViewContextKey) + if (!context) + throw new Error('The Provider view must be rendered inside the Hearing module.') + + return context +} diff --git a/packages/stage-ui/src/libs/providers/index.ts b/packages/stage-ui/src/libs/providers/index.ts index cd18b4215..a1c702098 100644 --- a/packages/stage-ui/src/libs/providers/index.ts +++ b/packages/stage-ui/src/libs/providers/index.ts @@ -1,4 +1,5 @@ export * from './attributes' +export * from './hearing-view' export * from './metadata' export * from './providers' export * from './types' diff --git a/packages/stage-ui/src/libs/providers/providers/apple-speech/hearing-settings.vue b/packages/stage-ui/src/libs/providers/providers/apple-speech/hearing-settings.vue new file mode 100644 index 000000000..5c2b19302 --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/apple-speech/hearing-settings.vue @@ -0,0 +1,110 @@ + + + diff --git a/packages/stage-ui/src/libs/providers/providers/apple-speech/index.test.ts b/packages/stage-ui/src/libs/providers/providers/apple-speech/index.test.ts new file mode 100644 index 000000000..47f841dff --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/apple-speech/index.test.ts @@ -0,0 +1,150 @@ +import type { AppleSpeechSessionOperations, StartStreamTranscriptionOptions, TranscriptionResult } from '@xsai-apple-speech/transcription' +import type { ZodObject } from 'zod' + +import { createStreamTranscriptionResult } from '@xsai-apple-speech/transcription' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { executeAppleSpeechStream, providerAppleSpeechTranscription } from '.' + +const mocks = vi.hoisted(() => ({ + dispose: vi.fn(), + getLocales: vi.fn(), +})) + +vi.mock('@moeru/eventa/adapters/electron/renderer', () => ({ + createContext: () => ({ context: {}, dispose: mocks.dispose }), +})) + +vi.mock('@proj-airi/stage-shared', () => ({ + isElectronWindow: () => true, + isStageTamagotchi: () => true, +})) + +vi.mock('@xsai-apple-speech/transcription-electron-plugin', () => ({ + createAppleSpeechProvider: () => ({ + getLocales: mocks.getLocales, + }), +})) + +describe('apple speech transcription provider', () => { + afterEach(() => { + vi.unstubAllGlobals() + vi.clearAllMocks() + }) + + it('loads labeled locale options with automatic transcriber selection', async () => { + vi.stubGlobal('window', { + electron: { ipcRenderer: {} }, + platform: 'darwin', + }) + mocks.getLocales.mockResolvedValue([ + { installed: false, locale: 'en-US' }, + { installed: true, locale: 'zh-CN' }, + ]) + + const schema = await providerAppleSpeechTranscription.createProviderConfig({ + t: input => input, + config: { locale: 'zh-CN' }, + }) + const localeMeta = (schema as ZodObject).shape.locale.meta() + + expect(mocks.getLocales).toHaveBeenCalledWith({ transcriber: 'automatic' }) + expect(localeMeta?.type).toBe('select') + expect(localeMeta?.options).toEqual([ + expect.objectContaining({ value: 'zh-CN' }), + expect.objectContaining({ value: 'en-US' }), + ]) + expect(localeMeta?.options).toEqual(expect.arrayContaining([ + expect.objectContaining({ label: expect.stringContaining('zh-CN') }), + expect.objectContaining({ label: expect.stringContaining('en-US') }), + ])) + expect(mocks.dispose).toHaveBeenCalledOnce() + }) + + it('converts PCM16 input and emits AIRI transcript snapshots', async () => { + const writtenSamples: Float32Array[] = [] + const finalResult: TranscriptionResult = { + locale: 'en-US', + results: [{ + range: { + durationMilliseconds: 400, + isFinal: true, + startMilliseconds: 100, + }, + text: 'Hello, AIRI.', + }], + text: 'Hello, AIRI.', + } + + const result = executeAppleSpeechStream({ + baseURL: new URL('apple-speech://transcription'), + fetch: globalThis.fetch, + inputAudioStream: new ReadableStream({ + start(controller) { + controller.enqueue(new Int16Array([-32768, -16384, 0, 16384, 32767]).buffer) + controller.close() + }, + }), + inputSampleRate: 16000, + model: 'apple-speech', + startStream: (streamOptions: StartStreamTranscriptionOptions) => createStreamTranscriptionResult({ + ...streamOptions, + locale: 'en-US', + async start(request): Promise { + return { + async dispose() {}, + async finish() { + return finalResult + }, + async write(samples) { + writtenSamples.push(samples.slice()) + await request.onPartial({ + locale: 'en-US', + range: { + durationMilliseconds: 300, + isFinal: false, + startMilliseconds: 100, + }, + text: 'Hello, Ari', + type: 'transcript.text.partial', + }) + }, + } + }, + }), + }) + + await expect(result.text).resolves.toBe('Hello, AIRI.') + expect(writtenSamples).toHaveLength(1) + expect(Array.from(writtenSamples[0] ?? [])).toEqual([ + -1, + -0.5, + 0, + 0.5, + 32767 / 32768, + ]) + + const events = [] + for await (const event of result.fullStream) + events.push(event) + + expect(events).toEqual([ + { + durationMilliseconds: 300, + isFinal: false, + locale: 'en-US', + startMilliseconds: 100, + text: 'Hello, Ari', + type: 'transcript.text.snapshot', + }, + { + durationMilliseconds: 400, + isFinal: true, + locale: 'en-US', + startMilliseconds: 100, + text: 'Hello, AIRI.', + type: 'transcript.text.snapshot', + }, + ]) + }) +}) diff --git a/packages/stage-ui/src/libs/providers/providers/apple-speech/index.ts b/packages/stage-ui/src/libs/providers/providers/apple-speech/index.ts new file mode 100644 index 000000000..887601b25 --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/apple-speech/index.ts @@ -0,0 +1,248 @@ +import type { + AppleSpeechTranscription, + TranscriptionEvent, + TranscriptionRange, +} from '@xsai-apple-speech/transcription' +import type { TranscriptionProviderWithExtraOptions } from '@xsai-ext/providers/utils' + +import type { AIRIStreamTranscriptionResult, StreamTranscriptionOptions } from '../../stream-transcription' +import type { ProviderConfigContext } from '../../types' +import type { AppleSpeechConfig } from './provider' + +import { createContext } from '@moeru/eventa/adapters/electron/renderer' +import { toFloat32FromPCM16 } from '@proj-airi/audio/encoding' +import { isElectronWindow, isStageTamagotchi } from '@proj-airi/stage-shared' +import { streamTranscription as streamAppleSpeechTranscription } from '@xsai-apple-speech/transcription' +import { createAppleSpeechProvider as createElectronAppleSpeechProvider } from '@xsai-apple-speech/transcription-electron-plugin' + +import { defineProvider } from '../registry' +import { appleSpeechConfigSchema, listAppleSpeechLocaleOptions } from './provider' + +export type { AppleSpeechConfig } from './provider' +export { listAppleSpeechLocaleOptions } from './provider' + +export const APPLE_SPEECH_TRANSCRIPTION_PROVIDER_ID = 'apple-speech-transcription' + +/** Request options applied by AIRI before Apple Speech creates a batch or live session. */ +export interface AppleSpeechProviderOptions { + /** Cancels native preparation and active transcription work. */ + abortSignal?: AbortSignal + /** PCM input rate in hertz. @default 16000 */ + inputSampleRate?: number + /** Exact Apple Speech locale for this request. The Provider configuration is the default. */ + locale?: string +} + +type AIRIAppleSpeechProvider = TranscriptionProviderWithExtraOptions<'apple-speech', AppleSpeechProviderOptions> & { + dispose: () => void +} + +type AppleSpeechStreamOptions = StreamTranscriptionOptions & AppleSpeechTranscription & { + inputSampleRate?: number +} + +async function createAppleSpeechConfigSchema(context: ProviderConfigContext) { + const { t } = context + const localeOptions = await listAppleSpeechLocaleOptions(context) + return appleSpeechConfigSchema.extend({ + locale: appleSpeechConfigSchema.shape.locale.meta({ + type: 'select', + labelLocalized: t('settings.pages.providers.provider.apple-speech-transcription.fields.locale.label'), + descriptionLocalized: t('settings.pages.providers.provider.apple-speech-transcription.fields.locale.description'), + placeholderLocalized: t('settings.pages.providers.provider.apple-speech-transcription.fields.locale.placeholder'), + options: localeOptions, + }), + }) +} + +function createRendererAppleSpeechProvider(config: AppleSpeechConfig): AIRIAppleSpeechProvider { + if (typeof window === 'undefined' || !isElectronWindow(window)) + throw new Error('Apple Speech transcription requires the Electron desktop app.') + + const eventa = createContext(window.electron.ipcRenderer) + const provider = createElectronAppleSpeechProvider({ context: eventa.context }) + const configuredLocale = config.locale?.trim() || 'en-US' + + return { + transcription(_model, requestOptions = {}) { + const locale = requestOptions.locale?.trim() || configuredLocale + return { + ...provider.transcription({ locale, transcriber: 'automatic' }), + ...requestOptions, + inputSampleRate: requestOptions.inputSampleRate ?? 16000, + } + }, + dispose() { + eventa.dispose() + }, + } +} + +async function isAppleSpeechAvailable() { + if (!isStageTamagotchi() || typeof window === 'undefined' || !isElectronWindow(window) || window.platform !== 'darwin') + return false + + const { context, dispose } = createContext(window.electron.ipcRenderer) + + try { + const provider = createElectronAppleSpeechProvider({ context }) + const availability = await provider.isAvailable() + return availability.available + } + catch { + return false + } + finally { + dispose() + } +} + +function isAppleSpeechStreamRequest( + options: StreamTranscriptionOptions, +): options is AppleSpeechStreamOptions { + return options.baseURL instanceof URL + && typeof options.fetch === 'function' + && 'model' in options + && typeof options.model === 'string' + && 'startStream' in options + && typeof options.startStream === 'function' +} + +/** + * Normalizes one PCM chunk to a byte view without copying its sample data. + * + * @example + * audioChunkBytes(new Int16Array([0, 1])) + * // => Uint8Array(4) + */ +function audioChunkBytes(chunk: ArrayBuffer | ArrayBufferView) { + return ArrayBuffer.isView(chunk) + ? new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength) + : new Uint8Array(chunk) +} + +function combinedRange(event: TranscriptionEvent): TranscriptionRange { + if (event.type === 'transcript.text.partial') + return event.range + + const ranges = event.results?.map(result => result.range) ?? [] + if (ranges.length === 0) + return { durationMilliseconds: 0, isFinal: true, startMilliseconds: 0 } + + const startMilliseconds = Math.min(...ranges.map(range => range.startMilliseconds)) + const endMilliseconds = Math.max(...ranges.map(range => range.startMilliseconds + range.durationMilliseconds)) + return { + durationMilliseconds: endMilliseconds - startMilliseconds, + isFinal: true, + startMilliseconds, + } +} + +/** + * Normalizes one Apple replacement event to the snapshot contract used by Hearing. + * + * @example + * appleEventSnapshot({ type: 'transcript.text.partial', text: 'Hello', locale: 'en-US', range }) + * // => { type: 'transcript.text.snapshot', text: 'Hello', isFinal: false, ... } + */ +function appleEventSnapshot(event: TranscriptionEvent) { + const range = combinedRange(event) + return { + durationMilliseconds: range.durationMilliseconds, + isFinal: event.type === 'transcript.text.done', + locale: event.locale, + startMilliseconds: range.startMilliseconds, + text: event.text, + type: 'transcript.text.snapshot' as const, + } +} + +async function pumpPcm16Input( + input: NonNullable, + writer: WritableStreamDefaultWriter, +) { + const reader = input.getReader() + try { + for (;;) { + const { done, value } = await reader.read() + if (done) + break + + await writer.write(toFloat32FromPCM16(audioChunkBytes(value))) + } + await writer.close() + } + catch (error) { + await writer.abort(error).catch(() => {}) + throw error + } + finally { + reader.releaseLock() + } +} + +/** + * Adapts AIRI's mono PCM16 VAD stream to Apple Speech live transcription. + * + * The Provider boundary converts each audio chunk and maps Apple replacement + * events to AIRI transcript snapshots. + */ +export function executeAppleSpeechStream(options: AppleSpeechStreamOptions): AIRIStreamTranscriptionResult +export function executeAppleSpeechStream(options: StreamTranscriptionOptions): AIRIStreamTranscriptionResult +export function executeAppleSpeechStream(options: StreamTranscriptionOptions): AIRIStreamTranscriptionResult { + if (!options.inputAudioStream) + throw new TypeError('Apple Speech live transcription requires an audio stream.') + if (!isAppleSpeechStreamRequest(options)) + throw new TypeError('Apple Speech live transcription requires a native stream request.') + + const inputSampleRate = options.inputSampleRate ?? 16000 + const live = streamAppleSpeechTranscription({ + ...options, + inputSampleRate, + }) + const inputPump = pumpPcm16Input(options.inputAudioStream, live.input.getWriter()) + void inputPump.catch(() => {}) + + return { + fullStream: live.fullStream.pipeThrough(new TransformStream({ + transform(event, controller) { + controller.enqueue(appleEventSnapshot(event)) + }, + })), + text: Promise.all([live.text, inputPump]).then(([text]) => text), + textStream: live.partialStream, + } +} + +export const providerAppleSpeechTranscription = defineProvider({ + id: APPLE_SPEECH_TRANSCRIPTION_PROVIDER_ID, + name: 'Apple Speech', + nameLocalize: ({ t }) => t('settings.pages.providers.provider.apple-speech-transcription.title'), + description: 'On-device speech recognition on macOS 26 or later. No API key is required.', + descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.apple-speech-transcription.description'), + tasks: ['speech-to-text', 'automatic-speech-recognition', 'asr', 'stt', 'streaming-transcription'], + requiresCredentials: false, + isAvailableBy: isAppleSpeechAvailable, + views: { + hearing: () => import('./hearing-settings.vue'), + }, + capabilities: { + transcription: { + protocol: 'native', + generateOutput: true, + streamOutput: true, + streamInput: true, + }, + }, + createProviderConfig: createAppleSpeechConfigSchema, + createProvider: createRendererAppleSpeechProvider, + validationRequiredWhen: () => false, + extraMethods: { + listModels: async () => [{ + id: 'apple-speech', + name: 'Apple Speech', + provider: APPLE_SPEECH_TRANSCRIPTION_PROVIDER_ID, + description: 'On-device Apple Speech transcription', + }], + }, +}) diff --git a/packages/stage-ui/src/libs/providers/providers/apple-speech/provider.ts b/packages/stage-ui/src/libs/providers/providers/apple-speech/provider.ts new file mode 100644 index 000000000..7c2cf44f4 --- /dev/null +++ b/packages/stage-ui/src/libs/providers/providers/apple-speech/provider.ts @@ -0,0 +1,59 @@ +import type { AppleSpeechLocale } from '@xsai-apple-speech/transcription' + +import type { ProviderConfigContext } from '../../types' + +import { createContext } from '@moeru/eventa/adapters/electron/renderer' +import { isElectronWindow } from '@proj-airi/stage-shared' +import { createAppleSpeechProvider } from '@xsai-apple-speech/transcription-electron-plugin' +import { z } from 'zod' + +export const appleSpeechConfigSchema = z.object({ + locale: z.string().trim().min(1).default('en-US'), +}) + +/** Serializable configuration for the Apple Speech Provider. */ +export type AppleSpeechConfig = z.input + +function localeLabel(locale: string) { + try { + const displayName = new Intl.DisplayNames([locale], { type: 'language' }).of(locale) + if (displayName && displayName !== locale) + return `${displayName} (${locale})` + } + catch { + // Apple owns this locale inventory. Keep its canonical identifier visible + // if the current JavaScript runtime cannot format a newer language tag. + } + return locale +} + +function sortLocales(locales: AppleSpeechLocale[]) { + return [...locales].sort((left, right) => { + if (left.installed !== right.installed) + return left.installed ? -1 : 1 + return left.locale.localeCompare(right.locale) + }) +} + +/** Lists labeled native locales available through Apple's automatic transcriber selection. */ +export async function listAppleSpeechLocaleOptions(context: ProviderConfigContext) { + if (context.config === undefined) + return [] + if (typeof window === 'undefined' || !isElectronWindow(window) || window.platform !== 'darwin') + return [] + + context.abortSignal?.throwIfAborted() + const eventa = createContext(window.electron.ipcRenderer) + try { + const provider = createAppleSpeechProvider({ context: eventa.context }) + const locales = await provider.getLocales({ transcriber: 'automatic' }) + context.abortSignal?.throwIfAborted() + return sortLocales(locales).map(({ locale }) => ({ + label: localeLabel(locale), + value: locale, + })) + } + finally { + eventa.dispose() + } +} diff --git a/packages/stage-ui/src/libs/providers/providers/index.ts b/packages/stage-ui/src/libs/providers/providers/index.ts index a30a8728e..21cde727b 100644 --- a/packages/stage-ui/src/libs/providers/providers/index.ts +++ b/packages/stage-ui/src/libs/providers/providers/index.ts @@ -1,4 +1,5 @@ import './amazon-bedrock' +import './apple-speech' import './openai' import './openai-audio' import './aihubmix' diff --git a/packages/stage-ui/src/libs/providers/types.ts b/packages/stage-ui/src/libs/providers/types.ts index fb8bb76a3..7f667a0a2 100644 --- a/packages/stage-ui/src/libs/providers/types.ts +++ b/packages/stage-ui/src/libs/providers/types.ts @@ -12,6 +12,7 @@ import type { } from '@xsai-ext/providers/utils' import type { ProgressInfo } from '@xsai-transformers/shared/types' import type { MaybePromise } from 'clustr' +import type { Component } from 'vue' import type { ComposerTranslation } from 'vue-i18n' import type { $ZodType } from 'zod/v4/core' @@ -214,6 +215,12 @@ export interface ProviderDefinition { */ configuredBy?: ProviderConfiguredBy + /** Provider-owned controls for module settings pages. */ + views?: { + /** Lazily loads additional controls shown for this Provider in the Hearing module. */ + hearing?: () => Promise<{ default: Component }> + } + /** Builds the validation schema and its UI metadata for the current draft. */ createProviderConfig: (contextOptions: ProviderConfigContext) => MaybePromise<$ZodType> onboardingFields?: (ctx: { t: ComposerTranslation }) => MaybePromise @@ -235,7 +242,7 @@ export interface ProviderDefinition { reasoning?: ChatReasoningCapability } transcription?: { - protocol: 'websocket' | 'http' + protocol: 'websocket' | 'http' | 'native' generateOutput: boolean streamOutput: boolean streamInput: boolean diff --git a/packages/stage-ui/src/libs/workers/worker.ts b/packages/stage-ui/src/libs/workers/worker.ts index 7c1fdae89..16c31bae6 100644 --- a/packages/stage-ui/src/libs/workers/worker.ts +++ b/packages/stage-ui/src/libs/workers/worker.ts @@ -31,6 +31,7 @@ import { TextStreamer, WhisperForConditionalGeneration, } from '@huggingface/transformers' +import { toFloat32FromPCM16 } from '@proj-airi/audio/encoding' import { errorMessageFromValue } from '@proj-airi/stage-shared' import { MODEL_IDS, MODEL_NAMES } from '../inference/constants' @@ -162,13 +163,7 @@ async function base64ToFeatures(base64Audio: string): Promise { bytes[i] = binaryString.charCodeAt(i) } - const samples = new Int16Array(bytes.buffer.slice(44)) - const audio = new Float32Array(samples.length) - for (let i = 0; i < samples.length; i++) { - audio[i] = samples[i] / 32768.0 - } - - return audio + return toFloat32FromPCM16(bytes.subarray(44)) } // --------------------------------------------------------------------------- diff --git a/packages/stage-ui/src/stores/modules/hearing.ts b/packages/stage-ui/src/stores/modules/hearing.ts index 0a2b8193a..23e52dc9e 100644 --- a/packages/stage-ui/src/stores/modules/hearing.ts +++ b/packages/stage-ui/src/stores/modules/hearing.ts @@ -8,6 +8,7 @@ import type { AIRIStreamTranscriptionResult } from '../../libs/providers/stream- import type { StreamingTranscriptionCallbacks, StreamingTranscriptionConsumer } from './streaming-transcription-consumers' import { errorMessageFrom, tryCatch } from '@moeru/std' +import { toPCM16FromFloat32 } from '@proj-airi/audio/encoding' import { errorMessageFromValue, IOAttributes, IOEvents, IOSpanNames, IOSubsystems } from '@proj-airi/stage-shared' import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables' import { refManualReset } from '@vueuse/core' @@ -21,6 +22,7 @@ import { useAnalytics } from '../../composables/use-analytics' import { activeTurnSpan, startSpan } from '../../composables/use-io-tracer' import { createVadStreamingSession } from '../../libs/audio/vad-streaming-session' import { OFFICIAL_TRANSCRIPTION_PROVIDER_ID } from '../../libs/providers' +import { APPLE_SPEECH_TRANSCRIPTION_PROVIDER_ID, executeAppleSpeechStream } from '../../libs/providers/providers/apple-speech' import { streamWebSpeechAPITranscription } from '../../libs/providers/providers/browser-web-speech-api' import { streamTranscription } from '../../libs/providers/stream-transcription' import { useVAD } from '../ai/models/vad' @@ -235,6 +237,7 @@ export function resolveTranscriptionFileName(file: File, explicitFileName?: stri const STREAM_TRANSCRIPTION_EXECUTORS: Record = { 'aliyun-nls-transcription': streamTranscription, + [APPLE_SPEECH_TRANSCRIPTION_PROVIDER_ID]: executeAppleSpeechStream, [OFFICIAL_TRANSCRIPTION_PROVIDER_ID]: streamTranscription, // Web Speech API is handled specially in transcribeForMediaStream since it works directly with MediaStream } @@ -796,21 +799,11 @@ export const useHearingSpeechInputPipeline = defineStore('modules:hearing:speech return await stopRealtimeTranscription(abort, disposeProviderId) } - function float32ToInt16(buffer: Float32Array) { - const output = new Int16Array(buffer.length) - for (let i = 0; i < buffer.length; i++) { - const value = Math.max(-1, Math.min(1, buffer[i])) - output[i] = value < 0 ? value * 0x8000 : value * 0x7FFF - } - - return output - } - function enqueueVadAudio(segment: NonNullable['activeSegment'], buffer: Float32Array) { if (!segment) return - const pcm16 = float32ToInt16(buffer) + const pcm16 = toPCM16FromFloat32(buffer) const chunk = pcm16.buffer.slice(0) if (segment.audioStreamController) { segment.audioStreamController.enqueue(chunk) diff --git a/packages/testing-audio/cases/apple-speech/case.audio.electron.test.ts b/packages/testing-audio/cases/apple-speech/case.audio.electron.test.ts new file mode 100644 index 000000000..b8acc8dda --- /dev/null +++ b/packages/testing-audio/cases/apple-speech/case.audio.electron.test.ts @@ -0,0 +1,96 @@ +import type { Page } from 'playwright' + +import { describe, expect, it } from '../../src' +import { configureModuleHearing, configureOnboarding } from '../shared/configurations' +import { enableHearingPlaygroundMicrophone, readHearingPlaygroundTranscriptions } from '../shared/interactions' +import { appleSpeechAsr } from '../shared/providers' + +// ROOT CAUSE: +// +// Chromium starts the non-looping fake microphone as soon as getUserMedia opens. +// Hearing requests microphone permission before the test starts monitoring, so a +// short fixture can finish before the Provider receives a speech segment. This +// fixture keeps 20 seconds of leading silence before the native transcription. +const input = new URL('../long-leading-silence/input.test.wav', import.meta.url) +const preflight = [ + configureOnboarding(() => ({ completed: true })), + configureModuleHearing(async (context) => { + const isMacOS = await context.runtime.runtimePage.evaluate(() => 'platform' in window && window.platform === 'darwin') + context.skip(!isMacOS, 'Apple Speech requires macOS 26 or later.') + if (!isMacOS) + return undefined + + return { + provider: appleSpeechAsr({ locale: 'en-US' }), + } + }), +] + +async function waitForStoredLocale(page: Page, locale: string) { + await page.waitForFunction((expectedLocale) => { + const stored = localStorage.getItem('settings/providers/configured') + if (!stored) + return false + const providers = JSON.parse(stored) as Record + return providers['apple-speech-transcription']?.config?.locale === expectedLocale + }, locale) +} + +describe('Apple Speech audio input', () => { + it('configures the native locale and transcribes through the Electron Provider', { input, preflight }, async ({ audio }) => { + const page = audio.runtimePage + audio.activatePage(page) + await page.evaluate(() => { + window.location.hash = '/settings/modules/hearing' + }) + await page.waitForURL(/#\/settings\/modules\/hearing/) + await page.getByTestId('hearing-playground-monitor-toggle').waitFor({ state: 'visible', timeout: 60_000 }) + const localeCombobox = page.getByTestId('apple-speech-locale').getByRole('combobox') + try { + await localeCombobox.waitFor({ state: 'visible', timeout: 10_000 }) + } + catch (error) { + const diagnostics = await page.evaluate(() => ({ + activeProvider: localStorage.getItem('settings/hearing/active-provider'), + configuredProviders: localStorage.getItem('settings/providers/configured'), + localeFieldCount: document.querySelectorAll('[data-testid="apple-speech-locale"]').length, + localeTextVisible: document.body.textContent?.includes('Locale') ?? false, + })) + throw new Error(`Apple Speech locale field is unavailable: ${JSON.stringify(diagnostics)}`, { cause: error }) + } + await localeCombobox.click() + const localeOptions = await page.getByRole('option').allTextContents() + expect(localeOptions.some(option => option.includes('en-US'))).toBe(true) + + const zhCNOption = page.getByRole('option').filter({ hasText: 'zh-CN' }).first() + await zhCNOption.click() + await waitForStoredLocale(page, 'zh-CN') + + await localeCombobox.click() + const enUSOption = page.getByRole('option').filter({ hasText: 'en-US' }).first() + await enUSOption.click() + await waitForStoredLocale(page, 'en-US') + + await enableHearingPlaygroundMicrophone(page) + try { + await readHearingPlaygroundTranscriptions(page, 1) + } + catch (error) { + const diagnostics = await page.evaluate(() => ({ + activeModel: localStorage.getItem('settings/hearing/active-model'), + activeProvider: localStorage.getItem('settings/hearing/active-provider'), + configuredProviders: localStorage.getItem('settings/providers/configured'), + piniaActionEvents: window.__airiAudioInputE2E?.piniaActionEvents ?? [], + probeInstalled: Boolean(window.__airiAudioInputE2E), + streamingTranscriptionReady: window.__airiAudioInputE2E?.streamingTranscriptionReady ?? false, + streamingTranscriptionUpdates: window.__airiAudioInputE2E?.streamingTranscriptionUpdates ?? [], + vadReady: window.__airiAudioInputE2E?.vadReady ?? false, + })) + throw new Error(`Apple Speech did not produce a transcript: ${JSON.stringify(diagnostics)}`, { cause: error }) + } + + await expect(audio).toHaveTranscriptions([ + ['Just let go.'], + ], { match: 'contains' }) + }) +}) diff --git a/packages/testing-audio/cases/shared/providers/apple-speech.ts b/packages/testing-audio/cases/shared/providers/apple-speech.ts new file mode 100644 index 000000000..9c4357cac --- /dev/null +++ b/packages/testing-audio/cases/shared/providers/apple-speech.ts @@ -0,0 +1,19 @@ +import type { ProviderConfiguration } from '../configurations/provider' + +/** Options for the Apple Speech Provider used by an Electron audio case. */ +export interface AppleSpeechAsrOptions { + /** @default 'en-US' */ + locale?: string +} + +/** Creates the macOS Apple Speech Provider configuration for one Electron case. */ +export function appleSpeechAsr(options: AppleSpeechAsrOptions = {}): ProviderConfiguration { + return { + id: 'apple-speech-transcription', + definitionId: 'apple-speech-transcription', + model: 'apple-speech', + config: { + locale: options.locale ?? 'en-US', + }, + } +} diff --git a/packages/testing-audio/cases/shared/providers/index.ts b/packages/testing-audio/cases/shared/providers/index.ts index 99a7d1ff0..1a2e5e1b3 100644 --- a/packages/testing-audio/cases/shared/providers/index.ts +++ b/packages/testing-audio/cases/shared/providers/index.ts @@ -1,4 +1,6 @@ export { aliyunNlsAsr } from './aliyun-nls' export type { AliyunNlsAsrOptions } from './aliyun-nls' +export { appleSpeechAsr } from './apple-speech' +export type { AppleSpeechAsrOptions } from './apple-speech' export { openaiAsr, openaiLlm, openaiTts } from './openai' export type { OpenAIProviderOptions, OpenAISpeechProviderOptions } from './openai' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6ece547f..2dc3da10c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,8 +244,8 @@ catalogs: specifier: 0.1.0-beta.19 version: 0.1.0-beta.19 '@moeru/eventa': - specifier: 1.0.0-beta.15 - version: 1.0.0-beta.15 + specifier: 1.0.0 + version: 1.0.0 '@moeru/std': specifier: 0.1.0-beta.17 version: 0.1.0-beta.17 @@ -549,6 +549,21 @@ catalogs: '@wxt-dev/module-vue': specifier: ^1.0.3 version: 1.0.3 + '@xsai-apple-speech/transcription': + specifier: 0.1.3 + version: 0.1.3 + '@xsai-apple-speech/transcription-electron-plugin': + specifier: 0.1.3 + version: 0.1.3 + '@xsai-apple-speech/transcription-native': + specifier: 0.1.3 + version: 0.1.3 + '@xsai-apple-speech/transcription-native-darwin-arm64': + specifier: 0.1.3 + version: 0.1.3 + '@xsai-apple-speech/transcription-native-darwin-x64': + specifier: 0.1.3 + version: 0.1.3 '@xsai-ext/providers': specifier: 0.5.0-beta.8 version: 0.5.0-beta.8 @@ -744,9 +759,6 @@ catalogs: histoire: specifier: 1.0.0-beta.1 version: 1.0.0-beta.1 - hono: - specifier: 4.11.3 - version: 4.11.3 hono-rate-limiter: specifier: ^0.5.3 version: 0.5.3 @@ -1203,6 +1215,7 @@ catalogs: overrides: array-flatten: npm:@nolyfill/array-flatten@^1.0.44 axios: npm:feaxios@^0.0.23 + hono: 4.13.4 is-core-module: npm:@nolyfill/is-core-module@^1.0.39 isarray: npm:@nolyfill/isarray@^1.0.44 onnxruntime-web: npm:onnxruntime-web@^1.24.3 @@ -1465,7 +1478,7 @@ importers: version: 3.8.1 '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -1871,7 +1884,7 @@ importers: version: 11.3.2 '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.12.2)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -1953,6 +1966,15 @@ importers: '@vueuse/shared': specifier: 'catalog:' version: 14.2.1(vue@3.5.32(typescript@5.9.3)) + '@xsai-apple-speech/transcription': + specifier: 'catalog:' + version: 0.1.3(@xsai/generate-transcription@0.5.0-beta.8) + '@xsai-apple-speech/transcription-electron-plugin': + specifier: 'catalog:' + version: 0.1.3(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0) + '@xsai-apple-speech/transcription-native': + specifier: 'catalog:' + version: 0.1.3(@xsai/generate-transcription@0.5.0-beta.8) '@xsai-ext/providers': specifier: 'catalog:' version: 0.5.0-beta.8 @@ -2335,6 +2357,13 @@ importers: yauzl: specifier: 'catalog:' version: 3.3.0 + optionalDependencies: + '@xsai-apple-speech/transcription-native-darwin-arm64': + specifier: 'catalog:' + version: 0.1.3 + '@xsai-apple-speech/transcription-native-darwin-x64': + specifier: 'catalog:' + version: 0.1.3 apps/stage-web: dependencies: @@ -2352,7 +2381,7 @@ importers: version: 3.8.1 '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -2498,8 +2527,8 @@ importers: specifier: 'catalog:' version: 1.0.7 hono: - specifier: 'catalog:' - version: 4.11.3 + specifier: 4.13.4 + version: 4.13.4 html2canvas: specifier: 'catalog:' version: 1.4.1 @@ -2740,7 +2769,7 @@ importers: version: 3.8.1 '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -3477,7 +3506,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.12.2)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0) crossws: specifier: 'catalog:' version: 0.4.5(srvx@0.11.22) @@ -3613,7 +3642,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@40.8.5)(h3@2.0.1-rc.25)(web-worker@1.5.0) + version: 1.0.0(electron@40.8.5)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) builder-util-runtime: specifier: 'catalog:' version: 9.5.1 @@ -3641,7 +3670,7 @@ importers: version: 3.0.2(electron@41.2.1) '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -3659,7 +3688,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@40.8.5)(h3@2.0.1-rc.25)(web-worker@1.5.0) + version: 1.0.0(electron@40.8.5)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -3766,7 +3795,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -3778,7 +3807,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@xsai/shared-chat': specifier: 'catalog:' version: 0.5.0-beta.8 @@ -3787,7 +3816,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -3809,7 +3838,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@proj-airi/plugin-sdk': specifier: workspace:* version: link:../plugin-sdk @@ -3951,7 +3980,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -4066,10 +4095,13 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 + '@proj-airi/audio': + specifier: workspace:^ + version: link:../audio '@proj-airi/ccc': specifier: workspace:* version: link:../ccc @@ -4215,7 +4247,7 @@ importers: version: 3.0.2(electron@41.2.1) '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@nekopaw/tempora': specifier: 'catalog:' version: 0.4.0-alpha.1 @@ -4242,7 +4274,7 @@ importers: version: 3.8.1 '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -4339,6 +4371,12 @@ importers: '@vueuse/shared': specifier: 'catalog:' version: 14.2.1(vue@3.5.32(typescript@5.9.3)) + '@xsai-apple-speech/transcription': + specifier: 'catalog:' + version: 0.1.3(@xsai/generate-transcription@0.5.0-beta.8) + '@xsai-apple-speech/transcription-electron-plugin': + specifier: 'catalog:' + version: 0.1.3(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0) '@xsai-ext/providers': specifier: 'catalog:' version: 0.5.0-beta.8 @@ -4415,8 +4453,8 @@ importers: specifier: 'catalog:' version: 1.0.7 hono: - specifier: 'catalog:' - version: 4.11.3 + specifier: 4.13.4 + version: 4.13.4 html2canvas: specifier: 'catalog:' version: 1.4.1 @@ -4946,7 +4984,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@pixiv/three-vrm': specifier: 'catalog:' version: 3.5.2(@types/three@0.184.0)(three@0.184.0) @@ -5225,7 +5263,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -5322,7 +5360,7 @@ importers: version: 1.2.4 '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -5367,13 +5405,13 @@ importers: version: 1.2.11 '@hono/node-server': specifier: 'catalog:' - version: 1.19.14(hono@4.11.3) + version: 1.19.14(hono@4.13.4) '@hono/node-ws': specifier: 'catalog:' - version: 1.3.0(@hono/node-server@1.19.14(hono@4.11.3))(bufferutil@4.1.0)(hono@4.11.3)(utf-8-validate@5.0.10) + version: 1.3.0(@hono/node-server@1.19.14(hono@4.13.4))(bufferutil@4.1.0)(hono@4.13.4)(utf-8-validate@5.0.10) '@hono/otel': specifier: 'catalog:' - version: 1.1.2(hono@4.11.3) + version: 1.1.2(hono@4.13.4) '@langfuse/otel': specifier: 'catalog:' version: 5.4.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.0(@opentelemetry/api@1.9.1))(@opentelemetry/exporter-trace-otlp-http@0.215.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.0(@opentelemetry/api@1.9.1)) @@ -5382,7 +5420,7 @@ importers: version: 5.4.0(@opentelemetry/api@1.9.1) '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -5453,11 +5491,11 @@ importers: specifier: 'catalog:' version: 1.50.0 hono: - specifier: 'catalog:' - version: 4.11.3 + specifier: 4.13.4 + version: 4.13.4 hono-rate-limiter: specifier: 'catalog:' - version: 0.5.3(hono@4.11.3)(unstorage@1.17.5(aws4fetch@1.0.20)(idb-keyval@6.2.2)(ioredis@5.10.1)) + version: 0.5.3(hono@4.13.4)(unstorage@1.17.5(aws4fetch@1.0.20)(idb-keyval@6.2.2)(ioredis@5.10.1)) injeca: specifier: 'catalog:' version: 0.2.0(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@5.1.11) @@ -5527,7 +5565,7 @@ importers: version: 1.2.11 '@hono/node-server': specifier: 'catalog:' - version: 1.19.14(hono@4.11.3) + version: 1.19.14(hono@4.13.4) '@moeru/std': specifier: 'catalog:' version: 0.1.0-beta.17 @@ -5589,11 +5627,11 @@ importers: specifier: 'catalog:' version: 0.45.2(@electric-sql/pglite@0.4.4)(@opentelemetry/api@1.9.1)(@prisma/client@5.22.0)(@types/pg@8.20.0)(better-sqlite3@12.5.0)(kysely@0.29.4)(pg@8.20.0)(postgres@3.4.9) hono: - specifier: 'catalog:' - version: 4.11.3 + specifier: 4.13.4 + version: 4.13.4 hono-rate-limiter: specifier: 'catalog:' - version: 0.5.3(hono@4.11.3)(unstorage@1.17.5(aws4fetch@1.0.20)(idb-keyval@6.2.2)(ioredis@5.10.1)) + version: 0.5.3(hono@4.13.4)(unstorage@1.17.5(aws4fetch@1.0.20)(idb-keyval@6.2.2)(ioredis@5.10.1)) injeca: specifier: 'catalog:' version: 0.2.0(@guiiai/logg@1.2.11)(error-stack-parser@2.1.4)(nanoid@6.0.1) @@ -5648,7 +5686,7 @@ importers: dependencies: '@moeru/eventa': specifier: 'catalog:' - version: 1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0) + version: 1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0) valibot: specifier: 'catalog:' version: 1.4.2(typescript@5.9.3) @@ -7674,7 +7712,7 @@ packages: resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} peerDependencies: - hono: ^4 + hono: 4.13.4 '@hono/node-ws@1.3.0': resolution: {integrity: sha512-ju25YbbvLuXdqBCmLZLqnNYu1nbHIQjoyUqA8ApZOeL1k4skuiTcw5SW77/5SUYo2Xi2NVBJoVlfQurnKEp03Q==} @@ -7682,12 +7720,12 @@ packages: deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: '@hono/node-server': ^1.19.2 - hono: ^4.6.0 + hono: 4.13.4 '@hono/otel@1.1.2': resolution: {integrity: sha512-UaBMKPGaQTj4sjvpGqQ+57eolTwMI2znzxV/QBUF99XxhcmqtqaZX95flXpGgWb+lnlinlCy23Y1sZ8+TzzM9A==} peerDependencies: - hono: '>=4.0.0' + hono: 4.13.4 '@huggingface/jinja@0.5.3': resolution: {integrity: sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==} @@ -8268,13 +8306,13 @@ packages: web-worker: optional: true - '@moeru/eventa@1.0.0-beta.15': - resolution: {integrity: sha512-isjbOHeQuZJmlYJAQ6C28awXa4lpSdSEi/YPHrMuZQADHKDDu6O2R2oUclBiLVWM0Ut8R4WJFY8uLRuZP6Hq6g==} + '@moeru/eventa@1.0.0': + resolution: {integrity: sha512-7Tz42aRNR5V3kzFuQb5evaAQ9szSxWq0sEBOQJ30KzR+0vCDd2d2XHh6oGQyQvF/HTQgk9w8JlgBti/xAeFG4A==} peerDependencies: '@tauri-apps/api': '>=2' electron: '>=39' h3: '>=2.0.1-rc.22' - hono: '>=4.12.25' + hono: 4.13.4 web-worker: ^1.5.0 peerDependenciesMeta: '@tauri-apps/api': @@ -12385,6 +12423,32 @@ packages: engines: {node: '>=10.0.0'} deprecated: this version has critical issues, please update to the latest version + '@xsai-apple-speech/transcription-electron-plugin@0.1.3': + resolution: {integrity: sha512-GdFvuzli/lUh3hOJTBEBR0QfhDww9avWEaOsjpaPL8m8HooDwLcfFHpW4t1jKC6gy4rOdQYDd2ftSj/cJ2kHdg==} + peerDependencies: + electron: '>=39' + peerDependenciesMeta: + electron: + optional: true + + '@xsai-apple-speech/transcription-native-darwin-arm64@0.1.3': + resolution: {integrity: sha512-8j8wYmAX5wyajhm713LqKyFaAYAdT5aP89PeeLz1i2wxjDRtiQCAuDaoDxsNJ8ToD/slROvT+WD95vOsWQJFeA==} + cpu: [arm64] + os: [darwin] + + '@xsai-apple-speech/transcription-native-darwin-x64@0.1.3': + resolution: {integrity: sha512-R5U5qI7rU7NSNO/xdapBwB8tMYghHV99eP+mUF7uIrU0I2rMz9wO6HrOm7v3a8mDoK5eqx6KNJzpGjiaeUNMuQ==} + cpu: [x64] + os: [darwin] + + '@xsai-apple-speech/transcription-native@0.1.3': + resolution: {integrity: sha512-XqrWBElWpVn4CxocBC7F1/baAoOei+aZouC9vHElQ4dfKUD3HVu9KgMwS3x8lRmnV+8KaH0R1skhCahfP0/nVg==} + + '@xsai-apple-speech/transcription@0.1.3': + resolution: {integrity: sha512-QHFZJcqgP53Lpo4wrtiTzL/FKmuVAlkXqZYV/Ml8x4L+03SKnCETcoi+CH2j4Xmp5IibY7zUjPUC6uZiloc2Mg==} + peerDependencies: + '@xsai/generate-transcription': '>=0.5.0-beta.8 <0.6.0' + '@xsai-ext/providers@0.4.4': resolution: {integrity: sha512-PVk3IFOPzPyvss9zY6IO6pJ890F5B6LWGWuhU6DzofWMTnqcUzm0hR2Ml/Qo8kZa3Qy/DZkUT0O1T+5g/fZkWw==} @@ -15422,18 +15486,14 @@ packages: hono-rate-limiter@0.5.3: resolution: {integrity: sha512-M0DxbVMpPELEzLi0AJg1XyBHLGJXz7GySjsPoK+gc5YeeBsdGDGe+2RvVuCAv8ydINiwlbxqYMNxUEyYfRji/A==} peerDependencies: - hono: ^4.10.8 + hono: 4.13.4 unstorage: ^1.17.3 peerDependenciesMeta: unstorage: optional: true - hono@4.11.3: - resolution: {integrity: sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==} - engines: {node: '>=16.9.0'} - - hono@4.12.2: - resolution: {integrity: sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==} + hono@4.13.4: + resolution: {integrity: sha512-AGEwKIyRMHRv1t8Wjwa3LHxQ61X5CqrdFT+4BRNTpqS5aJNnpl5WLjADb7vFlJzI/8uK7T5QLVApCMQKNa3LgQ==} engines: {node: '>=16.9.0'} hookable@5.5.3: @@ -22575,28 +22635,24 @@ snapshots: '@histoire/vendors@1.0.0-beta.1': {} - '@hono/node-server@1.19.14(hono@4.11.3)': + '@hono/node-server@1.19.14(hono@4.13.4)': dependencies: - hono: 4.11.3 + hono: 4.13.4 - '@hono/node-server@1.19.14(hono@4.12.2)': + '@hono/node-ws@1.3.0(@hono/node-server@1.19.14(hono@4.13.4))(bufferutil@4.1.0)(hono@4.13.4)(utf-8-validate@5.0.10)': dependencies: - hono: 4.12.2 - - '@hono/node-ws@1.3.0(@hono/node-server@1.19.14(hono@4.11.3))(bufferutil@4.1.0)(hono@4.11.3)(utf-8-validate@5.0.10)': - dependencies: - '@hono/node-server': 1.19.14(hono@4.11.3) - hono: 4.11.3 + '@hono/node-server': 1.19.14(hono@4.13.4) + hono: 4.13.4 ws: 8.20.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate - '@hono/otel@1.1.2(hono@4.11.3)': + '@hono/otel@1.1.2(hono@4.13.4)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.40.0 - hono: 4.11.3 + hono: 4.13.4 '@huggingface/jinja@0.5.3': {} @@ -23232,7 +23288,7 @@ snapshots: '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.14(hono@4.12.2) + '@hono/node-server': 1.19.14(hono@4.13.4) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -23242,7 +23298,7 @@ snapshots: eventsource-parser: 3.0.6 express: 5.2.1 express-rate-limit: 8.2.1(express@5.2.1) - hono: 4.12.2 + hono: 4.13.4 jose: 6.2.2 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -23282,33 +23338,34 @@ snapshots: h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)) web-worker: 1.5.0 - '@moeru/eventa@1.0.0-beta.15(electron@40.8.5)(h3@2.0.1-rc.25)(web-worker@1.5.0)': + '@moeru/eventa@1.0.0(electron@40.8.5)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0)': dependencies: nanoid: 6.0.1 picomatch: 4.0.4 optionalDependencies: electron: 40.8.5 h3: 2.0.1-rc.25 + hono: 4.13.4 web-worker: 1.5.0 - '@moeru/eventa@1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.12.2)(web-worker@1.5.0)': + '@moeru/eventa@1.0.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0)': dependencies: nanoid: 6.0.1 picomatch: 4.0.4 optionalDependencies: electron: 41.2.1 h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)) - hono: 4.12.2 + hono: 4.13.4 web-worker: 1.5.0 - '@moeru/eventa@1.0.0-beta.15(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0)': + '@moeru/eventa@1.0.0(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.13.4)(web-worker@1.5.0)': dependencies: nanoid: 6.0.1 picomatch: 4.0.4 optionalDependencies: electron: 41.2.1 h3: 2.0.1-rc.25 - hono: 4.11.3 + hono: 4.13.4 web-worker: 1.5.0 '@moeru/std@0.1.0-beta.1': {} @@ -27530,6 +27587,40 @@ snapshots: '@xmldom/xmldom@0.8.11': {} + '@xsai-apple-speech/transcription-electron-plugin@0.1.3(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0)': + dependencies: + '@moeru/eventa': 1.0.0(electron@41.2.1)(h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.22)))(hono@4.13.4)(web-worker@1.5.0) + '@xsai-apple-speech/transcription': 0.1.3(@xsai/generate-transcription@0.5.0-beta.8) + '@xsai/generate-transcription': 0.5.0-beta.8 + optionalDependencies: + electron: 41.2.1 + transitivePeerDependencies: + - '@tauri-apps/api' + - h3 + - hono + - web-worker + + '@xsai-apple-speech/transcription-native-darwin-arm64@0.1.3': + optional: true + + '@xsai-apple-speech/transcription-native-darwin-x64@0.1.3': + optional: true + + '@xsai-apple-speech/transcription-native@0.1.3(@xsai/generate-transcription@0.5.0-beta.8)': + dependencies: + '@moeru/std': 0.1.0-beta.17 + '@xsai-apple-speech/transcription': 0.1.3(@xsai/generate-transcription@0.5.0-beta.8) + optionalDependencies: + '@xsai-apple-speech/transcription-native-darwin-arm64': 0.1.3 + '@xsai-apple-speech/transcription-native-darwin-x64': 0.1.3 + transitivePeerDependencies: + - '@xsai/generate-transcription' + + '@xsai-apple-speech/transcription@0.1.3(@xsai/generate-transcription@0.5.0-beta.8)': + dependencies: + '@xsai/generate-transcription': 0.5.0-beta.8 + '@xsai/shared': 0.5.0-beta.8 + '@xsai-ext/providers@0.4.4': dependencies: '@xsai/shared': 0.4.4 @@ -30926,15 +31017,13 @@ snapshots: - utf-8-validate - yaml - hono-rate-limiter@0.5.3(hono@4.11.3)(unstorage@1.17.5(aws4fetch@1.0.20)(idb-keyval@6.2.2)(ioredis@5.10.1)): + hono-rate-limiter@0.5.3(hono@4.13.4)(unstorage@1.17.5(aws4fetch@1.0.20)(idb-keyval@6.2.2)(ioredis@5.10.1)): dependencies: - hono: 4.11.3 + hono: 4.13.4 optionalDependencies: unstorage: 1.17.5(aws4fetch@1.0.20)(idb-keyval@6.2.2)(ioredis@5.10.1) - hono@4.11.3: {} - - hono@4.12.2: {} + hono@4.13.4: {} hookable@5.5.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9023c3417..def7d8a16 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,6 +23,7 @@ packages: overrides: array-flatten: npm:@nolyfill/array-flatten@^1.0.44 axios: npm:feaxios@^0.0.23 + hono: 4.13.4 is-core-module: npm:@nolyfill/is-core-module@^1.0.39 isarray: npm:@nolyfill/isarray@^1.0.44 onnxruntime-web: npm:onnxruntime-web@^1.24.3 @@ -116,7 +117,7 @@ catalog: '@mediapipe/tasks-vision': ^0.10.34 '@modelcontextprotocol/sdk': ^1.29.0 '@moeru/eslint-config': 0.1.0-beta.19 - '@moeru/eventa': 1.0.0-beta.15 + '@moeru/eventa': 1.0.0 '@moeru/std': 0.1.0-beta.17 '@moeru/three-mmd': 0.1.0-beta.7 '@moeru/three-mmd-physics-ammo': 0.1.0-beta.7 @@ -218,6 +219,11 @@ catalog: '@vueuse/shared': ^14.2.1 '@webgpu/types': ^0.1.69 '@wxt-dev/module-vue': ^1.0.3 + '@xsai-apple-speech/transcription': 0.1.3 + '@xsai-apple-speech/transcription-electron-plugin': 0.1.3 + '@xsai-apple-speech/transcription-native': 0.1.3 + '@xsai-apple-speech/transcription-native-darwin-arm64': 0.1.3 + '@xsai-apple-speech/transcription-native-darwin-x64': 0.1.3 '@xsai-ext/providers': 0.5.0-beta.8 '@xsai-transformers/embed': ^0.1.0 '@xsai-transformers/shared': ^0.1.0 @@ -283,7 +289,7 @@ catalog: h3: 2.0.1-rc.20 hfup: ^1.0.4 histoire: 1.0.0-beta.1 - hono: 4.11.3 + hono: 4.13.4 hono-rate-limiter: ^0.5.3 html2canvas: ^1.4.1 idb-keyval: ^6.2.2