@aituber-onair/core
Version:
Core library for AITuber OnAir providing voice synthesis and chat processing
237 lines • 9.29 kB
JavaScript
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