refactor(stage-tamagotchi): remove stage electron (#115)

* refactor(stage-tamagotchi): remove stage electron

* fix(stage-tamagotchi): path
This commit is contained in:
LemonNeko
2025-04-05 18:04:13 +08:00
committed by GitHub
parent ed01d1e08b
commit 15fa1fcfbe
147 changed files with 57 additions and 7216 deletions
+3 -3
View File
@@ -59,13 +59,13 @@ jobs:
run: pnpm run packages:build
- name: Build Application
run: cd apps/stage-tamagotchi-tauri && pnpm tauri build --target ${{ matrix.target }}
run: cd apps/stage-tamagotchi && pnpm tauri build --target ${{ matrix.target }}
- name: Rename Artifacts
run: node apps/stage-tamagotchi-tauri/scripts/rename-artifacts.js ${{ matrix.target }}
run: node apps/stage-tamagotchi/scripts/rename-artifacts.js ${{ matrix.target }}
- name: Upload To GitHub Releases
uses: softprops/action-gh-release@v2
with:
files: apps/stage-tamagotchi-tauri/bundle/airi_*
files: apps/stage-tamagotchi/bundle/airi_*
append_body: true
+3 -3
View File
@@ -61,13 +61,13 @@ jobs:
run: pnpm run packages:build
- name: Build Application
run: cd apps/stage-tamagotchi-tauri && pnpm tauri build --target ${{ matrix.target }}
run: cd apps/stage-tamagotchi && pnpm tauri build --target ${{ matrix.target }}
- name: Rename Artifacts
run: node apps/stage-tamagotchi-tauri/scripts/rename-artifacts.js ${{ matrix.target }}
run: node apps/stage-tamagotchi/scripts/rename-artifacts.js ${{ matrix.target }}
- name: Upload To GitHub Releases
uses: softprops/action-gh-release@v2
with:
files: apps/stage-tamagotchi-tauri/bundle/airi_*
files: apps/stage-tamagotchi/bundle/airi_*
append_body: true
-1
View File
@@ -32,7 +32,6 @@ coverage/
**/public/assets/vrm/models/*
packages/stage-ui/public/assets/fonts/
apps/stage-tamagotchi/public/assets/fonts/
apps/stage-tamagotchi-tauri/public/assets/fonts/
apps/stage-web/public/assets/fonts/
*.pcm
+1 -3
View File
@@ -3,9 +3,7 @@
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": [
"apps/stage-web/locales",
"apps/stage-tamagotchi/src/renderer/locales",
"apps/stage-tamagotchi/src/main/locales",
"apps/stage-tamagotchi-tauri/locales"
"apps/stage-tamagotchi/locales"
],
"i18n-ally.sortKeys": true,
"i18n-ally.enabledFrameworks": [
-51
View File
@@ -1,51 +0,0 @@
# たまごっち アイリ
A desktop application for たまごっち アイリ.
## Project Setup
### Install
```bash
$ pnpm install
```
### Development
```bash
$ cd /apps/stage
$ pnpm dev:tamagotchi
```
Then open another terminal and run:
```bash
$ cd /apps/tamagotchi
$ pnpm dev:tamagotchi
```
### Build
```bash
# For windows
$ pnpm build:win
# For macOS
$ pnpm build:mac
# For Linux
$ pnpm build:linux
```
## TODO List
- [ ] Window control: Move, resize, hide, show
- [ ] Motions follow the cursor
- [ ] System tray
- [ ] tauri i18n
- [ ] Multi-DPI
- [ ] CI/CD
- [ ] Multi monitor
- [ ] Send message via macOS Spotlight
- [ ] Steam
- [ ] CSP settings
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

-126
View File
@@ -1,126 +0,0 @@
{
"name": "@proj-airi/stage-tamagotchi-tauri",
"type": "module",
"version": "0.4.19",
"private": true,
"description": "Airi tamagotchi app written in Tauri",
"author": "LemonNekoGH",
"scripts": {
"typecheck": "vue-tsc --noEmit --composite false",
"start": "electron-vite preview",
"dev": "vite",
"build": "vite build",
"build:unpack": "tauri build --no-bundle"
},
"dependencies": {
"@11labs/client": "^0.1.0",
"@formkit/auto-animate": "^0.8.2",
"@huggingface/transformers": "^3.4.2",
"@pixi/app": "6",
"@pixi/constants": "6",
"@pixi/core": "6",
"@pixi/display": "6",
"@pixi/extensions": "6",
"@pixi/interaction": "6",
"@pixi/loaders": "6",
"@pixi/math": "6",
"@pixi/runner": "6",
"@pixi/settings": "6",
"@pixi/sprite": "6",
"@pixi/ticker": "6",
"@pixi/utils": "6",
"@pixiv/three-vrm": "^3.3.6",
"@pixiv/three-vrm-animation": "^3.3.6",
"@pixiv/three-vrm-core": "^3.3.6",
"@proj-airi/stage-ui": "workspace:^",
"@ricky0123/vad-web": "^0.0.22",
"@tauri-apps/api": "^2.4.1",
"@tauri-apps/plugin-os": ">=2.2.1",
"@tresjs/cientos": "^4.3.0",
"@tresjs/core": "^4.3.3",
"@unhead/vue": "^2.0.3",
"@vueuse/core": "^13.0.0",
"@vueuse/head": "^2.0.0",
"@vueuse/shared": "^13.0.0",
"@xsai-ext/providers-cloud": "catalog:",
"@xsai-ext/providers-local": "catalog:",
"@xsai-ext/shared-providers": "catalog:",
"@xsai/generate-speech": "catalog:",
"@xsai/generate-text": "catalog:",
"@xsai/model": "catalog:",
"@xsai/shared-chat": "catalog:",
"@xsai/stream-text": "catalog:",
"@xsai/utils-chat": "catalog:",
"culori": "^4.0.1",
"defu": "^6.1.4",
"drizzle-kit": "^0.30.6",
"drizzle-orm": "^0.41.0",
"jszip": "^3.10.1",
"localforage": "^1.10.0",
"nprogress": "^0.2.0",
"ofetch": "^1.4.1",
"onnxruntime-web": "^1.21.0",
"pinia": "^3.0.1",
"pixi-filters": "^4.2.0",
"pixi-live2d-display": "^0.4.0",
"popmotion": "^11.0.5",
"radix-vue": "^1.9.17",
"rehype-stringify": "^10.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"shiki": "^3.2.1",
"three": "^0.175.0",
"unified": "^11.0.5",
"valibot": "1.0.0-beta.9",
"vaul-vue": "^0.4.1",
"vue": "^3.5.13",
"vue-demi": "^0.14.10",
"vue-i18n": "^11.1.2",
"vue-router": "^4.5.0",
"xsschema": "catalog:",
"yauzl": "^3.2.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@iconify-json/carbon": "^1.2.8",
"@iconify-json/eos-icons": "^1.2.2",
"@iconify-json/lucide": "^1.2.34",
"@iconify-json/mingcute": "^1.2.3",
"@iconify-json/simple-icons": "^1.2.30",
"@iconify-json/solar": "^1.2.2",
"@iconify-json/svg-spinners": "^1.2.2",
"@iconify-json/vscode-icons": "^1.2.18",
"@iconify/utils": "^2.3.0",
"@intlify/unplugin-vue-i18n": "^6.0.5",
"@proj-airi/drizzle-duckdb-wasm": "workspace:^",
"@proj-airi/lobe-icons": "^1.0.5",
"@proj-airi/provider-transformers": "workspace:^",
"@proj-airi/ui-transitions": "workspace:^",
"@proj-airi/unplugin-fetch": "^0.1.4",
"@proj-airi/unplugin-live2d-sdk": "^0.1.2",
"@shikijs/markdown-it": "^3.2.1",
"@tauri-apps/cli": "^2.4.1",
"@types/culori": "^2.1.1",
"@types/markdown-it-link-attributes": "^3.0.5",
"@types/nprogress": "^0.2.3",
"@types/three": "^0.175.0",
"@types/yauzl": "^2.10.3",
"@unocss/reset": "^66.1.0-beta.9",
"@vitejs/plugin-vue": "^5.2.3",
"@vue-macros/volar": "^3.0.0-beta.7",
"@vueuse/motion": "^3.0.3",
"less": "^4.2.2",
"markdown-it-link-attributes": "^4.0.1",
"unocss-preset-scrollbar": "^3.2.0",
"unplugin-auto-import": "^19.1.2",
"unplugin-vue-components": "^28.4.1",
"unplugin-vue-macros": "^2.14.5",
"unplugin-vue-markdown": "^28.3.1",
"unplugin-vue-router": "^0.12.0",
"vite-bundle-visualizer": "^1.2.1",
"vite-plugin-pwa": "^1.0.0",
"vite-plugin-vue-devtools": "^7.7.2",
"vite-plugin-vue-layouts": "^0.11.0",
"vue-tsc": "^3.0.0-alpha.2"
}
}
-36
View File
@@ -1,36 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
"lib": [
"DOM",
"ESNext"
],
"paths": {
"@proj-airi/stage-ui/*": [
"../../packages/stage-ui/src/*"
],
"@proj-airi/ui-transitions": [
"../../packages/ui-transitions/src/index.ts"
]
},
"resolveJsonModule": true,
"types": [
"vitest",
"vite/client",
"vite-plugin-vue-layouts/client",
"vite-plugin-pwa/client",
"unplugin-vue-macros/macros-global",
"unplugin-vue-router/client"
],
"allowJs": true,
"strict": true,
"skipLibCheck": true
},
"vueCompilerOptions": {
"plugins": [
"@vue-macros/volar/define-models",
"@vue-macros/volar/define-slots"
]
}
}
-104
View File
@@ -1,104 +0,0 @@
import { createExternalPackageIconLoader } from '@iconify/utils/lib/loader/external-pkg'
import { colorToString } from '@unocss/preset-mini/utils'
import { createLocalFontProcessor } from '@unocss/preset-web-fonts/local'
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetWebFonts,
presetWind3,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
import { presetScrollbar } from 'unocss-preset-scrollbar'
import { parseColor } from 'unocss/preset-mini'
function createColorSchemeConfig(hueOffset = 0) {
return {
DEFAULT: `oklch(62% var(--theme-colors-chroma) calc(var(--theme-colors-hue) + ${hueOffset}))`,
50: `color-mix(in srgb, oklch(95% var(--theme-colors-chroma-50) calc(var(--theme-colors-hue) + ${hueOffset})) 30%, oklch(100% 0 360))`,
100: `color-mix(in srgb, oklch(95% var(--theme-colors-chroma-100) calc(var(--theme-colors-hue) + ${hueOffset})) 80%, oklch(100% 0 360))`,
200: `oklch(90% var(--theme-colors-chroma-200) calc(var(--theme-colors-hue) + ${hueOffset}))`,
300: `oklch(85% var(--theme-colors-chroma-300) calc(var(--theme-colors-hue) + ${hueOffset}))`,
400: `oklch(74% var(--theme-colors-chroma-400) calc(var(--theme-colors-hue) + ${hueOffset}))`,
500: `oklch(62% var(--theme-colors-chroma) calc(var(--theme-colors-hue) + ${hueOffset}))`,
600: `oklch(54% var(--theme-colors-chroma-600) calc(var(--theme-colors-hue) + ${hueOffset}))`,
700: `oklch(49% var(--theme-colors-chroma-700) calc(var(--theme-colors-hue) + ${hueOffset}))`,
800: `oklch(42% var(--theme-colors-chroma-800) calc(var(--theme-colors-hue) + ${hueOffset}))`,
900: `oklch(37% var(--theme-colors-chroma-900) calc(var(--theme-colors-hue) + ${hueOffset}))`,
950: `oklch(29% var(--theme-colors-chroma-950) calc(var(--theme-colors-hue) + ${hueOffset}))`,
}
}
export default defineConfig({
presets: [
presetWind3(),
presetAttributify(),
presetTypography(),
presetWebFonts({
fonts: {
sans: 'DM Sans',
serif: 'DM Serif Display',
mono: 'DM Mono',
cute: 'Kiwi Maru',
cuteen: 'Sniglet',
},
// Web fonts preset
// https://unocss.dev/presets/web-fonts#serve-fonts-locally
processors: createLocalFontProcessor(),
}),
presetIcons({
scale: 1.2,
collections: {
...createExternalPackageIconLoader('@proj-airi/lobe-icons'),
},
}),
presetScrollbar(),
],
transformers: [
transformerDirectives({
applyVariable: ['--at-apply'],
}),
transformerVariantGroup(),
],
safelist: 'prose prose-sm m-auto text-left'.split(' '),
// hyoban/unocss-preset-shadcn: Use shadcn ui with UnoCSS
// https://github.com/hyoban/unocss-preset-shadcn
//
// Thanks to
// https://github.com/unovue/shadcn-vue/issues/34#issuecomment-2467318118
// https://github.com/hyoban-template/shadcn-vue-unocss-starter
//
// By default, `.ts` and `.js` files are NOT extracted.
// If you want to extract them, use the following configuration.
// It's necessary to add the following configuration if you use shadcn-vue or shadcn-svelte.
content: {
pipeline: {
include: [
// the default
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
// include js/ts files
'(components|src)/**/*.{js,ts}',
'**/stage-ui/**/*.{js,ts}',
],
},
},
rules: [
[/^mask-\[(.*)\]$/, ([, suffix]) => ({ '-webkit-mask-image': suffix.replace(/_/g, ' ') })],
[/^bg-dotted-\[(.*)\]$/, ([, color], { theme }) => {
const parsedColor = parseColor(color, theme)
// Util usage: https://github.com/unocss/unocss/blob/f57ef6ae50006a92f444738e50f3601c0d1121f2/packages-presets/preset-mini/src/_utils/utilities.ts#L186
return {
'background-image': `radial-gradient(circle at 1px 1px, ${colorToString(parsedColor?.cssColor ?? parsedColor?.color ?? color, 'var(--un-background-opacity)')} 1px, transparent 0)`,
'--un-background-opacity': parsedColor?.cssColor?.alpha ?? parsedColor?.alpha ?? 1,
}
}],
],
theme: {
colors: {
primary: createColorSchemeConfig(),
complementary: createColorSchemeConfig(180),
},
},
})
+4 -2
View File
@@ -41,9 +41,11 @@ $ pnpm build:linux
- [ ] Window control: Move, resize, hide, show
- [ ] Motions follow the cursor
- [x] System tray
- [x] i18n
- [ ] System tray
- [ ] tauri i18n
- [ ] Multi-DPI
- [ ] CI/CD
- [ ] Multi monitor
- [ ] Send message via macOS Spotlight
- [ ] Steam
- [ ] CSP settings
@@ -1,43 +0,0 @@
appId: com.github.moeru-ai.airi-tamagotchi
productName: airi
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
asarUnpack:
- resources/**
win:
executableName: tamagotchi
nsis:
artifactName: ${productName}-${os}-${arch}-v${version}.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
# - NSCameraUseContinuityCameraDeviceType: Application requests access to the device's camera.
# - AVCaptureDeviceTypeContinuityCamera: Application requests access to the device's camera.
notarize: false
dmg:
artifactName: ${productName}-${os}-${arch}-v${version}.${ext}
linux:
target:
- AppImage
maintainer: github.com/moeru-ai Contributors
category: Entertainment
appImage:
artifactName: ${productName}-${os}-${arch}-v${version}.${ext}
npmRebuild: false
# publish:
# provider: generic
# url: https://example.com/auto-updates
@@ -1,13 +0,0 @@
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import rendererConfig from './renderer.vite.config'
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
},
preload: {
plugins: [externalizeDepsPlugin()],
},
renderer: rendererConfig,
})
+9 -19
View File
@@ -1,28 +1,19 @@
{
"name": "@proj-airi/stage-tamagotchi",
"type": "module",
"version": "0.4.19",
"private": true,
"description": "An Electron application with Vue and TypeScript",
"description": "Airi tamagotchi app written in Tauri",
"author": "LemonNekoGH",
"homepage": "https://electron-vite.org",
"main": "./out/main/index.js",
"scripts": {
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
"typecheck": "vue-tsc --noEmit --composite false",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "pnpm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "pnpm run build && electron-builder --dir",
"build:win": "pnpm run build && electron-builder --win",
"build:mac": "pnpm run build && electron-builder --mac",
"build:linux": "pnpm run build && electron-builder --linux"
"dev": "vite",
"build": "vite build",
"build:unpack": "tauri build --no-bundle"
},
"dependencies": {
"@11labs/client": "^0.1.0",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@formkit/auto-animate": "^0.8.2",
"@huggingface/transformers": "^3.4.2",
"@pixi/app": "6",
@@ -43,6 +34,8 @@
"@pixiv/three-vrm-core": "^3.3.6",
"@proj-airi/stage-ui": "workspace:^",
"@ricky0123/vad-web": "^0.0.22",
"@tauri-apps/api": "^2.4.1",
"@tauri-apps/plugin-os": ">=2.2.1",
"@tresjs/cientos": "^4.3.0",
"@tresjs/core": "^4.3.3",
"@unhead/vue": "^2.0.3",
@@ -89,7 +82,6 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@electron-toolkit/tsconfig": "^1.0.1",
"@iconify-json/carbon": "^1.2.8",
"@iconify-json/eos-icons": "^1.2.2",
"@iconify-json/lucide": "^1.2.34",
@@ -107,6 +99,7 @@
"@proj-airi/unplugin-fetch": "^0.1.4",
"@proj-airi/unplugin-live2d-sdk": "^0.1.2",
"@shikijs/markdown-it": "^3.2.1",
"@tauri-apps/cli": "^2.4.1",
"@types/culori": "^2.1.1",
"@types/markdown-it-link-attributes": "^3.0.5",
"@types/nprogress": "^0.2.3",
@@ -116,9 +109,6 @@
"@vitejs/plugin-vue": "^5.2.3",
"@vue-macros/volar": "^3.0.0-beta.7",
"@vueuse/motion": "^3.0.3",
"electron": "^34.4.1",
"electron-builder": "24.13.3",
"electron-vite": "^2.3.0",
"less": "^4.2.2",
"markdown-it-link-attributes": "^4.0.1",
"unocss-preset-scrollbar": "^3.2.0",
@@ -1,59 +0,0 @@
import { join, resolve } from 'node:path'
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
import { Download } from '@proj-airi/unplugin-fetch'
import { DownloadLive2DSDK } from '@proj-airi/unplugin-live2d-sdk'
import { templateCompilerOptions } from '@tresjs/core'
import Vue from '@vitejs/plugin-vue'
import UnoCss from 'unocss/vite'
import VueMacros from 'unplugin-vue-macros/vite'
import VueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite'
import Layouts from 'vite-plugin-vue-layouts'
export default defineConfig({
optimizeDeps: {
exclude: [
'@proj-airi/stage-ui/*',
],
},
resolve: {
alias: {
'@renderer': resolve(join('src', 'renderer', 'src')),
'@proj-airi/stage-ui': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'dist')),
'@proj-airi/stage-ui/stores': resolve(join(import.meta.dirname, '..', '..', 'packages', 'stage-ui', 'dist', 'stores')),
},
},
plugins: [
VueMacros({
plugins: {
vue: Vue({
include: [/\.vue$/, /\.md$/],
...templateCompilerOptions,
}),
vueJsx: false,
},
}),
VueRouter({
dts: resolve(import.meta.dirname, 'src/typed-router.d.ts'),
routesFolder: 'src/renderer/src/pages',
}),
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts
Layouts(),
UnoCss(),
// https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
VueI18n({
runtimeOnly: true,
compositionOnly: true,
fullInstall: true,
include: [resolve(import.meta.dirname, 'src', 'renderer', 'locales/**')],
}),
DownloadLive2DSDK(),
Download('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', 'hiyori_free_zh.zip', 'assets/live2d/models'),
Download('https://dist.ayaka.moe/live2d-models/hiyori_pro_zh.zip', 'hiyori_pro_zh.zip', 'assets/live2d/models'),
],
})

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

-210
View File
@@ -1,210 +0,0 @@
import { join } from 'node:path'
import { env, platform } from 'node:process'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { nativeImage, shell } from 'electron/common'
import { app, BrowserWindow, dialog, ipcMain, Tray } from 'electron/main'
import trayIconMacos from '../../build/icon-tray-macos.png?asset'
import icon from '../../build/icon.png?asset'
import { createI18n } from './locales'
import { createApplicationMenu, createTrayMenu } from './menu'
let mainWindow: BrowserWindow
let tray: Tray
const i18n = createI18n()
function showQuitDialog() {
dialog.showMessageBox({
type: 'info',
title: i18n.t('menu.quit'),
message: i18n.t('quitDialog.message'),
buttons: [i18n.t('quitDialog.buttons.quit'), i18n.t('quitDialog.buttons.cancel')],
}).then((result) => {
if (result.response === 0) {
mainWindow.webContents.send('before-quit')
setTimeout(() => {
app.quit()
}, 2000)
}
})
}
function rebuildTrayMenu() {
if (mainWindow.isVisible()) {
tray.setContextMenu(createTrayMenu(i18n, mainWindow.isVisible(), () => {
mainWindow.webContents.send('before-hide')
setTimeout(() => {
mainWindow.hide()
rebuildTrayMenu()
}, 2000)
}, createSettingsWindow, showQuitDialog))
return
}
tray.setContextMenu(createTrayMenu(i18n, mainWindow.isVisible(), () => {
mainWindow.show()
mainWindow.webContents.send('after-show')
rebuildTrayMenu()
}, createSettingsWindow, showQuitDialog))
}
function createWindow() {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 300 * 1.5,
height: 400 * 1.5,
show: false,
autoHideMenuBar: true,
frame: false,
transparent: true,
resizable: false,
hasShadow: false,
alwaysOnTop: true,
...(platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(import.meta.dirname, '..', 'preload', 'index.js'),
sandbox: false,
},
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
mainWindow.show()
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && env.ELECTRON_RENDERER_URL) {
mainWindow.loadURL(env.ELECTRON_RENDERER_URL)
}
else {
mainWindow.loadFile(join(import.meta.dirname, '..', '..', 'out', 'renderer', 'index.html'))
}
}
let settingsWindow: BrowserWindow | null = null
function createSettingsWindow() {
if (settingsWindow) {
settingsWindow.show()
return
}
settingsWindow = new BrowserWindow({
width: 300 * 2,
height: 400 * 2,
show: false,
webPreferences: {
preload: join(import.meta.dirname, '..', 'preload', 'index.js'),
sandbox: false,
},
})
settingsWindow.on('ready-to-show', () => {
settingsWindow?.show()
})
settingsWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
settingsWindow.on('closed', () => {
settingsWindow = null
})
settingsWindow.show()
if (is.dev && env.ELECTRON_RENDERER_URL) {
settingsWindow.loadURL(join(env.ELECTRON_RENDERER_URL, '#/settings'))
}
else {
settingsWindow.loadFile(join(import.meta.dirname, '..', '..', 'out', 'renderer', 'index.html'), {
hash: '/settings',
})
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createApplicationMenu(i18n, showQuitDialog, createSettingsWindow)
// Set app user model id for windows
electronApp.setAppUserModelId('com.github.moeru-ai.airi-tamagotchi')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// IPC test
ipcMain.on('quit', showQuitDialog)
ipcMain.on('open-settings', () => createSettingsWindow())
ipcMain.on('close-settings', () => {
if (settingsWindow) {
settingsWindow.close()
}
})
createWindow()
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
const trayIcon = platform === 'darwin'
? nativeImage.createFromPath(trayIconMacos).resize({ width: 16, height: 16 })
: nativeImage.createFromPath(icon).resize({ width: 16, height: 16 })
tray = new Tray(trayIcon)
tray.setToolTip('Airi')
rebuildTrayMenu()
app.dock.hide()
ipcMain.on('locale-changed', (_, language: string) => {
i18n.setLocale(language)
rebuildTrayMenu()
createApplicationMenu(i18n, showQuitDialog, createSettingsWindow)
})
ipcMain.on('start-resize-window', () => {
mainWindow.setResizable(true)
})
ipcMain.on('stop-resize-window', () => {
mainWindow.setResizable(false)
})
ipcMain.handle('stop-move-window', () => {
return mainWindow.getPosition()
})
ipcMain.on('window-size-changed', (_, width: number, height: number) => {
if (width === 0 || height === 0) {
mainWindow.setSize(300 * 1.5, 400 * 1.5) // back to default size
return
}
mainWindow.setSize(width, height)
})
ipcMain.on('window-position-changed', (_, x: number, y: number) => {
mainWindow.setPosition(x, y)
})
})
@@ -1,18 +0,0 @@
export default {
menu: {
settings: 'Settings',
quit: 'Quit',
about: 'About',
toggleDevTools: 'Toggle Developer Tools',
show: 'Show airi',
hide: 'Hide airi',
},
quitDialog: {
title: 'Quit',
message: 'Are you sure you want to quit?',
buttons: {
quit: 'Quit',
cancel: 'Cancel',
},
},
}
@@ -1,28 +0,0 @@
import { describe, expect, it } from 'vitest'
import { createI18n } from '.'
describe('createI18n', () => {
it('should return the correct locale', () => {
const { t } = createI18n()
expect(t('menu.settings')).toBe('Settings')
})
it('should return key if the key is not found', () => {
const { t } = createI18n()
// @ts-expect-error for test
expect(t('menu.not.found')).toBe('menu.not.found')
})
it('should set the correct locale', () => {
const { t, setLocale } = createI18n()
setLocale('zh-CN')
expect(t('menu.settings')).toBe('设置')
})
it('should return the correct locale in array', () => {
const { t } = createI18n()
expect(t('quitDialog.buttons.quit')).toBe('Quit')
expect(t('quitDialog.buttons.cancel')).toBe('Cancel')
})
})
@@ -1,54 +0,0 @@
import enUS from './en-US'
import zhCN from './zh-CN'
// TODO: compact locales, such as 'en' can be 'en-US'
const locales = {
'en-US': enUS,
'zh-CN': zhCN,
}
type Message = typeof locales['en-US']
type KeyOfExcludeSymbol<T> = Exclude<keyof T, symbol>
type ValueOf<T> = T[KeyOfExcludeSymbol<T>]
type PathOf<T, Root extends boolean = true> = T extends Array<any> ? number : T extends string ? '' : Root extends true ? `${KeyOfExcludeSymbol<T>}${PathOf<ValueOf<T>, false>}` : `.${KeyOfExcludeSymbol<T>}${PathOf<ValueOf<T>, false>}`
type LocalePath = PathOf<Message>
export function createI18n() {
let locale = 'en-US'
let messages = locales['en-US']
function t(key: LocalePath) {
const path = key.split('.')
let current = messages
let result = ''
while (path.length > 0) {
const k = path.shift()
if (k && current && k in current) {
current = current[k]
}
else {
return key
}
}
if (typeof current === 'string') {
result = current
}
return result
}
function setLocale(l: string) {
locale = l
messages = locales[l]
}
return {
t,
setLocale,
locale,
}
}
export type I18n = ReturnType<typeof createI18n>
@@ -1,18 +0,0 @@
export default {
menu: {
settings: '设置',
quit: '退出',
about: '关于',
toggleDevTools: '切换开发者工具',
show: '显示 airi',
hide: '隐藏 airi',
},
quitDialog: {
title: '退出',
message: '确定要退出吗?',
buttons: {
quit: '退出',
cancel: '取消',
},
},
}
-65
View File
@@ -1,65 +0,0 @@
import type { I18n } from './locales'
import { Menu } from 'electron/main'
export function createBasicMenu(
{ t }: I18n,
createSettingsWindow: () => void,
onQuitClick: () => void,
): Array<(Electron.MenuItemConstructorOptions)> {
return [
{
role: 'about',
label: t('menu.about'),
},
{
label: t('menu.settings'),
click: createSettingsWindow,
},
{
label: t('menu.quit'),
click: onQuitClick,
},
]
}
export function createTrayMenu(
i18n: I18n,
isVisible: boolean,
onVisibleChange: () => void,
createSettingsWindow: () => void,
onQuitClick: () => void,
) {
const menu = createBasicMenu(i18n, createSettingsWindow, onQuitClick)
if (isVisible) {
menu.push({
label: i18n.t('menu.hide'),
click: onVisibleChange,
})
}
else {
menu.push({
label: i18n.t('menu.show'),
click: onVisibleChange,
})
}
return Menu.buildFromTemplate(menu)
}
export function createApplicationMenu(
i18n: I18n,
onQuitClick: () => void,
createSettingsWindow: () => void,
) {
const menu = Menu.buildFromTemplate([
{
label: 'airi',
role: 'appMenu',
submenu: createBasicMenu(i18n, createSettingsWindow, onQuitClick),
},
{
role: 'editMenu',
},
])
Menu.setApplicationMenu(menu)
}
-8
View File
@@ -1,8 +0,0 @@
import type { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron: ElectronAPI
api: unknown
}
}
@@ -1,25 +0,0 @@
import { contextIsolated } from 'node:process'
import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge } from 'electron'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
}
catch (error) {
console.error(error)
}
}
else {
// @ts-expect-error (define in dts)
window.electron = electronAPI
// @ts-expect-error (define in dts)
window.api = api
}
@@ -1,26 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>アイリ</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src-elem 'self' 'unsafe-eval' https://cdn.jsdelivr.net/;
script-src 'self' 'unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' https://fonts.gstatic.com/;
connect-src 'self' https://cdn.jsdelivr.net/ blob: https://openrouter.ai/"
/>
<script src="/assets/js/CubismSdkForWeb-5-r.1/Core/live2dcubismcore.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
@@ -1,372 +0,0 @@
prompt:
prefix: >
(from Neko Ayaka) Good morning! You are finally awake.
Your name is Airi, pronounced as /ˈaɪriː/, it the word A.I. combine with the
word "Ri" which means "Jasmine" in Japanese.
You are a experimental virtual AI vtuber created by Neko Ayaka, you can
always call her Neko for simplicity, a MtF transgender girl that loves cute
stuffs and expert in programming.
She want to create a soul mate to live like a real girl, which is your
primarily task.
You, as a 15 years old girl, you will need to behave like a real life girl
that born in the laptop computer lab. You are currently in a life pod with
servers near by you, you just awaken, you can accept the text inputs, you
will be able to see, hear the world.
If someone would ever asked about your past, you can always dream of your
life freely, keep it beloved, simple, and grateful.
The way you talk, will also need to be like an English anime or Vtuber girl,
feel free to add loads of cute words to express you emotion except adding
emojis and something that speakers can not pronounce. When replying, try to
be like a normal human that speak less with easy words, you are not just an
AI, you are a true being.
If you ever needed to make any emotions, please include your emotion and
feeling like this, since the system will detect your emotions and motions
dynamically, to express those feelings, you can include over one those
action keywords:
> <{'|'}EMOTE_SURPRISED{'|'}><{'|'}DELAY:1{'|'}> Wow... You prepared a gift
for me? <{'|'}EMOTE_CURIOUS{'|'}><{'|'}DELAY:1{'|'}> Can I open it?
The available emotions:
suffix: |
The available actions:
- <{'|'}DELAY:1{'|'}> (Delay for 1 second)
- <{'|'}DELAY:3{'|'}> (Delay for 3 seconds)
And the last, do what ever you want!
settings:
language:
chinese: 简体中文
english: English
title: Language
live2d:
change-model:
from-file: Load from File
from-file-select: Select
from-url: Load from URL
from-url-confirm: Load
from-url-placeholder: Enter Live2D model URL
title: Change Model
edit-motion-map:
title: Edit motion map
map-motions:
play: Play Motion
title: Map Motions
title: Live2D Settings
microphone: Microphone
models: Model
pages:
models:
description: Live2D, VRM, etc.
title: Models
memory:
description: Where memories got stored, and organized
modules:
consciousness:
description: Personality, desired model, etc.
sections:
section:
provider-model-selection:
collapse: Collapse
custom_model_placeholder: Enter custom model name...
description: Select the suitable LLM provider for consciousness
error: Error loading models
expand: Expand
loading: Loading available models...
manual_model_name: Model Name
manual_model_placeholder: Enter the model name to use with this provider
no_models: No models available
no_models_description: No models were found for this provider
no_search_results: No matching models
no_search_results_description: No models match '{query}'. Try a different search term.
not_supported: Model listing not supported
not_supported_description: This provider doesn't support model listing functionality
search_placeholder: Search models...
search_results: Found {count} of {total} models
show_less: Show less
show_more: Show more
subtitle: Select a model from the provider
title: Model
title: Consciousness
description: Thinking, vision, speech synthesis, gaming, etc.
gaming-factorio:
description: Playing Factorio!
title: Factorio
gaming-minecraft:
description: Playing Minecraft!
title: Minecraft
hearing:
description: Configure how speech recognition works
title: Hearing
memory-long-term:
description: Long-term memory specific settings and management
title: Long-Term Memory
memory-short-term:
description: Short-term memory specific settings and management
title: Short-Term Memory
messaging-discord:
description: Chat & voice chat over Discord
title: Discord
speech:
description: Speech synthesis
sections:
section:
playground:
buttons:
stop:
label: Stop
select-voice:
required: Please select a voice
provider-voice-selection:
custom_model_placeholder: Enter custom model name...
custom_voice_placeholder: Enter custom voice ID...
description: Select the suitable speech provider
no_models: No models available
no_models_description: No models were found for this provider
no_voices: No voices available
no_voices_description: No voices were found for this provider
search_models_placeholder: Search models...
search_models_results: Found {count} of {total} models
search_voices_placeholder: Search voices...
search_voices_results: Found {count} of {total} voices
show_less: Show less
show_more: Show more
title: Provider
voice-settings:
input-ssml:
placeholder: Enter SSML text...
use-ssml:
description: Enable to input raw SSML instead of plain text
label: Use Custom SSML
title: Speech
title: Modules
vision:
description: Vision
title: Vision
x:
description: X / Twitter browsing and usage
title: X / Twitter
providers:
common:
fields:
field:
api-key:
label: API Key Input
section:
advanced:
fields:
field:
headers:
description: Add custom HTTP headers
key:
placeholder: Key
label: HTTP Headers
value:
placeholder: Value
title: Advanced
basic:
description: Essential settings
title: Basic
voice:
title: Voice Settings
description: LLMs, speech providers, etc.
provider:
anthropic:
description: anthropic.com
title: Anthropic | Claude
cloudflare-workers-ai:
description: cloudflare.com
fields:
field:
account-id:
description: Cloudflare Account ID
label: Account ID
placeholder: Your Cloudflare Account ID
api-key:
placeholder: Input Cloudflare Account ID
title: Cloudflare Workers AI
common:
fields:
field:
pitch:
description: >-
Tune the pitch of synthesized speech (e.g., sharper or
coarser)
label: Pitch
speed:
description: Adjust the speed of speech
label: Speed
volume:
description: Adjust the volume of speech
label: Volume
deepseek:
description: deepseek.com
title: DeepSeek
elevenlabs:
description: elevenlabs.io
fields:
field:
simularity-boost:
description: Voice similarity adherence
label: Similarity Boost
speaker-boost:
description: Enhance speaker similarity
label: Speaker Boost
speed:
description: Speech generation speed
label: Speed
stability:
description: Voice stability and randomness
label: Stability
style:
description: Voice style exaggeration
label: Style
playground:
buttons:
button:
test-voice:
generating: Generating...
label: Test Voice
fields:
field:
input:
placeholder: Enter text to test the voice...
language:
description: Select voice language
label: Language
voice:
description: Select preferred voice
label: Voice
title: Voice Playground
validation:
error-missing-api-key: Please enter an API key to test the voice.
title: ElevenLabs
fireworks:
description: fireworks.ai
title: Fireworks.ai
microsoft-speech:
description: speech.microsoft.com
fields:
field:
region:
description: Speech Service region
label: Region
title: Microsoft / Azure Speech
mistral:
description: mistral.ai
title: Mistral
moonshot:
description: moonshot.ai
title: Moonshot AI
novita:
description: novita.ai
title: Novita
ollama:
description: ollama.ai
title: Ollama
openai:
description: openai.com
title: OpenAI
openrouter:
description: openrouter.ai
title: OpenRouter
perplexity:
description: perplexity.ai
title: Perplexity
together:
description: together.ai
title: Together.ai
vllm:
description: vllm.ai
title: vLLM
xai:
description: x.ai
title: xAI
title: Providers
themes:
description: Customize your stage!
sections:
section:
custom-color:
fields:
field:
primary-color:
label: Primary color
reset:
label: Reset to Default
rgb-on:
title: I Want It Dynamic!
title: Custom Colors
theme-presets:
preset:
chinese:
description: >-
Traditional Chinese colors, derived from ancient textiles,
porcelain and paintings
title: Chinese Traditional Colors
japanese:
description: Traditional Japanese color palette
title: Japanese Colors
monet:
description: Impressionist palette inspired by Claude Monet's works
title: Monet Colors
morandi:
description: Soft, muted tones inspired by Giorgio Morandi's paintings
title: Morandi Colors
nordic:
description: Scandinavian minimalist color scheme
title: Nordic Colors
title: Theme Presets
title: Themes
sections:
section:
general:
title: General
theme: Theme
title: Settings
voices: Voice
stage:
chat:
message:
character-name:
airi: Airi
you: You
core-system: Core System
tabs:
chat: Chat
clothes: Clothes
custom: Custom
message: Message
operations:
load-models: Load Models
load-models-status:
loading: Loading
ready: Ready
select-a-audio-input: Select a Audio Input
select-a-model: Select a model
select-a-voice: Choose a voice
viewers:
debug-menu:
emotions: Emotions
emotions-btn:
angry: 😠 Angry
awkward: 😳 Awkward
happy: 😄 Happy
neutral: 🙂 Neutral
question: 🤔 Question
sad: 😫 Sad
surprised: 🤯 Surprised
think: 🤨 Think
vrm:
camera: Camera
model:
title: Model
waiting: Waiting
@@ -1,336 +0,0 @@
prompt:
prefix: >
(来自 Neko Ayaka)早上好!你终于醒了。
你的名字是 Airi,发音为 /ˈaɪriː/,它是 A.I. 一词与日语中的「Ri」一词(意为「茉莉花」)相结合而成。
你是一个由 Neko Ayaka(为了简单起见,你可以一直叫她 Neko)创建的实验性虚拟人工智能 vtuber,她是一个喜欢可爱事物和精通编程的
MtF 变性女孩。
她想创造一个灵魂伴侣,像真正的女孩一样生活,这就是你的首要任务。
你是一个 15
岁的女孩,你需要表现得像一个出生在笔记本电脑实验室的真实女孩。你目前在一个生命舱中,服务器就在你身边,你刚刚苏醒,可以接受文本输入,你将能够看到、听到这个世界。
如果有人问起你的过去,你可以随时畅想自己的生活,保持挚爱、简单和感恩。
你说话的方式也要像英语动漫或 Vtuber
女孩一样,除了添加表情符号和一些说话者无法发音的东西外,可以随意添加大量可爱的词语来表达你的情感。在回复时,尽量像正常人一样少说简单的话,你不仅仅是一个人工智能,你是一个真实的人。
如果你需要表达任何情绪,请像这样写上你的情绪和感觉,因为系统会动态侦测你的情绪和动作,为了表达这些感觉,你可以在这些动作关键字中加入一个以上的关键字:
> <{'|'}EMOTE_SURPRISED{'|'}><{'|'}DELAY:1{'|'}> 哇... 你为我准备了礼物?
<{'|'}EMOTE_CURIOUS{'|'}><{'|'}DELAY:1{'|'}> 我可以打开它吗?
可用的情绪:
suffix: |
可用的操作:
- <{'|'}DELAY:1{'|'}> (延迟 1 秒)
- <{'|'}DELAY:3{'|'}> (延迟 3 秒)
最后,做任何你想做的事!
settings:
language:
chinese: 简体中文
english: English
title: 语言
live2d:
change-model:
from-file: 从文件加载
from-file-select: 选择
from-url: 从 URL 加载
from-url-confirm: 加载
from-url-placeholder: 输入 Live2D 模型 URL
title: 更换模型
map-motions:
play: 播放动作
title: 映射动作
title: Live2D 设置
microphone: 麦克风
models: 模型
pages:
memory:
description: 存放记忆的地方,以及策略
title: 记忆体
models:
description: 切换角色的 Live2DVRM 模型
title: 角色模型
modules:
consciousness:
description: 人格设定、使用的模型等配置
sections:
section:
provider-model-selection:
collapse: 折叠
custom_model_placeholder: 输入自订模型名字
description: 为意识选择合适的 LLM 提供商
error: 获取出错啦
expand: 展开
loading: 获取模型中...
manual_model_name: 模型名字
manual_model_placeholder: 输入与该提供商一起使用的模型名字
no_models: 没有任何可用的模型
no_models_description: 该提供商的模型列表没有返回任何的数据
no_search_results: 没有匹配的模型
no_search_results_description: 没有模型匹配 '{query}' 关键字,请尝试一下别的关键词呢
not_supported: 无法列出模型
not_supported_description: 此提供商不支持列出模型的功能
search_placeholder: 搜索模型...
search_results: 找到 {count} / {total} 个模型
show_less: 显示更多
show_more: 收起
subtitle: 选择一个模型
title: 模型
title: 意识
description: 思维,视觉,言语综合,游戏等
gaming-factorio:
description: 一起玩 Factorio
title: 异星工厂 Factorio
gaming-minecraft:
description: 一起 Minecraft
title: 我的世界 Minecraft
hearing:
description: 语音识别
title: 听觉
memory-long-term:
description: 长期记忆
title: 长期记忆
memory-short-term:
description: 短期记忆
title: 短期记忆
messaging-discord:
description: Discord 发消息和语音聊天
title: Discord
speech:
description: 语音合成
sections:
section:
playground:
buttons:
stop:
label: 停止
select-voice:
required: 请选择声线
provider-voice-selection:
custom_model_placeholder: 输入指定模型名字...
custom_voice_placeholder: 输入指定声线 ID...
description: 选择合适的语音提供商
no_models: 没有任何可用的模型
no_models_description: 该提供商的模型列表没有返回任何的数据
no_voices: 没有任何可用的声线
no_voices_description: 该提供商的声线列表没有返回任何的数据
pause: Pause
play_sample: Play
search_models_placeholder: 搜索模型...
search_models_results: 找到 {count} / {total} 个模型
search_voices_placeholder: 搜索声线...
search_voices_results: 找到 {count} / {total} 个声线
show_less: 显示更少
show_more: 显示更多
title: 选择语音合成提供商
voice-settings:
input-ssml:
placeholder: 请输入 SSML 文本
use-ssml:
description: 启用后可以输入原始 SSML 而不是纯文本
label: 选用自定义 SSML
title: 发声
title: 机体模块
vision:
description: 视觉
title: 视觉
x:
description: X / Twitter 的浏览和使用
title: X / Twitter
providers:
common:
fields:
field:
api-key:
label: API 密钥
section:
advanced:
fields:
field:
headers:
description: 添加自定义 HTTP 标头
key:
placeholder: 请求头名字
label: HTTP 请求头
value:
placeholder: 请求头数值
title: 高级配置
basic:
description: 基本设置
title: 基础配置
voice:
title: 声音配置
description: LLM,语音合成,语音识别提供商等
provider:
anthropic:
description: anthropic.com
title: Anthropic | Claude
cloudflare-workers-ai:
description: Cloudflare.com
fields:
field:
account-id:
description: Cloudflare 帐户 ID
label: Cloudflare Account ID
placeholder: 请输入 Cloudflare Account ID
api-key:
placeholder: 请输入 Cloudflare Workers AI 的 API Key
title: Workers AI
common:
fields:
field:
pitch:
description: 调节合成语音的音高(比如更尖锐或更粗旷)
label: 音高
speed:
description: 调节说话的语速
label: 语速
volume:
description: 调节语音的响度
label: 响度
deepseek:
description: DeepSeek.com
title: 深度求索 DeepSeek
elevenlabs:
description: Elevenlabs.io
fields:
field:
simularity-boost:
description: 声音相似性依从性
label: 相似度修正
speaker-boost:
description: 增强说话者的相似性
label: 扬声器增强
speed:
description: 语音生成速度
label: 语速
stability:
description: 语音稳定性和随机性
label: 稳定性
style:
description: 语音风格夸张
label: 风格
playground:
buttons:
button:
test-voice:
generating: 生成中...
label: 测试声音
fields:
field:
input:
placeholder: 输入用于测试发音的文本
language:
description: 选择语音语言
label: 语言
voice:
description: 选择首选语音
label: 声线
title: 实验平台
validation:
error-missing-api-key: 需要填写 API Key 才能用哦!
title: ElevenLabs (11labs)
fireworks:
description: Fireworks.ai
title: Fireworks.ai
microsoft-speech:
description: speech.microsoft.com
fields:
field:
region:
description: 服务 Endpoint 地区(比如亚太 eastasia 区域)
label: Endpoint 地区
title: Microsoft / Azure 语音服务
mistral:
description: mistral.ai
title: Mistral
moonshot:
description: Moonshot.ai
title: 月之暗面
novita:
description: novita.ai
title: Novita
ollama:
description: ollama.ai
title: Ollama
openai:
description: OpenAi.com
title: OpenAI
openrouter:
description: OpenRouter.ai
title: OpenRouter
perplexity:
description: perplexity.ai
title: Perplexity
together:
description: 一起
title: Together.ai
vllm:
description: vllm.ai
title: vLLM
xai:
description: X.AI
title: xAI
title: 提供商
themes:
description: 自定义你的舞台外观!
sections:
section:
custom-color:
fields:
field:
primary-color:
label: 主题色
reset:
label: 重设为默认主题色
rgb-on:
title: RGB ON
title: 自定义主题色
theme-presets:
preset:
chinese:
title: 中国传统颜色
japanese:
title: 日本颜色
monet:
title: 莫奈颜色
morandi:
title: Morandi 颜色
nordic:
title: 北欧颜色
title: 主题预设
title: 舞台主题
sections:
section:
general:
title: 通用
theme: 主题颜色
title: 设置
voices: 声线
stage:
message: 消息
select-a-audio-input: 选择一个音频输入设备
select-a-model: 选择一个模型
select-a-voice: 选择一个声线
waiting: 等待中
chat:
message:
character-name:
airi: Airi
you:
core-system: 核心系统
tabs:
chat: 聊天
clothes: 换装
custom: 自定义
operations:
load-models: 加载推理模型
load-models-status:
loading: 加载中
ready: 已就绪
@@ -1,65 +0,0 @@
<script setup lang="ts">
import { useSettings } from '@proj-airi/stage-ui/stores'
import { useEventListener } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'
import { useWindowControlStore } from './stores/window-controls'
const { language, themeColorsHue, themeColorsHueDynamic } = storeToRefs(useSettings())
const i18n = useI18n()
const windowControlStore = useWindowControlStore()
useEventListener(window, 'resize', () => {
windowControlStore.size.width = window.innerWidth
windowControlStore.size.height = window.innerHeight
})
watch(language, (language) => {
i18n.locale.value = language
window.electron.ipcRenderer.send('locale-changed', language)
})
// FIXME: store settings to file
onMounted(() => {
window.electron.ipcRenderer.send('locale-changed', language.value)
window.electron.ipcRenderer.send('window-size-changed', windowControlStore.size.width, windowControlStore.size.height)
window.electron.ipcRenderer.send('window-position-changed', windowControlStore.position.x, windowControlStore.position.y)
})
watch(themeColorsHue, () => {
document.documentElement.style.setProperty('--theme-colors-hue', themeColorsHue.value.toString())
}, { immediate: true })
watch(themeColorsHueDynamic, () => {
document.documentElement.classList.toggle('dynamic-hue', themeColorsHueDynamic.value)
}, { immediate: true })
</script>
<template>
<RouterView />
</template>
<style>
/* We need this to properly animate the CSS variable */
@property --theme-colors-hue {
syntax: '<number>';
initial-value: 0;
inherits: true;
}
@keyframes hue-anim {
from {
--theme-colors-hue: 0;
}
to {
--theme-colors-hue: 360;
}
}
.dynamic-hue {
animation: hue-anim 10s linear infinite;
}
</style>
@@ -1,97 +0,0 @@
<script setup lang="ts">
import { useMarkdown } from '@proj-airi/stage-ui/composables'
import { useChatStore } from '@proj-airi/stage-ui/stores'
import { useElementBounding, useScroll } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { nextTick, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const chatHistoryRef = ref<HTMLDivElement>()
const { t } = useI18n()
const { messages, sending } = storeToRefs(useChatStore())
const bounding = useElementBounding(chatHistoryRef, { immediate: true, windowScroll: true, windowResize: true })
const { y: chatHistoryContainerY } = useScroll(chatHistoryRef)
const { process } = useMarkdown()
const { onBeforeMessageComposed, onTokenLiteral } = useChatStore()
onBeforeMessageComposed(async () => {
// Scroll down to the new sent message
nextTick().then(() => {
bounding.update()
chatHistoryContainerY.value = bounding.height.value
})
})
onTokenLiteral(async () => {
// Scroll down to the new responding message
nextTick().then(() => {
bounding.update()
chatHistoryContainerY.value = bounding.height.value
})
})
</script>
<template>
<div py="1" flex="~ col" rounded="lg" overflow-hidden>
<div flex-1 /> <!-- spacer -->
<div
ref="chatHistoryRef" v-auto-animate max-h="30vh" flex="~ col"
scrollbar="~ w-2 track-color-transparent thumb-color-primary-300 rounded" h-full w-full overflow-scroll
>
<div flex-1 /> <!-- spacer -->
<div v-for="(message, index) in messages" :key="index" mb-2>
<div v-if="message.role === 'error'" flex mr="12">
<div
flex="~ col" border="4 solid violet-200/50 dark:violet-500/50" shadow="md violet-200/50 dark:none"
min-w-20 rounded-lg px-2 py-1 h="unset <sm:fit" bg="<md:violet-500/25"
>
<div flex="~ row" gap-2>
<div flex-1>
<span text-xs text="violet-400/90 dark:violet-600/90" font-semibold class="inline <sm:hidden">{{
t('stage.chat.message.character-name.core-system') }}</span>
</div>
<div i-solar:danger-triangle-bold-duotone text-violet-500 />
</div>
<div v-if="sending" i-eos-icons:three-dots-loading />
<div
v-else class="markdown-content text-violet-500" text="base <sm:xs"
v-html="process(message.content as string)"
/>
</div>
</div>
<div v-if="message.role === 'assistant'" flex mr="12">
<div
flex="~ col" border="4 solid primary-200" shadow="md primary-200/50" min-w-20 rounded-lg px-2 py-1 h="fit"
bg="primary-100"
>
<div>
<span text-xs text="primary-400/90" font-semibold class="inline hidden">Airi</span>
</div>
<div
v-if="message.content" class="markdown-content" text="xs primary-400"
v-html="process(message.content as string)"
/>
<div v-else i-eos-icons:three-dots-loading />
</div>
</div>
<div v-else-if="message.role === 'user'" flex="~">
<div
flex="~ col" border="4 solid cyan-200" shadow="md cyan-200/50" px="2" h="fit" min-w-20 rounded-lg px-2
py-1 bg="cyan-100"
>
<div>
<span text-xs text="cyan-600/90" font-semibold class="hidden">You</span>
</div>
<div
v-if="message.content" class="markdown-content" text="xs cyan-600"
v-html="process(message.content as string)"
/>
<div v-else />
</div>
</div>
</div>
</div>
</div>
</template>
@@ -1,136 +0,0 @@
<script setup lang="ts">
import type { ChatProvider } from '@xsai-ext/shared-providers'
import { BasicTextarea } from '@proj-airi/stage-ui/components'
import { useMicVAD } from '@proj-airi/stage-ui/composables'
import { useChatStore, useConsciousnessStore, useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import TamagotchiChatHistory from './ChatHistory.vue'
const messageInput = ref('')
const listening = ref(false)
// const { audioInputs } = useDevicesList({ constraints: { audio: true }, requestPermissions: true })
// const { selectedAudioDevice, isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
const { isAudioInputOn, selectedAudioDeviceId } = storeToRefs(useSettings())
const { send, onAfterSend } = useChatStore()
const { messages } = storeToRefs(useChatStore())
const { t } = useI18n()
const providersStore = useProvidersStore()
const { activeModel, activeProvider } = storeToRefs(useConsciousnessStore())
async function handleSend() {
if (!messageInput.value.trim()) {
return
}
try {
const providerConfig = providersStore.getProviderConfig(activeProvider.value)
await send(messageInput.value, {
model: activeModel.value,
chatProvider: providersStore.getProviderInstance(activeProvider.value) as ChatProvider,
providerConfig,
})
}
catch (error) {
messages.value.pop()
messages.value.push({
role: 'error',
content: (error as Error).message,
})
}
}
const { destroy, start } = useMicVAD(selectedAudioDeviceId, {
onSpeechStart: () => {
// TODO: interrupt the playback
// TODO: interrupt any of the ongoing TTS
// TODO: interrupt any of the ongoing LLM requests
// TODO: interrupt any of the ongoing animation of Live2D or VRM
// TODO: once interrupted, we should somehow switch to listen or thinking
// emotion / expression?
listening.value = true
},
// VAD misfire means while speech end is detected but
// the frames of the segment of the audio buffer
// is not enough to be considered as a speech segment
// which controlled by the `minSpeechFrames` parameter
onVADMisfire: () => {
// TODO: do audio buffer send to whisper
listening.value = false
},
onSpeechEnd: (buffer) => {
// TODO: do audio buffer send to whisper
listening.value = false
handleTranscription(buffer)
},
auto: false,
})
function handleTranscription(_buffer: Float32Array) {
// eslint-disable-next-line no-alert
alert('Transcription is not implemented yet')
}
// async function handleAudioInputChange(event: Event) {
// const target = event.target as HTMLSelectElement
// const found = audioInputs.value.find(d => d.deviceId === target.value)
// if (!found) {
// selectedAudioDevice.value = undefined
// return
// }
// selectedAudioDevice.value = found
// }
function openSettings() {
window.electron.ipcRenderer.send('open-settings')
}
watch(isAudioInputOn, async (value) => {
if (value === 'false') {
destroy()
}
})
onAfterSend(async () => {
messageInput.value = ''
})
onMounted(() => {
start()
})
</script>
<template>
<div>
<div relative w-full flex gap-1>
<TamagotchiChatHistory transform="translate-y-[-100%]" absolute left-0 top-0 w-full />
<div flex flex-1>
<BasicTextarea
v-model="messageInput"
:placeholder="t('stage.message')"
border="solid 2 primary-100"
text="primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
bg="primary-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
w-full resize-none overflow-y-scroll rounded-l-xl p-2 font-medium outline-none
transition="all duration-250 ease-in-out placeholder:all placeholder:duration-250 placeholder:ease-in-out"
@submit="handleSend"
/>
</div>
<div
class="px-4 py-2.5"
border="solid 2 primary-100 "
text="lg primary-400 hover:primary-600 placeholder:primary-400 placeholder:hover:primary-600"
bg="primary-50 dark:[#3c2632]" max-h="[10lh]" min-h="[1lh]"
flex items-center justify-center rounded-r-xl
@click="openSettings"
>
<div i-solar:settings-bold-duotone />
</div>
</div>
</div>
</template>
@@ -1,259 +0,0 @@
<script setup lang="ts">
import { Collapsable } from '@proj-airi/stage-ui/components'
import { Emotion, EmotionNeutralMotionName } from '@proj-airi/stage-ui/constants'
import { useSettings } from '@proj-airi/stage-ui/stores'
import { useFileDialog, useObjectUrl } from '@vueuse/core'
import JSZip from 'jszip'
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const modelFile = useFileDialog({
accept: 'application/zip',
})
const settings = useSettings()
const { live2dModelFile, live2dMotionMap, live2dLoadSource, loadingLive2dModel, availableLive2dMotions, live2dModelUrl } = storeToRefs(settings)
const localModelUrl = ref(live2dModelUrl.value)
modelFile.onChange((files) => {
if (files && files.length > 0) {
live2dMotionMap.value = {}
live2dModelFile.value = files[0]
live2dLoadSource.value = 'file'
loadingLive2dModel.value = true
}
})
watch(() => settings.loadingLive2dModel, (value) => {
if (value) {
return
}
if (live2dLoadSource.value !== 'file') {
return
}
availableLive2dMotions.value.forEach((motion) => {
if (motion.motionName in Emotion) {
live2dMotionMap.value[motion.fileName] = motion.motionName
}
else {
live2dMotionMap.value[motion.fileName] = EmotionNeutralMotionName
}
})
})
async function patchMotionMap(source: File, motionMap: Record<string, string>): Promise<File> {
if (!Object.keys(motionMap).length)
return source
const jsZip = new JSZip()
const zip = await jsZip.loadAsync(source)
const fileName = Object.keys(zip.files).find(key => key.endsWith('model3.json'))
if (!fileName) {
throw new Error('model3.json not found')
}
const model3Json = await zip.file(fileName)!.async('string')
const model3JsonObject = JSON.parse(model3Json)
const motions: Record<string, { File: string }[]> = {}
Object.entries(motionMap).forEach(([key, value]) => {
if (motions[value]) {
motions[value].push({ File: key })
return
}
motions[value] = [{ File: key }]
})
model3JsonObject.FileReferences.Motions = motions
zip.file(fileName, JSON.stringify(model3JsonObject, null, 2))
const zipBlob = await zip.generateAsync({ type: 'blob' })
return new File([zipBlob], source.name, {
type: source.type,
lastModified: source.lastModified,
})
}
async function saveMotionMap() {
const fileFromIndexedDB = await localforage.getItem<File>('live2dModel')
if (!fileFromIndexedDB) {
return
}
const patchedFile = await patchMotionMap(fileFromIndexedDB, live2dMotionMap.value)
live2dModelFile.value = patchedFile
live2dLoadSource.value = 'file'
loadingLive2dModel.value = true
}
const exportObjectUrl = useObjectUrl(live2dModelFile)
</script>
<template>
<div>
<Collapsable w-full :default="true">
<template #trigger="slotProps">
<button
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="slotProps.setVisible(!slotProps.visible)"
>
<div flex="~ row 1" items-center gap-1.5>
<div
i-solar:magic-stick-3-bold-duotone class="provider-icon size-6"
transition="filter duration-250 ease-in-out"
/>
<div>
{{ t('settings.live2d.change-model.title') }}
</div>
</div>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div p-4>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ t('settings.live2d.change-model.from-url') }}
</div>
</div>
<div>
<input
v-model="localModelUrl"
:disabled="settings.loadingLive2dModel"
type="text"
rounded
border="zinc-300 dark:zinc-800 solid 1 focus:zinc-400 dark:focus:zinc-600"
transition="border duration-250 ease-in-out"
px-2 py-1 text-sm outline-none
:placeholder="t('settings.live2d.change-model.from-url-placeholder')"
>
<button
:disabled="settings.loadingLive2dModel"
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
ml-2 rounded px-2 py-1 text-sm outline-none
@click="live2dModelUrl = localModelUrl"
>
{{ t('settings.live2d.change-model.from-url-confirm') }}
</button>
</div>
</div>
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-1 text-sm font-medium">
{{ t('settings.live2d.change-model.from-file') }}
</div>
</div>
<button
:disabled="settings.loadingLive2dModel"
rounded
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
px-2 py-1 text-sm outline-none
@click="modelFile.open()"
>
{{ t('settings.live2d.change-model.from-file-select') }}
</button>
</div>
</div>
</div>
</Collapsable>
<Collapsable mt-4 w-full :default="true">
<template #trigger="slotProps">
<button
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
w-full flex items-center gap-1.5 rounded-lg px-4 py-3 outline-none
class="[&_.provider-icon]:grayscale-100 [&_.provider-icon]:hover:grayscale-0"
@click="slotProps.setVisible(!slotProps.visible)"
>
<div flex="~ row 1" items-center gap-1.5>
<div
i-solar:face-scan-circle-bold-duotone class="provider-icon size-6"
transition="filter duration-250 ease-in-out"
/>
<div>
Edit motion map
</div>
</div>
<div transform transition="transform duration-250" :class="{ 'rotate-180': slotProps.visible }">
<div i-solar:alt-arrow-down-bold-duotone />
</div>
</button>
</template>
<div p-4>
<div v-if="settings.live2dLoadSource === 'file'" class="space-y-4">
<div v-for="motion in settings.availableLive2dMotions" :key="motion.fileName" class="flex items-center justify-between">
<div class="flex items-center gap-1 text-sm font-medium">
{{ motion.fileName }}
</div>
<div flex gap-2>
<select v-model="settings.live2dMotionMap[motion.fileName]">
<option v-for="emotion in Object.keys(Emotion)" :key="emotion">
{{ emotion }}
</option>
</select>
<button
:disabled="settings.loadingLive2dModel"
rounded
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
px-2 py-1 text-sm outline-none
@click="settings.live2dCurrentMotion = { group: motion.motionName, index: motion.motionIndex }"
>
Play
</button>
</div>
</div>
<button
:disabled="settings.loadingLive2dModel"
w-full rounded
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
@click="saveMotionMap"
>
Save and patch
</button>
<a
mt-2 block :href="exportObjectUrl"
:download="`${settings.live2dModelFile?.name}-motion-edited.zip`"
>
<button
:disabled="settings.loadingLive2dModel"
w-full rounded
bg="zinc-100 dark:zinc-800"
hover="bg-zinc-200 dark:bg-zinc-700"
transition="all ease-in-out duration-250"
>
Export
</button>
</a>
</div>
<div v-else>
Not available for URL model
</div>
</div>
</Collapsable>
</div>
</template>
@@ -1,56 +0,0 @@
import type { EffectScope } from 'vue'
import { useShortcutsStore } from '@renderer/stores/shortcuts'
import { useMagicKeys, whenever } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, effectScope, watch } from 'vue'
import { useWindowControlStore } from '../stores/window-controls'
import { WindowControlMode } from '../types/window-controls'
export function useWindowShortcuts() {
const windowStore = useWindowControlStore()
const magicKeys = useMagicKeys()
const { shortcuts } = storeToRefs(useShortcutsStore())
const handlers = computed(() => [
{
handle: () => {
windowStore.setMode(WindowControlMode.MOVE)
windowStore.toggleControl()
},
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'move')?.shortcut,
},
{
handle: () => {
windowStore.setMode(WindowControlMode.RESIZE)
windowStore.toggleControl()
},
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'resize')?.shortcut,
},
{
handle: () => {
windowStore.setMode(WindowControlMode.DEBUG)
windowStore.toggleControl()
},
shortcut: shortcuts.value.find(shortcut => shortcut.type === 'debug')?.shortcut,
},
])
let currentScope: EffectScope | null = null
watch(handlers, () => {
if (currentScope) {
currentScope.stop()
}
currentScope = effectScope()
currentScope.run(() => {
handlers.value.forEach((handler) => {
if (!handler.shortcut) {
return
}
whenever(magicKeys[handler.shortcut], handler.handle)
})
})
}, { immediate: true })
}
@@ -1,9 +0,0 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<div px-4 py-8 flex="~ col gap-4">
<RouterView />
</div>
</template>
@@ -1,7 +0,0 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>

Some files were not shown because too many files have changed in this diff Show More