lerobotberto 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lerobotberto/__init__.py +0 -0
- lerobotberto/__main__.py +7 -0
- lerobotberto/gui.py +1167 -0
- lerobotberto-0.1.0.dist-info/METADATA +53 -0
- lerobotberto-0.1.0.dist-info/RECORD +9 -0
- lerobotberto-0.1.0.dist-info/WHEEL +5 -0
- lerobotberto-0.1.0.dist-info/entry_points.txt +2 -0
- lerobotberto-0.1.0.dist-info/licenses/LICENSE.txt +46 -0
- lerobotberto-0.1.0.dist-info/top_level.txt +1 -0
lerobotberto/__init__.py
ADDED
|
File without changes
|
lerobotberto/__main__.py
ADDED
lerobotberto/gui.py
ADDED
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Interfaz gráfica para lanzar dos políticas entrenadas con LeRobot
|
|
4
|
+
(mediante `lerobot-rollout`):
|
|
5
|
+
1) Insertar cable rojo en el portafusibles
|
|
6
|
+
2) Extraer fusible del portafusibles
|
|
7
|
+
|
|
8
|
+
Incluye una vista con la imagen de TU portafusibles (ruta configurable) y una
|
|
9
|
+
animación superpuesta del cable y del fusible mientras se ejecuta cada tarea.
|
|
10
|
+
|
|
11
|
+
Dependencias: solo tkinter. Para JPG y para escalar bien la imagen se
|
|
12
|
+
recomienda Pillow: pip install pillow (sin Pillow solo se admite PNG/GIF).
|
|
13
|
+
|
|
14
|
+
Control por voz (opcional):
|
|
15
|
+
pip install SpeechRecognition pyaudio
|
|
16
|
+
sudo apt install portaudio19-dev flac espeak-ng (Ubuntu/Debian)
|
|
17
|
+
El reconocimiento usa el servicio de Google, por lo que necesita internet.
|
|
18
|
+
|
|
19
|
+
Uso: python lerobot_gui.py
|
|
20
|
+
"""
|
|
21
|
+
import json
|
|
22
|
+
import math
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import shlex
|
|
26
|
+
import shutil
|
|
27
|
+
import signal
|
|
28
|
+
import subprocess
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
import unicodedata
|
|
32
|
+
import tkinter as tk
|
|
33
|
+
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from PIL import Image, ImageTk
|
|
37
|
+
HAS_PIL = True
|
|
38
|
+
RESAMPLE = getattr(Image, "Resampling", Image).LANCZOS
|
|
39
|
+
except ImportError:
|
|
40
|
+
HAS_PIL = False
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import speech_recognition as sr
|
|
44
|
+
HAS_SR = True
|
|
45
|
+
except ImportError:
|
|
46
|
+
HAS_SR = False
|
|
47
|
+
|
|
48
|
+
CONFIG_PATH = os.path.expanduser("~/.lerobot_gui.json")
|
|
49
|
+
LOG_PATH = os.path.expanduser("~/lerobot_gui.log")
|
|
50
|
+
PERIOD = 4.0 # segundos que dura un ciclo de animación
|
|
51
|
+
FRAME_MS = 33 # ~30 fps
|
|
52
|
+
|
|
53
|
+
DEFAULT_CONFIG = {
|
|
54
|
+
"cfg_version": 4,
|
|
55
|
+
"robot_args": (
|
|
56
|
+
'--robot.type=so101_follower '
|
|
57
|
+
'--robot.port=/dev/ttyACM1 '
|
|
58
|
+
'--robot.id=follower_arm '
|
|
59
|
+
'--robot.cameras="{ robot: {type: opencv, index_or_path: /dev/video4, '
|
|
60
|
+
'width: 320, height: 240, fps: 30}, top: {type: opencv, '
|
|
61
|
+
'index_or_path: /dev/video0, width: 320, height: 240, fps: 30}}"'
|
|
62
|
+
),
|
|
63
|
+
"episode_time_s": 120, # --duration
|
|
64
|
+
"device": "cpu", # --device
|
|
65
|
+
"conda_env": "lerobot", # solo se usa si lerobot-rollout no está en el PATH
|
|
66
|
+
# --- Vista del portafusibles
|
|
67
|
+
"image_path": "",
|
|
68
|
+
# Puntos en coordenadas relativas a la imagen (0..1). Se calibran con clic.
|
|
69
|
+
"points": {
|
|
70
|
+
"hole": [0.50, 0.32], # agujero de conexión (arriba)
|
|
71
|
+
"fuse": [0.50, 0.62], # fusible (abajo)
|
|
72
|
+
"cable_start": [0.14, 0.12], # de dónde sale el cable
|
|
73
|
+
},
|
|
74
|
+
"voice": {"lang": "es-ES", "mic": "", "confirm": True, "speak": True},
|
|
75
|
+
"anim_scale": 1.0,
|
|
76
|
+
"show_points": True,
|
|
77
|
+
"overlay_idle": True,
|
|
78
|
+
"tasks": {
|
|
79
|
+
"insertar": {
|
|
80
|
+
"label": "Insertar cable rojo",
|
|
81
|
+
"policy_path": "BertoTrujillo/act_superinscabmerged",
|
|
82
|
+
"task_text": "Insert the cable into its connector.",
|
|
83
|
+
},
|
|
84
|
+
"extraer": {
|
|
85
|
+
"label": "Extraer fusible",
|
|
86
|
+
"policy_path": "$HOME/lerobot_models/act_superextractfuse",
|
|
87
|
+
"task_text": "Extract the fuse from its holder.",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_config():
|
|
94
|
+
cfg = json.loads(json.dumps(DEFAULT_CONFIG))
|
|
95
|
+
if os.path.exists(CONFIG_PATH):
|
|
96
|
+
try:
|
|
97
|
+
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
98
|
+
data = json.load(f)
|
|
99
|
+
if data.get("cfg_version") != DEFAULT_CONFIG["cfg_version"]:
|
|
100
|
+
# Config de una versión anterior: se conservan solo imagen y opciones
|
|
101
|
+
for k in ("robot_args", "episode_time_s", "tasks", "points"):
|
|
102
|
+
data.pop(k, None)
|
|
103
|
+
for k, v in data.items():
|
|
104
|
+
if k == "tasks":
|
|
105
|
+
for key, val in v.items():
|
|
106
|
+
cfg["tasks"].setdefault(key, {}).update(val)
|
|
107
|
+
elif k == "points":
|
|
108
|
+
cfg["points"].update(v)
|
|
109
|
+
else:
|
|
110
|
+
cfg[k] = v
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
return cfg
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def save_config(cfg):
|
|
117
|
+
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
|
118
|
+
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------- comandos de voz
|
|
122
|
+
STOP_STRONG = {"detente", "detener", "alto", "basta", "emergencia", "stop", "halt",
|
|
123
|
+
"pare", "paro"}
|
|
124
|
+
STOP_SHORT = {"para", "parar"} # solo si es una frase muy corta
|
|
125
|
+
YES_WORDS = {"si", "confirmo", "confirmado", "vale", "adelante", "ok", "okay", "dale",
|
|
126
|
+
"afirmativo", "yes", "confirm", "go"}
|
|
127
|
+
NO_WORDS = {"no", "cancela", "cancelar", "cancelado", "negativo", "nada", "cancel"}
|
|
128
|
+
INS_VERBS = {"inserta", "insertar", "inserte", "insertas", "mete", "meter", "conecta",
|
|
129
|
+
"conectar", "enchufa", "enchufar", "insert", "plug", "connect"}
|
|
130
|
+
EXT_VERBS = {"extrae", "extraer", "extraiga", "saca", "sacar", "quita", "quitar",
|
|
131
|
+
"retira", "retirar", "extract", "remove", "pull"}
|
|
132
|
+
RESET_VERBS = {"reinicia", "reiniciar", "reset", "resetea", "restablece"}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _audio_level(audio):
|
|
136
|
+
"""Nivel RMS (16 bits) de un AudioData; comparable con r.energy_threshold."""
|
|
137
|
+
try:
|
|
138
|
+
import array
|
|
139
|
+
raw = audio.get_raw_data(convert_rate=16000, convert_width=2)
|
|
140
|
+
a = array.array("h", raw)
|
|
141
|
+
if not a:
|
|
142
|
+
return 0
|
|
143
|
+
return int((sum(x * x for x in a) / len(a)) ** 0.5)
|
|
144
|
+
except Exception:
|
|
145
|
+
return -1
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _words(text):
|
|
149
|
+
t = unicodedata.normalize("NFD", text.lower())
|
|
150
|
+
t = "".join(ch for ch in t if unicodedata.category(ch) != "Mn")
|
|
151
|
+
return re.findall(r"[a-z0-9]+", t)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def parse_voice(text):
|
|
155
|
+
"""Devuelve: 'stop', 'yes', 'no', 'insertar', 'extraer', 'reset' o None."""
|
|
156
|
+
w = _words(text)
|
|
157
|
+
ws = set(w)
|
|
158
|
+
if not w:
|
|
159
|
+
return None
|
|
160
|
+
if ws & STOP_STRONG or (len(w) <= 2 and ws & STOP_SHORT):
|
|
161
|
+
return "stop"
|
|
162
|
+
if len(w) <= 3 and ws & NO_WORDS:
|
|
163
|
+
return "no"
|
|
164
|
+
if len(w) <= 3 and ws & YES_WORDS:
|
|
165
|
+
return "yes"
|
|
166
|
+
ins = bool(ws & INS_VERBS) and bool(ws & {"cable", "cables"})
|
|
167
|
+
ext = bool(ws & EXT_VERBS) and bool(ws & {"fusible", "fusibles", "fuse", "fuses"})
|
|
168
|
+
if ins and not ext:
|
|
169
|
+
return "insertar"
|
|
170
|
+
if ext and not ins:
|
|
171
|
+
return "extraer"
|
|
172
|
+
if ws & RESET_VERBS and ws & {"vista", "view"}:
|
|
173
|
+
return "reset"
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------------- utilidades
|
|
178
|
+
def ease(t):
|
|
179
|
+
t = max(0.0, min(1.0, t))
|
|
180
|
+
return t * t * (3 - 2 * t)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def lerp(a, b, t):
|
|
184
|
+
return a + (b - a) * t
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def lerp_pt(p, q, t):
|
|
188
|
+
return (lerp(p[0], q[0], t), lerp(p[1], q[1], t))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def window(t, a, b):
|
|
192
|
+
"""0 antes de a, 1 después de b, suave entre medias."""
|
|
193
|
+
if t <= a:
|
|
194
|
+
return 0.0
|
|
195
|
+
if t >= b:
|
|
196
|
+
return 1.0
|
|
197
|
+
return ease((t - a) / (b - a))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class App(tk.Tk):
|
|
201
|
+
def __init__(self):
|
|
202
|
+
super().__init__()
|
|
203
|
+
self.title("LeRobot · Panel de control portafusibles")
|
|
204
|
+
self.geometry("1260x880")
|
|
205
|
+
self.minsize(1000, 720)
|
|
206
|
+
|
|
207
|
+
self.cfg = load_config()
|
|
208
|
+
self.proc = None
|
|
209
|
+
self.current_task = None
|
|
210
|
+
self.t_start = 0.0
|
|
211
|
+
self.t_anim0 = 0.0
|
|
212
|
+
self.task_vars = {}
|
|
213
|
+
|
|
214
|
+
# Estado visual de la escena
|
|
215
|
+
self.cable_in = False
|
|
216
|
+
self.fuse_out = False
|
|
217
|
+
self.calib_mode = None
|
|
218
|
+
self.raw = None
|
|
219
|
+
self.photo = None
|
|
220
|
+
self.img_rect = None
|
|
221
|
+
self._resize_job = None
|
|
222
|
+
|
|
223
|
+
# Estado del control por voz
|
|
224
|
+
self.voice_on = False
|
|
225
|
+
self.pending = None # (tarea, caducidad) a la espera de «sí»
|
|
226
|
+
self.speaking = False
|
|
227
|
+
self.last_speech_end = 0.0
|
|
228
|
+
self._speak_n = 0
|
|
229
|
+
self._speak_lock = threading.Lock()
|
|
230
|
+
self._voice_lang = "es-ES"
|
|
231
|
+
self._voice_mic_name = ""
|
|
232
|
+
|
|
233
|
+
self._build_style()
|
|
234
|
+
self._build_ui()
|
|
235
|
+
self.load_image()
|
|
236
|
+
self._tick()
|
|
237
|
+
self._anim_loop()
|
|
238
|
+
self.protocol("WM_DELETE_WINDOW", self.on_close)
|
|
239
|
+
|
|
240
|
+
# ------------------------------------------------------------------ UI
|
|
241
|
+
def _build_style(self):
|
|
242
|
+
style = ttk.Style(self)
|
|
243
|
+
try:
|
|
244
|
+
style.theme_use("clam")
|
|
245
|
+
except tk.TclError:
|
|
246
|
+
pass
|
|
247
|
+
style.configure("Big.TButton", font=("Helvetica", 14, "bold"), padding=14)
|
|
248
|
+
style.configure("Stop.TButton", font=("Helvetica", 14, "bold"), padding=14,
|
|
249
|
+
foreground="#b00020")
|
|
250
|
+
style.configure("Status.TLabel", font=("Helvetica", 13, "bold"))
|
|
251
|
+
|
|
252
|
+
def _build_ui(self):
|
|
253
|
+
root = ttk.Frame(self, padding=10)
|
|
254
|
+
root.pack(fill="both", expand=True)
|
|
255
|
+
|
|
256
|
+
# --- Estado (arriba)
|
|
257
|
+
top = ttk.Frame(root)
|
|
258
|
+
top.pack(fill="x", side="top")
|
|
259
|
+
self.status_dot = tk.Canvas(top, width=22, height=22, highlightthickness=0)
|
|
260
|
+
self.status_dot.pack(side="left")
|
|
261
|
+
self.dot = self.status_dot.create_oval(3, 3, 19, 19, fill="#9e9e9e", outline="")
|
|
262
|
+
self.status_lbl = ttk.Label(top, text="Parado", style="Status.TLabel")
|
|
263
|
+
self.status_lbl.pack(side="left", padx=8)
|
|
264
|
+
self.timer_lbl = ttk.Label(top, text="00:00", font=("Helvetica", 13))
|
|
265
|
+
self.timer_lbl.pack(side="right")
|
|
266
|
+
|
|
267
|
+
# --- Log (abajo)
|
|
268
|
+
log_frame = ttk.LabelFrame(root, text="Salida de LeRobot", padding=6)
|
|
269
|
+
log_frame.pack(fill="x", side="bottom", pady=(8, 0))
|
|
270
|
+
bar = ttk.Frame(log_frame)
|
|
271
|
+
bar.pack(fill="x", pady=(0, 4))
|
|
272
|
+
ttk.Button(bar, text="Copiar todo el log", command=self.copy_log).pack(side="left")
|
|
273
|
+
ttk.Button(bar, text="Limpiar", command=self.clear_log).pack(side="left", padx=6)
|
|
274
|
+
ttk.Label(bar, text=f"(también se guarda en {LOG_PATH})").pack(side="left", padx=6)
|
|
275
|
+
self.log = scrolledtext.ScrolledText(log_frame, height=9,
|
|
276
|
+
font=("Courier", 10),
|
|
277
|
+
bg="#111", fg="#d6d6d6",
|
|
278
|
+
insertbackground="#d6d6d6")
|
|
279
|
+
self.log.pack(fill="both", expand=True)
|
|
280
|
+
# Solo lectura, pero se puede seleccionar y copiar (Ctrl+C / Ctrl+A / clic derecho)
|
|
281
|
+
self.log.bind("<Key>", self._log_key)
|
|
282
|
+
for ev in ("<<Paste>>", "<<Cut>>"):
|
|
283
|
+
self.log.bind(ev, lambda e: "break")
|
|
284
|
+
self.log.bind("<Button-1>", lambda e: self.log.focus_set())
|
|
285
|
+
self.log_menu = tk.Menu(self, tearoff=0)
|
|
286
|
+
self.log_menu.add_command(label="Copiar selección", command=lambda: self.copy_log(False))
|
|
287
|
+
self.log_menu.add_command(label="Copiar todo", command=self.copy_log)
|
|
288
|
+
self.log.bind("<Button-3>", lambda e: self.log_menu.tk_popup(e.x_root, e.y_root))
|
|
289
|
+
|
|
290
|
+
# --- Zona central: vista + controles
|
|
291
|
+
mid = ttk.Frame(root)
|
|
292
|
+
mid.pack(fill="both", expand=True, pady=(8, 0))
|
|
293
|
+
mid.columnconfigure(0, weight=3)
|
|
294
|
+
mid.columnconfigure(1, weight=2)
|
|
295
|
+
mid.rowconfigure(0, weight=1)
|
|
296
|
+
|
|
297
|
+
self._build_visual(mid)
|
|
298
|
+
self._build_controls(mid)
|
|
299
|
+
|
|
300
|
+
def _build_visual(self, parent):
|
|
301
|
+
vis = ttk.LabelFrame(parent, text="Vista del portafusibles", padding=6)
|
|
302
|
+
vis.grid(row=0, column=0, sticky="nsew", padx=(0, 6))
|
|
303
|
+
|
|
304
|
+
self.canvas = tk.Canvas(vis, width=640, height=440, bg="#1e1e1e",
|
|
305
|
+
highlightthickness=0)
|
|
306
|
+
self.canvas.pack(fill="both", expand=True)
|
|
307
|
+
self.canvas.bind("<Configure>", self._on_canvas_resize)
|
|
308
|
+
self.canvas.bind("<Button-1>", self._on_canvas_click)
|
|
309
|
+
|
|
310
|
+
# Ruta de imagen
|
|
311
|
+
row = ttk.Frame(vis)
|
|
312
|
+
row.pack(fill="x", pady=(6, 0))
|
|
313
|
+
ttk.Label(row, text="Imagen:").pack(side="left")
|
|
314
|
+
self.img_path_var = tk.StringVar(value=self.cfg.get("image_path", ""))
|
|
315
|
+
ent = ttk.Entry(row, textvariable=self.img_path_var)
|
|
316
|
+
ent.pack(side="left", fill="x", expand=True, padx=6)
|
|
317
|
+
ent.bind("<Return>", lambda e: self.load_image())
|
|
318
|
+
ttk.Button(row, text="Examinar…", command=self.browse_image).pack(side="left")
|
|
319
|
+
ttk.Button(row, text="Cargar", command=self.load_image).pack(side="left", padx=(4, 0))
|
|
320
|
+
|
|
321
|
+
# Calibración
|
|
322
|
+
cal = ttk.Frame(vis)
|
|
323
|
+
cal.pack(fill="x", pady=(6, 0))
|
|
324
|
+
ttk.Label(cal, text="Calibrar (clic en la imagen):").pack(side="left")
|
|
325
|
+
for mode, txt in (("hole", "Agujero"), ("fuse", "Fusible"),
|
|
326
|
+
("cable_start", "Origen cable")):
|
|
327
|
+
ttk.Button(cal, text=txt, command=lambda m=mode: self.start_calib(m))\
|
|
328
|
+
.pack(side="left", padx=3)
|
|
329
|
+
|
|
330
|
+
opts = ttk.Frame(vis)
|
|
331
|
+
opts.pack(fill="x", pady=(6, 0))
|
|
332
|
+
self.show_points = tk.BooleanVar(value=bool(self.cfg.get("show_points", True)))
|
|
333
|
+
self.overlay_idle = tk.BooleanVar(value=bool(self.cfg.get("overlay_idle", True)))
|
|
334
|
+
ttk.Checkbutton(opts, text="Mostrar puntos", variable=self.show_points)\
|
|
335
|
+
.pack(side="left")
|
|
336
|
+
ttk.Checkbutton(opts, text="Dibujar cable/fusible en reposo",
|
|
337
|
+
variable=self.overlay_idle).pack(side="left", padx=10)
|
|
338
|
+
ttk.Label(opts, text="Tamaño:").pack(side="left")
|
|
339
|
+
self.scale_var = tk.DoubleVar(value=float(self.cfg.get("anim_scale", 1.0)))
|
|
340
|
+
ttk.Spinbox(opts, from_=0.3, to=4.0, increment=0.1, width=5,
|
|
341
|
+
textvariable=self.scale_var,
|
|
342
|
+
command=self._render_static).pack(side="left", padx=4)
|
|
343
|
+
ttk.Button(opts, text="Reiniciar vista", command=self.reset_view)\
|
|
344
|
+
.pack(side="right")
|
|
345
|
+
|
|
346
|
+
def _build_controls(self, parent):
|
|
347
|
+
ctrl = ttk.Frame(parent)
|
|
348
|
+
ctrl.grid(row=0, column=1, sticky="nsew")
|
|
349
|
+
|
|
350
|
+
actions = ttk.LabelFrame(ctrl, text="Acciones", padding=8)
|
|
351
|
+
actions.pack(fill="x")
|
|
352
|
+
actions.columnconfigure(0, weight=1)
|
|
353
|
+
self.buttons = {}
|
|
354
|
+
for r, (key, t) in enumerate(self.cfg["tasks"].items()):
|
|
355
|
+
b = ttk.Button(actions, text=f"▶ {t['label']}", style="Big.TButton",
|
|
356
|
+
command=lambda k=key: self.start_task(k))
|
|
357
|
+
b.grid(row=r, column=0, sticky="ew", pady=4)
|
|
358
|
+
self.buttons[key] = b
|
|
359
|
+
self.stop_btn = ttk.Button(actions, text="■ PARAR", style="Stop.TButton",
|
|
360
|
+
command=self.stop_task, state="disabled")
|
|
361
|
+
self.stop_btn.grid(row=len(self.buttons), column=0, sticky="ew", pady=4)
|
|
362
|
+
|
|
363
|
+
self._build_voice(ctrl)
|
|
364
|
+
|
|
365
|
+
nb = ttk.Notebook(ctrl)
|
|
366
|
+
nb.pack(fill="x", pady=8)
|
|
367
|
+
|
|
368
|
+
tab_general = ttk.Frame(nb, padding=8)
|
|
369
|
+
nb.add(tab_general, text="Robot")
|
|
370
|
+
ttk.Label(tab_general, text="Argumentos del robot (cámaras, puerto, id…):")\
|
|
371
|
+
.pack(anchor="w")
|
|
372
|
+
self.robot_txt = tk.Text(tab_general, height=6, wrap="word")
|
|
373
|
+
self.robot_txt.pack(fill="x", pady=4)
|
|
374
|
+
self.robot_txt.insert("1.0", self.cfg["robot_args"])
|
|
375
|
+
row = ttk.Frame(tab_general)
|
|
376
|
+
row.pack(fill="x")
|
|
377
|
+
ttk.Label(row, text="Duración (s):").pack(side="left")
|
|
378
|
+
self.ep_time = tk.IntVar(value=self.cfg["episode_time_s"])
|
|
379
|
+
ttk.Spinbox(row, from_=5, to=600, textvariable=self.ep_time, width=6)\
|
|
380
|
+
.pack(side="left", padx=6)
|
|
381
|
+
ttk.Label(row, text="Dispositivo:").pack(side="left", padx=(8, 0))
|
|
382
|
+
self.device_var = tk.StringVar(value=self.cfg.get("device", "cpu"))
|
|
383
|
+
ttk.Combobox(row, textvariable=self.device_var, width=7,
|
|
384
|
+
values=("cpu", "cuda", "mps")).pack(side="left", padx=6)
|
|
385
|
+
row2 = ttk.Frame(tab_general)
|
|
386
|
+
row2.pack(fill="x", pady=(6, 0))
|
|
387
|
+
ttk.Label(row2, text="Entorno conda (si no está activado):").pack(side="left")
|
|
388
|
+
self.conda_var = tk.StringVar(value=self.cfg.get("conda_env", "lerobot"))
|
|
389
|
+
ttk.Entry(row2, textvariable=self.conda_var, width=14).pack(side="left", padx=6)
|
|
390
|
+
|
|
391
|
+
for key, t in self.cfg["tasks"].items():
|
|
392
|
+
tab = ttk.Frame(nb, padding=8)
|
|
393
|
+
nb.add(tab, text=t["label"])
|
|
394
|
+
tab.columnconfigure(0, weight=1)
|
|
395
|
+
vars_ = {}
|
|
396
|
+
for r, (field, lbl) in enumerate([
|
|
397
|
+
("policy_path", "Carpeta de la política (--policy.path):"),
|
|
398
|
+
("task_text", "Descripción de la tarea (--task):"),
|
|
399
|
+
]):
|
|
400
|
+
ttk.Label(tab, text=lbl).grid(row=2 * r, column=0, sticky="w", pady=(4, 0))
|
|
401
|
+
v = tk.StringVar(value=t[field])
|
|
402
|
+
ttk.Entry(tab, textvariable=v).grid(row=2 * r + 1, column=0, sticky="ew")
|
|
403
|
+
vars_[field] = v
|
|
404
|
+
self.task_vars[key] = vars_
|
|
405
|
+
|
|
406
|
+
save_row = ttk.Frame(ctrl)
|
|
407
|
+
save_row.pack(fill="x")
|
|
408
|
+
ttk.Button(save_row, text="Guardar ajustes", command=self.save_settings)\
|
|
409
|
+
.pack(side="left")
|
|
410
|
+
ttk.Button(save_row, text="Limpiar log", command=self.clear_log)\
|
|
411
|
+
.pack(side="left", padx=6)
|
|
412
|
+
|
|
413
|
+
# ------------------------------------------------------------- helpers
|
|
414
|
+
def write_log(self, text):
|
|
415
|
+
self.log.insert("end", text)
|
|
416
|
+
self.log.see("end")
|
|
417
|
+
try:
|
|
418
|
+
with open(LOG_PATH, "a", encoding="utf-8") as f:
|
|
419
|
+
f.write(text)
|
|
420
|
+
except OSError:
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
def clear_log(self):
|
|
424
|
+
self.log.delete("1.0", "end")
|
|
425
|
+
|
|
426
|
+
def _log_key(self, event):
|
|
427
|
+
# Permite Ctrl+C, Ctrl+A y desplazamiento; bloquea cualquier edición
|
|
428
|
+
if event.state & 0x4 and event.keysym.lower() in ("c", "a", "insert"):
|
|
429
|
+
if event.keysym.lower() == "a":
|
|
430
|
+
self.log.tag_add("sel", "1.0", "end-1c")
|
|
431
|
+
return "break"
|
|
432
|
+
return None
|
|
433
|
+
if event.keysym in ("Up", "Down", "Left", "Right", "Prior", "Next",
|
|
434
|
+
"Home", "End"):
|
|
435
|
+
return None
|
|
436
|
+
return "break"
|
|
437
|
+
|
|
438
|
+
def copy_log(self, everything=True):
|
|
439
|
+
try:
|
|
440
|
+
text = self.log.get("sel.first", "sel.last") if not everything \
|
|
441
|
+
else self.log.get("1.0", "end-1c")
|
|
442
|
+
except tk.TclError:
|
|
443
|
+
text = self.log.get("1.0", "end-1c")
|
|
444
|
+
self.clipboard_clear()
|
|
445
|
+
self.clipboard_append(text)
|
|
446
|
+
self.update()
|
|
447
|
+
|
|
448
|
+
def set_status(self, text, color):
|
|
449
|
+
self.status_lbl.config(text=text)
|
|
450
|
+
self.status_dot.itemconfig(self.dot, fill=color)
|
|
451
|
+
|
|
452
|
+
def save_settings(self, quiet=False):
|
|
453
|
+
self.cfg["robot_args"] = self.robot_txt.get("1.0", "end").strip()
|
|
454
|
+
self.cfg["episode_time_s"] = int(self.ep_time.get())
|
|
455
|
+
self.cfg["device"] = self.device_var.get().strip() or "cpu"
|
|
456
|
+
self.cfg["conda_env"] = self.conda_var.get().strip()
|
|
457
|
+
self.cfg["image_path"] = self.img_path_var.get().strip()
|
|
458
|
+
self.cfg["show_points"] = bool(self.show_points.get())
|
|
459
|
+
self.cfg["overlay_idle"] = bool(self.overlay_idle.get())
|
|
460
|
+
try:
|
|
461
|
+
self.cfg["anim_scale"] = float(self.scale_var.get())
|
|
462
|
+
except (tk.TclError, ValueError):
|
|
463
|
+
pass
|
|
464
|
+
for key, vars_ in self.task_vars.items():
|
|
465
|
+
for field, v in vars_.items():
|
|
466
|
+
self.cfg["tasks"][key][field] = v.get().strip()
|
|
467
|
+
mic = self.voice_mic.get().strip()
|
|
468
|
+
self.cfg["voice"] = {
|
|
469
|
+
"lang": self.voice_lang.get().strip() or "es-ES",
|
|
470
|
+
"mic": "" if mic in ("", "Predeterminado") else mic,
|
|
471
|
+
"confirm": bool(self.voice_confirm.get()),
|
|
472
|
+
"speak": bool(self.voice_speak.get()),
|
|
473
|
+
}
|
|
474
|
+
save_config(self.cfg)
|
|
475
|
+
if not quiet:
|
|
476
|
+
self.write_log(f"[GUI] Ajustes guardados en {CONFIG_PATH}\n")
|
|
477
|
+
|
|
478
|
+
def build_command(self, key):
|
|
479
|
+
t = self.task_vars[key]
|
|
480
|
+
policy = os.path.expandvars(os.path.expanduser(t["policy_path"].get().strip()))
|
|
481
|
+
cmd = ["lerobot-rollout"]
|
|
482
|
+
cmd += shlex.split(self.robot_txt.get("1.0", "end").strip())
|
|
483
|
+
cmd += [
|
|
484
|
+
f"--policy.path={policy}",
|
|
485
|
+
f"--task={t['task_text'].get().strip()}",
|
|
486
|
+
f"--duration={int(self.ep_time.get())}",
|
|
487
|
+
f"--device={self.device_var.get().strip() or 'cpu'}",
|
|
488
|
+
]
|
|
489
|
+
# Si la GUI no se lanzó desde el entorno activado, usar conda run
|
|
490
|
+
env = self.conda_var.get().strip()
|
|
491
|
+
if shutil.which("lerobot-rollout") is None and env and shutil.which("conda"):
|
|
492
|
+
cmd = ["conda", "run", "--no-capture-output", "-n", env] + cmd
|
|
493
|
+
return cmd
|
|
494
|
+
|
|
495
|
+
# ------------------------------------------------------ imagen / vista
|
|
496
|
+
def browse_image(self):
|
|
497
|
+
types = [("Imágenes", "*.png *.gif *.jpg *.jpeg *.bmp *.webp"),
|
|
498
|
+
("Todos", "*.*")] if HAS_PIL else [("PNG / GIF", "*.png *.gif"),
|
|
499
|
+
("Todos", "*.*")]
|
|
500
|
+
p = filedialog.askopenfilename(title="Imagen del portafusibles", filetypes=types)
|
|
501
|
+
if p:
|
|
502
|
+
self.img_path_var.set(p)
|
|
503
|
+
self.load_image()
|
|
504
|
+
|
|
505
|
+
def load_image(self):
|
|
506
|
+
path = self.img_path_var.get().strip()
|
|
507
|
+
self.raw = None
|
|
508
|
+
self.photo = None
|
|
509
|
+
if path:
|
|
510
|
+
if not os.path.isfile(path):
|
|
511
|
+
self.write_log(f"[GUI] Imagen no encontrada: {path}\n")
|
|
512
|
+
else:
|
|
513
|
+
try:
|
|
514
|
+
self.raw = Image.open(path).convert("RGB") if HAS_PIL \
|
|
515
|
+
else tk.PhotoImage(file=path)
|
|
516
|
+
except Exception as e:
|
|
517
|
+
self.raw = None
|
|
518
|
+
hint = "" if HAS_PIL else \
|
|
519
|
+
" (sin Pillow solo se admiten PNG/GIF: pip install pillow)"
|
|
520
|
+
self.write_log(f"[GUI] No se pudo abrir la imagen: {e}{hint}\n")
|
|
521
|
+
self.cfg["image_path"] = path
|
|
522
|
+
self._render_static()
|
|
523
|
+
|
|
524
|
+
def _on_canvas_resize(self, _event):
|
|
525
|
+
if self._resize_job is not None:
|
|
526
|
+
self.after_cancel(self._resize_job)
|
|
527
|
+
self._resize_job = self.after(120, self._render_static)
|
|
528
|
+
|
|
529
|
+
def _render_static(self):
|
|
530
|
+
self._resize_job = None
|
|
531
|
+
c = self.canvas
|
|
532
|
+
c.delete("bg")
|
|
533
|
+
cw = max(100, c.winfo_width())
|
|
534
|
+
ch = max(100, c.winfo_height())
|
|
535
|
+
|
|
536
|
+
if self.raw is not None:
|
|
537
|
+
if HAS_PIL:
|
|
538
|
+
w, h = self.raw.size
|
|
539
|
+
k = min(cw / w, ch / h)
|
|
540
|
+
nw, nh = max(1, int(w * k)), max(1, int(h * k))
|
|
541
|
+
self.photo = ImageTk.PhotoImage(self.raw.resize((nw, nh), RESAMPLE))
|
|
542
|
+
else:
|
|
543
|
+
w, h = self.raw.width(), self.raw.height()
|
|
544
|
+
f = max(1, math.ceil(max(w / cw, h / ch)))
|
|
545
|
+
self.photo = self.raw.subsample(f)
|
|
546
|
+
nw, nh = self.photo.width(), self.photo.height()
|
|
547
|
+
x0, y0 = (cw - nw) // 2, (ch - nh) // 2
|
|
548
|
+
self.img_rect = (x0, y0, nw, nh)
|
|
549
|
+
c.create_image(x0, y0, image=self.photo, anchor="nw", tags="bg")
|
|
550
|
+
else:
|
|
551
|
+
# Esquema (sin imagen): base portafusibles cuadrada,
|
|
552
|
+
# agujero de conexión arriba y fusible abajo
|
|
553
|
+
w = min(cw, ch * 4 / 3)
|
|
554
|
+
h = w * 3 / 4
|
|
555
|
+
x0, y0 = (cw - w) / 2, (ch - h) / 2
|
|
556
|
+
self.img_rect = (x0, y0, w, h)
|
|
557
|
+
c.create_rectangle(x0, y0, x0 + w, y0 + h, fill="#eceff1", outline="",
|
|
558
|
+
tags="bg")
|
|
559
|
+
s = self._units()
|
|
560
|
+
H, F = self._P("hole"), self._P("fuse")
|
|
561
|
+
cx, cy = (H[0] + F[0]) / 2, (H[1] + F[1]) / 2
|
|
562
|
+
half = 0.31 * h
|
|
563
|
+
# cuadrado = base portafusibles
|
|
564
|
+
c.create_rectangle(cx - half, cy - half, cx + half, cy + half,
|
|
565
|
+
fill="#455a64", outline="#263238", width=4, tags="bg")
|
|
566
|
+
c.create_rectangle(cx - half + 10, cy - half + 10, cx + half - 10,
|
|
567
|
+
cy + half - 10, fill="#37474f", outline="#546e7a",
|
|
568
|
+
width=2, tags="bg")
|
|
569
|
+
c.create_text(cx, cy - half - 12, text="PORTAFUSIBLES", fill="#546e7a",
|
|
570
|
+
font=("Helvetica", 10, "bold"), tags="bg")
|
|
571
|
+
# agujero de conexión (arriba)
|
|
572
|
+
r = 14 * s
|
|
573
|
+
c.create_oval(H[0] - r * 1.5, H[1] - r * 1.5, H[0] + r * 1.5, H[1] + r * 1.5,
|
|
574
|
+
fill="#90a4ae", outline="#cfd8dc", width=2, tags="bg")
|
|
575
|
+
c.create_oval(H[0] - r, H[1] - r, H[0] + r, H[1] + r,
|
|
576
|
+
fill="#050505", outline="", tags="bg")
|
|
577
|
+
# hueco donde va el fusible
|
|
578
|
+
c.create_rectangle(F[0] - 14 * s, F[1] - 19 * s, F[0] + 14 * s, F[1] + 19 * s,
|
|
579
|
+
fill="#1b262c", outline="#78909c", width=2, tags="bg")
|
|
580
|
+
c.create_text(x0 + w / 2, y0 + h - 12, fill="#78909c", tags="bg",
|
|
581
|
+
font=("Helvetica", 9),
|
|
582
|
+
text="Sin imagen · indica una ruta abajo y pulsa Cargar")
|
|
583
|
+
c.tag_lower("bg")
|
|
584
|
+
|
|
585
|
+
# ---------------------------------------------------------- calibración
|
|
586
|
+
def start_calib(self, mode):
|
|
587
|
+
self.calib_mode = mode
|
|
588
|
+
self.canvas.config(cursor="crosshair")
|
|
589
|
+
self.show_points.set(True)
|
|
590
|
+
|
|
591
|
+
def _on_canvas_click(self, ev):
|
|
592
|
+
if not self.calib_mode or not self.img_rect:
|
|
593
|
+
return
|
|
594
|
+
x0, y0, w, h = self.img_rect
|
|
595
|
+
fx = min(1.0, max(0.0, (ev.x - x0) / w))
|
|
596
|
+
fy = min(1.0, max(0.0, (ev.y - y0) / h))
|
|
597
|
+
self.cfg["points"][self.calib_mode] = [round(fx, 4), round(fy, 4)]
|
|
598
|
+
names = {"hole": "agujero", "fuse": "fusible", "cable_start": "origen del cable"}
|
|
599
|
+
self.write_log(f"[GUI] Punto «{names[self.calib_mode]}» fijado en "
|
|
600
|
+
f"({fx:.2f}, {fy:.2f})\n")
|
|
601
|
+
self.calib_mode = None
|
|
602
|
+
self.canvas.config(cursor="")
|
|
603
|
+
self.save_settings(quiet=True)
|
|
604
|
+
self._render_static()
|
|
605
|
+
|
|
606
|
+
def reset_view(self):
|
|
607
|
+
self.cable_in = False
|
|
608
|
+
self.fuse_out = False
|
|
609
|
+
|
|
610
|
+
# ---------------------------------------------------------- animación
|
|
611
|
+
def _units(self):
|
|
612
|
+
w = self.img_rect[2] if self.img_rect else 600
|
|
613
|
+
try:
|
|
614
|
+
k = float(self.scale_var.get())
|
|
615
|
+
except (tk.TclError, ValueError, AttributeError):
|
|
616
|
+
k = 1.0
|
|
617
|
+
base = (w / 600.0) * (1.7 if self.raw is None else 1.0)
|
|
618
|
+
return max(0.3, base * k)
|
|
619
|
+
|
|
620
|
+
def _P(self, key):
|
|
621
|
+
x0, y0, w, h = self.img_rect
|
|
622
|
+
px, py = self.cfg["points"][key]
|
|
623
|
+
return (x0 + px * w, y0 + py * h)
|
|
624
|
+
|
|
625
|
+
def _anim_loop(self):
|
|
626
|
+
try:
|
|
627
|
+
self._anim_frame()
|
|
628
|
+
finally:
|
|
629
|
+
self.after(FRAME_MS, self._anim_loop)
|
|
630
|
+
|
|
631
|
+
def _anim_frame(self):
|
|
632
|
+
c = self.canvas
|
|
633
|
+
c.delete("anim")
|
|
634
|
+
if self.img_rect is None:
|
|
635
|
+
return
|
|
636
|
+
s = self._units()
|
|
637
|
+
H = self._P("hole")
|
|
638
|
+
F = self._P("fuse")
|
|
639
|
+
S = self._P("cable_start")
|
|
640
|
+
run = self.current_task
|
|
641
|
+
t = ((time.time() - self.t_anim0) % PERIOD) / PERIOD if run else 0.0
|
|
642
|
+
has_img = self.raw is not None
|
|
643
|
+
draw_idle = self.overlay_idle.get()
|
|
644
|
+
|
|
645
|
+
D = 0.27 * self.img_rect[3] # recorrido de extracción (hacia abajo)
|
|
646
|
+
hole_in = (H[0], H[1] + 3 * s) # cable metido
|
|
647
|
+
cable_tip = hole_in if self.cable_in else S
|
|
648
|
+
fuse_dy = D if self.fuse_out else 0.0
|
|
649
|
+
gripper = None # (x, y, gap)
|
|
650
|
+
label = ""
|
|
651
|
+
|
|
652
|
+
if run == "insertar":
|
|
653
|
+
A = (H[0], H[1] - 60 * s)
|
|
654
|
+
gap_closed, gap_open = 5 * s, 13 * s
|
|
655
|
+
gap = gap_closed
|
|
656
|
+
gdy = 0.0
|
|
657
|
+
if t < 0.10:
|
|
658
|
+
cable_tip = S
|
|
659
|
+
label = "Agarrando el cable…"
|
|
660
|
+
elif t < 0.55:
|
|
661
|
+
u = ease((t - 0.10) / 0.45)
|
|
662
|
+
cable_tip = lerp_pt(S, A, u)
|
|
663
|
+
cable_tip = (cable_tip[0], cable_tip[1] - math.sin(math.pi * u) * 30 * s)
|
|
664
|
+
label = "Transportando el cable…"
|
|
665
|
+
elif t < 0.75:
|
|
666
|
+
cable_tip = lerp_pt(A, hole_in, ease((t - 0.55) / 0.20))
|
|
667
|
+
label = "Insertando en el agujero…"
|
|
668
|
+
elif t < 0.95:
|
|
669
|
+
cable_tip = hole_in
|
|
670
|
+
gap = lerp(gap_closed, gap_open, window(t, 0.75, 0.82))
|
|
671
|
+
gdy = -55 * s * window(t, 0.85, 0.95)
|
|
672
|
+
label = "Soltando el cable"
|
|
673
|
+
else:
|
|
674
|
+
cable_tip = lerp_pt(hole_in, S, ease((t - 0.95) / 0.05))
|
|
675
|
+
gap = gap_open
|
|
676
|
+
gdy = -55 * s
|
|
677
|
+
gripper = (cable_tip[0], cable_tip[1] - 18 * s + gdy, gap, False)
|
|
678
|
+
|
|
679
|
+
elif run == "extraer":
|
|
680
|
+
U = (F[0], F[1] + 140 * s) # la pinza llega desde abajo
|
|
681
|
+
G0 = (F[0], F[1] + 14 * s) # punto de agarre del fusible
|
|
682
|
+
gap_closed, gap_open = 23 * s, 34 * s
|
|
683
|
+
if t < 0.30:
|
|
684
|
+
g = lerp_pt(U, G0, ease(t / 0.30))
|
|
685
|
+
gap = gap_open
|
|
686
|
+
lift = 0.0
|
|
687
|
+
label = "Acercando la pinza al fusible…"
|
|
688
|
+
elif t < 0.40:
|
|
689
|
+
g = G0
|
|
690
|
+
gap = lerp(gap_open, gap_closed, window(t, 0.30, 0.40))
|
|
691
|
+
lift = 0.0
|
|
692
|
+
label = "Agarrando el fusible…"
|
|
693
|
+
elif t < 0.75:
|
|
694
|
+
lift = ease((t - 0.40) / 0.35)
|
|
695
|
+
g = (G0[0] + math.sin(lift * math.pi * 3) * 1.5 * s, G0[1] + D * lift)
|
|
696
|
+
gap = gap_closed
|
|
697
|
+
label = "Extrayendo el fusible…"
|
|
698
|
+
elif t < 0.88:
|
|
699
|
+
lift = 1.0
|
|
700
|
+
g = (G0[0], G0[1] + D)
|
|
701
|
+
gap = gap_closed
|
|
702
|
+
label = "Fusible quemado extraído"
|
|
703
|
+
else:
|
|
704
|
+
lift = 1.0 - ease((t - 0.88) / 0.12)
|
|
705
|
+
g = (G0[0], G0[1] + D * lift)
|
|
706
|
+
gap = gap_closed
|
|
707
|
+
label = "Fusible quemado extraído"
|
|
708
|
+
fuse_dy = D * lift
|
|
709
|
+
gripper = (g[0], g[1], gap, True)
|
|
710
|
+
|
|
711
|
+
# --- Marco del fusible (siempre visible)
|
|
712
|
+
self._draw_slot(c, F, s)
|
|
713
|
+
|
|
714
|
+
# --- Cable
|
|
715
|
+
show_cable = run == "insertar" or (draw_idle and (self.cable_in or not has_img)) \
|
|
716
|
+
or (run == "extraer" and self.cable_in) \
|
|
717
|
+
or (draw_idle and has_img)
|
|
718
|
+
if show_cable:
|
|
719
|
+
self._draw_cable(c, S, cable_tip, s)
|
|
720
|
+
|
|
721
|
+
# --- Fusible
|
|
722
|
+
moving = run == "extraer" or self.fuse_out
|
|
723
|
+
if moving or (draw_idle and not has_img):
|
|
724
|
+
self._draw_fuse(c, F[0], F[1] + fuse_dy, s)
|
|
725
|
+
|
|
726
|
+
# --- Pinza
|
|
727
|
+
if gripper:
|
|
728
|
+
self._draw_gripper(c, gripper[0], gripper[1], s, gripper[2], gripper[3])
|
|
729
|
+
|
|
730
|
+
# --- Texto de estado
|
|
731
|
+
if self.calib_mode:
|
|
732
|
+
names = {"hole": "agujero de conexión", "fuse": "fusible",
|
|
733
|
+
"cable_start": "origen del cable"}
|
|
734
|
+
label = f"Haz clic en la imagen: {names[self.calib_mode]}"
|
|
735
|
+
elif not run:
|
|
736
|
+
if self.cable_in and self.fuse_out:
|
|
737
|
+
label = "Cable insertado · fusible extraído"
|
|
738
|
+
elif self.cable_in:
|
|
739
|
+
label = "Cable insertado"
|
|
740
|
+
elif self.fuse_out:
|
|
741
|
+
label = "Fusible quemado extraído"
|
|
742
|
+
if label:
|
|
743
|
+
x0, y0 = self.img_rect[0], self.img_rect[1]
|
|
744
|
+
c.create_rectangle(x0 + 8, y0 + 8, x0 + 12 + 8.2 * len(label), y0 + 32,
|
|
745
|
+
fill="#000000", outline="", tags="anim")
|
|
746
|
+
c.create_text(x0 + 14, y0 + 20, text=label, anchor="w", fill="#ffffff",
|
|
747
|
+
font=("Helvetica", 11, "bold"), tags="anim")
|
|
748
|
+
|
|
749
|
+
# --- Puntos de calibración
|
|
750
|
+
if self.show_points.get() or self.calib_mode:
|
|
751
|
+
for key, col, txt in (("hole", "#ff5252", "Agujero"),
|
|
752
|
+
("fuse", "#ffd740", "Fusible"),
|
|
753
|
+
("cable_start", "#ff8a80", "Origen cable")):
|
|
754
|
+
px, py = self._P(key)
|
|
755
|
+
c.create_oval(px - 5, py - 5, px + 5, py + 5, outline=col, width=2,
|
|
756
|
+
tags="anim")
|
|
757
|
+
c.create_line(px - 10, py, px + 10, py, fill=col, tags="anim")
|
|
758
|
+
c.create_line(px, py - 10, px, py + 10, fill=col, tags="anim")
|
|
759
|
+
c.create_text(px + 10, py - 12, text=txt, fill=col, anchor="w",
|
|
760
|
+
font=("Helvetica", 9, "bold"), tags="anim")
|
|
761
|
+
|
|
762
|
+
# ----- dibujo de elementos
|
|
763
|
+
def _draw_slot(self, c, F, s):
|
|
764
|
+
c.create_rectangle(F[0] - 12 * s, F[1] - 16 * s, F[0] + 12 * s, F[1] + 16 * s,
|
|
765
|
+
outline="#fff59d", dash=(4, 3), width=2, tags="anim")
|
|
766
|
+
|
|
767
|
+
def _draw_cable(self, c, S, T, s):
|
|
768
|
+
mid = ((S[0] + T[0]) / 2, max(S[1], T[1]) + 30 * s)
|
|
769
|
+
pts = [S[0], S[1], mid[0], mid[1], T[0], T[1] - 12 * s]
|
|
770
|
+
c.create_line(*pts, smooth=True, width=9 * s, fill="#7f0000",
|
|
771
|
+
capstyle="round", tags="anim")
|
|
772
|
+
c.create_line(*pts, smooth=True, width=6 * s, fill="#e53935",
|
|
773
|
+
capstyle="round", tags="anim")
|
|
774
|
+
# terminal metálico
|
|
775
|
+
c.create_rectangle(T[0] - 4 * s, T[1] - 14 * s, T[0] + 4 * s, T[1],
|
|
776
|
+
fill="#c98a4b", outline="#5d4037", tags="anim")
|
|
777
|
+
|
|
778
|
+
def _draw_fuse(self, c, cx, cy, s):
|
|
779
|
+
w, h = 11 * s, 15 * s
|
|
780
|
+
# patillas
|
|
781
|
+
for dx in (-5 * s, 5 * s):
|
|
782
|
+
c.create_rectangle(cx + dx - 2 * s, cy + h, cx + dx + 2 * s, cy + h + 8 * s,
|
|
783
|
+
fill="#b0bec5", outline="#37474f", tags="anim")
|
|
784
|
+
# cuerpo
|
|
785
|
+
c.create_rectangle(cx - w, cy - h, cx + w, cy + h, fill="#fbc02d",
|
|
786
|
+
outline="#6d4c00", width=2, tags="anim")
|
|
787
|
+
# ventana ennegrecida (fusible quemado)
|
|
788
|
+
c.create_rectangle(cx - 8 * s, cy - 12 * s, cx + 8 * s, cy + 5 * s,
|
|
789
|
+
fill="#212121", outline="#8d6e00", tags="anim")
|
|
790
|
+
# rayo
|
|
791
|
+
bolt = [(1.5, -9), (-5, 0.5), (-1, 0.5), (-2.5, 8), (5, -2), (1, -2)]
|
|
792
|
+
k = 0.85 * s
|
|
793
|
+
pts = []
|
|
794
|
+
for bx, by in bolt:
|
|
795
|
+
pts += [cx + bx * k, cy - 3.5 * s + by * k]
|
|
796
|
+
c.create_polygon(*pts, fill="#ffeb3b", outline="#ff6f00", tags="anim")
|
|
797
|
+
|
|
798
|
+
def _draw_gripper(self, c, x, y, s, gap, flip=False):
|
|
799
|
+
"""flip=False: muñeca arriba y mordazas hacia abajo; flip=True: al revés."""
|
|
800
|
+
k = -1 if flip else 1
|
|
801
|
+
jaw_w = 5 * s
|
|
802
|
+
# muñeca
|
|
803
|
+
c.create_rectangle(x - gap / 2 - jaw_w - 1 * s, y - k * 42 * s,
|
|
804
|
+
x + gap / 2 + jaw_w + 1 * s, y - k * 24 * s,
|
|
805
|
+
fill="#455a64", outline="#102027", tags="anim")
|
|
806
|
+
# mordazas
|
|
807
|
+
for sign in (-1, 1):
|
|
808
|
+
xi = x + sign * gap / 2
|
|
809
|
+
xo = x + sign * (gap / 2 + jaw_w)
|
|
810
|
+
c.create_rectangle(min(xi, xo), y - k * 26 * s, max(xi, xo), y + k * 6 * s,
|
|
811
|
+
fill="#78909c", outline="#102027", tags="anim")
|
|
812
|
+
|
|
813
|
+
# ------------------------------------------------------------------ voz
|
|
814
|
+
VOICE_HELP = ("Di: «inserta el cable» · «extrae el fusible» · «para» · "
|
|
815
|
+
"«reinicia la vista». Para confirmar: «sí» o «no».")
|
|
816
|
+
|
|
817
|
+
def _build_voice(self, parent):
|
|
818
|
+
vc = self.cfg.get("voice", {})
|
|
819
|
+
box = ttk.LabelFrame(parent, text="Control por voz", padding=8)
|
|
820
|
+
box.pack(fill="x", pady=(8, 0))
|
|
821
|
+
|
|
822
|
+
top = ttk.Frame(box)
|
|
823
|
+
top.pack(fill="x")
|
|
824
|
+
self.voice_btn = ttk.Button(top, text="🎤 Activar voz", command=self.toggle_voice)
|
|
825
|
+
self.voice_btn.pack(side="left")
|
|
826
|
+
self.voice_state = ttk.Label(top, text="Desactivada")
|
|
827
|
+
self.voice_state.pack(side="left", padx=10)
|
|
828
|
+
|
|
829
|
+
row = ttk.Frame(box)
|
|
830
|
+
row.pack(fill="x", pady=(6, 0))
|
|
831
|
+
ttk.Label(row, text="Idioma:").pack(side="left")
|
|
832
|
+
self.voice_lang = tk.StringVar(value=vc.get("lang", "es-ES"))
|
|
833
|
+
ttk.Combobox(row, textvariable=self.voice_lang, width=7,
|
|
834
|
+
values=("es-ES", "es-MX", "en-US", "en-GB")).pack(side="left", padx=4)
|
|
835
|
+
ttk.Label(row, text="Micrófono:").pack(side="left", padx=(8, 0))
|
|
836
|
+
self.voice_mic = tk.StringVar(value=vc.get("mic") or "Predeterminado")
|
|
837
|
+
self.mic_combo = ttk.Combobox(row, textvariable=self.voice_mic, width=22,
|
|
838
|
+
values=("Predeterminado",),
|
|
839
|
+
postcommand=self._refresh_mics)
|
|
840
|
+
self.mic_combo.pack(side="left", padx=4, fill="x", expand=True)
|
|
841
|
+
|
|
842
|
+
opts = ttk.Frame(box)
|
|
843
|
+
opts.pack(fill="x", pady=(6, 0))
|
|
844
|
+
self.voice_confirm = tk.BooleanVar(value=bool(vc.get("confirm", True)))
|
|
845
|
+
self.voice_speak = tk.BooleanVar(value=bool(vc.get("speak", True)))
|
|
846
|
+
ttk.Checkbutton(opts, text="Pedir confirmación («sí»)",
|
|
847
|
+
variable=self.voice_confirm).pack(side="left")
|
|
848
|
+
ttk.Checkbutton(opts, text="Respuesta hablada",
|
|
849
|
+
variable=self.voice_speak).pack(side="left", padx=10)
|
|
850
|
+
|
|
851
|
+
self.voice_heard = ttk.Label(box, text="Oído: —", wraplength=420)
|
|
852
|
+
self.voice_heard.pack(fill="x", pady=(6, 0))
|
|
853
|
+
ttk.Label(box, text=self.VOICE_HELP, wraplength=420,
|
|
854
|
+
foreground="#666666").pack(fill="x", pady=(2, 0))
|
|
855
|
+
|
|
856
|
+
def _refresh_mics(self):
|
|
857
|
+
if not HAS_SR:
|
|
858
|
+
return
|
|
859
|
+
try:
|
|
860
|
+
names = sr.Microphone.list_microphone_names()
|
|
861
|
+
self.mic_combo.configure(values=("Predeterminado", *names))
|
|
862
|
+
except Exception:
|
|
863
|
+
pass
|
|
864
|
+
|
|
865
|
+
def toggle_voice(self):
|
|
866
|
+
if self.voice_on:
|
|
867
|
+
self._voice_stop()
|
|
868
|
+
else:
|
|
869
|
+
self._voice_start()
|
|
870
|
+
|
|
871
|
+
def _voice_start(self):
|
|
872
|
+
if not HAS_SR:
|
|
873
|
+
messagebox.showerror(
|
|
874
|
+
"Falta SpeechRecognition",
|
|
875
|
+
"Para usar la voz instala, en el entorno donde ejecutas la GUI:\n\n"
|
|
876
|
+
" pip install SpeechRecognition pyaudio\n\n"
|
|
877
|
+
"En Ubuntu puede hacer falta antes:\n"
|
|
878
|
+
" sudo apt install portaudio19-dev flac espeak-ng")
|
|
879
|
+
return
|
|
880
|
+
self._voice_lang = self.voice_lang.get().strip() or "es-ES"
|
|
881
|
+
mic = self.voice_mic.get().strip()
|
|
882
|
+
self._voice_mic_name = "" if mic in ("", "Predeterminado") else mic
|
|
883
|
+
self.voice_on = True
|
|
884
|
+
self.voice_btn.config(text="🎤 Desactivar voz")
|
|
885
|
+
self.voice_state.config(text="Iniciando micrófono…")
|
|
886
|
+
self.save_settings(quiet=True)
|
|
887
|
+
threading.Thread(target=self._voice_worker, daemon=True).start()
|
|
888
|
+
|
|
889
|
+
def _voice_stop(self):
|
|
890
|
+
self.voice_on = False
|
|
891
|
+
self.pending = None
|
|
892
|
+
self.voice_btn.config(text="🎤 Activar voz")
|
|
893
|
+
self.voice_state.config(text="Desactivada")
|
|
894
|
+
|
|
895
|
+
def _voice_set_state(self, text):
|
|
896
|
+
if self.voice_on:
|
|
897
|
+
self.voice_state.config(text=text)
|
|
898
|
+
|
|
899
|
+
def _voice_failed(self, msg):
|
|
900
|
+
self.voice_on = False
|
|
901
|
+
self.pending = None
|
|
902
|
+
self.voice_btn.config(text="🎤 Activar voz")
|
|
903
|
+
self.voice_state.config(text="Error de micrófono")
|
|
904
|
+
self.write_log(f"[VOZ] No se pudo usar el micrófono: {msg}\n"
|
|
905
|
+
"[VOZ] Comprueba que PyAudio está instalado y que el micrófono "
|
|
906
|
+
"no está silenciado.\n")
|
|
907
|
+
|
|
908
|
+
def _voice_worker(self):
|
|
909
|
+
lang = self._voice_lang
|
|
910
|
+
mic_name = self._voice_mic_name
|
|
911
|
+
r = sr.Recognizer()
|
|
912
|
+
r.dynamic_energy_threshold = True
|
|
913
|
+
try:
|
|
914
|
+
idx = None
|
|
915
|
+
if mic_name:
|
|
916
|
+
names = sr.Microphone.list_microphone_names()
|
|
917
|
+
if mic_name in names:
|
|
918
|
+
idx = names.index(mic_name)
|
|
919
|
+
with sr.Microphone(device_index=idx) as source:
|
|
920
|
+
r.adjust_for_ambient_noise(source, duration=1.0)
|
|
921
|
+
self.after(0, self._voice_set_state, "🎤 Escuchando…")
|
|
922
|
+
self.after(0, self.write_log,
|
|
923
|
+
f"[VOZ] Micrófono listo ({mic_name or 'predeterminado'}), "
|
|
924
|
+
f"umbral de ruido {r.energy_threshold:.0f}\n")
|
|
925
|
+
silent = 0
|
|
926
|
+
while self.voice_on:
|
|
927
|
+
t0 = time.time()
|
|
928
|
+
try:
|
|
929
|
+
audio = r.listen(source, timeout=1, phrase_time_limit=6)
|
|
930
|
+
except sr.WaitTimeoutError:
|
|
931
|
+
silent += 1
|
|
932
|
+
if silent >= 15: # ~15 s sin detectar voz
|
|
933
|
+
silent = 0
|
|
934
|
+
self.after(0, self.write_log,
|
|
935
|
+
"[VOZ] No detecto voz: el nivel no supera el umbral "
|
|
936
|
+
f"({r.energy_threshold:.0f}). Habla más cerca, sube el "
|
|
937
|
+
"volumen de captura o prueba otro micrófono.\n")
|
|
938
|
+
continue
|
|
939
|
+
silent = 0
|
|
940
|
+
# Descartar audio que coincide con la voz del propio programa
|
|
941
|
+
if self.speaking or self.last_speech_end > t0:
|
|
942
|
+
continue
|
|
943
|
+
try:
|
|
944
|
+
text = r.recognize_google(audio, language=lang)
|
|
945
|
+
except sr.UnknownValueError:
|
|
946
|
+
dur = len(audio.frame_data) / (audio.sample_rate * audio.sample_width)
|
|
947
|
+
self.after(0, self.write_log,
|
|
948
|
+
f"[VOZ] Audio recibido pero no entendido "
|
|
949
|
+
f"({dur:.1f} s, nivel {_audio_level(audio)}, "
|
|
950
|
+
f"umbral {r.energy_threshold:.0f})\n")
|
|
951
|
+
continue
|
|
952
|
+
except sr.RequestError as e:
|
|
953
|
+
self.after(0, self.write_log,
|
|
954
|
+
f"[VOZ] Error de reconocimiento (¿sin internet?): {e}\n")
|
|
955
|
+
time.sleep(2)
|
|
956
|
+
continue
|
|
957
|
+
self.after(0, self._on_voice_text, text)
|
|
958
|
+
except Exception as e: # micrófono no disponible, PyAudio ausente, etc.
|
|
959
|
+
self.after(0, self._voice_failed, str(e))
|
|
960
|
+
|
|
961
|
+
def _on_voice_text(self, text):
|
|
962
|
+
if not self.voice_on:
|
|
963
|
+
return
|
|
964
|
+
self.voice_heard.config(text=f"Oído: «{text}»")
|
|
965
|
+
cmd = parse_voice(text)
|
|
966
|
+
self.write_log(f"[VOZ] «{text}»" + (f" → {cmd}" if cmd else " (sin comando)") + "\n")
|
|
967
|
+
if cmd is None:
|
|
968
|
+
return
|
|
969
|
+
|
|
970
|
+
if cmd == "stop":
|
|
971
|
+
self.pending = None
|
|
972
|
+
if self.proc is not None:
|
|
973
|
+
self.stop_task()
|
|
974
|
+
self._say("Parando")
|
|
975
|
+
else:
|
|
976
|
+
self._say("No hay ninguna tarea en marcha")
|
|
977
|
+
return
|
|
978
|
+
|
|
979
|
+
if cmd in ("yes", "no"):
|
|
980
|
+
if not self.pending:
|
|
981
|
+
return
|
|
982
|
+
key, deadline = self.pending
|
|
983
|
+
self.pending = None
|
|
984
|
+
self._voice_set_state("🎤 Escuchando…")
|
|
985
|
+
if cmd == "yes" and time.time() <= deadline and self.proc is None:
|
|
986
|
+
self._say("De acuerdo")
|
|
987
|
+
self.start_task(key, confirmed=True)
|
|
988
|
+
else:
|
|
989
|
+
self._say("Cancelado")
|
|
990
|
+
return
|
|
991
|
+
|
|
992
|
+
if cmd == "reset":
|
|
993
|
+
self.reset_view()
|
|
994
|
+
self._say("Vista reiniciada")
|
|
995
|
+
return
|
|
996
|
+
|
|
997
|
+
if cmd in self.cfg["tasks"]: # insertar / extraer
|
|
998
|
+
if self.proc is not None:
|
|
999
|
+
self._say("Ya hay una tarea en marcha")
|
|
1000
|
+
return
|
|
1001
|
+
label = self.cfg["tasks"][cmd]["label"]
|
|
1002
|
+
if self.voice_confirm.get():
|
|
1003
|
+
self.pending = (cmd, time.time() + 10)
|
|
1004
|
+
self.voice_state.config(text=f"¿Confirmas «{label}»? Di sí o no")
|
|
1005
|
+
self._say(f"¿Confirmas {label}? Di sí o no")
|
|
1006
|
+
else:
|
|
1007
|
+
self._say(label)
|
|
1008
|
+
self.start_task(cmd, confirmed=True)
|
|
1009
|
+
|
|
1010
|
+
def _voice_tick(self):
|
|
1011
|
+
if self.pending and time.time() > self.pending[1]:
|
|
1012
|
+
self.pending = None
|
|
1013
|
+
self.write_log("[VOZ] Confirmación caducada\n")
|
|
1014
|
+
self._voice_set_state("🎤 Escuchando…")
|
|
1015
|
+
|
|
1016
|
+
def _say(self, text):
|
|
1017
|
+
"""Registra la respuesta y, si hay un sintetizador instalado, la dice en voz alta."""
|
|
1018
|
+
self.write_log(f"[VOZ] ← {text}\n")
|
|
1019
|
+
if not self.voice_speak.get():
|
|
1020
|
+
return
|
|
1021
|
+
short = (self.voice_lang.get().strip() or "es-ES")[:2]
|
|
1022
|
+
if shutil.which("espeak-ng"):
|
|
1023
|
+
cmd = ["espeak-ng", "-v", short, text]
|
|
1024
|
+
elif shutil.which("espeak"):
|
|
1025
|
+
cmd = ["espeak", "-v", short, text]
|
|
1026
|
+
elif shutil.which("spd-say"):
|
|
1027
|
+
cmd = ["spd-say", "-w", "-l", short, text]
|
|
1028
|
+
else:
|
|
1029
|
+
return
|
|
1030
|
+
|
|
1031
|
+
def run():
|
|
1032
|
+
with self._speak_lock:
|
|
1033
|
+
self._speak_n += 1
|
|
1034
|
+
self.speaking = True
|
|
1035
|
+
try:
|
|
1036
|
+
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
1037
|
+
timeout=20)
|
|
1038
|
+
except Exception:
|
|
1039
|
+
pass
|
|
1040
|
+
finally:
|
|
1041
|
+
with self._speak_lock:
|
|
1042
|
+
self._speak_n -= 1
|
|
1043
|
+
if self._speak_n == 0:
|
|
1044
|
+
self.speaking = False
|
|
1045
|
+
self.last_speech_end = time.time()
|
|
1046
|
+
|
|
1047
|
+
threading.Thread(target=run, daemon=True).start()
|
|
1048
|
+
|
|
1049
|
+
# ------------------------------------------------------------- acciones
|
|
1050
|
+
def start_task(self, key, confirmed=False):
|
|
1051
|
+
if self.proc is not None:
|
|
1052
|
+
return
|
|
1053
|
+
self.save_settings(quiet=True)
|
|
1054
|
+
label = self.cfg["tasks"][key]["label"]
|
|
1055
|
+
try:
|
|
1056
|
+
cmd = self.build_command(key)
|
|
1057
|
+
except ValueError as e:
|
|
1058
|
+
messagebox.showerror("Error en los argumentos", str(e))
|
|
1059
|
+
return
|
|
1060
|
+
|
|
1061
|
+
if not confirmed and not messagebox.askokcancel(
|
|
1062
|
+
"Confirmar",
|
|
1063
|
+
f"Se va a ejecutar «{label}».\n\n"
|
|
1064
|
+
"Comprueba que la zona del brazo está despejada y "
|
|
1065
|
+
"ten a mano el botón PARAR."):
|
|
1066
|
+
return
|
|
1067
|
+
|
|
1068
|
+
self.write_log(f"\n[GUI] >>> {' '.join(shlex.quote(c) for c in cmd)}\n")
|
|
1069
|
+
try:
|
|
1070
|
+
kwargs = {}
|
|
1071
|
+
if os.name == "nt":
|
|
1072
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
1073
|
+
env = dict(os.environ, PYTHONUNBUFFERED="1")
|
|
1074
|
+
self.proc = subprocess.Popen(
|
|
1075
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
1076
|
+
text=True, bufsize=1, env=env, **kwargs)
|
|
1077
|
+
except FileNotFoundError:
|
|
1078
|
+
messagebox.showerror(
|
|
1079
|
+
"No se encuentra lerobot-rollout",
|
|
1080
|
+
"Lanza esta interfaz desde la terminal con el entorno activado "
|
|
1081
|
+
"(conda activate lerobot) o indica el nombre del entorno conda "
|
|
1082
|
+
"en la pestaña Robot.")
|
|
1083
|
+
self.proc = None
|
|
1084
|
+
return
|
|
1085
|
+
|
|
1086
|
+
# Al empezar, la tarea parte del estado inicial de su elemento
|
|
1087
|
+
if key == "insertar":
|
|
1088
|
+
self.cable_in = False
|
|
1089
|
+
elif key == "extraer":
|
|
1090
|
+
self.fuse_out = False
|
|
1091
|
+
|
|
1092
|
+
self.current_task = key
|
|
1093
|
+
self.t_start = time.time()
|
|
1094
|
+
self.t_anim0 = time.time()
|
|
1095
|
+
for b in self.buttons.values():
|
|
1096
|
+
b.config(state="disabled")
|
|
1097
|
+
self.stop_btn.config(state="normal")
|
|
1098
|
+
self.set_status(f"Ejecutando: {label}", "#2e7d32")
|
|
1099
|
+
threading.Thread(target=self._reader, daemon=True).start()
|
|
1100
|
+
|
|
1101
|
+
def _reader(self):
|
|
1102
|
+
proc = self.proc
|
|
1103
|
+
for line in proc.stdout:
|
|
1104
|
+
self.after(0, self.write_log, line)
|
|
1105
|
+
code = proc.wait()
|
|
1106
|
+
self.after(0, self._finished, code)
|
|
1107
|
+
|
|
1108
|
+
def _finished(self, code):
|
|
1109
|
+
key = self.current_task
|
|
1110
|
+
label = self.cfg["tasks"][key]["label"]
|
|
1111
|
+
self.write_log(f"[GUI] Proceso terminado (código {code})\n")
|
|
1112
|
+
if code == 0:
|
|
1113
|
+
if key == "insertar":
|
|
1114
|
+
self.cable_in = True
|
|
1115
|
+
elif key == "extraer":
|
|
1116
|
+
self.fuse_out = True
|
|
1117
|
+
self.proc = None
|
|
1118
|
+
self.current_task = None
|
|
1119
|
+
for b in self.buttons.values():
|
|
1120
|
+
b.config(state="normal")
|
|
1121
|
+
self.stop_btn.config(state="disabled")
|
|
1122
|
+
if code == 0:
|
|
1123
|
+
self.set_status(f"Terminado: {label}", "#1565c0")
|
|
1124
|
+
else:
|
|
1125
|
+
self.set_status(f"Detenido / error: {label}", "#c62828")
|
|
1126
|
+
|
|
1127
|
+
def stop_task(self):
|
|
1128
|
+
if self.proc is None:
|
|
1129
|
+
return
|
|
1130
|
+
self.write_log("[GUI] Deteniendo…\n")
|
|
1131
|
+
try:
|
|
1132
|
+
if os.name == "nt":
|
|
1133
|
+
self.proc.send_signal(signal.CTRL_BREAK_EVENT)
|
|
1134
|
+
else:
|
|
1135
|
+
self.proc.send_signal(signal.SIGINT) # parada limpia
|
|
1136
|
+
except Exception:
|
|
1137
|
+
self.proc.terminate()
|
|
1138
|
+
self.after(5000, self._force_kill)
|
|
1139
|
+
|
|
1140
|
+
def _force_kill(self):
|
|
1141
|
+
if self.proc is not None and self.proc.poll() is None:
|
|
1142
|
+
self.write_log("[GUI] Forzando cierre del proceso\n")
|
|
1143
|
+
self.proc.kill()
|
|
1144
|
+
|
|
1145
|
+
def _tick(self):
|
|
1146
|
+
if self.proc is not None:
|
|
1147
|
+
sec = int(time.time() - self.t_start)
|
|
1148
|
+
self.timer_lbl.config(text=f"{sec // 60:02d}:{sec % 60:02d}")
|
|
1149
|
+
self._voice_tick()
|
|
1150
|
+
self.after(500, self._tick)
|
|
1151
|
+
|
|
1152
|
+
def on_close(self):
|
|
1153
|
+
if self.proc is not None:
|
|
1154
|
+
if not messagebox.askyesno("Salir", "Hay una tarea en marcha. ¿Pararla y salir?"):
|
|
1155
|
+
return
|
|
1156
|
+
self.stop_task()
|
|
1157
|
+
time.sleep(1.0)
|
|
1158
|
+
self.voice_on = False
|
|
1159
|
+
try:
|
|
1160
|
+
self.save_settings(quiet=True)
|
|
1161
|
+
except Exception:
|
|
1162
|
+
pass
|
|
1163
|
+
self.destroy()
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
if __name__ == "__main__":
|
|
1167
|
+
App().mainloop()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lerobotberto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interfaz gráfica para ejecutar tareas entrenadas con LeRobot
|
|
5
|
+
License-Expression: LicenseRef-LerobotBerto-Proprietary
|
|
6
|
+
Requires-Python: <3.13,>=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE.txt
|
|
9
|
+
Provides-Extra: imagenes
|
|
10
|
+
Requires-Dist: Pillow==12.3.0; extra == "imagenes"
|
|
11
|
+
Provides-Extra: voz
|
|
12
|
+
Requires-Dist: SpeechRecognition==3.17.0; extra == "voz"
|
|
13
|
+
Requires-Dist: PyAudio==0.2.14; extra == "voz"
|
|
14
|
+
Provides-Extra: completo
|
|
15
|
+
Requires-Dist: Pillow==12.3.0; extra == "completo"
|
|
16
|
+
Requires-Dist: SpeechRecognition==3.17.0; extra == "completo"
|
|
17
|
+
Requires-Dist: PyAudio==0.2.14; extra == "completo"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# LeRobotBerto
|
|
21
|
+
|
|
22
|
+
Interfaz gráfica para ejecutar políticas entrenadas con LeRobot.
|
|
23
|
+
|
|
24
|
+
## Requisitos
|
|
25
|
+
|
|
26
|
+
- Python 3.12.
|
|
27
|
+
- Linux o un sistema compatible con Tkinter.
|
|
28
|
+
- LeRobot instalado en un entorno compatible.
|
|
29
|
+
- Robot y cámaras configurados para el equipo correspondiente.
|
|
30
|
+
|
|
31
|
+
Pillow permite mostrar imágenes adicionales. SpeechRecognition y PyAudio
|
|
32
|
+
son opcionales y se utilizan para el control por voz.
|
|
33
|
+
|
|
34
|
+
## Ejecutar la interfaz
|
|
35
|
+
|
|
36
|
+
Una vez instalado el paquete:
|
|
37
|
+
|
|
38
|
+
lerobotberto
|
|
39
|
+
|
|
40
|
+
También se puede ejecutar como módulo:
|
|
41
|
+
|
|
42
|
+
python -m lerobotberto
|
|
43
|
+
|
|
44
|
+
## Dependencias de LeRobot
|
|
45
|
+
|
|
46
|
+
Los archivos de `environments/` y `requirements/` contienen las referencias
|
|
47
|
+
de dependencias exportadas desde el entorno Linux de desarrollo.
|
|
48
|
+
|
|
49
|
+
No se deben instalar automáticamente en cualquier sistema operativo:
|
|
50
|
+
algunas dependencias son específicas de Linux, del hardware o de CUDA.
|
|
51
|
+
|
|
52
|
+
Antes de ejecutar tareas reales, hay que configurar los puertos, las cámaras,
|
|
53
|
+
las políticas y los parámetros del robot en la interfaz.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
lerobotberto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
lerobotberto/__main__.py,sha256=cuOke-HtgFcKIDHo1hRlTJsPc6y-CrtULHDBqayYREU,110
|
|
3
|
+
lerobotberto/gui.py,sha256=bsYgdgMjibgeB02GnsqH1hCjvjNvfTfMu5kV8ly2gGc,48604
|
|
4
|
+
lerobotberto-0.1.0.dist-info/licenses/LICENSE.txt,sha256=gupReBd-ERKxRqiOUQRyREfsNvVh3jQqUpijfSBZPIg,1748
|
|
5
|
+
lerobotberto-0.1.0.dist-info/METADATA,sha256=x_SBr4kYer86kholKFutkZ2sIlfzvAUzLjKKL4ClmeI,1686
|
|
6
|
+
lerobotberto-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
lerobotberto-0.1.0.dist-info/entry_points.txt,sha256=gddRylDSIaCGnlTEjvbRrGbvnRKVQpaK6ClsMenskIw,60
|
|
8
|
+
lerobotberto-0.1.0.dist-info/top_level.txt,sha256=N8u58qQWs1YL_lbhCDiGcQDPxG9h5fpifM3YFmJa-dM,13
|
|
9
|
+
lerobotberto-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
LEROBERTO PROPRIETARY LICENSE
|
|
2
|
+
Copyright (c) 2026 Alberto Trujillo Sanz - FUSGEMENT S.L.U.
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
1. COPYRIGHT
|
|
6
|
+
The software known as LeRobotBerto, including its source code,
|
|
7
|
+
documentation and associated materials, is protected by copyright.
|
|
8
|
+
All rights not expressly granted in this license are reserved by
|
|
9
|
+
the copyright holder.
|
|
10
|
+
|
|
11
|
+
2. FREE LICENSE TO USE
|
|
12
|
+
The copyright holder grants each recipient a non-exclusive,
|
|
13
|
+
worldwide, royalty-free license to download, install and use the
|
|
14
|
+
software for personal, educational, research or commercial purposes.
|
|
15
|
+
|
|
16
|
+
3. RESTRICTIONS
|
|
17
|
+
Unless the copyright holder gives prior written permission, recipients
|
|
18
|
+
may not:
|
|
19
|
+
- Sell, sublicense or commercially redistribute the software itself.
|
|
20
|
+
- Publish or distribute modified or derivative versions of the software.
|
|
21
|
+
- Redistribute copies of the software outside the distribution channels
|
|
22
|
+
authorized by the copyright holder.
|
|
23
|
+
|
|
24
|
+
4. OWNERSHIP
|
|
25
|
+
This license does not transfer ownership or copyright in the software.
|
|
26
|
+
All rights not expressly granted remain with the copyright holder.
|
|
27
|
+
|
|
28
|
+
5. FUTURE DISTRIBUTION
|
|
29
|
+
The copyright holder may stop distributing the software, change the
|
|
30
|
+
terms for future releases, or discontinue this license for future
|
|
31
|
+
downloads and releases.
|
|
32
|
+
|
|
33
|
+
Termination or withdrawal of distribution does not, by itself,
|
|
34
|
+
automatically revoke permissions previously granted to recipients.
|
|
35
|
+
Any termination of an existing recipient's license is subject to
|
|
36
|
+
the applicable terms and law.
|
|
37
|
+
|
|
38
|
+
6. NO WARRANTY
|
|
39
|
+
The software is provided "AS IS", without warranty of any kind,
|
|
40
|
+
to the fullest extent permitted by applicable law.
|
|
41
|
+
|
|
42
|
+
7. APPLICABLE LAW
|
|
43
|
+
The interpretation and enforceability of this license are subject
|
|
44
|
+
to applicable law.
|
|
45
|
+
|
|
46
|
+
8. You have to ACCEPT that BERTO is cool.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lerobotberto
|