svector-sdk
Version:
Official JavaScript and TypeScript SDK for accessing SVECTOR APIs.
376 lines (375 loc) • 15.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Vision = void 0;
class Vision {
constructor(client) {
this.client = client;
}
async makeVisionRequest(chatRequest, options) {
const baseURL = this.client.baseURL;
const endpoints = [
`${baseURL}/api/chat/completions`,
'https://spec-chat.tech/api/chat/completions',
'https://spec-chat.tech/api/chat/completions'
];
const headers = {
'Authorization': `Bearer ${this.client.apiKey}`,
'Content-Type': 'application/json',
...(options?.headers || {})
};
const requestBody = JSON.stringify(chatRequest);
const timeout = options?.timeout || 60000;
const maxRetries = 2;
for (const [endpointIndex, endpoint] of endpoints.entries()) {
for (let retry = 0; retry < maxRetries; retry++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: requestBody,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
if (response.status >= 400 && response.status < 500) {
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
if (response.status >= 500) {
const isLastRetry = retry === maxRetries - 1;
const isLastEndpoint = endpointIndex === endpoints.length - 1;
if (isLastRetry && isLastEndpoint) {
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
continue;
}
if (retry === maxRetries - 1) {
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
continue;
}
return await response.json();
}
catch (error) {
const isLastRetry = retry === maxRetries - 1;
const isLastEndpoint = endpointIndex === endpoints.length - 1;
if (error instanceof Error) {
if (error.name === 'AbortError' || error.message.includes('fetch')) {
if (!isLastRetry || !isLastEndpoint) {
const delay = Math.min(1000 * Math.pow(2, retry), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}
if (isLastRetry && isLastEndpoint) {
throw new Error(`Vision API request failed: ${error.message}`);
}
}
}
}
}
throw new Error('Vision API request failed after multiple retries');
}
async analyze(params, options) {
const { image_url, image_base64, file_id, prompt, model, max_tokens, temperature, detail } = params;
if (!image_url && !image_base64 && !file_id) {
throw new Error('Must provide one of: image_url, image_base64, or file_id');
}
const messageContent = [
{
type: 'text',
text: prompt || 'Analyze this image and describe what you see in detail.'
}
];
if (image_url) {
messageContent.push({
type: 'image_url',
image_url: {
url: image_url,
detail: detail || 'auto'
}
});
}
else if (image_base64) {
const dataUrl = image_base64.startsWith('data:')
? image_base64
: `data:image/jpeg;base64,${image_base64}`;
messageContent.push({
type: 'image_url',
image_url: {
url: dataUrl,
detail: detail || 'auto'
}
});
}
else if (file_id) {
messageContent.push({
type: 'image_url',
image_url: {
url: `file://${file_id}`,
detail: detail || 'auto'
}
});
}
const chatRequest = {
model: model || 'spec-3-turbo',
messages: [{
role: 'user',
content: messageContent
}],
max_tokens: max_tokens || 1000,
temperature: temperature || 0.7
};
try {
const response = await this.makeVisionRequest(chatRequest, {
...options,
timeout: options?.timeout || 120000
});
const analysis = response.choices?.[0]?.message?.content;
if (!analysis) {
throw new Error('No analysis content returned from API');
}
return {
analysis,
usage: response.usage,
_request_id: response._request_id
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
if (errorMessage.includes('504')) {
throw new Error(`Vision analysis failed: Gateway timeout. Please try again later.`);
}
else if (errorMessage.includes('413')) {
throw new Error(`Vision analysis failed: Image too large. Please use a smaller image.`);
}
else if (errorMessage.includes('401')) {
throw new Error(`Vision analysis failed: Authentication failed. Please check your API key.`);
}
else if (errorMessage.includes('429')) {
throw new Error(`Vision analysis failed: Rate limit exceeded. Please wait before retrying.`);
}
throw new Error(`Vision analysis failed: ${errorMessage}`);
}
}
async analyzeFromUrl(imageUrl, prompt, options) {
return this.analyze({
image_url: imageUrl,
prompt,
...options
});
}
async analyzeFromBase64(base64Data, prompt, options) {
return this.analyze({
image_base64: base64Data,
prompt,
...options
});
}
async analyzeFromFileId(fileId, prompt, options) {
return this.analyze({
file_id: fileId,
prompt,
...options
});
}
async compareImages(images, prompt, options) {
const messageContent = [
{
type: 'text',
text: prompt || 'Compare these images and describe the similarities and differences.'
}
];
for (const image of images) {
if (image.url) {
messageContent.push({
type: 'image_url',
image_url: {
url: image.url,
detail: options?.detail || 'auto'
}
});
}
else if (image.base64) {
const dataUrl = image.base64.startsWith('data:')
? image.base64
: `data:image/jpeg;base64,${image.base64}`;
messageContent.push({
type: 'image_url',
image_url: {
url: dataUrl,
detail: options?.detail || 'auto'
}
});
}
else if (image.file_id) {
messageContent.push({
type: 'image_url',
image_url: {
url: `file://${image.file_id}`,
detail: options?.detail || 'auto'
}
});
}
}
const chatRequest = {
model: options?.model || 'spec-3-turbo',
messages: [{
role: 'user',
content: messageContent
}],
max_tokens: options?.max_tokens || 1000,
temperature: options?.temperature || 0.7
};
try {
const response = await this.makeVisionRequest(chatRequest);
return {
analysis: response.choices?.[0]?.message?.content || 'No analysis generated',
usage: response.usage,
_request_id: response._request_id
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new Error(`Image comparison failed: ${errorMessage}`);
}
}
async extractText(params, options) {
return this.analyze({
...params,
prompt: 'Extract and transcribe all text visible in this image. Return only the text content, maintaining the original formatting where possible.'
}, options);
}
async describeForAccessibility(params, options) {
return this.analyze({
...params,
prompt: 'Provide a detailed description of this image suitable for screen readers and accessibility purposes. Include information about colors, layout, text, people, objects, and any other relevant visual elements.'
}, options);
}
async detectObjects(params, objectTypes, options) {
const objectList = objectTypes?.length
? objectTypes.join(', ')
: 'people, vehicles, animals, furniture, electronics, and other significant objects';
return this.analyze({
...params,
prompt: `Identify and list all instances of the following objects in this image: ${objectList}. For each object, provide its location, size, and any relevant details.`
}, options);
}
async create(params, options) {
const analysisParams = {
image_url: params.image_url,
image_base64: params.image_base64,
file_id: params.file_id,
prompt: params.prompt,
model: params.model,
max_tokens: params.max_tokens,
temperature: params.temperature,
detail: params.detail
};
const response = await this.analyze(analysisParams, options);
return {
analysis: response.analysis,
usage: response.usage,
_request_id: response._request_id
};
}
async createResponse(params, options) {
const userMessage = params.input.find(msg => msg.role === 'user');
if (!userMessage) {
throw new Error('User message is required');
}
let prompt = '';
let imageInput = {};
for (const content of userMessage.content) {
if (content.type === 'input_text' && content.text) {
prompt += content.text + ' ';
}
else if (content.type === 'input_image') {
if (content.image_url) {
if (content.image_url.startsWith('data:')) {
imageInput.image_base64 = content.image_url;
}
else {
imageInput.image_url = content.image_url;
}
}
else if (content.file_id) {
imageInput.file_id = content.file_id;
}
}
}
const result = await this.analyze({
...imageInput,
prompt: prompt.trim(),
model: params.model,
max_tokens: params.max_tokens,
temperature: params.temperature
}, options);
return {
output_text: result.analysis,
usage: result.usage,
_request_id: result._request_id
};
}
async batchAnalyze(images, options) {
const results = [];
const delay = options?.delay || 1000;
for (const [index, image] of images.entries()) {
try {
const result = await this.analyze({
...image,
model: options?.model,
max_tokens: options?.max_tokens,
temperature: options?.temperature,
detail: options?.detail
});
results.push({
analysis: result.analysis,
usage: result.usage
});
if (index < images.length - 1) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
results.push({
analysis: '',
error: errorMessage
});
}
}
return results;
}
async analyzeWithConfidence(params, options) {
const enhancedParams = {
...params,
prompt: (params.prompt || 'Analyze this image') +
' Please also provide a confidence score (0-100) for your analysis at the end in the format: [Confidence: XX%]'
};
const result = await this.analyze(enhancedParams, options);
const confidenceMatch = result.analysis.match(/\[Confidence:\s*(\d+)%\]/);
const confidence = confidenceMatch ? parseInt(confidenceMatch[1]) : undefined;
const cleanAnalysis = result.analysis.replace(/\[Confidence:\s*\d+%\]/, '').trim();
return {
...result,
analysis: cleanAnalysis,
confidence
};
}
async generateCaption(params, style, options) {
const stylePrompts = {
professional: 'Generate a professional, informative caption for this image suitable for business or educational content.',
casual: 'Write a casual, friendly caption for this image that would work well on social media.',
funny: 'Create a humorous, entertaining caption for this image that would get engagement on social media.',
technical: 'Provide a detailed, technical description of this image suitable for academic or scientific purposes.'
};
return this.analyze({
...params,
prompt: stylePrompts[style || 'casual']
}, options);
}
}
exports.Vision = Vision;