trainer_training_view.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. # -*- coding: utf-8 -*-
  2. """
  3. Trainer SubView: Training (F6).
  4. Steuerung und Visualisierung des Trainings:
  5. Start/Pause/Resume/Stop Buttons, Metriken-Zeile und LossChart.
  6. """
  7. from __future__ import annotations
  8. from textual.app import ComposeResult
  9. from textual.containers import Horizontal, Vertical
  10. from textual.widgets import Static, Button
  11. from trixy_core.tui.views.base import MainView
  12. from trixy_core.tui.widgets.loss_chart import LossChart
  13. class TrainerTrainingView(MainView):
  14. """F6 im Trainer-Kontext: Training-Steuerung und Visualisierung."""
  15. TITLE = "Training"
  16. VIEW_ID = "trainer_training"
  17. def __init__(self, **kwargs) -> None:
  18. super().__init__(**kwargs)
  19. self._trainer_data: dict = {}
  20. self._is_training: bool = False
  21. self._is_paused: bool = False
  22. def set_trainer(self, trainer_data: dict) -> None:
  23. """Setzt die Trainer-Basisdaten."""
  24. self._trainer_data = trainer_data
  25. self._is_training = False
  26. self._is_paused = False
  27. def compose(self) -> ComposeResult:
  28. # Steuerungsbuttons
  29. with Horizontal(id="trainer-controls"):
  30. yield Button("Start", id="trainer-btn-start", variant="success")
  31. yield Button("Pause", id="trainer-btn-pause", variant="warning", disabled=True)
  32. yield Button("Resume", id="trainer-btn-resume", variant="primary", disabled=True)
  33. yield Button("Stop", id="trainer-btn-stop", variant="error", disabled=True)
  34. # Metriken-Leiste
  35. yield Static("", id="trainer-metrics")
  36. # Status
  37. yield Static("", id="trainer-training-status")
  38. # Loss-Chart
  39. with Vertical(classes="trainer-section"):
  40. yield Static("[bold]Loss-Verlauf[/bold]", classes="trainer-section-header")
  41. yield LossChart(id="trainer-chart")
  42. async def on_activate(self) -> None:
  43. """Prueft ob ein Training laeuft."""
  44. await self._poll_progress()
  45. async def refresh_data(self) -> None:
  46. """Pollt Progress waehrend Training (alle 2s durch _periodic_refresh)."""
  47. if self._is_training:
  48. await self._poll_progress()
  49. async def _poll_progress(self) -> None:
  50. """Holt den aktuellen Trainings-Fortschritt."""
  51. if not self.connection:
  52. return
  53. try:
  54. progress = await self.connection.request_training_progress()
  55. except Exception:
  56. return
  57. if progress is None:
  58. return
  59. state = progress.state if hasattr(progress, "state") else "idle"
  60. self._is_training = state in ("training", "paused", "starting")
  61. self._is_paused = state == "paused"
  62. self._update_buttons(state)
  63. self._update_metrics(progress)
  64. self._update_chart(progress)
  65. def _update_buttons(self, state: str) -> None:
  66. """Aktualisiert Button-Zustaende."""
  67. start_btn = self.query_one("#trainer-btn-start", Button)
  68. pause_btn = self.query_one("#trainer-btn-pause", Button)
  69. resume_btn = self.query_one("#trainer-btn-resume", Button)
  70. stop_btn = self.query_one("#trainer-btn-stop", Button)
  71. is_active = state in ("training", "paused", "starting")
  72. start_btn.disabled = is_active
  73. pause_btn.disabled = state != "training"
  74. resume_btn.disabled = state != "paused"
  75. stop_btn.disabled = not is_active
  76. def _update_metrics(self, progress: object) -> None:
  77. """Aktualisiert die Metriken-Leiste."""
  78. metrics = self.query_one("#trainer-metrics", Static)
  79. state = getattr(progress, "state", "idle")
  80. epoch = getattr(progress, "epoch", 0)
  81. total = getattr(progress, "total_epochs", 0)
  82. loss = getattr(progress, "loss", 0.0)
  83. val_loss = getattr(progress, "val_loss", 0.0)
  84. accuracy = getattr(progress, "accuracy", 0.0)
  85. lr = getattr(progress, "learning_rate", 0.0)
  86. eta = getattr(progress, "eta_seconds", 0.0)
  87. vram = getattr(progress, "vram_used_mb", 0.0)
  88. # Status-Farbe
  89. state_colors = {
  90. "idle": "dim",
  91. "starting": "yellow",
  92. "training": "green",
  93. "paused": "yellow",
  94. "stopping": "yellow",
  95. "completed": "cyan",
  96. "failed": "red",
  97. }
  98. color = state_colors.get(state, "dim")
  99. # ETA formatieren
  100. eta_str = ""
  101. if eta > 0:
  102. mins, secs = divmod(int(eta), 60)
  103. hours, mins = divmod(mins, 60)
  104. if hours > 0:
  105. eta_str = f"{hours}:{mins:02d}:{secs:02d}"
  106. else:
  107. eta_str = f"{mins:02d}:{secs:02d}"
  108. parts = [
  109. f"[{color}]{state.upper()}[/{color}]",
  110. f"Epoche: {epoch}/{total}" if total > 0 else "",
  111. f"Loss: {loss:.4f}" if loss > 0 else "",
  112. f"Val: {val_loss:.4f}" if val_loss > 0 else "",
  113. f"Acc: {accuracy:.1f}%" if accuracy > 0 else "",
  114. f"LR: {lr:.1e}" if lr > 0 else "",
  115. f"ETA: {eta_str}" if eta_str else "",
  116. f"VRAM: {vram:.1f} GB" if vram > 0 else "",
  117. ]
  118. metrics.update(" | ".join(p for p in parts if p))
  119. # Status-Nachricht
  120. status = self.query_one("#trainer-training-status", Static)
  121. message = getattr(progress, "message", "")
  122. error = getattr(progress, "error", "")
  123. if error:
  124. status.update(f"[red]{error}[/red]")
  125. elif message:
  126. status.update(f"[dim]{message}[/dim]")
  127. def _update_chart(self, progress: object) -> None:
  128. """Aktualisiert den Loss-Chart."""
  129. chart = self.query_one("#trainer-chart", LossChart)
  130. loss_history = getattr(progress, "loss_history", [])
  131. val_loss_history = getattr(progress, "val_loss_history", [])
  132. if loss_history:
  133. chart.update_data(loss_history, val_loss_history or None)
  134. async def on_button_pressed(self, event: Button.Pressed) -> None:
  135. """Verarbeitet Button-Klicks."""
  136. btn_id = event.button.id
  137. if not self.connection:
  138. return
  139. trainer_id = self._trainer_data.get("trainer_id", "")
  140. status = self.query_one("#trainer-training-status", Static)
  141. if btn_id == "trainer-btn-start":
  142. if not trainer_id:
  143. status.update("[red]Kein Trainer ausgewaehlt[/red]")
  144. return
  145. status.update("[yellow]Training wird gestartet...[/yellow]")
  146. try:
  147. result = await self.connection.start_training(trainer_id)
  148. if result and result.success:
  149. self._is_training = True
  150. self._update_buttons("training")
  151. status.update("[green]Training gestartet[/green]")
  152. else:
  153. error = result.error if result else "Unbekannter Fehler"
  154. status.update(f"[red]{error}[/red]")
  155. except Exception as e:
  156. status.update(f"[red]Fehler: {e}[/red]")
  157. elif btn_id == "trainer-btn-pause":
  158. try:
  159. result = await self.connection.pause_training()
  160. if result and result.success:
  161. self._is_paused = True
  162. self._update_buttons("paused")
  163. status.update("[yellow]Training pausiert[/yellow]")
  164. except Exception as e:
  165. status.update(f"[red]Fehler: {e}[/red]")
  166. elif btn_id == "trainer-btn-resume":
  167. try:
  168. result = await self.connection.resume_training()
  169. if result and result.success:
  170. self._is_paused = False
  171. self._update_buttons("training")
  172. status.update("[green]Training fortgesetzt[/green]")
  173. except Exception as e:
  174. status.update(f"[red]Fehler: {e}[/red]")
  175. elif btn_id == "trainer-btn-stop":
  176. status.update("[yellow]Training wird gestoppt...[/yellow]")
  177. try:
  178. result = await self.connection.stop_training()
  179. if result and result.success:
  180. self._is_training = False
  181. self._is_paused = False
  182. self._update_buttons("idle")
  183. status.update("[cyan]Training gestoppt[/cyan]")
  184. except Exception as e:
  185. status.update(f"[red]Fehler: {e}[/red]")