Javier Montalvo commited on
Commit
2a9b8d1
·
1 Parent(s): f005306

Added more providers, changed entrypoint, removed local llama for now

Browse files
DEMO.md ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tiny Trigger Demo Runbook
2
+
3
+ Use this if you need to run the demo without rebuilding context.
4
+
5
+ ## Launch
6
+
7
+ ```bash
8
+ pip install -r requirements.txt
9
+ python server.py
10
+ ```
11
+
12
+ Or:
13
+
14
+ ```bash
15
+ poetry install
16
+ poetry run python server.py
17
+ ```
18
+
19
+ Open:
20
+
21
+ ```text
22
+ http://127.0.0.1:7860
23
+ ```
24
+
25
+ For Hugging Face Spaces, the entrypoint is `server.py`; it serves the already
26
+ built frontend from `frontend/dist`.
27
+
28
+ ## API Keys
29
+
30
+ Open Settings, choose one compiler provider, and paste its API key:
31
+
32
+ - Replicate: `REPLICATE_API_TOKEN`
33
+ - OpenAI: `OPENAI_API_KEY`
34
+ - Claude: `ANTHROPIC_API_KEY`
35
+
36
+ Keys pasted in the UI are only sent with compile requests. If you own the Space,
37
+ set the matching variable as a Space secret instead.
38
+
39
+ ## Demo Video
40
+
41
+ No demo video is checked in yet. Record a short MP4 or MOV with:
42
+
43
+ - one obvious person or hand
44
+ - one clear object such as guitar, monitor, steering wheel, cup, laptop, chair
45
+ - at least one moment where the object appears or disappears
46
+ - 5-15 seconds is enough
47
+
48
+ The detector downloads YOLOE weights on first use, so the first run can be slow.
49
+
50
+ Recommended starting detector settings:
51
+
52
+ - Classes: leave defaults, or add obvious objects from the video.
53
+ - Confidence: `0.15` for normal objects, `0.05` if detections are missing.
54
+ - Frame stride: `5`.
55
+ - Max frames: `120`.
56
+ - Model: `yoloe-26s-seg.pt` for speed, `yoloe-26l-seg.pt` for a stronger demo.
57
+
58
+ Rules automatically add their referenced labels to the detector class list.
59
+
60
+ ## Prompts To Try
61
+
62
+ State assertion, should fire once when true:
63
+
64
+ ```text
65
+ While there is a guitar in the scene, amplifier must be on.
66
+ ```
67
+
68
+ Enter/exit behavior:
69
+
70
+ ```text
71
+ If a person is near the monitor, turn on the LED lights. When the person leaves, turn them off.
72
+ ```
73
+
74
+ Cooldown behavior:
75
+
76
+ ```text
77
+ If a person is near the steering wheel, turn on the PC. Do not repeat for five minutes.
78
+ ```
79
+
80
+ Simple presence:
81
+
82
+ ```text
83
+ When a laptop is visible, notify me.
84
+ ```
85
+
86
+ ## Expected Flow
87
+
88
+ 1. Upload the video in Detector.
89
+ 2. Go to Settings and choose a cloud compiler provider.
90
+ 3. Paste the API key if the Space has no secret configured.
91
+ 4. Go to Rule Studio, Compose.
92
+ 5. Compile one of the prompts above.
93
+ 6. Check Source to see the generated rule document.
94
+ 7. Check Rules to see enabled rules and label pills.
95
+ 8. Run detection.
96
+ 9. Watch Activity & Firings for fired actions.
97
+ 10. Use the replay to inspect detections and event timing.
98
+
99
+ ## What To Say
100
+
101
+ Tiny Trigger turns natural language video automation ideas into validated rules.
102
+ The LLM only writes JSON/YAML, never executable code. YOLOE searches both user
103
+ classes and labels referenced by active rules. Rules use edge triggers by
104
+ default, so "turn on when present" does not spam every frame.
105
+
106
+ ## If Something Fails
107
+
108
+ - No compile: check the provider and API key.
109
+ - No detections: lower confidence, add labels manually, or try a larger YOLOE model.
110
+ - Slow first run: model weights are downloading.
111
+ - Repeated actions: check the rule trigger. For most demos, prefer `enter` or `change`.
112
+ - Missing frontend in a Space: make sure `frontend/dist` is included in the upload.
README.md CHANGED
@@ -6,10 +6,10 @@ colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.17.3
8
  python_version: '3.13'
9
- app_file: app.py
10
  pinned: false
11
  license: other
12
- short_description: 'Open-vocabulary video automations with YOLOE and llama.cpp'
13
  ---
14
 
15
  # Tiny Trigger
@@ -19,26 +19,29 @@ automations. It uses YOLOE to detect user-supplied classes plus every label
19
  referenced by enabled rules in a video, then evaluates small structured rules
20
  that can trigger simulated actions or optional webhook POSTs.
21
 
22
- The LLM path is intentionally constrained: llama.cpp compiles natural language
23
- into JSON/YAML automation rules. The app validates those rules before evaluating
24
- them, and the LLM never emits executable code.
 
 
25
 
26
  ## Run Locally
27
 
28
  ```bash
29
  pip install -r requirements.txt
30
- python app.py
31
  ```
32
 
33
  Or with Poetry:
34
 
35
  ```bash
36
  poetry install
37
- poetry run python app.py
38
  ```
39
 
40
  The detector defaults to `yoloe-26s-seg.pt`, which Ultralytics downloads on
41
- first use if it is not already cached.
 
42
 
43
  For small or background objects, use a larger model such as `yoloe-26l-seg.pt`,
44
  set device to `cuda:0`, lower confidence to `0.05`-`0.15`, and raise image size
@@ -57,19 +60,12 @@ Or:
57
  poetry run pytest
58
  ```
59
 
60
- ## llama.cpp Rule Compiler
61
-
62
- Start a llama.cpp server with the small GGUF model:
63
-
64
- ```bash
65
- llama-server -hf ggml-org/Qwen3-1.7B-GGUF:Q4_K_M
66
- ```
67
-
68
- Then use the default compiler endpoint in the UI:
69
 
70
- ```text
71
- http://127.0.0.1:8080/v1
72
- ```
 
73
 
74
  ## Rule Shape
75
 
@@ -138,7 +134,10 @@ default_detector_model: "yoloe-26x-seg.pt"
138
  default_device: "cuda:0"
139
  default_image_size: 1280
140
  default_max_detections: 300
141
- llamacpp_base_url: "http://127.0.0.1:8080/v1"
 
 
 
142
  ```
143
 
144
  ## License
 
6
  sdk: gradio
7
  sdk_version: 6.17.3
8
  python_version: '3.13'
9
+ app_file: server.py
10
  pinned: false
11
  license: other
12
+ short_description: 'Open-vocabulary video automations with YOLOE and cloud rule compilers'
13
  ---
14
 
15
  # Tiny Trigger
 
19
  referenced by enabled rules in a video, then evaluates small structured rules
20
  that can trigger simulated actions or optional webhook POSTs.
21
 
22
+ The LLM path is intentionally constrained: Replicate, OpenAI, or Claude compile
23
+ natural language into JSON/YAML automation rules. The app validates those rules
24
+ before evaluating them, and the LLM never emits executable code.
25
+
26
+ For a quick operator checklist, see [`DEMO.md`](DEMO.md).
27
 
28
  ## Run Locally
29
 
30
  ```bash
31
  pip install -r requirements.txt
32
+ python server.py
33
  ```
34
 
35
  Or with Poetry:
36
 
37
  ```bash
38
  poetry install
39
+ poetry run python server.py
40
  ```
41
 
42
  The detector defaults to `yoloe-26s-seg.pt`, which Ultralytics downloads on
43
+ first use if it is not already cached. Model weights are not checked into the
44
+ repo.
45
 
46
  For small or background objects, use a larger model such as `yoloe-26l-seg.pt`,
47
  set device to `cuda:0`, lower confidence to `0.05`-`0.15`, and raise image size
 
60
  poetry run pytest
61
  ```
62
 
63
+ ## Cloud Rule Compiler
 
 
 
 
 
 
 
 
64
 
65
+ Choose Replicate, OpenAI, or Claude in Settings and paste the matching API key.
66
+ Keys entered in the UI are sent only with compile requests. Hugging Face Space
67
+ owners can also set `REPLICATE_API_TOKEN`, `OPENAI_API_KEY`, or
68
+ `ANTHROPIC_API_KEY` as Space secrets.
69
 
70
  ## Rule Shape
71
 
 
134
  default_device: "cuda:0"
135
  default_image_size: 1280
136
  default_max_detections: 300
137
+ llm_provider: "anthropic"
138
+ replicate_model: "openai/gpt-5.2"
139
+ openai_model: "gpt-5.5"
140
+ anthropic_model: "claude-sonnet-4-6"
141
  ```
142
 
143
  ## License
frontend/.gitignore CHANGED
@@ -1,4 +1,3 @@
1
  node_modules
2
- dist
3
  *.local
4
  .vite
 
1
  node_modules
 
2
  *.local
3
  .vite
frontend/dist/assets/__vite-browser-external-DYxpcVy9-BIHI7g3E.js ADDED
@@ -0,0 +1 @@
 
 
1
+ const e={};export{e as default};
frontend/dist/assets/index-Cs8-YHSt.js ADDED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-DpGaJEG3.css ADDED
@@ -0,0 +1 @@
 
 
1
+ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-relaxed:1.625;--animate-spin:spin 1s linear infinite;--blur-sm:8px;--blur-xl:24px;--aspect-video:16 / 9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-background:var(--background);--color-foreground:var(--foreground);--color-primary:var(--primary);--color-border:var(--border);--font-display:var(--font-display)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{border-color:var(--color-border)}html{-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility}body{background-color:var(--color-background);min-height:100vh;color:var(--color-foreground);font-family:var(--font-sans);font-feature-settings:"ss01","cv01","tnum";background-image:radial-gradient(90rem 60rem at 82% -10%,#c5ec4512,#0000 60%),radial-gradient(70rem 50rem at -10% 110%,#4cc8ee0f,#0000 55%),linear-gradient(90deg,#ffffff05 1px,#0000 1px),linear-gradient(#ffffff05 1px,#0000 1px);background-size:100% 100%,100% 100%,56px 56px,56px 56px;background-attachment:fixed;margin:0}::selection{background:#c5ec4547}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:#ffffff1a padding-box content-box;border:2px solid #0000;border-radius:999px}::-webkit-scrollbar-thumb:hover{background:#ffffff2e padding-box content-box}}@layer components;@layer utilities{.\@container{container-type:inline-size}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.not-sr-only{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.-top-0,.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.bottom-0{bottom:calc(var(--spacing) * 0)}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.isolate{isolation:isolate}.isolation-auto{isolation:auto}.z-10{z-index:10}.z-30{z-index:30}.z-50{z-index:50}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.inline-table{display:inline-table}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row{display:table-row}.table-row-group{display:table-row-group}.aspect-video{aspect-ratio:var(--aspect-video)}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-full{width:100%;height:100%}.h-1{height:calc(var(--spacing) * 1)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-16{height:calc(var(--spacing) * 16)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-28{max-height:calc(var(--spacing) * 28)}.max-h-72{max-height:calc(var(--spacing) * 72)}.min-h-16{min-height:calc(var(--spacing) * 16)}.min-h-20{min-height:calc(var(--spacing) * 20)}.min-h-24{min-height:calc(var(--spacing) * 24)}.min-h-64{min-height:calc(var(--spacing) * 64)}.min-h-screen{min-height:100vh}.w-0\.5{width:calc(var(--spacing) * .5)}.w-9{width:calc(var(--spacing) * 9)}.w-16{width:calc(var(--spacing) * 16)}.w-full{width:100%}.w-px{width:1px}.max-w-\[1500px\]{max-width:1500px}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.flex-\[2\]{flex:2}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.caption-bottom{caption-side:bottom}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing) * 4);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-none{translate:none}.scale-3d{scale:var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z)}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-in{animation:enter var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.touch-pinch-zoom{--tw-pinch-zoom:pinch-zoom;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.scroll-mt-20{scroll-margin-top:calc(var(--spacing) * 20)}.scroll-mt-24{scroll-margin-top:calc(var(--spacing) * 24)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-reverse>:not(:last-child)){--tw-space-y-reverse:1}.gap-x-4{column-gap:calc(var(--spacing) * 4)}:where(.space-x-reverse>:not(:last-child)){--tw-space-x-reverse:1}.gap-y-1{row-gap:calc(var(--spacing) * 1)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px * var(--tw-divide-x-reverse));border-inline-end-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-reverse>:not(:last-child)){--tw-divide-y-reverse:1}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.\!rounded-lg{border-radius:var(--radius)!important}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-xl{border-radius:calc(var(--radius) + 4px)}.rounded-s{border-start-start-radius:.25rem;border-end-start-radius:.25rem}.rounded-ss{border-start-start-radius:.25rem}.rounded-e{border-start-end-radius:.25rem;border-end-end-radius:.25rem}.rounded-se{border-start-end-radius:.25rem}.rounded-ee{border-end-end-radius:.25rem}.rounded-es{border-end-start-radius:.25rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-tl{border-top-left-radius:.25rem}.rounded-tl-md{border-top-left-radius:calc(var(--radius) - 2px)}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-tr{border-top-right-radius:.25rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-br{border-bottom-right-radius:.25rem}.rounded-br-md{border-bottom-right-radius:calc(var(--radius) - 2px)}.rounded-bl{border-bottom-left-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-s{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px}.border-e{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}.border-bs{border-block-start-style:var(--tw-border-style);border-block-start-width:1px}.border-be{border-block-end-style:var(--tw-border-style);border-block-end-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.\!border-border{border-color:var(--border)!important}.border-border,.border-border\/60{border-color:var(--border)}@supports (color:color-mix(in lab,red,red)){.border-border\/60{border-color:color-mix(in oklab,var(--border) 60%,transparent)}}.border-destructive\/30{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/30{border-color:color-mix(in oklab,var(--destructive) 30%,transparent)}}.border-destructive\/40{border-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.border-destructive\/40{border-color:color-mix(in oklab,var(--destructive) 40%,transparent)}}.border-info\/40{border-color:var(--info)}@supports (color:color-mix(in lab,red,red)){.border-info\/40{border-color:color-mix(in oklab,var(--info) 40%,transparent)}}.border-input{border-color:var(--input)}.border-primary,.border-primary\/40{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/40{border-color:color-mix(in oklab,var(--primary) 40%,transparent)}}.border-primary\/50{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/50{border-color:color-mix(in oklab,var(--primary) 50%,transparent)}}.border-primary\/60{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.border-primary\/60{border-color:color-mix(in oklab,var(--primary) 60%,transparent)}}.border-warning\/30{border-color:var(--warning)}@supports (color:color-mix(in lab,red,red)){.border-warning\/30{border-color:color-mix(in oklab,var(--warning) 30%,transparent)}}.border-warning\/40{border-color:var(--warning)}@supports (color:color-mix(in lab,red,red)){.border-warning\/40{border-color:color-mix(in oklab,var(--warning) 40%,transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.border-white\/10{border-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.\!bg-popover{background-color:var(--popover)!important}.\!bg-primary{background-color:var(--primary)!important}.bg-background,.bg-background\/70{background-color:var(--background)}@supports (color:color-mix(in lab,red,red)){.bg-background\/70{background-color:color-mix(in oklab,var(--background) 70%,transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/20{background-color:#0003}@supports (color:color-mix(in lab,red,red)){.bg-black\/20{background-color:color-mix(in oklab,var(--color-black) 20%,transparent)}}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab,red,red)){.bg-black\/30{background-color:color-mix(in oklab,var(--color-black) 30%,transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black) 60%,transparent)}}.bg-border{background-color:var(--border)}.bg-card\/40{background-color:var(--card)}@supports (color:color-mix(in lab,red,red)){.bg-card\/40{background-color:color-mix(in oklab,var(--card) 40%,transparent)}}.bg-card\/80{background-color:var(--card)}@supports (color:color-mix(in lab,red,red)){.bg-card\/80{background-color:color-mix(in oklab,var(--card) 80%,transparent)}}.bg-destructive\/5{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/5{background-color:color-mix(in oklab,var(--destructive) 5%,transparent)}}.bg-destructive\/10{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/10{background-color:color-mix(in oklab,var(--destructive) 10%,transparent)}}.bg-destructive\/15{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/15{background-color:color-mix(in oklab,var(--destructive) 15%,transparent)}}.bg-info\/10{background-color:var(--info)}@supports (color:color-mix(in lab,red,red)){.bg-info\/10{background-color:color-mix(in oklab,var(--info) 10%,transparent)}}.bg-muted-foreground,.bg-muted-foreground\/40{background-color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.bg-muted-foreground\/40{background-color:color-mix(in oklab,var(--muted-foreground) 40%,transparent)}}.bg-popover{background-color:var(--popover)}.bg-primary,.bg-primary\/5{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/5{background-color:color-mix(in oklab,var(--primary) 5%,transparent)}}.bg-primary\/10{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/10{background-color:color-mix(in oklab,var(--primary) 10%,transparent)}}.bg-primary\/30{background-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.bg-primary\/30{background-color:color-mix(in oklab,var(--primary) 30%,transparent)}}.bg-secondary{background-color:var(--secondary)}.bg-transparent{background-color:#0000}.bg-warning\/5{background-color:var(--warning)}@supports (color:color-mix(in lab,red,red)){.bg-warning\/5{background-color:color-mix(in oklab,var(--warning) 5%,transparent)}}.bg-warning\/10{background-color:var(--warning)}@supports (color:color-mix(in lab,red,red)){.bg-warning\/10{background-color:color-mix(in oklab,var(--warning) 10%,transparent)}}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/5{background-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.bg-white\/10{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-primary\/90{--tw-gradient-from:var(--primary)}@supports (color:color-mix(in lab,red,red)){.from-primary\/90{--tw-gradient-from:color-mix(in oklab, var(--primary) 90%, transparent)}}.from-primary\/90{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-primary\/70{--tw-gradient-to:var(--primary)}@supports (color:color-mix(in lab,red,red)){.to-primary\/70{--tw-gradient-to:color-mix(in oklab, var(--primary) 70%, transparent)}}.to-primary\/70{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.bg-repeat{background-repeat:repeat}.mask-no-clip{-webkit-mask-clip:no-clip;mask-clip:no-clip}.mask-repeat{-webkit-mask-repeat:repeat;mask-repeat:repeat}.object-contain{object-fit:contain}.p-1{padding:calc(var(--spacing) * 1)}.p-3{padding:calc(var(--spacing) * 3)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-7{padding-top:calc(var(--spacing) * 7)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.\!font-sans{font-family:var(--font-sans)!important}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.5rem\]{font-size:.5rem}.text-\[0\.95rem\]{font-size:.95rem}.text-\[0\.625rem\]{font-size:.625rem}.text-\[0\.6875rem\]{font-size:.6875rem}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.2em\]{--tw-tracking:.2em;letter-spacing:.2em}.tracking-\[0\.16em\]{--tw-tracking:.16em;letter-spacing:.16em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-wrap{text-wrap:wrap}.text-clip{text-overflow:clip}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.\!text-foreground{color:var(--foreground)!important}.\!text-muted-foreground{color:var(--muted-foreground)!important}.\!text-primary-foreground{color:var(--primary-foreground)!important}.text-destructive,.text-destructive\/90{color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.text-destructive\/90{color:color-mix(in oklab,var(--destructive) 90%,transparent)}}.text-foreground{color:var(--foreground)}.text-info{color:var(--info)}.text-muted-foreground,.text-muted-foreground\/50{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.text-muted-foreground\/50{color:color-mix(in oklab,var(--muted-foreground) 50%,transparent)}}.text-popover-foreground{color:var(--popover-foreground)}.text-primary{color:var(--primary)}.text-primary-foreground{color:var(--primary-foreground)}.text-primary\/70{color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.text-primary\/70{color:color-mix(in oklab,var(--primary) 70%,transparent)}}.text-secondary-foreground{color:var(--secondary-foreground)}.text-warning{color:var(--warning)}.text-white\/70{color:#ffffffb3}@supports (color:color-mix(in lab,red,red)){.text-white\/70{color:color-mix(in oklab,var(--color-white) 70%,transparent)}}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.not-italic{font-style:normal}.diagonal-fractions{--tw-numeric-fraction:diagonal-fractions;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.lining-nums{--tw-numeric-figure:lining-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.oldstyle-nums{--tw-numeric-figure:oldstyle-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.proportional-nums{--tw-numeric-spacing:proportional-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.slashed-zero{--tw-slashed-zero:slashed-zero;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.stacked-fractions{--tw-numeric-fraction:stacked-fractions;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.normal-nums{font-variant-numeric:normal}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.subpixel-antialiased{-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.opacity-40{opacity:.4}.opacity-100{opacity:1}.opacity-\[0\.07\]{opacity:.07}.\!shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_1px_oklch\(0\.885_0\.19_122_\/_0\.4\)\,0_8px_24px_-12px_oklch\(0\.885_0\.19_122_\/_0\.6\)\]{--tw-shadow:0 0 0 1px var(--tw-shadow-color,oklch(88.5% .19 122/.4)), 0 8px 24px -12px var(--tw-shadow-color,oklch(88.5% .19 122/.6));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_3px_oklch\(0\.885_0\.19_122_\/_0\.2\)\]{--tw-shadow:0 0 0 3px var(--tw-shadow-color,oklch(88.5% .19 122/.2));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_1px_0_0_oklch\(1_0_0_\/_0\.04\)_inset\,0_24px_48px_-32px_oklch\(0_0_0_\/_0\.8\)\]{--tw-shadow:0 1px 0 0 var(--tw-shadow-color,oklch(100% 0 0/.04)) inset, 0 24px 48px -32px var(--tw-shadow-color,oklch(0% 0 0/.8));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_8px_24px_-12px_oklch\(0\.885_0\.19_122_\/_0\.8\)\]{--tw-shadow:0 8px 24px -12px var(--tw-shadow-color,oklch(88.5% .19 122/.8));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.inset-ring{--tw-inset-ring-shadow:inset 0 0 0 1px var(--tw-inset-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-offset-background{--tw-ring-offset-color:var(--background)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-grayscale{--tw-backdrop-grayscale:grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-invert{--tw-backdrop-invert:invert(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-sepia{--tw-backdrop-sepia:sepia(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.fade-in-0{--tw-enter-opacity:0}.select-none{-webkit-user-select:none;user-select:none}.zoom-in-95{--tw-enter-scale:.95}.\[writing-mode\:vertical-rl\]{writing-mode:vertical-rl}:where(.divide-x-reverse>:not(:last-child)){--tw-divide-x-reverse:1}.paused{animation-play-state:paused}.ring-inset{--tw-ring-inset:inset}.running{animation-play-state:running}.zoom-in{--tw-enter-scale:0}.zoom-out{--tw-exit-scale:0}@media(hover:hover){.group-hover\:text-foreground:is(:where(.group):hover *){color:var(--foreground)}}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.placeholder\:text-muted-foreground\/60::placeholder{color:var(--muted-foreground)}@supports (color:color-mix(in lab,red,red)){.placeholder\:text-muted-foreground\/60::placeholder{color:color-mix(in oklab,var(--muted-foreground) 60%,transparent)}}@media(hover:hover){.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:border-white\/15:hover{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){.hover\:border-white\/15:hover{border-color:color-mix(in oklab,var(--color-white) 15%,transparent)}}.hover\:border-white\/20:hover{border-color:#fff3}@supports (color:color-mix(in lab,red,red)){.hover\:border-white\/20:hover{border-color:color-mix(in oklab,var(--color-white) 20%,transparent)}}.hover\:bg-accent:hover{background-color:var(--accent)}.hover\:bg-destructive\/25:hover{background-color:var(--destructive)}@supports (color:color-mix(in lab,red,red)){.hover\:bg-destructive\/25:hover{background-color:color-mix(in oklab,var(--destructive) 25%,transparent)}}.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.hover\:bg-white\/\[0\.02\]:hover{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.02\]:hover{background-color:color-mix(in oklab,var(--color-white) 2%,transparent)}}.hover\:bg-white\/\[0\.025\]:hover{background-color:#ffffff06}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.025\]:hover{background-color:color-mix(in oklab,var(--color-white) 2.5%,transparent)}}.hover\:text-foreground:hover{color:var(--foreground)}.hover\:brightness-110:hover{--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.focus-visible\:border-primary\/50:focus-visible{border-color:var(--primary)}@supports (color:color-mix(in lab,red,red)){.focus-visible\:border-primary\/50:focus-visible{border-color:color-mix(in oklab,var(--primary) 50%,transparent)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--ring)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:scale-\[0\.98\]:active{scale:.98}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.data-\[state\=active\]\:bg-secondary[data-state=active]{background-color:var(--secondary)}.data-\[state\=active\]\:text-foreground[data-state=active]{color:var(--foreground)}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation:exit var(--tw-animation-duration,var(--tw-duration,.15s))var(--tw-ease,ease)var(--tw-animation-delay,0s)var(--tw-animation-iteration-count,1)var(--tw-animation-direction,normal)var(--tw-animation-fill-mode,none)}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity:0}@media(min-width:40rem){.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media(min-width:48rem){.md\:flex{display:flex}}@media(min-width:64rem){.lg\:col-span-4{grid-column:span 4/span 4}.lg\:col-span-5{grid-column:span 5/span 5}.lg\:col-span-6{grid-column:span 6/span 6}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:col-span-12{grid-column:span 12/span 12}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}}.\[\&_svg\]\:size-4 svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.font-display{font-family:var(--font-display)}.text-balance{text-wrap:balance}.signal-dot{box-shadow:0 0 0 0 var(--color-primary);animation:1.8s cubic-bezier(.4,0,.2,1) infinite signal-pulse}@keyframes signal-pulse{0%{box-shadow:0 0 #c5ec4580}70%{box-shadow:0 0 0 8px #c5ec4500}to{box-shadow:0 0 #c5ec4500}}.reveal{opacity:0;animation:.55s cubic-bezier(.22,1,.36,1) forwards reveal-up}@keyframes reveal-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.hairline{background-image:linear-gradient(to right,transparent,var(--color-border),transparent)}}@property --tw-animation-delay{syntax:"*";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:"*";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:"*";inherits:false}@property --tw-animation-fill-mode{syntax:"*";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:"*";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:"*";inherits:false;initial-value:0}:root{--radius:.625rem;--background:oklch(14.5% .006 264);--foreground:oklch(93% .004 264);--card:oklch(18.5% .007 264);--card-foreground:oklch(93% .004 264);--popover:oklch(20.5% .008 264);--popover-foreground:oklch(93% .004 264);--primary:oklch(88.5% .19 122);--primary-foreground:oklch(18% .03 130);--secondary:oklch(23.5% .008 264);--secondary-foreground:oklch(93% .004 264);--muted:oklch(23.5% .008 264);--muted-foreground:oklch(64% .012 264);--accent:oklch(23.5% .008 264);--accent-foreground:oklch(93% .004 264);--destructive:oklch(63.7% .21 25.3);--destructive-foreground:oklch(97% .01 20);--warning:oklch(79% .15 78);--warning-foreground:oklch(20% .03 78);--info:oklch(78% .12 222);--info-foreground:oklch(18% .03 240);--border:oklch(100% 0 0/.08);--input:oklch(100% 0 0/.12);--ring:oklch(88.5% .19 122/.55);--font-display:"Clash Display", ui-sans-serif, system-ui, sans-serif;--font-sans:"Hanken Grotesk", ui-sans-serif, system-ui, sans-serif;--font-mono:"JetBrains Mono", ui-monospace, monospace}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0)scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1))rotate(var(--tw-enter-rotate,0));filter:blur(var(--tw-enter-blur,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0)scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1))rotate(var(--tw-exit-rotate,0));filter:blur(var(--tw-exit-blur,0))}}
frontend/dist/index.html ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Tiny Trigger — Automation Console</title>
7
+ <meta name="color-scheme" content="dark" />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link rel="preconnect" href="https://api.fontshare.com" crossorigin />
11
+ <link
12
+ href="https://api.fontshare.com/v2/css?f[]=clash-display@600,500,700&display=swap"
13
+ rel="stylesheet"
14
+ />
15
+ <link
16
+ href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap"
17
+ rel="stylesheet"
18
+ />
19
+ <script type="module" crossorigin src="/assets/index-Cs8-YHSt.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-DpGaJEG3.css">
21
+ </head>
22
+ <body>
23
+ <div id="root"></div>
24
+ </body>
25
+ </html>
frontend/src/lib/api.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { Client, handle_file } from "@gradio/client"
2
  import type {
3
  CompileResult,
 
4
  CompilerProvider,
5
  DetectParams,
6
  LocalConfig,
@@ -53,18 +54,26 @@ export async function compileRules(
53
  instruction: string,
54
  classes: string,
55
  existingRulesText: string,
56
- provider: CompilerProvider,
57
- baseUrl: string,
58
- model: string,
59
  ): Promise<CompileResult> {
 
 
 
 
 
 
 
 
 
 
60
  return call<CompileResult>("/compile_rules", {
61
  instruction,
62
  classes,
63
  existing_rules_text: existingRulesText,
64
  append: true,
65
- provider,
66
- base_url: baseUrl,
67
- model,
68
  })
69
  }
70
 
 
1
  import { Client, handle_file } from "@gradio/client"
2
  import type {
3
  CompileResult,
4
+ CloudCompilerConfig,
5
  CompilerProvider,
6
  DetectParams,
7
  LocalConfig,
 
54
  instruction: string,
55
  classes: string,
56
  existingRulesText: string,
57
+ compiler: CloudCompilerConfig,
 
 
58
  ): Promise<CompileResult> {
59
+ const apiKeyByProvider: Record<CompilerProvider, string> = {
60
+ replicate: compiler.replicateApiKey,
61
+ openai: compiler.openaiApiKey,
62
+ anthropic: compiler.anthropicApiKey,
63
+ }
64
+ const modelByProvider: Record<CompilerProvider, string> = {
65
+ replicate: "",
66
+ openai: compiler.openaiModel,
67
+ anthropic: compiler.anthropicModel,
68
+ }
69
  return call<CompileResult>("/compile_rules", {
70
  instruction,
71
  classes,
72
  existing_rules_text: existingRulesText,
73
  append: true,
74
+ provider: compiler.provider,
75
+ api_key: apiKeyByProvider[compiler.provider],
76
+ model: modelByProvider[compiler.provider],
77
  })
78
  }
79
 
frontend/src/lib/dashboard.tsx CHANGED
@@ -11,6 +11,7 @@ import type { ReactNode } from "react"
11
  import { toast } from "sonner"
12
  import * as api from "./api"
13
  import type {
 
14
  CompileResult,
15
  CompilerProvider,
16
  DetectParams,
@@ -47,6 +48,15 @@ const DEFAULT_PARAMS: DetectParams = {
47
  webhookUrl: "",
48
  }
49
 
 
 
 
 
 
 
 
 
 
50
  interface DashboardState {
51
  params: DetectParams
52
  setParam: <K extends keyof DetectParams>(key: K, value: DetectParams[K]) => void
@@ -64,8 +74,10 @@ interface DashboardState {
64
  save: () => Promise<void>
65
  setRuleEnabled: (ruleName: string, enabled: boolean) => Promise<void>
66
  deleteRule: (ruleName: string) => Promise<void>
67
- compilerProvider: CompilerProvider
68
  setCompilerProvider: (provider: CompilerProvider) => void
 
 
69
  compile: (instruction: string) => Promise<CompileResult | null>
70
  compiling: boolean
71
  }
@@ -81,7 +93,15 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
81
  const [running, setRunning] = useState(false)
82
  const [error, setError] = useState<string | null>(null)
83
  const [validation, setValidation] = useState<ValidationResult | null>(null)
84
- const [compilerProvider, setCompilerProvider] = useState<CompilerProvider>("cloud")
 
 
 
 
 
 
 
 
85
  const [compiling, setCompiling] = useState(false)
86
  const previewRef = useRef<string | null>(null)
87
  const compileInFlightRef = useRef(false)
@@ -107,9 +127,14 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
107
  maxDetections: cfg.default_max_detections ?? p.maxDetections,
108
  webhookUrl: cfg.webhook_url ?? p.webhookUrl,
109
  }))
110
- if (cfg.llm_provider === "cloud" || cfg.llm_provider === "local") {
111
  setCompilerProvider(cfg.llm_provider)
112
  }
 
 
 
 
 
113
  })
114
  .catch(() => void 0)
115
  api
@@ -127,6 +152,26 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
127
  [],
128
  )
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  const setVideo = useCallback((file: File | null) => {
131
  if (previewRef.current) URL.revokeObjectURL(previewRef.current)
132
  if (file) {
@@ -229,9 +274,7 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
229
  instruction,
230
  params.classes,
231
  rulesText,
232
- compilerProvider,
233
- "http://127.0.0.1:8080/v1",
234
- "ggml-org/Qwen3-1.7B-GGUF:Q4_K_M",
235
  )
236
  await applyRulesText(c.rules_text)
237
  toast.success(`Added rule. ${c.rule_count} total.`)
@@ -246,7 +289,7 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
246
  setCompiling(false)
247
  }
248
  },
249
- [applyRulesText, compilerProvider, params.classes, rulesText],
250
  )
251
 
252
  const value = useMemo<DashboardState>(
@@ -267,8 +310,10 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
267
  save,
268
  setRuleEnabled,
269
  deleteRule,
270
- compilerProvider,
271
  setCompilerProvider,
 
 
272
  compile,
273
  compiling,
274
  }),
@@ -288,7 +333,8 @@ export function DashboardProvider({ children }: { children: ReactNode }) {
288
  save,
289
  setRuleEnabled,
290
  deleteRule,
291
- compilerProvider,
 
292
  compile,
293
  compiling,
294
  ],
 
11
  import { toast } from "sonner"
12
  import * as api from "./api"
13
  import type {
14
+ CloudCompilerConfig,
15
  CompileResult,
16
  CompilerProvider,
17
  DetectParams,
 
48
  webhookUrl: "",
49
  }
50
 
51
+ const DEFAULT_COMPILER: CloudCompilerConfig = {
52
+ provider: "anthropic",
53
+ replicateApiKey: "",
54
+ openaiApiKey: "",
55
+ anthropicApiKey: "",
56
+ openaiModel: "gpt-5.5",
57
+ anthropicModel: "claude-sonnet-4-6",
58
+ }
59
+
60
  interface DashboardState {
61
  params: DetectParams
62
  setParam: <K extends keyof DetectParams>(key: K, value: DetectParams[K]) => void
 
74
  save: () => Promise<void>
75
  setRuleEnabled: (ruleName: string, enabled: boolean) => Promise<void>
76
  deleteRule: (ruleName: string) => Promise<void>
77
+ compiler: CloudCompilerConfig
78
  setCompilerProvider: (provider: CompilerProvider) => void
79
+ setCompilerApiKey: (provider: CompilerProvider, apiKey: string) => void
80
+ setCompilerModel: (provider: "openai" | "anthropic", model: string) => void
81
  compile: (instruction: string) => Promise<CompileResult | null>
82
  compiling: boolean
83
  }
 
93
  const [running, setRunning] = useState(false)
94
  const [error, setError] = useState<string | null>(null)
95
  const [validation, setValidation] = useState<ValidationResult | null>(null)
96
+ const [compiler, setCompiler] = useState<CloudCompilerConfig>(() => ({
97
+ ...DEFAULT_COMPILER,
98
+ provider:
99
+ (localStorage.getItem("tiny-trigger-compiler-provider") as CompilerProvider | null) ??
100
+ DEFAULT_COMPILER.provider,
101
+ openaiModel: localStorage.getItem("tiny-trigger-openai-model") ?? DEFAULT_COMPILER.openaiModel,
102
+ anthropicModel:
103
+ localStorage.getItem("tiny-trigger-anthropic-model") ?? DEFAULT_COMPILER.anthropicModel,
104
+ }))
105
  const [compiling, setCompiling] = useState(false)
106
  const previewRef = useRef<string | null>(null)
107
  const compileInFlightRef = useRef(false)
 
127
  maxDetections: cfg.default_max_detections ?? p.maxDetections,
128
  webhookUrl: cfg.webhook_url ?? p.webhookUrl,
129
  }))
130
+ if (cfg.llm_provider === "replicate" || cfg.llm_provider === "openai" || cfg.llm_provider === "anthropic") {
131
  setCompilerProvider(cfg.llm_provider)
132
  }
133
+ setCompiler((current) => ({
134
+ ...current,
135
+ openaiModel: cfg.openai_model ?? current.openaiModel,
136
+ anthropicModel: cfg.anthropic_model ?? current.anthropicModel,
137
+ }))
138
  })
139
  .catch(() => void 0)
140
  api
 
152
  [],
153
  )
154
 
155
+ const setCompilerProvider = useCallback((provider: CompilerProvider) => {
156
+ localStorage.setItem("tiny-trigger-compiler-provider", provider)
157
+ setCompiler((current) => ({ ...current, provider }))
158
+ }, [])
159
+
160
+ const setCompilerApiKey = useCallback((provider: CompilerProvider, apiKey: string) => {
161
+ const keyByProvider: Record<CompilerProvider, keyof CloudCompilerConfig> = {
162
+ replicate: "replicateApiKey",
163
+ openai: "openaiApiKey",
164
+ anthropic: "anthropicApiKey",
165
+ }
166
+ setCompiler((current) => ({ ...current, [keyByProvider[provider]]: apiKey }))
167
+ }, [])
168
+
169
+ const setCompilerModel = useCallback((provider: "openai" | "anthropic", model: string) => {
170
+ const key = provider === "openai" ? "openaiModel" : "anthropicModel"
171
+ localStorage.setItem(`tiny-trigger-${provider}-model`, model)
172
+ setCompiler((current) => ({ ...current, [key]: model }))
173
+ }, [])
174
+
175
  const setVideo = useCallback((file: File | null) => {
176
  if (previewRef.current) URL.revokeObjectURL(previewRef.current)
177
  if (file) {
 
274
  instruction,
275
  params.classes,
276
  rulesText,
277
+ compiler,
 
 
278
  )
279
  await applyRulesText(c.rules_text)
280
  toast.success(`Added rule. ${c.rule_count} total.`)
 
289
  setCompiling(false)
290
  }
291
  },
292
+ [applyRulesText, compiler, params.classes, rulesText],
293
  )
294
 
295
  const value = useMemo<DashboardState>(
 
310
  save,
311
  setRuleEnabled,
312
  deleteRule,
313
+ compiler,
314
  setCompilerProvider,
315
+ setCompilerApiKey,
316
+ setCompilerModel,
317
  compile,
318
  compiling,
319
  }),
 
333
  save,
334
  setRuleEnabled,
335
  deleteRule,
336
+ compiler,
337
+ setCompilerModel,
338
  compile,
339
  compiling,
340
  ],
frontend/src/lib/types.ts CHANGED
@@ -65,7 +65,16 @@ export interface RuleMutationResult {
65
  rule_count: number
66
  }
67
 
68
- export type CompilerProvider = "local" | "cloud"
 
 
 
 
 
 
 
 
 
69
 
70
  export interface LocalConfig {
71
  camera_url: string | null
@@ -76,10 +85,10 @@ export interface LocalConfig {
76
  default_image_size: number | null
77
  default_max_detections: number | null
78
  llm_provider: CompilerProvider | null
79
- llamacpp_base_url: string | null
80
- llamacpp_model: string | null
81
  replicate_model: string | null
82
  replicate_reasoning_effort: string | null
 
 
83
  }
84
 
85
  export interface DetectParams {
 
65
  rule_count: number
66
  }
67
 
68
+ export type CompilerProvider = "replicate" | "openai" | "anthropic"
69
+
70
+ export interface CloudCompilerConfig {
71
+ provider: CompilerProvider
72
+ replicateApiKey: string
73
+ openaiApiKey: string
74
+ anthropicApiKey: string
75
+ openaiModel: string
76
+ anthropicModel: string
77
+ }
78
 
79
  export interface LocalConfig {
80
  camera_url: string | null
 
85
  default_image_size: number | null
86
  default_max_detections: number | null
87
  llm_provider: CompilerProvider | null
 
 
88
  replicate_model: string | null
89
  replicate_reasoning_effort: string | null
90
+ openai_model: string | null
91
+ anthropic_model: string | null
92
  }
93
 
94
  export interface DetectParams {
frontend/src/modules/rules/RuleStudioPanel.tsx CHANGED
@@ -2,8 +2,6 @@ import { useState } from "react"
2
  import {
3
  CheckCircle2,
4
  CircleSlash,
5
- Cloud,
6
- Cpu,
7
  Loader2,
8
  Save,
9
  ShieldCheck,
@@ -29,8 +27,7 @@ export function RuleStudioPanel() {
29
  save,
30
  setRuleEnabled,
31
  deleteRule,
32
- compilerProvider,
33
- setCompilerProvider,
34
  compile,
35
  compiling,
36
  } = useDashboard()
@@ -77,23 +74,9 @@ export function RuleStudioPanel() {
77
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
78
  <p className="text-xs text-muted-foreground">
79
  Describe the automation in plain language. It compiles to validated rules through{" "}
80
- {compilerProvider === "cloud" ? "Replicate" : "llama.cpp"} — never executable code.
81
  </p>
82
- <div className="flex shrink-0 items-center gap-2 rounded-md border border-border bg-black/20 px-3 py-2">
83
- {compilerProvider === "cloud" ? (
84
- <Cloud className="size-3.5 text-primary" />
85
- ) : (
86
- <Cpu className="size-3.5 text-muted-foreground" />
87
- )}
88
- <Label htmlFor="compiler-provider" className="text-xs text-foreground">
89
- Cloud
90
- </Label>
91
- <Switch
92
- id="compiler-provider"
93
- checked={compilerProvider === "cloud"}
94
- onCheckedChange={(checked) => setCompilerProvider(checked ? "cloud" : "local")}
95
- />
96
- </div>
97
  </div>
98
  <Textarea
99
  value={instruction}
@@ -105,9 +88,7 @@ export function RuleStudioPanel() {
105
  {compiling ? <Loader2 className="size-4 animate-spin" /> : <Wand2 className="size-4" />}
106
  {compiling
107
  ? "Compiling…"
108
- : compilerProvider === "cloud"
109
- ? "Compile with cloud"
110
- : "Compile to rules"}
111
  </Button>
112
  </div>
113
  </TabsContent>
 
2
  import {
3
  CheckCircle2,
4
  CircleSlash,
 
 
5
  Loader2,
6
  Save,
7
  ShieldCheck,
 
27
  save,
28
  setRuleEnabled,
29
  deleteRule,
30
+ compiler,
 
31
  compile,
32
  compiling,
33
  } = useDashboard()
 
74
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
75
  <p className="text-xs text-muted-foreground">
76
  Describe the automation in plain language. It compiles to validated rules through{" "}
77
+ {compiler.provider} — never executable code.
78
  </p>
79
+ <Badge variant="default">{compiler.provider}</Badge>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
  <Textarea
82
  value={instruction}
 
88
  {compiling ? <Loader2 className="size-4 animate-spin" /> : <Wand2 className="size-4" />}
89
  {compiling
90
  ? "Compiling…"
91
+ : `Compile with ${compiler.provider}`}
 
 
92
  </Button>
93
  </div>
94
  </TabsContent>
frontend/src/modules/settings/SettingsPanel.tsx CHANGED
@@ -2,12 +2,57 @@ import { AlertTriangle } from "lucide-react"
2
  import { useDashboard } from "@/lib/dashboard"
3
  import { Label } from "@/components/ui/label"
4
  import { Input } from "@/components/ui/input"
 
5
  import { Switch } from "@/components/ui/switch"
6
  import { Separator } from "@/components/ui/separator"
7
  import { cn } from "@/lib/utils"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  export function SettingsPanel() {
10
- const { params, setParam } = useDashboard()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  return (
13
  <div className="space-y-5">
@@ -56,17 +101,50 @@ export function SettingsPanel() {
56
  <div className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-muted-foreground">
57
  rule compiler
58
  </div>
59
- <p className="text-xs text-muted-foreground">
60
- llama.cpp endpoint:{" "}
61
- <span className="font-mono text-foreground">127.0.0.1:8080/v1</span>
62
- </p>
63
- <p className="text-[0.6875rem] text-muted-foreground">
64
- Start with{" "}
65
- <span className="font-mono">llama-server -hf ggml-org/Qwen3-1.7B-GGUF:Q4_K_M</span>
66
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  <p className="text-[0.6875rem] text-muted-foreground">
68
- Cloud compile uses <span className="font-mono">REPLICATE_API_TOKEN</span> or{" "}
69
- <span className="font-mono">.local/config.yaml</span>.
70
  </p>
71
  </div>
72
  </div>
 
2
  import { useDashboard } from "@/lib/dashboard"
3
  import { Label } from "@/components/ui/label"
4
  import { Input } from "@/components/ui/input"
5
+ import { Button } from "@/components/ui/button"
6
  import { Switch } from "@/components/ui/switch"
7
  import { Separator } from "@/components/ui/separator"
8
  import { cn } from "@/lib/utils"
9
+ import type { CompilerProvider } from "@/lib/types"
10
+
11
+ const PROVIDERS: { id: CompilerProvider; label: string; placeholder: string }[] = [
12
+ { id: "replicate", label: "Replicate", placeholder: "r8_..." },
13
+ { id: "openai", label: "OpenAI", placeholder: "sk-..." },
14
+ { id: "anthropic", label: "Claude", placeholder: "sk-ant-..." },
15
+ ]
16
+
17
+ const MODELS = {
18
+ openai: [
19
+ { value: "gpt-5.5", label: "GPT-5.5" },
20
+ { value: "gpt-5.4", label: "GPT-5.4" },
21
+ { value: "gpt-5.4-mini", label: "GPT-5.4 mini" },
22
+ ],
23
+ anthropic: [
24
+ { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
25
+ { value: "claude-opus-4-8", label: "Claude Opus 4.8" },
26
+ { value: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5" },
27
+ ],
28
+ }
29
 
30
  export function SettingsPanel() {
31
+ const {
32
+ params,
33
+ setParam,
34
+ compiler,
35
+ setCompilerProvider,
36
+ setCompilerApiKey,
37
+ setCompilerModel,
38
+ } = useDashboard()
39
+ const activeProvider = PROVIDERS.find((provider) => provider.id === compiler.provider) ?? PROVIDERS[0]
40
+ const apiKey =
41
+ compiler.provider === "replicate"
42
+ ? compiler.replicateApiKey
43
+ : compiler.provider === "openai"
44
+ ? compiler.openaiApiKey
45
+ : compiler.anthropicApiKey
46
+ const model =
47
+ compiler.provider === "openai"
48
+ ? compiler.openaiModel
49
+ : compiler.provider === "anthropic"
50
+ ? compiler.anthropicModel
51
+ : ""
52
+ const modelProvider =
53
+ compiler.provider === "openai" || compiler.provider === "anthropic"
54
+ ? compiler.provider
55
+ : null
56
 
57
  return (
58
  <div className="space-y-5">
 
101
  <div className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-muted-foreground">
102
  rule compiler
103
  </div>
104
+ <div className="grid grid-cols-3 gap-1 rounded-lg border border-border bg-black/20 p-1">
105
+ {PROVIDERS.map((provider) => (
106
+ <Button
107
+ key={provider.id}
108
+ type="button"
109
+ size="sm"
110
+ variant={compiler.provider === provider.id ? "default" : "ghost"}
111
+ onClick={() => setCompilerProvider(provider.id)}
112
+ >
113
+ {provider.label}
114
+ </Button>
115
+ ))}
116
+ </div>
117
+ <div className="space-y-1.5">
118
+ <Label htmlFor="compiler-api-key">{activeProvider.label} API key</Label>
119
+ <Input
120
+ id="compiler-api-key"
121
+ type="password"
122
+ value={apiKey}
123
+ onChange={(e) => setCompilerApiKey(compiler.provider, e.target.value)}
124
+ placeholder={activeProvider.placeholder}
125
+ className="font-mono text-xs"
126
+ />
127
+ </div>
128
+ {modelProvider && (
129
+ <div className="space-y-1.5">
130
+ <Label htmlFor="compiler-model">{activeProvider.label} model</Label>
131
+ <select
132
+ id="compiler-model"
133
+ value={model}
134
+ onChange={(e) => setCompilerModel(modelProvider, e.target.value)}
135
+ className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
136
+ >
137
+ {MODELS[modelProvider].map((option) => (
138
+ <option key={option.value} value={option.value}>
139
+ {option.label}
140
+ </option>
141
+ ))}
142
+ </select>
143
+ </div>
144
+ )}
145
  <p className="text-[0.6875rem] text-muted-foreground">
146
+ Keys pasted here are sent only when compiling. Space owners can also set provider
147
+ secrets as environment variables.
148
  </p>
149
  </div>
150
  </div>
poetry.lock CHANGED
@@ -24,6 +24,36 @@ files = [
24
  {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
25
  ]
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  [[package]]
28
  name = "anyio"
29
  version = "4.13.0"
@@ -860,17 +890,34 @@ docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
860
  tests = ["pytest", "pytest-cov", "pytest-xdist"]
861
 
862
  [[package]]
863
- name = "diskcache"
864
- version = "5.6.3"
865
- description = "Disk Cache -- Disk and file backed persistent cache."
866
  optional = false
867
- python-versions = ">=3"
868
  groups = ["main"]
869
  files = [
870
- {file = "diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19"},
871
- {file = "diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc"},
872
  ]
873
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
874
  [[package]]
875
  name = "docutils"
876
  version = "0.23"
@@ -1477,6 +1524,125 @@ MarkupSafe = ">=2.0"
1477
  [package.extras]
1478
  i18n = ["Babel (>=2.7)"]
1479
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1480
  [[package]]
1481
  name = "keyring"
1482
  version = "25.7.0"
@@ -1635,35 +1801,6 @@ files = [
1635
  {file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"},
1636
  ]
1637
 
1638
- [[package]]
1639
- name = "llama-cpp-python"
1640
- version = "0.3.28"
1641
- description = "Python bindings for the llama.cpp library"
1642
- optional = false
1643
- python-versions = ">=3.8"
1644
- groups = ["main"]
1645
- files = [
1646
- {file = "llama_cpp_python-0.3.28.tar.gz", hash = "sha256:958227b394f413425d6039952096daa0b8b98328c6b99d652862aec775f1672d"},
1647
- ]
1648
-
1649
- [package.dependencies]
1650
- diskcache = ">=5.6.1"
1651
- fastapi = {version = ">=0.100.0", optional = true, markers = "extra == \"server\""}
1652
- jinja2 = ">=2.11.3"
1653
- numpy = ">=1.20.0"
1654
- pydantic-settings = {version = ">=2.0.1", optional = true, markers = "extra == \"server\""}
1655
- PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"server\""}
1656
- sse-starlette = {version = ">=1.6.1", optional = true, markers = "extra == \"server\""}
1657
- starlette-context = {version = ">=0.3.6,<0.4", optional = true, markers = "extra == \"server\""}
1658
- typing-extensions = ">=4.5.0"
1659
- uvicorn = {version = ">=0.22.0", optional = true, markers = "extra == \"server\""}
1660
-
1661
- [package.extras]
1662
- all = ["llama_cpp_python[dev,server,test]"]
1663
- dev = ["httpx (>=0.24.1)", "mkdocs (>=1.4.3)", "mkdocs-material (>=9.1.18)", "mkdocstrings[python] (>=0.22.0)", "pytest (>=7.4.0)", "ruff (>=0.15.7)", "twine (>=4.0.2)"]
1664
- server = ["PyYAML (>=5.1)", "fastapi (>=0.100.0)", "pydantic-settings (>=2.0.1)", "sse-starlette (>=1.6.1)", "starlette-context (>=0.3.6,<0.4)", "uvicorn (>=0.22.0)"]
1665
- test = ["fastapi (>=0.100.0)", "httpx (>=0.24.1)", "huggingface-hub (>=0.23.0)", "pydantic-settings (>=2.0.1)", "pytest (>=7.4.0)", "scipy (>=1.10)", "sse-starlette (>=1.6.1)", "starlette-context (>=0.3.6,<0.4)"]
1666
-
1667
  [[package]]
1668
  name = "markdown-it-py"
1669
  version = "4.2.0"
@@ -2442,6 +2579,34 @@ files = [
2442
  {file = "nvidia_nvtx-13.0.85-py3-none-win_amd64.whl", hash = "sha256:d66ea44254dd3c6eacc300047af6e1288d2269dd072b417e0adffbf479e18519"},
2443
  ]
2444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2445
  [[package]]
2446
  name = "opencv-python"
2447
  version = "4.13.0.92"
@@ -3173,30 +3338,6 @@ files = [
3173
  [package.dependencies]
3174
  typing-extensions = ">=4.14.1"
3175
 
3176
- [[package]]
3177
- name = "pydantic-settings"
3178
- version = "2.14.1"
3179
- description = "Settings management using Pydantic"
3180
- optional = false
3181
- python-versions = ">=3.10"
3182
- groups = ["main"]
3183
- files = [
3184
- {file = "pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de"},
3185
- {file = "pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa"},
3186
- ]
3187
-
3188
- [package.dependencies]
3189
- pydantic = ">=2.7.0"
3190
- python-dotenv = ">=0.21.0"
3191
- typing-inspection = ">=0.4.0"
3192
-
3193
- [package.extras]
3194
- aws-secrets-manager = ["boto3 (>=1.35.0)", "types-boto3[secretsmanager]"]
3195
- azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
3196
- gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
3197
- toml = ["tomli (>=2.0.1)"]
3198
- yaml = ["pyyaml (>=6.0.1)"]
3199
-
3200
  [[package]]
3201
  name = "pydub"
3202
  version = "0.25.1"
@@ -3278,21 +3419,6 @@ files = [
3278
  [package.dependencies]
3279
  six = ">=1.5"
3280
 
3281
- [[package]]
3282
- name = "python-dotenv"
3283
- version = "1.2.2"
3284
- description = "Read key-value pairs from a .env file and set them as environment variables"
3285
- optional = false
3286
- python-versions = ">=3.10"
3287
- groups = ["main"]
3288
- files = [
3289
- {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
3290
- {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
3291
- ]
3292
-
3293
- [package.extras]
3294
- cli = ["click (>=5.0)"]
3295
-
3296
  [[package]]
3297
  name = "python-multipart"
3298
  version = "0.0.32"
@@ -3764,28 +3890,17 @@ files = [
3764
  ]
3765
 
3766
  [[package]]
3767
- name = "sse-starlette"
3768
- version = "3.4.4"
3769
- description = "SSE plugin for Starlette"
3770
  optional = false
3771
- python-versions = ">=3.10"
3772
  groups = ["main"]
3773
  files = [
3774
- {file = "sse_starlette-3.4.4-py3-none-any.whl", hash = "sha256:3f4dd50d8aed2771a091f3a83000323fc3844541c16b4fe585ae2420cc6df973"},
3775
- {file = "sse_starlette-3.4.4.tar.gz", hash = "sha256:07e0fa0460138baf25cdd5fb28683472c3995dc1642225191b3832d62526bcb0"},
3776
  ]
3777
 
3778
- [package.dependencies]
3779
- anyio = ">=4.7.0"
3780
- starlette = ">=0.49.1"
3781
-
3782
- [package.extras]
3783
- daphne = ["daphne (>=4.2.0)"]
3784
- examples = ["fastapi (>=0.115.12)", "pydantic (>=2)", "uvicorn (>=0.34.0)"]
3785
- examples-db = ["aiosqlite (>=0.21.0)", "sqlalchemy[asyncio] (>=2.0.41)"]
3786
- granian = ["granian (>=2.3.1)"]
3787
- uvicorn = ["uvicorn (>=0.34.0)"]
3788
-
3789
  [[package]]
3790
  name = "starlette"
3791
  version = "1.3.1"
@@ -3805,21 +3920,6 @@ typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""
3805
  [package.extras]
3806
  full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
3807
 
3808
- [[package]]
3809
- name = "starlette-context"
3810
- version = "0.3.6"
3811
- description = "Middleware for Starlette that allows you to store and access the context data of a request. Can be used with logging so logs automatically use request headers such as x-request-id or x-correlation-id."
3812
- optional = false
3813
- python-versions = ">=3.8,<4.0"
3814
- groups = ["main"]
3815
- files = [
3816
- {file = "starlette_context-0.3.6-py3-none-any.whl", hash = "sha256:b14ce373fbb6895a2182a7104b9f63ba20c8db83444005fb9a844dd77ad9895c"},
3817
- {file = "starlette_context-0.3.6.tar.gz", hash = "sha256:d361a36ba2d4acca3ab680f917b25e281533d725374752d47607a859041958cb"},
3818
- ]
3819
-
3820
- [package.dependencies]
3821
- starlette = "*"
3822
-
3823
  [[package]]
3824
  name = "static-ffmpeg"
3825
  version = "3.0"
@@ -4331,4 +4431,4 @@ type = ["pytest-mypy (>=1.0.1) ; platform_python_implementation != \"PyPy\""]
4331
  [metadata]
4332
  lock-version = "2.1"
4333
  python-versions = ">=3.10,!=3.14.1,<3.15"
4334
- content-hash = "b136468f5b1d7e59f642fdb59b4cd8e9a940b6a70a13feb0b54c049f08dd656d"
 
24
  {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
25
  ]
26
 
27
+ [[package]]
28
+ name = "anthropic"
29
+ version = "0.109.1"
30
+ description = "The official Python library for the anthropic API"
31
+ optional = false
32
+ python-versions = ">=3.9"
33
+ groups = ["main"]
34
+ files = [
35
+ {file = "anthropic-0.109.1-py3-none-any.whl", hash = "sha256:ce7d94a7657f2aa29338cca448945eac621b4f62c1794cf461cb32847223e9b8"},
36
+ {file = "anthropic-0.109.1.tar.gz", hash = "sha256:83e06b3d9d40ff5898f588020e0cc4e42187de954549a3b5fbe6e2685a09c785"},
37
+ ]
38
+
39
+ [package.dependencies]
40
+ anyio = ">=3.5.0,<5"
41
+ distro = ">=1.7.0,<2"
42
+ docstring-parser = ">=0.15,<1"
43
+ httpx = ">=0.25.0,<1"
44
+ jiter = ">=0.4.0,<1"
45
+ pydantic = ">=1.9.0,<3"
46
+ sniffio = "*"
47
+ typing-extensions = ">=4.14,<5"
48
+
49
+ [package.extras]
50
+ aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"]
51
+ aws = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"]
52
+ bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"]
53
+ mcp = ["mcp (>=1.0) ; python_version >= \"3.10\""]
54
+ vertex = ["google-auth[requests] (>=2,<3)"]
55
+ webhooks = ["standardwebhooks (>=1.0.1,<2)"]
56
+
57
  [[package]]
58
  name = "anyio"
59
  version = "4.13.0"
 
890
  tests = ["pytest", "pytest-cov", "pytest-xdist"]
891
 
892
  [[package]]
893
+ name = "distro"
894
+ version = "1.9.0"
895
+ description = "Distro - an OS platform information API"
896
  optional = false
897
+ python-versions = ">=3.6"
898
  groups = ["main"]
899
  files = [
900
+ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"},
901
+ {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
902
  ]
903
 
904
+ [[package]]
905
+ name = "docstring-parser"
906
+ version = "0.18.0"
907
+ description = "Parse Python docstrings in reST, Google and Numpydoc format"
908
+ optional = false
909
+ python-versions = ">=3.8"
910
+ groups = ["main"]
911
+ files = [
912
+ {file = "docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b"},
913
+ {file = "docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015"},
914
+ ]
915
+
916
+ [package.extras]
917
+ dev = ["pre-commit (>=2.16.0) ; python_version >= \"3.9\"", "pydoctor (>=25.4.0)", "pytest"]
918
+ docs = ["pydoctor (>=25.4.0)"]
919
+ test = ["pytest"]
920
+
921
  [[package]]
922
  name = "docutils"
923
  version = "0.23"
 
1524
  [package.extras]
1525
  i18n = ["Babel (>=2.7)"]
1526
 
1527
+ [[package]]
1528
+ name = "jiter"
1529
+ version = "0.15.0"
1530
+ description = "Fast iterable JSON parser."
1531
+ optional = false
1532
+ python-versions = ">=3.9"
1533
+ groups = ["main"]
1534
+ files = [
1535
+ {file = "jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4"},
1536
+ {file = "jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f"},
1537
+ {file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18"},
1538
+ {file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f"},
1539
+ {file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4"},
1540
+ {file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6"},
1541
+ {file = "jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c"},
1542
+ {file = "jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512"},
1543
+ {file = "jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a"},
1544
+ {file = "jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887"},
1545
+ {file = "jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823"},
1546
+ {file = "jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53"},
1547
+ {file = "jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1"},
1548
+ {file = "jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2"},
1549
+ {file = "jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67"},
1550
+ {file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a"},
1551
+ {file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7"},
1552
+ {file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd"},
1553
+ {file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281"},
1554
+ {file = "jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708"},
1555
+ {file = "jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928"},
1556
+ {file = "jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd"},
1557
+ {file = "jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e"},
1558
+ {file = "jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef"},
1559
+ {file = "jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32"},
1560
+ {file = "jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04"},
1561
+ {file = "jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865"},
1562
+ {file = "jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d"},
1563
+ {file = "jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0"},
1564
+ {file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138"},
1565
+ {file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61"},
1566
+ {file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687"},
1567
+ {file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879"},
1568
+ {file = "jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d"},
1569
+ {file = "jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb"},
1570
+ {file = "jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871"},
1571
+ {file = "jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77"},
1572
+ {file = "jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d"},
1573
+ {file = "jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d"},
1574
+ {file = "jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7"},
1575
+ {file = "jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b"},
1576
+ {file = "jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3"},
1577
+ {file = "jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5"},
1578
+ {file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279"},
1579
+ {file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4"},
1580
+ {file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258"},
1581
+ {file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894"},
1582
+ {file = "jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45"},
1583
+ {file = "jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29"},
1584
+ {file = "jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b"},
1585
+ {file = "jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7"},
1586
+ {file = "jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712"},
1587
+ {file = "jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c"},
1588
+ {file = "jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0"},
1589
+ {file = "jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba"},
1590
+ {file = "jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8"},
1591
+ {file = "jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c"},
1592
+ {file = "jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4"},
1593
+ {file = "jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b"},
1594
+ {file = "jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7"},
1595
+ {file = "jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49"},
1596
+ {file = "jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86"},
1597
+ {file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f"},
1598
+ {file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e"},
1599
+ {file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6"},
1600
+ {file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9"},
1601
+ {file = "jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c"},
1602
+ {file = "jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd"},
1603
+ {file = "jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89"},
1604
+ {file = "jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554"},
1605
+ {file = "jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a"},
1606
+ {file = "jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec"},
1607
+ {file = "jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558"},
1608
+ {file = "jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866"},
1609
+ {file = "jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d"},
1610
+ {file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6"},
1611
+ {file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995"},
1612
+ {file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8"},
1613
+ {file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5"},
1614
+ {file = "jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b"},
1615
+ {file = "jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8"},
1616
+ {file = "jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec"},
1617
+ {file = "jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e"},
1618
+ {file = "jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5"},
1619
+ {file = "jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52"},
1620
+ {file = "jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854"},
1621
+ {file = "jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0"},
1622
+ {file = "jiter-0.15.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:04b400bbf8c9efb03d9bdd976475c919c1d85593b04b9fff7ae234065daf87ae"},
1623
+ {file = "jiter-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25ffbe229aa8cd98c28879d8aa1a6e34ae77992ab984a65fba800859dab16269"},
1624
+ {file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5607e6013ed7e6b0ec9661e467b7ffde0aa7ab36833a04850f26fcf88ed4845b"},
1625
+ {file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50164d7610c00e7cd913a873fce30b6beeebf4b37e53983e33f22de4c900f6b8"},
1626
+ {file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab596fa3837e91e7e6a31b5f639988bfc6a35d1f915ac3932d946062219d588f"},
1627
+ {file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72d8af5c1013656a8870c866660627d1a75bc185814ee022c8533caa1de88ae"},
1628
+ {file = "jiter-0.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c84c1b7be454b0c16f8499b4ebfbfd82ea5cca6527cceefcbbc06a7557b5ed2e"},
1629
+ {file = "jiter-0.15.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:d636d5095155afd364247f65070fab7beda13498d7ff4de331046e704ab9657f"},
1630
+ {file = "jiter-0.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d3d6683288c11cbab50e865f2e2f13950179aa45410e30b2cfbd3fb7b0177bf"},
1631
+ {file = "jiter-0.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7ce8902f939970048b233087082e7bb829db29375811c7ad50687b8624c6fd08"},
1632
+ {file = "jiter-0.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4363818355dbc70ae1a8e9eaba9de350d93ede4ff6992b8f8eb8cbb6e5122d42"},
1633
+ {file = "jiter-0.15.0-cp39-cp39-win32.whl", hash = "sha256:8f7e9bc0f1135039b22ee6eab588d42df1ce55842b30740a352885eb267bd941"},
1634
+ {file = "jiter-0.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:1c15024a3d892223b18f597c86d59387249dc396590844ce6b9f6131d1093bae"},
1635
+ {file = "jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750"},
1636
+ {file = "jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b"},
1637
+ {file = "jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b"},
1638
+ {file = "jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c"},
1639
+ {file = "jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0"},
1640
+ {file = "jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45"},
1641
+ {file = "jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c"},
1642
+ {file = "jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a"},
1643
+ {file = "jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76"},
1644
+ ]
1645
+
1646
  [[package]]
1647
  name = "keyring"
1648
  version = "25.7.0"
 
1801
  {file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"},
1802
  ]
1803
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1804
  [[package]]
1805
  name = "markdown-it-py"
1806
  version = "4.2.0"
 
2579
  {file = "nvidia_nvtx-13.0.85-py3-none-win_amd64.whl", hash = "sha256:d66ea44254dd3c6eacc300047af6e1288d2269dd072b417e0adffbf479e18519"},
2580
  ]
2581
 
2582
+ [[package]]
2583
+ name = "openai"
2584
+ version = "2.41.1"
2585
+ description = "The official Python library for the openai API"
2586
+ optional = false
2587
+ python-versions = ">=3.9"
2588
+ groups = ["main"]
2589
+ files = [
2590
+ {file = "openai-2.41.1-py3-none-any.whl", hash = "sha256:a939565f350cb7443cb843b801b88c716ac8024b492fb94ca269d5f6b1bbefd6"},
2591
+ {file = "openai-2.41.1.tar.gz", hash = "sha256:23d617a0432457ad844973bee8f540be9da90894f7c5686852d2d365da058f57"},
2592
+ ]
2593
+
2594
+ [package.dependencies]
2595
+ anyio = ">=3.5.0,<5"
2596
+ distro = ">=1.7.0,<2"
2597
+ httpx = ">=0.23.0,<1"
2598
+ jiter = ">=0.10.0,<1"
2599
+ pydantic = ">=1.9.0,<3"
2600
+ sniffio = "*"
2601
+ tqdm = ">4"
2602
+ typing-extensions = ">=4.14,<5"
2603
+
2604
+ [package.extras]
2605
+ aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.9)"]
2606
+ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"]
2607
+ realtime = ["websockets (>=13,<16)"]
2608
+ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
2609
+
2610
  [[package]]
2611
  name = "opencv-python"
2612
  version = "4.13.0.92"
 
3338
  [package.dependencies]
3339
  typing-extensions = ">=4.14.1"
3340
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3341
  [[package]]
3342
  name = "pydub"
3343
  version = "0.25.1"
 
3419
  [package.dependencies]
3420
  six = ">=1.5"
3421
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3422
  [[package]]
3423
  name = "python-multipart"
3424
  version = "0.0.32"
 
3890
  ]
3891
 
3892
  [[package]]
3893
+ name = "sniffio"
3894
+ version = "1.3.1"
3895
+ description = "Sniff out which async library your code is running under"
3896
  optional = false
3897
+ python-versions = ">=3.7"
3898
  groups = ["main"]
3899
  files = [
3900
+ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
3901
+ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
3902
  ]
3903
 
 
 
 
 
 
 
 
 
 
 
 
3904
  [[package]]
3905
  name = "starlette"
3906
  version = "1.3.1"
 
3920
  [package.extras]
3921
  full = ["httpx (>=0.27.0,<0.29.0)", "httpx2 (>=2.0.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
3922
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3923
  [[package]]
3924
  name = "static-ffmpeg"
3925
  version = "3.0"
 
4431
  [metadata]
4432
  lock-version = "2.1"
4433
  python-versions = ">=3.10,!=3.14.1,<3.15"
4434
+ content-hash = "a7600cee02a0c3ea4a71e855778accdc7e1e1272a3e3a42cfff5744a6adbc455"
pyproject.toml CHANGED
@@ -1,7 +1,7 @@
1
  [project]
2
  name = "tiny-trigger"
3
  version = "0.1.0"
4
- description = "Open-vocabulary video automations with YOLOE and llama.cpp"
5
  authors = [
6
  {name = "Javier Montalvo",email = "jmrgua@gmail.com"}
7
  ]
@@ -15,13 +15,14 @@ dependencies = [
15
  "PyYAML>=6.0",
16
  "requests>=2.32",
17
  "replicate>=1.0",
 
 
18
  "numpy>=1.26",
19
  "imageio-ffmpeg>=0.6.0",
20
  "static-ffmpeg>=2.13",
21
  "torch (>=2.11.0,<3.0.0)",
22
  "torchvision (>=0.26.0,<0.27.0)",
23
  "torchaudio (>=2.11.0,<3.0.0)",
24
- "llama-cpp-python[server] (>=0.3.28,<0.4.0)",
25
  ]
26
 
27
  [tool.poetry.group.dev.dependencies]
 
1
  [project]
2
  name = "tiny-trigger"
3
  version = "0.1.0"
4
+ description = "Open-vocabulary video automations with YOLOE and cloud rule compilers"
5
  authors = [
6
  {name = "Javier Montalvo",email = "jmrgua@gmail.com"}
7
  ]
 
15
  "PyYAML>=6.0",
16
  "requests>=2.32",
17
  "replicate>=1.0",
18
+ "openai>=2.0",
19
+ "anthropic>=0.75",
20
  "numpy>=1.26",
21
  "imageio-ffmpeg>=0.6.0",
22
  "static-ffmpeg>=2.13",
23
  "torch (>=2.11.0,<3.0.0)",
24
  "torchvision (>=0.26.0,<0.27.0)",
25
  "torchaudio (>=2.11.0,<3.0.0)",
 
26
  ]
27
 
28
  [tool.poetry.group.dev.dependencies]
requirements.txt CHANGED
@@ -5,4 +5,6 @@ pydantic>=2.8
5
  PyYAML>=6.0
6
  requests>=2.32
7
  replicate>=1.0
 
 
8
  numpy>=1.26
 
5
  PyYAML>=6.0
6
  requests>=2.32
7
  replicate>=1.0
8
+ openai>=2.0
9
+ anthropic>=0.75
10
  numpy>=1.26
server.py CHANGED
@@ -1,7 +1,6 @@
1
  """gradio.Server backend for the custom Tiny Trigger dashboard.
2
 
3
- Runs independently of ``app.py`` (the legacy Gradio Blocks UI). It serves the
4
- built React frontend (``frontend/dist``) and exposes the Tiny Trigger engine as
5
  ``@app.api`` endpoints that keep Gradio's queuing / SSE / gradio_client
6
  compatibility. The frontend talks to these via the ``@gradio/client`` JS library.
7
 
@@ -21,7 +20,11 @@ from gradio import FileData, Server
21
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
22
 
23
  from tiny_trigger import (
24
- compile_automation_with_llamacpp,
 
 
 
 
25
  compile_automation_with_replicate,
26
  evaluate_video_detections,
27
  load_automation_text,
@@ -176,10 +179,10 @@ def compile_rules(
176
  classes: str = "",
177
  existing_rules_text: str = "",
178
  append: bool = True,
179
- provider: str = "local",
180
- base_url: str = "http://127.0.0.1:8080/v1",
181
- model: str = "ggml-org/Qwen3-1.7B-GGUF:Q4_K_M",
182
- replicate_model: str = "openai/gpt-5.2",
183
  replicate_reasoning_effort: str = "medium",
184
  ) -> dict:
185
  """Compile a natural-language request into validated automation rules."""
@@ -188,26 +191,41 @@ def compile_rules(
188
  existing = load_automation_text(existing_rules_text)
189
  class_names = _merge_class_names(class_names, document_labels(existing))
190
  cfg = load_local_config()
191
- if provider == "cloud":
192
- api_token = os.environ.get("REPLICATE_API_TOKEN") or cfg.replicate_api_token
193
  if not api_token:
194
- raise ValueError("Set REPLICATE_API_TOKEN or replicate_api_token in .local/config.yaml.")
195
  compiled = compile_automation_with_replicate(
196
  instruction=instruction,
197
  class_names=class_names,
198
  api_token=api_token,
199
- model=replicate_model or cfg.replicate_model or "openai/gpt-5.2",
200
  reasoning_effort=(
201
  replicate_reasoning_effort or cfg.replicate_reasoning_effort or "medium"
202
  ),
203
  )
204
- else:
205
- compiled = compile_automation_with_llamacpp(
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  instruction=instruction,
207
  class_names=class_names,
208
- base_url=base_url or cfg.llamacpp_base_url or "http://127.0.0.1:8080/v1",
209
- model=model or cfg.llamacpp_model or "ggml-org/Qwen3-1.7B-GGUF:Q4_K_M",
210
  )
 
 
211
  document = compiled.document
212
  if append and existing_rules_text.strip():
213
  existing = load_automation_text(existing_rules_text)
@@ -296,7 +314,7 @@ def load_rules() -> dict:
296
  @app.api(name="get_config")
297
  def get_config() -> dict:
298
  cfg = load_local_config()
299
- return cfg.model_dump(exclude={"replicate_api_token"})
300
 
301
 
302
  def _merge_documents(existing: AutomationDocument, compiled: AutomationDocument) -> AutomationDocument:
@@ -361,4 +379,8 @@ def media(file_path: str) -> Any:
361
 
362
 
363
  if __name__ == "__main__":
364
- app.launch(server_name="127.0.0.1", server_port=7860, show_error=True)
 
 
 
 
 
1
  """gradio.Server backend for the custom Tiny Trigger dashboard.
2
 
3
+ Serves the built React frontend (``frontend/dist``) and exposes the Tiny Trigger engine as
 
4
  ``@app.api`` endpoints that keep Gradio's queuing / SSE / gradio_client
5
  compatibility. The frontend talks to these via the ``@gradio/client`` JS library.
6
 
 
20
  from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
21
 
22
  from tiny_trigger import (
23
+ DEFAULT_ANTHROPIC_MODEL,
24
+ DEFAULT_OPENAI_MODEL,
25
+ DEFAULT_REPLICATE_MODEL,
26
+ compile_automation_with_anthropic,
27
+ compile_automation_with_openai,
28
  compile_automation_with_replicate,
29
  evaluate_video_detections,
30
  load_automation_text,
 
179
  classes: str = "",
180
  existing_rules_text: str = "",
181
  append: bool = True,
182
+ provider: str = "anthropic",
183
+ api_key: str = "",
184
+ model: str = "",
185
+ replicate_model: str = DEFAULT_REPLICATE_MODEL,
186
  replicate_reasoning_effort: str = "medium",
187
  ) -> dict:
188
  """Compile a natural-language request into validated automation rules."""
 
191
  existing = load_automation_text(existing_rules_text)
192
  class_names = _merge_class_names(class_names, document_labels(existing))
193
  cfg = load_local_config()
194
+ if provider == "replicate":
195
+ api_token = api_key or os.environ.get("REPLICATE_API_TOKEN") or cfg.replicate_api_token
196
  if not api_token:
197
+ raise ValueError("Paste a Replicate API token or set REPLICATE_API_TOKEN.")
198
  compiled = compile_automation_with_replicate(
199
  instruction=instruction,
200
  class_names=class_names,
201
  api_token=api_token,
202
+ model=model or replicate_model or cfg.replicate_model or DEFAULT_REPLICATE_MODEL,
203
  reasoning_effort=(
204
  replicate_reasoning_effort or cfg.replicate_reasoning_effort or "medium"
205
  ),
206
  )
207
+ elif provider == "openai":
208
+ openai_key = api_key or os.environ.get("OPENAI_API_KEY") or cfg.openai_api_key
209
+ if not openai_key:
210
+ raise ValueError("Paste an OpenAI API key or set OPENAI_API_KEY.")
211
+ compiled = compile_automation_with_openai(
212
+ instruction=instruction,
213
+ class_names=class_names,
214
+ api_key=openai_key,
215
+ model=model or cfg.openai_model or DEFAULT_OPENAI_MODEL,
216
+ )
217
+ elif provider in {"anthropic", "claude"}:
218
+ anthropic_key = api_key or os.environ.get("ANTHROPIC_API_KEY") or cfg.anthropic_api_key
219
+ if not anthropic_key:
220
+ raise ValueError("Paste an Anthropic API key or set ANTHROPIC_API_KEY.")
221
+ compiled = compile_automation_with_anthropic(
222
  instruction=instruction,
223
  class_names=class_names,
224
+ api_key=anthropic_key,
225
+ model=model or cfg.anthropic_model or DEFAULT_ANTHROPIC_MODEL,
226
  )
227
+ else:
228
+ raise ValueError("Provider must be replicate, openai, or anthropic.")
229
  document = compiled.document
230
  if append and existing_rules_text.strip():
231
  existing = load_automation_text(existing_rules_text)
 
314
  @app.api(name="get_config")
315
  def get_config() -> dict:
316
  cfg = load_local_config()
317
+ return cfg.model_dump(exclude={"replicate_api_token", "openai_api_key", "anthropic_api_key"})
318
 
319
 
320
  def _merge_documents(existing: AutomationDocument, compiled: AutomationDocument) -> AutomationDocument:
 
379
 
380
 
381
  if __name__ == "__main__":
382
+ app.launch(
383
+ server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
384
+ server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")),
385
+ show_error=True,
386
+ )
tests/test_automation.py CHANGED
@@ -269,6 +269,35 @@ def test_change_trigger_fires_enter_and_exit() -> None:
269
  assert [event.action for event in exit_events] == ["turn off lights"]
270
 
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  def test_video_evaluation_uses_empty_frames_for_exit_triggers() -> None:
273
  document = load_automation_text(
274
  json.dumps(
 
269
  assert [event.action for event in exit_events] == ["turn off lights"]
270
 
271
 
272
+ def test_exit_trigger_accepts_plain_then_actions() -> None:
273
+ document = load_automation_text(
274
+ json.dumps(
275
+ {
276
+ "rules": [
277
+ {
278
+ "name": "person-leaves-turn-off-lights",
279
+ "when": {"all": [{"present": {"label": "person", "min_count": 1}}]},
280
+ "trigger": {"on": "exit"},
281
+ "gate": {"enabled": True},
282
+ "then": [{"type": "simulate", "name": "turn off lights"}],
283
+ }
284
+ ]
285
+ }
286
+ )
287
+ )
288
+ engine = RuleEngine(document.rules)
289
+
290
+ first_events = engine.evaluate_frame(
291
+ [detection("person", (0.1, 0.1, 0.2, 0.2))],
292
+ frame_index=0,
293
+ timestamp_sec=0.0,
294
+ )
295
+ exit_events = engine.evaluate_frame([], frame_index=1, timestamp_sec=1.0)
296
+
297
+ assert first_events == []
298
+ assert [event.action for event in exit_events] == ["turn off lights"]
299
+
300
+
301
  def test_video_evaluation_uses_empty_frames_for_exit_triggers() -> None:
302
  document = load_automation_text(
303
  json.dumps(
tests/test_llm.py CHANGED
@@ -5,12 +5,17 @@ import sys
5
 
6
  from tiny_trigger.automation import AutomationDocument
7
  from tiny_trigger.llm import (
 
 
8
  SYSTEM_PROMPT,
 
9
  _build_user_prompt,
10
  _chat_payload,
 
 
 
 
11
  compile_automation_with_replicate,
12
- compile_automation_with_llamacpp,
13
- _post_chat_completion,
14
  _validate_compile_result,
15
  extract_json_object,
16
  )
@@ -26,7 +31,7 @@ def test_extract_json_object_from_fenced_response() -> None:
26
  assert document.rules[0].name == "notify"
27
 
28
 
29
- def test_llamacpp_payload_uses_json_object() -> None:
30
  payload = _chat_payload(model="qwen", user_prompt="compile this", response_format="json_object")
31
 
32
  assert payload["response_format"] == {"type": "json_object"}
@@ -58,6 +63,15 @@ def test_prompt_teaches_near_relations() -> None:
58
  assert '"gate": {"enabled": true, "cooldown": {"key": "package-at-door", "minutes": 15}}' in prompt
59
 
60
 
 
 
 
 
 
 
 
 
 
61
  def test_invalid_empty_object_fails_validation() -> None:
62
  try:
63
  _validate_compile_result("{}")
@@ -67,94 +81,252 @@ def test_invalid_empty_object_fails_validation() -> None:
67
  raise AssertionError("{} should not validate as an automation document")
68
 
69
 
70
- def test_post_chat_completion_extracts_content() -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  class Response:
72
  def raise_for_status(self) -> None:
73
  return None
74
 
75
  def json(self):
76
- return {"choices": [{"message": {"content": "{\"rules\": []}"}}]}
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  class Requests:
79
- def post(self, endpoint, json, timeout):
80
- assert endpoint == "http://localhost/v1/chat/completions"
 
 
 
81
  assert json["response_format"] == {"type": "json_object"}
82
- assert timeout == 1
83
  return Response()
84
 
85
- assert (
86
- _post_chat_completion(
87
- endpoint="http://localhost/v1/chat/completions",
88
- model="qwen",
89
- user_prompt="compile",
90
- timeout=1,
91
- requests_module=Requests(),
92
- )
93
- == "{\"rules\": []}"
94
  )
95
 
 
 
 
 
 
 
 
 
 
96
 
97
- def test_compile_retries_after_invalid_response(monkeypatch) -> None:
98
  class Response:
99
- def __init__(self, content: str) -> None:
100
- self.content = content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  def raise_for_status(self) -> None:
103
  return None
104
 
105
  def json(self):
106
- return {"choices": [{"message": {"content": self.content}}]}
 
 
 
 
 
 
 
 
 
 
107
 
108
  class Requests:
109
- calls = 0
110
-
111
- @classmethod
112
- def post(cls, endpoint, json, timeout):
113
- cls.calls += 1
114
- if cls.calls == 1:
115
- return Response("{}")
116
- return Response(
117
- '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},'
118
- '"then":[{"type":"simulate","name":"notify"}]}]}'
119
- )
120
 
121
  monkeypatch.setitem(sys.modules, "requests", Requests)
122
 
123
- result = compile_automation_with_llamacpp(
124
  instruction="if person present notify",
125
  class_names=["person"],
126
- base_url="http://localhost/v1",
127
- model="qwen",
128
- timeout=1,
129
  )
130
 
131
- assert Requests.calls == 2
132
  assert result.document.rules[0].name == "notify"
133
 
134
 
135
- def test_replicate_compile_uses_stream_api(monkeypatch) -> None:
136
- class Replicate:
137
- class Client:
138
- def __init__(self, api_token):
139
- assert api_token == "test-token"
140
 
141
- def stream(self, model, input):
142
- assert model == "openai/gpt-5.2"
143
- assert input["messages"] == []
144
- assert input["verbosity"] == "medium"
145
- assert input["reasoning_effort"] == "low"
146
- assert "Return only the JSON object." in input["prompt"]
147
- yield '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},'
148
- yield '"then":[{"type":"simulate","name":"notify"}]}]}'
149
 
150
- monkeypatch.setitem(sys.modules, "replicate", Replicate)
 
 
 
 
 
151
 
152
- result = compile_automation_with_replicate(
153
- instruction="if person present notify",
154
- class_names=["person"],
155
- api_token="test-token",
156
- model="openai/gpt-5.2",
157
- reasoning_effort="low",
 
 
 
 
 
 
 
 
 
 
158
  )
159
 
160
- assert result.document.rules[0].name == "notify"
 
5
 
6
  from tiny_trigger.automation import AutomationDocument
7
  from tiny_trigger.llm import (
8
+ DEFAULT_ANTHROPIC_MODEL,
9
+ DEFAULT_OPENAI_MODEL,
10
  SYSTEM_PROMPT,
11
+ _anthropic_message,
12
  _build_user_prompt,
13
  _chat_payload,
14
+ _clean_api_key,
15
+ _openai_chat_completion,
16
+ compile_automation_with_anthropic,
17
+ compile_automation_with_openai,
18
  compile_automation_with_replicate,
 
 
19
  _validate_compile_result,
20
  extract_json_object,
21
  )
 
31
  assert document.rules[0].name == "notify"
32
 
33
 
34
+ def test_openai_payload_uses_json_object() -> None:
35
  payload = _chat_payload(model="qwen", user_prompt="compile this", response_format="json_object")
36
 
37
  assert payload["response_format"] == {"type": "json_object"}
 
63
  assert '"gate": {"enabled": true, "cooldown": {"key": "package-at-door", "minutes": 15}}' in prompt
64
 
65
 
66
+ def test_api_key_rejects_pasted_traceback() -> None:
67
+ try:
68
+ _clean_api_key(' File "/tmp/example.py", line 1\nrequests.exceptions.HTTPError', "Anthropic")
69
+ except ValueError as exc:
70
+ assert "not an API key" in str(exc) or "single-line token" in str(exc)
71
+ else:
72
+ raise AssertionError("Tracebacks should not be accepted as API keys")
73
+
74
+
75
  def test_invalid_empty_object_fails_validation() -> None:
76
  try:
77
  _validate_compile_result("{}")
 
81
  raise AssertionError("{} should not validate as an automation document")
82
 
83
 
84
+ def test_replicate_compile_uses_stream_api(monkeypatch) -> None:
85
+ class Replicate:
86
+ class Client:
87
+ def __init__(self, api_token):
88
+ assert api_token == "test-token"
89
+
90
+ def stream(self, model, input):
91
+ assert model == "openai/gpt-5.2"
92
+ assert input["messages"] == []
93
+ assert input["verbosity"] == "medium"
94
+ assert input["reasoning_effort"] == "low"
95
+ assert "Return only the JSON object." in input["prompt"]
96
+ yield '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},'
97
+ yield '"then":[{"type":"simulate","name":"notify"}]}]}'
98
+
99
+ monkeypatch.setitem(sys.modules, "replicate", Replicate)
100
+
101
+ result = compile_automation_with_replicate(
102
+ instruction="if person present notify",
103
+ class_names=["person"],
104
+ api_token="test-token",
105
+ model="openai/gpt-5.2",
106
+ reasoning_effort="low",
107
+ )
108
+
109
+ assert result.document.rules[0].name == "notify"
110
+
111
+
112
+ def test_replicate_invalid_json_does_not_auto_repair(monkeypatch) -> None:
113
+ class Replicate:
114
+ class Client:
115
+ calls = 0
116
+
117
+ def __init__(self, api_token):
118
+ return None
119
+
120
+ def stream(self, model, input):
121
+ self.__class__.calls += 1
122
+ yield "not json"
123
+
124
+ monkeypatch.setitem(sys.modules, "replicate", Replicate)
125
+
126
+ try:
127
+ compile_automation_with_replicate(
128
+ instruction="if person present notify",
129
+ class_names=["person"],
130
+ api_token="test-token",
131
+ model="openai/gpt-5.2",
132
+ )
133
+ except ValueError as exc:
134
+ assert "Raw response: not json" in str(exc)
135
+ else:
136
+ raise AssertionError("Invalid provider output should fail validation")
137
+
138
+ assert Replicate.Client.calls == 1
139
+
140
+
141
+ def test_replicate_rate_limit_error_is_human_readable(monkeypatch) -> None:
142
+ class RateLimitError(Exception):
143
+ status = 429
144
+ detail = "Request was throttled. Your rate limit"
145
+
146
+ class Replicate:
147
+ class Client:
148
+ def __init__(self, api_token):
149
+ return None
150
+
151
+ def stream(self, model, input):
152
+ raise RateLimitError()
153
+ yield ""
154
+
155
+ monkeypatch.setitem(sys.modules, "replicate", Replicate)
156
+
157
+ try:
158
+ compile_automation_with_replicate(
159
+ instruction="if person present notify",
160
+ class_names=["person"],
161
+ api_token="test-token",
162
+ model="openai/gpt-5.2",
163
+ )
164
+ except RuntimeError as exc:
165
+ message = str(exc)
166
+ assert "Replicate API request failed" in message
167
+ assert "status 429" in message
168
+ assert "provider rate limit" in message
169
+ else:
170
+ raise AssertionError("Rate limits should surface as RuntimeError")
171
+
172
+
173
+ def test_openai_compile_uses_chat_completions(monkeypatch) -> None:
174
+ monkeypatch.setitem(sys.modules, "openai", None)
175
+
176
  class Response:
177
  def raise_for_status(self) -> None:
178
  return None
179
 
180
  def json(self):
181
+ return {
182
+ "choices": [
183
+ {
184
+ "message": {
185
+ "content": (
186
+ '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},'
187
+ '"then":[{"type":"simulate","name":"notify"}]}]}'
188
+ )
189
+ }
190
+ }
191
+ ]
192
+ }
193
 
194
  class Requests:
195
+ @staticmethod
196
+ def post(endpoint, headers, json, timeout):
197
+ assert endpoint == "https://api.openai.com/v1/chat/completions"
198
+ assert headers["Authorization"] == "Bearer test-key"
199
+ assert json["model"] == "gpt-test"
200
  assert json["response_format"] == {"type": "json_object"}
201
+ assert timeout == 120.0
202
  return Response()
203
 
204
+ monkeypatch.setitem(sys.modules, "requests", Requests)
205
+
206
+ result = compile_automation_with_openai(
207
+ instruction="if person present notify",
208
+ class_names=["person"],
209
+ api_key="test-key",
210
+ model="gpt-test",
 
 
211
  )
212
 
213
+ assert result.document.rules[0].name == "notify"
214
+
215
+
216
+ def test_openai_sdk_completion() -> None:
217
+ class Message:
218
+ content = '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},"then":[{"type":"simulate","name":"notify"}]}]}'
219
+
220
+ class Choice:
221
+ message = Message()
222
 
 
223
  class Response:
224
+ choices = [Choice()]
225
+
226
+ class Completions:
227
+ @staticmethod
228
+ def create(**payload):
229
+ assert payload["model"] == DEFAULT_OPENAI_MODEL
230
+ assert payload["response_format"] == {"type": "json_object"}
231
+ return Response()
232
+
233
+ class Chat:
234
+ completions = Completions()
235
+
236
+ class OpenAIClient:
237
+ chat = Chat()
238
+
239
+ def __init__(self, api_key, timeout):
240
+ assert api_key == "test-key"
241
+ assert timeout == 120.0
242
 
243
+ class OpenAIModule:
244
+ OpenAI = OpenAIClient
245
+
246
+ raw = _openai_chat_completion(
247
+ api_key="test-key",
248
+ model=DEFAULT_OPENAI_MODEL,
249
+ user_prompt="compile",
250
+ timeout=120.0,
251
+ openai_module=OpenAIModule,
252
+ )
253
+
254
+ assert json.loads(raw)["rules"][0]["name"] == "notify"
255
+
256
+
257
+ def test_anthropic_compile_uses_messages_api(monkeypatch) -> None:
258
+ monkeypatch.setitem(sys.modules, "anthropic", None)
259
+
260
+ class Response:
261
  def raise_for_status(self) -> None:
262
  return None
263
 
264
  def json(self):
265
+ return {
266
+ "content": [
267
+ {
268
+ "type": "text",
269
+ "text": (
270
+ '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},'
271
+ '"then":[{"type":"simulate","name":"notify"}]}]}'
272
+ ),
273
+ }
274
+ ]
275
+ }
276
 
277
  class Requests:
278
+ @staticmethod
279
+ def post(endpoint, headers, json, timeout):
280
+ assert endpoint == "https://api.anthropic.com/v1/messages"
281
+ assert headers["x-api-key"] == "test-key"
282
+ assert headers["anthropic-version"] == "2023-06-01"
283
+ assert json["model"] == "claude-test"
284
+ assert json["system"] == SYSTEM_PROMPT
285
+ assert timeout == 120.0
286
+ return Response()
 
 
287
 
288
  monkeypatch.setitem(sys.modules, "requests", Requests)
289
 
290
+ result = compile_automation_with_anthropic(
291
  instruction="if person present notify",
292
  class_names=["person"],
293
+ api_key="test-key",
294
+ model="claude-test",
 
295
  )
296
 
 
297
  assert result.document.rules[0].name == "notify"
298
 
299
 
300
+ def test_anthropic_sdk_message() -> None:
301
+ class TextBlock:
302
+ text = '{"rules":[{"name":"notify","when":{"all":[{"present":{"label":"person"}}]},"then":[{"type":"simulate","name":"notify"}]}]}'
 
 
303
 
304
+ class Response:
305
+ content = [TextBlock()]
 
 
 
 
 
 
306
 
307
+ class Messages:
308
+ @staticmethod
309
+ def create(**payload):
310
+ assert payload["model"] == DEFAULT_ANTHROPIC_MODEL
311
+ assert payload["system"] == SYSTEM_PROMPT
312
+ return Response()
313
 
314
+ class AnthropicClient:
315
+ messages = Messages()
316
+
317
+ def __init__(self, api_key, timeout):
318
+ assert api_key == "test-key"
319
+ assert timeout == 120.0
320
+
321
+ class AnthropicModule:
322
+ Anthropic = AnthropicClient
323
+
324
+ raw = _anthropic_message(
325
+ api_key="test-key",
326
+ model=DEFAULT_ANTHROPIC_MODEL,
327
+ user_prompt="compile",
328
+ timeout=120.0,
329
+ anthropic_module=AnthropicModule,
330
  )
331
 
332
+ assert json.loads(raw)["rules"][0]["name"] == "notify"
tests/test_store.py CHANGED
@@ -22,9 +22,11 @@ def test_local_config_yaml(tmp_path) -> None:
22
  [
23
  "default_device: cuda:0",
24
  "webhook_url: http://example.test",
25
- "llm_provider: cloud",
26
  "replicate_model: openai/gpt-5.2",
27
  "replicate_reasoning_effort: medium",
 
 
28
  ]
29
  ),
30
  encoding="utf-8",
@@ -34,9 +36,11 @@ def test_local_config_yaml(tmp_path) -> None:
34
 
35
  assert config.default_device == "cuda:0"
36
  assert config.webhook_url == "http://example.test"
37
- assert config.llm_provider == "cloud"
38
  assert config.replicate_model == "openai/gpt-5.2"
39
  assert config.replicate_reasoning_effort == "medium"
 
 
40
 
41
 
42
  def test_save_and_load_automations(tmp_path) -> None:
 
22
  [
23
  "default_device: cuda:0",
24
  "webhook_url: http://example.test",
25
+ "llm_provider: anthropic",
26
  "replicate_model: openai/gpt-5.2",
27
  "replicate_reasoning_effort: medium",
28
+ "openai_model: gpt-5.5",
29
+ "anthropic_model: claude-sonnet-4-6",
30
  ]
31
  ),
32
  encoding="utf-8",
 
36
 
37
  assert config.default_device == "cuda:0"
38
  assert config.webhook_url == "http://example.test"
39
+ assert config.llm_provider == "anthropic"
40
  assert config.replicate_model == "openai/gpt-5.2"
41
  assert config.replicate_reasoning_effort == "medium"
42
+ assert config.openai_model == "gpt-5.5"
43
+ assert config.anthropic_model == "claude-sonnet-4-6"
44
 
45
 
46
  def test_save_and_load_automations(tmp_path) -> None:
tiny_trigger/__init__.py CHANGED
@@ -2,13 +2,24 @@
2
 
3
  from .automation import RuleEngine, evaluate_video_detections, load_automation_text
4
  from .detector import UltralyticsYOLOEDetector, parse_class_prompt
5
- from .llm import compile_automation_with_llamacpp, compile_automation_with_replicate
 
 
 
 
 
 
 
6
  from .video import process_video
7
 
8
  __all__ = [
9
  "RuleEngine",
10
  "UltralyticsYOLOEDetector",
11
- "compile_automation_with_llamacpp",
 
 
 
 
12
  "compile_automation_with_replicate",
13
  "evaluate_video_detections",
14
  "load_automation_text",
 
2
 
3
  from .automation import RuleEngine, evaluate_video_detections, load_automation_text
4
  from .detector import UltralyticsYOLOEDetector, parse_class_prompt
5
+ from .llm import (
6
+ DEFAULT_ANTHROPIC_MODEL,
7
+ DEFAULT_OPENAI_MODEL,
8
+ DEFAULT_REPLICATE_MODEL,
9
+ compile_automation_with_anthropic,
10
+ compile_automation_with_openai,
11
+ compile_automation_with_replicate,
12
+ )
13
  from .video import process_video
14
 
15
  __all__ = [
16
  "RuleEngine",
17
  "UltralyticsYOLOEDetector",
18
+ "DEFAULT_ANTHROPIC_MODEL",
19
+ "DEFAULT_OPENAI_MODEL",
20
+ "DEFAULT_REPLICATE_MODEL",
21
+ "compile_automation_with_anthropic",
22
+ "compile_automation_with_openai",
23
  "compile_automation_with_replicate",
24
  "evaluate_video_detections",
25
  "load_automation_text",
tiny_trigger/automation.py CHANGED
@@ -138,8 +138,8 @@ class AutomationRule(BaseModel):
138
  if isinstance(self.then, list):
139
  if not self.then:
140
  raise ValueError("A rule needs at least one action.")
141
- if self.trigger.on in {"exit", "change"}:
142
- raise ValueError("exit/change triggers need then.exit or then.enter/then.exit actions.")
143
  return self
144
 
145
  actions = _actions_for_trigger(self.then, self.trigger.on)
@@ -408,7 +408,7 @@ def _actions_for_trigger(
408
  trigger: Literal["while", "enter", "exit", "change"],
409
  ) -> list[ActionSpec]:
410
  if isinstance(actions, list):
411
- return actions if trigger in {"while", "enter"} else []
412
  if trigger == "while":
413
  return actions.while_actions or actions.enter
414
  if trigger == "enter":
 
138
  if isinstance(self.then, list):
139
  if not self.then:
140
  raise ValueError("A rule needs at least one action.")
141
+ if self.trigger.on == "change":
142
+ raise ValueError("change triggers need then.enter/then.exit actions.")
143
  return self
144
 
145
  actions = _actions_for_trigger(self.then, self.trigger.on)
 
408
  trigger: Literal["while", "enter", "exit", "change"],
409
  ) -> list[ActionSpec]:
410
  if isinstance(actions, list):
411
+ return actions if trigger in {"while", "enter", "exit"} else []
412
  if trigger == "while":
413
  return actions.while_actions or actions.enter
414
  if trigger == "enter":
tiny_trigger/llm.py CHANGED
@@ -17,65 +17,33 @@ Use state gates in gate: enabled, cooldown.
17
  Use trigger.on for edge behavior: while, enter, exit, change.
18
  Use only these action types: simulate, webhook.
19
  Use trigger.on="enter" for state assertions like "must be on", "should be on", "keep on", "turn on when", or "notify when".
 
20
  Use trigger.on="while" only when the user explicitly wants repeated actions while a condition remains true, usually with a cooldown.
21
  When the request says one object is near, next to, beside, at, by, close to, or in front of another object, you MUST emit a near condition.
22
  Do not replace a near relation with two present conditions.
23
  Use max_gap_percent for near/far box-edge distance. It is the largest horizontal/vertical edge gap between boxes in normalized frame percent; touching or overlapping boxes have gap 0.
24
  If the user mentions elapsed time since an action or limiting repeat fires, encode it as gate.cooldown.
25
- If the user asks for an action when a condition starts and another action when it stops, use trigger.on="change" and then.enter / then.exit.
26
  """
27
 
 
 
 
 
28
 
29
  class LLMCompileResult(BaseModel):
30
  raw_text: str
31
  document: AutomationDocument
32
 
33
 
34
- def compile_automation_with_llamacpp(
35
- *,
36
- instruction: str,
37
- class_names: list[str],
38
- base_url: str = "http://127.0.0.1:8080/v1",
39
- model: str = "ggml-org/Qwen3-1.7B-GGUF:Q4_K_M",
40
- timeout: float = 60.0,
41
- ) -> LLMCompileResult:
42
- """Compile natural language into validated rules through llama.cpp server."""
43
- try:
44
- import requests
45
- except ImportError as exc: # pragma: no cover - dependency guard
46
- raise RuntimeError("Install requests to use the llama.cpp compiler.") from exc
47
-
48
- endpoint = base_url.rstrip("/") + "/chat/completions"
49
- user_prompt = _build_user_prompt(instruction=instruction, class_names=class_names)
50
- raw_text = _post_chat_completion(
51
- endpoint=endpoint,
52
- model=model,
53
- user_prompt=user_prompt,
54
- timeout=timeout,
55
- requests_module=requests,
56
  )
57
- try:
58
- return _validate_compile_result(raw_text)
59
- except (json.JSONDecodeError, ValidationError, ValueError) as first_error:
60
- repair_prompt = _build_repair_prompt(
61
- original_prompt=user_prompt,
62
- bad_response=raw_text,
63
- error=str(first_error),
64
- )
65
- repaired_text = _post_chat_completion(
66
- endpoint=endpoint,
67
- model=model,
68
- user_prompt=repair_prompt,
69
- timeout=timeout,
70
- requests_module=requests,
71
- )
72
- try:
73
- return _validate_compile_result(repaired_text)
74
- except (json.JSONDecodeError, ValidationError, ValueError) as second_error:
75
- raise ValueError(
76
- "llama.cpp returned invalid automation JSON after a repair attempt. "
77
- f"Last validation error: {second_error}. Last response: {repaired_text}"
78
- ) from second_error
79
 
80
 
81
  def compile_automation_with_replicate(
@@ -83,11 +51,12 @@ def compile_automation_with_replicate(
83
  instruction: str,
84
  class_names: list[str],
85
  api_token: str,
86
- model: str = "openai/gpt-5.2",
87
  reasoning_effort: str = "medium",
88
  timeout: float = 600.0,
89
  ) -> LLMCompileResult:
90
  """Compile natural language into validated rules through Replicate."""
 
91
  try:
92
  import replicate
93
  except ImportError as exc: # pragma: no cover - dependency guard
@@ -104,27 +73,88 @@ def compile_automation_with_replicate(
104
  )
105
  try:
106
  return _validate_compile_result(raw_text)
107
- except (json.JSONDecodeError, ValidationError, ValueError) as first_error:
108
- repair_prompt = _build_repair_prompt(
109
- original_prompt=user_prompt,
110
- bad_response=raw_text,
111
- error=str(first_error),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  )
113
- repaired_text = _stream_replicate_completion(
 
 
114
  model=model,
115
- prompt=_provider_prompt(repair_prompt),
116
- api_token=api_token,
117
- reasoning_effort=reasoning_effort,
118
  timeout=timeout,
119
- replicate_module=replicate,
120
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  try:
122
- return _validate_compile_result(repaired_text)
123
- except (json.JSONDecodeError, ValidationError, ValueError) as second_error:
124
- raise ValueError(
125
- "Replicate returned invalid automation JSON after a repair attempt. "
126
- f"Last validation error: {second_error}. Last response: {repaired_text}"
127
- ) from second_error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
 
130
  def _provider_prompt(user_prompt: str) -> str:
@@ -149,8 +179,11 @@ def _stream_replicate_completion(
149
  }
150
  client = replicate_module.Client(api_token=api_token)
151
  chunks: list[str] = []
152
- for event in client.stream(model, input=payload):
153
- chunks.append(str(event))
 
 
 
154
  text = "".join(chunks).strip()
155
  if not text:
156
  raise ValueError("Replicate stream returned no output.")
@@ -164,21 +197,115 @@ def _split_replicate_model(model: str) -> tuple[str, str]:
164
  return parts[0], parts[1]
165
 
166
 
167
- def _post_chat_completion(
168
  *,
169
  endpoint: str,
 
170
  model: str,
171
  user_prompt: str,
172
  timeout: float,
173
  requests_module: Any,
174
  ) -> str:
175
  payload = _chat_payload(model=model, user_prompt=user_prompt, response_format="json_object")
176
- response = requests_module.post(endpoint, json=payload, timeout=timeout)
177
- response.raise_for_status()
 
 
 
 
 
 
 
 
178
  body = response.json()
179
  return body["choices"][0]["message"]["content"]
180
 
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  def _validate_compile_result(raw_text: str) -> LLMCompileResult:
183
  data = json.loads(extract_json_object(raw_text))
184
  document = AutomationDocument.model_validate(data)
@@ -187,6 +314,60 @@ def _validate_compile_result(raw_text: str) -> LLMCompileResult:
187
  return LLMCompileResult(raw_text=raw_text, document=document)
188
 
189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  def extract_json_object(text: str) -> str:
191
  stripped = text.strip()
192
  if stripped.startswith("```"):
@@ -197,7 +378,7 @@ def extract_json_object(text: str) -> str:
197
 
198
  match = re.search(r"\{.*\}", stripped, flags=re.DOTALL)
199
  if not match:
200
- raise ValueError("llama.cpp response did not contain a JSON object.")
201
  return match.group(0)
202
 
203
 
@@ -214,7 +395,7 @@ Return a JSON object matching this high-level shape:
214
  {{
215
  "name": "short-kebab-case-name",
216
  "when": {{"all": [{{"present": {{"label": "cat", "min_count": 1}}}}]}},
217
- "trigger": {{"on": "while"}},
218
  "gate": {{"enabled": true}},
219
  "then": [{{"type": "simulate", "name": "action name"}}]
220
  }}
 
17
  Use trigger.on for edge behavior: while, enter, exit, change.
18
  Use only these action types: simulate, webhook.
19
  Use trigger.on="enter" for state assertions like "must be on", "should be on", "keep on", "turn on when", or "notify when".
20
+ Use trigger.on="exit" with a plain then action list for requests like "when the person leaves", "when it disappears", or "when it stops meeting the condition".
21
  Use trigger.on="while" only when the user explicitly wants repeated actions while a condition remains true, usually with a cooldown.
22
  When the request says one object is near, next to, beside, at, by, close to, or in front of another object, you MUST emit a near condition.
23
  Do not replace a near relation with two present conditions.
24
  Use max_gap_percent for near/far box-edge distance. It is the largest horizontal/vertical edge gap between boxes in normalized frame percent; touching or overlapping boxes have gap 0.
25
  If the user mentions elapsed time since an action or limiting repeat fires, encode it as gate.cooldown.
26
+ If the user asks for one action when a condition starts and another action when it stops, use trigger.on="change" and then.enter / then.exit.
27
  """
28
 
29
+ DEFAULT_REPLICATE_MODEL = "openai/gpt-5.2"
30
+ DEFAULT_OPENAI_MODEL = "gpt-5.5"
31
+ DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6"
32
+
33
 
34
  class LLMCompileResult(BaseModel):
35
  raw_text: str
36
  document: AutomationDocument
37
 
38
 
39
+ def _invalid_json_error(provider: str, raw_text: str, error: Exception) -> ValueError:
40
+ preview = raw_text.strip()
41
+ if len(preview) > 1200:
42
+ preview = preview[:1200] + "..."
43
+ return ValueError(
44
+ f"{provider} returned text that was not valid Tiny Trigger JSON. "
45
+ f"Validation error: {error}. Raw response: {preview}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
 
49
  def compile_automation_with_replicate(
 
51
  instruction: str,
52
  class_names: list[str],
53
  api_token: str,
54
+ model: str = DEFAULT_REPLICATE_MODEL,
55
  reasoning_effort: str = "medium",
56
  timeout: float = 600.0,
57
  ) -> LLMCompileResult:
58
  """Compile natural language into validated rules through Replicate."""
59
+ api_token = _clean_api_key(api_token, "Replicate")
60
  try:
61
  import replicate
62
  except ImportError as exc: # pragma: no cover - dependency guard
 
73
  )
74
  try:
75
  return _validate_compile_result(raw_text)
76
+ except (json.JSONDecodeError, ValidationError, ValueError) as exc:
77
+ raise _invalid_json_error("Replicate", raw_text, exc) from exc
78
+
79
+
80
+ def compile_automation_with_openai(
81
+ *,
82
+ instruction: str,
83
+ class_names: list[str],
84
+ api_key: str,
85
+ model: str = DEFAULT_OPENAI_MODEL,
86
+ timeout: float = 120.0,
87
+ ) -> LLMCompileResult:
88
+ """Compile natural language into validated rules through OpenAI."""
89
+ api_key = _clean_api_key(api_key, "OpenAI")
90
+ user_prompt = _build_user_prompt(instruction=instruction, class_names=class_names)
91
+ try:
92
+ import openai
93
+ except ImportError:
94
+ try:
95
+ import requests
96
+ except ImportError as exc: # pragma: no cover - dependency guard
97
+ raise RuntimeError("Install openai or requests to use the OpenAI compiler.") from exc
98
+ raw_text = _post_openai_chat_completion(
99
+ endpoint="https://api.openai.com/v1/chat/completions",
100
+ api_key=api_key,
101
+ model=model,
102
+ user_prompt=user_prompt,
103
+ timeout=timeout,
104
+ requests_module=requests,
105
  )
106
+ else:
107
+ raw_text = _openai_chat_completion(
108
+ api_key=api_key,
109
  model=model,
110
+ user_prompt=user_prompt,
 
 
111
  timeout=timeout,
112
+ openai_module=openai,
113
  )
114
+ try:
115
+ return _validate_compile_result(raw_text)
116
+ except (json.JSONDecodeError, ValidationError, ValueError) as exc:
117
+ raise _invalid_json_error("OpenAI", raw_text, exc) from exc
118
+
119
+
120
+ def compile_automation_with_anthropic(
121
+ *,
122
+ instruction: str,
123
+ class_names: list[str],
124
+ api_key: str,
125
+ model: str = DEFAULT_ANTHROPIC_MODEL,
126
+ timeout: float = 120.0,
127
+ ) -> LLMCompileResult:
128
+ """Compile natural language into validated rules through Anthropic Claude."""
129
+ api_key = _clean_api_key(api_key, "Anthropic")
130
+ user_prompt = _build_user_prompt(instruction=instruction, class_names=class_names)
131
+ try:
132
+ import anthropic
133
+ except ImportError:
134
  try:
135
+ import requests
136
+ except ImportError as exc: # pragma: no cover - dependency guard
137
+ raise RuntimeError("Install anthropic or requests to use the Anthropic compiler.") from exc
138
+ raw_text = _post_anthropic_message(
139
+ endpoint="https://api.anthropic.com/v1/messages",
140
+ api_key=api_key,
141
+ model=model,
142
+ user_prompt=user_prompt,
143
+ timeout=timeout,
144
+ requests_module=requests,
145
+ )
146
+ else:
147
+ raw_text = _anthropic_message(
148
+ api_key=api_key,
149
+ model=model,
150
+ user_prompt=user_prompt,
151
+ timeout=timeout,
152
+ anthropic_module=anthropic,
153
+ )
154
+ try:
155
+ return _validate_compile_result(raw_text)
156
+ except (json.JSONDecodeError, ValidationError, ValueError) as exc:
157
+ raise _invalid_json_error("Anthropic", raw_text, exc) from exc
158
 
159
 
160
  def _provider_prompt(user_prompt: str) -> str:
 
179
  }
180
  client = replicate_module.Client(api_token=api_token)
181
  chunks: list[str] = []
182
+ try:
183
+ for event in client.stream(model, input=payload):
184
+ chunks.append(str(event))
185
+ except Exception as exc:
186
+ raise RuntimeError(_provider_exception_message("Replicate", exc)) from exc
187
  text = "".join(chunks).strip()
188
  if not text:
189
  raise ValueError("Replicate stream returned no output.")
 
197
  return parts[0], parts[1]
198
 
199
 
200
+ def _post_openai_chat_completion(
201
  *,
202
  endpoint: str,
203
+ api_key: str,
204
  model: str,
205
  user_prompt: str,
206
  timeout: float,
207
  requests_module: Any,
208
  ) -> str:
209
  payload = _chat_payload(model=model, user_prompt=user_prompt, response_format="json_object")
210
+ try:
211
+ response = requests_module.post(
212
+ endpoint,
213
+ headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
214
+ json=payload,
215
+ timeout=timeout,
216
+ )
217
+ response.raise_for_status()
218
+ except Exception as exc:
219
+ raise RuntimeError(_provider_exception_message("OpenAI", exc)) from exc
220
  body = response.json()
221
  return body["choices"][0]["message"]["content"]
222
 
223
 
224
+ def _openai_chat_completion(
225
+ *,
226
+ api_key: str,
227
+ model: str,
228
+ user_prompt: str,
229
+ timeout: float,
230
+ openai_module: Any,
231
+ ) -> str:
232
+ client = openai_module.OpenAI(api_key=api_key, timeout=timeout)
233
+ try:
234
+ response = client.chat.completions.create(
235
+ **_chat_payload(model=model, user_prompt=user_prompt, response_format="json_object")
236
+ )
237
+ except Exception as exc:
238
+ raise RuntimeError(_provider_exception_message("OpenAI", exc)) from exc
239
+ content = response.choices[0].message.content
240
+ if isinstance(content, list):
241
+ return "".join(str(part.get("text", "")) for part in content if isinstance(part, dict))
242
+ return str(content or "")
243
+
244
+
245
+ def _post_anthropic_message(
246
+ *,
247
+ endpoint: str,
248
+ api_key: str,
249
+ model: str,
250
+ user_prompt: str,
251
+ timeout: float,
252
+ requests_module: Any,
253
+ ) -> str:
254
+ try:
255
+ response = requests_module.post(
256
+ endpoint,
257
+ headers={
258
+ "x-api-key": api_key,
259
+ "anthropic-version": "2023-06-01",
260
+ "Content-Type": "application/json",
261
+ },
262
+ json={
263
+ "model": model,
264
+ "system": SYSTEM_PROMPT,
265
+ "messages": [{"role": "user", "content": user_prompt}],
266
+ "max_tokens": 512,
267
+ "temperature": 0,
268
+ },
269
+ timeout=timeout,
270
+ )
271
+ response.raise_for_status()
272
+ except Exception as exc:
273
+ raise RuntimeError(_provider_exception_message("Anthropic", exc)) from exc
274
+ body = response.json()
275
+ chunks = body.get("content") or []
276
+ return "".join(str(chunk.get("text", "")) for chunk in chunks if isinstance(chunk, dict))
277
+
278
+
279
+ def _anthropic_message(
280
+ *,
281
+ api_key: str,
282
+ model: str,
283
+ user_prompt: str,
284
+ timeout: float,
285
+ anthropic_module: Any,
286
+ ) -> str:
287
+ client = anthropic_module.Anthropic(api_key=api_key, timeout=timeout)
288
+ try:
289
+ response = client.messages.create(
290
+ model=model,
291
+ system=SYSTEM_PROMPT,
292
+ messages=[{"role": "user", "content": user_prompt}],
293
+ max_tokens=512,
294
+ temperature=0,
295
+ )
296
+ except Exception as exc:
297
+ raise RuntimeError(_provider_exception_message("Anthropic", exc)) from exc
298
+ chunks = getattr(response, "content", []) or []
299
+ texts: list[str] = []
300
+ for chunk in chunks:
301
+ text = getattr(chunk, "text", None)
302
+ if text is None and isinstance(chunk, dict):
303
+ text = chunk.get("text")
304
+ if text:
305
+ texts.append(str(text))
306
+ return "".join(texts)
307
+
308
+
309
  def _validate_compile_result(raw_text: str) -> LLMCompileResult:
310
  data = json.loads(extract_json_object(raw_text))
311
  document = AutomationDocument.model_validate(data)
 
314
  return LLMCompileResult(raw_text=raw_text, document=document)
315
 
316
 
317
+ def _clean_api_key(api_key: str | None, provider: str) -> str:
318
+ cleaned = (api_key or "").strip()
319
+ if not cleaned:
320
+ raise ValueError(f"Paste a {provider} API key.")
321
+ if any(char in cleaned for char in ("\n", "\r", "\t")):
322
+ raise ValueError(f"{provider} API key must be a single-line token with no whitespace.")
323
+ if "Traceback" in cleaned or 'File "' in cleaned:
324
+ raise ValueError(
325
+ f"{provider} API key field looks like it contains a pasted error log, not an API key."
326
+ )
327
+ return cleaned
328
+
329
+
330
+ def _provider_exception_message(provider: str, exc: Exception) -> str:
331
+ parts = [f"{provider} API request failed"]
332
+ status = getattr(exc, "status", None) or getattr(exc, "status_code", None)
333
+ if status:
334
+ parts.append(f"status {status}")
335
+ detail = getattr(exc, "detail", None)
336
+ if detail:
337
+ parts.append(str(detail))
338
+ response = getattr(exc, "response", None)
339
+ if response is not None:
340
+ response_status = getattr(response, "status_code", None)
341
+ if response_status and not status:
342
+ parts.append(f"status {response_status}")
343
+ try:
344
+ body = response.json()
345
+ except Exception:
346
+ body = getattr(response, "text", "")
347
+ if body:
348
+ parts.append(str(body))
349
+ if len(parts) == 1:
350
+ parts.append(str(exc))
351
+ message = ". ".join(part for part in parts if part)
352
+ status_text = str(status or "")
353
+ if response is not None:
354
+ response_status = getattr(response, "status_code", None)
355
+ if response_status:
356
+ status_text = str(response_status)
357
+ lower_message = message.lower()
358
+ if status_text == "429" or "throttled" in lower_message or "rate limit" in lower_message:
359
+ message += (
360
+ ". This is a provider rate limit, separate from account credits. "
361
+ "Wait a bit or switch provider."
362
+ )
363
+ if status_text == "404" or "not found" in lower_message:
364
+ message += (
365
+ ". This usually means the configured model is unavailable, deprecated, "
366
+ "or misspelled for this provider."
367
+ )
368
+ return message
369
+
370
+
371
  def extract_json_object(text: str) -> str:
372
  stripped = text.strip()
373
  if stripped.startswith("```"):
 
378
 
379
  match = re.search(r"\{.*\}", stripped, flags=re.DOTALL)
380
  if not match:
381
+ raise ValueError("LLM response did not contain a JSON object.")
382
  return match.group(0)
383
 
384
 
 
395
  {{
396
  "name": "short-kebab-case-name",
397
  "when": {{"all": [{{"present": {{"label": "cat", "min_count": 1}}}}]}},
398
+ "trigger": {{"on": "enter"}},
399
  "gate": {{"enabled": true}},
400
  "then": [{{"type": "simulate", "name": "action name"}}]
401
  }}
tiny_trigger/store.py CHANGED
@@ -26,12 +26,14 @@ class LocalConfig(BaseModel):
26
  default_device: str | None = None
27
  default_image_size: int | None = None
28
  default_max_detections: int | None = None
29
- llamacpp_base_url: str | None = None
30
- llamacpp_model: str | None = None
31
  llm_provider: str | None = None
32
  replicate_api_token: str | None = None
33
  replicate_model: str | None = None
34
  replicate_reasoning_effort: str | None = None
 
 
 
 
35
 
36
 
37
  class RuntimeState(BaseModel):
 
26
  default_device: str | None = None
27
  default_image_size: int | None = None
28
  default_max_detections: int | None = None
 
 
29
  llm_provider: str | None = None
30
  replicate_api_token: str | None = None
31
  replicate_model: str | None = None
32
  replicate_reasoning_effort: str | None = None
33
+ openai_api_key: str | None = None
34
+ openai_model: str | None = None
35
+ anthropic_api_key: str | None = None
36
+ anthropic_model: str | None = None
37
 
38
 
39
  class RuntimeState(BaseModel):