1
0

visionDiagnosis.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. import { VisionBackend } from "@/types/backend";
  2. import { EvaluationResult } from "./diagnosisScript";
  3. const additionalUrls = {
  4. vision_openai: "/v1/chat/completions",
  5. vision_llamacpp: "/completion",
  6. vision_ollama: "/api/chat",
  7. };
  8. const TIME_OUT = 20000;
  9. const MIN_DURATION = 5000;
  10. export async function loadImage(
  11. url: string,
  12. maxWidth = 320,
  13. maxHeight = 240,
  14. quality = 0.8 // JPEG compression quality (0 to 1)
  15. ): Promise<string> {
  16. // Fetch the image blob
  17. const response = await fetch(url);
  18. const blob = await response.blob();
  19. const imageUrl = URL.createObjectURL(blob);
  20. const img = new Image();
  21. await new Promise<void>((resolve, reject) => {
  22. img.onload = () => resolve();
  23. img.onerror = reject;
  24. img.src = imageUrl;
  25. });
  26. // Calculate new dimensions maintaining aspect ratio
  27. let { width, height } = img;
  28. const aspectRatio = width / height;
  29. if (width > maxWidth || height > maxHeight) {
  30. if (aspectRatio > 1) {
  31. // Landscape
  32. width = maxWidth;
  33. height = maxWidth / aspectRatio;
  34. } else {
  35. // Portrait
  36. height = maxHeight;
  37. width = maxHeight * aspectRatio;
  38. }
  39. }
  40. // Draw on canvas
  41. const canvas = document.createElement('canvas');
  42. canvas.width = width;
  43. canvas.height = height;
  44. const ctx = canvas.getContext('2d');
  45. if (!ctx) throw new Error("Unable to get canvas context");
  46. ctx.drawImage(img, 0, 0, width, height);
  47. // Compress and convert to base64
  48. const base64 = canvas
  49. .toDataURL('image/jpeg', quality)
  50. .replace('data:image/jpeg;base64,', '');
  51. URL.revokeObjectURL(imageUrl);
  52. return base64;
  53. }
  54. // Utility to safely call fetch
  55. async function safeFetch(
  56. fullUrl: string,
  57. options?: RequestInit,
  58. timeoutMs = TIME_OUT
  59. ): Promise<EvaluationResult> {
  60. const controller = new AbortController();
  61. const id = setTimeout(() => controller.abort(), timeoutMs);
  62. const start = performance.now();
  63. try {
  64. if (!options) {
  65. const res = await fetch(fullUrl, {
  66. signal: controller.signal,
  67. });
  68. const end = performance.now();
  69. clearTimeout(id);
  70. const duration = end - start;
  71. const status = res.ok ? "pass" : "fail";
  72. const score = calculateScore({ status, duration });
  73. return { status, score };
  74. } else {
  75. const res = await fetch(fullUrl, {
  76. ...options,
  77. signal: controller.signal,
  78. });
  79. const end = performance.now();
  80. clearTimeout(id);
  81. const duration = end - start;
  82. const status = res.ok ? "pass" : "fail";
  83. const score = calculateScore({ status, duration });
  84. return { status, score };
  85. }
  86. } catch (err: any) {
  87. const end = performance.now();
  88. clearTimeout(id);
  89. const duration = end - start;
  90. const isAbort = err.name === "AbortError";
  91. return { status: "fail", score: calculateScore({ status: "fail", duration, timeout: isAbort }) };
  92. }
  93. }
  94. // Score calculation logic
  95. function calculateScore({
  96. status,
  97. duration,
  98. timeout = false,
  99. }: {
  100. status: "pass" | "fail";
  101. duration: number;
  102. timeout?: boolean;
  103. }): number {
  104. if (timeout) return 0;
  105. let score = 0;
  106. if (status === "pass") score += 50;
  107. if (duration < MIN_DURATION) score += 50 * ((MIN_DURATION - duration) / MIN_DURATION);
  108. return Math.round(score);
  109. }
  110. // Individual backend handlers
  111. const backendHandlers: Record<
  112. string,
  113. (params: VisionBackend) => Promise<EvaluationResult>
  114. > = {
  115. vision_openai: async (params) => {
  116. const { vision_openai_apikey, vision_openai_model, vision_openai_url } =
  117. params.vision_openai || {};
  118. if (!vision_openai_apikey || !vision_openai_model || !vision_openai_url)
  119. return {status:"fail", score: 0};
  120. let image = await loadImage("/sample-image.jpeg");
  121. const messages = [
  122. {
  123. role: "user",
  124. // @ts-ignore normally this is a string
  125. content: [
  126. {
  127. type: "text",
  128. text: "Describe the image as accurately as possible",
  129. },
  130. {
  131. type: "image_url",
  132. image_url: {
  133. url: `data:image/jpeg;base64,${image}`,
  134. },
  135. },
  136. ],
  137. },
  138. ];
  139. return await safeFetch(
  140. `${vision_openai_url}${additionalUrls.vision_openai}`,
  141. {
  142. method: "POST",
  143. headers: {
  144. "Content-Type": "application/json",
  145. Authorization: `Bearer ${vision_openai_apikey}`,
  146. "HTTP-Referer": "https://amica.arbius.ai",
  147. "X-Title": "Amica",
  148. },
  149. body: JSON.stringify({
  150. vision_openai_model,
  151. messages,
  152. stream: true,
  153. max_tokens: 200,
  154. }),
  155. },
  156. );
  157. },
  158. vision_llamacpp: async (params) => {
  159. const { vision_llamacpp_url } = params.vision_llamacpp || {};
  160. if (!vision_llamacpp_url) return {status:"fail", score: 0};
  161. let image = await loadImage("/sample-image.jpeg");
  162. const prompt = `User: Describe the image as accurately as possible`;
  163. return await safeFetch(`${vision_llamacpp_url}${additionalUrls.vision_llamacpp}`, {
  164. method: "POST",
  165. headers: {
  166. "Content-Type": "application/json",
  167. "Connection": "keep-alive",
  168. "Accept": "text/event-stream",
  169. },
  170. body: JSON.stringify({
  171. stream: true,
  172. n_predict: 400,
  173. temperature: 0.7,
  174. cache_prompt: true,
  175. image_data: [{
  176. data: image,
  177. id: 10,
  178. }],
  179. prompt,
  180. }),
  181. });
  182. },
  183. vision_ollama: async (params) => {
  184. const { vision_ollama_url, vision_ollama_model } =
  185. params.vision_ollama || {};
  186. if (!vision_ollama_url || !vision_ollama_model) return {status:"fail", score: 0};
  187. let image = await loadImage("/sample-image.jpeg");
  188. const messages = [
  189. {
  190. role: "user",
  191. content: "Describe the image as accurately as possible",
  192. },
  193. ];
  194. return await safeFetch(`${vision_ollama_url}${additionalUrls.vision_ollama}`, {
  195. method: "POST",
  196. headers: {
  197. "Content-Type": "application/json",
  198. },
  199. body: JSON.stringify({
  200. model: vision_ollama_model,
  201. messages,
  202. images: [image],
  203. stream: false,
  204. }),
  205. });
  206. },
  207. };
  208. // Dispatcher function
  209. export async function visionDiagnosis(
  210. backend: string,
  211. params: VisionBackend,
  212. ): Promise<EvaluationResult> {
  213. const handler = backendHandlers[backend];
  214. if (!handler) return {status:"fail", score: 0};
  215. return await handler(params);
  216. }