ai.translate5_GUI のソースコード

import os
import threading
import configparser
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import traceback

try:
    from ttkthemes import ThemedTk
except Exception:
    ThemedTk = None

#from translate5 import (
from translate5_com import (
    initialize,
    update_variables,
    execute,
)

PROGRAM_NAME = "translate5 GUI Runner"

# translate5.py に合わせた API 一覧
API_CHOICES = ["openai5", "openai", "google", "gemini", "deepl"]

DEFAULT_GEMINI_MODEL = "gemini-3.1-pro-preview"
DEFAULT_OPENAI_MODEL4 = "gpt-4o"
DEFAULT_OPENAI_MODEL5 = "gpt-5.4"



OPENAI_MODELS_V4 = ["gpt-4o", "gpt-4.5", "gpt-3.5-turbo"]
OPENAI_MODELS_V5 = ["gpt-5.4-pro", "gpt-5.3-chat-latest", "gpt-5-mini", "gpt-5-nano", "gpt-3.5-turbo-16k"]
GOOGLE_MODELS = ["gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-light"]
DEEPL_MODELS = ["(auto)"]

REASONING_EFFORTS = ["low", "medium", "high"]
PROCESS_UNITS = ["paragraph", "run", "md"]  # md は GUI 専用で use_md フラグにマップ
MODES = [("日本語→英語 (je)", "je"), ("英語→日本語 (ej)", "ej")]

CONFIG_INI = os.path.splitext(os.path.abspath(__file__))[0] + ".ini"


[ドキュメント] class CopyableErrorDialog(tk.Toplevel): def __init__(self, parent, title, message): super().__init__(parent) self.title(title) self.geometry("700x400") self.transient(parent) self.grab_set() text_widget = tk.Text(self, wrap="word", font=("Courier", 10)) text_widget.pack(padx=10, pady=10, fill="both", expand=True) text_widget.insert("1.0", message) text_widget.config(state="disabled") button = ttk.Button(self, text="Close", command=self.destroy) button.pack(pady=10) self.update_idletasks() x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2) y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2) self.geometry(f"+{x}+{y}") button.focus_set() self.wait_window(self)
[ドキュメント] class App: def __init__(self, master): self.master = master master.title(PROGRAM_NAME) master.geometry("960x800") self.config = configparser.ConfigParser(comment_prefixes=('~',)) print("--- [INFO] Initialize translate5 config ---") self.cfg, self.parser = initialize() self.cfg = update_variables(self.cfg, self.parser) print("endpoint (from env/args) =", getattr(self.cfg, "endpoint", None)) self._busy = False self._build_ui() self._load_config() self._apply_api_to_models() master.protocol("WM_DELETE_WINDOW", self.on_close) # ---------------- UI 構築 ---------------- def _build_ui(self): # Input / Template io_frame = ttk.LabelFrame(self.master, text="Input / Template") io_frame.pack(fill="x", padx=10, pady=8) ttk.Label(io_frame, text="Input file (.docx/.pptx/.pdf/.html/.md/.txt):").grid( row=0, column=0, sticky="w", padx=6, pady=6 ) self.infile_var = tk.StringVar( value=self.cfg.infile if hasattr(self.cfg, "infile") else "" ) e_in = ttk.Entry(io_frame, textvariable=self.infile_var) e_in.grid(row=0, column=1, sticky="ew", padx=6, pady=6) ttk.Button(io_frame, text="Browse", command=self._pick_infile).grid( row=0, column=2, padx=6, pady=6 ) ttk.Label(io_frame, text="HTML template (template_translate.html):").grid( row=1, column=0, sticky="w", padx=6, pady=6 ) self.tpl_var = tk.StringVar( value=getattr(self.cfg, "html_template_path", "template_translate.html") ) e_tpl = ttk.Entry(io_frame, textvariable=self.tpl_var) e_tpl.grid(row=1, column=1, sticky="ew", padx=6, pady=6) ttk.Button(io_frame, text="Browse", command=self._pick_template).grid( row=1, column=2, padx=6, pady=6 ) io_frame.grid_columnconfigure(1, weight=1) # Mode / Options mode_frame = ttk.LabelFrame(self.master, text="Mode / Options") mode_frame.pack(fill="x", padx=10, pady=8) self.mode_var = tk.StringVar(value=getattr(self.cfg, "mode", "je")) col = 0 for label, val in MODES: ttk.Radiobutton( mode_frame, text=label, variable=self.mode_var, value=val ).grid(row=0, column=col, padx=6, pady=6, sticky="w") col += 1 ttk.Label(mode_frame, text="Process unit:").grid( row=0, column=col, padx=12, pady=6, sticky="e" ) self.proc_unit_var = tk.StringVar( value=getattr(self.cfg, "process_unit", "paragraph") ) cb_proc = ttk.Combobox( mode_frame, state="readonly", values=PROCESS_UNITS, textvariable=self.proc_unit_var, width=10, ) cb_proc.grid(row=0, column=col + 1, padx=6, pady=6, sticky="w") # API api_frame = ttk.LabelFrame(self.master, text="API") api_frame.pack(fill="x", padx=10, pady=8) ttk.Label(api_frame, text="API:").grid( row=0, column=0, padx=6, pady=6, sticky="w" ) self.api_var = tk.StringVar( value=getattr(self.cfg, "api", "openai5") ) self.api_cb = ttk.Combobox( api_frame, state="readonly", values=API_CHOICES, textvariable=self.api_var, width=10, ) self.api_cb.grid(row=0, column=1, padx=6, pady=6, sticky="w") self.api_cb.bind("<<ComboboxSelected>>", lambda e: self._apply_api_to_models()) ttk.Label(api_frame, text="Model:").grid( row=0, column=2, padx=12, pady=6, sticky="e" ) self.model_var = tk.StringVar(value="") self.model_cb = ttk.Combobox( api_frame, state="readonly", values=[], textvariable=self.model_var, width=24, ) self.model_cb.grid(row=0, column=3, padx=6, pady=6, sticky="w") # Endpointは DeepL/custom 用のみ ttk.Label(api_frame, text="Endpoint (DeepL / custom):").grid( row=0, column=4, padx=12, pady=6, sticky="e" ) endpoint_default = os.environ.get("endpoint", "") print("endpoint_default(env) =", endpoint_default) self.endpoint_var = tk.StringVar( value=endpoint_default if endpoint_default else "" ) ttk.Entry(api_frame, textvariable=self.endpoint_var, width=50).grid( row=0, column=5, padx=6, pady=6, sticky="w" ) api_frame.grid_columnconfigure(6, weight=1) # Generation Parameters gen_frame = ttk.LabelFrame(self.master, text="Generation Parameters") gen_frame.pack(fill="x", padx=10, pady=8) ttk.Label(gen_frame, text="temperature (OpenAI/Google/Gemini):").grid( row=0, column=0, padx=6, pady=6, sticky="e" ) self.temp_var = tk.StringVar( value=str(getattr(self.cfg, "temperature", 0.3)) ) ttk.Entry(gen_frame, textvariable=self.temp_var, width=8).grid( row=0, column=1, padx=6, pady=6, sticky="w" ) ttk.Label(gen_frame, text="max_tokens (or max_output_tokens):").grid( row=0, column=2, padx=12, pady=6, sticky="e" ) self.max_tok_var = tk.StringVar( value=str(getattr(self.cfg, "max_tokens", 2000)) ) ttk.Entry(gen_frame, textvariable=self.max_tok_var, width=10).grid( row=0, column=3, padx=6, pady=6, sticky="w" ) ttk.Label(gen_frame, text="reasoning_effort (OpenAI5):").grid( row=0, column=4, padx=12, pady=6, sticky="e" ) self.effort_var = tk.StringVar( value=getattr(self.cfg, "reasoning_effort", "low") ) ttk.Combobox( gen_frame, state="readonly", values=REASONING_EFFORTS, textvariable=self.effort_var, width=8, ).grid(row=0, column=5, padx=6, pady=6, sticky="w") # Translation Rules rule_frame = ttk.LabelFrame(self.master, text="Translation Rules") rule_frame.pack(fill="x", padx=10, pady=8) ttk.Label(rule_frame, text="min_translate_length:").grid( row=0, column=0, padx=6, pady=6, sticky="e" ) self.min_len_var = tk.StringVar( value=str(getattr(self.cfg, "min_translate_length", 5)) ) ttk.Entry(rule_frame, textvariable=self.min_len_var, width=6).grid( row=0, column=1, padx=6, pady=6, sticky="w" ) ttk.Label(rule_frame, text="allowed_translation_length_ratio:").grid( row=0, column=2, padx=12, pady=6, sticky="e" ) self.len_ratio_var = tk.StringVar( value=str(getattr(self.cfg, "allowed_translation_length_ratio", 5.0)) ) ttk.Entry(rule_frame, textvariable=self.len_ratio_var, width=6).grid( row=0, column=3, padx=6, pady=6, sticky="w" ) self.limit_mb_var = tk.BooleanVar( value=bool( getattr( self.cfg, "limit_to_multibyte_str", self._mode_implies_mb(getattr(self.cfg, "mode", "je")), ) ) ) ttk.Checkbutton( rule_frame, text="Limit to multibyte strings (JA source)", variable=self.limit_mb_var, ).grid(row=0, column=4, padx=12, pady=6, sticky="w") # Prompts(translate_prompt / reformat_prompt のみ) rp_frame = ttk.LabelFrame(self.master, text="Prompts") rp_frame.pack(fill="both", expand=True, padx=10, pady=8) ttk.Label(rp_frame, text="Translate Prompt (full instructions):").grid( row=0, column=0, padx=6, pady=6, sticky="nw" ) self.tr_prompt = tk.Text(rp_frame, height=8, wrap="word") self.tr_prompt.grid(row=0, column=1, padx=6, pady=6, sticky="nsew") self.tr_prompt.insert( "1.0", getattr(self.cfg, "translate_prompt", ""), ) ttk.Label(rp_frame, text="Reformat Prompt (PDF→MD etc.):").grid( row=1, column=0, padx=6, pady=6, sticky="nw" ) self.rf_prompt = tk.Text(rp_frame, height=6, wrap="word") self.rf_prompt.grid(row=1, column=1, padx=6, pady=6, sticky="nsew") self.rf_prompt.insert( "1.0", getattr(self.cfg, "reformat_prompt", ""), ) rp_frame.grid_columnconfigure(1, weight=1) rp_frame.grid_rowconfigure(0, weight=1) rp_frame.grid_rowconfigure(1, weight=1) # Buttons btn_frame = ttk.Frame(self.master) btn_frame.pack(pady=10) self.run_btn = ttk.Button( btn_frame, text="Run execute()", command=self._run_execute_thread ) self.run_btn.grid(row=0, column=0, padx=6) ttk.Button(btn_frame, text="Close", command=self.on_close).grid( row=0, column=1, padx=6 ) # Status bar self.status = ttk.Label( self.master, text="Ready", relief="sunken", anchor="w" ) self.status.pack(fill="x", padx=0, pady=(4, 0), ipady=2) def _mode_implies_mb(self, mode): return True if mode and mode.startswith("j") else False def _pick_infile(self): fp = filedialog.askopenfilename( title="Select input file", filetypes=[ ( "Supported", "*.docx;*.pptx;*.pdf;*.html;*.htm;*.md;*.txt", ), ("All files", "*.*"), ], ) if fp: self.infile_var.set(fp) def _pick_template(self): fp = filedialog.askopenfilename( title="Select HTML template", filetypes=[("HTML", "*.html;*.htm"), ("All files", "*.*")], ) if fp: self.tpl_var.set(fp) # APIごとにモデル候補を切り替え(translate5仕様に合わせ) def _apply_api_to_models(self): api = self.api_var.get() if api == "openai": self.model_cb["values"] = OPENAI_MODELS_V4 default = getattr(self.cfg, "openai_model", "gpt-4o") self.model_var.set( default if default in OPENAI_MODELS_V4 else "gpt-4o" ) elif api == "openai5": self.model_cb["values"] = OPENAI_MODELS_V5 default = getattr(self.cfg, "openai_model5", "gpt-5-nano") self.model_var.set( default if default in OPENAI_MODELS_V5 else "gpt-5-nano" ) elif api in ("google", "gemini"): self.model_cb["values"] = GOOGLE_MODELS new_default = GOOGLE_MODELS[0] if GOOGLE_MODELS else "" default = getattr(self.cfg, "google_model", new_default) self.model_var.set( default if default in GOOGLE_MODELS else new_default ) elif api == "deepl": self.model_cb["values"] = DEEPL_MODELS self.model_var.set("(auto)") else: self.model_cb["values"] = [] self.model_var.set("") def _worker_run(self): try: execute(self.cfg) self._set_status("Done.") except SystemExit: self._set_status("Finished (SystemExit).") except Exception as e: error_details = traceback.format_exc() self._set_status(f"Error: {e}") self._show_error_async("Execution Error", error_details) finally: self.master.after( 0, lambda: self.run_btn.config(state="normal") ) self._busy = False def _show_error_async(self, title, msg): self.master.after( 0, lambda: CopyableErrorDialog(self.master, title, msg) ) def _run_execute_thread(self): if self._busy: return try: self._update_cfg_from_ui() except Exception as e: messagebox.showerror( "設定エラー", f"設定の読み取りに失敗しました:\n{e}", ) return if not os.path.isfile(self.cfg.infile): messagebox.showerror( "入力エラー", f"入力ファイルが見つかりません:\n{self.cfg.infile}", ) return self._busy = True self.status.config(text="Running execute() ...") self.run_btn.config(state="disabled") t = threading.Thread(target=self._worker_run) t.daemon = True t.start() def _set_status(self, text): self.master.after(0, lambda: self.status.config(text=text)) def _update_cfg_from_ui(self): # 基本情報 self.cfg.infile = self.infile_var.get().strip() self.cfg.html_template_path = ( self.tpl_var.get().strip() or "template_translate.html" ) self.cfg.mode = self.mode_var.get() # process_unit / use_md (md は GUI 独自指定として use_md=True にマップ) proc_unit = self.proc_unit_var.get() if proc_unit == "md": self.cfg.use_md = True # 実際の処理で使わないが一応 paragraph にしておく self.cfg.process_unit = "paragraph" else: self.cfg.use_md = False self.cfg.process_unit = proc_unit or "paragraph" # API / model api = self.api_var.get() self.cfg.api = api model = self.model_var.get() if api == "openai": self.cfg.openai_model = model self.cfg.openai_model5 = None self.cfg.google_model = None elif api == "openai5": self.cfg.openai_model5 = model self.cfg.openai_model = None self.cfg.google_model = None elif api in ("google", "gemini"): self.cfg.google_model = model self.cfg.openai_model = None self.cfg.openai_model5 = None elif api == "deepl": self.cfg.openai_model = None self.cfg.openai_model5 = None self.cfg.google_model = None else: self.cfg.openai_model = None self.cfg.openai_model5 = None self.cfg.google_model = None # Endpoint は DeepL / custom のみ使用 if api == "deepl": self.cfg.endpoint = ( self.endpoint_var.get().strip() or getattr(self.cfg, "endpoint", None) ) else: self.cfg.endpoint = None # 各種パラメータ self.cfg.temperature = float(self.temp_var.get()) self.cfg.max_tokens = int(float(self.max_tok_var.get())) self.cfg.reasoning_effort = self.effort_var.get() self.cfg.min_translate_length = int(float(self.min_len_var.get())) self.cfg.allowed_translation_length_ratio = float( self.len_ratio_var.get() ) self.cfg.limit_to_multibyte_str = bool(self.limit_mb_var.get()) # translate5 新仕様: prompt は一体型のみ self.cfg.translate_prompt = ( self.tr_prompt.get("1.0", "end").strip() or getattr(self.cfg, "translate_prompt", "") ) self.cfg.reformat_prompt = ( self.rf_prompt.get("1.0", "end").strip() or getattr(self.cfg, "reformat_prompt", "") ) # APIキーなど(環境変数から) self.cfg.openai_api_key = os.getenv("OPENAI_API_KEY") self.cfg.google_api_key = os.getenv("GOOGLE_API_KEY") self.cfg.deepl_api_key = os.getenv("DEEPL_API_KEY") # その他固定値(translate5 と同等の既定値) self.cfg.output_html_path = None self.cfg.force_server_charcode = "utf-8" self.cfg.tsleep_rpm = getattr(self.cfg, "tsleep_rpm", 0.0) self.cfg.top_p = getattr(self.cfg, "top_p", 0.8) self.cfg.top_k = getattr(self.cfg, "top_k", 40) # mode から言語方向(translate5 と同じロジックに合わせて再計算) m = self.cfg.mode if m[0] == "j": self.cfg.source_lang = "JA" # GUIのチェック優先だが、je系ならデフォルトTrueでもよい if not hasattr(self.cfg, "limit_to_multibyte_str"): self.cfg.limit_to_multibyte_str = True else: self.cfg.source_lang = "EN" if m[1] == "j": self.cfg.target_lang = "JA" else: self.cfg.target_lang = "EN" def _load_config(self): if os.path.exists(CONFIG_INI): try: self.config.read(CONFIG_INI, encoding="utf-8") s = self.config["Settings"] if "geometry" in s: self.master.geometry(s.get("geometry")) self.infile_var.set( s.get("infile", self.infile_var.get()) ) self.tpl_var.set( s.get("html_template_path", self.tpl_var.get()) ) self.mode_var.set( s.get("mode", self.mode_var.get()) ) self.proc_unit_var.set( s.get("process_unit", self.proc_unit_var.get()) ) self.api_var.set( s.get("api", self.api_var.get()) ) self.model_var.set( s.get("model", self.model_var.get()) ) self.endpoint_var.set( s.get("endpoint", self.endpoint_var.get()) ) self.temp_var.set( s.get("temperature", self.temp_var.get()) ) self.max_tok_var.set( s.get("max_tokens", self.max_tok_var.get()) ) self.effort_var.set( s.get("reasoning_effort", self.effort_var.get()) ) self.min_len_var.set( s.get( "min_translate_length", self.min_len_var.get(), ) ) self.len_ratio_var.set( s.get( "allowed_translation_length_ratio", self.len_ratio_var.get(), ) ) self.limit_mb_var.set( s.getboolean( "limit_to_multibyte_str", self.limit_mb_var.get(), ) ) # promptのみ tr_p = s.get("translate_prompt", "").strip() if tr_p: self.tr_prompt.delete("1.0", "end") self.tr_prompt.insert("1.0", tr_p) rf_p = s.get("reformat_prompt", "").strip() if rf_p: self.rf_prompt.delete("1.0", "end") self.rf_prompt.insert("1.0", rf_p) except Exception: # 壊れていても無視してデフォルトで続行 pass def _save_config(self): if "Settings" not in self.config: self.config["Settings"] = {} s = self.config["Settings"] s["geometry"] = self.master.winfo_geometry() s["infile"] = self.infile_var.get() s["html_template_path"] = self.tpl_var.get() s["mode"] = self.mode_var.get() s["process_unit"] = self.proc_unit_var.get() s["api"] = self.api_var.get() s["model"] = self.model_var.get() s["endpoint"] = self.endpoint_var.get() s["temperature"] = self.temp_var.get() s["max_tokens"] = self.max_tok_var.get() s["reasoning_effort"] = self.effort_var.get() s["min_translate_length"] = self.min_len_var.get() s["allowed_translation_length_ratio"] = self.len_ratio_var.get() s["limit_to_multibyte_str"] = str(bool(self.limit_mb_var.get())) s["translate_prompt"] = self.tr_prompt.get("1.0", "end").strip() s["reformat_prompt"] = self.rf_prompt.get("1.0", "end").strip() try: with open(CONFIG_INI, "w", encoding="utf-8") as f: self.config.write(f) except Exception as e: messagebox.showwarning( "保存警告", f"設定保存に失敗しました: {e}", )
[ドキュメント] def on_close(self): try: self._save_config() finally: self.master.destroy()
[ドキュメント] def main(): root = ThemedTk(theme="plastik") if ThemedTk else tk.Tk() if not ThemedTk: try: style = ttk.Style() style.theme_use("clam") except Exception: pass App(root) root.mainloop()
if __name__ == "__main__": main()