feat(tools): pywebview bootstrap + python bridge API
Single window (900x720, dark background) loading web/index.html with a Python bridge exposing list_existing_commands, list_exes, list_sound_names, preview_sound, slugify, validate_draft, save_command, plus llm_suggest hook for M3. Sound preview spawned on a daemon thread so the GUI does not block.
This commit is contained in:
parent
80d9eedead
commit
9814181d1a
2 changed files with 148 additions and 0 deletions
4
tools/command_builder/__main__.py
Normal file
4
tools/command_builder/__main__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from tools.command_builder.app import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
144
tools/command_builder/app.py
Normal file
144
tools/command_builder/app.py
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import webview
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
WEB_DIR = Path(__file__).resolve().parent / "web"
|
||||||
|
COMMANDS_YAML = REPO_ROOT / "commands.yaml"
|
||||||
|
CUSTOM_COMMANDS_DIR = REPO_ROOT / "custom-commands"
|
||||||
|
SOUND_DIR = REPO_ROOT / "sound"
|
||||||
|
|
||||||
|
if str(REPO_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(REPO_ROOT))
|
||||||
|
|
||||||
|
from tools.command_builder.schema import (
|
||||||
|
CommandEntry,
|
||||||
|
SchemaError,
|
||||||
|
VALID_SOUND_NAMES,
|
||||||
|
action_from_dict,
|
||||||
|
command_from_dict,
|
||||||
|
slugify,
|
||||||
|
)
|
||||||
|
from tools.command_builder import yaml_writer
|
||||||
|
|
||||||
|
|
||||||
|
class Bridge:
|
||||||
|
def list_existing_commands(self):
|
||||||
|
try:
|
||||||
|
return yaml_writer.list_command_ids(COMMANDS_YAML)
|
||||||
|
except Exception as ex:
|
||||||
|
return {"error": str(ex)}
|
||||||
|
|
||||||
|
def list_exes(self):
|
||||||
|
if not CUSTOM_COMMANDS_DIR.exists():
|
||||||
|
return []
|
||||||
|
names = []
|
||||||
|
for p in sorted(CUSTOM_COMMANDS_DIR.iterdir()):
|
||||||
|
if p.suffix.lower() == ".exe":
|
||||||
|
names.append(p.stem)
|
||||||
|
return names
|
||||||
|
|
||||||
|
def list_sound_names(self):
|
||||||
|
return list(VALID_SOUND_NAMES)
|
||||||
|
|
||||||
|
def preview_sound(self, name):
|
||||||
|
if name not in VALID_SOUND_NAMES:
|
||||||
|
return {"ok": False, "error": "unknown sound"}
|
||||||
|
candidates = []
|
||||||
|
if name in ("ok", "greet"):
|
||||||
|
for i in (1, 2, 3):
|
||||||
|
candidates.append(SOUND_DIR / f"{name}{i}.wav")
|
||||||
|
candidates.append(SOUND_DIR / f"{name}.wav")
|
||||||
|
target = next((c for c in candidates if c.exists()), None)
|
||||||
|
if target is None:
|
||||||
|
return {"ok": False, "error": f"file not found for {name}"}
|
||||||
|
|
||||||
|
def _play():
|
||||||
|
try:
|
||||||
|
import simpleaudio as sa
|
||||||
|
wave_obj = sa.WaveObject.from_wave_file(str(target))
|
||||||
|
play_obj = wave_obj.play()
|
||||||
|
play_obj.wait_done()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
threading.Thread(target=_play, daemon=True).start()
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
def slugify(self, phrase):
|
||||||
|
return slugify(phrase or "")
|
||||||
|
|
||||||
|
def validate_draft(self, draft_json):
|
||||||
|
try:
|
||||||
|
data = json.loads(draft_json) if isinstance(draft_json, str) else draft_json
|
||||||
|
cmd_id = data.get("cmd_id") or ""
|
||||||
|
if not cmd_id.strip():
|
||||||
|
return {"ok": False, "error": "Не указан идентификатор команды"}
|
||||||
|
entry = command_from_dict(cmd_id, {
|
||||||
|
"phrases": data.get("phrases") or [],
|
||||||
|
"action": data.get("action") or {},
|
||||||
|
**({"confirm_sound": data["confirm_sound"]} if data.get("confirm_sound") else {}),
|
||||||
|
})
|
||||||
|
return {"ok": True, "id": entry.cmd_id}
|
||||||
|
except SchemaError as ex:
|
||||||
|
return {"ok": False, "error": str(ex)}
|
||||||
|
except Exception as ex:
|
||||||
|
return {"ok": False, "error": f"Внутренняя ошибка: {ex}"}
|
||||||
|
|
||||||
|
def save_command(self, draft_json):
|
||||||
|
try:
|
||||||
|
data = json.loads(draft_json) if isinstance(draft_json, str) else draft_json
|
||||||
|
cmd_id = (data.get("cmd_id") or "").strip()
|
||||||
|
if not cmd_id:
|
||||||
|
return {"ok": False, "error": "Не указан идентификатор команды"}
|
||||||
|
entry = command_from_dict(cmd_id, {
|
||||||
|
"phrases": data.get("phrases") or [],
|
||||||
|
"action": data.get("action") or {},
|
||||||
|
**({"confirm_sound": data["confirm_sound"]} if data.get("confirm_sound") else {}),
|
||||||
|
})
|
||||||
|
existing = set(yaml_writer.list_command_ids(COMMANDS_YAML))
|
||||||
|
if entry.cmd_id in existing and not data.get("overwrite"):
|
||||||
|
return {"ok": False, "error": f"Команда {entry.cmd_id} уже существует"}
|
||||||
|
if data.get("overwrite"):
|
||||||
|
yaml_writer.upsert_command(entry, path=COMMANDS_YAML)
|
||||||
|
else:
|
||||||
|
yaml_writer.append_command(entry, path=COMMANDS_YAML)
|
||||||
|
return {"ok": True, "id": entry.cmd_id}
|
||||||
|
except SchemaError as ex:
|
||||||
|
return {"ok": False, "error": str(ex)}
|
||||||
|
except Exception as ex:
|
||||||
|
return {"ok": False, "error": f"Не удалось сохранить: {ex}"}
|
||||||
|
|
||||||
|
def llm_suggest(self, prompt):
|
||||||
|
try:
|
||||||
|
from tools.command_builder import llm_suggest as ls
|
||||||
|
return ls.suggest(prompt or "")
|
||||||
|
except Exception as ex:
|
||||||
|
return {"ok": False, "error": f"LLM недоступен: {ex}"}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
bridge = Bridge()
|
||||||
|
index_path = WEB_DIR / "index.html"
|
||||||
|
if not index_path.exists():
|
||||||
|
raise SystemExit(f"web/index.html not found at {index_path}")
|
||||||
|
webview.create_window(
|
||||||
|
title="J.A.R.V.I.S — конструктор команд",
|
||||||
|
url=str(index_path),
|
||||||
|
js_api=bridge,
|
||||||
|
width=900,
|
||||||
|
height=720,
|
||||||
|
min_size=(720, 560),
|
||||||
|
background_color="#0a1214",
|
||||||
|
)
|
||||||
|
webview.start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue