Co-authored-by: Artur Tamazian <artur-tamazian>
Co-authored-by: Javier Gonzalez <javierggt@yahoo.com>
This commit is contained in:
authored and GitHub committed 2026-06-05 13:22:00 +02:00
1 parent 6555001fe9
commit ad244ffc7e
34 files changed
+2286 -465

No files matched your search

+62 -27
View File
@@ -24,6 +24,7 @@ from kivymd.uix.selectioncontrol import MDCheckbox
from kivymd.uix.textfield import MDTextField
from katrain.core.ai import ai_rank_estimation, game_report
from katrain.core.engine import resolve_engine_backend
from katrain.core.constants import (
AI_CONFIG_DEFAULT,
AI_DEFAULT,
@@ -39,7 +40,6 @@ from katrain.core.constants import (
PLAYER_HUMAN,
ADDITIONAL_MOVE_ORDER,
)
from katrain.core.engine import KataGoEngine
from katrain.core.lang import i18n, rank_label
from katrain.core.sgf_parser import Move
from katrain.core.utils import PATHS, find_package_resource, evaluation_class
@@ -455,11 +455,36 @@ class ConfigAIPopup(QuickConfigGui):
class EngineRecoveryPopup(QuickConfigGui):
error_message = StringProperty("")
code = ObjectProperty(None)
engine_type = StringProperty("local")
recovery_message = StringProperty("")
def __init__(self, katrain, error_message, code):
def __init__(self, katrain, error_message, code, engine_type="local"):
super().__init__(katrain)
self.error_message = str(error_message)
self.code = code
self.engine_type = engine_type or "local"
self.recovery_message = self._build_message()
def _build_message(self):
settings_link = "[color=#CCCC11][u][ref=engine_settings]" + i18n._("menu:settings") + "[/ref][/u][/color]"
help_link = "[color=#CCCC11][u][ref=engine_help]" + i18n._("link_here") + "[/ref][/u][/color]"
if self.engine_type == "remote":
opening_key = "remote engine disconnected popup opening message"
suggestion = i18n._("remote engine check url suggestion").format(link=settings_link)
else:
opening_key = "engine died popup opening message"
suggestion = i18n._("change engine suggestion").format(link=settings_link)
opening = i18n._(opening_key).format(code=self.code, error_message=self.error_message)
help_text = i18n._("go to engine help page").format(link=help_link)
return opening + "\n\n" + suggestion + "\n\n" + help_text
def retry(self):
"""Rebuild the engine from current config and re-analyze. For a
remote engine this reconnects; for a local one it respawns the
subprocess. Recovers a transient failure without changing settings."""
if self.popup:
self.popup.dismiss()
Clock.schedule_once(lambda _dt: self.katrain.restart_engine(), 0)
class BaseConfigPopup(QuickConfigGui):
@@ -481,23 +506,28 @@ class BaseConfigPopup(QuickConfigGui):
KATAGOS = {
"win": {
"OpenCL v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-opencl-windows-x64.zip",
"Eigen AVX2 (Modern CPUs) v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-eigenavx2-windows-x64.zip",
"Eigen (CPU, Non-optimized) v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-eigen-windows-x64.zip",
"OpenCL v1.16.0 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-opencl-windows-x64+bs50.zip",
"OpenCL v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-opencl-windows-x64.zip",
"Eigen AVX2 (Modern CPUs) v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-eigenavx2-windows-x64.zip",
"Eigen (CPU, Non-optimized) v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-eigen-windows-x64.zip",
"OpenCL v1.16.5 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-opencl-windows-x64+bs50.zip",
},
"linux": {
"OpenCL v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-opencl-linux-x64.zip",
"Eigen AVX2 (Modern CPUs) v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-eigenavx2-linux-x64.zip",
"Eigen (CPU, Non-optimized) v1.16.0": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-eigen-linux-x64.zip",
"OpenCL v1.16.0 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.16.0/katago-v1.16.0-opencl-linux-x64+bs50.zip",
"OpenCL v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-opencl-linux-x64.zip",
"Eigen AVX2 (Modern CPUs) v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-eigenavx2-linux-x64.zip",
"Eigen (CPU, Non-optimized) v1.16.5": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-eigen-linux-x64.zip",
"OpenCL v1.16.5 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.16.5/katago-v1.16.5-opencl-linux-x64+bs50.zip",
},
"just-descriptions": {},
}
def __init__(self, katrain):
super().__init__(katrain)
self.paths = [self.katrain.config("engine/model"), self.katrain.config("engine/humanlike_model"), "katrain/models", DATA_FOLDER]
self.paths = [
self.katrain.config("engine/model"),
self.katrain.config("engine/humanlike_model"),
"katrain/models",
DATA_FOLDER,
]
self.katago_paths = [self.katrain.config("engine/katago"), DATA_FOLDER]
self.last_clicked_download_models = 0
@@ -561,7 +591,9 @@ class BaseConfigPopup(QuickConfigGui):
key=lambda descpath: ("Recommended" not in descpath[0], " - " not in descpath[0], descpath[0]),
)
humanlike_models_available_msg = i18n._("models available").format(num=len(humanlike_model_files))
self.humanlike_model_files.values = [humanlike_models_available_msg] + [desc for desc, path in humanlike_model_files]
self.humanlike_model_files.values = [humanlike_models_available_msg] + [
desc for desc, path in humanlike_model_files
]
self.humanlike_model_files.value_keys = [""] + [path for desc, path in humanlike_model_files]
self.humanlike_model_files.text = humanlike_models_available_msg
@@ -650,7 +682,9 @@ class BaseConfigPopup(QuickConfigGui):
for name, url in {**self.MODELS, **dist_models}.items():
filename = os.path.split(url)[1]
if not any(os.path.split(f)[1] == filename for f in self.model_files.values + self.humanlike_model_files.values):
if not any(
os.path.split(f)[1] == filename for f in self.model_files.values + self.humanlike_model_files.values
):
savepath = os.path.expanduser(os.path.join(DATA_FOLDER, filename))
savepath_tmp = savepath + ".part"
self.katrain.log(f"Downloading {name} from {url} to {savepath_tmp}", OUTPUT_INFO)
@@ -759,14 +793,28 @@ class BaseConfigPopup(QuickConfigGui):
class ConfigPopup(BaseConfigPopup):
ENGINE_TAB_BUTTONS = {"local": "local_tab_button", "remote": "remote_tab_button", "custom": "custom_tab_button"}
def __init__(self, katrain):
super().__init__(katrain)
Clock.schedule_once(self.check_katas)
Clock.schedule_once(self.select_engine_tab)
MDApp.get_running_app().bind(language=self.check_models)
MDApp.get_running_app().bind(language=self.check_katas)
def select_engine_tab(self, *_args):
# The active tab is authoritative for which engine is used; pick it based on the current config.
backend = resolve_engine_backend(self.katrain.config("engine"))
self.engine_sm.current = backend
getattr(self, self.ENGINE_TAB_BUTTONS[backend]).state = "down"
def update_config(self, save_to_file=True, close_popup=True):
old_backend = self.katrain.config("engine/backend", "")
backend = self.engine_sm.current
self.katrain._config["engine"]["backend"] = backend
updated = super().update_config(save_to_file=save_to_file, close_popup=close_popup)
if backend != old_backend:
updated.add("engine/backend")
self.katrain.debug_level = self.katrain.config("general/debug_level", OUTPUT_INFO)
ignore = {"max_visits", "fast_visits", "max_time", "enable_ownership", "wide_root_noise"}
@@ -774,21 +822,8 @@ class ConfigPopup(BaseConfigPopup):
if detected_restart:
def restart_engine(_dt):
self.katrain.controls.set_status("", STATUS_INFO)
self.katrain.log(f"Restarting Engine after {detected_restart} settings change")
self.katrain.controls.set_status(i18n._("restarting engine"), STATUS_INFO)
old_engine = self.katrain.engine # type: KataGoEngine
old_proc = old_engine.katago_process
if old_proc:
old_engine.shutdown(finish=False)
new_engine = KataGoEngine(self.katrain, self.katrain.config("engine"))
self.katrain.engine = new_engine
self.katrain.game.engines = {"B": new_engine, "W": new_engine}
self.katrain.game.analyze_all_nodes(
analyze_fast=True
) # old engine was possibly broken, so make sure we redo any failures
self.katrain.update_state()
self.katrain.restart_engine()
Clock.schedule_once(restart_engine, 0)
+45 -11
View File
@@ -1,9 +1,9 @@
from kivy.clock import Clock
from kivymd.app import MDApp
from kivy.core.audio import SoundLoader
from kivy.utils import platform
cached_sounds = {}
_audio_available = False
# prefer ffpyplayer on linux, then others, avoid gst and avoid or ffpyplayer on windows
ranking = [("ffplay", 98 if platform in ["win", "macosx"] else -2), ("sdl", -1), ("gst", 99), ("", 0)]
@@ -13,22 +13,56 @@ except Exception as e:
print("Exception sorting sound loaders: ", e) # private vars, so could break with versions etc
def play_sound(file, volume=1, cache=True):
def _play(sound):
if sound:
sound.play()
sound.seek(0)
def preload_sounds(sound_dir):
"""Preload all sounds before the SDL2 window is created.
SoundLoader.load() deadlocks when no audio output device is available,
so we test audio in a subprocess first.
"""
global _audio_available
import os
import sys
import subprocess
audio_files = [
os.path.join(sound_dir, fn) for fn in os.listdir(sound_dir)
if fn.endswith((".wav", ".ogg", ".mp3"))
]
if not audio_files:
return
# Test if audio subsystem works by loading one sound in a subprocess
try:
subprocess.run(
[sys.executable, "-c",
f"from kivy.core.audio import SoundLoader; SoundLoader.load({audio_files[0]!r})"],
timeout=3, capture_output=True,
)
except subprocess.TimeoutExpired as e:
print(f"Warning: Audio unavailable ({e}). Sounds disabled.", file=sys.stderr)
return
# Audio works, preload all sounds
for path in audio_files:
cached_sounds[os.path.basename(path)] = SoundLoader.load(path)
_audio_available = True
def play_sound(file, volume=1, cache=True):
if not _audio_available:
return
# the following import causes the creation of an SDL2 window, and if there
# is no sound device, it deadlocks. We delay the import until we know audio
# is available
from kivymd.app import MDApp
app = MDApp.get_running_app()
if app and app.gui and app.gui.config("timer/sound"):
sound = cached_sounds.get(file)
if sound is None:
sound = SoundLoader.load(file)
if cache:
cached_sounds[file] = sound
if sound is not None:
sound.volume = volume
Clock.schedule_once(lambda _dt: _play(sound), 0)
sound.play()
sound.seek(0)
def stop_sound(file):