AIRI:开源的虚拟伙伴容器,让AI角色走进现实

0 阅读4分钟

Project AIRI Banner

Project AIRI

Discord License X (Twitter)

AIRI 是一个雄心勃勃的开源项目,旨在创建一个能够承载 AI 虚拟角色(如 VTuber)的“灵魂容器”。受 Neuro-sama 启发,AIRI 不仅仅是一个聊天机器人,而是一个完整的框架,让开发者可以构建、部署和交互具备个性、视觉表现和多种能力的 AI 角色。

功能特性

  • 多平台支持 (Multi-Platform):采用模块化架构,支持多种运行环境。
    • 桌面端 (Desktop):基于 Electron 构建,提供原生应用体验。
    • Web端 (Web):基于 Vue 3 的单页应用,方便快速访问和演示。
    • 移动端 (Mobile):使用 Capacitor 打包,可构建 iOS 和 Android 应用。
  • 丰富的角色系统 (Rich Character System):角色不仅包含对话能力,还拥有完整的资料、配置和互动状态。
    • 角色资料:包含名称、描述、头像、封面等基础信息。
    • 能力配置 (Capabilities):支持配置 LLM(大语言模型)、TTS(文本转语音)、VLM(视觉语言模型)和 ASR(语音识别)等多种 AI 能力。
    • 角色互动:实现了对角色点赞、收藏、分叉(Fork)等社交功能,并统计互动数据。
  • 语音交互 (Voice Interaction):内置音频输入、录制和语音活动检测 (VAD) 功能。
    • 音频输入管理:通过 useAudioInputuseAudioRecord 等 Composables,轻松管理麦克风设备、获取用户媒体流和录制音频。
    • 语音活动检测 (VAD):集成了基于 Silero VAD 模型的处理器,能够实时检测用户语音起止,优化对话体验。
  • 实时通信与同步 (Real-time Communication & Sync):前后端采用多种机制保证数据实时性和一致性。
    • Eventa IPC/RPC:桌面端通过自定义的 @moeru/eventa 库实现高效、类型安全的进程间通信。
    • 聊天同步 (Chat Sync):前端可以与服务端同步聊天记录、成员和消息,支持多种聊天类型(私聊、群聊)和角色(用户、AI角色)。
  • 高度可扩展性 (High Extensibility)
    • 插件系统 (Plugin System):定义了清晰的 Manifest 规范 (manifestV1Schema),允许开发者通过插件扩展 Electron 主进程和渲染进程的功能。插件可以声明自己的能力(Capability),并被宿主应用发现和管理。
    • MCP 服务器集成 (MCP Server Integration):支持管理和运行 Model Context Protocol (MCP) 服务器。通过 StdioClientTransport 与 MCP 服务器通信,可以动态获取并调用服务器提供的工具(Tools),极大地扩展了 AI 角色的能力边界。
  • 开发者友好的配置与工具 (Dev-friendly Configuration & Tools)
    • 统一的代码规范:通过 @moeru/eslint-config 统一管理 ESLint 规则,保证代码质量。
    • 现代化的前端技术栈:Web 和桌面端渲染进程采用 Vue 3、Vite、Pinia、UnoCSS 等前沿技术,开发体验流畅。
    • 数据库集成:服务端使用 Drizzle ORM 配合 PostgreSQL,提供类型安全的数据库操作。Schema 定义清晰,并支持迁移。

安装指南

系统要求

  • Node.js (最新稳定版)
  • pnpm (包管理器)
  • 根据目标平台,可能需要相应的开发环境:
    • 桌面端 (Electron):Windows、macOS 或 Linux 构建工具链。
    • 移动端 (Capacitor):Android Studio (Android) 或 Xcode (iOS)。

分步安装

  1. 克隆仓库

    git clone https://github.com/moeru-ai/airi.git
    cd airi
    
  2. 安装依赖 项目是一个 monorepo,使用 pnpm 管理依赖。

    pnpm install
    
  3. 环境配置 根据需要,为特定的应用或服务配置环境变量。例如,Web 端可能需要设置 PostHog 的 API Key (在 posthog.config.ts 中查看或覆盖)。

    # 复制并修改环境变量示例文件(如果有)
    # cp apps/stage-web/.env.example apps/stage-web/.env
    
  4. 启动开发服务器 你可以启动不同平台的开发版本。

    # 启动 Web 应用
    pnpm --filter stage-web dev
    
    # 启动 Electron 桌面应用
    pnpm --filter stage-tamagotchi dev
    
    # 启动服务端
    pnpm --filter server dev
    

使用说明

基础使用示例:创建一个角色 (服务端 API)

以下示例展示了如何使用 AIRI 服务端的 API 创建一个新角色。这需要服务端已运行,并通过了认证。

// 假设你有一个认证后的用户 token
const createCharacter = async (authToken: string) => {
  const response = await fetch('http://localhost:your_server_port/api/characters', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${authToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      character: {
        version: '1.0.0',
        coverUrl: 'https://example.com/cover.jpg',
        characterId: 'my-ai-assistant', // 角色的唯一标识符
        name: 'Aria', // 通过 i18n 字段传递
      },
      i18n: [
        {
          language: 'en',
          name: 'Aria',
          description: 'A helpful and friendly AI assistant.',
          tags: ['assistant', 'friendly'],
        },
      ],
      capabilities: [
        {
          type: 'llm',
          config: {
            apiKey: 'your-llm-api-key',
            apiBaseUrl: 'https://api.openai.com/v1',
            llm: {
              model: 'gpt-3.5-turbo',
              temperature: 0.7,
            },
          },
        },
        {
          type: 'tts',
          config: {
            apiKey: 'your-tts-api-key',
            apiBaseUrl: 'your-tts-endpoint',
            tts: {
              voiceId: 'en-US-AriaNeural',
              speed: 1.0,
              pitch: 1.0,
            },
          },
        },
      ],
      // 可选:配置角色的 3D/2D 模型
      avatarModels: [
        {
          type: 'vrm',
          config: {
            vrm: {
              urls: ['https://example.com/model.vrm'],
            },
          },
        },
      ],
    }),
  });

  if (response.ok) {
    const newCharacter = await response.json();
    console.log('Character created:', newCharacter);
  } else {
    console.error('Failed to create character', await response.text());
  }
};

典型使用场景:前端实时显示鼠标位置 (Electron)

在 Electron 渲染进程中,你可以使用 Eventa 提供的 API 轻松获取并显示主进程中的屏幕光标位置。

<script setup lang="ts">
import { cursorScreenPoint } from '@proj-airi/eventa'
import { useElectronInvoke, useElectronSubscription } from '@proj-airi/electron-vueuse'
import { ref } from 'vue'

// 调用主进程方法,开始循环发送光标位置
useElectronInvoke(startLoopGetCursorScreenPoint, [])

const cursorPos = ref({ x: 0, y: 0 })
// 订阅主进程发送的光标位置更新
useElectronSubscription(cursorScreenPoint, (point) => {
  cursorPos.value = point
})
</script>

<template>
  <div>
    当前鼠标位置: X: {{ cursorPos.x }}, Y: {{ cursorPos.y }}
  </div>
</template>

核心代码

1. 角色服务 (Character Service) - 业务逻辑核心

此模块处理与角色相关的所有核心业务逻辑,如查找、创建、更新角色,以及处理点赞和收藏等互动。

// apps/server/src/services/characters.ts (简化版)
import type { Database } from '../libs/db'
import { and, eq, isNull, sql } from 'drizzle-orm'
import * as schema from '../schemas/characters'
import * as userCharacterSchema from '../schemas/user-character'

export function createCharacterService(db: Database) {
  return {
    // 根据ID查找角色,并关联加载其能力、i18n、封面等数据
    async findById(id: string) {
      return await db.query.character.findFirst({
        where: and(eq(schema.character.id, id), isNull(schema.character.deletedAt)),
        with: { capabilities: true, avatarModels: true, i18n: true, prompts: true, likes: true, bookmarks: true, cover: true, },
      })
    },

    // 处理用户对角色点赞/取消点赞
    async like(userId: string, characterId: string) {
      return await db.transaction(async (tx) => {
        // 检查是否已经点赞
        const existing = await tx.query.characterLikes.findFirst({
          where: and(eq(userCharacterSchema.characterLikes.userId, userId), eq(userCharacterSchema.characterLikes.characterId, characterId)),
        })

        if (existing) {
          // 已点赞,则取消点赞
          await tx.delete(userCharacterSchema.characterLikes)
            .where(and(eq(userCharacterSchema.characterLikes.userId, userId), eq(userCharacterSchema.characterLikes.characterId, characterId)))
          await tx.update(schema.character).set({ likesCount: sql`${schema.character.likesCount} - 1` }).where(eq(schema.character.id, characterId))
          return { liked: false }
        } else {
          // 未点赞,则添加点赞
          await tx.insert(userCharacterSchema.characterLikes).values({ userId, characterId })
          await tx.update(schema.character).set({ likesCount: sql`${schema.character.likesCount} + 1` }).where(eq(schema.character.id, characterId))
          return { liked: true }
        }
      })
    },

    // ... 其他方法:findAll, create, update, delete, bookmark 等
  }
}

2. 语音活动检测 (VAD) - 前端交互核心

该模块负责在前端实时处理音频流,判断用户何时开始和结束说话。

// packages/stage-ui/libs/audio/vad/vad.ts (简化版)
import type { PreTrainedModel } from '@huggingface/transformers'
import { AutoModel, Tensor } from '@huggingface/transformers'

export class VAD {
  private model: PreTrainedModel | undefined
  private state: Tensor
  private sampleRateTensor: Tensor
  private buffer: Float32Array
  // ... 其他属性和构造函数

  /**
   * 初始化并加载 Silero VAD 模型
   */
  public async initialize(): Promise<void> {
    try {
      this.emit('status', { type: 'info', message: 'Loading VAD model...' })
      // 从 Hugging Face 加载预训练的 Silero VAD 模型 (ONNX 格式)
      this.model = await AutoModel.from_pretrained('onnx-community/silero-vad', {
        config: { model_type: 'custom' } as any,
        dtype: 'fp32',
      })
      this.isReady = true
      this.emit('status', { type: 'info', message: 'VAD model loaded successfully' })
    } catch (error) {
      // 错误处理...
    }
  }

  /**
   * 处理输入的音频块,返回是否检测到语音的概率
   */
  public async process(audioChunk: Float32Array): Promise<number | null> {
    if (!this.model || !this.isReady) return null

    // 将音频块转换为模型所需的 Tensor 格式
    const inputTensor = new Tensor('float32', audioChunk, [1, audioChunk.length])
    // 执行模型推理
    const output = await this.model({ input: inputTensor, sr: this.sampleRateTensor, state: this.state, sr: this.sampleRateTensor })
    this.state = output.stateNew // 更新内部状态
    const speechProb = output.output[0].data[0] // 获取语音概率
    return speechProb
  }
  // ... 其他方法
}

3. MCP 服务器管理 - 能力扩展核心

此代码展示了如何在 Electron 主进程中管理 MCP 服务器,包括启动、停止和调用其工具。

// apps/stage-tamagotchi-electron/src/services/airi/mcp-servers.ts (简化版)
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import type { ElectronMcpStdioServerConfig } from '../../../shared/eventa'

interface McpServerSession {
  client: Client
  transport: StdioClientTransport
  config: ElectronMcpStdioServerConfig
}

export class McpStdioManager {
  private servers: Map<string, McpServerSession> = new Map()

  // 启动或重启一个 MCP 服务器
  async startServer(name: string, config: ElectronMcpStdioServerConfig): Promise<void> {
    await this.stopServer(name) // 停止已存在的实例

    const client = new Client({ name: `mcp-client-${name}`, version: '1.0.0' })
    const transport = new StdioClientTransport({
      command: config.command,
      args: config.args,
      env: config.env,
      cwd: config.cwd,
    })

    await client.connect(transport)
    this.servers.set(name, { client, transport, config })
    console.info(`MCP server "${name}" started.`)
  }

  // 列出所有已连接 MCP 服务器提供的工具
  async listTools(): Promise<any[]> {
    const allTools = []
    for (const [name, session] of this.servers.entries()) {
      if (!session.config.enabled) continue
      try {
        const result = await session.client.listTools()
        allTools.push(...result.tools.map(tool => ({ ...tool, serverName: name })))
      } catch (error) {
        console.error(`Failed to list tools from ${name}:`, error)
      }
    }
    return allTools
  }

  // 调用特定服务器的特定工具
  async callTool(serverName: string, toolName: string, args: any): Promise<any> {
    const session = this.servers.get(serverName)
    if (!session) throw new Error(`Server "${serverName}" not found`)
    const result = await session.client.callTool({ name: toolName, arguments: args })
    return result
  }

  // ... 其他方法:stopAll, getRuntimeStatus 等
}

nXsqf0J/5YTRWD2RR/QZbIcCSjkalHy+DDXL7orjx3E=