feat(stage-tamagotchi): add Apple Speech transcription (#2364)

This commit is contained in:
Neko
2026-08-25 17:41:43 +00:00
committed by GitHub
parent ca40f6ea7a
commit 95cc685b1b
32 changed files with 1142 additions and 191 deletions
@@ -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
@@ -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',
],
},
},
+7
View File
@@ -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:",
+7 -1
View File
@@ -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) => {
@@ -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<void> | 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 }
}
+41 -1
View File
@@ -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', () => {
+42 -10
View File
@@ -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<ArrayBuffer> {
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<ArrayBuffer> {
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)
}
/**
@@ -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",
@@ -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
@@ -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
+1
View File
@@ -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:*",
@@ -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
@@ -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<typeof setTimeout> | 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<string, unknown>) {
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(() => {
</div>
</div>
<component
:is="activeProviderHearingView"
v-if="activeProviderHearingView"
/>
<!-- Model selection section -->
<div v-if="activeTranscriptionProvider">
<div flex="~ col gap-4">
@@ -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))
}
@@ -526,6 +526,15 @@ function handleDeleteProvider() {
:required="field.required"
@update:model-value="setFieldValue(field.key, $event)"
/>
<FieldCombobox
v-else-if="field.type === 'select'"
:model-value="getStringField(field.key)"
:label="field.label"
:description="field.description"
:placeholder="field.placeholder"
:options="field.options"
@update:model-value="setFieldValue(field.key, $event)"
/>
<FieldInput
v-else
v-model="providerConfigEdit.config[field.key]"
+2
View File
@@ -105,6 +105,8 @@
"@vueuse/core": "catalog:",
"@vueuse/motion": "catalog:",
"@vueuse/shared": "catalog:",
"@xsai-apple-speech/transcription": "catalog:",
"@xsai-apple-speech/transcription-electron-plugin": "catalog:",
"@xsai-ext/providers": "catalog:",
"@xsai-transformers/embed": "catalog:",
"@xsai-transformers/shared": "catalog:",
@@ -9,6 +9,7 @@ import type { VoiceKey, Voices } from '../../../workers/kokoro/types'
import type { AllocationToken } from '../gpu-resource-coordinator'
import type { ProgressPayload } from '../protocol'
import { toWav } from '@proj-airi/audio/encoding'
import { defaultPerfTracer } from '@proj-airi/stage-shared'
import { Mutex } from 'async-mutex'
@@ -74,57 +75,6 @@ export interface KokoroAdapter {
const LOAD_MODEL_TIMEOUT = TIMEOUTS.KOKORO_LOAD
const GENERATE_TIMEOUT = TIMEOUTS.KOKORO_GENERATE
// ---------------------------------------------------------------------------
// Audio Encoding
// ---------------------------------------------------------------------------
/**
* Encode raw PCM Float32Array samples into a WAV ArrayBuffer.
* This runs on the main thread intentionally lightweight (just header + int16 conversion).
*/
function encodeWav(samples: Float32Array, sampleRate: number, numChannels = 1): ArrayBuffer {
const bitsPerSample = 16
const bytesPerSample = bitsPerSample / 8
const dataLength = samples.length * bytesPerSample
const headerLength = 44
const buffer = new ArrayBuffer(headerLength + dataLength)
const view = new DataView(buffer)
// RIFF header
writeString(view, 0, 'RIFF')
view.setUint32(4, 36 + dataLength, true)
writeString(view, 8, 'WAVE')
// fmt chunk
writeString(view, 12, 'fmt ')
view.setUint32(16, 16, true) // chunk size
view.setUint16(20, 1, true) // PCM format
view.setUint16(22, numChannels, true)
view.setUint32(24, sampleRate, true)
view.setUint32(28, sampleRate * numChannels * bytesPerSample, true) // byte rate
view.setUint16(32, numChannels * bytesPerSample, true) // block align
view.setUint16(34, bitsPerSample, true)
// data chunk
writeString(view, 36, 'data')
view.setUint32(40, dataLength, true)
// Convert Float32 [-1, 1] to Int16
const output = new Int16Array(buffer, headerLength)
for (let i = 0; i < samples.length; i++) {
const s = Math.max(-1, Math.min(1, samples[i]))
output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF
}
return buffer
}
function writeString(view: DataView, offset: number, str: string): void {
for (let i = 0; i < str.length; i++) {
view.setUint8(offset + i, str.charCodeAt(i))
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
@@ -454,7 +404,7 @@ export function createKokoroAdapter(): KokoroAdapter {
if (output.action === 'generate') {
state = 'ready'
onSuccess()
return encodeWav(output.samples as Float32Array, output.samplingRate as number)
return toWav((output.samples as Float32Array).buffer, output.samplingRate as number)
}
const errorCode = classifyError(new Error('Unexpected output action'))
@@ -0,0 +1,23 @@
import type { ComputedRef, InjectionKey } from 'vue'
import { inject } from 'vue'
/** APIs that the Hearing module exposes to the active Provider view. */
export interface HearingProviderViewContext {
/** Configuration for the active transcription Provider. */
providerConfig: ComputedRef<Readonly<Record<string, unknown>> | undefined>
/** Saves a partial configuration as configured and refreshes the monitoring session. */
updateProviderConfig: (patch: Record<string, unknown>) => Promise<void>
}
export const hearingProviderViewContextKey: InjectionKey<HearingProviderViewContext>
= 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
}
@@ -1,4 +1,5 @@
export * from './attributes'
export * from './hearing-view'
export * from './metadata'
export * from './providers'
export * from './types'
@@ -0,0 +1,110 @@
<script setup lang="ts">
import { errorMessageFrom } from '@moeru/std'
import { FieldCombobox, GhostButton } from '@proj-airi/ui'
import { computedAsync } from '@vueuse/core'
import { computed, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { useHearingProviderViewContext } from '../../hearing-view'
import { listAppleSpeechLocaleOptions } from './provider'
const { t } = useI18n()
const { providerConfig, updateProviderConfig } = useHearingProviderViewContext()
const loadAttempt = shallowRef(0)
const isLoading = shallowRef(false)
const isSaving = shallowRef(false)
const loadError = shallowRef<string>()
const saveError = shallowRef<string>()
const locale = computed(() => {
const value = providerConfig.value?.locale
return typeof value === 'string' && value.trim() ? value : 'en-US'
})
const localeOptions = computedAsync(async (onCancel) => {
void loadAttempt.value
const abortController = new AbortController()
onCancel(() => abortController.abort())
loadError.value = undefined
try {
return await listAppleSpeechLocaleOptions({
abortSignal: abortController.signal,
config: { locale: locale.value },
t,
})
}
catch (cause) {
if (!abortController.signal.aborted) {
loadError.value = errorMessageFrom(cause)
?? t('settings.pages.providers.catalog.edit.config.load-error')
}
return []
}
}, [], { evaluating: isLoading })
function retry() {
loadAttempt.value++
}
async function updateLocale(value: string | undefined) {
if (!value || value === locale.value || isSaving.value)
return
isSaving.value = true
saveError.value = undefined
try {
await updateProviderConfig({ locale: value })
}
catch (cause) {
saveError.value = errorMessageFrom(cause)
?? t('settings.pages.providers.catalog.edit.config.save-error')
}
finally {
isSaving.value = false
}
}
</script>
<template>
<div :class="['flex', 'flex-col', 'gap-2']">
<FieldCombobox
data-testid="apple-speech-locale"
:model-value="locale"
:label="t('settings.pages.providers.provider.apple-speech-transcription.fields.locale.label')"
:description="t('settings.pages.providers.provider.apple-speech-transcription.fields.locale.description')"
:placeholder="isLoading
? t('settings.pages.providers.catalog.edit.config.loading')
: t('settings.pages.providers.provider.apple-speech-transcription.fields.locale.placeholder')"
:options="localeOptions"
:disabled="isLoading || isSaving || !!loadError"
layout="vertical"
@update:model-value="updateLocale"
>
<template #label>
<div :class="['flex', 'items-center', 'gap-2']">
<span>{{ t('settings.pages.providers.provider.apple-speech-transcription.fields.locale.label') }}</span>
<div v-if="isLoading || isSaving" :class="['i-svg-spinners:ring-resize', 'text-sm', 'text-neutral-400']" />
</div>
</template>
</FieldCombobox>
<div
v-if="loadError || saveError"
role="alert"
:class="[
'flex', 'items-center', 'justify-between', 'gap-2',
'text-xs', 'text-red-600', 'dark:text-red-400',
]"
>
<span>{{ loadError || saveError }}</span>
<GhostButton
v-if="loadError"
size="sm"
:label="t('settings.pages.providers.catalog.edit.config.retry')"
@click="retry"
/>
</div>
</div>
</template>
@@ -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<AppleSpeechSessionOperations> {
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',
},
])
})
})
@@ -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<AppleSpeechConfig>) {
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<StreamTranscriptionOptions['inputAudioStream']>,
writer: WritableStreamDefaultWriter<Float32Array>,
) {
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<AppleSpeechConfig>({
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',
}],
},
})
@@ -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<typeof appleSpeechConfigSchema>
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<AppleSpeechConfig>) {
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()
}
}
@@ -1,4 +1,5 @@
import './amazon-bedrock'
import './apple-speech'
import './openai'
import './openai-audio'
import './aihubmix'
@@ -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<TConfig extends any = any> {
*/
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<TConfig>) => MaybePromise<$ZodType<TConfig>>
onboardingFields?: (ctx: { t: ComposerTranslation }) => MaybePromise<ProviderOnboardingField[]>
@@ -235,7 +242,7 @@ export interface ProviderDefinition<TConfig extends any = any> {
reasoning?: ChatReasoningCapability
}
transcription?: {
protocol: 'websocket' | 'http'
protocol: 'websocket' | 'http' | 'native'
generateOutput: boolean
streamOutput: boolean
streamInput: boolean
+2 -7
View File
@@ -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<Float32Array> {
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))
}
// ---------------------------------------------------------------------------
@@ -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<string, StreamTranscription> = {
'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<typeof streamingVadSession.value>['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)
@@ -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<string, { config?: { locale?: string } }>
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' })
})
})
@@ -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',
},
}
}
@@ -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'
+165 -76
View File
@@ -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: {}
+8 -2
View File
@@ -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