Merge pull request #2805 from SillyTavern/gemini-vision-fix Fix Gemini multimodal with JPG images

5e522d6e35a5f838bf5530a2d07d2e3951cd2776

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
4 files changed, +33 -13Ignore whitespace
public/scripts/extensions/shared.js+1 -6
@@ -20,7 +20,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
2020
21 throwIfInvalidModel(useReverseProxy);21 throwIfInvalidModel(useReverseProxy);
2222
23 const noPrefix = ['google', 'ollama', 'llamacpp'].includes(extension_settings.caption.multimodal_api);23 const noPrefix = ['ollama', 'llamacpp'].includes(extension_settings.caption.multimodal_api);
2424
25 if (noPrefix && base64Img.startsWith('data:image/')) {25 if (noPrefix && base64Img.startsWith('data:image/')) {
26 base64Img = base64Img.split(',')[1];26 base64Img = base64Img.split(',')[1];
@@ -28,7 +28,6 @@ export async function getMultimodalCaption(base64Img, prompt) {
2828
29 // OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy.29 // OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy.
30 // Ooba requires all images to be JPEGs. Koboldcpp just asked nicely.30 // Ooba requires all images to be JPEGs. Koboldcpp just asked nicely.
31 const isGoogle = extension_settings.caption.multimodal_api === 'google';
32 const isOllama = extension_settings.caption.multimodal_api === 'ollama';31 const isOllama = extension_settings.caption.multimodal_api === 'ollama';
33 const isLlamaCpp = extension_settings.caption.multimodal_api === 'llamacpp';32 const isLlamaCpp = extension_settings.caption.multimodal_api === 'llamacpp';
34 const isCustom = extension_settings.caption.multimodal_api === 'custom';33 const isCustom = extension_settings.caption.multimodal_api === 'custom';
@@ -40,10 +39,6 @@ export async function getMultimodalCaption(base64Img, prompt) {
40 if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {39 if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
41 const maxSide = 1024;40 const maxSide = 1024;
42 base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');41 base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
43
44 if (isGoogle) {
45 base64Img = base64Img.split(',')[1];
46 }
47 }42 }
4843
49 const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';44 const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';
public/scripts/openai.js+26 -3
@@ -47,6 +47,7 @@ import { SECRET_KEYS, secret_state, writeSecret } from './secrets.js';
4747
48import { getEventSourceStream } from './sse-stream.js';48import { getEventSourceStream } from './sse-stream.js';
49import {49import {
50 createThumbnail,
50 delay,51 delay,
51 download,52 download,
52 getBase64Async,53 getBase64Async,
@@ -2440,15 +2441,14 @@ class Message {
2440 if (!response.ok) throw new Error('Failed to fetch image');2441 if (!response.ok) throw new Error('Failed to fetch image');
2441 const blob = await response.blob();2442 const blob = await response.blob();
2442 image = await getBase64Async(blob);2443 image = await getBase64Async(blob);
2443 if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
2444 image = image.split(',')[1];
2445 }
2446 } catch (error) {2444 } catch (error) {
2447 console.error('Image adding skipped', error);2445 console.error('Image adding skipped', error);
2448 return;2446 return;
2449 }2447 }
2450 }2448 }
24512449
2450 image = await this.compressImage(image);
2451
2452 const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;2452 const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;
2453 this.content = [2453 this.content = [
2454 { type: 'text', text: textContent },2454 { type: 'text', text: textContent },
@@ -2464,6 +2464,29 @@ class Message {
2464 }2464 }
2465 }2465 }
24662466
2467 /**
2468 * Compress an image if it exceeds the size threshold for the current chat completion source.
2469 * @param {string} image Data URL of the image.
2470 * @returns {Promise<string>} Compressed image as a Data URL.
2471 */
2472 async compressImage(image) {
2473 if ([chat_completion_sources.OPENROUTER, chat_completion_sources.MAKERSUITE].includes(oai_settings.chat_completion_source)) {
2474 const sizeThreshold = 2 * 1024 * 1024;
2475 const dataSize = image.length * 0.75;
2476 const maxSide = 1024;
2477 if (dataSize > sizeThreshold) {
2478 image = await createThumbnail(image, maxSide);
2479 }
2480 }
2481 return image;
2482 }
2483
2484 /**
2485 * Get the token cost of an image.
2486 * @param {string} dataUrl Data URL of the image.
2487 * @param {string} quality String representing the quality of the image. Can be 'low', 'auto', or 'high'.
2488 * @returns {Promise<number>} The token cost of the image.
2489 */
2467 async getImageTokenCost(dataUrl, quality) {2490 async getImageTokenCost(dataUrl, quality) {
2468 if (quality === 'low') {2491 if (quality === 'low') {
2469 return Message.tokensPerImage;2492 return Message.tokensPerImage;
src/endpoints/google.js+2 -2
@@ -22,8 +22,8 @@ router.post('/caption-image', jsonParser, async (request, response) => {
22 { text: request.body.prompt },22 { text: request.body.prompt },
23 {23 {
24 inlineData: {24 inlineData: {
25 mimeType: 'image/png', // It needs to specify a MIME type in data if it's not a PNG25 mimeType: mimeType,
26 data: mimeType === 'image/png' ? base64Data : request.body.image,26 data: base64Data,
27 },27 },
28 }],28 }],
29 }],29 }],
src/prompt-converters.js+4 -2
@@ -335,10 +335,12 @@ function convertGooglePrompt(messages, model, useSysPrompt = false, charName = '
335 if (part.type === 'text') {335 if (part.type === 'text') {
336 parts.push({ text: part.text });336 parts.push({ text: part.text });
337 } else if (part.type === 'image_url' && isMultimodal) {337 } else if (part.type === 'image_url' && isMultimodal) {
338 const mimeType = part.image_url.url.split(';')[0].split(':')[1];
339 const base64Data = part.image_url.url.split(',')[1];
338 parts.push({340 parts.push({
339 inlineData: {341 inlineData: {
340 mimeType: 'image/png',342 mimeType: mimeType,
341 data: part.image_url.url,343 data: base64Data,
342 },344 },
343 });345 });
344 hasImage = true;346 hasImage = true;