UNPKG

@aituber-onair/core

Version:

Core library for AITuber OnAir providing voice synthesis and chat processing

842 lines (636 loc) 29.2 kB
# AITuber OnAir Core ![AITuber OnAir Core - logo](https://raw.githubusercontent.com/shinshin86/aituber-onair/refs/heads/main/packages/core/images/aituber-onair-core.png) [AITuber OnAir Core](https://www.npmjs.com/package/@aituber-onair/core) is a TypeScript library developed to provide functionality for the [AITuber OnAir](https://aituberonair.com) web service, designed for AI-based virtual streaming (AITuber). [日本語版はこちら](https://github.com/shinshin86/aituber-onair/blob/main/packages/core/README_ja.md) While it is primarily intended to provide functionality for [AITuber OnAir](https://aituberonair.com), this project is published as open-source software and is available as an [npm package](https://www.npmjs.com/package/@aituber-onair/core) under the MIT License. It specializes in generating response text and audio from text or image inputs, and is designed to easily integrate with other parts of an application (storage, YouTube integration, avatar control, etc.). ## Table of Contents - [Overview](#overview) - [Installation](#installation) - [Main Features](#main-features) - [Basic Usage](#basic-usage) - [Architecture](#architecture) - [Main Components](#main-components) - [Event System](#event-system) - [Supported Speech Engines](#supported-speech-engines) - [AI Provider System](#ai-provider-system) - [Memory & Persistence](#memory--persistence) - [Examples](#examples) - [Integration with Existing Applications](#integration-with-existing-applications) - [Testing & Development](#testing--development) ## Overview **AITuberOnAirCore** is the central module that provides core features for AI tubers. It forms the core of the AITuber OnAir application. It encapsulates complex AI response generation, conversation context management, speech synthesis, and more, making these features available through a simple API. ## Installation You can install AITuber OnAir Core using npm: ```bash npm install @aituber-onair/core ``` Or using yarn: ```bash yarn add @aituber-onair/core ``` Or using pnpm: ```bash pnpm install @aituber-onair/core ``` ## Main Features - **AI Response Generation from Text Input** Generates natural responses to user text input using OpenAI GPT models. - **AI Response Generation from Images (Vision)** Generates AI responses based on recognized content from images (e.g., live broadcast screens). - **Conversation Context Management & Memory** Maintains long-running conversation context via short-, mid-, and long-term memory systems. - **Text-to-Speech Conversion** Compatible with multiple speech engines (VOICEVOX, VoicePeak, NijiVoice, AivisSpeech, OpenAI TTS). - **Emotion Extraction & Processing** Extracts emotion from AI responses and utilizes it for speech synthesis or avatar expressions. - **Event-Driven Architecture** Emits events at each stage of processing to simplify external integrations. - **Customizable Prompts** Allows customization of prompts for vision processing and conversation summarization. - **Pluggable Persistence** Memory features can be persisted via LocalStorage, IndexedDB, or other customizable methods. ## Basic Usage Below is a simplified example of how to use **AITuber OnAir Core**: ```typescript import { AITuberOnAirCore, AITuberOnAirCoreEvent, AITuberOnAirCoreOptions } from '@aituber-onair/core'; // 1. Define options const options: AITuberOnAirCoreOptions = { chatProvider: 'openai', // Optional. If omitted, the default OpenAI will be used. apiKey: 'YOUR_API_KEY', chatOptions: { systemPrompt: 'You are an AI streamer. Act as a cheerful and friendly live broadcaster.', visionSystemPrompt: 'Please comment like a streamer on what is shown on screen.', visionPrompt: 'Look at the broadcast screen and provide commentary suited to the situation.', memoryNote: 'This is a summary of past conversations. Please refer to it appropriately to continue the conversation.', }, // OpenAI Default model is gpt-4o-mini // You can specify different models for text chat and vision processing // model: 'o3-mini', // Lightweight model for text chat (no vision support) // visionModel: 'gpt-4o', // Model capable of image processing memoryOptions: { enableSummarization: true, shortTermDuration: 60 * 1000, // 1 minute midTermDuration: 4 * 60 * 1000, // 4 minutes longTermDuration: 9 * 60 * 1000, // 9 minutes maxMessagesBeforeSummarization: 20, maxSummaryLength: 256, // You can specify a custom summarization prompt summaryPromptTemplate: 'Please summarize the following conversation in under {maxLength} characters. Include important points.' }, voiceOptions: { engineType: 'voicevox', // Speech engine type speaker: '1', // Speaker ID apiKey: 'ENGINE_SPECIFIC_API_KEY', // If required (e.g., NijiVoice) onComplete: () => console.log('Voice playback completed'), // Custom API endpoint URLs (optional) voicevoxApiUrl: 'http://custom-voicevox-server:50021', voicepeakApiUrl: 'http://custom-voicepeak-server:20202', aivisSpeechApiUrl: 'http://custom-aivis-server:10101', }, debug: true, // Enable debug output }; // 2. Create an instance const aituber = new AITuberOnAirCore(options); // 3. Set up event listeners aituber.on(AITuberOnAirCoreEvent.PROCESSING_START, () => { console.log('Processing started'); }); aituber.on(AITuberOnAirCoreEvent.ASSISTANT_PARTIAL, (text) => { // Receive streaming responses and display in UI console.log(`Partial response: ${text}`); }); aituber.on(AITuberOnAirCoreEvent.ASSISTANT_RESPONSE, (data) => { const { message, screenplay, rawText } = data; console.log(`Complete response: ${message.content}`); console.log(`Original text with emotion tags: ${rawText}`); if (screenplay.emotion) { console.log(`Emotion: ${screenplay.emotion}`); } }); aituber.on(AITuberOnAirCoreEvent.SPEECH_START, (data) => { // The SPEECH_START event includes the screenplay object and rawText if (data && data.screenplay) { console.log(`Speech playback started: emotion = ${data.screenplay.emotion || 'neutral'}`); console.log(`Original text with emotion tags: ${data.rawText}`); } else { console.log('Speech playback started'); } }); aituber.on(AITuberOnAirCoreEvent.SPEECH_END, () => { console.log('Speech playback finished'); }); aituber.on(AITuberOnAirCoreEvent.ERROR, (error) => { console.error('Error occurred:', error); }); // 4. Process text input await aituber.processChat('Hello, how is the weather today?'); // 5. Clear event listeners if needed aituber.offAll(); ``` ## Architecture **AITuberOnAirCore** is designed with the following layered structure: ``` AITuberOnAirCore (Integration Layer) ├── ChatProcessor (Conversation handling) │ └── ChatService (AI Chat) ├── MemoryManager (Memory handling) │ └── Summarizer (Summarization) └── VoiceService (Speech processing) └── VoiceEngineAdapter (Speech Engine Interface) └── Various Speech Engines (VOICEVOX, NijiVoice, etc.) ``` ### Directory Structure The source code is organized around the following directory structure: ``` src/ ├── constants/ # Constants and configuration │ ├── index.ts # Exported constants │ └── prompts.ts # Default prompts and templates ├── core/ # Core components │ ├── AITuberOnAirCore.ts │ ├── ChatProcessor.ts │ └── MemoryManager.ts ├── services/ # Service implementations │ ├── chat/ # Chat services │ │ ├── ChatService.ts # Base interface │ │ ├── ChatServiceFactory.ts # Factory for providers │ │ └── providers/ # AI provider implementations │ │ ├── ChatServiceProvider.ts # Provider interface │ │ ├── claude/ # Claude-specific │ │ │ ├── ClaudeChatService.ts │ │ │ ├── ClaudeChatServiceProvider.ts │ │ │ └── ClaudeSummarizer.ts │ │ ├── gemini/ # Gemini-specific │ │ │ ├── GeminiChatService.ts │ │ │ ├── GeminiChatServiceProvider.ts │ │ │ └── GeminiSummarizer.ts │ │ └── openai/ # OpenAI-specific │ │ ├── OpenAIChatService.ts │ │ ├── OpenAIChatServiceProvider.ts │ │ └── OpenAISummarizer.ts │ ├── voice/ # Voice services │ │ ├── VoiceService.ts │ │ ├── VoiceEngineAdapter.ts │ │ └── engines/ # Voice engine implementations │ └── youtube/ # YouTube API integration │ └── YouTubeDataApiService.ts # YouTube Data API client ├── types/ # TypeScript type definitions └── utils/ # Utilities and helpers ├── screenplay.ts # Text and emotion processing └── storage.ts # Storage utilities ``` ## Main Components ### AITuberOnAirCore This is the overall integration class, responsible for initializing and coordinating other components. It extends `EventEmitter` and emits events at various processing stages. In most cases, you will interact primarily with this class to use its features. **Main methods** include: - `processChat(text)` – Process text input - `processVisionChat(imageDataUrl, visionPrompt?)` – Process image input (optionally pass a custom prompt) - `stopSpeech()` – Stop speech playback - `getChatHistory()` – Retrieve chat history - `setChatHistory(messages)` – Set chat history from external source (e.g., for replay or migration) - `clearChatHistory()` – Clear chat history - `updateVoiceService(options)` – Update speech settings - `isMemoryEnabled()` – Check if memory functionality is enabled - `offAll()` – Remove all event listeners ### ChatProcessor The component that sends text input to an AI model (e.g., OpenAI GPT) and receives responses. It manages the conversation flow and supports streaming responses. It also handles emotion extraction from responses. - `updateOptions(newOptions)` – Allows you to update settings at runtime ### MemoryManager **MemoryManager is designed to prevent issues such as API token limits, increased costs, and slow responses that can occur when the chat log grows too large. When a certain time or message threshold is exceeded, older chat history is summarized and stored as short-, mid-, and long-term memory. This allows recent conversation to be sent as-is, while past context is provided as a summary, maintaining context for the AI while keeping API requests efficient.** Handles conversational context. In long conversations, older messages are summarized and maintained as short-term (1 min), mid-term (4 min), and long-term (9 min) memory. This helps maintain consistency in AI responses. - **Custom Settings**: - `summaryPromptTemplate` can be customized for summarization (it uses a `{maxLength}` placeholder). ### VoiceService Converts text to speech. It integrates with multiple external speech synthesis engines through the `VoiceEngineAdapter`. #### speakTextWithOptions Method The `AITuberOnAirCore` class provides a flexible `speakTextWithOptions` method for speech playback: ```typescript // Example of speaking text with temporary settings await aituberOnairCore.speakTextWithOptions('[happy] Hello, everyone watching!', { // Enable or disable avatar animation enableAnimation: true, // Temporarily override current speech settings temporaryVoiceOptions: { engineType: 'voicevox', speaker: '8', apiKey: 'YOUR_API_KEY' // If required }, // Specify the ID of the HTML audio element for playback audioElementId: 'custom-audio-player' }); ``` **Key Features**: 1. **Temporary Voice Settings**: Override current speech settings without permanently changing them. 2. **Animation Control**: Control avatar animation with the `enableAnimation` option. 3. **Flexible Audio Playback**: Play audio in a specified HTML audio element. 4. **Automatic Emotion Extraction**: Extract emotion tags (e.g., `[happy]`) from text and provide them in the `SPEECH_START` event. ## Event System **AITuberOnAirCore** emits the following events: - `PROCESSING_START`: When processing begins - `PROCESSING_END`: When processing finishes - `ASSISTANT_PARTIAL`: Upon receiving partial responses from the assistant (streaming) - `ASSISTANT_RESPONSE`: Upon receiving a complete response (includes a screenplay object and rawText with emotion tags) - `SPEECH_START`: When speech playback starts (includes a screenplay object with emotion and rawText with emotion tags) - `SPEECH_END`: When speech playback ends - `ERROR`: When an error occurs ### Safely Handling Event Data In particular, when implementing a listener for the `SPEECH_START` event, it is recommended to check if data is present: ```typescript // Safe handling of SPEECH events aituber.on(AITuberOnAirCoreEvent.SPEECH_START, (data) => { if (!data) { console.log('No data available'); return; } const screenplay = data.screenplay; if (!screenplay) { console.log('No screenplay object'); return; } const emotion = screenplay.emotion || 'neutral'; console.log(`Speech started: Emotion = ${emotion}`); // Get original text with emotion tags console.log(`Original text: ${data.rawText}`); // Update UI or avatar animation updateUIWithEmotion(emotion); }); ``` ### Emotion Handling In a React application, you might use `useRef` to store the latest emotion data for immediate access: ```typescript // Example in a React component const [currentEmotion, setCurrentEmotion] = useState('neutral'); const emotionRef = useRef({ emotion: 'neutral', text: '' }); useEffect(() => { if (aituberOnairCore) { aituberOnairCore.on(AITuberOnAirCoreEvent.SPEECH_START, (data) => { if (data?.screenplay?.emotion) { setCurrentEmotion(data.screenplay.emotion); emotionRef.current = data.screenplay; } }); } }, [aituberOnairCore]); // Use the ref for animation callbacks const handleAnimation = () => { const emotion = emotionRef.current.emotion || 'neutral'; // Perform animation based on emotion }; ``` ### ChatProcessor Events The internal `ChatProcessor` emits additional events: - `chatLogUpdated`: Fired when the chat log is updated (e.g., when new messages are added or history is cleared). You can access this event by referencing the `ChatProcessor` instance directly: ```typescript // Example: using the chatLogUpdated event in ChatProcessor const aituber = new AITuberOnAirCore(options); const chatProcessor = aituber['chatProcessor']; // Accessing internal component chatProcessor.on('chatLogUpdated', (chatLog) => { console.log('Chat log updated:', chatLog); // Example: Update UI updateChatDisplay(chatLog); // Example: Sync with an external system syncChatToExternalSystem(chatLog); }); ``` Possible use cases for `chatLogUpdated` include: 1. **Real-Time Chat UI Updates** Reflect new messages or cleared logs in the UI immediately. 2. **External System Integration** Save chat logs to a database or send them to an analytics service. 3. **Debugging & Monitoring** Monitor changes in the chat log during development. ## Supported Speech Engines **AITuberOnAirCore** supports the following speech engines: - **VOICEVOX**: High-quality Japanese speech synthesis engine. - **VoicePeak**: Speech synthesis engine with rich emotional expression. - **NijiVoice**: AI-based speech synthesis service (requires an API key). - **AivisSpeech**: Speech synthesis using AI technology. - **OpenAI TTS**: Text-to-speech API from OpenAI. You can dynamically switch the speech engine via `updateVoiceService`: ```typescript // Example of switching speech engines aituber.updateVoiceService({ engineType: 'nijivoice', speaker: 'some-speaker-id', apiKey: 'YOUR_NIJIVOICE_API_KEY' }); ``` ### Custom API Endpoints For locally hosted voice engines (VOICEVOX, VoicePeak, AivisSpeech), you can specify custom API endpoint URLs: ```typescript // Example of setting custom API endpoints aituber.updateVoiceService({ engineType: 'voicevox', speaker: '1', // Custom endpoint for a self-hosted or alternative VOICEVOX server voicevoxApiUrl: 'http://custom-voicevox-server:50021' }); // Example for VoicePeak aituber.updateVoiceService({ engineType: 'voicepeak', speaker: '2', voicepeakApiUrl: 'http://custom-voicepeak-server:20202' }); // Example for AivisSpeech aituber.updateVoiceService({ engineType: 'aivisSpeech', speaker: '3', aivisSpeechApiUrl: 'http://custom-aivis-server:10101' }); ``` This is useful when running voice engines on different ports or remote servers. ## AI Provider System AITuber OnAir Core adopts an extensible provider system, enabling integration with various AI APIs. Currently, OpenAI API, Gemini API, and Claude API are available. If you would like to use any other API, please submit a PR or send us a message. ### Available Providers Currently, the following AI provider is built-in: - **OpenAI**: Supports models like GPT-4.1(including mini and nano), GPT-4, GPT-4o-mini, O3-mini, o1, o1-mini - **Gemini**: Supports models like Gemini 2.0 Flash, Gemini 2.0 Flash-Lite, Gemini 1.5 Flash, Gemini 1.5 Pro, Gemini 2.5 Pro(Experimental) - **Claude**: Supports models like Claude 3 Haiku, Claude 3.5 Haiku, Claude 3.5 Sonnet v2, Claude 3.7 Sonnet ### Specifying a Provider You can specify the provider when instantiating `AITuberOnAirCore`: ```typescript const aituberCore = new AITuberOnAirCore({ chatProvider: 'openai', // Provider name apiKey: 'your-api-key', model: 'gpt-4o-mini', // Optional (if omitted, the default model 'gpt-4o-mini' will be used) // Other options... }); ``` ### Model-Specific Feature Limitations Different AI models support different features. For example: - **GPT-4o**, **GPT-4o-mini**: Support both text chat and image processing (Vision) - **O3-mini**: Supports text chat only (does not support image processing) When selecting a model, be aware of these limitations. Attempting to use unsupported features will result in an explicit error. **Note**: If you don't specify a model, the default model used is 'gpt-4o-mini'. This model supports both text chat and image processing. ### Using Different Models Together If you want to use different models for text chat and image processing, you can use the `visionModel` option: ```typescript const aituberCore = new AITuberOnAirCore({ apiKey: 'your-api-key', chatProvider: 'openai', model: 'o3-mini', // For text chat visionModel: 'gpt-4o', // For image processing // Other options... }); ``` This allows for optimizations such as using a lightweight model for text chat and a more powerful model only when image processing is needed. Note: When specifying a visionModel, ensure it supports vision capabilities. The system will validate this during initialization and throw an error if an unsupported model is provided. ### Retrieving Providers & Models You can programmatically retrieve available providers and their supported models: ```typescript // Get all available providers const providers = AITuberOnAirCore.getAvailableProviders(); // Get supported models for a specific provider const models = AITuberOnAirCore.getSupportedModels('openai'); ``` ### Creating a Custom Provider To add a new AI provider, implement the `ChatServiceProvider` interface in a custom class and register it with the `ChatServiceFactory`: ```typescript import { ChatServiceFactory } from 'aituber-onair-core'; import { MyCustomProvider } from './MyCustomProvider'; // Register the custom provider ChatServiceFactory.registerProvider(new MyCustomProvider()); // Use the registered provider const aituberCore = new AITuberOnAirCore({ chatProvider: 'myCustomProvider', apiKey: 'your-api-key', // Other options... }); ``` ## Memory & Persistence **AITuberOnAirCore** includes a memory feature that maintains the context of long-running conversations. The AI summarizes older messages, preserving short-, mid-, and long-term context for more coherent responses. ### Memory Types There are three types of memory: 1. **Short-Term Memory** - Generated **1 minute** after the conversation starts - Holds recent conversation details 2. **Mid-Term Memory** - Generated **4 minutes** after the conversation starts - Holds slightly broader summaries of the conversation 3. **Long-Term Memory** - Generated **9 minutes** after the conversation starts - Holds key themes and important information from the overall conversation These memory records are automatically included in the AI prompts, helping the AI respond consistently over time. ### Memory Persistence AITuberOnAirCore has a pluggable design for memory persistence, so that the conversation context can be retained even if the application is restarted. #### MemoryStorage Interface Persistence is provided through the abstract `MemoryStorage` interface: ```typescript interface MemoryStorage { load(): Promise<MemoryRecord[]>; save(records: MemoryRecord[]): Promise<void>; clear(): Promise<void>; } ``` #### Default Implementations 1. **LocalStorageMemoryStorage** - Uses the browser's LocalStorage - Simple solution (subject to storage limits) 2. **IndexedDBMemoryStorage** (Planned) - Uses the browser's IndexedDB - Supports larger capacity and more complex data structures #### Custom Storage Implementations To create your own storage implementation, simply implement the `MemoryStorage` interface: ```typescript class CustomMemoryStorage implements MemoryStorage { async load(): Promise<MemoryRecord[]> { // Load records from a custom storage return customStorage.getItems(); } async save(records: MemoryRecord[]): Promise<void> { // Save records to a custom storage await customStorage.setItems(records); } async clear(): Promise<void> { // Clear records in a custom storage await customStorage.clear(); } } ``` ### Configuring the Memory Feature Enable the memory feature and set up persistence when initializing **AITuberOnAirCore**: ```typescript import { AITuberOnAirCore } from './lib/aituber-onair-core'; import { createMemoryStorage } from './lib/aituber-onair-core/utils/storage'; // Create a memory storage (LocalStorage example) const memoryStorage = createMemoryStorage('myapp.aiMemoryRecords'); // Initialize AITuberOnAirCore const aiTuber = new AITuberOnAirCore({ // Other options... // Memory options memoryOptions: { enableSummarization: true, shortTermDuration: 60 * 1000, // 1 minute (ms) midTermDuration: 4 * 60 * 1000, // 4 minutes longTermDuration: 9 * 60 * 1000, // 9 minutes maxMessagesBeforeSummarization: 20, maxSummaryLength: 256, memoryRetentionPeriod: 60 * 60 * 1000, // 1 hour }, // Memory storage memoryStorage: memoryStorage, }); ``` ### Memory-Related Events The memory feature triggers the following events: - `memoriesLoaded`: When memory is loaded from storage - `memoryCreated`: When a new memory record is created - `memoriesRemoved`: When a memory record is deleted - `memoriesSaved`: When memory records are saved to storage - `storageCleared`: When the storage is cleared These events are emitted by the `MemoryManager` instance internally, so you typically need a reference to the internal component to use them. ### Memory Cleanup Over time, memory records may grow and consume storage space. **AITuberOnAirCore** automatically removes old memories beyond the set retention period (default is 1 hour). - `cleanupOldMemories` is invoked automatically during user input processing. - You can manually trigger a cleanup if necessary. ```typescript // Clear both chat history and memory aiTuber.clearChatHistory(); // Or access the memory manager directly (not recommended for production) const memoryManager = aiTuber['memoryManager']; if (memoryManager) { await memoryManager.cleanupOldMemories(); } ``` ## Examples ### Vision (Image) Input Processing ```typescript // Obtain image data URL (e.g., via camera capture) const imageDataUrl = captureScreenshot(); // Basic vision processing with default prompt await aituber.processVisionChat(imageDataUrl); // Vision processing with a custom prompt await aituber.processVisionChat( imageDataUrl, 'Analyze the broadcast screen and provide entertaining comments for viewers.' ); ``` ### Custom Summarization Prompts ```typescript // Using a custom summarization prompt const aiTuberCore = new AITuberOnAirCore({ openAiKey: 'your_api_key', chatOptions: { /* ... */ }, memoryOptions: { enableSummarization: true, // Other memory settings summaryPromptTemplate: 'Please summarize the following conversation in under {maxLength} characters, highlighting the key points.', }, }); ``` ### Synchronized Speech Playback ```typescript // Example of waiting for speech playback to finish (using handleSpeakAi) async function playSequentially() { // Wait for the listener's speech playback await handleSpeakAi( listenerScreenplay, listenerVoiceType, listenerSpeaker, openAiKey ); console.log('Listener speech playback has finished'); // AI avatar response await aituber.processChat('Hello, any updates on the show so far?'); } ``` ## Integration with Existing Applications AITuberOnAirCore can be integrated into existing applications relatively easily. For example: 1. Initialize with relevant API keys or settings at application startup. 2. Set up event listeners to handle various stages of processing. 3. Call the appropriate methods (`processChat`, `processVisionChat`, etc.) when a user or vision input occurs. ```typescript // Example in App.tsx useEffect(() => { // If AITuberOnAirCore is already initialized, set up event listeners if (aituberOnairCore) { // Clear old listeners aituberOnairCore.offAll(); // Register new listeners aituberOnairCore.on(AITuberOnAirCoreEvent.PROCESSING_START, () => { setChatProcessing(true); setAssistantMessage('Loading...'); }); aituberOnairCore.on(AITuberOnAirCoreEvent.ASSISTANT_PARTIAL, (text) => { setAssistantMessage((prev) => { if (prev === 'Loading...') return text; return prev + text; }); }); // Other event listeners... } }, [aituberOnairCore]); ``` In real-world applications, you might update the speech engine settings when the user changes preferences, toggle the memory feature on or off, and so on. Though optimized for AITuber OnAir, it's flexible enough to be embedded into custom AITuber apps. ## Testing & Development **AITuberOnAirCore** includes a comprehensive test suite to ensure quality and stability. ### Test Structure Tests are organized in the following directory structure: ``` tests/ ├── core/ # Tests for core components ├── services/ # Tests for services (speech, chat, etc.) ├── utils/ # Tests for utility functions └── README.md # Detailed info on the test structure ``` ### Naming Conventions - Test files use the `.test.ts` suffix (e.g., `AITuberOnAirCore.test.ts`). - There should be a corresponding test file for each source file. ### Running Tests The test framework uses **Vitest**: ```bash # Navigate to the AITuberOnAirCore root directory cd src/lib/aituber-onair-core # Run all tests npm test # Watch mode (automatically reruns tests on file changes) npm run test:watch # Generate coverage report npm run test:coverage ``` ### Writing Tests Follow these guidelines: 1. Use the Arrange-Act-Assert pattern. 2. Properly mock external dependencies. 3. Keep tests isolated and independent. 4. Test both success and error cases. **Example**: ```typescript import { describe, it, expect } from 'vitest'; import { AITuberOnAirCore } from '../../core/AITuberOnAirCore'; describe('AITuberOnAirCore', () => { describe('constructor', () => { it('initializes properly with valid options', () => { // Arrange const options = { /* ... */ }; // Act const instance = new AITuberOnAirCore(options); // Assert expect(instance).toBeDefined(); }); }); }); ``` ### Coverage Requirements Particularly high test coverage is sought for: - Core functionality - Public APIs - Edge cases - Error handling ### Setting Up the Development Environment You will need: 1. Node.js (version 20 or higher) 2. npm (version 10 or higher) ```bash # Install dependencies npm install # Run the test suite npm test ```