Javier Montalvo commited on
Commit
498b1c6
·
1 Parent(s): 9b858f8

Remove app.py (old llama.cpp entrypoint, replaced by server.py)

Browse files
Files changed (1) hide show
  1. app.py +0 -1324
app.py DELETED
@@ -1,1324 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import html
5
- import os
6
- import shutil
7
- from pathlib import Path
8
- from typing import Any
9
-
10
- import gradio as gr
11
-
12
- from tiny_trigger.actions import dispatch_events
13
- from tiny_trigger.automation import evaluate_video_detections, load_automation_text
14
- from tiny_trigger.detector import parse_class_prompt
15
- from tiny_trigger.llm import compile_automation_with_llamacpp
16
- from tiny_trigger.store import (
17
- EVENTS_PATH,
18
- RuntimeState,
19
- append_events,
20
- load_local_config,
21
- load_runtime_state,
22
- load_saved_automations,
23
- save_automations,
24
- save_runtime_state,
25
- )
26
- from tiny_trigger.video import process_video, render_automation_video
27
-
28
-
29
- LOCAL_CONFIG = load_local_config()
30
- DEFAULT_CLASSES = os.getenv(
31
- "TINY_TRIGGER_CLASSES",
32
- LOCAL_CONFIG.default_classes or "person, wheel, clothes, chair, monitor, steering wheel, bag, sticker",
33
- )
34
- DEFAULT_MODEL = os.getenv("TINY_TRIGGER_YOLOE_MODEL", LOCAL_CONFIG.default_detector_model or "yoloe-26s-seg.pt")
35
- DEFAULT_DEVICE = os.getenv("TINY_TRIGGER_DEVICE", LOCAL_CONFIG.default_device or "auto")
36
- DEFAULT_IMAGE_SIZE = int(os.getenv("TINY_TRIGGER_IMAGE_SIZE", LOCAL_CONFIG.default_image_size or 960))
37
- DEFAULT_MAX_DETECTIONS = int(os.getenv("TINY_TRIGGER_MAX_DETECTIONS", LOCAL_CONFIG.default_max_detections or 100))
38
- DEFAULT_WEBHOOK_URL = os.getenv("TINY_TRIGGER_WEBHOOK_URL", LOCAL_CONFIG.webhook_url or "")
39
- DEFAULT_LLAMACPP_BASE_URL = os.getenv(
40
- "TINY_TRIGGER_LLAMACPP_BASE_URL",
41
- LOCAL_CONFIG.llamacpp_base_url or "http://127.0.0.1:8080/v1",
42
- )
43
- DEFAULT_LLAMACPP_MODEL = os.getenv(
44
- "TINY_TRIGGER_LLAMACPP_MODEL",
45
- LOCAL_CONFIG.llamacpp_model or "ggml-org/Qwen3-1.7B-GGUF:Q4_K_M",
46
- )
47
-
48
-
49
- def _ensure_ffmpeg_available() -> None:
50
- if shutil.which("ffmpeg") and shutil.which("ffprobe"):
51
- return
52
-
53
- try:
54
- import static_ffmpeg
55
-
56
- static_ffmpeg.add_paths()
57
- except ImportError:
58
- pass
59
- if shutil.which("ffmpeg") and shutil.which("ffprobe"):
60
- return
61
-
62
- try:
63
- import imageio_ffmpeg
64
- except ImportError:
65
- return
66
-
67
- ffmpeg_exe = Path(imageio_ffmpeg.get_ffmpeg_exe())
68
- local_bin = Path(".local") / "bin"
69
- local_bin.mkdir(parents=True, exist_ok=True)
70
- ffmpeg_link = local_bin / ("ffmpeg.exe" if os.name == "nt" else "ffmpeg")
71
- if not ffmpeg_link.exists():
72
- try:
73
- ffmpeg_link.symlink_to(ffmpeg_exe)
74
- except OSError:
75
- shutil.copy2(ffmpeg_exe, ffmpeg_link)
76
- ffmpeg_link.chmod(0o755)
77
- os.environ["PATH"] = f"{local_bin.resolve()}{os.pathsep}{os.environ.get('PATH', '')}"
78
-
79
-
80
- _ensure_ffmpeg_available()
81
-
82
- APP_CSS = """
83
- :root {
84
- --tt-bg: #f6f7f8;
85
- --tt-surface: #ffffff;
86
- --tt-surface-soft: #eef2f5;
87
- --tt-text: #17202a;
88
- --tt-muted: #687484;
89
- --tt-border: #dce1e7;
90
- --tt-border-strong: #cbd3dd;
91
- --tt-red: #d93a2f;
92
- --tt-red-dark: #b92f27;
93
- --tt-teal: #109f8f;
94
- --tt-blue: #3569d4;
95
- --tt-lime: #d5ef4a;
96
- --tt-shadow: 0 20px 45px rgba(31, 41, 55, 0.08);
97
- --tt-radius: 8px;
98
- }
99
-
100
- body,
101
- .gradio-container {
102
- background:
103
- linear-gradient(180deg, rgba(16, 159, 143, 0.08), rgba(246, 247, 248, 0) 310px),
104
- var(--tt-bg) !important;
105
- color: var(--tt-text) !important;
106
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
107
- letter-spacing: 0 !important;
108
- }
109
-
110
- .gradio-container {
111
- max-width: 1440px !important;
112
- margin: 0 auto !important;
113
- padding: 22px 24px 32px !important;
114
- }
115
-
116
- footer {
117
- display: none !important;
118
- }
119
-
120
- .tt-hero {
121
- display: flex;
122
- align-items: flex-start;
123
- justify-content: space-between;
124
- gap: 18px;
125
- margin-bottom: 18px;
126
- }
127
-
128
- .tt-kicker {
129
- margin: 0 0 5px;
130
- color: var(--tt-muted);
131
- font-size: 12px;
132
- font-weight: 800;
133
- letter-spacing: 0.08em;
134
- text-transform: uppercase;
135
- }
136
-
137
- .tt-title {
138
- margin: 0;
139
- color: var(--tt-text);
140
- font-size: 31px;
141
- line-height: 1.05;
142
- font-weight: 820;
143
- }
144
-
145
- .tt-subtitle {
146
- margin: 8px 0 0;
147
- color: var(--tt-muted);
148
- font-size: 14px;
149
- }
150
-
151
- .tt-chips {
152
- display: flex;
153
- flex-wrap: wrap;
154
- justify-content: flex-end;
155
- gap: 9px;
156
- }
157
-
158
- .tt-chip {
159
- display: inline-flex;
160
- align-items: center;
161
- min-height: 31px;
162
- padding: 0 12px;
163
- border: 1px solid var(--tt-border);
164
- border-radius: 999px;
165
- background: rgba(255, 255, 255, 0.76);
166
- color: var(--tt-text);
167
- font-size: 12px;
168
- font-weight: 800;
169
- white-space: nowrap;
170
- box-shadow: 0 8px 20px rgba(31, 41, 55, 0.04);
171
- }
172
-
173
- .tt-chip.live {
174
- border-color: transparent;
175
- background: var(--tt-teal);
176
- color: #ffffff;
177
- }
178
-
179
- .tt-grid {
180
- gap: 16px !important;
181
- align-items: stretch !important;
182
- }
183
-
184
- .tt-panel {
185
- border: 1px solid var(--tt-border) !important;
186
- border-radius: var(--tt-radius) !important;
187
- background: var(--tt-surface) !important;
188
- color: var(--tt-text) !important;
189
- box-shadow: var(--tt-shadow) !important;
190
- overflow: hidden !important;
191
- }
192
-
193
- .tt-panel > div,
194
- .tt-panel .form,
195
- .tt-panel .block,
196
- .tt-panel .wrap,
197
- .tt-panel .contain,
198
- .tt-video-panel > div,
199
- .tt-data-panel > div {
200
- background: var(--tt-surface) !important;
201
- color: var(--tt-text) !important;
202
- }
203
-
204
- .tt-panel > .form,
205
- .tt-panel .form {
206
- border: 0 !important;
207
- background: transparent !important;
208
- }
209
-
210
- .tt-panel-title h2,
211
- .tt-panel-title h3,
212
- .tt-section h2,
213
- .tt-section h3 {
214
- margin: 0 !important;
215
- color: var(--tt-muted) !important;
216
- font-size: 12px !important;
217
- font-weight: 850 !important;
218
- letter-spacing: 0.08em !important;
219
- line-height: 1.2 !important;
220
- text-transform: uppercase !important;
221
- }
222
-
223
- .tt-panel-title {
224
- margin-bottom: 10px !important;
225
- }
226
-
227
- .tt-panel .block,
228
- .tt-panel .block.padded {
229
- border-color: var(--tt-border) !important;
230
- border-radius: 7px !important;
231
- }
232
-
233
- .tt-panel label,
234
- .tt-panel .label-wrap,
235
- .tt-panel .wrap label,
236
- .tt-panel .block-label {
237
- color: var(--tt-muted) !important;
238
- font-size: 12px !important;
239
- font-weight: 780 !important;
240
- }
241
-
242
- .tt-panel textarea,
243
- .tt-panel input,
244
- .tt-panel select {
245
- color: var(--tt-text) !important;
246
- background: #ffffff !important;
247
- }
248
-
249
- .tt-panel textarea,
250
- .tt-panel input {
251
- border-color: var(--tt-border-strong) !important;
252
- }
253
-
254
- .tt-panel .secondary,
255
- .tt-panel button.secondary {
256
- border: 1px solid var(--tt-border) !important;
257
- background: var(--tt-surface-soft) !important;
258
- color: var(--tt-text) !important;
259
- }
260
-
261
- .tt-primary button,
262
- .tt-panel button.primary,
263
- button.primary {
264
- border: 0 !important;
265
- background: var(--tt-red) !important;
266
- color: #ffffff !important;
267
- font-weight: 820 !important;
268
- }
269
-
270
- .tt-panel button.primary:hover,
271
- button.primary:hover {
272
- background: var(--tt-red-dark) !important;
273
- }
274
-
275
- .tt-run-row {
276
- gap: 10px !important;
277
- align-items: stretch !important;
278
- flex-wrap: nowrap !important;
279
- }
280
-
281
- .tt-run-row button,
282
- .tt-panel button {
283
- min-height: 40px !important;
284
- border-radius: 7px !important;
285
- padding: 0 14px !important;
286
- font-size: 13px !important;
287
- font-weight: 820 !important;
288
- line-height: 1.1 !important;
289
- white-space: nowrap !important;
290
- }
291
-
292
- .tt-run-row button {
293
- width: 100% !important;
294
- }
295
-
296
- .tt-run-row .form,
297
- .tt-run-row > div {
298
- flex: 1 1 0 !important;
299
- min-width: 0 !important;
300
- }
301
-
302
- .gradio-container button[role="tab"] {
303
- min-height: 36px !important;
304
- border: 1px solid var(--tt-border) !important;
305
- border-radius: 7px !important;
306
- background: rgba(255, 255, 255, 0.78) !important;
307
- color: var(--tt-text) !important;
308
- font-size: 13px !important;
309
- font-weight: 820 !important;
310
- box-shadow: none !important;
311
- }
312
-
313
- .gradio-container button[role="tab"][aria-selected="true"] {
314
- border-color: transparent !important;
315
- background: var(--tt-blue) !important;
316
- color: #ffffff !important;
317
- }
318
-
319
- .tt-video-panel {
320
- border: 1px solid var(--tt-border) !important;
321
- border-radius: var(--tt-radius) !important;
322
- background: var(--tt-surface) !important;
323
- box-shadow: var(--tt-shadow) !important;
324
- overflow: hidden !important;
325
- }
326
-
327
- .tt-video-panel .tabs,
328
- .tt-data-panel .tabs,
329
- .tt-automation-panel .tabs {
330
- border: 0 !important;
331
- }
332
-
333
- .tt-video-panel .tab-nav,
334
- .tt-data-panel .tab-nav,
335
- .tt-automation-panel .tab-nav {
336
- gap: 7px !important;
337
- padding: 0 !important;
338
- border: 0 !important;
339
- }
340
-
341
- .tt-video-panel button[role="tab"],
342
- .tt-data-panel button[role="tab"],
343
- .tt-automation-panel button[role="tab"] {
344
- min-height: 34px !important;
345
- border: 1px solid var(--tt-border) !important;
346
- border-radius: 7px !important;
347
- background: var(--tt-surface-soft) !important;
348
- color: var(--tt-text) !important;
349
- font-size: 12px !important;
350
- font-weight: 820 !important;
351
- }
352
-
353
- .tt-video-panel button[role="tab"][aria-selected="true"],
354
- .tt-data-panel button[role="tab"][aria-selected="true"],
355
- .tt-automation-panel button[role="tab"][aria-selected="true"] {
356
- background: var(--tt-blue) !important;
357
- color: #ffffff !important;
358
- }
359
-
360
- .tt-result-video {
361
- min-height: 430px !important;
362
- }
363
-
364
- .tt-result-video video {
365
- border-radius: 8px !important;
366
- background:
367
- linear-gradient(90deg, rgba(23, 32, 42, 0.07) 1px, transparent 1px),
368
- linear-gradient(0deg, rgba(23, 32, 42, 0.07) 1px, transparent 1px),
369
- #edf1f5 !important;
370
- background-size: 48px 48px, 48px 48px, 100% 100% !important;
371
- }
372
-
373
- .tt-status-grid {
374
- gap: 10px !important;
375
- }
376
-
377
- .tt-status-card {
378
- min-height: 82px;
379
- padding: 13px 14px;
380
- border: 1px solid var(--tt-border);
381
- border-radius: var(--tt-radius);
382
- background: #ffffff;
383
- box-shadow: inset 0 0 0 1px rgba(255,255,255,0.4);
384
- }
385
-
386
- .tt-status-card strong {
387
- display: block;
388
- margin-bottom: 4px;
389
- color: var(--tt-text);
390
- font-size: 14px;
391
- line-height: 1.2;
392
- }
393
-
394
- .tt-status-card span {
395
- display: block;
396
- color: var(--tt-muted);
397
- font-size: 12px;
398
- line-height: 1.35;
399
- }
400
-
401
- .tt-status-card.success {
402
- border-color: rgba(16, 159, 143, 0.35);
403
- background: rgba(16, 159, 143, 0.08);
404
- }
405
-
406
- .tt-status-card.action {
407
- border-color: rgba(217, 58, 47, 0.32);
408
- background: rgba(217, 58, 47, 0.09);
409
- }
410
-
411
- .tt-status-card.warning {
412
- border-color: rgba(213, 160, 20, 0.38);
413
- background: rgba(213, 160, 20, 0.11);
414
- }
415
-
416
- .tt-status-card.idle {
417
- background: #f8fafc;
418
- }
419
-
420
- .tt-label-audit {
421
- display: grid;
422
- gap: 10px;
423
- padding: 13px 14px;
424
- border: 1px solid rgba(16, 159, 143, 0.28);
425
- border-radius: var(--tt-radius);
426
- background: rgba(16, 159, 143, 0.07);
427
- }
428
-
429
- .tt-label-audit strong {
430
- display: block;
431
- color: var(--tt-text);
432
- font-size: 14px;
433
- line-height: 1.2;
434
- }
435
-
436
- .tt-label-audit span {
437
- color: var(--tt-muted);
438
- font-size: 12px;
439
- }
440
-
441
- .tt-chip-row {
442
- display: flex;
443
- flex-wrap: wrap;
444
- gap: 8px;
445
- }
446
-
447
- .tt-flow-copy {
448
- margin: 0 0 12px;
449
- color: var(--tt-muted);
450
- font-size: 13px;
451
- line-height: 1.45;
452
- }
453
-
454
- .tt-code textarea,
455
- .tt-code code,
456
- .tt-code pre {
457
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace !important;
458
- font-size: 12px !important;
459
- line-height: 1.45 !important;
460
- }
461
-
462
- .tt-data-panel {
463
- border: 1px solid var(--tt-border) !important;
464
- border-radius: var(--tt-radius) !important;
465
- background: var(--tt-surface) !important;
466
- box-shadow: var(--tt-shadow) !important;
467
- overflow: hidden !important;
468
- }
469
-
470
- .tt-data-panel table {
471
- font-size: 13px !important;
472
- }
473
-
474
- .tt-data-panel th {
475
- color: var(--tt-muted) !important;
476
- font-size: 11px !important;
477
- font-weight: 820 !important;
478
- text-transform: uppercase !important;
479
- }
480
-
481
- .tt-compact textarea {
482
- min-height: 84px !important;
483
- }
484
-
485
- .tt-demo-state {
486
- margin-top: 10px;
487
- }
488
-
489
- @media (max-width: 980px) {
490
- .gradio-container {
491
- padding: 16px !important;
492
- }
493
-
494
- .tt-hero {
495
- flex-direction: column;
496
- }
497
-
498
- .tt-chips {
499
- justify-content: flex-start;
500
- }
501
-
502
- .tt-result-video {
503
- min-height: 300px !important;
504
- }
505
- }
506
- """
507
-
508
- FALLBACK_RULES = """rules:
509
- - name: person-near-steering-wheel
510
- when:
511
- all:
512
- - present: {label: person, min_count: 1}
513
- - near: {a: person, b: steering wheel, max_distance: 0.45}
514
- gate:
515
- enabled: true
516
- cooldown: {key: turn-on-pc, minutes: 5}
517
- then:
518
- - type: webhook
519
- name: turn on pc
520
- """
521
- SAVED_AUTOMATIONS = load_saved_automations()
522
- DEFAULT_RULES = (
523
- SAVED_AUTOMATIONS.model_dump_json(by_alias=True, indent=2)
524
- if SAVED_AUTOMATIONS
525
- else FALLBACK_RULES
526
- )
527
-
528
-
529
- def _detection_rows(detections: list[Any]) -> list[list[Any]]:
530
- return [
531
- [
532
- det.frame_index,
533
- round(det.timestamp_sec, 2),
534
- det.label,
535
- round(det.confidence, 3),
536
- [round(value, 1) for value in det.bbox_xyxy],
537
- [round(value, 3) for value in det.bbox_xyxy_norm],
538
- ]
539
- for det in detections
540
- ]
541
-
542
-
543
- def _event_rows(events: list[Any]) -> list[list[Any]]:
544
- return [
545
- [
546
- event.rule,
547
- event.action,
548
- event.type,
549
- event.status,
550
- event.frame_index,
551
- round(event.timestamp_sec, 2),
552
- event.response_status or "",
553
- event.error or "",
554
- ]
555
- for event in events
556
- ]
557
-
558
-
559
- def _result_json(result: Any) -> dict[str, Any]:
560
- return json.loads(result.model_dump_json())
561
-
562
-
563
- def _status_card(tone: str, title: str, body: str) -> str:
564
- return f"""
565
- <div class="tt-status-card {tone}">
566
- <strong>{title}</strong>
567
- <span>{body}</span>
568
- </div>
569
- """
570
-
571
-
572
- def _rules_json(document: Any) -> dict[str, Any]:
573
- return json.loads(document.model_dump_json(by_alias=True))
574
-
575
-
576
- def _automation_labels_from_document(document: Any) -> list[str]:
577
- labels: list[str] = []
578
- for rule in document.rules:
579
- for condition in [*rule.when.all_conditions, *rule.when.any_conditions]:
580
- if condition.present:
581
- labels.append(condition.present.label)
582
- if condition.count:
583
- labels.append(condition.count.label)
584
- if condition.near:
585
- labels.extend([condition.near.a, condition.near.b])
586
- return parse_class_prompt(labels)
587
-
588
-
589
- def _automation_labels_from_text(rules_text: str) -> list[str]:
590
- return _automation_labels_from_document(load_automation_text(rules_text))
591
-
592
-
593
- def _automation_labels_from_rule(rule: Any) -> list[str]:
594
- labels: list[str] = []
595
- for condition in [*rule.when.all_conditions, *rule.when.any_conditions]:
596
- if condition.present:
597
- labels.append(condition.present.label)
598
- if condition.count:
599
- labels.append(condition.count.label)
600
- if condition.near:
601
- labels.extend([condition.near.a, condition.near.b])
602
- return parse_class_prompt(labels)
603
-
604
-
605
- def _labels_summary(labels: list[str]) -> str:
606
- if not labels:
607
- return "No detector labels found in the active automations."
608
- return ", ".join(labels)
609
-
610
-
611
- def _labels_card(labels: list[str]) -> str:
612
- chips = "".join(f"<span class=\"tt-chip\">{html.escape(label)}</span>" for label in labels)
613
- chips = chips or "<span class=\"tt-chip\">No labels</span>"
614
- return f"""
615
- <div class="tt-label-audit">
616
- <div>
617
- <strong>Detector audit</strong>
618
- <span>{len(labels)} label{'s' if len(labels) != 1 else ''} derived from active automations</span>
619
- </div>
620
- <div class="tt-chip-row">{chips}</div>
621
- </div>
622
- """
623
-
624
-
625
- def _condition_summary(rule: Any) -> str:
626
- parts: list[str] = []
627
- for condition in [*rule.when.all_conditions, *rule.when.any_conditions]:
628
- if condition.present:
629
- parts.append(f"present {condition.present.label}")
630
- if condition.count:
631
- parts.append(f"{condition.count.label} >= {condition.count.minimum}")
632
- if condition.near:
633
- parts.append(f"{condition.near.a} near {condition.near.b}")
634
- if condition.cooldown:
635
- duration = condition.cooldown.minutes or condition.cooldown.seconds or 0
636
- unit = "min" if condition.cooldown.minutes is not None else "sec"
637
- parts.append(f"cooldown {duration:g}{unit}")
638
- return " + ".join(parts)
639
-
640
-
641
- def _action_summary(rule: Any) -> str:
642
- return ", ".join(f"{action.type}: {action.name}" for action in rule.then)
643
-
644
-
645
- def _rule_rows(document: Any) -> list[list[Any]]:
646
- rows: list[list[Any]] = []
647
- for rule in document.rules:
648
- rows.append(
649
- [
650
- "on" if rule.gate.enabled else "off",
651
- rule.name,
652
- _labels_summary(_automation_labels_from_rule(rule)),
653
- _condition_summary(rule),
654
- _action_summary(rule),
655
- ]
656
- )
657
- return rows
658
-
659
-
660
- def _rule_names(document: Any) -> list[str]:
661
- return [rule.name for rule in document.rules]
662
-
663
-
664
- def _rule_enabled(document: Any, rule_name: str | None) -> bool:
665
- for rule in document.rules:
666
- if rule.name == rule_name:
667
- return rule.gate.enabled
668
- return False
669
-
670
-
671
- def _rules_text_from_data(data: dict[str, Any]) -> str:
672
- document = load_automation_text(json.dumps(data))
673
- return document.model_dump_json(by_alias=True, indent=2)
674
-
675
-
676
- def _merge_generated_rules(existing_rules_text: str, generated_document: Any) -> Any:
677
- try:
678
- existing_data = _rules_json(load_automation_text(existing_rules_text))
679
- existing_rules = existing_data["rules"]
680
- except Exception:
681
- existing_rules = []
682
-
683
- generated_rules = _rules_json(generated_document)["rules"]
684
- merged_by_name: dict[str, dict[str, Any]] = {rule["name"]: rule for rule in existing_rules}
685
- ordered_names = [rule["name"] for rule in existing_rules]
686
- for rule in generated_rules:
687
- if rule["name"] not in merged_by_name:
688
- ordered_names.append(rule["name"])
689
- merged_by_name[rule["name"]] = rule
690
-
691
- merged_rules = [merged_by_name[name] for name in ordered_names]
692
- return load_automation_text(json.dumps({"rules": merged_rules}))
693
-
694
-
695
- def refresh_automation_manager(rules_text: str) -> tuple[list[list[Any]], Any, bool, dict[str, Any], str, str, str]:
696
- try:
697
- document = load_automation_text(rules_text)
698
- except Exception as exc:
699
- raise gr.Error(f"Rules did not validate: {exc}") from exc
700
- names = _rule_names(document)
701
- selected = names[0] if names else None
702
- labels = _automation_labels_from_document(document)
703
- return (
704
- _rule_rows(document),
705
- gr.update(choices=names, value=selected),
706
- _rule_enabled(document, selected),
707
- {"status": "valid", "rules": _rules_json(document)["rules"], "labels": labels},
708
- _status_card("success", "Manager refreshed", f"{len(names)} automation(s) loaded."),
709
- _labels_card(labels),
710
- _labels_card(labels),
711
- )
712
-
713
-
714
- def select_managed_rule(rules_text: str, rule_name: str | None) -> tuple[bool, str]:
715
- document = load_automation_text(rules_text)
716
- return (
717
- _rule_enabled(document, rule_name),
718
- _status_card("idle", "Automation selected", rule_name or "No automation selected."),
719
- )
720
-
721
-
722
- def set_rule_enabled(rules_text: str, rule_name: str | None, enabled: bool) -> tuple[
723
- str,
724
- list[list[Any]],
725
- Any,
726
- bool,
727
- dict[str, Any],
728
- str,
729
- str,
730
- str,
731
- ]:
732
- if not rule_name:
733
- raise gr.Error("Choose an automation first.")
734
-
735
- document = load_automation_text(rules_text)
736
- data = _rules_json(document)
737
- changed = False
738
- for rule in data["rules"]:
739
- if rule["name"] == rule_name:
740
- rule.setdefault("gate", {})
741
- rule["gate"]["enabled"] = enabled
742
- changed = True
743
- break
744
- if not changed:
745
- raise gr.Error(f"Automation not found: {rule_name}")
746
-
747
- updated_text = _rules_text_from_data(data)
748
- updated_document = load_automation_text(updated_text)
749
- labels = _automation_labels_from_document(updated_document)
750
- names = _rule_names(updated_document)
751
- return (
752
- updated_text,
753
- _rule_rows(updated_document),
754
- gr.update(choices=names, value=rule_name),
755
- enabled,
756
- {"status": "updated", "rules": _rules_json(updated_document)["rules"], "labels": labels},
757
- _status_card("success", "Automation updated", f"{rule_name} is {'on' if enabled else 'off'}."),
758
- _labels_card(labels),
759
- _labels_card(labels),
760
- )
761
-
762
-
763
- def _video_file_path(value: Any) -> str | None:
764
- if value is None:
765
- return None
766
- if isinstance(value, str):
767
- return value
768
- if isinstance(value, Path):
769
- return str(value)
770
- if isinstance(value, dict):
771
- return value.get("path") or value.get("name")
772
- path = getattr(value, "path", None) or getattr(value, "name", None)
773
- return str(path) if path else None
774
-
775
-
776
- def reset_demo_state() -> tuple[str, dict[str, Any]]:
777
- save_runtime_state(RuntimeState())
778
- if EVENTS_PATH.exists():
779
- EVENTS_PATH.unlink()
780
- return (
781
- _status_card("success", "Demo state reset", "Cooldown memory and action history are clear."),
782
- {"status": "reset", "last_fired": {}, "events": "cleared"},
783
- )
784
-
785
-
786
- def run_detector(
787
- video_path: str | None,
788
- class_prompt: str,
789
- confidence: float,
790
- frame_stride: int,
791
- max_frames: int,
792
- model_name: str,
793
- image_size: int,
794
- device: str,
795
- max_detections: int,
796
- ) -> tuple[str | None, list[list[Any]], dict[str, Any], str]:
797
- if not video_path:
798
- raise gr.Error("Upload a video first.")
799
- if not parse_class_prompt(class_prompt):
800
- raise gr.Error("Enter at least one class prompt.")
801
-
802
- result = process_video(
803
- video_path=video_path,
804
- class_prompt=class_prompt,
805
- confidence=confidence,
806
- frame_stride=frame_stride,
807
- max_frames=max_frames,
808
- model_name=model_name or DEFAULT_MODEL,
809
- image_size=image_size,
810
- device=None if device == "auto" else device,
811
- max_detections=max_detections,
812
- )
813
- return (
814
- result.output_video_path,
815
- _detection_rows(result.detections),
816
- _result_json(result),
817
- _status_card(
818
- "success",
819
- f"{len(result.detections)} detections",
820
- f"Processed {result.processed_frames} frames at {result.output_fps:.1f} FPS output.",
821
- ),
822
- )
823
-
824
-
825
- def compile_rules(
826
- instruction: str,
827
- current_rules_text: str,
828
- label_hints: str,
829
- base_url: str,
830
- model: str,
831
- ) -> tuple[str, dict[str, Any], str, str, list[list[Any]], Any, bool, str]:
832
- if not instruction.strip():
833
- raise gr.Error("Describe the automation first.")
834
-
835
- try:
836
- compiled = compile_automation_with_llamacpp(
837
- instruction=instruction,
838
- class_names=parse_class_prompt(label_hints),
839
- base_url=base_url or DEFAULT_LLAMACPP_BASE_URL,
840
- model=model or DEFAULT_LLAMACPP_MODEL,
841
- )
842
- except Exception as exc:
843
- raise gr.Error(f"Could not compile automation: {exc}") from exc
844
- merged_document = _merge_generated_rules(current_rules_text, compiled.document)
845
- rules_json = merged_document.model_dump_json(by_alias=True, indent=2)
846
- labels = _automation_labels_from_document(merged_document)
847
- names = _rule_names(merged_document)
848
- generated_names = _rule_names(compiled.document)
849
- selected = generated_names[0] if generated_names else (names[0] if names else None)
850
- labels_card = _labels_card(labels)
851
- return (
852
- rules_json,
853
- {
854
- "raw_text": compiled.raw_text,
855
- "generated": _rules_json(compiled.document)["rules"],
856
- "active_rules": json.loads(rules_json)["rules"],
857
- "labels": labels,
858
- },
859
- _status_card(
860
- "success",
861
- "Generated and added",
862
- f"{len(compiled.document.rules)} new rule(s). {len(merged_document.rules)} active automation(s).",
863
- ),
864
- labels_card,
865
- _rule_rows(merged_document),
866
- gr.update(choices=names, value=selected),
867
- _rule_enabled(merged_document, selected),
868
- labels_card,
869
- )
870
-
871
-
872
- def validate_rules(rules_text: str) -> tuple[dict[str, Any], str, str, list[list[Any]], Any, bool, str]:
873
- try:
874
- document = load_automation_text(rules_text)
875
- except Exception as exc:
876
- raise gr.Error(f"Rules did not validate: {exc}") from exc
877
- labels = _automation_labels_from_document(document)
878
- names = _rule_names(document)
879
- selected = names[0] if names else None
880
- labels_card = _labels_card(labels)
881
- return (
882
- {"status": "valid", "rules": _rules_json(document)["rules"], "labels": labels},
883
- _status_card("success", "Validation passed", f"{len(document.rules)} rule(s) checked."),
884
- labels_card,
885
- _rule_rows(document),
886
- gr.update(choices=names, value=selected),
887
- _rule_enabled(document, selected),
888
- labels_card,
889
- )
890
-
891
-
892
- def save_rules(rules_text: str) -> tuple[str, dict[str, Any], str, str, list[list[Any]], Any, bool, str]:
893
- document = load_automation_text(rules_text)
894
- save_automations(document)
895
- rules_json = document.model_dump_json(by_alias=True, indent=2)
896
- labels = _automation_labels_from_document(document)
897
- names = _rule_names(document)
898
- selected = names[0] if names else None
899
- labels_card = _labels_card(labels)
900
- return (
901
- rules_json,
902
- {"status": "saved", "rules": json.loads(rules_json), "labels": labels},
903
- _status_card(
904
- "success",
905
- "Saved locally",
906
- f"{len(document.rules)} rule(s) stored. Monitor will watch: {_labels_summary(labels)}.",
907
- ),
908
- labels_card,
909
- _rule_rows(document),
910
- gr.update(choices=names, value=selected),
911
- _rule_enabled(document, selected),
912
- labels_card,
913
- )
914
-
915
-
916
- def run_monitor_clip(
917
- clip_file: Any,
918
- rules_text: str,
919
- confidence: float,
920
- frame_stride: int,
921
- max_frames: int,
922
- model_name: str,
923
- image_size: int,
924
- device: str,
925
- max_detections: int,
926
- enable_webhooks: bool,
927
- webhook_url: str,
928
- ) -> tuple[str | None, list[list[Any]], list[list[Any]], dict[str, Any], str, str]:
929
- video_path = _video_file_path(clip_file)
930
- if not video_path:
931
- raise gr.Error("Upload a clip first.")
932
-
933
- rules = load_automation_text(rules_text)
934
- labels = _automation_labels_from_document(rules)
935
- if not labels:
936
- raise gr.Error("The active automations do not mention any detector labels.")
937
-
938
- state = load_runtime_state()
939
- result = process_video(
940
- video_path=video_path,
941
- class_prompt=labels,
942
- confidence=confidence,
943
- frame_stride=frame_stride,
944
- max_frames=max_frames,
945
- model_name=model_name or DEFAULT_MODEL,
946
- image_size=image_size,
947
- device=None if device == "auto" else device,
948
- max_detections=max_detections,
949
- )
950
- events, last_fired = evaluate_video_detections(rules.rules, result.detections, last_fired=state.last_fired)
951
- dispatched = dispatch_events(events, enable_webhooks=enable_webhooks, webhook_url=webhook_url or None)
952
- state.last_fired = last_fired
953
- save_runtime_state(state)
954
- append_events(dispatched)
955
- automation_video_path = render_automation_video(
956
- source_video_path=video_path,
957
- detections=result.detections,
958
- events=dispatched,
959
- frame_stride=frame_stride,
960
- max_frames=max_frames,
961
- )
962
-
963
- action_count = len(dispatched)
964
- status_tone = "action" if action_count else "warning"
965
- status_title = f"{action_count} event{'s' if action_count != 1 else ''}"
966
- status_body = (
967
- "Replay rendered with fired automation overlays."
968
- if action_count
969
- else f"No automations fired. Watched: {_labels_summary(labels)}."
970
- )
971
- return (
972
- automation_video_path,
973
- _event_rows(dispatched),
974
- _detection_rows(result.detections),
975
- {
976
- "watched_labels": labels,
977
- "video": _result_json(result),
978
- "actions": [event.model_dump(mode="json") for event in dispatched],
979
- },
980
- _status_card(status_tone, status_title, status_body),
981
- _labels_card(labels),
982
- )
983
-
984
-
985
- def run_detector_and_automations(
986
- video_path: str | None,
987
- class_prompt: str,
988
- confidence: float,
989
- frame_stride: int,
990
- max_frames: int,
991
- model_name: str,
992
- image_size: int,
993
- device: str,
994
- max_detections: int,
995
- rules_text: str,
996
- enable_webhooks: bool,
997
- webhook_url: str,
998
- ) -> tuple[str | None, list[list[Any]], list[list[Any]], dict[str, Any], str]:
999
- if not video_path:
1000
- raise gr.Error("Upload a video first.")
1001
-
1002
- rules = load_automation_text(rules_text)
1003
- state = load_runtime_state()
1004
- result = process_video(
1005
- video_path=video_path,
1006
- class_prompt=class_prompt,
1007
- confidence=confidence,
1008
- frame_stride=frame_stride,
1009
- max_frames=max_frames,
1010
- model_name=model_name or DEFAULT_MODEL,
1011
- image_size=image_size,
1012
- device=None if device == "auto" else device,
1013
- max_detections=max_detections,
1014
- )
1015
- events, last_fired = evaluate_video_detections(rules.rules, result.detections, last_fired=state.last_fired)
1016
- dispatched = dispatch_events(events, enable_webhooks=enable_webhooks, webhook_url=webhook_url or None)
1017
- state.last_fired = last_fired
1018
- save_runtime_state(state)
1019
- append_events(dispatched)
1020
- automation_video_path = render_automation_video(
1021
- source_video_path=video_path,
1022
- detections=result.detections,
1023
- events=dispatched,
1024
- frame_stride=frame_stride,
1025
- max_frames=max_frames,
1026
- )
1027
- action_count = len(dispatched)
1028
- status_tone = "action" if action_count else "warning"
1029
- status_title = f"{action_count} action{'s' if action_count != 1 else ''} fired"
1030
- status_body = (
1031
- "Automation replay rendered with fired-action overlay."
1032
- if action_count
1033
- else "Rules validated, but no frame matched the automation."
1034
- )
1035
- return (
1036
- automation_video_path,
1037
- _detection_rows(result.detections),
1038
- _event_rows(dispatched),
1039
- {
1040
- "video": _result_json(result),
1041
- "actions": [event.model_dump(mode="json") for event in dispatched],
1042
- },
1043
- _status_card(status_tone, status_title, status_body),
1044
- )
1045
-
1046
-
1047
- try:
1048
- INITIAL_RULE_DOCUMENT = load_automation_text(DEFAULT_RULES)
1049
- INITIAL_RULE_LABELS = _automation_labels_from_document(INITIAL_RULE_DOCUMENT)
1050
- INITIAL_RULE_ROWS = _rule_rows(INITIAL_RULE_DOCUMENT)
1051
- INITIAL_RULE_NAMES = _rule_names(INITIAL_RULE_DOCUMENT)
1052
- INITIAL_SELECTED_RULE = INITIAL_RULE_NAMES[0] if INITIAL_RULE_NAMES else None
1053
- INITIAL_SELECTED_ENABLED = _rule_enabled(INITIAL_RULE_DOCUMENT, INITIAL_SELECTED_RULE)
1054
- except Exception:
1055
- INITIAL_RULE_DOCUMENT = None
1056
- INITIAL_RULE_LABELS = []
1057
- INITIAL_RULE_ROWS = []
1058
- INITIAL_RULE_NAMES = []
1059
- INITIAL_SELECTED_RULE = None
1060
- INITIAL_SELECTED_ENABLED = False
1061
-
1062
-
1063
- with gr.Blocks(title="Tiny Trigger") as demo:
1064
- gr.HTML(
1065
- """
1066
- <header class="tt-hero">
1067
- <div>
1068
- <p class="tt-kicker">Hackathon MVP</p>
1069
- <h1 class="tt-title">Tiny Trigger</h1>
1070
- <p class="tt-subtitle">Video events from validated automations</p>
1071
- </div>
1072
- <div class="tt-chips">
1073
- <span class="tt-chip live">Validated rules</span>
1074
- <span class="tt-chip">Open vocabulary</span>
1075
- <span class="tt-chip">Camera-ready skeleton</span>
1076
- </div>
1077
- </header>
1078
- """
1079
- )
1080
-
1081
- with gr.Tabs():
1082
- with gr.Tab("Monitor"):
1083
- with gr.Row(elem_classes=["tt-grid"]):
1084
- with gr.Column(scale=4, min_width=320):
1085
- with gr.Group(elem_classes=["tt-panel"]):
1086
- gr.Markdown("### Clip Intake", elem_classes=["tt-panel-title"])
1087
- gr.Markdown(
1088
- "Upload a short clip. Tiny Trigger scans for the labels required by the active automations and renders the event replay.",
1089
- elem_classes=["tt-flow-copy"],
1090
- )
1091
- monitor_clip_input = gr.File(
1092
- label="Video clip",
1093
- file_types=[".mp4", ".mov", ".m4v", ".webm"],
1094
- type="filepath",
1095
- )
1096
- monitor_run_button = gr.Button("Run clip", variant="primary")
1097
- with gr.Group(elem_classes=["tt-panel"]):
1098
- gr.Markdown("### Active Watch", elem_classes=["tt-panel-title"])
1099
- monitor_labels_output = gr.HTML(_labels_card(INITIAL_RULE_LABELS))
1100
- monitor_status = gr.HTML(
1101
- _status_card("idle", "Waiting for clip", "Upload a clip to run the active automations.")
1102
- )
1103
-
1104
- with gr.Column(scale=7, min_width=520):
1105
- with gr.Group(elem_classes=["tt-video-panel"]):
1106
- gr.Markdown("### Event Replay", elem_classes=["tt-panel-title"])
1107
- automation_video = gr.Video(
1108
- label="Automation replay",
1109
- format="mp4",
1110
- autoplay=True,
1111
- elem_classes=["tt-result-video"],
1112
- )
1113
- with gr.Group(elem_classes=["tt-data-panel"]):
1114
- gr.Markdown("### Events", elem_classes=["tt-panel-title"])
1115
- actions_table = gr.Dataframe(
1116
- headers=["rule", "action", "type", "status", "frame", "time", "response", "error"],
1117
- datatype=["str", "str", "str", "str", "number", "number", "str", "str"],
1118
- label="Action timeline",
1119
- )
1120
- with gr.Accordion("Run details", open=False):
1121
- automation_detections = gr.Dataframe(
1122
- headers=["frame", "time", "label", "confidence", "bbox", "bbox_norm"],
1123
- datatype=["number", "number", "str", "number", "str", "str"],
1124
- label="Detections",
1125
- )
1126
- automation_json = gr.JSON(label="Run JSON")
1127
-
1128
- with gr.Tab("Automations"):
1129
- with gr.Row(elem_classes=["tt-grid"]):
1130
- with gr.Column(scale=4, min_width=340):
1131
- with gr.Group(elem_classes=["tt-panel", "tt-automation-panel"]):
1132
- gr.Markdown("### Generate Automation", elem_classes=["tt-panel-title"])
1133
- instruction = gr.Textbox(
1134
- label="Intent",
1135
- value="If person near steering wheel then you have to turn on pc.",
1136
- lines=5,
1137
- elem_classes=["tt-compact"],
1138
- )
1139
- label_hints = gr.Textbox(
1140
- label="Label hints for the generator",
1141
- value=DEFAULT_CLASSES,
1142
- lines=3,
1143
- elem_classes=["tt-compact"],
1144
- )
1145
- with gr.Accordion("Compiler endpoint", open=False):
1146
- llama_base_url = gr.Textbox(label="llama.cpp base URL", value=DEFAULT_LLAMACPP_BASE_URL)
1147
- llama_model = gr.Textbox(label="llama.cpp model", value=DEFAULT_LLAMACPP_MODEL)
1148
- with gr.Row(elem_classes=["tt-run-row"]):
1149
- compile_button = gr.Button("Generate", variant="primary")
1150
- validate_rules_button = gr.Button("Test", variant="secondary")
1151
- save_rules_button = gr.Button("Save", variant="secondary")
1152
-
1153
- with gr.Group(elem_classes=["tt-panel"]):
1154
- gr.Markdown("### Manage Automations", elem_classes=["tt-panel-title"])
1155
- automation_table = gr.Dataframe(
1156
- headers=["state", "automation", "labels", "when", "then"],
1157
- datatype=["str", "str", "str", "str", "str"],
1158
- value=INITIAL_RULE_ROWS,
1159
- interactive=False,
1160
- label="Active automations",
1161
- )
1162
- managed_rule = gr.Dropdown(
1163
- label="Automation",
1164
- choices=INITIAL_RULE_NAMES,
1165
- value=INITIAL_SELECTED_RULE,
1166
- )
1167
- managed_enabled = gr.Checkbox(label="Enabled", value=INITIAL_SELECTED_ENABLED)
1168
- with gr.Row(elem_classes=["tt-run-row"]):
1169
- apply_rule_button = gr.Button("Apply", variant="primary")
1170
- refresh_manager_button = gr.Button("Refresh", variant="secondary")
1171
- manager_save_button = gr.Button("Save all", variant="secondary")
1172
-
1173
- with gr.Group(elem_classes=["tt-panel"]):
1174
- gr.Markdown("### Audit", elem_classes=["tt-panel-title"])
1175
- rules_status = gr.HTML(
1176
- _status_card("idle", "Rules loaded", "Generate or test rules before running a clip.")
1177
- )
1178
- rules_labels_output = gr.HTML(_labels_card(INITIAL_RULE_LABELS))
1179
-
1180
- with gr.Column(scale=7, min_width=520):
1181
- with gr.Group(elem_classes=["tt-panel"]):
1182
- gr.Markdown("### Active Rules", elem_classes=["tt-panel-title"])
1183
- rules_text = gr.Code(
1184
- label="Rules JSON/YAML",
1185
- language="yaml",
1186
- value=DEFAULT_RULES,
1187
- lines=22,
1188
- elem_classes=["tt-code"],
1189
- )
1190
- with gr.Group(elem_classes=["tt-data-panel"]):
1191
- gr.Markdown("### Compile Test", elem_classes=["tt-panel-title"])
1192
- compiled_json = gr.JSON(label="Rule audit")
1193
-
1194
- with gr.Tab("Settings"):
1195
- with gr.Row(elem_classes=["tt-grid"]):
1196
- with gr.Column(scale=4, min_width=320):
1197
- with gr.Group(elem_classes=["tt-panel"]):
1198
- gr.Markdown("### Detector", elem_classes=["tt-panel-title"])
1199
- model_name = gr.Textbox(label="YOLOE model", value=DEFAULT_MODEL)
1200
- device = gr.Dropdown(label="Device", choices=["auto", "mps", "cuda:0", "cpu"], value=DEFAULT_DEVICE)
1201
- image_size = gr.Slider(label="Image size", minimum=320, maximum=1536, step=32, value=DEFAULT_IMAGE_SIZE)
1202
- confidence = gr.Slider(label="Sensitivity", minimum=0.05, maximum=0.95, step=0.05, value=0.25)
1203
- frame_stride = gr.Slider(label="Frame stride", minimum=1, maximum=30, step=1, value=5)
1204
- max_frames = gr.Slider(label="Max processed frames", minimum=10, maximum=600, step=10, value=120)
1205
- max_detections = gr.Slider(
1206
- label="Max detections",
1207
- minimum=1,
1208
- maximum=300,
1209
- step=1,
1210
- value=DEFAULT_MAX_DETECTIONS,
1211
- )
1212
- with gr.Column(scale=4, min_width=320):
1213
- with gr.Group(elem_classes=["tt-panel"]):
1214
- gr.Markdown("### Dispatch", elem_classes=["tt-panel-title"])
1215
- enable_webhooks = gr.Checkbox(label="Enable webhook POSTs", value=False)
1216
- webhook_url = gr.Textbox(label="Fallback webhook URL", value=DEFAULT_WEBHOOK_URL)
1217
- with gr.Group(elem_classes=["tt-panel"]):
1218
- gr.Markdown("### Demo State", elem_classes=["tt-panel-title"])
1219
- demo_state_output = gr.HTML(
1220
- _status_card("idle", "Ready for replay", "Cooldown memory is kept between automation runs."),
1221
- elem_classes=["tt-demo-state"],
1222
- )
1223
- reset_state_button = gr.Button("Reset cooldowns and history", variant="secondary")
1224
- demo_state_json = gr.JSON(label="State event", visible=False)
1225
- with gr.Column(scale=4, min_width=320):
1226
- with gr.Group(elem_classes=["tt-panel"]):
1227
- gr.Markdown("### IP Camera Skeleton", elem_classes=["tt-panel-title"])
1228
- gr.Markdown(
1229
- "The MVP runs uploaded clips. This section reserves the next product path: a persistent camera feed that runs the same active automations on a schedule.",
1230
- elem_classes=["tt-flow-copy"],
1231
- )
1232
- camera_url = gr.Textbox(label="Camera / RTSP URL", value=LOCAL_CONFIG.camera_url or "")
1233
- poll_interval = gr.Slider(label="Polling interval seconds", minimum=1, maximum=60, step=1, value=5)
1234
- gr.HTML(_status_card("idle", "Not active in MVP", "Prepared for continuous monitoring after the hackathon demo."))
1235
-
1236
- monitor_inputs = [
1237
- monitor_clip_input,
1238
- rules_text,
1239
- confidence,
1240
- frame_stride,
1241
- max_frames,
1242
- model_name,
1243
- image_size,
1244
- device,
1245
- max_detections,
1246
- enable_webhooks,
1247
- webhook_url,
1248
- ]
1249
- monitor_outputs = [
1250
- automation_video,
1251
- actions_table,
1252
- automation_detections,
1253
- automation_json,
1254
- monitor_status,
1255
- monitor_labels_output,
1256
- ]
1257
- generated_rule_outputs = [
1258
- rules_text,
1259
- compiled_json,
1260
- rules_status,
1261
- rules_labels_output,
1262
- automation_table,
1263
- managed_rule,
1264
- managed_enabled,
1265
- monitor_labels_output,
1266
- ]
1267
- tested_rule_outputs = [
1268
- compiled_json,
1269
- rules_status,
1270
- rules_labels_output,
1271
- automation_table,
1272
- managed_rule,
1273
- managed_enabled,
1274
- monitor_labels_output,
1275
- ]
1276
- manager_refresh_outputs = [
1277
- automation_table,
1278
- managed_rule,
1279
- managed_enabled,
1280
- compiled_json,
1281
- rules_status,
1282
- rules_labels_output,
1283
- monitor_labels_output,
1284
- ]
1285
- manager_apply_outputs = [
1286
- rules_text,
1287
- automation_table,
1288
- managed_rule,
1289
- managed_enabled,
1290
- compiled_json,
1291
- rules_status,
1292
- rules_labels_output,
1293
- monitor_labels_output,
1294
- ]
1295
- monitor_clip_input.change(run_monitor_clip, inputs=monitor_inputs, outputs=monitor_outputs)
1296
- monitor_run_button.click(run_monitor_clip, inputs=monitor_inputs, outputs=monitor_outputs)
1297
- compile_button.click(
1298
- compile_rules,
1299
- inputs=[instruction, rules_text, label_hints, llama_base_url, llama_model],
1300
- outputs=generated_rule_outputs,
1301
- )
1302
- validate_rules_button.click(
1303
- validate_rules,
1304
- inputs=[rules_text],
1305
- outputs=tested_rule_outputs,
1306
- )
1307
- save_rules_button.click(
1308
- save_rules,
1309
- inputs=[rules_text],
1310
- outputs=generated_rule_outputs,
1311
- )
1312
- manager_save_button.click(save_rules, inputs=[rules_text], outputs=generated_rule_outputs)
1313
- refresh_manager_button.click(refresh_automation_manager, inputs=[rules_text], outputs=manager_refresh_outputs)
1314
- managed_rule.change(select_managed_rule, inputs=[rules_text, managed_rule], outputs=[managed_enabled, rules_status])
1315
- apply_rule_button.click(
1316
- set_rule_enabled,
1317
- inputs=[rules_text, managed_rule, managed_enabled],
1318
- outputs=manager_apply_outputs,
1319
- )
1320
- reset_state_button.click(reset_demo_state, outputs=[demo_state_output, demo_state_json])
1321
-
1322
-
1323
- if __name__ == "__main__":
1324
- demo.launch(ssr_mode=False, css=APP_CSS)