Spaces:
Running on Zero
Running on Zero
Private CPU interface integration with ephemeral result lifecycle
Browse files- .gitattributes +1 -0
- DATA_POLICY.md +35 -0
- GODOT_LICENSE +43 -0
- IMPORTER_BUILD_RECEIPT.json +60 -0
- LICENSE +21 -0
- README.md +8 -27
- RELEASE_MANIFEST.json +22 -0
- app.py +28 -31
- assets/formula_template.json +111 -0
- bin/extract_saved_track +3 -0
- client.js +111 -0
- delivery.py +151 -0
- index.html +22 -137
- requirements.txt +2 -1
- server.py +55 -285
- worker.py +125 -0
.gitattributes
CHANGED
|
@@ -39,3 +39,4 @@ tracks/R09[[:space:]]Eau[[:space:]]Noir.track filter=lfs diff=lfs merge=lfs -tex
|
|
| 39 |
tracks/The[[:space:]]Champions[[:space:]]Trial.track filter=lfs diff=lfs merge=lfs -text
|
| 40 |
bin/altd-sim filter=lfs diff=lfs merge=lfs -text
|
| 41 |
bin/libaltd_gpu.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 39 |
tracks/The[[:space:]]Champions[[:space:]]Trial.track filter=lfs diff=lfs merge=lfs -text
|
| 40 |
bin/altd-sim filter=lfs diff=lfs merge=lfs -text
|
| 41 |
bin/libaltd_gpu.so filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
bin/extract_saved_track filter=lfs diff=lfs merge=lfs -text
|
DATA_POLICY.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Data handling & deletion policy — private integration test
|
| 2 |
+
|
| 3 |
+
Operator: TiGa (seeker-cyber-maker). Version: draft 2026-10-08.
|
| 4 |
+
|
| 5 |
+
Files are processed only for the requested job, not collected for our own
|
| 6 |
+
training, analytics or public examples. Inputs remain in the browser until Run.
|
| 7 |
+
This local test uses native CPU; the hosted GPU path is not yet integrated.
|
| 8 |
+
|
| 9 |
+
Each job uses temporary files removed before a successful result is published.
|
| 10 |
+
The application result store is memory-only. No public file URL is created.
|
| 11 |
+
The browser holds a random job access key in memory, not a user account or HF token.
|
| 12 |
+
Anyone who obtains that key can access the job; do not share it. Reloading the
|
| 13 |
+
page loses access. Expiry is fixed at 30 minutes from result publication.
|
| 14 |
+
|
| 15 |
+
Automatic deletion is enabled by default. The browser validates the complete
|
| 16 |
+
download and acknowledges receipt; that deletes the result store's copy. One
|
| 17 |
+
download attempt is permitted. We cannot verify that the browser saved to disk.
|
| 18 |
+
An interrupted attempt is not retryable in automatic mode; Delete now and expiry
|
| 19 |
+
still work. Unchecking automatic deletion allows repeat downloads until expiry.
|
| 20 |
+
Delete now removes the stored result immediately. Downloads never reset expiry.
|
| 21 |
+
|
| 22 |
+
Access is refused at the deadline. While the server is running, the expiry loop
|
| 23 |
+
releases expired stored data within approximately one second, subject to runtime
|
| 24 |
+
scheduling. Shutdown clears the store; a restart has no saved job data to reload.
|
| 25 |
+
This is deletion of application-controlled files and stored results, not a claim
|
| 26 |
+
of physical RAM/disk erasure, prevention of swap, or deletion of response buffers.
|
| 27 |
+
No claim is made about hosting-provider logs or infrastructure retention.
|
| 28 |
+
|
| 29 |
+
Results received by the browser and files saved locally are the user's to manage.
|
| 30 |
+
The browser retains its received result in page memory so Save received result
|
| 31 |
+
again can retry a local save without another server retrieval. Discard browser
|
| 32 |
+
result clears the app's reference; closing or refreshing the page loses it.
|
| 33 |
+
No bucket support, network sandbox or provider-wide ZDR guarantee is offered.
|
| 34 |
+
The development service is single-process and loopback-only. Public deployment
|
| 35 |
+
and its framework/proxy behavior must be tested separately before release.
|
GODOT_LICENSE
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Godot Engine source adaptations in src/physics/broadphase.rs
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md).
|
| 4 |
+
Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
|
| 5 |
+
|
| 6 |
+
Permission is hereby granted, free of charge, to any person obtaining
|
| 7 |
+
a copy of this software and associated documentation files (the
|
| 8 |
+
"Software"), to deal in the Software without restriction, including
|
| 9 |
+
without limitation the rights to use, copy, modify, merge, publish,
|
| 10 |
+
distribute, sublicense, and/or sell copies of the Software, and to
|
| 11 |
+
permit persons to whom the Software is furnished to do so, subject to
|
| 12 |
+
the following conditions:
|
| 13 |
+
The above copyright notice and this permission notice shall be
|
| 14 |
+
included in all copies or substantial portions of the Software.
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
| 16 |
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
| 17 |
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
| 18 |
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
| 19 |
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
| 20 |
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
| 21 |
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
| 22 |
+
|
| 23 |
+
Modified tree-balancing algorithm, originally Randy Gaul qu3e:
|
| 24 |
+
/**
|
| 25 |
+
* @file q3DynamicAABBTree.h
|
| 26 |
+
* @author Randy Gaul
|
| 27 |
+
* @date 10/10/2014
|
| 28 |
+
* Copyright (c) 2014 Randy Gaul http://www.randygaul.net
|
| 29 |
+
* This software is provided 'as-is', without any express or implied
|
| 30 |
+
* warranty. In no event will the authors be held liable for any damages
|
| 31 |
+
* arising from the use of this software.
|
| 32 |
+
* Permission is granted to anyone to use this software for any purpose,
|
| 33 |
+
* including commercial applications, and to alter it and redistribute it
|
| 34 |
+
* freely, subject to the following restrictions:
|
| 35 |
+
* 1. The origin of this software must not be misrepresented; you must not
|
| 36 |
+
* claim that you wrote the original software. If you use this software
|
| 37 |
+
* in a product, an acknowledgment in the product documentation would be
|
| 38 |
+
* appreciated but is not required.
|
| 39 |
+
* 2. Altered source versions must be plainly marked as such, and must not
|
| 40 |
+
* be misrepresented as being the original software.
|
| 41 |
+
* 3. This notice may not be removed or altered from any source distribution.
|
| 42 |
+
*/
|
| 43 |
+
|
IMPORTER_BUILD_RECEIPT.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"created_utc": "2026-10-08T19:25:38.550739+00:00",
|
| 3 |
+
"status": "built_and_smoke_verified",
|
| 4 |
+
"repository": "https://github.com/Zorro909/driving_sim_rs",
|
| 5 |
+
"source_revision": "d8ab74c25df70b6a86ed101080e33d5fb785ea6f",
|
| 6 |
+
"source_worktree_clean": true,
|
| 7 |
+
"build_command": "RUSTFLAGS=\"-C target-cpu=x86-64\" cargo build --locked --release --no-default-features --features cli --example extract_saved_track",
|
| 8 |
+
"build_exit_code": 0,
|
| 9 |
+
"rustc": "rustc 1.93.0 (254b59607 2026-01-19)\nbinary: rustc\ncommit-hash: 254b59607d4417e9dffbc307138ae5c86280fe4c\ncommit-date: 2026-01-19\nhost: x86_64-unknown-linux-gnu\nrelease: 1.93.0\nLLVM version: 21.1.8",
|
| 10 |
+
"cargo": "cargo 1.93.0 (083ac5135 2025-12-15)",
|
| 11 |
+
"architecture": "x86_64-unknown-linux-gnu",
|
| 12 |
+
"cpu_codegen": "x86-64 baseline (target-cpu=native overridden)",
|
| 13 |
+
"host_libc": "Debian GLIBC 2.41-12+deb13u4",
|
| 14 |
+
"binary_minimum_glibc_symbol_version": "2.34",
|
| 15 |
+
"dynamic_libraries": [
|
| 16 |
+
"libgcc_s.so.1",
|
| 17 |
+
"libc.so.6"
|
| 18 |
+
],
|
| 19 |
+
"elf_interpreter": "/lib64/ld-linux-x86-64.so.2",
|
| 20 |
+
"runtime_compatibility_note": "ELF symbol requirements indicate glibc 2.34 or newer; tested only on this glibc 2.41 host. Destination integration remains untested.",
|
| 21 |
+
"smoke_commands": [
|
| 22 |
+
"./extract_saved_track --help",
|
| 23 |
+
"./extract_saved_track verification/generated_saved_track.json synthetic-smoke assets/scenes/formula_template.json verification/synthetic-scene.json"
|
| 24 |
+
],
|
| 25 |
+
"verification": {
|
| 26 |
+
"help_exit_code": 0,
|
| 27 |
+
"synthetic_conversion_exit_code": 0,
|
| 28 |
+
"name_matches": true,
|
| 29 |
+
"tile_count_matches": true,
|
| 30 |
+
"tile_map_position_matches": true,
|
| 31 |
+
"native_broadphase_false": true,
|
| 32 |
+
"formula_vehicle_preserved": true,
|
| 33 |
+
"formula_physics_preserved": true,
|
| 34 |
+
"tiles": 24,
|
| 35 |
+
"physics_shapes": 160,
|
| 36 |
+
"walls": 401,
|
| 37 |
+
"path_points": 4593
|
| 38 |
+
},
|
| 39 |
+
"scope": "Importer build and smoke checks only. No full project test suite, private game data, deployment, shared source edits, or external services.",
|
| 40 |
+
"checksums_sha256": {
|
| 41 |
+
"GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
|
| 42 |
+
"LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
|
| 43 |
+
"SOURCE_REVISION": "1d1f925697ae11f17015c08189043fb55bcc48b724e97daf5e102d043d1d1f0a",
|
| 44 |
+
"assets/scenes/formula_template.json": "80f745df82bb7be45af52785fd582460bcddc63c5a4e1d8375d85b3122db4c6b",
|
| 45 |
+
"extract_saved_track": "3a7733c45b4301a786fcbfe92660a1c484749272da5f608d62f34878bd4ff31a",
|
| 46 |
+
"verification/Cargo.lock": "d8b8dfcfd4ddd90388a9aadae6c12a675e46277a3f26dabcce5ba8af6ff12183",
|
| 47 |
+
"verification/build.log": "55c65f1a725126537b35f3fafacccc5bcc8e6707a60f02d26b803da3a48b8763",
|
| 48 |
+
"verification/cargo-version.txt": "49f46ff5ffe434e6c1c66388a9230ac33bbe3dbd9175314e9ffc9aa7c418d329",
|
| 49 |
+
"verification/elf-report.txt": "ce9a1831871e3e7ce7cb24bb338934c92518cc65559d619a75c05657c09e5788",
|
| 50 |
+
"verification/file.txt": "18dcdc24bdaaf8eb6cd84fb37ab0ba9096766ffa31a5595cb9262ae8c3430363",
|
| 51 |
+
"verification/generated_saved_track.json": "dafb51079bddbc7b2a958acff37b6df3bbdd3af70fcae2b08df536db2f92f59b",
|
| 52 |
+
"verification/help.stderr.txt": "2da7efbb9ab342a5b0efb5e30855709859bc81fbbc134c539fd33cea1e86bf70",
|
| 53 |
+
"verification/help.stdout.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 54 |
+
"verification/ldd.txt": "38f782b358e5a768f4fccfff32f68262f4274fc1575b8568bf74d361d2d33a5d",
|
| 55 |
+
"verification/rustc-version.txt": "83e4eefd5e010a92d9f63328bf8219e23fe5e4cf5e5d17ce53d0263cfa4218a6",
|
| 56 |
+
"verification/smoke.stderr.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 57 |
+
"verification/smoke.stdout.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
| 58 |
+
"verification/synthetic-scene.json": "1d87b18c7844402df8ef8272f99f3f32670f98a240f3f72e14b505c2b45743e0"
|
| 59 |
+
}
|
| 60 |
+
}
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Zorro909
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,33 +1,14 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji: 🏎️
|
| 4 |
-
colorFrom: green
|
| 5 |
-
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.29.1
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
-
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
Private
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
Rollback: restore README.md from revision ba07e35a39211af7a0deda0ebd9adb2c554b4f38
|
| 20 |
-
and request CPU Basic. BINARY_PROVENANCE.json pins the two exact HF-built binaries
|
| 21 |
-
to that revision and upstream d8ab74c25df70b6a86ed101080e33d5fb785ea6f.
|
| 22 |
-
|
| 23 |
-
No CUDA initialization outside the lease. First check device discovery and a
|
| 24 |
-
three-value sine kernel in process, then the normal Rust gpu-info subprocess.
|
| 25 |
-
No fallback, trajectory parity, speedup or H200 claim. A second lease
|
| 26 |
-
must independently pass before testing training. Retained PACKAGE_MANIFEST.json
|
| 27 |
-
describes the prior Docker snapshot only; ZEROGPU_MANIFEST.json covers this overlay.
|
| 28 |
-
|
| 29 |
-
The upload test validates the exact supplied A01 track and a 6-4-2 Formula network
|
| 30 |
-
before leasing a GPU for at most 90 seconds. It repeats qualification, then runs
|
| 31 |
-
4 vehicles for 1 generation of 60 physics ticks, seed 42, proton math profile.
|
| 32 |
-
The subprocess has a 25-second timeout. It exports a candidate and receipt, not
|
| 33 |
-
an improvement claim. Real-game import compatibility and physics parity remain unverified.
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AI Learns To Drive — TiGa interface
|
|
|
|
|
|
|
|
|
|
| 3 |
sdk: gradio
|
| 4 |
sdk_version: 6.29.1
|
| 5 |
+
python_version: "3.10"
|
| 6 |
app_file: app.py
|
|
|
|
| 7 |
---
|
| 8 |
|
| 9 |
+
Private CPU integration candidate. Custom FastAPI interface on port 7860.
|
| 10 |
+
Simulator: https://github.com/Zorro909/driving_sim_rs
|
| 11 |
+
Hosting adaptation and UI: TiGa https://github.com/seeker-cyber-maker/driving_sim_rs
|
| 12 |
+
GPU is deliberately unavailable in this candidate. No bundled tracks or networks.
|
| 13 |
+
One server process; no persistent storage. See DATA_POLICY.md for retention limits.
|
| 14 |
+
Not yet verified in the hosted Linux runtime; do not publish as a stable release.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RELEASE_MANIFEST.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "cpu",
|
| 3 |
+
"files": {
|
| 4 |
+
"DATA_POLICY.md": "99844eda4df9f68be4b06ce7671a18474bd2a860ff20b30a221163698bfaef6d",
|
| 5 |
+
"GODOT_LICENSE": "fb2b3bb82c60f1e76841ba2122eb902f8e39c151476fe21ce10c9a05d1045304",
|
| 6 |
+
"IMPORTER_BUILD_RECEIPT.json": "4ba5c533cc617105478e4d85c83f456a7dfa68b1388049624619871ba9e42e9d",
|
| 7 |
+
"LICENSE": "53d5851b6440feeae36f72a5f57fcab37858c48b7084765ece4b0c54b9a28ed8",
|
| 8 |
+
"README.md": "8711574749eb4cc53da0253c40692868b337adb63ce045f2c881840982a032a8",
|
| 9 |
+
"app.py": "40cef7e65bbcf9798eaa1374a2c7be4bb45a5d4e70e981a1eeb8b0b2f2a00c35",
|
| 10 |
+
"assets/formula_template.json": "80f745df82bb7be45af52785fd582460bcddc63c5a4e1d8375d85b3122db4c6b",
|
| 11 |
+
"bin/altd-sim": "2f2f2144ae14b0ef599ca8510f95712da00243c6875881491d384b394e7499f7",
|
| 12 |
+
"bin/extract_saved_track": "3a7733c45b4301a786fcbfe92660a1c484749272da5f608d62f34878bd4ff31a",
|
| 13 |
+
"client.js": "828d29ae0291d9696364415397c113e907d1c512e01c0ef4cb771da63e5a48bf",
|
| 14 |
+
"delivery.py": "effc7e33c11c2e999b48620feaae263975a13df59c85f9c3a12c3f038ad47a5a",
|
| 15 |
+
"index.html": "cf4c0670c81184f8ea2726041457ae3cecc6c79b9030a41fe545b1bc503f0a47",
|
| 16 |
+
"requirements.txt": "dbdedd8a6f30e16dc41bc3e210c1a936f679655420075f8f89bdd40bdd03f91b",
|
| 17 |
+
"server.py": "df0f3d22c55653de9216da2443d17cd098cf5552f66a0fafb742160b5b29eefe",
|
| 18 |
+
"worker.py": "b6178a26cc8cb916d9b24d4f13131f522a8055eaa834b84c4a101307852b9e33"
|
| 19 |
+
},
|
| 20 |
+
"hosted_verified": false,
|
| 21 |
+
"upstream_revision": "d8ab74c25df70b6a86ed101080e33d5fb785ea6f"
|
| 22 |
+
}
|
app.py
CHANGED
|
@@ -1,36 +1,33 @@
|
|
| 1 |
-
|
| 2 |
-
import
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
return run_probe(False)
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
def
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
return gpu_smoke()
|
| 17 |
-
raise ValueError('Choose CPU or GPU')
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
GPU uses up to 90 seconds of included allowance; check quota first. No automatic runs.
|
| 26 |
-
No user uploads; temporary model files are deleted before the receipt is returned.
|
| 27 |
-
This tests backend execution, not useful learning, performance, or full UI integration.
|
| 28 |
-
''')
|
| 29 |
-
backend=gr.Radio(choices=[('CPU — no GPU allowance needed','cpu'),('GPU — uses included GPU allowance','gpu')],value='cpu',label='Compute')
|
| 30 |
-
report=gr.JSON(label='Execution receipt')
|
| 31 |
-
gr.Button('Run selected backend',variant='primary').click(selected_smoke,inputs=backend,outputs=report,api_name='selected_smoke',concurrency_limit=1,concurrency_id='native')
|
| 32 |
-
gr.Button('Run CPU-only test').click(cpu_smoke,outputs=report,api_name='cpu_smoke',concurrency_limit=1,concurrency_id='native')
|
| 33 |
-
gr.Button('Run GPU test').click(gpu_smoke,outputs=report,api_name='gpu_smoke',concurrency_limit=1,concurrency_id='native')
|
| 34 |
-
|
| 35 |
-
if __name__=='__main__':
|
| 36 |
-
demo.queue(max_size=1).launch()
|
|
|
|
| 1 |
+
"""HF CPU release entrypoint. One process owns all ephemeral results."""
|
| 2 |
+
import hashlib
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from server import create_app
|
| 6 |
+
from worker import run_native
|
| 7 |
|
| 8 |
+
ROOT=Path(__file__).resolve().parent
|
|
|
|
| 9 |
|
| 10 |
+
def verify_package(root=ROOT):
|
| 11 |
+
manifest=json.loads((root/'RELEASE_MANIFEST.json').read_text())
|
| 12 |
+
for name,expected in manifest['files'].items():
|
| 13 |
+
path=(root/name).resolve()
|
| 14 |
+
if not path.is_relative_to(root.resolve()) or not path.is_file():
|
| 15 |
+
raise RuntimeError('Invalid release member')
|
| 16 |
+
if hashlib.sha256(path.read_bytes()).hexdigest()!=expected:
|
| 17 |
+
raise RuntimeError('Release integrity check failed: '+name)
|
| 18 |
+
return manifest
|
| 19 |
|
| 20 |
+
def cpu_worker(track,network):
|
| 21 |
+
return run_native(track,network,binary=ROOT/'bin/altd-sim',
|
| 22 |
+
importer=ROOT/'bin/extract_saved_track',
|
| 23 |
+
scene_template=ROOT/'assets/formula_template.json',with_report=True)
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
def main():
|
| 26 |
+
verify_package()
|
| 27 |
+
for name in ('altd-sim','extract_saved_track'):
|
| 28 |
+
(ROOT/'bin'/name).chmod(0o755)
|
| 29 |
+
import uvicorn
|
| 30 |
+
uvicorn.run(create_app(worker=cpu_worker),host='0.0.0.0',port=7860,
|
| 31 |
+
workers=1,access_log=False)
|
| 32 |
|
| 33 |
+
if __name__=='__main__': main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/formula_template.json
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"vehicle": {
|
| 3 |
+
"type": "Formula",
|
| 4 |
+
"mass": 2,
|
| 5 |
+
"inertia_property": 0,
|
| 6 |
+
"inertia_server": 1038.1666259765625,
|
| 7 |
+
"shape_size": [
|
| 8 |
+
73,
|
| 9 |
+
30
|
| 10 |
+
],
|
| 11 |
+
"shape_position": [
|
| 12 |
+
0,
|
| 13 |
+
0
|
| 14 |
+
],
|
| 15 |
+
"shape_rotation": 1.5708,
|
| 16 |
+
"gravity_scale": 1,
|
| 17 |
+
"linear_damp": 0,
|
| 18 |
+
"angular_damp": 0,
|
| 19 |
+
"max_contacts_reported": 4,
|
| 20 |
+
"friction": 0.95,
|
| 21 |
+
"bounce": 0.1,
|
| 22 |
+
"grip": 2,
|
| 23 |
+
"steering_speed": 0.4000000059604645,
|
| 24 |
+
"center_steering": true,
|
| 25 |
+
"air_resistance": 0.10000000149011612,
|
| 26 |
+
"max_velocity": 1200,
|
| 27 |
+
"surfaces": {
|
| 28 |
+
"asphalt": {
|
| 29 |
+
"grip": 1,
|
| 30 |
+
"power": 1,
|
| 31 |
+
"steering": 1
|
| 32 |
+
},
|
| 33 |
+
"dirt": {
|
| 34 |
+
"grip": 0.8,
|
| 35 |
+
"power": 0.5,
|
| 36 |
+
"steering": 1
|
| 37 |
+
},
|
| 38 |
+
"ice": {
|
| 39 |
+
"grip": 0.3,
|
| 40 |
+
"power": 0.5,
|
| 41 |
+
"steering": 1
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"wheels": [
|
| 45 |
+
{
|
| 46 |
+
"position": [
|
| 47 |
+
-11,
|
| 48 |
+
-17
|
| 49 |
+
],
|
| 50 |
+
"steering": true,
|
| 51 |
+
"max_angle_deg": 35,
|
| 52 |
+
"power": 0,
|
| 53 |
+
"brake_power": 3.5,
|
| 54 |
+
"handbrake_power": 0
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"position": [
|
| 58 |
+
11,
|
| 59 |
+
-17
|
| 60 |
+
],
|
| 61 |
+
"steering": true,
|
| 62 |
+
"max_angle_deg": 35,
|
| 63 |
+
"power": 0,
|
| 64 |
+
"brake_power": 3.5,
|
| 65 |
+
"handbrake_power": 0
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"position": [
|
| 69 |
+
-13,
|
| 70 |
+
29
|
| 71 |
+
],
|
| 72 |
+
"steering": false,
|
| 73 |
+
"max_angle_deg": 0,
|
| 74 |
+
"power": 6,
|
| 75 |
+
"brake_power": 3,
|
| 76 |
+
"handbrake_power": 3
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"position": [
|
| 80 |
+
13,
|
| 81 |
+
29
|
| 82 |
+
],
|
| 83 |
+
"steering": false,
|
| 84 |
+
"max_angle_deg": 0,
|
| 85 |
+
"power": 6,
|
| 86 |
+
"brake_power": 3,
|
| 87 |
+
"handbrake_power": 3
|
| 88 |
+
}
|
| 89 |
+
]
|
| 90 |
+
},
|
| 91 |
+
"physics": {
|
| 92 |
+
"real_precision": "single",
|
| 93 |
+
"contact_recycle_radius": 1,
|
| 94 |
+
"contact_max_separation": 1.5,
|
| 95 |
+
"contact_max_allowed_penetration": 0.3,
|
| 96 |
+
"contact_default_bias": 0.8,
|
| 97 |
+
"solver_iterations": 16
|
| 98 |
+
},
|
| 99 |
+
"track": {
|
| 100 |
+
"name": "Generated track template",
|
| 101 |
+
"tiles": [],
|
| 102 |
+
"path": [],
|
| 103 |
+
"walls": [],
|
| 104 |
+
"physics_shapes": []
|
| 105 |
+
},
|
| 106 |
+
"reset_position": [
|
| 107 |
+
0,
|
| 108 |
+
0
|
| 109 |
+
],
|
| 110 |
+
"reset_rotation": 0
|
| 111 |
+
}
|
bin/extract_saved_track
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3a7733c45b4301a786fcbfe92660a1c484749272da5f608d62f34878bd4ff31a
|
| 3 |
+
size 3247776
|
client.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use strict';
|
| 2 |
+
const $ = id => document.getElementById(id);
|
| 3 |
+
let job = null;
|
| 4 |
+
let busy = false;
|
| 5 |
+
let localResult = null;
|
| 6 |
+
let capabilitiesReady = false;
|
| 7 |
+
const say = text => { $('status').textContent = text; };
|
| 8 |
+
function buttons() {
|
| 9 |
+
$('run').disabled = busy || !!job || !capabilitiesReady;
|
| 10 |
+
$('backend').disabled = busy || !!job || !capabilitiesReady;
|
| 11 |
+
$('download').disabled = busy || !job;
|
| 12 |
+
$('erase').disabled = busy || !job;
|
| 13 |
+
$('automatic').disabled = busy || !!job;
|
| 14 |
+
$('saveLocal').disabled = busy || !localResult;
|
| 15 |
+
$('forgetLocal').disabled = busy || !localResult;
|
| 16 |
+
}
|
| 17 |
+
function saveLocal() {
|
| 18 |
+
if(!localResult)return;
|
| 19 |
+
const link=document.createElement('a'),url=URL.createObjectURL(localResult);
|
| 20 |
+
link.href=url;link.download='candidate.evolution';document.body.appendChild(link);link.click();link.remove();
|
| 21 |
+
setTimeout(()=>URL.revokeObjectURL(url),60000);
|
| 22 |
+
}
|
| 23 |
+
$('saveLocal').onclick=saveLocal;
|
| 24 |
+
$('forgetLocal').onclick=()=>{localResult=null;buttons();say('Browser result discarded. Downloaded files are unaffected.');};
|
| 25 |
+
async function request(path, options = {}) {
|
| 26 |
+
const response = await fetch(path, {cache:'no-store', credentials:'omit', ...options});
|
| 27 |
+
if (!response.ok) throw new Error(response.status === 404 ? 'Result unavailable or expired.' : 'Request failed ('+response.status+').');
|
| 28 |
+
return response;
|
| 29 |
+
}
|
| 30 |
+
async function encoded(file) {
|
| 31 |
+
if (!file || file.size > 1048576) throw new Error('Choose both files, no more than 1 MB each.');
|
| 32 |
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
| 33 |
+
let text=''; for (let i=0;i<bytes.length;i+=8192) text+=String.fromCharCode(...bytes.subarray(i,i+8192));
|
| 34 |
+
return btoa(text);
|
| 35 |
+
}
|
| 36 |
+
const headers = () => ({Authorization:'Bearer '+job.key});
|
| 37 |
+
$('run').onclick = async () => {
|
| 38 |
+
localResult=null;
|
| 39 |
+
const started=performance.now();
|
| 40 |
+
$('runState').textContent='RUNNING — waiting for server confirmation';
|
| 41 |
+
$('runState').style.color='#ffd073';
|
| 42 |
+
const tick=()=>{$('runSummary').textContent='Backend: '+$('backend').value.toUpperCase()+'\nElapsed: '+((performance.now()-started)/1000).toFixed(1)+' s\nCompletion is not yet confirmed.';};
|
| 43 |
+
tick(); const progress=setInterval(tick,250);
|
| 44 |
+
busy=true; buttons(); say('Processing the bounded test…');
|
| 45 |
+
try {
|
| 46 |
+
const body={track:await encoded($('track').files[0]),network:await encoded($('network').files[0]),automatic:$('automatic').checked,backend:$('backend').value};
|
| 47 |
+
job=await (await request('/api/process',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json();
|
| 48 |
+
clearInterval(progress);
|
| 49 |
+
$('runState').textContent='COMPLETE — candidate ready to download';
|
| 50 |
+
$('runState').style.color='#62dfb1';
|
| 51 |
+
const r=job.report;
|
| 52 |
+
const metric=n=>typeof n==='number'?n.toFixed(3):'Not reported';
|
| 53 |
+
$('runSummary').textContent=r?[
|
| 54 |
+
'Backend: '+job.backend.toUpperCase(),
|
| 55 |
+
'Generations: '+r.generations_completed+' / '+r.generations_requested+' · Population: '+r.population,
|
| 56 |
+
'Server processing: '+metric(r.elapsed_seconds)+' s',
|
| 57 |
+
'Best score: '+metric(r.best_score)+' · Mean score: '+metric(r.mean_score),
|
| 58 |
+
'Best lap: '+(r.best_lap_seconds==null?'No completed lap':metric(r.best_lap_seconds)+' s'),
|
| 59 |
+
'Cars completing a lap: '+(r.lapped_cars??'Not reported'),
|
| 60 |
+
'Reported simulation ticks: '+(r.simulated_ticks??'Not reported')+' · Requested: '+r.requested_ticks,
|
| 61 |
+
'Candidate validated: '+(r.candidate_validated?'Yes':'No'),
|
| 62 |
+
'A completed test does not establish improved driving.'
|
| 63 |
+
].join('\n'):'Server returned a candidate; detailed metrics unavailable.';
|
| 64 |
+
$('track').value=''; $('network').value='';
|
| 65 |
+
say('Result ready ('+job.backend.toUpperCase()+'). Inputs and intermediate worker files have been removed.');
|
| 66 |
+
} catch (error) {
|
| 67 |
+
clearInterval(progress);
|
| 68 |
+
$('runState').textContent='ERROR — no successful result confirmed';
|
| 69 |
+
$('runState').style.color='#ffb5a2';
|
| 70 |
+
$('runSummary').textContent='Elapsed: '+((performance.now()-started)/1000).toFixed(1)+' s\nNo downloadable result was received. A connection error does not prove the server stopped.';
|
| 71 |
+
say(error.message);
|
| 72 |
+
}
|
| 73 |
+
finally {clearInterval(progress);busy=false; buttons();}
|
| 74 |
+
};
|
| 75 |
+
$('download').onclick = async () => {
|
| 76 |
+
busy=true; buttons();
|
| 77 |
+
try {
|
| 78 |
+
const response=await request('/api/jobs/'+job.id+'/download',{method:'POST',headers:headers()});
|
| 79 |
+
const bytes=await response.arrayBuffer();
|
| 80 |
+
const digest=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),x=>x.toString(16).padStart(2,'0')).join('');
|
| 81 |
+
if (bytes.byteLength!==job.size || digest!==job.sha256) throw new Error('Result validation failed; no deletion acknowledgment sent. Use Delete now or await expiry.');
|
| 82 |
+
// The browser has the complete validated result. Disk-save success cannot be observed.
|
| 83 |
+
localResult=new Blob([bytes]); saveLocal();
|
| 84 |
+
const acknowledgment=await (await request('/api/jobs/'+job.id+'/ack',{method:'POST',headers:{...headers(),'Content-Type':'application/json'},body:JSON.stringify({receipt:response.headers.get('x-delivery-receipt')})})).json();
|
| 85 |
+
if (acknowledgment.deleted) { job=null; say('Result received by browser. Server result deleted. Save your local download; server retrieval is no longer available.'); }
|
| 86 |
+
else say('Result received by browser. Verify your saved file, then select Delete now; repeat downloads remain available until expiry.');
|
| 87 |
+
} catch (error) { say(error.message+' If receipt acknowledgment failed, use Delete now; automatic expiry still applies.'); }
|
| 88 |
+
finally {busy=false; buttons();}
|
| 89 |
+
};
|
| 90 |
+
$('erase').onclick=async()=>{
|
| 91 |
+
if(!confirm('Delete remaining server-side result and revoke download access? Local files are unaffected.'))return;
|
| 92 |
+
busy=true; buttons();
|
| 93 |
+
try {await request('/api/jobs/'+job.id,{method:'DELETE',headers:headers()}); job=null; say('Server result deleted. Local files are unaffected.');}
|
| 94 |
+
catch(error){say(error.message);}
|
| 95 |
+
finally {busy=false; buttons();}
|
| 96 |
+
};
|
| 97 |
+
setInterval(()=>{
|
| 98 |
+
if(!job){$('deadline').textContent='';return;}
|
| 99 |
+
const remaining=Math.max(0,Math.ceil(job.expires_at-Date.now()/1000));
|
| 100 |
+
$('deadline').textContent='Automatic expiry: '+new Date(job.expires_at*1000).toLocaleTimeString()+' · '+Math.floor(remaining/60)+'m '+remaining%60+'s remaining';
|
| 101 |
+
if(!remaining && !busy){job=null; buttons(); say('Result access has expired. Server cleanup runs independently of this browser.');}
|
| 102 |
+
},1000);
|
| 103 |
+
buttons();
|
| 104 |
+
(async()=>{
|
| 105 |
+
try {
|
| 106 |
+
const capabilities=await (await request('/api/capabilities')).json();
|
| 107 |
+
$('gpuOption').disabled=!capabilities.gpu;
|
| 108 |
+
$('backendNote').textContent=capabilities.gpu?'CPU uses no GPU allowance. GPU requires available allowance. No automatic switching.':'CPU available. GPU is not connected on this deployment; no GPU allowance is needed for CPU.';
|
| 109 |
+
capabilitiesReady=true; buttons();
|
| 110 |
+
} catch(error) {say('Cannot connect to the service. Open the hosted app, not a downloaded HTML file.');}
|
| 111 |
+
})();
|
delivery.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Process-local result delivery. No disk cache, public URLs, or background service.
|
| 2 |
+
|
| 3 |
+
Run one ASGI worker. Restarts discard results. This does not promise physical
|
| 4 |
+
memory erasure or control provider logs, swap, snapshots, or response buffers.
|
| 5 |
+
"""
|
| 6 |
+
import asyncio
|
| 7 |
+
from contextlib import asynccontextmanager
|
| 8 |
+
import hashlib
|
| 9 |
+
import hmac
|
| 10 |
+
import secrets
|
| 11 |
+
import threading
|
| 12 |
+
import time
|
| 13 |
+
from fastapi import FastAPI, Header, HTTPException
|
| 14 |
+
from fastapi.responses import Response
|
| 15 |
+
from pydantic import BaseModel, Field
|
| 16 |
+
|
| 17 |
+
class Unavailable(Exception): pass
|
| 18 |
+
class CapacityExceeded(Exception): pass
|
| 19 |
+
|
| 20 |
+
class ResultStore:
|
| 21 |
+
def __init__(self, clock=time.monotonic, wall_clock=time.time, max_bytes=64*1024*1024):
|
| 22 |
+
self.clock, self.wall_clock = clock, wall_clock
|
| 23 |
+
self.max_bytes = max_bytes
|
| 24 |
+
self.lock = threading.RLock()
|
| 25 |
+
self.jobs = {}
|
| 26 |
+
|
| 27 |
+
@property
|
| 28 |
+
def retained_bytes(self):
|
| 29 |
+
with self.lock: return sum(len(j['data']) for j in self.jobs.values())
|
| 30 |
+
|
| 31 |
+
def _delete(self, ident):
|
| 32 |
+
record = self.jobs.pop(ident)
|
| 33 |
+
record['data'][:] = b'\0'*len(record['data'])
|
| 34 |
+
record['data'].clear()
|
| 35 |
+
|
| 36 |
+
def expire(self):
|
| 37 |
+
with self.lock:
|
| 38 |
+
for ident in list(self.jobs):
|
| 39 |
+
if self.clock() >= self.jobs[ident]['deadline']: self._delete(ident)
|
| 40 |
+
|
| 41 |
+
def clear(self):
|
| 42 |
+
with self.lock:
|
| 43 |
+
for ident in list(self.jobs): self._delete(ident)
|
| 44 |
+
|
| 45 |
+
def publish(self, data, automatic=True):
|
| 46 |
+
if not isinstance(data, bytes) or not data or len(data)>4*1024*1024:
|
| 47 |
+
raise CapacityExceeded()
|
| 48 |
+
with self.lock:
|
| 49 |
+
self.expire()
|
| 50 |
+
if len(self.jobs)>=16 or self.retained_bytes+len(data)>self.max_bytes:
|
| 51 |
+
raise CapacityExceeded()
|
| 52 |
+
ident, key = secrets.token_urlsafe(24), secrets.token_urlsafe(32)
|
| 53 |
+
expires = self.wall_clock()+1800
|
| 54 |
+
self.jobs[ident] = dict(key=key, data=bytearray(data), automatic=automatic,
|
| 55 |
+
deadline=self.clock()+1800, expires_at=expires, delivered=False, receipt=None)
|
| 56 |
+
return dict(id=ident,key=key,expires_at=expires,automatic=automatic,
|
| 57 |
+
sha256=hashlib.sha256(data).hexdigest(),size=len(data))
|
| 58 |
+
|
| 59 |
+
def _get(self, ident, key):
|
| 60 |
+
self.expire()
|
| 61 |
+
record = self.jobs.get(ident)
|
| 62 |
+
if not record or not hmac.compare_digest(record['key'],key): raise Unavailable()
|
| 63 |
+
return record
|
| 64 |
+
|
| 65 |
+
def status(self, ident, key):
|
| 66 |
+
with self.lock:
|
| 67 |
+
r = self._get(ident,key)
|
| 68 |
+
return dict(expires_at=r['expires_at'],automatic=r['automatic'],delivered=r['delivered'])
|
| 69 |
+
|
| 70 |
+
def download(self, ident, key):
|
| 71 |
+
with self.lock:
|
| 72 |
+
r = self._get(ident,key)
|
| 73 |
+
if r['automatic'] and r['delivered']: raise Unavailable()
|
| 74 |
+
r['delivered'] = True
|
| 75 |
+
r['receipt'] = secrets.token_urlsafe(32)
|
| 76 |
+
return bytes(r['data']), r['receipt']
|
| 77 |
+
|
| 78 |
+
def acknowledge(self, ident, key, receipt):
|
| 79 |
+
with self.lock:
|
| 80 |
+
r = self._get(ident,key)
|
| 81 |
+
if not r['receipt'] or not hmac.compare_digest(r['receipt'],receipt): raise Unavailable()
|
| 82 |
+
if r['automatic']:
|
| 83 |
+
self._delete(ident)
|
| 84 |
+
return dict(deleted=True)
|
| 85 |
+
return dict(deleted=False,expires_at=r['expires_at'])
|
| 86 |
+
|
| 87 |
+
def delete(self, ident, key):
|
| 88 |
+
with self.lock:
|
| 89 |
+
self._get(ident,key)
|
| 90 |
+
self._delete(ident)
|
| 91 |
+
return dict(deleted=True)
|
| 92 |
+
|
| 93 |
+
class Acknowledgment(BaseModel):
|
| 94 |
+
receipt: str = Field(min_length=1,max_length=128)
|
| 95 |
+
|
| 96 |
+
def create_api(store=None):
|
| 97 |
+
store = store if store is not None else ResultStore()
|
| 98 |
+
@asynccontextmanager
|
| 99 |
+
async def lifespan(app):
|
| 100 |
+
async def expiry():
|
| 101 |
+
while True:
|
| 102 |
+
await asyncio.sleep(1)
|
| 103 |
+
store.expire()
|
| 104 |
+
task = asyncio.create_task(expiry())
|
| 105 |
+
try: yield
|
| 106 |
+
finally:
|
| 107 |
+
task.cancel()
|
| 108 |
+
try: await task
|
| 109 |
+
except asyncio.CancelledError: pass
|
| 110 |
+
store.clear()
|
| 111 |
+
|
| 112 |
+
app = FastAPI(lifespan=lifespan,docs_url=None,redoc_url=None,openapi_url=None)
|
| 113 |
+
app.state.results = store
|
| 114 |
+
@app.middleware('http')
|
| 115 |
+
async def headers(request,call_next):
|
| 116 |
+
response = await call_next(request)
|
| 117 |
+
response.headers['Cache-Control']='no-store'
|
| 118 |
+
response.headers['Referrer-Policy']='no-referrer'
|
| 119 |
+
response.headers['X-Content-Type-Options']='nosniff'
|
| 120 |
+
return response
|
| 121 |
+
|
| 122 |
+
def key(authorization):
|
| 123 |
+
if not authorization or not authorization.startswith('Bearer '):
|
| 124 |
+
raise HTTPException(404,'Job unavailable')
|
| 125 |
+
value = authorization[7:]
|
| 126 |
+
if not value.isascii() or len(value)>128: raise HTTPException(404,'Job unavailable')
|
| 127 |
+
return value
|
| 128 |
+
|
| 129 |
+
def call(fn,*args):
|
| 130 |
+
try: return fn(*args)
|
| 131 |
+
except Unavailable: raise HTTPException(404,'Job unavailable') from None
|
| 132 |
+
|
| 133 |
+
@app.get('/api/jobs/{ident}')
|
| 134 |
+
def status(ident:str,authorization:str|None=Header(default=None)):
|
| 135 |
+
return call(store.status,ident,key(authorization))
|
| 136 |
+
|
| 137 |
+
@app.post('/api/jobs/{ident}/download')
|
| 138 |
+
def download(ident:str,authorization:str|None=Header(default=None)):
|
| 139 |
+
data,receipt = call(store.download,ident,key(authorization))
|
| 140 |
+
return Response(data,media_type='application/octet-stream',headers={
|
| 141 |
+
'Content-Disposition':'attachment; filename="candidate.evolution"',
|
| 142 |
+
'X-Delivery-Receipt':receipt})
|
| 143 |
+
|
| 144 |
+
@app.post('/api/jobs/{ident}/ack')
|
| 145 |
+
def ack(ident:str,body:Acknowledgment,authorization:str|None=Header(default=None)):
|
| 146 |
+
return call(store.acknowledge,ident,key(authorization),body.receipt)
|
| 147 |
+
|
| 148 |
+
@app.delete('/api/jobs/{ident}')
|
| 149 |
+
def delete(ident:str,authorization:str|None=Header(default=None)):
|
| 150 |
+
return call(store.delete,ident,key(authorization))
|
| 151 |
+
return app
|
index.html
CHANGED
|
@@ -1,138 +1,23 @@
|
|
| 1 |
-
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
| 2 |
<style>
|
| 3 |
-
:root{color-scheme:dark;font:16px system-ui;background:#0c1420;color:#e5edf5}body{max-width:1320px;margin:auto;padding:28px}h1{font-size:32px
|
| 4 |
-
|
| 5 |
-
<
|
| 6 |
-
<
|
| 7 |
-
<
|
| 8 |
-
<
|
| 9 |
-
<
|
| 10 |
-
<label for="
|
| 11 |
-
<label
|
| 12 |
-
<
|
| 13 |
-
<
|
| 14 |
-
<
|
| 15 |
-
<
|
| 16 |
-
<
|
| 17 |
-
<
|
| 18 |
-
<
|
| 19 |
-
<
|
| 20 |
-
<
|
| 21 |
-
<
|
| 22 |
-
<
|
| 23 |
-
<label for="decay">Weight decay</label><input id="decay" type="number" min="0" max="1" step="0.0001" value="0"><small>Allowed 0–1; suggested 0–0.001. Decay 0.05 shrinks children by 5%.</small>
|
| 24 |
-
</details>
|
| 25 |
-
<details id="reward_settings"><summary>Rewards · presets and up to five metrics</summary>
|
| 26 |
-
<label for="reward_preset">Reward preset</label><select id="reward_preset"><option value="custom">Custom</option><option value="distance">First laps · distance only</option><option value="center">Your starter mix · center, clearance, collisions</option><option value="clean">Clean driving · distance + collision penalty</option><option value="lap">Lap refinement · best lap only</option><option value="multilap">Endurance · distance only, longer horizon</option></select><p id="reward_preset_note" class="hint"></p>
|
| 27 |
-
<div id="rewards"></div><p id="budget" class="hint"></p><button id="add">+ Add reward</button><p id="guidance" class="hint"></p><small>Weights −100…+100; absolute shares normalize to 100%. Five distinct metrics maximum. Braking is zero for this two-output network.</small>
|
| 28 |
-
</details></div>
|
| 29 |
-
<details id="run_settings"><summary>Run settings · timing and elimination</summary><div class="row"><div><label for="control_hz">Decisions per second</label><select id="control_hz"><option>60</option><option>30</option><option>15</option></select></div><div><label for="workers">Parallel simulations</label><select id="workers"><option>1</option><option selected>2</option><option>3</option><option>4</option></select></div></div><small>Physics always 60 Hz. Start with 2 workers; frozen replay uses one car.</small><div id="seed_group"><label for="seed">Random seed</label><input id="seed" type="number" min="0" max="2147483647" value="42105"></div><label><input id="collision" type="checkbox"> Eliminate on wall contact</label><label><input id="idle" type="checkbox"> Eliminate when stuck</label></details>
|
| 30 |
-
</div>
|
| 31 |
-
<div class="card"><details id="comparison"><summary>Website comparison and track downloads</summary><p><a href="https://drivinglab.jectrum.de/" target="_blank" rel="noopener">Open Drive Lab ↗</a></p><p class="hint">Optional website WebGPU comparison. Our local engines use CPU here; native WebSocket server port 47800. No recipes are sent there by this dashboard.</p><details><summary>All track downloads</summary><div id="track_downloads"></div></details></details></div></section>
|
| 32 |
-
<section><div class="card run-card"><h2>Run & results</h2><div class="toolbar"><button class="primary" id="start">Start</button><button id="stop" disabled>Stop run</button><button id="reset">Reset form</button><a id="download" hidden>Download candidate .evolution</a></div><div id="error" role="alert"></div><div id="status">Ready. Select a network to begin.</div><details id="run_notes" hidden><summary>Run details</summary><p id="run_note" class="hint"></p></details><div id="artifacts"></div>
|
| 33 |
-
<section id="map_panel" style="margin-top:18px"><h2>Selected track & terrain</h2><div id="map_legend" class="hint"></div><canvas id="map" width="800" height="550"></canvas><p id="map_note" class="hint">Choose a track to preview its layout.</p></section>
|
| 34 |
-
<details id="history_panel"><summary>Generation results</summary><div class="table-scroll"><table><thead><tr><th>Generation</th><th>Net laps (ours)</th><th>Distance (native)</th><th>Lap / circuit</th><th>Contacts</th></tr></thead><tbody id="history"></tbody></table></div></details>
|
| 35 |
-
<details id="saved_panel"><summary>Saved runs</summary><div id="runs"></div><small>Exact inputs and settings are kept with each run. Original-game validation remains separate.</small></details>
|
| 36 |
-
</div></section></div>
|
| 37 |
-
</div>
|
| 38 |
-
<section id="training" class="pane" hidden><div class="card"><h2>Training settings</h2><p class="hint">Choose the engine and track on Drive & view. Save settings and rewards separately, then combine their permanent references into a run.</p><div id="training_home"></div><div id="timing_home"></div></div><div id="library_status" role="status"></div><p><button id="refresh_presets">Refresh library</button> <a href="/api/presets" download="tao-preset-library.json">Download preset library</a></p><div class="library" id="libraries"></div></section>
|
| 39 |
-
<section id="batch" class="pane" hidden><div class="card"><h2>Training batches · A–J</h2><label for="batch_slot">Batch slot</label><select id="batch_slot"></select><label for="batch_name">Display name</label><input id="batch_name" type="text" maxlength="80" value="My curriculum"><label><input id="carry" type="checkbox" checked> Continue from the previous run’s candidate</label><p class="hint">Unchecked: each run starts from the original uploaded network. Repeated run references are allowed. Recipes freeze when the batch starts.</p><div id="queue"></div><button id="queue_add">+ Add run</button><div class="toolbar"><button id="batch_save">Save batch</button><button id="batch_load">Load batch</button><button class="primary" id="batch_start">Start saved batch</button><button id="batch_stop" disabled>Stop batch</button></div><div id="batch_status" role="status">No batch selected.</div><div id="batch_history"></div><div id="preset_message" role="status"></div></div></section>
|
| 40 |
-
<section id="editor" class="pane" hidden><div class="card"><h2>Network editor</h2><p class="hint">An original editor inspired by <a href="https://the-mars-guy.github.io/AI-Learns-To-Drive-Tool/" target="_blank" rel="noopener">Mars’s network creator</a>. Formula Beginner inputs and steering/acceleration outputs remain fixed here. You can create and download other hidden-layer shapes; the trainer accepts Formula networks with steering/acceleration outputs. Native CPU supports additional sensors; Godot currently supports raycasts + speed.</p><div class="toolbar"><label for="editor_file">Load an existing .evolution file</label><input id="editor_file" type="file" accept=".evolution"><button id="editor_import">Inspect selected network</button><button id="editor_create">Create fresh network</button><button id="editor_apply">Use edited network</button><a id="editor_download" hidden download>Download .evolution</a></div><div class="row"><div><label for="editor_name">Network name</label><input id="editor_name" type="text" maxlength="80" value="TAO fresh network"><label for="editor_shape">Layer sizes</label><input id="editor_shape" type="text" value="6,4,2"><small>6 inputs → hidden layers of 1–64 nodes → 2 outputs. Up to 20 layers.</small></div><div><label for="editor_seed">Initialization seed</label><input id="editor_seed" type="number" min="0" max="2147483647" value="42105"><label><input id="editor_xavier" type="checkbox" checked> Xavier uniform weights</label><label><input id="editor_bias" type="checkbox"> Xavier uniform biases</label><small>Unchecked: zeros. Biases default to zero.</small></div></div><p id="editor_status" role="status">Create a fresh network or inspect the selected source.</p><p id="editor_metadata" class="hint"></p><p id="editor_legality" class="hint"></p><details id="editor_history_panel"><summary>Training history · recorded generations</summary><div class="table-scroll"><table><thead><tr><th>Record</th><th>Time</th><th>Mutation</th><th>Best score</th><th>Best lap</th></tr></thead><tbody id="editor_history"></tbody></table></div><details><summary>Full saved training metadata</summary><pre id="editor_stats"></pre></details></details><canvas id="network_map" width="1100" height="450"></canvas><details id="weight_values"><summary>Individual weight and bias values</summary><div class="table-scroll" style="max-height:420px;overflow:auto"><table><thead><tr><th>Connection / bias</th><th>Value</th></tr></thead><tbody id="editor_parameters"></tbody></table></div></details><details><summary>Weights, biases and input contract</summary><pre id="editor_contract"></pre><label for="editor_json">NeuralNetwork JSON · editable weights and biases</label><textarea id="editor_json" rows="16" style="width:100%;box-sizing:border-box;background:#0c1725;color:#e5edf5"></textarea></details></div></section><script>
|
| 41 |
-
const $=id=>document.getElementById(id);let trackCatalog=[],catalog=[],data=null,job=null,lastPath=null,mapRequest=0,activeRun=false;const json=async(url,opts)=>{let r=await fetch(url,opts),x=await r.json();if(!r.ok)throw Error(x.error||'Request failed');return x};
|
| 42 |
-
function reward(metric='TotalScore',type='Default',weight=100){if($('rewards').children.length>=5)return;let used=[...$('rewards').children].map(r=>r.children[0].value);if(used.includes(metric))metric=catalog.find(r=>!used.includes(r.metric)).metric;let row=document.createElement('div');row.className='reward';let sel=document.createElement('select'),typ=document.createElement('select'),w=document.createElement('input'),del=document.createElement('button');sel.setAttribute('aria-label','Reward metric');typ.setAttribute('aria-label','Reward averaging');w.setAttribute('aria-label','Reward weight');catalog.forEach(r=>sel.add(new Option(r.label,r.metric)));sel.value=metric;w.type='number';w.min=-100;w.max=100;w.value=weight;del.textContent='×';del.setAttribute('aria-label','Remove reward');function change(){let r=catalog.find(r=>r.metric===sel.value);typ.replaceChildren();r.types.forEach(t=>typ.add(new Option(t,t)));$('guidance').textContent=r.description+' '+r.game_sign_guidance;budget();}sel.onchange=()=>{customReward();change()};del.onclick=()=>{customReward();row.remove();budget()};w.oninput=()=>{customReward();budget()};typ.onchange=()=>{customReward();budget()};row.append(sel,typ,w,del);$('rewards').append(row);change();typ.value=type;budget();}
|
| 43 |
-
function customReward(){$('reward_preset').value='custom';$('reward_preset_note').textContent='Custom reward mix; the percentages below show its effective allocation.'}
|
| 44 |
-
function budget(){let rows=[...$('rewards').children],sum=rows.reduce((a,r)=>a+Math.abs(Number(r.children[2].value)),0);rows.forEach(row=>[...row.children[0].options].forEach(o=>o.disabled=rows.some(other=>other!==row&&other.children[0].value===o.value)));$('add').disabled=rows.length>=5;$('budget').textContent=rows.length+'/5 slots · absolute weight total '+sum+' → 100% allocation: '+rows.map(r=>r.children[0].selectedOptions[0].text+' '+(sum?100*Number(r.children[2].value)/sum:0).toFixed(1)+'%').join(' · ')}
|
| 45 |
-
function support(){
|
| 46 |
-
const native=$('backend').value==='github',train=$('mode').value==='train';
|
| 47 |
-
const r=trackCatalog.find(x=>x.name===$('track').value);
|
| 48 |
-
$('track_support').textContent=native?'Local native CPU · all-track training and replay; Formula vehicle.':(r?.ours_training?'Godot · A01 training and replay.':'Godot · replay only here; native engine supports training.');
|
| 49 |
-
$('training_controls').hidden=!train;$('seed_group').hidden=!train;
|
| 50 |
-
$('native_settings').hidden=!(native&&train);
|
| 51 |
-
if(!native){$('adaptive_mutation').checked=false;$('schedule').value='fixed';$('native_settings').open=false;}
|
| 52 |
-
['schedule','mutation_end','adaptive_mutation','selection','parents','elite','crossover','decay','preset'].forEach(k=>$(k).disabled=!(native&&train));
|
| 53 |
-
$('end_rate_group').hidden=!(native&&train&&$('schedule').value!=='fixed');
|
| 54 |
-
$('mutation_label').textContent=native?'Mutation rate (native)':'Mutation strength (Gaussian σ)';
|
| 55 |
-
$('start').textContent=train?'Start training':'Run frozen network';
|
| 56 |
-
if(!activeRun&&(!data||train))$('map_panel').open=false;
|
| 57 |
-
}
|
| 58 |
-
function applyRewardPreset(k){
|
| 59 |
-
if(k==='custom')return;
|
| 60 |
-
const recipes={distance:[['TotalScore','Default',100]],center:[['TotalScore','Default',100],['TotalDistanceFromCenter','Average',-60],['TotalDistanceFromWall','Average',10],['CollisionCount','Default',-100],['BestLapPerformance','Default',5]],clean:[['TotalScore','Default',100],['CollisionCount','Default',-30]],lap:[['BestLapPerformance','Default',100]],multilap:[['TotalScore','Default',100]]};
|
| 61 |
-
$('rewards').replaceChildren();recipes[k].forEach(row=>reward(...row));$('reward_preset').value=k;
|
| 62 |
-
$('reward_preset_note').textContent={distance:'Learn forward progress before optimizing speed.',center:'Your five-metric starter mix. Center and clearance can conflict with tight racing lines; relax them later.',clean:'Distance stays dominant; collisions carry a smaller penalty.',lap:'Use after laps are reliable. Faster completed laps lead; incomplete laps may provide little selection signal.',multilap:'Distance rewards sustained progress. Set the horizon long enough for multiple laps; this preset does not change timing.'}[k];
|
| 63 |
-
}
|
| 64 |
-
$('reward_preset').onchange=()=>applyRewardPreset($('reward_preset').value);
|
| 65 |
-
$('mode').onchange=()=>{support();if(data&&$('mode').value==='replay')$('map_panel').open=true;};
|
| 66 |
-
$('schedule').onchange=support;
|
| 67 |
-
$('track').onchange=()=>{support();draw(null,$('track').value);};
|
| 68 |
-
$('preset').onchange=()=>{let k=$('preset').value;if(k==='custom')return;$('backend').value='github';$('mode').value='train';$('population').value=64;$('generations').value=30;$('parents').value=3;$('elite').value=1;$('selection').value='best';$('crossover').value='none';$('schedule').value='fixed';$('adaptive_mutation').checked=false;$('mutation').value={starter:.3,explore:.5,refine:.1,speed:.05}[k];$('decay').value={starter:.0005,explore:.0005,refine:.0001,speed:0}[k];$('idle').checked=true;$('collision').checked=k==='refine'||k==='speed';$('seconds').value=k==='starter'||k==='explore'?15:90;applyRewardPreset(k==='speed'?'lap':'distance');support();$('backend').onchange()};
|
| 69 |
-
let editorData=null,editorUrl=null;
|
| 70 |
-
async function readFile(){let f=$('file').files[0];if(!f)throw Error('Browse for a network first');return await new Promise((resolve,reject)=>{let reader=new FileReader();reader.onerror=()=>reject(Error('Could not read the selected file'));reader.onload=()=>resolve({data:reader.result.split(',')[1],filename:f.name});reader.readAsDataURL(f);});}
|
| 71 |
-
async function readSource(){let x;if($('source_library').checked)x=await json('/api/library/'+$('library_car').value);else if($('source_latest').checked)x=await json('/api/latest');else if($('source_editor').checked){if(!editorData)throw Error('Apply the editor network first');x=editorData;}else x=await readFile();data=x.data;$('network').textContent=x.filename;let info=await json('/api/network/editor',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'inspect',data})});$('network_contract').textContent='Game rules: '+info.legality.classification+' · '+[...info.legality.errors,...info.legality.warnings].join(' · ')+' · Shape '+info.network.Shape.join(' → ')+' · Native: '+info.compatibility.github+' · Godot: '+info.compatibility.ours;return data;}
|
| 72 |
-
$('add').onclick=()=>{customReward();reward()};$('sample').onclick=async()=>{try{$('source_latest').checked=true;await readSource();$('error').textContent='';$('map_panel').open=$('mode').value==='replay';draw(null,$('track').value);}catch(e){$('error').textContent=e.message}};
|
| 73 |
-
$('file').onchange=async()=>{try{$('source_upload').checked=true;await readSource();$('error').textContent='';$('map_panel').open=$('mode').value==='replay';draw(null,$('track').value)}catch(e){$('error').textContent=e.message}};
|
| 74 |
-
['source_upload','source_latest','source_editor','source_library'].forEach(k=>$(k).onchange=async()=>{try{await readSource();$('error').textContent=''}catch(e){$('network').textContent='Selected source not loaded';$('error').textContent=e.message}});
|
| 75 |
-
$('library_load').onclick=async()=>{try{$('source_library').checked=true;await readSource();$('error').textContent='';}catch(e){$('error').textContent=e.message}};
|
| 76 |
-
function showEditor(x){let n=x.network;$('editor_json').value=JSON.stringify(n,null,2);$('editor_shape').value=n.Shape.join(',');$('editor_status').textContent='Shape '+n.Shape.join(' → ')+' · '+n.Weights.reduce((sum,w,i)=>sum+w.flat().length+n.Biases[i].length,0)+' parameters · '+'Shape parsed · engine compatibility depends on vehicle, sensors and outputs';if(x.name)$('editor_name').value=x.name;showEditorInfo(x);if(x.sensors)$('editor_contract').textContent=JSON.stringify({sensors:x.sensors,outputs:x.outputs,vehicle:x.vehicle},null,2);drawNetwork(n);}
|
| 77 |
-
function drawNetwork(n){let cv=$('network_map'),ctx=cv.getContext('2d');ctx.clearRect(0,0,cv.width,cv.height);let points=n.Shape.map((count,l)=>Array.from({length:count},(_,i)=>[45+l*(cv.width-90)/(n.Shape.length-1),35+(i+.5)*(cv.height-70)/count]));n.Weights.forEach((w,l)=>w.forEach((row,i)=>row.forEach((v,j)=>{ctx.strokeStyle=v<0?'#f27676':'#62dfb1';ctx.globalAlpha=Math.min(.8,.08+Math.abs(v)*.35);ctx.lineWidth=Math.min(4,.4+Math.abs(v));ctx.beginPath();ctx.moveTo(...points[l][i]);ctx.lineTo(...points[l+1][j]);ctx.stroke();})));ctx.globalAlpha=1;points.forEach(layer=>layer.forEach(([x,y])=>{ctx.fillStyle='#0c1725';ctx.strokeStyle='#e5edf5';ctx.beginPath();ctx.arc(x,y,Math.min(7,140/layer.length),0,Math.PI*2);ctx.fill();ctx.stroke();}));}
|
| 78 |
-
function showEditorInfo(x){let generations=x.stats?.Generations||[],times=generations.map(g=>g.Time).filter(v=>typeof v==='number'&&Number.isFinite(v));let total=times.reduce((a,b)=>a+b,0),duration=Math.floor(total/3600)+'h '+Math.floor(total%3600/60)+'m '+(total%60).toFixed(1)+'s';$('editor_metadata').textContent='Vehicle: '+['Formula','Rally','Snowmobile','Truck'][x.vehicle]+' · '+generations.length+' saved generation records · recorded training time '+duration+' ('+total.toFixed(1)+' s)'+(times.length!==generations.length?' (some record times missing)':'')+' · '+(x.snapshots?.length||0)+' snapshots';$('editor_legality').textContent=x.legality?('Game rules: '+x.legality.classification+' · '+[...x.legality.errors,...x.legality.warnings].join(' · ')):'';$('editor_stats').textContent=JSON.stringify({stats:x.stats,snapshots:x.snapshots||[]},null,2);$('editor_history').replaceChildren();generations.forEach((g,i)=>{let row=document.createElement('tr');[i+1,g.Time==null?'—':g.Time+' s',g.MutationRate??'—',g.BestScore??'—',g.BestLapTime??'—'].forEach(v=>{let td=document.createElement('td');td.textContent=v;row.append(td)});$('editor_history').append(row)});$('editor_parameters').replaceChildren();let n=x.network;function param(label,path,value){let row=document.createElement('tr'),cell=document.createElement('td'),val=document.createElement('td'),input=document.createElement('input');cell.textContent=label;input.type='number';input.step='any';input.value=value;input.setAttribute('aria-label',label);input.oninput=()=>{let edited=JSON.parse($('editor_json').value),target=edited;path.slice(0,-1).forEach(k=>target=target[k]);target[path.at(-1)]=Number(input.value);$('editor_json').value=JSON.stringify(edited,null,2);drawNetwork(edited)};val.append(input);row.append(cell,val);$('editor_parameters').append(row);}n.Weights.forEach((w,l)=>w.forEach((r,i)=>r.forEach((v,j)=>param('Layer '+(l+1)+' · input '+i+' → node '+j,['Weights',l,i,j],v))));n.Biases.forEach((b,l)=>b.forEach((v,j)=>param('Layer '+(l+1)+' · node '+j+' bias',['Biases',l,j],v)));}
|
| 79 |
-
$('editor_file').onchange=async()=>{try{let f=$('editor_file').files[0];if(!f)return;let bytes=await f.arrayBuffer(),encoded=btoa(Array.from(new Uint8Array(bytes),v=>String.fromCharCode(v)).join(''));data=encoded;editorData={data,filename:f.name};showEditor(await editorRequest('inspect'));$('editor_status').textContent+=' · Loaded '+f.name;}catch(e){$('editor_status').textContent=e.message}};
|
| 80 |
-
async function editorRequest(action,extra={}){return await json('/api/network/editor',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action,data,...extra})});}
|
| 81 |
-
$('editor_import').onclick=async()=>{try{await readSource();editorData={data,filename:$('network').textContent};showEditor(await editorRequest('inspect'));}catch(e){$('editor_status').textContent=e.message}};
|
| 82 |
-
$('editor_create').onclick=async()=>{try{let x=await editorRequest('create',{data:null,shape:$('editor_shape').value.split(',').map(Number),seed:Number($('editor_seed').value),xavier:$('editor_xavier').checked,xavier_bias:$('editor_bias').checked,name:$('editor_name').value});editorData=x;showEditor(x);editorDownload(x);}catch(e){$('editor_status').textContent=e.message}};
|
| 83 |
-
function editorDownload(x){if(editorUrl)URL.revokeObjectURL(editorUrl);editorUrl=URL.createObjectURL(new Blob([Uint8Array.from(atob(x.data),c=>c.charCodeAt(0))],{type:'application/octet-stream'}));$('editor_download').href=editorUrl;$('editor_download').download=x.filename;$('editor_download').hidden=false;}
|
| 84 |
-
$('editor_apply').onclick=async()=>{try{let x=await editorRequest('save',{data:editorData?.data||data,network:JSON.parse($('editor_json').value),name:$('editor_name').value});editorData=x;data=x.data;showEditor(x);editorDownload(x);$('editor_source_group').hidden=false;$('source_editor').checked=true;$('network').textContent=x.filename;$('editor_status').textContent+=' · Applied as active source';}catch(e){$('editor_status').textContent=e.message}};
|
| 85 |
-
$('start').onclick=async()=>{try{$('error').textContent='';await readSource();let c=readConfig();let x=await json('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({config:c,data})});job=x.id;lastPath=null;activeRun=true;$('history_panel').open=c.mode==='train';$('map_panel').open=c.mode==='replay';$('download').hidden=true;await poll();}catch(e){$('error').textContent=e.message}};
|
| 86 |
-
|
| 87 |
-
function readConfig(){let c={};['seconds','population','generations','workers','control_hz','seed','mutation','parents','elite','decay','mutation_end'].forEach(k=>c[k]=Number($(k).value));['mode','track','backend','selection','crossover','schedule'].forEach(k=>c[k]=$(k).value);['collision','idle','adaptive_mutation','require_standard'].forEach(k=>c[k]=$(k).checked);if(c.backend==='ours'){c.adaptive_mutation=false;c.schedule='fixed';c.decay=0;c.selection='best';c.parents=1;c.elite=1;c.crossover='none';}if(c.mode==='replay'){c.mutation=0;c.decay=0;c.adaptive_mutation=false;c.schedule='fixed';c.population=1;c.generations=1;c.parents=1;c.elite=1;c.mutation_end=0;}c.rewards=[...$('rewards').children].map(r=>({metric:r.children[0].value,type:r.children[1].value,weight:Number(r.children[2].value)}));if(c.mode==='replay')c.rewards=[{metric:'TotalScore',type:'Default',weight:100}];return c;}
|
| 88 |
-
let book=null,batchId=null;
|
| 89 |
-
function tab(id){if(id==='training'){$('mode').value='train';support();}document.querySelectorAll('.pane').forEach(p=>p.hidden=p.id!==id);document.querySelectorAll('[data-tab]').forEach(b=>b.setAttribute('aria-selected',String(b.dataset.tab===id)));}
|
| 90 |
-
document.querySelectorAll('[data-tab]').forEach(b=>b.onclick=()=>tab(b.dataset.tab));
|
| 91 |
-
$('training_home').append($('training_controls'));$('timing_home').append($('run_settings'));
|
| 92 |
-
const slots='ABCDEFGHIJ'.split('');
|
| 93 |
-
function fillSlots(sel,family){let old=sel.value;sel.replaceChildren(...slots.map(k=>new Option(k+' · '+(book?.[family]?.[k]?.name||'Empty'),k)));if(old)sel.value=old;}
|
| 94 |
-
function setConfig(c){Object.entries(c).forEach(([k,v])=>{if(k==='rewards'||!$(k))return;if(typeof v==='boolean')$(k).checked=v;else $(k).value=v;});support();draw(null,$('track').value);}
|
| 95 |
-
function setRewards(rows){$('rewards').replaceChildren();rows.forEach(r=>reward(r.metric,r.type,r.weight));customReward();}
|
| 96 |
-
function refreshBook(){['settings','rewards','runs'].forEach(f=>fillSlots($(f+'_slot'),f));fillSlots($('run_settings_ref'),'settings');fillSlots($('run_rewards_ref'),'rewards');fillSlots($('batch_slot'),'batches');document.querySelectorAll('.queue-row select').forEach(s=>fillSlots(s,'runs'));}
|
| 97 |
-
async function saveSlot(f,record){book=await json('/api/presets',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({revision:book.revision,family:f,slot:$(f==='batches'?'batch_slot':f+'_slot').value,record})});refreshBook();$('preset_message').textContent='Saved · library revision '+book.revision;$('error').textContent='';$('library_status').textContent='Saved · library revision '+book.revision;}
|
| 98 |
-
function notice(e){$('preset_message').textContent=e.message;$('error').textContent=e.message;}
|
| 99 |
-
function buildLibraries(){['settings','rewards','runs'].forEach(f=>{let card=document.createElement('div');card.className='card';let h=document.createElement('h2');h.textContent={settings:'Settings slots',rewards:'Reward slots',runs:'Training run slots'}[f];card.append(h);let sel=document.createElement('select');sel.id=f+'_slot';sel.setAttribute('aria-label',f+' slot');card.append(sel);let name=document.createElement('input');name.id=f+'_name';name.type='text';name.maxLength=80;name.placeholder='Display name';name.setAttribute('aria-label',f+' display name');card.append(name);if(f==='runs'){['settings','rewards'].forEach(t=>{let label=document.createElement('label');label.textContent=t+' reference';label.htmlFor='run_'+t+'_ref';let ref=document.createElement('select');ref.id='run_'+t+'_ref';card.append(label,ref);});}let save=document.createElement('button'),load=document.createElement('button');save.textContent='Save slot';load.textContent='Load slot';save.onclick=async()=>{try{let record={name:name.value};if(f==='settings'){record.config=readConfig();record.config.mode='train';}if(f==='rewards')record.rows=[...$('rewards').children].map(r=>({metric:r.children[0].value,type:r.children[1].value,weight:Number(r.children[2].value)}));if(f==='runs'){record.settings=$('run_settings_ref').value;record.rewards=$('run_rewards_ref').value;}await saveSlot(f,record);}catch(e){notice(e)}};load.onclick=()=>{try{let r=book[f][sel.value];if(!r)throw Error('This slot is empty');name.value=r.name;if(f==='settings')setConfig(r.config);if(f==='rewards')setRewards(r.rows);if(f==='runs'){$('run_settings_ref').value=r.settings;$('run_rewards_ref').value=r.rewards;setConfig(book.settings[r.settings].config);setRewards(book.rewards[r.rewards].rows);} }catch(e){notice(e)}};sel.onchange=()=>{name.value=book[f][sel.value]?.name||'';};card.append(save,load);$('libraries').append(card);});}
|
| 100 |
-
function queueRow(slot='A'){let row=document.createElement('div');row.className='queue-row';let sel=document.createElement('select');sel.setAttribute('aria-label','Queued training run');fillSlots(sel,'runs');sel.value=slot;let del=document.createElement('button');del.textContent='Remove';del.onclick=()=>row.remove();let up=document.createElement('button'),down=document.createElement('button');up.textContent='↑';down.textContent='↓';up.setAttribute('aria-label','Move run earlier');down.setAttribute('aria-label','Move run later');up.onclick=()=>{if(row.previousElementSibling)row.before(row.previousElementSibling);};down.onclick=()=>{if(row.nextElementSibling)row.after(row.nextElementSibling);};row.append(sel,up,down,del);$('queue').append(row);}
|
| 101 |
-
$('refresh_presets').onclick=async()=>{book=await json('/api/presets');refreshBook();$('library_status').textContent='Loaded revision '+book.revision;};
|
| 102 |
-
$('reset').onclick=()=>{if(activeRun)return notice(Error('Finish or stop the active run before resetting'));$('mode').value='replay';$('backend').value='github';$('track').value='A01 Beginner';$('seconds').value=60;$('population').value=8;$('generations').value=30;$('mutation').value=.1;$('parents').value=1;$('elite').value=1;$('workers').value=2;$('seed').value=42105;$('control_hz').value=60;$('collision').checked=false;$('idle').checked=false;$('decay').value=0;$('adaptive_mutation').checked=false;$('schedule').value='fixed';applyRewardPreset('center');support();draw(null,$('track').value);};
|
| 103 |
-
$('queue_add').onclick=()=>{if($('queue').children.length<100)queueRow();};
|
| 104 |
-
$('batch_save').onclick=async()=>{try{await saveSlot('batches',{name:$('batch_name').value,carry:$('carry').checked,runs:[...$('queue').children].map(r=>r.children[0].value)});}catch(e){notice(e)}};
|
| 105 |
-
$('batch_load').onclick=()=>{let b=book.batches[$('batch_slot').value];if(!b)return notice(Error('This batch slot is empty'));$('batch_name').value=b.name;$('carry').checked=b.carry;$('queue').replaceChildren();b.runs.forEach(queueRow);};
|
| 106 |
-
$('batch_start').onclick=async()=>{try{await readSource();let b=await json('/api/batch/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({slot:$('batch_slot').value,data})});batchId=b.id;await pollBatches();}catch(e){notice(e)}};
|
| 107 |
-
$('batch_stop').onclick=async()=>{if(batchId)await json('/api/batch/stop',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:batchId})});};
|
| 108 |
-
async function pollBatches(){try{let all=await json('/api/batches');let active=all.find(b=>['queued','running'].includes(b.state));if(active)batchId=active.id;let b=all.find(b=>b.id===batchId);$('batch_stop').disabled=!active;$('batch_start').disabled=!!active; if(b){$('batch_status').textContent=b.slot+' · '+b.name+' · '+b.state+' · run '+b.step+'/'+b.total+(b.error?' · '+b.error:'');if(active&&b.current_job&&job!==b.current_job){job=b.current_job;lastPath=null;poll();}}$('batch_history').replaceChildren();all.slice().reverse().forEach(b=>{let p=document.createElement('p'),button=document.createElement('button');button.textContent=b.name+' · '+b.state+' · '+b.step+'/'+b.total;button.onclick=()=>{batchId=b.id;if(b.current_job){job=b.current_job;lastPath=null;poll();}pollBatches();};p.append(button);['manifest','status'].forEach(name=>{let a=document.createElement('a');a.href='/batches/'+b.id+'/'+name+'.json';a.textContent=' · '+name;a.download=b.id+'-'+name+'.json';p.append(a);});$('batch_history').append(p);});}catch(e){notice(e)}}
|
| 109 |
-
$('stop').onclick=async()=>{if(job)await json('/api/stop',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:job})})};
|
| 110 |
-
function num(x,d=2){return x==null?'—':Number(x).toFixed(d)}
|
| 111 |
-
const modelCache=new Map();
|
| 112 |
-
function decode(x){if(x&&typeof x==='object'){if(x.$ald_f64_le){let bytes=new Uint8Array(x.$ald_f64_le.match(/../g).map(h=>parseInt(h,16)));return new DataView(bytes.buffer).getFloat64(0,true)}if(Array.isArray(x))return x.map(decode);return Object.fromEntries(Object.entries(x).map(([k,v])=>[k,decode(v)]))}return x;}
|
| 113 |
-
async function draw(path,track){
|
| 114 |
-
const request=++mapRequest;
|
| 115 |
-
try{
|
| 116 |
-
if(!modelCache.has(track))modelCache.set(track,await json('/godot/assets/imported/'+encodeURIComponent(track)+'.model.json'));
|
| 117 |
-
const m=modelCache.get(track).track,polys=m.tiles.flatMap(t=>(t.collision?.layers||[]).flatMap(l=>l.polygons_world||[]));
|
| 118 |
-
const tiles=m.tiles.map(t=>[t.cell_origin,[t.cell_origin[0]+768,t.cell_origin[1]+768]]);
|
| 119 |
-
let pts=[];
|
| 120 |
-
if(path){let response=await fetch('/'+path+'/trajectory.jsonl');if(!response.ok)throw Error('Trajectory unavailable');let txt=await response.text();pts=txt.trim().split('\n').filter(Boolean).map(s=>decode(JSON.parse(s)).pose.position).filter(p=>p.every(Number.isFinite));}
|
| 121 |
-
if(request!==mapRequest)return;
|
| 122 |
-
const all=[...tiles.flat(),...pts];if(!all.length)return;
|
| 123 |
-
const xs=all.map(p=>p[0]),ys=all.map(p=>p[1]),minx=Math.min(...xs),maxx=Math.max(...xs),miny=Math.min(...ys),maxy=Math.max(...ys);
|
| 124 |
-
const cv=$('map'),ctx=cv.getContext('2d'),scale=Math.min((cv.width-40)/(maxx-minx||1),(cv.height-40)/(maxy-miny||1));
|
| 125 |
-
const screen=p=>[20+(p[0]-minx)*scale,20+(p[1]-miny)*scale];ctx.clearRect(0,0,cv.width,cv.height);
|
| 126 |
-
m.tiles.forEach(t=>{let [x,y]=screen(t.cell_origin);ctx.fillStyle={asphalt:'#52708b',dirt:'#ae7246',ice:'#79cde5'}[t.surface]||'#52708b';ctx.fillRect(x,y,768*scale,768*scale)});
|
| 127 |
-
polys.forEach(poly=>{ctx.beginPath();poly.forEach((p,i)=>ctx[i?'lineTo':'moveTo'](...screen(p)));ctx.closePath();ctx.fillStyle='#0a111c';ctx.fill();ctx.strokeStyle='#b5c3cf';ctx.lineWidth=1;ctx.stroke()});
|
| 128 |
-
if(pts.length){ctx.beginPath();pts.forEach((p,i)=>ctx[i?'lineTo':'moveTo'](...screen(p)));ctx.strokeStyle='#ffb064';ctx.lineWidth=2;ctx.stroke();let [x,y]=screen(pts.at(-1));ctx.fillStyle='#ffb064';ctx.beginPath();ctx.arc(x,y,4,0,2*Math.PI);ctx.fill();}
|
| 129 |
-
let r=trackCatalog.find(r=>r.name===track);if(r?.ours_spawn){let [x,y]=screen(r.ours_spawn.position);ctx.fillStyle='#8ce8c1';ctx.beginPath();ctx.arc(x,y,4,0,2*Math.PI);ctx.fill();}
|
| 130 |
-
let counts={};m.tiles.forEach(t=>counts[t.surface]=(counts[t.surface]||0)+1);$('map_legend').replaceChildren();Object.entries(counts).forEach(([surface,count])=>{let label=document.createElement('span');label.style.marginRight='16px';let swatch=document.createElement('span');swatch.textContent='■ ';swatch.style.color={asphalt:'#52708b',dirt:'#ae7246',ice:'#79cde5'}[surface]||'#52708b';label.append(swatch,document.createTextNode(surface+' · '+count+' tiles'));$('map_legend').append(label)});let obstacle=document.createElement('span');obstacle.textContent='▧ Dark outlined areas: collision walls / solid obstacles · Green: spawn · Orange: recorded path';$('map_legend').append(obstacle);$('map').dataset.track=track;$('map').dataset.trajectoryPoints=String(pts.length);$('map').dataset.wallPolygons=String(polys.length);
|
| 131 |
-
$('map_note').textContent=track+' · '+(pts.length?'Orange = frozen/selected car trajectory; green = spawn.':'Track layout preview; green = spawn. Native jobs do not export car trajectories; Godot replay adds the actual path.');
|
| 132 |
-
}catch(e){if(request===mapRequest)$('map_note').textContent='Map unavailable: '+e.message;}
|
| 133 |
-
}
|
| 134 |
-
$('backend').onchange=()=>{support();draw(null,$('track').value);};
|
| 135 |
-
async function poll(){if(!job)return;try{let j=await json('/api/job/'+job);let active=['queued','running'].includes(j.state);activeRun=active;$('start').disabled=active;$('reset').disabled=active;$('stop').disabled=!active;$('status').textContent=`${j.state.toUpperCase()} · ${j.network_name}\n${j.config.track} · ${j.config.mode} · ${j.config.backend||'ours'}\nGeneration ${j.generation} / ${j.config.mode==='train'?j.config.generations:1} · ${j.evaluations} evaluations\nElapsed ${num(j.elapsed,1)} s${j.error?'\n'+j.error:''}`; $('run_notes').hidden=!j.note;$('run_note').textContent=j.note||'';$('history').replaceChildren();j.history.slice(-30).forEach(h=>{let tr=document.createElement('tr');[h.generation,num(h.score.progress,3),num(h.score.native_distance_score,3),h.score.full_circuit_seconds==null?'—':num(h.score.full_circuit_seconds,1)+' s',h.score.contacts==null?'—':h.score.contacts].forEach(v=>{let td=document.createElement('td');td.textContent=v;tr.append(td)});$('history').append(tr)});if(j.best){if(!j.best.path&&lastPath!=='native:'+j.id){lastPath='native:'+j.id;await draw(null,j.config.track);}$('download').hidden=(j.config.backend==='github'&&!j.candidate_ready&&j.state==='running');$('download').href='/runs/'+j.id+'/candidate.evolution';if(j.best.path && lastPath!==j.best.path){lastPath=j.best.path;await draw(lastPath,j.config.track);if(j.config.mode==='replay')$('map_panel').open=true}}$('artifacts').replaceChildren();(j.artifacts||[]).forEach(a=>{let el=document.createElement('a');el.href='/'+a.path;el.textContent='Best lap: '+a.label;let para=document.createElement('p');para.append(el);$('artifacts').append(para)});if(!active)await loadRuns();}catch(e){$('error').textContent=e.message}}
|
| 136 |
-
async function loadRuns(){let c=await json('/api/config');$('runs').replaceChildren();c.runs.reverse().forEach(j=>{let p=document.createElement('p'),b=document.createElement('button');b.textContent=j.id+' · '+j.state;b.onclick=()=>{job=j.id;lastPath=null;$('map_panel').open=j.config.mode==='replay';$('history_panel').open=j.config.mode==='train';poll()};p.append(b);$('runs').append(p)})}
|
| 137 |
-
(async()=>{let c=await json('/api/config');catalog=c.rewards;trackCatalog=c.track_catalog;book=await json('/api/presets');let cars=await json('/api/library');cars.forEach(c=>$('library_car').add(new Option(c.name,c.id)));$('library_load').disabled=!cars.length;buildLibraries();refreshBook();queueRow();await pollBatches();setInterval(pollBatches,1500);['A','B','R','Bonus'].forEach(group=>{let opt=document.createElement('optgroup');opt.label=group==='Bonus'?'Bonus':'Series '+group;trackCatalog.filter(r=>r.group===group).forEach(r=>opt.append(new Option(r.name,r.name)));$('track').append(opt)});support();trackCatalog.forEach(r=>{let a=document.createElement('a');a.href='/tracks/'+encodeURIComponent(r.name)+'.track';a.textContent=r.name;let q=document.createElement('p');q.append(a);$('track_downloads').append(q)});applyRewardPreset('center');await draw(null,$('track').value);await loadRuns();let live=c.runs.find(j=>['queued','running'].includes(j.state));if(live)job=live.id;setInterval(poll,1500)})().catch(e=>$('error').textContent=e.message);
|
| 138 |
-
</script></html>
|
|
|
|
| 1 |
+
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
| 2 |
+
<title>AI Learns To Drive — HF integration test</title>
|
| 3 |
<style>
|
| 4 |
+
:root{color-scheme:dark;font:16px system-ui;background:#0c1420;color:#e5edf5}body{max-width:1320px;margin:auto;padding:28px}h1{font-size:32px}h2{font-size:21px}a{color:#7ce4c3}p{line-height:1.6}.grid{display:grid;grid-template-columns:minmax(300px,430px) minmax(0,1fr);gap:24px}.card{background:#152233;border:1px solid #2a3d52;border-radius:16px;padding:22px;margin-top:20px}input,select,button{font:inherit;background:#0c1725;color:#e5edf5;border:1px solid #49627c;border-radius:7px;padding:9px;max-width:100%;box-sizing:border-box}button{cursor:pointer;margin:4px 0}button:disabled{opacity:.45;cursor:default}#run{background:#62dfb1;color:#102522;font-weight:700}#status{white-space:pre-wrap}small,.hint{color:#a2b4c8}.map{min-height:260px;background:#09111b;border-radius:10px;display:grid;place-content:center;text-align:center} @media(max-width:850px){.grid{grid-template-columns:1fr}body{padding:16px}}
|
| 5 |
+
</style>
|
| 6 |
+
<body><main><h1>AI Learns To Drive</h1>
|
| 7 |
+
<p>Game by ActivationFunction · <a href="https://store.steampowered.com/app/3312030/AI_Learns_To_Drive/">Steam store</a></p>
|
| 8 |
+
<p>Simulator by <a href="https://github.com/Zorro909/driving_sim_rs">Zorro909</a> · UI and HF implementation by <a href="https://github.com/seeker-cyber-maker/driving_sim_rs">TiGa (seeker-cyber-maker)</a></p>
|
| 9 |
+
<p class="hint">Private release integration · bounded training test. No app account needed; private hosting may require access.</p>
|
| 10 |
+
<div class="grid"><section class="card"><h2>Simulation</h2><p>4 vehicles · 1 generation · 60 requested ticks. Formula networks, steering and acceleration; speed and raycast sensors. This is not yet the full trainer.</p>
|
| 11 |
+
<p><label for="backend">Compute</label><select id="backend" disabled><option value="cpu">CPU — no GPU allowance</option><option id="gpuOption" value="gpu" disabled>GPU — uses GPU allowance</option></select></p><p id="backendNote" class="hint">Checking available backends…</p>
|
| 12 |
+
<p><label>Track (.track) <input id="track" type="file" accept=".track"></label></p>
|
| 13 |
+
<p><label>Formula network (.evolution) <input id="network" type="file" accept=".evolution"></label></p>
|
| 14 |
+
<p><label><input id="automatic" type="checkbox" checked> Automatically delete server-side job data after receiving the result</label></p>
|
| 15 |
+
<p>If unchecked, results expire 30 minutes after processing finishes. Repeat downloads do not extend that deadline. <a href="/policy" target="_blank" rel="noopener">Full data handling & deletion policy</a></p>
|
| 16 |
+
<button id="run" disabled>Run short test</button></section><section class="card"><h2>Run & results</h2><button id="download" disabled>Download result</button> <button id="erase" disabled>Delete server-side job data now</button>
|
| 17 |
+
<p><button id="saveLocal" disabled>Save received result again (browser only)</button> <button id="forgetLocal" disabled>Discard browser result</button></p>
|
| 18 |
+
<p>The browser keeps the received result in memory until discarded, replaced by a new run, or this page is closed. It is not saved to browser storage.</p>
|
| 19 |
+
<h3 id="runState" role="status" aria-live="polite">READY — no run started</h3>
|
| 20 |
+
<pre id="runSummary" style="white-space:pre-wrap;line-height:1.7" aria-live="polite"></pre>
|
| 21 |
+
<p id="deadline"></p><p id="status" role="status">Ready. Inputs remain on this device until you run the test.</p>
|
| 22 |
+
<h2>Selected track & terrain</h2><div class="map" role="img" aria-label="Track map placeholder"><strong>Track map</strong><p>Renderer not connected yet.</p></div></section></div>
|
| 23 |
+
</main><script src="/client.js"></script></body></html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1 +1,2 @@
|
|
| 1 |
-
|
|
|
|
|
|
| 1 |
+
fastapi==0.141.1
|
| 2 |
+
uvicorn==0.52.1
|
server.py
CHANGED
|
@@ -1,290 +1,60 @@
|
|
| 1 |
-
"""
|
| 2 |
-
import
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
-
|
| 5 |
-
from
|
| 6 |
-
from
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
from reward_extraction import RewardEvaluation,extract_rewards,reward_models
|
| 11 |
-
RUNS=ROOT/'runs';RUNS.mkdir(exist_ok=True)
|
| 12 |
-
CAT=json.loads((ROOT/'reward-catalog.json').read_text())
|
| 13 |
-
for r in CAT:r['enabled']=True;r['reason']=None
|
| 14 |
-
SAVELOCK=threading.RLock()
|
| 15 |
-
TRACK_CATALOG=json.loads((ROOT/'tracks/catalog.json').read_text());TRACKS=[r['name'] for r in TRACK_CATALOG];JOBS={};LOCK=threading.RLock()
|
| 16 |
-
SAMPLE=ROOT/'demo.evolution'
|
| 17 |
-
|
| 18 |
-
def parse_network(data):
|
| 19 |
-
if data[:2]==b'\x1f\x8b':
|
| 20 |
-
import io
|
| 21 |
-
with gzip.GzipFile(fileobj=io.BytesIO(data)) as f:data=f.read(8*1024*1024+1)
|
| 22 |
-
if len(data)>8*1024*1024:raise ValueError('Network exceeds 8 MB')
|
| 23 |
-
x=json.loads(data)
|
| 24 |
-
if 'NeuralNetwork' not in x:raise ValueError('Upload a game .evolution network')
|
| 25 |
-
n=x['NeuralNetwork'];shape=n['Shape']
|
| 26 |
-
if x.get('Type')!=0:raise ValueError('This dashboard currently simulates Formula physics. This file is a different vehicle type; choosing an engine does not change its vehicle physics.')
|
| 27 |
-
if not isinstance(shape,list) or not 2<=len(shape)<=20 or any(type(z)!=int or not 1<=z<=64 for z in shape):raise ValueError('Network layers must contain 1–64 nodes, with 2–20 layers')
|
| 28 |
-
if x.get('Outputs')!=[0,1] or shape[-1]!=2:raise ValueError('Current dashboard output contract is steering + acceleration only; brake, handbrake and boost adapters are pending')
|
| 29 |
-
sensors=x.get('Sensors')
|
| 30 |
-
if not isinstance(sensors,list) or len(sensors)!=shape[0]:raise ValueError('Sensor count does not match the network input layer')
|
| 31 |
-
supported={'raycast','speed','trackCurvature','wheelAngle','angularVelocity','correctDirection','grip','accelerationFront','accelerationSide','distanceFromWall','boostCapacity','velocityFront','velocitySide'}
|
| 32 |
-
if any(sensor.get('$type') not in supported for sensor in sensors):raise ValueError('Network contains a sensor type not supported by the native adapter')
|
| 33 |
-
if len(n['Weights'])!=len(shape)-1 or len(n['Biases'])!=len(shape)-1:raise ValueError('Weight/bias layer count differs from shape')
|
| 34 |
-
v=[]
|
| 35 |
-
for i,(a,b) in enumerate(zip(shape[:-1],shape[1:])):
|
| 36 |
-
w=n['Weights'][i];bs=n['Biases'][i]
|
| 37 |
-
if len(w)!=a or any(len(row)!=b for row in w) or len(bs)!=b:raise ValueError('Malformed weight dimensions')
|
| 38 |
-
v.extend(z for row in w for z in row);v.extend(bs)
|
| 39 |
-
if any(type(z) not in (int,float) or not math.isfinite(z) for z in v):raise ValueError('All weights must be finite numbers')
|
| 40 |
-
return x,v
|
| 41 |
-
|
| 42 |
-
def engine_contract(template,c):
|
| 43 |
-
if c.get('backend','ours')=='ours':
|
| 44 |
-
missing=sorted({r['$type'] for r in template['Sensors']} - {'raycast','speed'})
|
| 45 |
-
if missing:raise ValueError('Our Godot runner does not yet sample '+', '.join(missing)+'. Choose the local Native CPU engine for this Formula network.')
|
| 46 |
-
|
| 47 |
-
def shaped_network(template,v):
|
| 48 |
-
n=template['NeuralNetwork'];shape=n['Shape'];weights=[];biases=[];offset=0
|
| 49 |
-
for a,b in zip(shape[:-1],shape[1:]):
|
| 50 |
-
weights.append([v[offset+i*b:offset+(i+1)*b] for i in range(a)]);offset+=a*b;biases.append(v[offset:offset+b]);offset+=b
|
| 51 |
-
if offset!=len(v):raise ValueError('Parameter vector does not match shape')
|
| 52 |
-
return {'shape':shape,'weights':weights,'biases':biases,'sensors':template['Sensors'],'outputs':template['Outputs']}
|
| 53 |
-
|
| 54 |
-
def validate(c):
|
| 55 |
-
def integer(k,lo,hi):
|
| 56 |
-
z=c[k]
|
| 57 |
-
if type(z)!=int or not lo<=z<=hi:raise ValueError(f'{k} must be {lo}–{hi}')
|
| 58 |
-
for k,lo,hi in [('seconds',1,300),('population',1,128),('generations',1,1000),('workers',1,4),('control_hz',15,60),('seed',0,2147483647)]:integer(k,lo,hi)
|
| 59 |
-
if c['control_hz'] not in (15,30,60):raise ValueError('Choose 15, 30 or 60 controller decisions/sec')
|
| 60 |
-
if c['track'] not in TRACKS or c['mode'] not in ('replay','train'):raise ValueError('Unknown track or mode')
|
| 61 |
-
if c.get('backend')!='github':raise ValueError('Cloud CPU Space supports the native comparison engine only; our Godot runner remains local')
|
| 62 |
-
if c.get('backend','ours') not in ('ours','github'):raise ValueError('Unknown simulator')
|
| 63 |
-
if c.get('backend','ours')=='ours' and c['mode']=='train' and c['track']!='A01 Beginner':raise ValueError('Our runner currently validates training rewards on A01 only. Choose the local GitHub engine to train this track.')
|
| 64 |
-
z=c['mutation']
|
| 65 |
-
if type(z) not in (float,int) or not math.isfinite(z) or not 0<=z<=4.2:raise ValueError('Mutation strength must be 0–4.2')
|
| 66 |
-
for k in ['collision','idle']:
|
| 67 |
-
if type(c[k])!=bool:raise ValueError('Invalid elimination setting')
|
| 68 |
-
rows=c['rewards']
|
| 69 |
-
if not isinstance(rows,list) or not 1<=len(rows)<=5:raise ValueError('Select 1–5 reward slots')
|
| 70 |
-
catalog={x['metric']:x for x in CAT};seen=set()
|
| 71 |
-
for r in rows:
|
| 72 |
-
ident=r['metric']
|
| 73 |
-
if ident in seen or r['metric'] not in catalog or r['type'] not in catalog[r['metric']]['types']:raise ValueError('Invalid or duplicate reward')
|
| 74 |
-
seen.add(ident)
|
| 75 |
-
if type(r['weight'])!=int or not -100<=r['weight']<=100:raise ValueError('Reward weights must be −100 to 100')
|
| 76 |
-
if not any(r['weight'] for r in rows):raise ValueError('At least one reward weight must be nonzero')
|
| 77 |
-
if c.get('selection','best') not in ('best','tournament','roulette') or c.get('crossover','none') not in ('none','single_point','uniform'):raise ValueError('Unknown selection or crossover')
|
| 78 |
-
for k in ['parents','elite']:
|
| 79 |
-
z=c.get(k,1)
|
| 80 |
-
if type(z)!=int or not (0 if k=='elite' else 1)<=z<=c['population']:raise ValueError(f'{k} must fit the population')
|
| 81 |
-
z=c.get('decay',0)
|
| 82 |
-
if type(z) not in (float,int) or not math.isfinite(z) or not 0<=z<=1:raise ValueError('Decay must be 0–1')
|
| 83 |
-
for k in ['adaptive_mutation']:
|
| 84 |
-
if type(c.get(k,False))!=bool:raise ValueError('Invalid adaptive mutation setting')
|
| 85 |
-
if c.get('schedule','fixed') not in ('fixed','linear','geometric'):raise ValueError('Unknown mutation schedule')
|
| 86 |
-
end=c.get('mutation_end',c['mutation'])
|
| 87 |
-
if type(end) not in (int,float) or not math.isfinite(end) or not 0<=end<=4.2:raise ValueError('End mutation must be 0–4.2')
|
| 88 |
-
if c.get('backend','ours')=='github' and c['mode']=='train' and c.get('schedule','fixed')!='fixed' and (c['mutation']==0 or end==0):raise ValueError('Native schedules need positive start/end rates. Use fixed mutation 0 for frozen replay.')
|
| 89 |
-
if c.get('backend','ours')=='ours' and (c.get('schedule','fixed')!='fixed' or c.get('adaptive_mutation',False)):raise ValueError('Scheduled/adaptive native mutation requires the GitHub engine')
|
| 90 |
-
return c
|
| 91 |
-
|
| 92 |
-
def public_job(job):
|
| 93 |
-
return {k:v for k,v in job.items() if k not in ('cancel','processes','template','vector')}
|
| 94 |
-
|
| 95 |
-
def save(job):
|
| 96 |
-
public=public_job(job)
|
| 97 |
-
with SAVELOCK:
|
| 98 |
-
target=RUNS/job['id']/'status.json';temp=target.with_suffix('.tmp');temp.write_text(json.dumps(public,indent=2,allow_nan=False));temp.replace(target)
|
| 99 |
-
return public
|
| 100 |
-
|
| 101 |
-
def rollout(v,d,c,job):
|
| 102 |
-
d.mkdir();request={'ticks':c['seconds']*60,'record_trace':True,'network':shaped_network(job['template'],v),'track':c['track'],'control_interval':60//c['control_hz'],'collision_elimination':c['collision'],'idle_elimination':c['idle']}
|
| 103 |
-
request['spawn']=next(r['ours_spawn'] for r in TRACK_CATALOG if r['name']==c['track'])
|
| 104 |
-
(d/'request.json').write_text(json.dumps(request));t=time.perf_counter()
|
| 105 |
-
with (d/'stdout.log').open('w') as a,(d/'stderr.log').open('w') as b:
|
| 106 |
-
p=subprocess.Popen([str(core.ENGINE),'--headless','--disable-render-loop','--fixed-fps','60','--path',str(ROOT/'godot'),'--log-file',str(d/'engine.log'),'--script','res://benchmark.gd','--',str(d)],stdout=a,stderr=b)
|
| 107 |
-
with LOCK:job['processes'].append(p)
|
| 108 |
-
try:
|
| 109 |
-
while p.poll() is None:
|
| 110 |
-
if job['cancel'].is_set():p.terminate();p.wait(timeout=10);raise InterruptedError('Stopped by user')
|
| 111 |
-
if time.perf_counter()-t>180:p.terminate();p.wait(timeout=10);raise RuntimeError('Simulation exceeded three-minute timeout')
|
| 112 |
-
time.sleep(.05)
|
| 113 |
-
if p.returncode:raise RuntimeError('Simulation failed: '+(d/'stderr.log').read_text()[-1500:])
|
| 114 |
-
finally:
|
| 115 |
-
with LOCK:job['processes'].remove(p)
|
| 116 |
-
result=core.decode(json.loads((d/'result.json').read_text()))
|
| 117 |
-
if result['coordinator_error']:raise RuntimeError(str(result['coordinator_error']))
|
| 118 |
-
if c['track']=='A01 Beginner':s=core.collect(d)
|
| 119 |
-
else:s={'ticks':result['ticks'],'contacts':result['contact_ticks'],'progress':None,'full_circuit_seconds':None,'raw':{},'stats_count':result['ticks']//6}
|
| 120 |
-
s['wall_seconds']=time.perf_counter()-t;s['path']=str(d.relative_to(ROOT));(d/'score.json').write_text(json.dumps(s,indent=2));return s
|
| 121 |
|
| 122 |
-
|
| 123 |
-
mean=[sum(v[i] for v in vectors)/len(vectors) for i in range(len(vectors[0]))]
|
| 124 |
-
for s,v in zip(scores,vectors):s['raw']['NetworkNovelty']=math.sqrt(sum((a-b)**2 for a,b in zip(v,mean)))
|
| 125 |
-
raw={k:[s['raw'][k] for s in scores] for k in scores[0]['raw']};policy={(r['metric'],r['type']):r['weight'] for r in rows}
|
| 126 |
-
selected,_=extract_rewards(RewardEvaluation(raw,[s['stats_count'] for s in scores]),reward_models(policy),len(scores))
|
| 127 |
-
rewards=core.combine_rewards(selected,policy)
|
| 128 |
-
return sorted(range(len(scores)),key=lambda i:(-rewards.relative[i],i)),list(rewards.relative)
|
| 129 |
-
|
| 130 |
-
def export(x,v,path):
|
| 131 |
-
x=copy.deepcopy(x);n=shaped_network(x,v);x['NeuralNetwork']['Weights']=n['weights'];x['NeuralNetwork']['Biases']=n['biases'];x['Name']='TAO dashboard candidate';x['Id']=str(uuid.uuid4());x['Stats']={'Generations':[]};x['Snapshots']=[];x['OpponentId']=None;x['SteamWorkshopId']=None;x['Description']='Local simulator candidate; original-game validation pending.'
|
| 132 |
-
path.write_bytes(gzip.compress(json.dumps(x).encode(),mtime=0));parse_network(path.read_bytes())
|
| 133 |
-
|
| 134 |
-
def run(job):
|
| 135 |
-
d=RUNS/job['id'];c=job['config'];rng=random.Random(c['seed']);v=job['vector'].copy();start=time.perf_counter()
|
| 136 |
-
try:
|
| 137 |
-
job['state']='running';save(job)
|
| 138 |
-
if c.get('backend')=='github':
|
| 139 |
-
from github_adapter import execute
|
| 140 |
-
execute(job,save,export);job['state']='complete';return
|
| 141 |
-
for gen in range(c['generations'] if c['mode']=='train' else 1):
|
| 142 |
-
if job['cancel'].is_set():raise InterruptedError('Stopped by user')
|
| 143 |
-
pop=[v.copy()]
|
| 144 |
-
if c['mode']=='train':pop += [[z+rng.gauss(0,c['mutation']) for z in v] for _ in range(c['population']-1)]
|
| 145 |
-
(d/f'g{gen:04d}-weights.json').write_text(json.dumps(pop))
|
| 146 |
-
def ev(pair):
|
| 147 |
-
i,w=pair;s=rollout(w,d/f'g{gen:04d}-c{i:04d}',c,job)
|
| 148 |
-
with LOCK:job['evaluations']+=1
|
| 149 |
-
return s
|
| 150 |
-
with ThreadPoolExecutor(max_workers=c['workers']) as pool:scores=list(pool.map(ev,enumerate(pop)))
|
| 151 |
-
order,rewards=rank(scores,pop,c['rewards']) if c['mode']=='train' else ([0],[0])
|
| 152 |
-
winner=order[0];v=pop[winner];best=scores[winner]
|
| 153 |
-
job['history'].append({'generation':gen+1,'score':best,'winner':winner,'rewards':rewards,'rank':order});job['generation']=gen+1;job['best']=best;job['elapsed']=time.perf_counter()-start
|
| 154 |
-
(d/f'g{gen:04d}-selection.json').write_text(json.dumps(job['history'][-1],indent=2));export(job['template'],v,d/'candidate.evolution');save(job)
|
| 155 |
-
job['state']='complete'
|
| 156 |
-
except InterruptedError:job['state']='stopped'
|
| 157 |
-
except Exception as e:job['state']='failed';job['error']=str(e)
|
| 158 |
-
finally:job['elapsed']=time.perf_counter()-start;save(job)
|
| 159 |
-
|
| 160 |
-
def make_job(c,data,batch_id=None):
|
| 161 |
-
template,v=parse_network(data);legality=network_legality(template)
|
| 162 |
-
if legality['errors']:raise ValueError('Invalid network: '+'; '.join(legality['errors']))
|
| 163 |
-
if c.get('require_standard',True) and not legality['standard']:raise ValueError('Network is outside standard game rules. Review the legality report; research mode can explicitly allow modified networks.')
|
| 164 |
-
engine_contract(template,c)
|
| 165 |
-
with LOCK:
|
| 166 |
-
ident=time.strftime('%Y%m%d-%H%M%S')+'-'+uuid.uuid4().hex[:6];d=RUNS/ident;d.mkdir();(d/'input.evolution').write_bytes(data)
|
| 167 |
-
job={'id':ident,'output_dir':str(d),'batch_id':batch_id,'state':'queued','config':c,'network_name':template.get('Name','Uploaded network'),'input_sha256':hashlib.sha256(data).hexdigest(),'generation':0,'evaluations':0,'history':[],'cancel':threading.Event(),'processes':[],'template':template,'vector':v}
|
| 168 |
-
JOBS[ident]=job;(d/'config.json').write_text(json.dumps(c,indent=2));save(job)
|
| 169 |
-
return job
|
| 170 |
-
|
| 171 |
-
from network_legality import network_legality
|
| 172 |
-
from preset_store import Store
|
| 173 |
-
from batch_runner import Batches
|
| 174 |
-
PRESETS=Store(ROOT/'preset-library',validate)
|
| 175 |
-
BATCHES=Batches(ROOT/'batches',PRESETS,parse_network,make_job,run)
|
| 176 |
-
|
| 177 |
-
def network_library():
|
| 178 |
-
base=Path('/Volumes/Steam/CrossOver/ald')
|
| 179 |
-
if not base.exists():base=Path.home()/'Library/Application Support/CrossOver/ald'
|
| 180 |
-
return {hashlib.sha256(str(p.relative_to(base)).encode()).hexdigest()[:16]:p for p in sorted(base.rglob('*.evolution')) if p.is_file() and p.stat().st_size<=8*1024*1024} if base.exists() else {}
|
| 181 |
-
|
| 182 |
-
def latest_network():
|
| 183 |
-
candidates=[(RUNS/j['id']/'candidate.evolution',j) for j in list(JOBS.values()) if j['state']=='complete']
|
| 184 |
-
candidates=[(p,j) for p,j in candidates if p.is_file()]
|
| 185 |
-
if not candidates:raise ValueError('No completed candidate exists yet. Upload a network or create one in the editor.')
|
| 186 |
-
p,j=max(candidates,key=lambda pair:pair[0].stat().st_mtime_ns)
|
| 187 |
-
return {'filename':j['id']+'/candidate.evolution','job_id':j['id'],'data':base64.b64encode(p.read_bytes()).decode()}
|
| 188 |
|
| 189 |
-
def
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
-
class Handler(BaseHTTPRequestHandler):
|
| 227 |
-
def allowed(self):
|
| 228 |
-
host=self.headers.get('Host','').split(':')[0]
|
| 229 |
-
origins=('https://huggingface.co','https://tiga-rce-ald-tao-gpu.hf.space','http://localhost:7860','http://127.0.0.1:7860')
|
| 230 |
-
return host in ('localhost','127.0.0.1','tiga-rce-ald-tao-gpu.hf.space','0.0.0.0') and self.headers.get('Origin',origins[0]) in origins
|
| 231 |
-
def send(self,x,status=200):
|
| 232 |
-
data=json.dumps(x,allow_nan=False).encode();self.send_response(status);self.send_header('Content-Type','application/json');self.send_header('Content-Length',str(len(data)));self.end_headers();self.wfile.write(data)
|
| 233 |
-
def do_GET(self):
|
| 234 |
-
if not self.allowed():return self.send({'error':'Local access only'},403)
|
| 235 |
-
if self.path=='/api/presets':return self.send(PRESETS.read())
|
| 236 |
-
if self.path=='/api/batches':return self.send([BATCHES.public(b) for b in list(BATCHES.items.values())])
|
| 237 |
-
if self.path=='/api/config':return self.send({'service':'tao-local-driving-dashboard','port':self.server.server_address[1],'tracks':TRACKS,'track_catalog':TRACK_CATALOG,'rewards':CAT,'runs':[public_job(j) for j in list(JOBS.values())]})
|
| 238 |
-
if self.path.startswith('/api/job/'):
|
| 239 |
-
j=JOBS.get(self.path.split('/')[-1]);return self.send(public_job(j) if j else {'error':'Unknown run'},200 if j else 404)
|
| 240 |
-
if self.path=='/api/library':return self.send([{'id':id,'name':str(p.relative_to(p.parents[1])) if p.parent.name in ('Steam','Steam-AI') else p.name} for id,p in network_library().items()])
|
| 241 |
-
if self.path.startswith('/api/library/'):
|
| 242 |
-
p=network_library().get(self.path.rsplit('/',1)[-1])
|
| 243 |
-
if not p:return self.send({'error':'Saved network not found'},404)
|
| 244 |
-
return self.send({'filename':p.parent.name+'/'+p.name,'data':base64.b64encode(p.read_bytes()).decode()})
|
| 245 |
-
if self.path=='/api/latest':
|
| 246 |
-
try:return self.send(latest_network())
|
| 247 |
-
except ValueError as e:return self.send({'error':str(e)},400)
|
| 248 |
-
if self.path=='/api/sample':return self.send({'filename':SAMPLE.name,'data':base64.b64encode(SAMPLE.read_bytes()).decode()})
|
| 249 |
-
path=ROOT/('index.html' if self.path=='/' else unquote(self.path.lstrip('/')))
|
| 250 |
-
path=path.resolve()
|
| 251 |
-
if not path.is_relative_to(ROOT) or not path.is_file() or path.suffix not in ('.html','.evolution','.track','.json','.jsonl'):return self.send({'error':'Not found'},404)
|
| 252 |
-
data=path.read_bytes();typ='text/html' if path.suffix=='.html' else 'application/octet-stream';self.send_response(200);self.send_header('Content-Type',typ);self.send_header('Content-Length',str(len(data)))
|
| 253 |
-
if path.suffix in ('.evolution','.track'):self.send_header('Content-Disposition',f'attachment; filename="{path.name}"')
|
| 254 |
-
self.end_headers();self.wfile.write(data)
|
| 255 |
-
def do_POST(self):
|
| 256 |
-
if not self.allowed():return self.send({'error':'Local access only'},403)
|
| 257 |
-
try:
|
| 258 |
-
size=int(self.headers.get('Content-Length','0'))
|
| 259 |
-
if not 0<size<=12*1024*1024:raise ValueError('Request too large')
|
| 260 |
-
x=json.loads(self.rfile.read(size))
|
| 261 |
-
if self.path=='/api/stop':
|
| 262 |
-
job=JOBS[x['id']]
|
| 263 |
-
if job.get('batch_id'):BATCHES.stop(job['batch_id'],JOBS)
|
| 264 |
-
else:job['cancel'].set()
|
| 265 |
-
return self.send({'state':'stopping'})
|
| 266 |
-
if self.path=='/api/network/editor':return self.send(edit_network(x))
|
| 267 |
-
if self.path=='/api/presets':return self.send(PRESETS.update(x))
|
| 268 |
-
if self.path=='/api/batch/stop':return self.send(BATCHES.stop(x['id'],JOBS))
|
| 269 |
-
if self.path not in ('/api/run','/api/batch/start'):return self.send({'error':'Unknown action'},404)
|
| 270 |
-
data=base64.b64decode(x['data'],validate=True)
|
| 271 |
-
with LOCK:
|
| 272 |
-
if BATCHES.active() or any(j['state'] in ('queued','running') for j in JOBS.values()):raise ValueError('Stop or finish the current run or batch first')
|
| 273 |
-
if self.path=='/api/batch/start':return self.send(BATCHES.start(x['slot'],data))
|
| 274 |
-
job=make_job(validate(x['config']),data)
|
| 275 |
-
threading.Thread(target=run,args=(job,),daemon=True).start();return self.send({'id':job['id']})
|
| 276 |
-
except (ValueError,KeyError,TypeError,IndexError) as e:return self.send({'error':str(e)},400)
|
| 277 |
-
def log_message(self,*args):pass
|
| 278 |
if __name__=='__main__':
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
if j['state'] in ('queued','running'):j['state']='interrupted';save(j)
|
| 282 |
-
JOBS[j['id']]=j
|
| 283 |
-
ports=[int(os.environ['ALD_DASHBOARD_PORT'])] if 'ALD_DASHBOARD_PORT' in os.environ else [47810,47811]
|
| 284 |
-
for port in ports:
|
| 285 |
-
try:http=ThreadingHTTPServer(('0.0.0.0',port),Handler);break
|
| 286 |
-
except OSError:
|
| 287 |
-
if port==ports[-1]:raise
|
| 288 |
-
(ROOT/'dashboard-port.json').write_text(json.dumps({'port':port}))
|
| 289 |
-
print(f'TAO dashboard: http://127.0.0.1:{port}',flush=True)
|
| 290 |
-
http.serve_forever()
|
|
|
|
| 1 |
+
"""Anonymous local integration test server; not yet a ZeroGPU entrypoint."""
|
| 2 |
+
import base64
|
| 3 |
+
import json
|
| 4 |
from pathlib import Path
|
| 5 |
+
import threading
|
| 6 |
+
from fastapi import HTTPException, Request
|
| 7 |
+
from fastapi.responses import FileResponse, Response
|
| 8 |
+
from starlette.concurrency import run_in_threadpool
|
| 9 |
+
from delivery import create_api, CapacityExceeded
|
| 10 |
+
from worker import run_reported
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
ROOT=Path(__file__).resolve().parent
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
def create_app(worker=run_reported,store=None,gpu_worker=None):
|
| 15 |
+
app=create_api(store)
|
| 16 |
+
admission=threading.BoundedSemaphore(1)
|
| 17 |
+
@app.get('/api/capabilities')
|
| 18 |
+
def capabilities():
|
| 19 |
+
return {'cpu':True,'gpu':gpu_worker is not None,'default':'cpu'}
|
| 20 |
+
@app.get('/')
|
| 21 |
+
def index(): return FileResponse(ROOT/'index.html',media_type='text/html')
|
| 22 |
+
@app.get('/client.js')
|
| 23 |
+
def script(): return FileResponse(ROOT/'client.js',media_type='text/javascript')
|
| 24 |
+
@app.get('/policy')
|
| 25 |
+
def policy(): return FileResponse(ROOT/'DATA_POLICY.md',media_type='text/plain')
|
| 26 |
+
@app.post('/api/process')
|
| 27 |
+
async def process(request:Request):
|
| 28 |
+
if not admission.acquire(blocking=False): raise HTTPException(429,'Worker busy; try later')
|
| 29 |
+
try:
|
| 30 |
+
data=bytearray()
|
| 31 |
+
async for chunk in request.stream():
|
| 32 |
+
if len(data)+len(chunk)>3*1024*1024: raise HTTPException(413,'Request too large')
|
| 33 |
+
data.extend(chunk)
|
| 34 |
+
try:
|
| 35 |
+
body=json.loads(data)
|
| 36 |
+
automatic=body.get('automatic',True)
|
| 37 |
+
backend=body.get('backend','cpu')
|
| 38 |
+
if backend not in ('cpu','gpu'): raise ValueError()
|
| 39 |
+
if type(automatic)!=bool: raise ValueError()
|
| 40 |
+
track=base64.b64decode(body['track'],validate=True)
|
| 41 |
+
network=base64.b64decode(body['network'],validate=True)
|
| 42 |
+
if not track or not network or max(len(track),len(network))>1024*1024: raise ValueError()
|
| 43 |
+
except Exception: raise HTTPException(400,'Invalid input envelope') from None
|
| 44 |
+
finally: data.clear()
|
| 45 |
+
selected=worker if backend=='cpu' else gpu_worker
|
| 46 |
+
if selected is None: raise HTTPException(503,'GPU is unavailable; select CPU to run without a GPU lease')
|
| 47 |
+
try:
|
| 48 |
+
result=await run_in_threadpool(selected,track,network)
|
| 49 |
+
payload,report=result if isinstance(result,tuple) else (result,None)
|
| 50 |
+
return {**app.state.results.publish(payload,automatic),'backend':backend,'report':report}
|
| 51 |
+
except CapacityExceeded: raise HTTPException(503,'Result capacity reached') from None
|
| 52 |
+
except Exception: raise HTTPException(422,'Processing failed; no result published. Cleanup status requires verification.') from None
|
| 53 |
+
finally: admission.release()
|
| 54 |
+
return app
|
| 55 |
+
|
| 56 |
+
app=create_app()
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
if __name__=='__main__':
|
| 59 |
+
import uvicorn
|
| 60 |
+
uvicorn.run(app,host='127.0.0.1',port=47831,access_log=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
worker.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bounded native adapter. No engine modifications; no network calls.
|
| 2 |
+
|
| 3 |
+
This is NOT a network sandbox. Private deployment pending integration verification.
|
| 4 |
+
"""
|
| 5 |
+
import gzip
|
| 6 |
+
import io
|
| 7 |
+
import json
|
| 8 |
+
import math
|
| 9 |
+
import os
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import subprocess
|
| 12 |
+
import tempfile
|
| 13 |
+
import time
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 16 |
+
|
| 17 |
+
def read_json(data):
|
| 18 |
+
if len(data)>1024*1024: raise ValueError('Input exceeds 1 MB')
|
| 19 |
+
if data[:2]==b'\x1f\x8b':
|
| 20 |
+
with gzip.GzipFile(fileobj=io.BytesIO(data)) as stream:
|
| 21 |
+
data=stream.read(4*1024*1024+1)
|
| 22 |
+
if len(data)>4*1024*1024: raise ValueError('Expanded input exceeds 4 MB')
|
| 23 |
+
result=json.loads(data,parse_constant=lambda _: (_ for _ in ()).throw(ValueError('Nonfinite JSON')))
|
| 24 |
+
if not isinstance(result,dict): raise ValueError('Expected JSON object')
|
| 25 |
+
return result
|
| 26 |
+
|
| 27 |
+
def network_input(data):
|
| 28 |
+
value=read_json(data)
|
| 29 |
+
network=value['NeuralNetwork']; shape=network['Shape']
|
| 30 |
+
if value.get('Type')!=0 or value.get('Outputs')!=[0,1]: raise ValueError('Formula steering/acceleration only')
|
| 31 |
+
if not isinstance(shape,list) or not 2<=len(shape)<=4 or any(type(n)!=int or not 1<=n<=16 for n in shape) or shape[-1]!=2:
|
| 32 |
+
raise ValueError('Unsupported shape')
|
| 33 |
+
sensors=value['Sensors']
|
| 34 |
+
if not isinstance(sensors,list) or len(sensors)!=shape[0]: raise ValueError('Sensor count')
|
| 35 |
+
# Deliberately small first supported contract. Do not forward unknown settings.
|
| 36 |
+
clean_sensors=[]
|
| 37 |
+
for sensor in sensors:
|
| 38 |
+
if sensor.get('$type')=='speed': clean_sensors.append({'$type':'speed'})
|
| 39 |
+
elif sensor.get('$type')=='raycast':
|
| 40 |
+
selected={'$type':'raycast'}
|
| 41 |
+
for key in ('Degrees','Length'):
|
| 42 |
+
n=sensor[key]
|
| 43 |
+
if type(n) not in (int,float) or not math.isfinite(n): raise ValueError('Sensor value')
|
| 44 |
+
if key=='Degrees' and not -360<=n<=360: raise ValueError('Degrees')
|
| 45 |
+
if key=='Length' and not 1<=n<=10000: raise ValueError('Length')
|
| 46 |
+
selected[key]=n
|
| 47 |
+
clean_sensors.append(selected)
|
| 48 |
+
else: raise ValueError('Only speed and raycast sensors supported in smoke version')
|
| 49 |
+
weights,biases=network['Weights'],network['Biases']
|
| 50 |
+
if len(weights)!=len(shape)-1 or len(biases)!=len(shape)-1: raise ValueError('Layer count')
|
| 51 |
+
for i,(a,b) in enumerate(zip(shape,shape[1:])):
|
| 52 |
+
if len(weights[i])!=a or any(len(row)!=b for row in weights[i]) or len(biases[i])!=b: raise ValueError('Dimensions')
|
| 53 |
+
for n in [x for row in weights[i] for x in row]+biases[i]:
|
| 54 |
+
if type(n) not in (int,float) or not math.isfinite(n) or abs(n)>1e6: raise ValueError('Weight value')
|
| 55 |
+
return {'Type':0,'Outputs':[0,1],'Sensors':clean_sensors,
|
| 56 |
+
'Name':'HF candidate','NeuralNetwork':{'Shape':shape,'Weights':weights,'Biases':biases}}
|
| 57 |
+
|
| 58 |
+
def track_input(data):
|
| 59 |
+
value=read_json(data)
|
| 60 |
+
tiles=value['Tiles']
|
| 61 |
+
if not isinstance(tiles,dict) or not 4<=len(tiles)<=256: raise ValueError('Track tile limit')
|
| 62 |
+
start=value.get('StartTileConnection')
|
| 63 |
+
if not isinstance(start,dict): raise ValueError('StartTileConnection must be an object')
|
| 64 |
+
# Match the local runner's saved-track import: game files can omit default Type.
|
| 65 |
+
# Normalize only this parsed copy; never rewrite the uploaded source bytes.
|
| 66 |
+
if 'Type' not in start: start['Type']=0
|
| 67 |
+
elif type(start['Type']) is not int: raise ValueError('StartTileConnection.Type must be an integer')
|
| 68 |
+
return value
|
| 69 |
+
|
| 70 |
+
def run_native(track_bytes,network_bytes,*,binary=None,importer=None,scene_template=None,with_report=False):
|
| 71 |
+
started=time.perf_counter()
|
| 72 |
+
track=track_input(track_bytes); template=network_input(network_bytes)
|
| 73 |
+
# Deployment paths are supplied by trusted application configuration, never uploads.
|
| 74 |
+
binary=Path(binary or ROOT/'target/release/altd-sim').resolve()
|
| 75 |
+
importer=Path(importer or ROOT/'target/release/examples/extract_saved_track').resolve()
|
| 76 |
+
scene_template=Path(scene_template or ROOT/'assets/scenes/formula_template.json').resolve()
|
| 77 |
+
if not all(p.is_file() for p in (binary,importer,scene_template)):
|
| 78 |
+
raise RuntimeError('Native tools or scene template unavailable')
|
| 79 |
+
# No inherited provider tokens, credential paths, or caller-chosen commands.
|
| 80 |
+
env={'PATH':'/usr/bin:/bin','LANG':'C','RUST_BACKTRACE':'0'}
|
| 81 |
+
with tempfile.TemporaryDirectory(prefix='tiga-native-') as directory:
|
| 82 |
+
work=Path(directory)
|
| 83 |
+
env.update(HOME=directory,TMPDIR=directory)
|
| 84 |
+
def save(name,value):
|
| 85 |
+
path=work/name; path.write_text(json.dumps(value,allow_nan=False)); return str(path)
|
| 86 |
+
saved=save('track.json',track)
|
| 87 |
+
scene=work/'scene.json'
|
| 88 |
+
subprocess.run([str(importer),saved,'User track',str(scene_template),str(scene)],
|
| 89 |
+
cwd=work,env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=10,check=True)
|
| 90 |
+
imported=json.loads(scene.read_text())
|
| 91 |
+
spawn=save('spawn.json',{'frames':[{'position':imported['reset_position'],'rotation':imported['reset_rotation']}]})
|
| 92 |
+
n=template['NeuralNetwork']; names=[f'sensor-{i}' for i in range(n['Shape'][0])]
|
| 93 |
+
network=save('network.json',{'shape':n['Shape'],'weights':n['Weights'],'biases':n['Biases'],'inputs':names,'outputs':['Steering','Acceleration']})
|
| 94 |
+
model=save('model.json',{'sensor_layout':{'names':names,'sensors':template['Sensors']}})
|
| 95 |
+
settings=save('settings.json',{'population':4,'selection_algorithm':'best','selection_size':1,
|
| 96 |
+
'crossover':'none','mutation_rate':.01,'adaptive_mutation':False,'weight_decay':0,
|
| 97 |
+
'preserve_parents':'on_custom','preserve_parents_size':1,
|
| 98 |
+
'rewards':[{'metric':'total_score','type':'default','weight':1}]})
|
| 99 |
+
out=work/'result'
|
| 100 |
+
command=[str(binary),'--math-profile','proton','--threads','1','train-scratch',
|
| 101 |
+
'--scene',str(scene),'--spawn-trace',spawn,'--network',network,'--init-network',network,
|
| 102 |
+
'--model',model,'--settings',settings,'--shape',','.join(map(str,n['Shape'])),
|
| 103 |
+
'--population','4','--generations','1','--ticks','60','--seed','42','--batch-count','2',
|
| 104 |
+
'--out-dir',str(out),'--mutation-start','.01','--mutation-end','.01',
|
| 105 |
+
'--schedule','linear','--save-final-candidate']
|
| 106 |
+
subprocess.run(command,cwd=work,env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=25,check=True)
|
| 107 |
+
candidate=out/'candidate.json'
|
| 108 |
+
if candidate.stat().st_size>1024*1024: raise ValueError('Output too large')
|
| 109 |
+
trained=json.loads(candidate.read_text())
|
| 110 |
+
n['Weights']=trained['weights']; n['Biases']=trained['biases']
|
| 111 |
+
result=gzip.compress(json.dumps(template,allow_nan=False).encode(),mtime=0)
|
| 112 |
+
network_input(result)
|
| 113 |
+
history=[json.loads(line) for line in (out/'log.jsonl').read_text().splitlines() if line.strip()]
|
| 114 |
+
last=history[-1] if history else {}
|
| 115 |
+
report={'state':'completed','elapsed_seconds':time.perf_counter()-started,
|
| 116 |
+
'generations_completed':len(history),'generations_requested':1,'population':4,
|
| 117 |
+
'best_score':last.get('best_score'),'mean_score':last.get('mean_score'),
|
| 118 |
+
'best_lap_seconds':last.get('best_lap_s'),'lapped_cars':last.get('lapped_cars'),
|
| 119 |
+
'simulated_ticks':last.get('simulated_ticks'),'requested_ticks':60,
|
| 120 |
+
'candidate_validated':True}
|
| 121 |
+
# The context has removed input/intermediate files before result publication.
|
| 122 |
+
return (result,report) if with_report else result
|
| 123 |
+
|
| 124 |
+
def run_reported(track_bytes,network_bytes):
|
| 125 |
+
return run_native(track_bytes,network_bytes,with_report=True)
|