UNPKG

@aituber-onair/core

Version:

Core library for AITuber OnAir providing voice synthesis and chat processing

237 lines 9.29 kB
import { MODEL_CLAUDE_3_HAIKU, CLAUDE_VISION_SUPPORTED_MODELS } from '../../constants'; import { Anthropic } from '@anthropic-ai/sdk'; /** * Claude implementation of ChatService */ export class ClaudeChatService { /** * Constructor * @param apiKey Anthropic API key * @param model Name of the model to use * @param visionModel Name of the vision model */ constructor(apiKey, model = MODEL_CLAUDE_3_HAIKU, visionModel = MODEL_CLAUDE_3_HAIKU) { /** Provider name */ this.provider = 'claude'; this.apiKey = apiKey; this.model = model || MODEL_CLAUDE_3_HAIKU; this.visionModel = visionModel || MODEL_CLAUDE_3_HAIKU; this.anthropic = new Anthropic({ apiKey: this.apiKey, dangerouslyAllowBrowser: true // CORSエラーを回避するための設定 }); } /** * Get the current model name * @returns Model name */ getModel() { return this.model; } /** * Process chat messages * @param messages Array of messages to send * @param onPartialResponse Callback to receive each part of streaming response * @param onCompleteResponse Callback to execute when response is complete */ async processChat(messages, onPartialResponse, onCompleteResponse) { try { // Extract system message (if any) and regular messages const systemMessage = messages.find(msg => msg.role === 'system'); const nonSystemMessages = messages.filter(msg => msg.role !== 'system'); // Convert messages to Claude format const claudeMessages = this.convertMessagesToClaudeFormat(nonSystemMessages); // Create message stream using Anthropic SDK const stream = await this.anthropic.messages.create({ model: this.model, messages: claudeMessages, system: systemMessage?.content || '', stream: true, max_tokens: 1000, }); let fullText = ''; // Process streaming response for await (const chunk of stream) { if (chunk.type === 'content_block_delta' && chunk.delta && 'text' in chunk.delta) { const deltaText = chunk.delta.text || ''; if (deltaText) { fullText += deltaText; onPartialResponse(deltaText); } } } // Complete response callback await onCompleteResponse(fullText); } catch (error) { console.error('Error in processChat:', error); throw error; } } /** * Process chat messages with images * @param messages Array of messages to send (including images) * @param onPartialResponse Callback to receive each part of streaming response * @param onCompleteResponse Callback to execute when response is complete * @throws Error if the selected model doesn't support vision */ async processVisionChat(messages, onPartialResponse, onCompleteResponse) { try { // Check if the vision model supports vision capabilities if (!CLAUDE_VISION_SUPPORTED_MODELS.includes(this.visionModel)) { throw new Error(`Model ${this.visionModel} does not support vision capabilities.`); } // Extract system message (if any) and regular messages const systemMessage = messages.find(msg => msg.role === 'system'); const nonSystemMessages = messages.filter(msg => msg.role !== 'system'); // Convert messages to Claude vision format const claudeMessages = this.convertVisionMessagesToClaudeFormat(nonSystemMessages); // Create message stream using Anthropic SDK const stream = await this.anthropic.messages.create({ model: this.visionModel, messages: claudeMessages, // @ts-ignore system: systemMessage?.content || '', stream: true, max_tokens: 1000, }); let fullText = ''; // Process streaming response for await (const chunk of stream) { if (chunk.type === 'content_block_delta' && chunk.delta && 'text' in chunk.delta) { const deltaText = chunk.delta.text || ''; if (deltaText) { fullText += deltaText; onPartialResponse(deltaText); } } } // Complete response callback await onCompleteResponse(fullText); } catch (error) { console.error('Error in processVisionChat:', error); throw error; } } /** * Convert AITuber OnAir messages to Claude format * @param messages Array of messages * @returns Claude formatted messages */ convertMessagesToClaudeFormat(messages) { return messages.map(msg => ({ role: this.mapRoleToClaude(msg.role), content: msg.content, })); } /** * Convert AITuber OnAir vision messages to Claude format * @param messages Array of vision messages * @returns Claude formatted vision messages */ convertVisionMessagesToClaudeFormat(messages) { return messages.map(msg => { // If message content is a string, create a text-only message if (typeof msg.content === 'string') { return { role: this.mapRoleToClaude(msg.role), content: [ { type: 'text', text: msg.content, }, ], }; } // If message content is an array of blocks, convert each block else if (Array.isArray(msg.content)) { const content = msg.content.map(block => { if (block.type === 'text') { return { type: 'text', text: block.text, }; } else if (block.type === 'image_url') { // データURLかどうかをチェック if (block.image_url.url.startsWith('data:')) { // Data URLからBase64部分を抽出 const matches = block.image_url.url.match(/^data:([A-Za-z-+/]+);base64,(.+)$/); if (matches && matches.length >= 3) { const mediaType = matches[1]; const base64Data = matches[2]; return { type: 'image', source: { type: 'base64', media_type: mediaType, data: base64Data } }; } } // 通常のURLの場合 return { type: 'image', source: { type: 'url', url: block.image_url.url, media_type: this.getMimeTypeFromUrl(block.image_url.url), }, }; } return null; }).filter(item => item !== null); return { role: this.mapRoleToClaude(msg.role), content, }; } return { role: this.mapRoleToClaude(msg.role), content: [], }; }); } /** * Map AITuber OnAir roles to Claude roles * @param role AITuber OnAir role * @returns Claude role */ mapRoleToClaude(role) { switch (role) { case 'system': // Claude handles system messages separately, but we'll map it anyway return 'system'; case 'user': return 'user'; case 'assistant': return 'assistant'; default: return 'user'; } } /** * Get MIME type from URL * @param url Image URL * @returns MIME type */ getMimeTypeFromUrl(url) { const extension = url.split('.').pop()?.toLowerCase(); switch (extension) { case 'jpg': case 'jpeg': return 'image/jpeg'; case 'png': return 'image/png'; case 'gif': return 'image/gif'; case 'webp': return 'image/webp'; default: return 'image/jpeg'; } } } //# sourceMappingURL=ClaudeChatService.js.map