@trillboards/edge-sdk 0.2.154 → 0.2.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/litertlm/test_trillboards_litertlm_shim.py +106 -0
- package/deploy/litertlm/trillboards_litertlm_shim.py +65 -1
- package/deploy/windows/apply-update.ps1 +1 -1
- package/deploy/windows/install-update.ps1 +124 -0
- package/deploy/windows/setup-litertlm.ps1 +152 -36
- package/dist/EdgeAgent.js +1 -1
- package/dist/LiteRtVlmProvider.d.ts +3 -0
- package/dist/LiteRtVlmProvider.d.ts.map +1 -1
- package/dist/LiteRtVlmProvider.js +42 -3
- package/dist/LiteRtVlmProvider.js.map +1 -1
- package/dist/PhonePersonViews.d.ts +53 -0
- package/dist/PhonePersonViews.d.ts.map +1 -0
- package/dist/PhonePersonViews.js +143 -0
- package/dist/PhonePersonViews.js.map +1 -0
- package/dist/ReviewModelUse.d.ts.map +1 -1
- package/dist/ReviewModelUse.js +4 -0
- package/dist/ReviewModelUse.js.map +1 -1
- package/dist/ScopedVlmReview.d.ts +9 -5
- package/dist/ScopedVlmReview.d.ts.map +1 -1
- package/dist/ScopedVlmReview.js +57 -3
- package/dist/ScopedVlmReview.js.map +1 -1
- package/dist/SensingProfileClient.d.ts +2 -0
- package/dist/SensingProfileClient.d.ts.map +1 -1
- package/dist/SensingProfileClient.js +2 -0
- package/dist/SensingProfileClient.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/localSensing.d.ts +6 -0
- package/dist/localSensing.d.ts.map +1 -1
- package/dist/localSensing.js +15 -0
- package/dist/localSensing.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ the recorded kwargs prove WHICH configs are dropped and which survive.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import importlib
|
|
9
|
+
import base64
|
|
9
10
|
import hashlib
|
|
10
11
|
import os
|
|
11
12
|
import sys
|
|
@@ -208,6 +209,111 @@ class ShimTest(unittest.TestCase):
|
|
|
208
209
|
self.assertEqual(captured["response"]["choices"], [{"text": "unchanged"}])
|
|
209
210
|
self.assertNotIn("runtimeModel", captured["response"])
|
|
210
211
|
|
|
212
|
+
def test_native_images_are_hashed_at_the_ffi_boundary_not_copied_from_headers(self):
|
|
213
|
+
shim = self._load(TRILLBOARDS_LITERTLM_SHIM="1")
|
|
214
|
+
seen = []
|
|
215
|
+
class Conversation:
|
|
216
|
+
def send_message_async(self, message):
|
|
217
|
+
seen.append(message)
|
|
218
|
+
return "native result"
|
|
219
|
+
shim._apply_native_images(SimpleNamespace(Conversation=Conversation))
|
|
220
|
+
payloads = [b"whole marked area", b"person one", b"person two"]
|
|
221
|
+
expected = [hashlib.sha256(b).hexdigest() for b in payloads]
|
|
222
|
+
state = {"expected": expected, "consumed": None}
|
|
223
|
+
token = shim._image_request.set(state)
|
|
224
|
+
self.addCleanup(lambda: shim._image_request.reset(token))
|
|
225
|
+
message = {"content": [{"type": "image", "blob": base64.b64encode(b).decode()} for b in payloads]}
|
|
226
|
+
self.assertEqual(Conversation().send_message_async(message), "native result")
|
|
227
|
+
self.assertEqual(state["consumed"], expected)
|
|
228
|
+
self.assertEqual(len(seen), 1)
|
|
229
|
+
with self.assertRaisesRegex(ValueError, "native image"):
|
|
230
|
+
Conversation().send_message_async({"content": message["content"][:1]})
|
|
231
|
+
self.assertEqual(len(seen), 1)
|
|
232
|
+
|
|
233
|
+
def test_image_capacity_is_lazy_and_only_added_for_explicit_native_requests(self):
|
|
234
|
+
shim = self._load(TRILLBOARDS_LITERTLM_SHIM="1")
|
|
235
|
+
calls = []
|
|
236
|
+
class Engine:
|
|
237
|
+
def __init__(self, **kwargs):
|
|
238
|
+
calls.append(kwargs)
|
|
239
|
+
shim._apply_model_snapshot(Engine)
|
|
240
|
+
Engine()
|
|
241
|
+
self.assertEqual(calls, [{}])
|
|
242
|
+
token = shim._image_request.set({"expected": ["a" * 64, "b" * 64], "consumed": None})
|
|
243
|
+
try:
|
|
244
|
+
engine = Engine()
|
|
245
|
+
self.assertEqual(calls[1], {"max_num_images": 5})
|
|
246
|
+
self.assertEqual(engine._trillboards_image_capacity, 5)
|
|
247
|
+
finally:
|
|
248
|
+
shim._image_request.reset(token)
|
|
249
|
+
Engine()
|
|
250
|
+
self.assertEqual(calls[2], {})
|
|
251
|
+
|
|
252
|
+
def _native_http(self, low_memory=False):
|
|
253
|
+
shim = self._load(TRILLBOARDS_LITERTLM_SHIM="1")
|
|
254
|
+
events = []
|
|
255
|
+
images = [b"area", b"person"]
|
|
256
|
+
hashes = [hashlib.sha256(b).hexdigest() for b in images]
|
|
257
|
+
class Engine:
|
|
258
|
+
def __init__(self, **kwargs):
|
|
259
|
+
events.append(("allocate", kwargs.get("max_num_images", 1)))
|
|
260
|
+
if low_memory and kwargs.get("max_num_images") == 5:
|
|
261
|
+
raise MemoryError("test image capacity allocation")
|
|
262
|
+
self.backend = SimpleNamespace(get_name=lambda: "cpu")
|
|
263
|
+
self._trillboards_artifact_sha256 = "a" * 64
|
|
264
|
+
def __exit__(self, *args):
|
|
265
|
+
events.append(("release", self._trillboards_image_capacity))
|
|
266
|
+
class Conversation:
|
|
267
|
+
def send_message_async(self, message):
|
|
268
|
+
events.append(("native", len(message["content"])))
|
|
269
|
+
shim._apply_model_snapshot(Engine)
|
|
270
|
+
shim._apply_native_images(SimpleNamespace(Conversation=Conversation))
|
|
271
|
+
server = SimpleNamespace(litert_lm_engine=Engine())
|
|
272
|
+
class Handler:
|
|
273
|
+
headers = {"X-Trillboards-Model-Sha256": "a" * 64, "X-Trillboards-Image-Sha256": ",".join(hashes)}
|
|
274
|
+
def __init__(self):
|
|
275
|
+
self.server = server
|
|
276
|
+
def send_error(self, code, message):
|
|
277
|
+
events.append(("error", code))
|
|
278
|
+
def _get_engine(self, model_id, *, backend):
|
|
279
|
+
if self.server.litert_lm_engine is None:
|
|
280
|
+
try:
|
|
281
|
+
self.server.litert_lm_engine = Engine()
|
|
282
|
+
except MemoryError:
|
|
283
|
+
self.send_error(500, "allocation failed")
|
|
284
|
+
return None
|
|
285
|
+
return self.server.litert_lm_engine
|
|
286
|
+
def do_POST(self):
|
|
287
|
+
if self._get_engine("gemma", backend="cpu") is not None:
|
|
288
|
+
Conversation().send_message_async({"content": [
|
|
289
|
+
{"type": "image", "blob": base64.b64encode(b).decode()} for b in images]})
|
|
290
|
+
module._dump_json({"object": "chat.completion"})
|
|
291
|
+
output = []
|
|
292
|
+
module = SimpleNamespace(OpenAIHandler=Handler, _dump_json=lambda data, **kwargs: output.append(data))
|
|
293
|
+
shim.apply_runtime_handler(module)
|
|
294
|
+
handler = Handler()
|
|
295
|
+
handler.do_POST()
|
|
296
|
+
return handler, events, output, hashes
|
|
297
|
+
|
|
298
|
+
def test_native_http_receipt_is_outside_model_text_and_capacity_reuses_one_engine(self):
|
|
299
|
+
handler, events, output, hashes = self._native_http()
|
|
300
|
+
self.assertEqual(events[:3], [("allocate", 1), ("release", 1), ("allocate", 5)])
|
|
301
|
+
self.assertEqual(output[0]["nativeImageInputs"], {"version": 1, "sha256": hashes})
|
|
302
|
+
handler.do_POST()
|
|
303
|
+
self.assertEqual(sum(e[0] == "allocate" for e in events), 2)
|
|
304
|
+
handler.headers = {"X-Trillboards-Model-Sha256": "a" * 64}
|
|
305
|
+
handler.do_POST()
|
|
306
|
+
self.assertNotIn("nativeImageInputs", output[-1])
|
|
307
|
+
|
|
308
|
+
def test_capacity_allocation_failure_leaves_single_image_recovery_available(self):
|
|
309
|
+
handler, events, output, _ = self._native_http(low_memory=True)
|
|
310
|
+
self.assertEqual(events[:4], [("allocate", 1), ("release", 1), ("allocate", 5), ("error", 500)])
|
|
311
|
+
self.assertEqual(output, [])
|
|
312
|
+
handler.headers = {"X-Trillboards-Model-Sha256": "a" * 64}
|
|
313
|
+
handler.do_POST()
|
|
314
|
+
self.assertEqual(events[-2:], [("allocate", 1), ("native", 2)])
|
|
315
|
+
self.assertNotIn("nativeImageInputs", output[-1])
|
|
316
|
+
|
|
211
317
|
|
|
212
318
|
if __name__ == "__main__":
|
|
213
319
|
unittest.main()
|
|
@@ -82,6 +82,9 @@ from __future__ import annotations
|
|
|
82
82
|
|
|
83
83
|
import os
|
|
84
84
|
import sys
|
|
85
|
+
import contextvars
|
|
86
|
+
|
|
87
|
+
_image_request = contextvars.ContextVar('trillboards_native_images', default=None)
|
|
85
88
|
|
|
86
89
|
__all__ = ["install", "apply_to", "should_drop", "TAG"]
|
|
87
90
|
|
|
@@ -129,6 +132,7 @@ def apply_to(litert_lm_module: object) -> bool:
|
|
|
129
132
|
Returns True when the wrap is in place (including when it already was).
|
|
130
133
|
"""
|
|
131
134
|
engine_cls = getattr(litert_lm_module, "Engine", None)
|
|
135
|
+
_apply_native_images(litert_lm_module)
|
|
132
136
|
if engine_cls is not None:
|
|
133
137
|
_apply_model_snapshot(engine_cls)
|
|
134
138
|
original = getattr(engine_cls, "create_conversation", None)
|
|
@@ -261,9 +265,16 @@ def _apply_model_snapshot(engine_cls):
|
|
|
261
265
|
return
|
|
262
266
|
|
|
263
267
|
def initialize(self, *args, **kwargs):
|
|
268
|
+
images = _image_request.get()
|
|
269
|
+
if images is not None:
|
|
270
|
+
# The public option used by the saved-frame comparison. No larger
|
|
271
|
+
# engine is allocated on ordinary single-image/off-switch starts.
|
|
272
|
+
kwargs['max_num_images'] = 5
|
|
264
273
|
path = kwargs.get('model_path', args[0] if args else None)
|
|
265
274
|
if path is None: # lightweight contract-test engines have no artifact
|
|
266
|
-
|
|
275
|
+
result = original(self, *args, **kwargs)
|
|
276
|
+
self._trillboards_image_capacity = kwargs.get('max_num_images', 1)
|
|
277
|
+
return result
|
|
267
278
|
snapshot, sha = _verified_snapshot(path)
|
|
268
279
|
if 'model_path' in kwargs:
|
|
269
280
|
kwargs['model_path'] = snapshot
|
|
@@ -271,11 +282,41 @@ def _apply_model_snapshot(engine_cls):
|
|
|
271
282
|
args = (snapshot, *args[1:])
|
|
272
283
|
original(self, *args, **kwargs)
|
|
273
284
|
self._trillboards_artifact_sha256 = sha
|
|
285
|
+
self._trillboards_image_capacity = kwargs.get('max_num_images', 1)
|
|
274
286
|
|
|
275
287
|
initialize._trillboards_model_snapshot = True
|
|
276
288
|
engine_cls.__init__ = initialize
|
|
277
289
|
|
|
278
290
|
|
|
291
|
+
def _apply_native_images(module):
|
|
292
|
+
"""Prove the exact JPEGs consumed immediately before native serialization."""
|
|
293
|
+
import base64
|
|
294
|
+
import hashlib
|
|
295
|
+
conversation = getattr(module, 'Conversation', None)
|
|
296
|
+
original = getattr(conversation, 'send_message_async', None)
|
|
297
|
+
if original is None or getattr(original, '_trillboards_native_images', False):
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
def send(self, message, *args, **kwargs):
|
|
301
|
+
state = _image_request.get()
|
|
302
|
+
if state is not None:
|
|
303
|
+
parts = message.get('content', []) if isinstance(message, dict) else []
|
|
304
|
+
hashes = []
|
|
305
|
+
for part in parts:
|
|
306
|
+
if isinstance(part, dict) and part.get('type') == 'image':
|
|
307
|
+
blob = part.get('blob')
|
|
308
|
+
if not isinstance(blob, str):
|
|
309
|
+
raise ValueError('native image must be an inline JPEG')
|
|
310
|
+
hashes.append(hashlib.sha256(base64.b64decode(blob, validate=True)).hexdigest())
|
|
311
|
+
if hashes != state['expected']:
|
|
312
|
+
raise ValueError('native image input count, order or bytes do not match')
|
|
313
|
+
state['consumed'] = hashes
|
|
314
|
+
return original(self, message, *args, **kwargs)
|
|
315
|
+
|
|
316
|
+
send._trillboards_native_images = True
|
|
317
|
+
conversation.send_message_async = send
|
|
318
|
+
|
|
319
|
+
|
|
279
320
|
def apply_runtime_handler(module):
|
|
280
321
|
"""Bind proof to the engine used by this HTTP request, outside model text."""
|
|
281
322
|
import contextvars
|
|
@@ -291,13 +332,31 @@ def apply_runtime_handler(module):
|
|
|
291
332
|
if expected is not None and not re.fullmatch(r'[a-f0-9]{64}', expected):
|
|
292
333
|
self.send_error(400, 'invalid selected artifact hash')
|
|
293
334
|
return
|
|
335
|
+
raw_images = self.headers.get('X-Trillboards-Image-Sha256')
|
|
336
|
+
images = raw_images.split(',') if raw_images is not None else None
|
|
337
|
+
if images is not None and (expected is None or not 2 <= len(images) <= 5
|
|
338
|
+
or any(not re.fullmatch(r'[a-f0-9]{64}', value) for value in images)):
|
|
339
|
+
self.send_error(400, 'invalid pinned native image request')
|
|
340
|
+
return
|
|
341
|
+
image_token = _image_request.set({'expected': images, 'consumed': None} if images is not None else None)
|
|
294
342
|
token = active.set({'expected': expected, 'proof': None})
|
|
295
343
|
try:
|
|
296
344
|
return original_post(self)
|
|
297
345
|
finally:
|
|
298
346
|
active.reset(token)
|
|
347
|
+
_image_request.reset(image_token)
|
|
299
348
|
|
|
300
349
|
def get_engine(self, *args, **kwargs):
|
|
350
|
+
images = _image_request.get()
|
|
351
|
+
if images is not None:
|
|
352
|
+
server = getattr(self, 'server', None)
|
|
353
|
+
previous = getattr(server, 'litert_lm_engine', None)
|
|
354
|
+
if previous is not None and getattr(previous, '_trillboards_image_capacity', 1) < 5:
|
|
355
|
+
# The pinned CLI's HTTPServer is serial. Release its previous
|
|
356
|
+
# engine BEFORE allocating the larger one; no duplicate model.
|
|
357
|
+
previous.__exit__(None, None, None)
|
|
358
|
+
server.litert_lm_engine = None
|
|
359
|
+
_say('native image request: upgrading idle engine capacity to five')
|
|
301
360
|
engine = original_engine(self, *args, **kwargs)
|
|
302
361
|
state = active.get()
|
|
303
362
|
if engine is None or state is None:
|
|
@@ -326,6 +385,11 @@ def apply_runtime_handler(module):
|
|
|
326
385
|
state = active.get()
|
|
327
386
|
if isinstance(data, dict) and data.get('object') == 'chat.completion' and state and state['proof']:
|
|
328
387
|
data = {**data, 'runtimeModel': dict(state['proof'])}
|
|
388
|
+
images = _image_request.get()
|
|
389
|
+
if isinstance(data, dict) and data.get('object') == 'chat.completion' and images is not None:
|
|
390
|
+
if images['consumed'] != images['expected']:
|
|
391
|
+
raise ValueError('native image consumption was not verified')
|
|
392
|
+
data = {**data, 'nativeImageInputs': {'version': 1, 'sha256': images['consumed']}}
|
|
329
393
|
return original_dump(data, **kwargs)
|
|
330
394
|
|
|
331
395
|
handler.do_POST, handler._get_engine, module._dump_json = do_post, get_engine, dump_json
|
|
@@ -499,7 +499,7 @@ function Invoke-Commit {
|
|
|
499
499
|
# failure and $ErrorActionPreference = 'Stop' turns that into ours.
|
|
500
500
|
& $installUpdate -InstallDir $InstallDir -StagingDir $PayloadDir
|
|
501
501
|
} catch {
|
|
502
|
-
Fail 'failed' "the
|
|
502
|
+
Fail 'failed' "the update transaction failed: $($_.Exception.Message)"
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
# The measured half. The probe predicts; this observes.
|
|
@@ -10,9 +10,17 @@ param(
|
|
|
10
10
|
|
|
11
11
|
$ErrorActionPreference = "Stop"
|
|
12
12
|
$previousDir = Join-Path $InstallDir ".previous"
|
|
13
|
+
$runtimeRepairMarker = Join-Path $previousDir 'model-runtime-rollback-failed.json'
|
|
13
14
|
$parts = @("bin", "lib")
|
|
14
15
|
$movedPrevious = New-Object System.Collections.Generic.List[string]
|
|
15
16
|
$movedStaged = New-Object System.Collections.Generic.List[string]
|
|
17
|
+
$script:modelRuntimeUpdate = $null
|
|
18
|
+
|
|
19
|
+
# A failed external-runtime rollback owns this recovery tree. A later update
|
|
20
|
+
# must not delete it in the normal .previous cleanup or restart sensing over it.
|
|
21
|
+
if (Test-Path -LiteralPath $runtimeRepairMarker) {
|
|
22
|
+
throw 'The previous local AI rollback needs repair; saved connector/runtime backups were left untouched'
|
|
23
|
+
}
|
|
16
24
|
|
|
17
25
|
function Assert-ConnectorVersion([string]$Root) {
|
|
18
26
|
$node = Join-Path $Root "bin\node.exe"
|
|
@@ -43,6 +51,109 @@ function Register-Connector([string]$Root) {
|
|
|
43
51
|
if ($LASTEXITCODE -ne 0) { throw "Scheduled-task registration failed." }
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
function Get-OwnedLocalAiProcesses {
|
|
55
|
+
# Task Scheduler can stop the uv launcher while leaving its Python child
|
|
56
|
+
# alive. Match only this user's model server on the verified task's port;
|
|
57
|
+
# never kill all Python processes or a different user's listener.
|
|
58
|
+
$portPattern = '(?:^|\s)--port(?:\s+|=)' + $script:modelRuntimeUpdate.Port + '(?=\s|$)'
|
|
59
|
+
$processes = Get-CimInstance Win32_Process -Filter "Name = 'python.exe' OR Name = 'pythonw.exe' OR Name = 'litert-lm.exe'" -OperationTimeoutSec 5 -ErrorAction Stop
|
|
60
|
+
foreach ($process in $processes) {
|
|
61
|
+
if (-not $process.CommandLine -or $process.CommandLine -notmatch 'litert[-_]lm' -or
|
|
62
|
+
$process.CommandLine -notmatch '(?:^|\s)serve(?=\s|$)' -or $process.CommandLine -notmatch $portPattern) { continue }
|
|
63
|
+
$owner = Invoke-CimMethod -InputObject $process -MethodName GetOwnerSid -OperationTimeoutSec 5 -ErrorAction Stop
|
|
64
|
+
if ($owner.ReturnValue -ne 0 -or -not $owner.Sid) { throw 'Cannot verify the model process owner; no unverified process was stopped' }
|
|
65
|
+
if ($owner.Sid -eq $script:modelRuntimeUpdate.OwnerSid) { $process }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function Stop-LocalAiForUpdate {
|
|
70
|
+
Disable-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop | Out-Null
|
|
71
|
+
Stop-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop
|
|
72
|
+
for ($attempt = 0; $attempt -lt 15; $attempt++) {
|
|
73
|
+
$task = Get-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop
|
|
74
|
+
$remaining = @(Get-OwnedLocalAiProcesses)
|
|
75
|
+
foreach ($process in $remaining) { Stop-Process -Id $process.ProcessId -Force -ErrorAction Stop }
|
|
76
|
+
$listenerErrors = @()
|
|
77
|
+
$listeners = @(Get-NetTCPConnection -LocalPort $script:modelRuntimeUpdate.Port -State Listen -ErrorAction SilentlyContinue -ErrorVariable listenerErrors)
|
|
78
|
+
if (@($listenerErrors | Where-Object { $_.CategoryInfo.Category -ne 'ObjectNotFound' }).Count) { throw 'Cannot verify the local AI port is free' }
|
|
79
|
+
if ($task.State -notin @('Running', 'Queued') -and $remaining.Count -eq 0 -and $listeners.Count -eq 0) { return }
|
|
80
|
+
Start-Sleep -Seconds 1
|
|
81
|
+
}
|
|
82
|
+
throw 'The local AI task, server process or listening port did not stop; runtime files were not replaced'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Restore-LocalAiTaskState {
|
|
86
|
+
if ($null -eq $script:modelRuntimeUpdate) { return }
|
|
87
|
+
if ($script:modelRuntimeUpdate.Enabled) {
|
|
88
|
+
Enable-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop | Out-Null
|
|
89
|
+
if ($script:modelRuntimeUpdate.Running) { Start-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop }
|
|
90
|
+
} else {
|
|
91
|
+
Disable-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction Stop | Out-Null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Update-LocalAiRuntime([string]$Root) {
|
|
96
|
+
$modelTaskErrors = @()
|
|
97
|
+
$task = Get-ScheduledTask -TaskName 'TrillboardsLiteRtLm' -ErrorAction SilentlyContinue -ErrorVariable modelTaskErrors
|
|
98
|
+
if (-not $task) {
|
|
99
|
+
# Only a genuine absence belongs to first-time setup. An inaccessible task
|
|
100
|
+
# must not be mistaken for "no model service" and silently keep old code.
|
|
101
|
+
if (@($modelTaskErrors | Where-Object { $_.CategoryInfo.Category -ne 'ObjectNotFound' }).Count) {
|
|
102
|
+
throw 'The local AI task is inaccessible; runtime refresh was not skipped'
|
|
103
|
+
}
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
$taskUser = [string]$task.Principal.UserId
|
|
107
|
+
$taskSid = if ($taskUser -match '^S-1-') { $taskUser } else {
|
|
108
|
+
(New-Object Security.Principal.NTAccount($taskUser)).Translate([Security.Principal.SecurityIdentifier]).Value
|
|
109
|
+
}
|
|
110
|
+
$currentSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
111
|
+
$action = @($task.Actions)
|
|
112
|
+
$ownTool = Join-Path $env:USERPROFILE '.local\bin\litert-lm.exe'
|
|
113
|
+
if ($taskSid -ne $currentSid -or $action.Count -ne 1 -or
|
|
114
|
+
[IO.Path]::GetFullPath([string]$action[0].Execute) -ine [IO.Path]::GetFullPath($ownTool)) {
|
|
115
|
+
throw 'Cannot verify ownership of the local AI task; no runtime was changed'
|
|
116
|
+
}
|
|
117
|
+
$serverArguments = [string]$action[0].Arguments
|
|
118
|
+
$ports = [regex]::Matches($serverArguments, '(?:^|\s)--port(?:\s+|=)([0-9]+)(?=\s|$)')
|
|
119
|
+
if ($serverArguments -notmatch '^serve\s' -or $ports.Count -ne 1 -or
|
|
120
|
+
[long]$ports[0].Groups[1].Value -lt 1 -or [long]$ports[0].Groups[1].Value -gt 65535) {
|
|
121
|
+
throw 'Cannot verify the local AI task port; no runtime was changed'
|
|
122
|
+
}
|
|
123
|
+
$setup = Join-Path $Root 'bin\setup-litertlm.ps1'
|
|
124
|
+
if (-not (Test-Path -LiteralPath $setup -PathType Leaf)) { throw 'The released local AI setup is missing' }
|
|
125
|
+
# Inspect first: an unchanged shim does NOT restart a healthy model server on
|
|
126
|
+
# every SDK update. /v1/models and the CLI version cannot detect stale code.
|
|
127
|
+
$state = (& $setup -RuntimeAction Inspect | Out-String).Trim() | ConvertFrom-Json
|
|
128
|
+
if ($LASTEXITCODE -ne 0 -or $state.version -ne 1) { throw 'Could not inspect the installed local AI runtime' }
|
|
129
|
+
if (-not $state.needsRefresh) { return }
|
|
130
|
+
$script:modelRuntimeUpdate = [pscustomobject]@{
|
|
131
|
+
Enabled = [bool]$task.Settings.Enabled
|
|
132
|
+
Running = ($task.State -eq 'Running')
|
|
133
|
+
OwnerSid = $currentSid
|
|
134
|
+
Port = [int]$ports[0].Groups[1].Value
|
|
135
|
+
Setup = $setup
|
|
136
|
+
Backup = (Join-Path $previousDir 'litertlm-runtime')
|
|
137
|
+
}
|
|
138
|
+
Stop-LocalAiForUpdate
|
|
139
|
+
# This mode is local file replacement only. It never downloads a CLI/model,
|
|
140
|
+
# imports weights, resets tuning, re-registers tasks or runs image inference.
|
|
141
|
+
& $setup -RuntimeAction Refresh -RuntimeBackupDir $script:modelRuntimeUpdate.Backup
|
|
142
|
+
if ($LASTEXITCODE -ne 0) { throw 'The local AI runtime refresh failed' }
|
|
143
|
+
Restore-LocalAiTaskState
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function Restore-LocalAiRuntime {
|
|
147
|
+
if ($null -eq $script:modelRuntimeUpdate) { return }
|
|
148
|
+
Stop-LocalAiForUpdate
|
|
149
|
+
$receipt = Join-Path $script:modelRuntimeUpdate.Backup 'runtime-backup.json'
|
|
150
|
+
if (Test-Path -LiteralPath $receipt -PathType Leaf) {
|
|
151
|
+
& $script:modelRuntimeUpdate.Setup -RuntimeAction Restore -RuntimeBackupDir $script:modelRuntimeUpdate.Backup
|
|
152
|
+
if ($LASTEXITCODE -ne 0) { throw 'The previous local AI runtime could not be verified; rollback backup was preserved' }
|
|
153
|
+
}
|
|
154
|
+
Restore-LocalAiTaskState
|
|
155
|
+
}
|
|
156
|
+
|
|
46
157
|
<#
|
|
47
158
|
Carry the detector's RAM ceiling onto an EXISTING install.
|
|
48
159
|
|
|
@@ -192,6 +303,9 @@ try {
|
|
|
192
303
|
}
|
|
193
304
|
|
|
194
305
|
Assert-ConnectorVersion $InstallDir
|
|
306
|
+
# The Python venv is OUTSIDE bin/lib. Refresh it transactionally before the
|
|
307
|
+
# connector can review a saved frame with the new native-image contract.
|
|
308
|
+
Update-LocalAiRuntime $InstallDir
|
|
195
309
|
Register-Connector $InstallDir
|
|
196
310
|
Assert-ConnectorScheduled
|
|
197
311
|
if (Test-Path (Join-Path $InstallDir "trillboards.config.json")) {
|
|
@@ -206,11 +320,21 @@ try {
|
|
|
206
320
|
Write-Host "Trillboards Edge update installed successfully."
|
|
207
321
|
} catch {
|
|
208
322
|
$failure = $_
|
|
323
|
+
$runtimeRollbackFailure = $null
|
|
209
324
|
try {
|
|
210
325
|
$stopScript = Join-Path $InstallDir "bin\stop-existing.ps1"
|
|
211
326
|
if (Test-Path $stopScript) { & $stopScript -InstallDir $InstallDir }
|
|
212
327
|
} catch {}
|
|
213
328
|
|
|
329
|
+
# Use the NEW setup while its bin tree still exists. Restore external runtime
|
|
330
|
+
# bytes before rolling bin/lib back; neither path touches pairings or spools.
|
|
331
|
+
try { Restore-LocalAiRuntime } catch { $runtimeRollbackFailure = $_.Exception.Message }
|
|
332
|
+
if ($runtimeRollbackFailure) {
|
|
333
|
+
[IO.File]::WriteAllText($runtimeRepairMarker,
|
|
334
|
+
(@{ version = 1; failedAtUtc = [DateTime]::UtcNow.ToString('o'); detail = $runtimeRollbackFailure } | ConvertTo-Json),
|
|
335
|
+
[Text.UTF8Encoding]::new($false))
|
|
336
|
+
throw "Update failed: $($failure.Exception.Message). Local AI rollback needs repair: $runtimeRollbackFailure. Preserved both connector trees, runtime backups, pairings and saved evidence."
|
|
337
|
+
}
|
|
214
338
|
foreach ($part in $movedStaged) {
|
|
215
339
|
Remove-Item -Recurse -Force (Join-Path $InstallDir $part) -ErrorAction SilentlyContinue
|
|
216
340
|
}
|
|
@@ -24,6 +24,10 @@ param(
|
|
|
24
24
|
[switch]$RequireBundledModel,
|
|
25
25
|
[switch]$AllowModelDownload,
|
|
26
26
|
[string]$ProgressPath = "",
|
|
27
|
+
# Upgrades refresh only our shipped Python adapter, never the CLI, weights,
|
|
28
|
+
# model config or task registration. The caller owns stopping/restarting it.
|
|
29
|
+
[ValidateSet('Setup', 'Inspect', 'Refresh', 'Restore')][string]$RuntimeAction = 'Setup',
|
|
30
|
+
[string]$RuntimeBackupDir = "",
|
|
27
31
|
[switch]$SmokeTest
|
|
28
32
|
)
|
|
29
33
|
|
|
@@ -333,6 +337,128 @@ function Import-VerifiedModel([string]$SourcePath, [string]$ExecutablePath) {
|
|
|
333
337
|
return (($LASTEXITCODE -eq 0) -and (Test-LitertLmModel $ExecutablePath))
|
|
334
338
|
}
|
|
335
339
|
|
|
340
|
+
# One owner for the shim on fresh installs AND upgrades. None of these helpers
|
|
341
|
+
# import a model, rewrite model config, install dependencies or register tasks.
|
|
342
|
+
function Get-LitertlmShimPaths([string]$UvPath) {
|
|
343
|
+
$shim = $null
|
|
344
|
+
foreach ($candidate in @(
|
|
345
|
+
(Join-Path $PSScriptRoot 'trillboards_litertlm_shim.py'),
|
|
346
|
+
(Join-Path $PSScriptRoot '..\litertlm\trillboards_litertlm_shim.py')
|
|
347
|
+
)) { if (Test-Path -LiteralPath $candidate -PathType Leaf) { $shim = $candidate; break } }
|
|
348
|
+
if (-not $shim) { throw 'The released litert-lm shim is missing' }
|
|
349
|
+
$toolDir = (& $UvPath tool dir 2>$null | Select-Object -First 1)
|
|
350
|
+
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($toolDir)) { throw 'Cannot resolve uv tool dir' }
|
|
351
|
+
$venv = [IO.Path]::GetFullPath((Join-Path $toolDir.Trim() 'litert-lm'))
|
|
352
|
+
$py = Join-Path $venv 'Scripts\python.exe'
|
|
353
|
+
if (-not (Test-Path -LiteralPath $py -PathType Leaf)) { throw 'The installed litert-lm Python is missing' }
|
|
354
|
+
$site = (& $py -c "import sysconfig;print(sysconfig.get_paths()['purelib'])" 2>$null | Select-Object -First 1)
|
|
355
|
+
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($site)) { throw 'Cannot resolve litert-lm site-packages' }
|
|
356
|
+
$site = [IO.Path]::GetFullPath($site.Trim())
|
|
357
|
+
if (-not $site.StartsWith(($venv.TrimEnd('\') + '\'), [StringComparison]::OrdinalIgnoreCase) -or
|
|
358
|
+
-not (Test-Path -LiteralPath $site -PathType Container)) { throw 'The runtime site is outside its managed venv' }
|
|
359
|
+
return [pscustomobject]@{ Source = $shim; Python = $py; Site = $site }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function Get-LitertlmShimFiles($Paths) {
|
|
363
|
+
return @(
|
|
364
|
+
[pscustomobject]@{ Name = 'trillboards_litertlm_shim.py'; Bytes = [IO.File]::ReadAllBytes($Paths.Source) },
|
|
365
|
+
[pscustomobject]@{ Name = 'trillboards-litertlm-shim.pth'; Bytes = [Text.Encoding]::ASCII.GetBytes("import trillboards_litertlm_shim`r`n") }
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function Get-ShimBytesHash([byte[]]$Bytes) {
|
|
370
|
+
$hasher = [Security.Cryptography.SHA256]::Create()
|
|
371
|
+
try { return ([BitConverter]::ToString($hasher.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant() }
|
|
372
|
+
finally { $hasher.Dispose() }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function Get-LitertlmShimState([string]$UvPath) {
|
|
376
|
+
$paths = Get-LitertlmShimPaths $UvPath
|
|
377
|
+
$needsRefresh = $false
|
|
378
|
+
foreach ($file in (Get-LitertlmShimFiles $paths)) {
|
|
379
|
+
$target = Join-Path $paths.Site $file.Name
|
|
380
|
+
if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { $needsRefresh = $true; continue }
|
|
381
|
+
if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne (Get-ShimBytesHash $file.Bytes)) { $needsRefresh = $true }
|
|
382
|
+
}
|
|
383
|
+
return [pscustomobject]@{ version = 1; needsRefresh = $needsRefresh }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function Install-LitertlmShim {
|
|
387
|
+
param([string]$UvPath, [string]$BackupDirectory = '', [switch]$Restore)
|
|
388
|
+
$ErrorActionPreference = 'Stop'
|
|
389
|
+
try {
|
|
390
|
+
$paths = Get-LitertlmShimPaths $UvPath
|
|
391
|
+
$files = @(Get-LitertlmShimFiles $paths)
|
|
392
|
+
$receiptPath = if ($BackupDirectory) { Join-Path $BackupDirectory 'runtime-backup.json' } else { '' }
|
|
393
|
+
if ($Restore) {
|
|
394
|
+
if (-not $receiptPath) { throw 'Runtime rollback requires its backup directory' }
|
|
395
|
+
$receipt = Get-Content -LiteralPath $receiptPath -Raw | ConvertFrom-Json
|
|
396
|
+
if ($receipt.version -ne 1 -or $receipt.site -ine $paths.Site -or @($receipt.files).Count -ne $files.Count) { throw 'Runtime rollback scope mismatch' }
|
|
397
|
+
# Validate the COMPLETE restore before changing either file. Refuse an
|
|
398
|
+
# unexpected concurrent write rather than overwriting another repair.
|
|
399
|
+
foreach ($file in $files) {
|
|
400
|
+
$entry = @($receipt.files | Where-Object { $_.name -eq $file.Name })
|
|
401
|
+
if ($entry.Count -ne 1) { throw 'Runtime rollback file set mismatch' }
|
|
402
|
+
$target = Join-Path $paths.Site $file.Name
|
|
403
|
+
if (Test-Path -LiteralPath $target) {
|
|
404
|
+
$current = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
405
|
+
if ($current -ne $entry[0].installedSha256 -and $current -ne $entry[0].previousSha256) { throw 'Runtime changed after the update; rollback refused' }
|
|
406
|
+
}
|
|
407
|
+
if ($entry[0].existed -and (Get-FileHash -LiteralPath (Join-Path $BackupDirectory $file.Name) -Algorithm SHA256).Hash.ToLowerInvariant() -ne $entry[0].previousSha256) { throw 'Runtime rollback backup hash mismatch' }
|
|
408
|
+
}
|
|
409
|
+
foreach ($entry in $receipt.files) {
|
|
410
|
+
$target = Join-Path $paths.Site $entry.name
|
|
411
|
+
if ($entry.existed) { Copy-Item -LiteralPath (Join-Path $BackupDirectory $entry.name) -Destination $target -Force }
|
|
412
|
+
elseif (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Force }
|
|
413
|
+
if ($entry.existed) {
|
|
414
|
+
if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne $entry.previousSha256) { throw 'Restored runtime hash mismatch' }
|
|
415
|
+
} elseif (Test-Path -LiteralPath $target) { throw 'Runtime rollback left a newly introduced file' }
|
|
416
|
+
}
|
|
417
|
+
Log 'restored the previous litert-lm shim bytes'
|
|
418
|
+
return $true
|
|
419
|
+
}
|
|
420
|
+
if ($BackupDirectory) {
|
|
421
|
+
if (Test-Path -LiteralPath $receiptPath) { throw 'Preserve the existing runtime rollback receipt' }
|
|
422
|
+
New-Item -ItemType Directory -Force -Path $BackupDirectory | Out-Null
|
|
423
|
+
$backups = @(foreach ($file in $files) {
|
|
424
|
+
$target = Join-Path $paths.Site $file.Name
|
|
425
|
+
$existed = Test-Path -LiteralPath $target -PathType Leaf
|
|
426
|
+
$previousSha = $null
|
|
427
|
+
if ($existed) {
|
|
428
|
+
$previousSha = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
429
|
+
$backup = Join-Path $BackupDirectory $file.Name
|
|
430
|
+
Copy-Item -LiteralPath $target -Destination $backup
|
|
431
|
+
if ((Get-FileHash -LiteralPath $backup -Algorithm SHA256).Hash.ToLowerInvariant() -ne $previousSha) { throw 'Runtime rollback copy did not verify' }
|
|
432
|
+
}
|
|
433
|
+
[pscustomobject]@{ name = $file.Name; existed = [bool]$existed; previousSha256 = $previousSha; installedSha256 = (Get-ShimBytesHash $file.Bytes) }
|
|
434
|
+
})
|
|
435
|
+
[IO.File]::WriteAllText($receiptPath, (@{ version = 1; site = $paths.Site; files = $backups } | ConvertTo-Json -Depth 5), [Text.UTF8Encoding]::new($false))
|
|
436
|
+
}
|
|
437
|
+
foreach ($file in $files) {
|
|
438
|
+
$target = Join-Path $paths.Site $file.Name
|
|
439
|
+
$temporary = "$target.$PID.update"
|
|
440
|
+
try {
|
|
441
|
+
[IO.File]::WriteAllBytes($temporary, $file.Bytes)
|
|
442
|
+
if ((Get-FileHash -LiteralPath $temporary -Algorithm SHA256).Hash.ToLowerInvariant() -ne (Get-ShimBytesHash $file.Bytes)) { throw 'Runtime refresh hash mismatch' }
|
|
443
|
+
# PS 5.1 binds an ordinary $null to an empty String here, which .NET
|
|
444
|
+
# treats as an invalid backup path. Pass an actual null String.
|
|
445
|
+
if (Test-Path -LiteralPath $target) { [IO.File]::Replace($temporary, $target, [NullString]::Value) }
|
|
446
|
+
else { [IO.File]::Move($temporary, $target) }
|
|
447
|
+
if ((Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant() -ne (Get-ShimBytesHash $file.Bytes)) { throw 'Installed runtime refresh hash mismatch' }
|
|
448
|
+
} finally { if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Force } }
|
|
449
|
+
}
|
|
450
|
+
# A fresh interpreter proves the installed wrapper imports. No engine,
|
|
451
|
+
# weights, image inference, model config or camera credentials are loaded.
|
|
452
|
+
& $paths.Python -c "import litert_lm,sys; sys.exit(0 if getattr(litert_lm.Engine.create_conversation,'_trillboards_litertlm_shim',False) else 1)" *>> $LogPath
|
|
453
|
+
if ($LASTEXITCODE -ne 0) { throw 'The refreshed model-service shim did not import' }
|
|
454
|
+
Log "litert-lm fast-conversation shim active in $($paths.Site)"
|
|
455
|
+
return $true
|
|
456
|
+
} catch {
|
|
457
|
+
Log "WARN: litert-lm shim operation failed: $_"
|
|
458
|
+
return $false
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
336
462
|
if ($SmokeTest) {
|
|
337
463
|
if ([string]::IsNullOrWhiteSpace($ProgressPath)) { throw "SmokeTest requires ProgressPath" }
|
|
338
464
|
Write-SetupProgress -Stage "local_ai" -Title "Installing local AI" -Message "Smoke test" -Phase "installing_model" -Percent 62 -TotalBytes $ExpectedModelBytes
|
|
@@ -345,7 +471,10 @@ if ($SmokeTest) {
|
|
|
345
471
|
exit 0
|
|
346
472
|
}
|
|
347
473
|
|
|
348
|
-
|
|
474
|
+
# S4U/boot tasks and the interactive installer run in different sessions. A
|
|
475
|
+
# Local mutex cannot serialize them; scope the Global name to this user's SID.
|
|
476
|
+
$setupSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
477
|
+
$setupMutex = New-Object System.Threading.Mutex($false, ("Global\TrillboardsLiteRtLmSetup-" + $setupSid))
|
|
349
478
|
$haveSetupMutex = $false
|
|
350
479
|
try {
|
|
351
480
|
$haveSetupMutex = $setupMutex.WaitOne(0)
|
|
@@ -354,6 +483,10 @@ try {
|
|
|
354
483
|
}
|
|
355
484
|
|
|
356
485
|
if (-not $haveSetupMutex) {
|
|
486
|
+
if ($RuntimeAction -ne 'Setup') {
|
|
487
|
+
$setupMutex.Dispose()
|
|
488
|
+
throw 'Local AI setup is busy; no concurrent runtime refresh was attempted'
|
|
489
|
+
}
|
|
357
490
|
Log "LiteRT-LM provisioning is already running; attaching setup progress"
|
|
358
491
|
if ([string]::IsNullOrWhiteSpace($ProgressPath) -and -not $RequireBundledModel) {
|
|
359
492
|
$setupMutex.Dispose()
|
|
@@ -389,6 +522,24 @@ function Stop-SetupLock {
|
|
|
389
522
|
$setupMutex.Dispose()
|
|
390
523
|
}
|
|
391
524
|
|
|
525
|
+
if ($RuntimeAction -ne 'Setup') {
|
|
526
|
+
try {
|
|
527
|
+
$refreshUvPath = Join-Path $LocalBin 'uv.exe'
|
|
528
|
+
if (-not (Test-Path -LiteralPath $refreshUvPath -PathType Leaf)) {
|
|
529
|
+
$refreshUv = Get-Command uv -ErrorAction Stop
|
|
530
|
+
$refreshUvPath = $refreshUv.Source
|
|
531
|
+
}
|
|
532
|
+
if ($RuntimeAction -eq 'Inspect') {
|
|
533
|
+
Get-LitertlmShimState $refreshUvPath | ConvertTo-Json -Compress
|
|
534
|
+
} else {
|
|
535
|
+
if ([string]::IsNullOrWhiteSpace($RuntimeBackupDir)) { throw 'A runtime rollback directory is required' }
|
|
536
|
+
$refreshArgs = @{ UvPath = $refreshUvPath; BackupDirectory = $RuntimeBackupDir; Restore = ($RuntimeAction -eq 'Restore') }
|
|
537
|
+
if (-not (Install-LitertlmShim @refreshArgs)) { throw 'The local AI runtime refresh/rollback did not verify' }
|
|
538
|
+
}
|
|
539
|
+
} finally { Stop-SetupLock }
|
|
540
|
+
exit 0
|
|
541
|
+
}
|
|
542
|
+
|
|
392
543
|
Log "starting LiteRT-LM provisioning (Windows x64, CPU backend)"
|
|
393
544
|
Write-SetupProgress -Stage "local_ai" -Title "Installing local AI" -Message "Preparing the private on-device AI runtime. Cloud inference remains off." -Phase "installing_runtime" -Percent 0
|
|
394
545
|
|
|
@@ -585,41 +736,6 @@ if ((Test-LitertLmCli) -and ($installedLitertLmVersion -eq $LitertLmVersion)) {
|
|
|
585
736
|
# short-circuits that branch on a healthy CLI -- gating this behind it is exactly
|
|
586
737
|
# how a repaired-and-healthy machine would keep the slow serve and report fine.
|
|
587
738
|
# Never throws: this is a speed fix, and it must not be able to fail an install.
|
|
588
|
-
function Install-LitertlmShim {
|
|
589
|
-
param([string]$UvPath)
|
|
590
|
-
try {
|
|
591
|
-
$shim = $null
|
|
592
|
-
foreach ($c in @(
|
|
593
|
-
(Join-Path $PSScriptRoot "trillboards_litertlm_shim.py"),
|
|
594
|
-
(Join-Path $PSScriptRoot "..\litertlm\trillboards_litertlm_shim.py")
|
|
595
|
-
)) { if (Test-Path -LiteralPath $c -PathType Leaf) { $shim = $c; break } }
|
|
596
|
-
if (-not $shim) { Log "WARN: litert-lm shim source not found; serve keeps stock (slower) conversation setup"; return $false }
|
|
597
|
-
|
|
598
|
-
$toolDir = (& $UvPath tool dir 2>$null | Select-Object -First 1)
|
|
599
|
-
if ([string]::IsNullOrWhiteSpace($toolDir)) { Log "WARN: could not resolve 'uv tool dir'; skipping litert-lm shim"; return $false }
|
|
600
|
-
$py = Join-Path $toolDir.Trim() "litert-lm\Scripts\python.exe"
|
|
601
|
-
if (-not (Test-Path -LiteralPath $py -PathType Leaf)) { Log "WARN: litert-lm venv python missing at $py; skipping shim"; return $false }
|
|
602
|
-
|
|
603
|
-
$site = (& $py -c "import sysconfig;print(sysconfig.get_paths()['purelib'])" 2>$null | Select-Object -First 1)
|
|
604
|
-
if ([string]::IsNullOrWhiteSpace($site) -or -not (Test-Path -LiteralPath $site.Trim())) {
|
|
605
|
-
Log "WARN: could not resolve litert-lm site-packages; skipping shim"; return $false
|
|
606
|
-
}
|
|
607
|
-
$site = $site.Trim()
|
|
608
|
-
Copy-Item -LiteralPath $shim -Destination (Join-Path $site "trillboards_litertlm_shim.py") -Force
|
|
609
|
-
Set-Content -LiteralPath (Join-Path $site "trillboards-litertlm-shim.pth") -Value "import trillboards_litertlm_shim" -Encoding ASCII
|
|
610
|
-
|
|
611
|
-
# Capability is established by ATTEMPTING THE OPERATION. Importing litert_lm
|
|
612
|
-
# here is the only thing that proves the wrap landed on the class the serve
|
|
613
|
-
# will use; a file that merely exists proves nothing.
|
|
614
|
-
& $py -c "import litert_lm,sys; sys.exit(0 if getattr(litert_lm.Engine.create_conversation,'_trillboards_litertlm_shim',False) else 1)" *>> $LogPath
|
|
615
|
-
if ($LASTEXITCODE -eq 0) { Log "litert-lm fast-conversation shim active in $site"; return $true }
|
|
616
|
-
Log "WARN: litert-lm shim did not take effect; serve keeps stock (slower) conversation setup"
|
|
617
|
-
return $false
|
|
618
|
-
} catch {
|
|
619
|
-
Log "WARN: litert-lm shim install failed: $_"
|
|
620
|
-
return $false
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
739
|
Install-LitertlmShim -UvPath $uvCmd.Source | Out-Null
|
|
624
740
|
|
|
625
741
|
$bundledModelVerified = $false
|
package/dist/EdgeAgent.js
CHANGED
|
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
|
|
|
44
44
|
const CommandRouter_1 = require("./CommandRouter");
|
|
45
45
|
const config_1 = require("./config");
|
|
46
46
|
const dataDir_1 = require("./dataDir");
|
|
47
|
-
const SDK_VERSION = '0.2.
|
|
47
|
+
const SDK_VERSION = '0.2.157';
|
|
48
48
|
class EdgeAgent extends events_1.EventEmitter {
|
|
49
49
|
constructor(config, deps) {
|
|
50
50
|
super();
|
|
@@ -381,6 +381,8 @@ export interface LocalAnalyzeRequest {
|
|
|
381
381
|
expectedModel?: ModelPin;
|
|
382
382
|
/** Raw JPEG bytes for this frame. */
|
|
383
383
|
jpeg: Buffer;
|
|
384
|
+
/** One moment: entire scoped area first, then separate person views. */
|
|
385
|
+
images?: Buffer[];
|
|
384
386
|
/** The JSON Schema the output is validated against (the profile contract). */
|
|
385
387
|
schema: Record<string, unknown>;
|
|
386
388
|
/** The analysis prompt (same instruction the cloud VLM gets). */
|
|
@@ -416,6 +418,7 @@ export declare class LiteRtVlmProvider {
|
|
|
416
418
|
* an observation or a request?" — see LiteRtAcceleratorHealth.delegateConfirmed.
|
|
417
419
|
*/
|
|
418
420
|
private confirmedModel;
|
|
421
|
+
private static readonly unavailableMultiImageRuntimes;
|
|
419
422
|
/** Did the serve list the `,gpu` id? null = never asked (the healthy path). */
|
|
420
423
|
private gpuAdvertised;
|
|
421
424
|
/** Set once the GPU variant failed a whole frame and we fell back to CPU. */
|