Skip to content

Commit b1fb176

Browse files
authored
Merge pull request #62 from led-mirage/feature/v1.34.0
Feature/v1.34.0
2 parents 38fa759 + 6416967 commit b1fb176

File tree

11 files changed

+156
-13
lines changed

11 files changed

+156
-13
lines changed

Readme.en.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Here are the highlights of this app ✨
4242
* Compatible with Linux Mint (Cinnamon/x64 only, Japanese input via IBus only, printing not supported)
4343
* Dark mode supported
4444
* Custom CSS support
45-
* Image-based questions are now supported (experimental, OpenAI only) ✨
45+
* Image-based questions are now supported (experimental) ✨
4646

4747
## 💎 Language Support
4848

@@ -69,7 +69,7 @@ The following values can be configured:
6969
- Raspberry Pi OS Bookworm 64bit
7070
- Linux Mint 22.1 Cinnamon Edition
7171
- Python 3.10–3.13 (development environment: 3.12.0)
72-
- VOICEVOX 0.22.3
72+
- VOICEVOX 0.25.0
7373
- A.I.VOICE Editor 1.4.10.0
7474
- COEIROINK v.2.3.4
7575

@@ -363,6 +363,11 @@ License:MIT License
363363
Homepage:https://github.com/mhammond/pywin32
364364
License:Python Software Foundation License (PSF)
365365

366+
### 🔖 Pillow 12.0.0
367+
368+
Homepage: https://github.com/python-pillow/Pillow
369+
License:MIT-CMU license
370+
366371
### 🔖 MathJax 3.2.2
367372

368373
Homepage: https://github.com/mathjax/MathJax

Readme.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ APIを使ってAIとチャットするアプリなのだ。
4545
- Linux Mint対応(Cinnamon/x64、日本語入力はIBus限定、印刷機能は非対応)
4646
- ダークモード対応
4747
- スタイルシート(CSS)をカスタマイズ可能
48-
- 画像を使った質問が可能(実験的機能、OpenAIのみ)✨
48+
- 画像を使った質問が可能(実験的機能)✨
4949

5050
アプリの紹介と、もっとも手軽な導入方法を[Zennの記事](https://zenn.dev/ledmirage/articles/7650f36d3a784a)にしたので、そちらも参考にしてほしいのだ✨
5151

@@ -60,7 +60,7 @@ Raspberry Pi、Linuxへの導入に関しても[Zennの記事](https://zenn.dev/
6060
- Raspberry Pi OS Bookworm 64bit
6161
- Linux Mint 22.1 Cinnamon Edition
6262
- Python 3.10-3.13(開発環境は 3.12.0)
63-
- VOICEVOX 0.23.0
63+
- VOICEVOX 0.25.0
6464
- A.I.VOICE Editor 1.4.10.0
6565
- COEIROINK v.2.3.4
6666

@@ -206,7 +206,7 @@ Windowsの場合は、Windowsの検索窓で「環境変数を編集」で検索
206206

207207
以下のリンクから ZundaGPT2.ZIP をダウンロードして、作成したフォルダに展開するのだ。
208208

209-
https://github.com/led-mirage/ZundaGPT2/releases/tag/v1.33.0
209+
https://github.com/led-mirage/ZundaGPT2/releases/tag/v1.34.0
210210

211211
#### 3. 実行
212212

@@ -354,11 +354,11 @@ CSSを知らない人はなんのことかわからないかもしれないけ
354354

355355
VirusTotalでのチェック結果はこちらなのだ。
356356

357-
- Windows版: [71個中1個のアンチウィルスエンジンで検出 :2025/10/31 v1.33.0](https://www.virustotal.com/gui/file/e93d31b74243520d3d202a6307723e41d1fb1517d31b585409739423913ffd76/detection)
358-
- Raspberry Pi版: [61個中0個のアンチウィルスエンジンで検出 :2025/10/31 v1.33.0](https://www.virustotal.com/gui/file/409a69577a8c8e25ee7f0ae24a0fe0a8f1376ba3d81746dd1ada6bcde98b8ce5/detection)
359-
- Linux版: [63個中0個のアンチウィルスエンジンで検出 :2025/10/31 v1.33.0](https://www.virustotal.com/gui/file/cd622f4756cc4b111da71db6bfcfe156beeee662539e91f9b85e9862cb9194ef/detection)
357+
- Windows版: [72個中2個のアンチウィルスエンジンで検出 :2025/11/02 v1.34.0](https://www.virustotal.com/gui/file/ee47b5a5c9c70fca1c3d080bf4f63356bf86d38a5000a375e8a8db910f6e3754/detection)
358+
- Raspberry Pi版: [60個中0個のアンチウィルスエンジンで検出 :2025/11/02 v1.34.0](https://www.virustotal.com/gui/file/8d8cdb96624cdf83da439297b762e5ae2595486d799142ad8457cfd8071bddf3/detection)
359+
- Linux版: [62個中0個のアンチウィルスエンジンで検出 :2025/11/02 v1.34.0](https://www.virustotal.com/gui/file/23315c375d2badf83a31c84dfb986fc9addb250d69bb68b33d36780f0e39e2ff/detection)
360360

361-
<img src="doc/images/virustotal_1.33.0.png" width="600">
361+
<img src="doc/images/virustotal_1.34.0.png" width="600">
362362

363363
### ⚡ ご利用について
364364

@@ -440,6 +440,11 @@ VirusTotalでのチェック結果はこちらなのだ。
440440
ホームページ:https://github.com/mhammond/pywin32
441441
ライセンス:Python Software Foundation License (PSF)
442442

443+
### 🔖 Pillow 12.0.0
444+
445+
ホームページ: https://github.com/python-pillow/Pillow
446+
ライセンス: MIT-CMUライセンス
447+
443448
### 🔖 MathJax 3.2.2
444449

445450
ホームページ: https://github.com/mathjax/MathJax
@@ -489,6 +494,12 @@ VirusTotalでのチェック結果はこちらなのだ。
489494

490495
## 💎 バージョン履歴
491496

497+
### 1.34.0 (2025/11/02)
498+
499+
- 画像送信機能の追加(実験的機能)
500+
- Claude、Geminiにも対応
501+
- 画像をクリックすることで拡大表示する機能を追加
502+
492503
### 1.33.0 (2025/10/31)
493504

494505
- 画像送信機能の追加(実験的機能)

app/chat/chat_claude.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
# このソースコードは MITライセンス の下でライセンスされています。
77
# ライセンスの詳細については、このプロジェクトのLICENSEファイルを参照してください。
88

9+
import copy
910
import os
1011
from datetime import datetime
1112

1213
import anthropic
1314

1415
from .chat import Chat
1516
from .listener import SendMessageListener
17+
from utility.utils import parse_data_url, resize_base64_image
1618
from utility.multi_lang import get_text_resource
1719

1820

@@ -66,7 +68,22 @@ def send_message(
6668
self.stop_send_event.clear()
6769

6870
self.messages.append({"role": "user", "content": text})
69-
messages = self.get_history()
71+
messages = copy.deepcopy(self.get_history())
72+
73+
if images and len(images) > 0:
74+
messages = messages[:-1]
75+
content = []
76+
if text:
77+
content.append({"type": "text", "text": text})
78+
79+
for image in images:
80+
media_type, image_format, b64 = parse_data_url(image)
81+
b64 = resize_base64_image(b64, max_size_mb=3.0, output_format=image_format)
82+
content.append(
83+
{"type": "image", "source": {"type": "base64", "media_type": media_type, "data": b64}}
84+
)
85+
86+
messages.append({"role": "user", "content": content})
7087

7188
content = ""
7289
sentence = ""

app/chat/chat_gemini.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from .chat import Chat
1818
from .listener import SendMessageListener
19+
from utility.utils import parse_data_url, resize_base64_image
1920
from utility.multi_lang import get_text_resource
2021

2122

@@ -62,9 +63,25 @@ def send_message(
6263
try:
6364
self.stop_send_event.clear()
6465

65-
self.messages.append({"role": "user", "content": text})
66+
user_parts = [{"text": text}]
67+
for img_dataurl in images or []:
68+
media_type, image_format, b64 = parse_data_url(img_dataurl)
69+
b64 = resize_base64_image(b64, max_size_mb=15.0, output_format=image_format)
70+
user_parts.append({
71+
"inline_data": {
72+
"mime_type": media_type,
73+
"data": b64
74+
}
75+
})
76+
6677
messages = copy.deepcopy(self.get_history())
6778
messages = self.convert_messages(messages)
79+
messages.append({"role": "user", "parts": user_parts})
80+
self.messages.append({"role": "user", "content": text})
81+
82+
#self.messages.append({"role": "user", "content": text})
83+
#messages = copy.deepcopy(self.get_history())
84+
#messages = self.convert_messages(messages)
6885

6986
stream = self.client.models.generate_content_stream(
7087
model=self.model,

app/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
# ライセンスの詳細については、このプロジェクトのLICENSEファイルを参照してください。
88

99
APP_NAME = "ZundaGPT2"
10-
APP_VERSION = "1.33.0"
10+
APP_VERSION = "1.34.0"
1111
COPYRIGHT = "© 2024-2025 led-mirage"

app/html/js/index.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,12 +1454,42 @@ function addImagePreview(src) {
14541454
container.appendChild(img);
14551455
container.appendChild(delBtn);
14561456
chatMessages.appendChild(container);
1457+
enlargeImage(img);
14571458

14581459
g_pastedImages.push({ src, sent: false, element: container });
14591460

14601461
scrollToBottom();
14611462
}
14621463

1464+
// 貼り付けた画像の拡大処理
1465+
function enlargeImage(img) {
1466+
img.addEventListener('click', () => {
1467+
const modal = document.createElement('div');
1468+
modal.style.position = 'fixed';
1469+
modal.style.top = 0;
1470+
modal.style.left = 0;
1471+
modal.style.width = '100vw';
1472+
modal.style.height = '100vh';
1473+
modal.style.background = 'rgba(0,0,0,0.7)';
1474+
modal.style.display = 'flex';
1475+
modal.style.alignItems = 'center';
1476+
modal.style.justifyContent = 'center';
1477+
modal.style.zIndex = 99999;
1478+
1479+
const bigImg = document.createElement('img');
1480+
bigImg.src = img.src;
1481+
bigImg.style.maxWidth = '90%';
1482+
bigImg.style.maxHeight = '90%';
1483+
bigImg.style.borderRadius = '8px';
1484+
bigImg.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
1485+
1486+
modal.appendChild(bigImg);
1487+
document.body.appendChild(modal);
1488+
1489+
modal.addEventListener('click', () => modal.remove());
1490+
});
1491+
}
1492+
14631493
// Pythonから呼び出される関数(グローバルスコープに登録)
14641494
window.applyCustomCSS = applyCustomCSS;
14651495
window.setChatInfo = setChatInfo;

app/utility/utils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88

99
import base64
1010
import inspect
11+
import io
1112
import mimetypes
1213
import os
14+
import re
1315
import sys
1416
import tkinter as tk
1517
from pathlib import Path
1618
from urllib.parse import urlparse
1719

20+
from PIL import Image
21+
1822

1923
# 文字をエスケープする
2024
def escape_js_string(s: str):
@@ -90,3 +94,60 @@ def get_screen_size(window_handle=None) -> tuple[int, int]:
9094
except Exception:
9195
# 取得できない場合はデフォルトサイズを返す
9296
return 800, 600
97+
98+
# data URL 形式の文字列を解析して (media_type, subtype, base64_data) を返す
99+
# 想定外の形式の場合は ('image/png', 'png', data_url) を返す
100+
def parse_data_url(data_url: str) -> tuple[str, str, str]:
101+
if not isinstance(data_url, str):
102+
return "image/png", "png", ""
103+
104+
match = re.match(r"^data:(.*?);base64,(.*)$", data_url)
105+
if match:
106+
media_type = match.group(1).strip() or "image/png"
107+
b64_data = match.group(2).strip()
108+
109+
# 画像タイプ部分だけ抽出する
110+
subtype_match = re.match(r"^image/(\w+)$", media_type)
111+
subtype = subtype_match.group(1) if subtype_match else "png"
112+
113+
return media_type, subtype, b64_data
114+
else:
115+
# 想定外の場合はPNG扱い
116+
return "image/png", "png", data_url.strip()
117+
118+
# Base64エンコードされた画像データを指定サイズ以下に圧縮する
119+
def resize_base64_image(b64_data: str, max_size_mb: float, output_format="JPEG", quality_step=5) -> str:
120+
img_bytes = base64.b64decode(b64_data)
121+
image = Image.open(io.BytesIO(img_bytes))
122+
size_mb = len(img_bytes) / (1024 * 1024)
123+
if size_mb <= max_size_mb:
124+
return b64_data
125+
126+
buffer = io.BytesIO()
127+
if output_format.upper() == "JPEG":
128+
# JPEGは画質を下げながら圧縮
129+
quality = 95
130+
while True:
131+
buffer = io.BytesIO()
132+
image.save(buffer, format="JPEG", quality=quality)
133+
new_data = buffer.getvalue()
134+
new_size = len(new_data) / (1024 * 1024)
135+
if new_size <= max_size_mb or quality <= 10:
136+
break
137+
quality -= quality_step
138+
139+
else:
140+
# PNGなどはリサイズ主体で対応
141+
width, height = image.size
142+
while True:
143+
buffer = io.BytesIO()
144+
image.save(buffer, format=output_format, optimize=True, compress_level=9)
145+
new_data = buffer.getvalue()
146+
new_size = len(new_data) / (1024 * 1024)
147+
if new_size <= max_size_mb or (width < 100 or height < 100):
148+
break
149+
# サイズがまだ大きいなら10%ずつ縮小
150+
width, height = int(width * 0.9), int(height * 0.9)
151+
image = image.resize((width, height), Image.LANCZOS)
152+
153+
return base64.b64encode(buffer.getvalue()).decode("utf-8")

doc/images/virustotal_1.34.0.png

113 KB
Loading

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ langdetect==1.0.9
1010
pyperclip==1.9.0
1111
pydub==0.25.1
1212
pywin32==306
13+
pillow==12.0.0
1314
pyinstaller_versionfile==3.0.1

requirements_linux.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ gtts==2.5.1
77
langdetect==1.0.9
88
pyperclip==1.9.0
99
pydub==0.25.1
10+
pillow==12.0.0

0 commit comments

Comments
 (0)