Skip to main content
Glama
clipsense
by clipsense
client.ts11 kB
/** * ClipSense API Client * * Handles communication with ClipSense backend for video analysis. */ import axios, { AxiosInstance } from "axios"; import { createReadStream, statSync } from "fs"; import { basename } from "path"; const API_BASE_URL = "https://api.clipsense.app/api/v1"; const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB export class ClipSenseClient { private client: AxiosInstance; private apiKey: string; constructor(apiKey: string) { this.apiKey = apiKey; this.client = axios.create({ baseURL: API_BASE_URL, headers: { Authorization: `Bearer ${apiKey}`, }, timeout: 300000, // 5 minutes (video analysis takes time) }); } /** * Analyze a video file */ async analyzeVideo( videoPath: string, question: string ): Promise<{ jobId: string; analysis: string }> { // Validate file exists and get stats let stats; try { stats = statSync(videoPath); } catch (error: any) { if (error.code === 'ENOENT') { throw new Error( `Video file not found: ${videoPath}\n\n` + `Please check:\n` + ` • The file path is correct\n` + ` • You have permission to read the file\n` + ` • The file exists at the specified location` ); } throw new Error( `Failed to access video file: ${error.message}\n\n` + `Path: ${videoPath}` ); } // Validate it's a file, not a directory if (!stats.isFile()) { throw new Error( `Path is not a file: ${videoPath}\n\n` + `Please provide a path to a video file, not a directory.` ); } // Validate file size if (stats.size === 0) { throw new Error( `Video file is empty (0 bytes): ${videoPath}\n\n` + `Please ensure the video file is valid and not corrupted.` ); } if (stats.size > MAX_FILE_SIZE) { const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2); const maxSizeMB = (MAX_FILE_SIZE / (1024 * 1024)).toFixed(0); throw new Error( `Video file too large: ${fileSizeMB}MB (max: ${maxSizeMB}MB)\n\n` + `To fix this:\n` + ` • Trim the video to show only the bug (crash moment + 10 seconds before)\n` + ` • Compress with: ffmpeg -i input.mp4 -vcodec h264 -acodec aac output.mp4\n` + ` • Use a shorter screen recording\n\n` + `File: ${videoPath}` ); } // Validate file extension const ext = videoPath.toLowerCase().split(".").pop() || ""; const supportedFormats = ["mp4", "mov", "webm", "avi", "mkv", "flv", "mpeg", "mpg", "3gp", "wmv"]; if (!supportedFormats.includes(ext)) { throw new Error( `Unsupported video format: .${ext}\n\n` + `Supported formats: ${supportedFormats.join(", ")}\n\n` + `To convert your video:\n` + ` ffmpeg -i ${videoPath} output.mp4\n\n` + `File: ${videoPath}` ); } try { // Step 1: Get presigned upload URL const { data: presignData } = await this.client.post("/upload/presign", { filename: basename(videoPath), content_type: this.getContentType(videoPath), file_size: stats.size, }); const { upload_url, video_key } = presignData; // Step 2: Upload video to Cloudflare R2 console.error(`Uploading ${basename(videoPath)} (${(stats.size / (1024 * 1024)).toFixed(2)}MB)...`); const fileStream = createReadStream(videoPath); await axios.put(upload_url, fileStream, { headers: { "Content-Type": this.getContentType(videoPath), }, timeout: 120000, // 2 minutes for upload maxBodyLength: MAX_FILE_SIZE, maxContentLength: MAX_FILE_SIZE, }); console.error(`Upload complete. Starting analysis...`); // Step 3: Start analysis job const { data: jobData } = await this.client.post("/analyze/start", { video_key: video_key, filename: basename(videoPath), question, analysis_type: "mobile_bug", }); const jobId = jobData.id; console.error(`Analysis job started (ID: ${jobId}). This may take 2-3 minutes...`); // Step 4: Poll for results const result = await this.pollJobStatus(jobId); return { jobId, analysis: this.formatAnalysis(result), }; } catch (error: any) { // Handle specific error types if (error.response) { // HTTP error from API const status = error.response.status; const message = error.response.data?.detail || error.response.data?.error || error.message; if (status === 401 || status === 403) { throw new Error( `Authentication failed\n\n` + `Your API key is invalid or expired.\n\n` + `To fix this:\n` + ` 1. Get a new API key: curl -X POST "https://api.clipsense.app/api/v1/keys/request" -H "Content-Type: application/json" -d '{"email":"your-email@example.com"}'\n` + ` 2. Update your MCP settings with the new key\n` + ` 3. Restart your IDE\n\n` + `Error: ${message}` ); } if (status === 429) { throw new Error( `Rate limit exceeded\n\n` + `You've used all your monthly analyses.\n\n` + `To fix this:\n` + ` • Wait until next month (free tier resets monthly)\n` + ` • Upgrade to PRO: https://clipsense.app/pricing\n\n` + `Error: ${message}` ); } if (status === 413) { throw new Error( `Video file too large for server\n\n` + `The server rejected your video file.\n\n` + `To fix this:\n` + ` • Trim the video to only show the bug\n` + ` • Compress with: ffmpeg -i ${videoPath} -vcodec h264 -acodec aac output.mp4\n\n` + `Error: ${message}` ); } if (status >= 500) { throw new Error( `ClipSense server error (${status})\n\n` + `The ClipSense API is experiencing issues.\n\n` + `To fix this:\n` + ` • Try again in a few minutes\n` + ` • Check status: https://clipsense.app/status\n` + ` • Contact support: support@clipsense.app\n\n` + `Error: ${message}` ); } throw new Error( `API error (${status}): ${message}\n\n` + `Please contact support@clipsense.app if this persists.` ); } if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { throw new Error( `Cannot connect to ClipSense API\n\n` + `Please check:\n` + ` • Your internet connection\n` + ` • Firewall settings (allow https://api.clipsense.app)\n` + ` • VPN/proxy settings\n\n` + `Error: ${error.message}` ); } if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') { throw new Error( `Request timeout\n\n` + `The upload or API request took too long.\n\n` + `To fix this:\n` + ` • Check your internet connection\n` + ` • Try a smaller video file\n` + ` • Try again in a few minutes\n\n` + `Error: ${error.message}` ); } // Re-throw if already formatted throw error; } } /** * Poll job status until complete */ private async pollJobStatus(jobId: string): Promise<any> { const maxAttempts = 120; // 10 minutes max (5s interval) let attempts = 0; while (attempts < maxAttempts) { try { const { data } = await this.client.get(`/analyze/jobs/${jobId}/status`); if (data.status === "completed") { console.error(`Analysis complete!`); // Get full job details const { data: fullJob } = await this.client.get(`/analyze/jobs/${jobId}`); return fullJob; } if (data.status === "failed") { throw new Error( `Analysis failed\n\n` + `The video analysis encountered an error.\n\n` + `Error details: ${data.error_message || "Unknown error"}\n\n` + `To fix this:\n` + ` • Ensure the video file is valid and not corrupted\n` + ` • Try a different video format\n` + ` • Contact support@clipsense.app with job ID: ${jobId}` ); } // Progress indicator (every 30 seconds) if (attempts % 6 === 0 && attempts > 0) { const elapsed = (attempts * 5) / 60; console.error(`Still processing... (${elapsed.toFixed(1)} min elapsed)`); } // Wait 5 seconds before next poll await new Promise((resolve) => setTimeout(resolve, 5000)); attempts++; } catch (error: any) { // If polling fails due to network error, provide helpful message if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') { throw new Error( `Lost connection to ClipSense API\n\n` + `Your video is still being processed (Job ID: ${jobId})\n\n` + `To check status:\n` + ` • Visit: https://clipsense.app/results/${jobId}\n` + ` • Or try again in a few minutes\n\n` + `Error: ${error.message}` ); } throw error; } } throw new Error( `Analysis timeout\n\n` + `The analysis is taking longer than expected (>10 minutes).\n\n` + `Your job is still processing. To check status:\n` + ` • Visit: https://clipsense.app/results/${jobId}\n` + ` • Contact support@clipsense.app with job ID: ${jobId}\n\n` + `This usually happens with very long videos (>5 minutes).` ); } /** * Format analysis result for display */ private formatAnalysis(job: any): string { const { result, cost_total, tokens_used } = job; if (!result?.response) { throw new Error("No analysis result found"); } return ` ## Mobile Bug Analysis ${result.response} --- **Analysis Details:** - Frames analyzed: ${job.frames_extracted || "N/A"} - Tokens used: ${tokens_used || 0} - Cost: $${(cost_total || 0).toFixed(4)} `.trim(); } /** * Get content type from file extension */ private getContentType(filepath: string): string { const ext = filepath.toLowerCase().split(".").pop(); const types: Record<string, string> = { mp4: "video/mp4", mov: "video/quicktime", webm: "video/webm", avi: "video/x-msvideo", mkv: "video/x-matroska", flv: "video/x-flv", mpeg: "video/mpeg", mpg: "video/mpeg", "3gp": "video/3gpp", wmv: "video/x-ms-wmv", }; return types[ext || ""] || "video/mp4"; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/clipsense/-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server