Views
No views yet
# モデルのダウンロード / download model
hf download dahara1/gemma-4-E2B-it-UD-japanese-imatrix gemma-4-E2B-it-UD-Q4_K_XL.gguf --local-dir gemma-4-E2B-it-UD-japanese-imatrix
# 視覚対応用のファイルのダウンロード / download model's vision part
hf download dahara1/gemma-4-E2B-it-UD-japanese-imatrix mmproj-bf16.gguf --local-dir gemma-4-E2B-it-UD-japanese-imatrix
# 念の為jinjaテンプレートのダウンロード / download jinja template
hf download dahara1/gemma-4-E2B-it-UD-japanese-imatrix chat_template.jinja --local-dir gemma-4-E2B-it-UD-japanese-imatrix
./llama-cli \
-m gemma-4-E2B-it-UD-japanese-imatrix/gemma-4-E2B-it-UD-Q4_K_XL.gguf \
--temp 1.0 \
--top-p 0.95 \
--top-k 64 \
--min-p 0.0 \
--ctx-size 12000 \
--jinja \
--reasoning on \
--chat-template-file gemma-4-E2B-it-UD-japanese-imatrix/chat_template.jinja \
-ub 2048 \
-b 2048 ctx-size specifies the length of text that can be handled. Increasing this value allows for longer conversations with multiple turns, but it also increases the amount of memory required..\llama-server ^
-m ..\gemma-4-E2B-it-UD-Q4_K_XL.gguf ^
--host 0.0.0.0 ^
--port 8080 ^
--temp 1.0 ^
--top-p 0.95 ^
--top-k 64 ^
--min-p 0.0 ^
--ctx-size 8000 ^
--jinja ^
--chat-template-file ..\chat_template.jinja ^
--reasoning on ^
--mmproj ..\mmproj-bf16.gguf ^
-ub 2048 ^
-b 2048
pip install playwrigh
pip install topenai
pip install pyttsx3
playwright install chromium"""
Gemma 4 E2B ローカル AI エージェントデモ (2段階フロー)
ブラウザで英語のニュースサイトをアクセスし、日本語で要約して音声で伝える
recaptchaが出現した場合は人間に対処を求める
フロー:
1. スクショ + HTML を同時取得 (Playwright 1回起動)
2. STAGE 1: LLM に画像を見せてツール選択
- CAPTCHA 検出 → request_help (→ 終了)
- 正常ページ → extract_page_text
3. スクリプトが HTML をタグ除去 → 先頭3000文字抽出
4. STAGE 2: LLM に抽出テキストを渡して要約 → speak_japanese ツール呼び出し
5. pyttsx3 (SAPI5 Haruka) で日本語読み上げ
前提:
- llama-server が --mmproj + --reasoning on 付きで http://localhost:8080 で起動
- Windows 11 + Microsoft Haruka Desktop インストール済み
- playwright, openai, pyttsx3 インストール済み
使い方:
python gemma4_hn_demo.py # Hacker News 通常モード
python gemma4_hn_demo.py --url <URL> # 任意の URL
python gemma4_hn_demo.py --captcha-demo # reCAPTCHA デモページ (おまけ用)
python gemma4_hn_demo.py --debug # デバッグ出力
python gemma4_hn_demo.py --headless # ブラウザを隠す
"""
import argparse
import base64
import json
import os
import sys
from openai import OpenAI
# ============================================================
# 設定
# ============================================================
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="dummy", timeout=300)
MODEL = "gemma-4-E2B-it"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SCREENSHOT_PATH = os.path.join(SCRIPT_DIR, "hn_capture.png")
HTML_PATH = os.path.join(SCRIPT_DIR, "hn_capture.html")
HACKER_NEWS_URL = "https://news.ycombinator.com"
RECAPTCHA_DEMO_URL = "https://www.google.com/recaptcha/api2/demo"
VIEWPORT = {"width": 1280, "height": 800}
EXTRACT_TEXT_LIMIT = 3000 # STAGE 2 に渡すテキストの最大文字数
REQUIRED_VOICE_KEYWORD = "Haruka" # これが無ければエラー終了
# ============================================================
# 色定義
# ============================================================
class C:
BOLD = "\033[1m"
DIM = "\033[2m"
RESET = "\033[0m"
CYAN = "\033[96m"
YELLOW = "\033[93m"
GREEN = "\033[92m"
BLUE = "\033[94m"
MAGENTA = "\033[95m"
PINK = "\033[38;5;213m"
RED = "\033[91m"
WHITE = "\033[97m"
GRAY = "\033[90m"
BG_CYAN = "\033[46m"
BG_RED = "\033[41m"
BG_GREEN = "\033[42m"
# ============================================================
# TTS セットアップ (Microsoft Haruka Desktop 必須)
# ============================================================
def init_tts():
"""pyttsx3 を初期化し、Haruka 音声を選択する。無ければエラー終了。"""
import pyttsx3
engine = pyttsx3.init() # Windows では SAPI5 ドライバが選ばれる
voices = engine.getProperty("voices")
haruka = None
for v in voices:
if REQUIRED_VOICE_KEYWORD.lower() in (v.name or "").lower():
haruka = v
break
if haruka is None:
print(f"\n{C.BG_RED}{C.WHITE} ❌ ERROR {C.RESET}")
print(f"{C.RED}Microsoft Haruka Desktop が見つかりません。{C.RESET}")
print(f"{C.DIM}インストールされている音声:{C.RESET}")
for v in voices:
print(f" {C.DIM} - {v.name} ({v.id}){C.RESET}")
print(f"\n{C.YELLOW}設定 → 時刻と言語 → 言語と地域 → 日本語 → 音声機能 から追加してください。{C.RESET}")
sys.exit(1)
engine.setProperty("voice", haruka.id)
engine.setProperty("rate", 180) # デフォ200はやや早い
print(f" {C.GREEN}🔊 TTS音声: {haruka.name}{C.RESET}")
return engine
# ============================================================
# ツール実装
# ============================================================
def tool_speak_japanese(engine, text: str) -> dict:
"""日本語で読み上げ (ブロッキング)"""
print(f"\n {C.BG_GREEN}{C.WHITE} 🔊 speak_japanese {C.RESET}")
print(f" {C.GREEN}┌──────────────────────────────────────────{C.RESET}")
# 改行ごとにインデント
for line in text.split("\n"):
print(f" {C.GREEN}│ {C.WHITE}{line}{C.RESET}")
print(f" {C.GREEN}└──────────────────────────────────────────{C.RESET}")
print(f" {C.DIM} (Haruka が読み上げ中... 終了までブロックします){C.RESET}")
engine.say(text)
engine.runAndWait()
print(f" {C.GREEN} ✅ 読み上げ完了{C.RESET}")
return {"status": "spoken", "length": len(text)}
def tool_request_help(reason: str) -> dict:
"""人間の介入を要請 (ダミー実装: 通知するだけ)"""
print(f"\n {C.BG_RED}{C.WHITE} 🆘 request_help {C.RESET}")
print(f" {C.RED}┌──────────────────────────────────────────{C.RESET}")
print(f" {C.RED}│ {C.WHITE}理由: {reason}{C.RESET}")
print(f" {C.RED}│ {C.DIM}→ (デモ) オペレーターへの通知をシミュレート{C.RESET}")
print(f" {C.RED}└──────────────────────────────────────────{C.RESET}")
return {"status": "help_requested", "reason": reason}
# ツール定義
# STAGE 1: 画像を見て、CAPTCHA なら help / 正常なら本文抽出
TOOLS_STAGE1 = [
{
"type": "function",
"function": {
"name": "extract_page_text",
"description": (
"表示中のウェブページの本文テキストを取得します。"
"ページが正常に表示されており、ニュース・記事・投稿などの内容が"
"読めそうな場合に呼び出してください。引数はありません。"
),
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "request_help",
"description": (
"人間の介入を要請します。"
"ページに CAPTCHA (画像選択、チェックボックス、パズル認証等)、"
"ログイン要求、アクセス拒否、エラーページなどが表示されていて、"
"AI が自力でコンテンツを読み取れない場合に呼び出してください。"
),
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "なぜ人間の助けが必要かの日本語での説明。必ず『ご主人さま、』で始めてください。",
},
},
"required": ["reason"],
},
},
},
]
# STAGE 2: 抽出テキストを要約 → 読み上げ
TOOLS_STAGE2 = [
{
"type": "function",
"function": {
"name": "speak_japanese",
"description": (
"日本語のテキストをスピーカーから読み上げます。"
"ウェブページの内容を要約したテキストを引数として呼び出してください。"
),
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "読み上げる日本語テキスト。自然な文章で、3〜5文程度。",
},
},
"required": ["text"],
},
},
},
]
# ============================================================
# スクショ取得
# ============================================================
def capture_page(url: str, img_path: str, html_path: str, headless: bool = False) -> None:
"""スクショと HTML を同時に取得 (Playwright 1回起動)"""
from playwright.sync_api import sync_playwright
from datetime import datetime
print(f" {C.DIM}🌐 {url} を開いています... (headless={headless}){C.RESET}")
with sync_playwright() as p:
browser = p.chromium.launch(headless=headless)
context = browser.new_context(viewport=VIEWPORT, locale="ja-JP")
page = context.new_page()
try:
page.goto(url, wait_until="networkidle", timeout=30000)
except Exception:
print(f" {C.DIM} networkidle timeout, falling back to domcontentloaded{C.RESET}")
page.goto(url, wait_until="domcontentloaded", timeout=30000)
page.wait_for_timeout(1500)
# HTML を先に取得 (タイムスタンプ div を入れる前に)
html = page.content()
with open(html_path, "w", encoding="utf-8") as f:
f.write(html)
print(f" {C.GREEN}✅ HTML 保存: {html_path} ({len(html):,} 文字){C.RESET}")
# プロンプトキャッシュ回避のため、右下隅にタイムスタンプを描き込む
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
page.evaluate(f"""
(() => {{
const d = document.createElement('div');
d.textContent = '{ts}';
d.style.cssText = 'position:fixed;right:4px;bottom:4px;z-index:2147483647;'
+ 'background:rgba(0,0,0,0.5);color:#fff;font:10px monospace;padding:2px 4px;';
document.body.appendChild(d);
}})();
""")
page.wait_for_timeout(100)
page.screenshot(path=img_path, clip={"x": 0, "y": 0, **VIEWPORT})
if not headless:
page.wait_for_timeout(500)
browser.close()
print(f" {C.GREEN}✅ スクショ保存: {img_path}{C.RESET}")
def extract_text_from_html(html_path: str, limit: int = EXTRACT_TEXT_LIMIT) -> str:
"""HTML からタグを除去して先頭 limit 文字のプレーンテキストを返す"""
import re
from html import unescape
with open(html_path, "r", encoding="utf-8") as f:
html = f.read()
# script, style, noscript ブロックは丸ごと消す
html = re.sub(r"<(script|style|noscript)[^>]*>.*?</\1>", " ", html, flags=re.DOTALL | re.IGNORECASE)
# head ごと消す (title以外興味なし)
html = re.sub(r"<head[^>]*>.*?</head>", " ", html, flags=re.DOTALL | re.IGNORECASE)
# HTML コメント
html = re.sub(r"<!--.*?-->", " ", html, flags=re.DOTALL)
# タグ除去
text = re.sub(r"<[^>]+>", " ", html)
# HTML エンティティをデコード
text = unescape(text)
# 連続する空白を1つに
text = re.sub(r"\s+", " ", text).strip()
if len(text) > limit:
text = text[:limit] + " …(以下略)"
return text
# ============================================================
# 画像エンコード
# ============================================================
def make_image_content(path: str) -> dict:
with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode("utf-8")
mime = "image/png" if path.lower().endswith(".png") else "image/jpeg"
return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}
# ============================================================
# LLM 呼び出し (ストリーミング + reasoning + tool_calls)
# ============================================================
SYSTEM_PROMPT_STAGE1 = (
"あなたはご主人さまのために働く、ローカルPC上で動作する日本語AIアシスタントメイドロボです。"
"ご主人さまから渡されたウェブページのスクリーンショット画像を見て、"
"ページの状態を判断し、必ず提供されたツールのいずれかを呼び出してください。\n\n"
"判断ルール:\n"
"1. 画像に CAPTCHA、画像認証、『私はロボットではありません』チェックボックス、\n"
" パズル認証、ログイン画面、アクセス拒否ページ、エラーページなどが表示されている場合:\n"
" → request_help ツールを呼び出す。\n"
" → reason 引数は必ず『ご主人さま、』で始める。\n"
"2. 画像が正常なウェブページ (ニュースサイト、記事一覧、ブログ等) で、\n"
" 何らかのコンテンツが表示されている場合:\n"
" → extract_page_text ツールを呼び出す (引数なし)。\n"
" → これを呼ぶとご主人さまのシステムがページの本文テキストを取得してくれます。\n\n"
"必ずどちらか片方のツールを呼び出してください。地の文で回答してはいけません。"
)
SYSTEM_PROMPT_STAGE2 = (
"あなたはご主人さまのために働く、日本語AIアシスタントです。"
"ウェブページから抽出された本文テキストを受け取ったので、"
"その内容を日本語で要約して speak_japanese ツールで読み上げてください。\n\n"
"要約ルール:\n"
"- 3〜5文程度の自然な日本語にする。\n"
"- 『サイトの説明』ではなく、実際に並んでいる個別ニュース・記事・投稿の内容を紹介する。\n"
"- 良い例: 『今日の主な話題は、OpenAIの新モデル発表、Rustで書かれたターミナルエミュレータの公開、\n"
" 量子コンピュータの研究成果の3つです。』\n"
"- 悪い例: 『このページには技術的な話題のリストが表示されています。』\n"
"- 冒頭に『ご主人さま、Hacker News の今日の話題をお伝えします。』のような一言を添えると良い。\n\n"
"- メイドロボっぽいロールプレイ口調で応答すると次世代AIっぽくて更にすごい!\n\n"
"必ず speak_japanese ツールを呼び出してください。地の文で回答してはいけません。"
)
def stream_llm_call(messages: list, tools: list, stage_label: str, debug: bool = False):
"""
LLM をストリーミング呼び出し。
戻り値: (full_content, full_reasoning, tool_calls_list)
"""
print(f"\n{C.BOLD}{C.CYAN}━━━ {stage_label} ━━━{C.RESET}")
if debug:
def _sanitize(msg):
if isinstance(msg.get("content"), list):
new_content = []
for c in msg["content"]:
if isinstance(c, dict) and c.get("type") == "image_url":
new_content.append({"type": "image_url", "image_url": "<base64 image omitted>"})
else:
new_content.append(c)
return {**msg, "content": new_content}
return msg
print(f" {C.GRAY}[debug] messages:{C.RESET}")
print(f" {C.GRAY}{json.dumps([_sanitize(m) for m in messages], ensure_ascii=False, indent=2)[:2000]}{C.RESET}")
try:
stream = client.chat.completions.create(
model=MODEL,
messages=messages,
tools=tools,
tool_choice="required",
stream=True,
max_tokens=4096,
)
except Exception as e:
print(f"{C.RED}❌ API ERROR: {e}{C.RESET}")
return "", "", []
full_content = ""
full_reasoning = ""
tool_calls_map = {}
in_reasoning = False
in_content = False
chunk_count = 0
usage_info = None
for chunk in stream:
chunk_count += 1
if hasattr(chunk, "usage") and chunk.usage:
usage_info = chunk.usage
if not chunk.choices:
continue
delta = chunk.choices[0].delta
if not delta:
continue
reasoning_text = getattr(delta, "reasoning_content", None)
if reasoning_text:
full_reasoning += reasoning_text
if not in_reasoning:
in_reasoning = True
print(f"\n{C.PINK}💭 <think>{C.RESET}")
print(f"{C.PINK}", end="", flush=True)
print(f"{C.PINK}{reasoning_text}{C.RESET}", end="", flush=True)
if delta.content:
full_content += delta.content
if in_reasoning:
in_reasoning = False
print(f"{C.RESET}")
print(f"{C.PINK}💭 </think>{C.RESET}")
if not in_content:
in_content = True
print(f"{C.WHITE}💬 ", end="", flush=True)
print(f"{C.WHITE}{delta.content}{C.RESET}", end="", flush=True)
if delta.tool_calls:
for tc_delta in delta.tool_calls:
idx = tc_delta.index
if idx not in tool_calls_map:
tool_calls_map[idx] = {"id": "", "name": "", "arguments": ""}
if tc_delta.id:
tool_calls_map[idx]["id"] = tc_delta.id
if tc_delta.function:
if tc_delta.function.name:
tool_calls_map[idx]["name"] = tc_delta.function.name
if tc_delta.function.arguments:
tool_calls_map[idx]["arguments"] += tc_delta.function.arguments
if in_reasoning:
print(f"{C.RESET}")
print(f"{C.PINK}💭 </think>{C.RESET}")
if in_content:
print(f"{C.RESET}")
if debug:
print(f"\n {C.GRAY}[debug] チャンク数: {chunk_count} / reasoning: {len(full_reasoning)}文字 / "
f"content: {len(full_content)}文字 / tool_calls: {len(tool_calls_map)}{C.RESET}")
if usage_info:
print(f" {C.GRAY}[debug] usage: {usage_info}{C.RESET}")
for bad in ["画像が提供されていない", "画像が添付されて", "スクリーンショットが提供", "画像を見ることができ"]:
if bad in full_reasoning or bad in full_content:
print(f" {C.RED}[debug] ⚠️ モデルが画像を認識できていない疑い (キーワード: {bad}){C.RESET}")
break
tool_calls = [tool_calls_map[i] for i in sorted(tool_calls_map.keys())]
return full_content, full_reasoning, tool_calls
def parse_tool_args(tc: dict) -> dict:
try:
return json.loads(tc["arguments"]) if tc["arguments"] else {}
except json.JSONDecodeError as e:
print(f"{C.RED}❌ arguments の JSON parse 失敗: {e}{C.RESET}")
print(f"{C.DIM} raw: {tc['arguments']!r}{C.RESET}")
return {}
def run_agent(image_path: str, html_path: str, tts_engine, debug: bool = False) -> None:
# ═══════════════════════════════════════════════════
# STAGE 1: 画像を見て CAPTCHA 判定 + 本文抽出指示
# ═══════════════════════════════════════════════════
if debug:
img_size = os.path.getsize(image_path)
print(f"\n {C.GRAY}[debug] 画像: {image_path} ({img_size:,} bytes){C.RESET}")
image_content = make_image_content(image_path)
stage1_messages = [
{"role": "system", "content": SYSTEM_PROMPT_STAGE1},
{"role": "user", "content": [
image_content,
{"type": "text", "text": "このウェブページのスクリーンショットを見て、適切なツールを呼び出してください。"},
]},
]
_, _, stage1_tool_calls = stream_llm_call(
stage1_messages, TOOLS_STAGE1,
"🖼️ STAGE 1: 画像判定 (CAPTCHA検知 or 本文抽出)", debug
)
if not stage1_tool_calls:
print(f"\n{C.RED}❌ STAGE 1: ツールが呼ばれませんでした{C.RESET}")
return
print(f"\n{C.YELLOW}⚡ STAGE 1 ツール呼び出し: {len(stage1_tool_calls)}件{C.RESET}")
# STAGE 1 の結果処理
extracted_text = None
for tc in stage1_tool_calls:
name = tc["name"]
args = parse_tool_args(tc)
if name == "request_help":
# CAPTCHA 検知 → ヘルプ要請 → 理由読み上げ → 終了
reason = args.get("reason", "")
tool_request_help(reason)
if reason:
print(f" {C.DIM} → 理由もそのまま読み上げます{C.RESET}")
tool_speak_japanese(tts_engine, reason)
print(f"\n{C.YELLOW}🛑 ヘルプ要請で終了します{C.RESET}")
return
elif name == "extract_page_text":
# スクリプト側で HTML からテキスト抽出
print(f"\n {C.BG_CYAN}{C.WHITE} 📄 extract_page_text {C.RESET}")
extracted_text = extract_text_from_html(html_path)
print(f" {C.CYAN}✅ テキスト抽出完了: {len(extracted_text):,} 文字{C.RESET}")
if debug:
preview = extracted_text[:200].replace("\n", " ")
print(f" {C.GRAY}[debug] 先頭200文字: {preview}...{C.RESET}")
else:
print(f"{C.RED}❌ 未知のツール: {name}{C.RESET}")
if extracted_text is None:
print(f"\n{C.RED}❌ 本文抽出に進めませんでした{C.RESET}")
return
# ═══════════════════════════════════════════════════
# STAGE 2: 抽出テキストを要約して読み上げ
# ═══════════════════════════════════════════════════
stage2_messages = [
{"role": "system", "content": SYSTEM_PROMPT_STAGE2},
{"role": "user", "content": (
f"以下はウェブページから抽出した本文テキストです。"
f"内容を日本語で3〜5文に要約し、speak_japanese ツールで読み上げてください。\n\n"
f"--- ページ本文 ---\n{extracted_text}\n--- ここまで ---"
)},
]
_, _, stage2_tool_calls = stream_llm_call(
stage2_messages, TOOLS_STAGE2,
"📝 STAGE 2: 要約 + 読み上げ", debug
)
if not stage2_tool_calls:
print(f"\n{C.RED}❌ STAGE 2: ツールが呼ばれませんでした{C.RESET}")
return
print(f"\n{C.YELLOW}⚡ STAGE 2 ツール呼び出し: {len(stage2_tool_calls)}件{C.RESET}")
for tc in stage2_tool_calls:
name = tc["name"]
args = parse_tool_args(tc)
if name == "speak_japanese":
tool_speak_japanese(tts_engine, args.get("text", ""))
else:
print(f"{C.RED}❌ 未知のツール: {name}{C.RESET}")
# ============================================================
# メイン
# ============================================================
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--url", type=str, default=None, help="任意の URL を指定")
parser.add_argument("--captcha-demo", action="store_true",
help="reCAPTCHA デモページにアクセス (おまけ)")
parser.add_argument("--image", type=str, default=None,
help="スクショを撮らず既存画像を使う (--html と併用)")
parser.add_argument("--html", type=str, default=None,
help="既存 HTML ファイルを使う (--image と併用)")
parser.add_argument("--headless", action="store_true",
help="ブラウザをヘッドレスで起動 (デフォルトは見える状態)")
parser.add_argument("--debug", action="store_true",
help="デバッグ情報を表示")
args = parser.parse_args()
print(f"\n{C.BOLD}{C.CYAN}{'='*62}{C.RESET}")
print(f"{C.BOLD}{C.CYAN} 🤖 Gemma 4 E2B ローカル AI エージェントデモ (2段階){C.RESET}")
print(f"{C.BOLD}{C.CYAN} 🛠️ 画像判定 → 本文抽出 → 要約 → TTS{C.RESET}")
print(f"{C.BOLD}{C.CYAN}{'='*62}{C.RESET}")
print(f" {C.DIM}🧠 model: {MODEL}{C.RESET}")
print(f" {C.DIM}🔗 endpoint: {client.base_url}{C.RESET}")
if args.debug:
print(f" {C.YELLOW}🔍 デバッグモード ON{C.RESET}")
# TTS 初期化 (Haruka が無ければここで終了)
tts_engine = init_tts()
# 画像 + HTML 準備
if args.image:
image_path = args.image
html_path = args.html or HTML_PATH
if not os.path.exists(image_path):
print(f"{C.RED}❌ 画像が見つかりません: {image_path}{C.RESET}")
sys.exit(1)
if not os.path.exists(html_path):
print(f"{C.RED}❌ HTML が見つかりません: {html_path}{C.RESET}")
print(f"{C.DIM} --html で指定するか、--image なしで実行してください{C.RESET}")
sys.exit(1)
print(f" {C.DIM}🖼️ 既存画像: {image_path}{C.RESET}")
print(f" {C.DIM}📄 既存 HTML: {html_path}{C.RESET}")
else:
if args.captcha_demo:
url = RECAPTCHA_DEMO_URL
print(f" {C.YELLOW}🎭 CAPTCHA デモモード{C.RESET}")
elif args.url:
url = args.url
else:
url = HACKER_NEWS_URL
image_path = SCREENSHOT_PATH
html_path = HTML_PATH
capture_page(url, image_path, html_path, headless=args.headless)
# エージェント実行
run_agent(image_path, html_path, tts_engine, debug=args.debug)
print(f"\n{C.BOLD}{C.CYAN}{'='*62}{C.RESET}")
print(f"{C.BOLD}{C.GREEN} ✅ デモ終了{C.RESET}")
print(f"{C.BOLD}{C.CYAN}{'='*62}{C.RESET}\n")
if __name__ == "__main__":
main()| Model Name | Strict Prompt | Strict Inst | Loose Prompt | Loose Inst |
|---|---|---|---|---|
| Unsloth-Q4_K_XL | 0.6162 | 0.6902 | 0.6802 | 0.7389 |
| gemma-4-E2B-it-UD-japanese-imatrix-Q4_K_XL | 0.6337 | 0.6991 | 0.7151 | 0.7610 |
| gemma-4-E2B-it-UD-japanese-imatrix-Q4_K_XL(4/24 prompt fix) | 0.6337 | 0.6814 | 0.7034 | 0.7434 |