增加Kimi多模态Api,这样可以灵活选择!
This commit is contained in:
@@ -15,7 +15,7 @@ from tkinterdnd2 import DND_FILES, TkinterDnD
|
||||
from exp_multimodal.labels import build_labels, _normalize, _base_ingredient
|
||||
from exp_multimodal.vlm_classifier import classify_image
|
||||
from exp_multimodal.ollama_client import OLLAMA_URL as DEFAULT_OLLAMA_URL, DEFAULT_MODEL as DEFAULT_VLM_MODEL
|
||||
from exp_multimodal import ollama_client as _ollama_mod
|
||||
from exp_multimodal.vlm_providers import VLMProvider, OllamaProvider, KimiProvider
|
||||
|
||||
|
||||
ctk.set_appearance_mode("System")
|
||||
@@ -38,8 +38,15 @@ class MultiModalFoodApp:
|
||||
|
||||
# 配置
|
||||
self.mode_var = ctk.StringVar(value="dish") # dish | whole | processed
|
||||
|
||||
# VLM Provider 配置
|
||||
self.provider_var = ctk.StringVar(value="ollama") # ollama | kimi
|
||||
self.ollama_url_var = ctk.StringVar(value=DEFAULT_OLLAMA_URL)
|
||||
self.vlm_model_var = ctk.StringVar(value=DEFAULT_VLM_MODEL)
|
||||
self.kimi_api_key_var = ctk.StringVar(value="")
|
||||
self.kimi_base_url_var = ctk.StringVar(value="https://api.moonshot.cn/v1")
|
||||
self.kimi_model_var = ctk.StringVar(value="moonshot-v1-32k-vision-preview")
|
||||
|
||||
self.alias_map_path: Optional[str] = None
|
||||
|
||||
# Fewshot 可视化编辑(C 方案): dict[label] = hint
|
||||
@@ -58,6 +65,9 @@ class MultiModalFoodApp:
|
||||
|
||||
# UI
|
||||
self.create_widgets()
|
||||
|
||||
# 加载保存的配置
|
||||
self.load_config()
|
||||
|
||||
# -------------------- 图像加载工具 --------------------
|
||||
def load_image_with_chinese_path(self, file_path: str):
|
||||
@@ -323,14 +333,58 @@ class MultiModalFoodApp:
|
||||
rb = ctk.CTkRadioButton(mode_frame, text=text, variable=self.mode_var, value=val)
|
||||
rb.pack(side="left", padx=8, pady=8)
|
||||
|
||||
# 服务配置
|
||||
ctk.CTkLabel(panel, text="VLM 服务配置", font=("Arial", 14, "bold")).pack(anchor="w", pady=(10, 6))
|
||||
svc = ctk.CTkFrame(panel)
|
||||
svc.pack(fill="x", pady=(0, 10))
|
||||
ctk.CTkLabel(svc, text="OLLAMA_URL:").pack(side="left", padx=6, pady=8)
|
||||
ctk.CTkEntry(svc, textvariable=self.ollama_url_var, width=320).pack(side="left", padx=4, pady=8)
|
||||
ctk.CTkLabel(svc, text="VLM_MODEL:").pack(side="left", padx=12, pady=8)
|
||||
ctk.CTkEntry(svc, textvariable=self.vlm_model_var, width=220).pack(side="left", padx=4, pady=8)
|
||||
# VLM 服务提供商选择
|
||||
ctk.CTkLabel(panel, text="VLM 服务提供商", font=("Arial", 14, "bold")).pack(anchor="w", pady=(10, 6))
|
||||
provider_frame = ctk.CTkFrame(panel)
|
||||
provider_frame.pack(fill="x", pady=(0, 10))
|
||||
|
||||
for val, text in [("ollama", "Ollama (本地/自建)"), ("kimi", "Kimi 1.5 (厂商API)")]:
|
||||
rb = ctk.CTkRadioButton(
|
||||
provider_frame,
|
||||
text=text,
|
||||
variable=self.provider_var,
|
||||
value=val,
|
||||
command=self.on_provider_change
|
||||
)
|
||||
rb.pack(side="left", padx=8, pady=8)
|
||||
|
||||
# Ollama 配置区域
|
||||
self.ollama_config_frame = ctk.CTkFrame(panel)
|
||||
self.ollama_config_frame.pack(fill="x", pady=(0, 10))
|
||||
ctk.CTkLabel(self.ollama_config_frame, text="Ollama 配置", font=("Arial", 12, "bold")).pack(anchor="w", pady=(4, 4))
|
||||
|
||||
ollama_row1 = ctk.CTkFrame(self.ollama_config_frame)
|
||||
ollama_row1.pack(fill="x", pady=(0, 4))
|
||||
ctk.CTkLabel(ollama_row1, text="OLLAMA_URL:").pack(side="left", padx=6, pady=4)
|
||||
ctk.CTkEntry(ollama_row1, textvariable=self.ollama_url_var, width=420).pack(side="left", padx=4, pady=4)
|
||||
|
||||
ollama_row2 = ctk.CTkFrame(self.ollama_config_frame)
|
||||
ollama_row2.pack(fill="x", pady=(0, 4))
|
||||
ctk.CTkLabel(ollama_row2, text="MODEL:").pack(side="left", padx=6, pady=4)
|
||||
ctk.CTkEntry(ollama_row2, textvariable=self.vlm_model_var, width=420).pack(side="left", padx=4, pady=4)
|
||||
|
||||
# Kimi 配置区域
|
||||
self.kimi_config_frame = ctk.CTkFrame(panel)
|
||||
self.kimi_config_frame.pack(fill="x", pady=(0, 10))
|
||||
ctk.CTkLabel(self.kimi_config_frame, text="Kimi 配置", font=("Arial", 12, "bold")).pack(anchor="w", pady=(4, 4))
|
||||
|
||||
kimi_row1 = ctk.CTkFrame(self.kimi_config_frame)
|
||||
kimi_row1.pack(fill="x", pady=(0, 4))
|
||||
ctk.CTkLabel(kimi_row1, text="API_KEY:").pack(side="left", padx=6, pady=4)
|
||||
ctk.CTkEntry(kimi_row1, textvariable=self.kimi_api_key_var, width=420, show="*").pack(side="left", padx=4, pady=4)
|
||||
|
||||
kimi_row2 = ctk.CTkFrame(self.kimi_config_frame)
|
||||
kimi_row2.pack(fill="x", pady=(0, 4))
|
||||
ctk.CTkLabel(kimi_row2, text="BASE_URL:").pack(side="left", padx=6, pady=4)
|
||||
ctk.CTkEntry(kimi_row2, textvariable=self.kimi_base_url_var, width=420).pack(side="left", padx=4, pady=4)
|
||||
|
||||
kimi_row3 = ctk.CTkFrame(self.kimi_config_frame)
|
||||
kimi_row3.pack(fill="x", pady=(0, 4))
|
||||
ctk.CTkLabel(kimi_row3, text="MODEL:").pack(side="left", padx=6, pady=4)
|
||||
ctk.CTkEntry(kimi_row3, textvariable=self.kimi_model_var, width=420).pack(side="left", padx=4, pady=4)
|
||||
|
||||
# 初始化显示状态
|
||||
self.on_provider_change()
|
||||
|
||||
# Alias Map
|
||||
alias = ctk.CTkFrame(panel)
|
||||
@@ -385,6 +439,16 @@ class MultiModalFoodApp:
|
||||
self.refresh_extra_labels_view()
|
||||
self.refresh_fewshot_view()
|
||||
|
||||
# 配置保存/加载
|
||||
cfg_manage = ctk.CTkFrame(panel)
|
||||
cfg_manage.pack(fill="x", pady=(10, 10))
|
||||
ctk.CTkLabel(cfg_manage, text="配置管理", font=("Arial", 14, "bold")).pack(anchor="w", pady=(0, 6))
|
||||
cfg_row = ctk.CTkFrame(cfg_manage)
|
||||
cfg_row.pack(fill="x")
|
||||
ctk.CTkButton(cfg_row, text="保存当前配置", command=self.save_config, width=140, fg_color="blue", hover_color="darkblue").pack(side="left", padx=6)
|
||||
ctk.CTkButton(cfg_row, text="加载配置", command=self.load_config_from_file, width=140).pack(side="left", padx=6)
|
||||
ctk.CTkLabel(cfg_row, text="(自动保存到 config.json)", font=("Arial", 10), text_color="gray").pack(side="left", padx=12)
|
||||
|
||||
def pick_alias_file(self):
|
||||
path = filedialog.askopenfilename(title="选择 alias_map.json", filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")])
|
||||
if path:
|
||||
@@ -395,6 +459,16 @@ class MultiModalFoodApp:
|
||||
self.alias_map_path = None
|
||||
self.alias_label_var.set("未选择")
|
||||
|
||||
def on_provider_change(self):
|
||||
"""切换 Provider 时显示/隐藏对应的配置区域"""
|
||||
provider = self.provider_var.get()
|
||||
if provider == "ollama":
|
||||
self.ollama_config_frame.pack(fill="x", pady=(0, 10), before=self.kimi_config_frame)
|
||||
self.kimi_config_frame.pack_forget()
|
||||
elif provider == "kimi":
|
||||
self.kimi_config_frame.pack(fill="x", pady=(0, 10), before=self.ollama_config_frame)
|
||||
self.ollama_config_frame.pack_forget()
|
||||
|
||||
# 额外标签编辑
|
||||
def refresh_extra_labels_view(self):
|
||||
self.extra_labels_listbox.configure(state="normal")
|
||||
@@ -562,12 +636,13 @@ class MultiModalFoodApp:
|
||||
labels = self._build_final_labels(mode)
|
||||
ingredient_only = (mode in {"whole", "processed"})
|
||||
use_hints = self.fewshot_hints if self.fewshot_enabled_var.get() else None
|
||||
# 环境覆盖(仅对运行时生效,不修改模块常量)
|
||||
os.environ["OLLAMA_URL"] = self.ollama_url_var.get().strip() or DEFAULT_OLLAMA_URL
|
||||
os.environ["VLM_MODEL"] = self.vlm_model_var.get().strip() or DEFAULT_VLM_MODEL
|
||||
# 同时覆盖已导入模块中的运行时常量,确保 classify 调用使用最新配置
|
||||
_ollama_mod.OLLAMA_URL = os.environ["OLLAMA_URL"]
|
||||
_ollama_mod.DEFAULT_MODEL = os.environ["VLM_MODEL"]
|
||||
|
||||
# 创建 VLM Provider
|
||||
provider = self._create_vlm_provider()
|
||||
if provider is None:
|
||||
self.root.after(0, lambda: messagebox.showerror("错误", "Provider 配置错误,请检查配置"))
|
||||
self.root.after(0, self.recognition_completed)
|
||||
return
|
||||
|
||||
self.current_results.clear()
|
||||
for i, img in enumerate(self.uploaded_images):
|
||||
@@ -575,12 +650,13 @@ class MultiModalFoodApp:
|
||||
result = classify_image(
|
||||
image_path=img['path'],
|
||||
labels=labels,
|
||||
provider=provider,
|
||||
fewshot_hints=use_hints,
|
||||
ingredient_only=ingredient_only,
|
||||
)
|
||||
pred = result.get("label", "Unknown")
|
||||
conf = result.get("confidence", 0.0)
|
||||
# 若非数值,后续展示为 N/A
|
||||
# 若非数值,后续展示为 N/A
|
||||
if not isinstance(conf, (int, float)):
|
||||
try:
|
||||
conf = float(conf)
|
||||
@@ -619,6 +695,33 @@ class MultiModalFoodApp:
|
||||
self.root.after(0, lambda: messagebox.showerror("错误", f"识别过程中出错: {e}"))
|
||||
self.root.after(0, self.recognition_completed)
|
||||
|
||||
def _create_vlm_provider(self) -> Optional[VLMProvider]:
|
||||
"""根据配置创建 VLM Provider"""
|
||||
provider_type = self.provider_var.get()
|
||||
|
||||
try:
|
||||
if provider_type == "ollama":
|
||||
url = self.ollama_url_var.get().strip() or DEFAULT_OLLAMA_URL
|
||||
model = self.vlm_model_var.get().strip() or DEFAULT_VLM_MODEL
|
||||
return OllamaProvider(ollama_url=url, model=model)
|
||||
|
||||
elif provider_type == "kimi":
|
||||
api_key = self.kimi_api_key_var.get().strip()
|
||||
if not api_key:
|
||||
messagebox.showerror("错误", "Kimi API Key 不能为空")
|
||||
return None
|
||||
base_url = self.kimi_base_url_var.get().strip() or "https://api.moonshot.cn/v1"
|
||||
model = self.kimi_model_var.get().strip() or "moonshot-v1-32k-vision-preview"
|
||||
return KimiProvider(api_key=api_key, base_url=base_url, model=model)
|
||||
|
||||
else:
|
||||
messagebox.showerror("错误", f"未知的 Provider 类型: {provider_type}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"创建 Provider 失败: {e}")
|
||||
return None
|
||||
|
||||
def update_progress(self, current: int, total: int):
|
||||
self.recognize_button.configure(text=f"识别中... ({current}/{total})")
|
||||
self.update_images_display()
|
||||
@@ -631,6 +734,142 @@ class MultiModalFoodApp:
|
||||
self.update_stats()
|
||||
messagebox.showinfo("完成", f"所有图片识别完成!耗时: {self.recognition_duration:.2f}秒")
|
||||
|
||||
# -------------------- 配置管理 --------------------
|
||||
def get_config_path(self) -> str:
|
||||
"""获取配置文件路径(项目根目录下的 config.json)"""
|
||||
return os.path.join(os.path.dirname(os.path.dirname(__file__)), "vlm_config.json")
|
||||
|
||||
def save_config(self):
|
||||
"""保存当前配置到 JSON 文件"""
|
||||
config = {
|
||||
"mode": self.mode_var.get(),
|
||||
"provider": self.provider_var.get(),
|
||||
"ollama": {
|
||||
"url": self.ollama_url_var.get(),
|
||||
"model": self.vlm_model_var.get(),
|
||||
},
|
||||
"kimi": {
|
||||
"api_key": self.kimi_api_key_var.get(),
|
||||
"base_url": self.kimi_base_url_var.get(),
|
||||
"model": self.kimi_model_var.get(),
|
||||
},
|
||||
"alias_map_path": self.alias_map_path,
|
||||
"fewshot_enabled": self.fewshot_enabled_var.get(),
|
||||
"fewshot_hints": self.fewshot_hints,
|
||||
"fewshot_file_path": self.fewshot_file_path,
|
||||
"extra_labels": self.extra_labels,
|
||||
"extra_labels_file_path": self.extra_labels_file_path,
|
||||
}
|
||||
|
||||
try:
|
||||
config_path = self.get_config_path()
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=2)
|
||||
messagebox.showinfo("成功", f"配置已保存到 {os.path.basename(config_path)}")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存配置失败: {e}")
|
||||
|
||||
def load_config(self):
|
||||
"""自动加载配置(启动时调用)"""
|
||||
config_path = self.get_config_path()
|
||||
if not os.path.exists(config_path):
|
||||
return
|
||||
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
# 恢复配置
|
||||
if "mode" in config:
|
||||
self.mode_var.set(config["mode"])
|
||||
if "provider" in config:
|
||||
self.provider_var.set(config["provider"])
|
||||
|
||||
if "ollama" in config:
|
||||
self.ollama_url_var.set(config["ollama"].get("url", DEFAULT_OLLAMA_URL))
|
||||
self.vlm_model_var.set(config["ollama"].get("model", DEFAULT_VLM_MODEL))
|
||||
|
||||
if "kimi" in config:
|
||||
self.kimi_api_key_var.set(config["kimi"].get("api_key", ""))
|
||||
self.kimi_base_url_var.set(config["kimi"].get("base_url", "https://api.moonshot.cn/v1"))
|
||||
self.kimi_model_var.set(config["kimi"].get("model", "moonshot-v1-32k-vision-preview"))
|
||||
|
||||
if "alias_map_path" in config and config["alias_map_path"]:
|
||||
self.alias_map_path = config["alias_map_path"]
|
||||
self.alias_label_var.set(os.path.basename(self.alias_map_path))
|
||||
|
||||
if "fewshot_enabled" in config:
|
||||
self.fewshot_enabled_var.set(config["fewshot_enabled"])
|
||||
if "fewshot_hints" in config:
|
||||
self.fewshot_hints = config["fewshot_hints"]
|
||||
if "fewshot_file_path" in config:
|
||||
self.fewshot_file_path = config["fewshot_file_path"]
|
||||
|
||||
if "extra_labels" in config:
|
||||
self.extra_labels = config["extra_labels"]
|
||||
if "extra_labels_file_path" in config:
|
||||
self.extra_labels_file_path = config["extra_labels_file_path"]
|
||||
|
||||
self.refresh_extra_labels_view()
|
||||
self.refresh_fewshot_view()
|
||||
self.on_provider_change()
|
||||
|
||||
print(f"[Config] 配置已从 {config_path} 加载")
|
||||
except Exception as e:
|
||||
print(f"[Config] 加载配置失败: {e}")
|
||||
|
||||
def load_config_from_file(self):
|
||||
"""从用户选择的文件加载配置"""
|
||||
path = filedialog.askopenfilename(
|
||||
title="选择配置文件",
|
||||
filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")]
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
|
||||
# 恢复配置(同上)
|
||||
if "mode" in config:
|
||||
self.mode_var.set(config["mode"])
|
||||
if "provider" in config:
|
||||
self.provider_var.set(config["provider"])
|
||||
|
||||
if "ollama" in config:
|
||||
self.ollama_url_var.set(config["ollama"].get("url", DEFAULT_OLLAMA_URL))
|
||||
self.vlm_model_var.set(config["ollama"].get("model", DEFAULT_VLM_MODEL))
|
||||
|
||||
if "kimi" in config:
|
||||
self.kimi_api_key_var.set(config["kimi"].get("api_key", ""))
|
||||
self.kimi_base_url_var.set(config["kimi"].get("base_url", "https://api.moonshot.cn/v1"))
|
||||
self.kimi_model_var.set(config["kimi"].get("model", "moonshot-v1-32k-vision-preview"))
|
||||
|
||||
if "alias_map_path" in config and config["alias_map_path"]:
|
||||
self.alias_map_path = config["alias_map_path"]
|
||||
self.alias_label_var.set(os.path.basename(self.alias_map_path))
|
||||
|
||||
if "fewshot_enabled" in config:
|
||||
self.fewshot_enabled_var.set(config["fewshot_enabled"])
|
||||
if "fewshot_hints" in config:
|
||||
self.fewshot_hints = config["fewshot_hints"]
|
||||
if "fewshot_file_path" in config:
|
||||
self.fewshot_file_path = config["fewshot_file_path"]
|
||||
|
||||
if "extra_labels" in config:
|
||||
self.extra_labels = config["extra_labels"]
|
||||
if "extra_labels_file_path" in config:
|
||||
self.extra_labels_file_path = config["extra_labels_file_path"]
|
||||
|
||||
self.refresh_extra_labels_view()
|
||||
self.refresh_fewshot_view()
|
||||
self.on_provider_change()
|
||||
|
||||
messagebox.showinfo("成功", f"配置已从 {os.path.basename(path)} 加载")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"加载配置失败: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
root = TkinterDnD.Tk() # 必须使用 TkinterDnD.Tk 以支持拖拽
|
||||
|
||||
Reference in New Issue
Block a user