diff --git a/apps/realtime-audio/index.html b/apps/realtime-audio/index.html new file mode 100644 index 000000000..4915c5337 --- /dev/null +++ b/apps/realtime-audio/index.html @@ -0,0 +1,23 @@ + + + + + Project AIRI Real-time Audio Playground + + + + + + +
+ + + + diff --git a/apps/realtime-audio/netlify.toml b/apps/realtime-audio/netlify.toml new file mode 100755 index 000000000..e66d8ece3 --- /dev/null +++ b/apps/realtime-audio/netlify.toml @@ -0,0 +1,13 @@ +[build] +base = "/" +command = "pnpm -F @proj-airi/realtime-audio... run build" +publish = "/apps/realtime-audio/dist" + +[build.environment] +NODE_VERSION = "23" + +[[redirects]] +from = "/*" +to = "/index.html" +status = 200 +force = false diff --git a/apps/realtime-audio/package.json b/apps/realtime-audio/package.json new file mode 100644 index 000000000..e45964b9d --- /dev/null +++ b/apps/realtime-audio/package.json @@ -0,0 +1,42 @@ +{ + "name": "@proj-airi/realtime-audio", + "type": "module", + "private": true, + "description": "Realtime audio", + "author": { + "name": "Neko Ayaka", + "email": "neko@ayaka.moe", + "url": "https://github.com/nekomeowww" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/moeru-ai/airi.git", + "directory": "apps/realtime-audio" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@vueuse/core": "^13.0.0", + "@xsai/generate-text": "catalog:", + "@xsai/shared-chat": "catalog:", + "@xsai/stream-text": "catalog:", + "defu": "^6.1.4", + "es-toolkit": "^1.33.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@iconify-json/solar": "^1.2.2", + "@unocss/reset": "^66.1.0-beta.5", + "@vitejs/plugin-vue": "^5.2.3", + "superjson": "^2.2.2", + "unplugin-vue-router": "^0.12.0", + "vite": "^6.2.2", + "vue-router": "^4.5.0", + "vue-tsc": "^3.0.0-alpha.2" + } +} diff --git a/apps/realtime-audio/public/favicon-96x96.png b/apps/realtime-audio/public/favicon-96x96.png new file mode 100644 index 000000000..a70eda1af Binary files /dev/null and b/apps/realtime-audio/public/favicon-96x96.png differ diff --git a/apps/realtime-audio/public/favicon.svg b/apps/realtime-audio/public/favicon.svg new file mode 100644 index 000000000..17940af47 --- /dev/null +++ b/apps/realtime-audio/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/apps/realtime-audio/src/App.vue b/apps/realtime-audio/src/App.vue new file mode 100644 index 000000000..cdf59b6c6 --- /dev/null +++ b/apps/realtime-audio/src/App.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/apps/realtime-audio/src/composables/queue.ts b/apps/realtime-audio/src/composables/queue.ts new file mode 100644 index 000000000..81d006b36 --- /dev/null +++ b/apps/realtime-audio/src/composables/queue.ts @@ -0,0 +1,114 @@ +import type { Ref } from 'vue' + +import { ref } from 'vue' + +export interface HandlerContext { + data: T + itemsToBeProcessed: () => number + emit: (eventName: string, ...params: any[]) => void +} + +// FIXME: should be exported? +export interface Events { + add: Array<(payload: T) => void> + pick: Array<(payload: T) => void> + processing: Array<(payload: T, handler: (param: HandlerContext) => Promise) => void> + error: Array<(payload: T, error: Error, handler: (param: HandlerContext) => Promise) => void> + processed: Array<(payload: T, result: R, handler: (param: HandlerContext) => Promise) => void> + done: Array<(payload: T) => void> +} + +export function useQueue(options: { + handlers: Array<(ctx: HandlerContext) => Promise> +}) { + const queue = ref([]) as Ref + const isProcessing = ref(false) + const internalEventHandler: Events = { + add: [], + pick: [], + processing: [], + error: [], + processed: [], + done: [], + } + const internalHandlerEventHandler: Record void>> = {} + + function on>(eventName: E, handler: Events[E][number]) { + internalEventHandler[eventName].push(handler as any) + } + + function emit>(eventName: E, ...params: Parameters[E][number]>) { + const handlers = internalEventHandler[eventName] as Events[E] + handlers.forEach((handler) => { + (handler as any)(...params) + }) + } + + function onHandlerEvent(eventName: string, handler: (...params: any[]) => void) { + internalHandlerEventHandler[eventName] = internalHandlerEventHandler[eventName] || [] + internalHandlerEventHandler[eventName].push(handler) + } + + function emitHandlerEvent(eventName: string, ...params: any[]) { + const handlers = internalHandlerEventHandler[eventName] || [] + handlers.forEach((handler) => { + handler(...params) + }) + } + + async function add(payload: T) { + queue.value.push(payload) + emit('add', payload) + } + + function pick() { + const payload = queue.value.shift() + if (!payload) + return + + emit('pick', payload) + return payload + } + + async function handleItem() { + if (isProcessing.value) + return + + const payload = pick() + if (!payload) + return + + isProcessing.value = true + + for (const handler of options.handlers) { + emit('processing', payload, handler) + try { + const result = await handler({ data: payload, itemsToBeProcessed: () => queue.value.length, emit: emitHandlerEvent }) + emit('processed', payload, result, handler) + } + catch (err) { + emit('error', payload, err as Error, handler) + continue + } + } + + isProcessing.value = false + emit('done', payload) + + // Process next item if any + if (queue.value.length > 0) + handleItem() + } + + on('add', handleItem) + on('done', handleItem) + + return { + add, + on, + onHandlerEvent, + queue, + } +} + +export type UseQueueReturn = ReturnType> diff --git a/apps/realtime-audio/src/main.ts b/apps/realtime-audio/src/main.ts new file mode 100644 index 000000000..52e83a7af --- /dev/null +++ b/apps/realtime-audio/src/main.ts @@ -0,0 +1,13 @@ +import { createApp } from 'vue' +import { createRouter, createWebHashHistory } from 'vue-router' +import { routes } from 'vue-router/auto-routes' + +import App from './App.vue' +import '@unocss/reset/tailwind.css' +import 'uno.css' + +const router = createRouter({ routes, history: createWebHashHistory() }) + +createApp(App) + .use(router) + .mount('#app') diff --git a/apps/realtime-audio/src/pages/index.vue b/apps/realtime-audio/src/pages/index.vue new file mode 100644 index 000000000..d77107046 --- /dev/null +++ b/apps/realtime-audio/src/pages/index.vue @@ -0,0 +1,234 @@ + + +