TiGa-RCE commited on
Commit
2031f77
·
verified ·
1 Parent(s): 9ed8b8c

Private CPU interface integration with ephemeral result lifecycle

Browse files
Files changed (16) hide show
  1. .gitattributes +1 -0
  2. DATA_POLICY.md +35 -0
  3. GODOT_LICENSE +43 -0
  4. IMPORTER_BUILD_RECEIPT.json +60 -0
  5. LICENSE +21 -0
  6. README.md +8 -27
  7. RELEASE_MANIFEST.json +22 -0
  8. app.py +28 -31
  9. assets/formula_template.json +111 -0
  10. bin/extract_saved_track +3 -0
  11. client.js +111 -0
  12. delivery.py +151 -0
  13. index.html +22 -137
  14. requirements.txt +2 -1
  15. server.py +55 -285
  16. 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: TAO ZeroGPU Qualification
3
- emoji: 🏎️
4
- colorFrom: green
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.29.1
8
- python_version: '3.10'
9
  app_file: app.py
10
- pinned: false
11
  ---
12
 
13
- Private native CUDA compatibility probe and bounded upload smoke test, not a qualified trainer.
14
- Uses included ZeroGPU allowance through a 45-second leased call. No paid Jobs
15
- or dedicated hardware requested. Check the authenticated caller's included quota
16
- before each call; the app itself does not know account billing balances.
17
-
18
- Original Docker source and dashboard remain in this repository but are inactive.
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
- import spaces
2
- import gradio as gr
3
- from cloud_probe import run_probe
 
 
 
4
 
5
- def cpu_smoke():
6
- return run_probe(False)
7
 
8
- @spaces.GPU(duration=90)
9
- def gpu_smoke():
10
- return run_probe(True)
 
 
 
 
 
 
11
 
12
- def selected_smoke(backend):
13
- if backend == 'cpu':
14
- return cpu_smoke()
15
- if backend == 'gpu':
16
- return gpu_smoke()
17
- raise ValueError('Choose CPU or GPU')
18
 
19
- with gr.Blocks() as demo:
20
- gr.Markdown('''# AI Learns To Drive — private compute test
21
- Simulator by [Zorro909](https://github.com/Zorro909/driving_sim_rs).
22
- UI and HF implementation by TiGa (seeker-cyber-maker).
 
 
 
23
 
24
- Fixed synthetic network, 4 cars, 1 generation, 60 ticks. CPU runs without a GPU lease.
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"><title>TAO · Driving Lab</title>
 
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;margin:0}h2{font-size:21px;margin:0 0 14px}small,.hint{color:#a2b4c8;line-height:1.6}.grid{display:grid;grid-template-columns:minmax(350px,430px) minmax(0,1fr);gap:24px}.card{background:#152233;border:1px solid #2a3d52;border-radius:16px;padding:22px;margin-top:20px}label{display:block;margin:12px 0 5px}input,select,button{font:inherit;background:#0c1725;color:#e5edf5;border:1px solid #49627c;border-radius:7px;padding:9px}input[type=number],select{width:100%;box-sizing:border-box}input[type=file]{max-width:100%;box-sizing:border-box}button{cursor:pointer}button.primary{background:#62dfb1;color:#102522;font-weight:700}button:disabled{opacity:.4;cursor:default}.row{display:grid;grid-template-columns:1fr 1fr;gap:15px}.reward{display:grid;grid-template-columns:1.7fr 1fr 70px 30px;gap:5px;margin:8px 0}.reward input{width:70px;box-sizing:border-box}a{color:#7ce4c3}canvas{width:100%;background:#09111b;border-radius:10px}#status{white-space:pre-wrap;line-height:1.7}table{width:100%;text-align:left;border-collapse:collapse;font-size:14px}td,th{padding:9px;border-bottom:1px solid #2a3d52}.pill{display:inline-block;background:#284539;border-radius:20px;padding:5px 12px;color:#91efc6;margin:6px 0}#error{color:#ffb5a2;white-space:pre-wrap}.toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding-bottom:16px;border-bottom:1px solid #2a3d52;margin-bottom:15px}.run-card{position:sticky;top:20px}details{margin-top:18px;border-top:1px solid #2a3d52;padding-top:14px}summary{cursor:pointer;font-weight:600;color:#c6d9e9}details[open]>summary{margin-bottom:12px}[hidden]{display:none!important}#map_note{font-size:14px}#runs{max-height:380px;overflow:auto}.table-scroll{overflow:auto}@media(max-width:850px){.grid{grid-template-columns:1fr}#drive .grid>section:nth-child(2){order:-1}.run-card{position:static}body{padding:16px}}
4
- .tabs{display:flex;flex-wrap:wrap;gap:10px;margin:20px 0}.tabs button[aria-selected=true]{background:#62dfb1;color:#102522}.library{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:20px}.library input[type=text]{width:100%;box-sizing:border-box}.queue-row{display:flex;gap:8px;margin:8px 0}.queue-row select{flex:1}.pane{min-width:0}</style>
5
- <header><h1>TAO · CPU Lab</h1><p class="hint">Private CPU comparison Space · uploaded networks run on Hugging Face CPU hardware. Download receipts before a restart; container storage is temporary.</p></header>
6
- <nav class="tabs" aria-label="Lab sections"><button data-tab="drive" aria-selected="true">1 · Drive & view</button><button data-tab="training" aria-selected="false">2 · Training runs & presets</button><button data-tab="batch" aria-selected="false">3 · Batches</button><button data-tab="editor" aria-selected="false">4 · Network editor</button></nav><div id="drive" class="pane"><div class="grid"><section><div class="card"><h2>Simulation</h2><span class="pill">Formula · steering + acceleration</span>
7
- <label for="backend">Simulation engine</label><select id="backend"><option value="github">GitHub simulator · Native CPU</option></select>
8
- <label><input type="radio" name="network_source" id="source_upload" checked> Use uploaded network</label><label for="file">Upload a network (.evolution)</label><input id="file" type="file" accept=".evolution"><label><input type="radio" name="network_source" id="source_latest"> Use latest completed candidate</label><p><button id="sample">Load latest completed candidate</button></p><label id="editor_source_group" hidden><input type="radio" name="network_source" id="source_editor"> Use network from editor</label><details><summary>Saved cars · common ALD folder</summary><label><input type="radio" name="network_source" id="source_library"> Use saved car</label><select id="library_car" aria-label="Saved car"></select><button id="library_load">Load saved car</button></details><label><input id="require_standard" type="checkbox" checked> Require standard game network</label><small>Uncheck only for explicitly modified research networks. Engine compatibility is checked separately.</small><p><small id="network">No network selected</small></p><p id="network_contract" class="hint"></p>
9
- <label for="track">Track</label><select id="track"></select><small id="track_support"></small>
10
- <label for="mode">Activity</label><select id="mode"><option value="replay">Drive / evaluate frozen network</option><option value="train">Train a population</option></select>
11
- <label for="seconds">Seconds per attempt</label><input id="seconds" type="number" min="1" max="300" value="60"><small>1–300 simulated seconds. Start with 10–15 when learning, then extend.</small>
12
- <div id="training_controls" hidden>
13
- <div class="row"><div><label for="population">Population</label><input id="population" type="number" min="1" max="128" value="8"><small>1–128; native 64 or Godot 8–16 initially.</small></div><div><label for="generations">Generations</label><input id="generations" type="number" min="1" max="1000" value="30"><small>1–1,000; try 20–50 before review.</small></div></div>
14
- <label id="mutation_label" for="mutation">Mutation strength (Gaussian σ)</label><input id="mutation" type="number" min="0" max="4.2" step="0.01" value="0.1"><small>0–4.2; native 0.3 → 0.1 → 0.05. Native rates and Godot σ are different scales.</small>
15
- <details id="native_settings" hidden><summary>Native evolution settings</summary>
16
- <label for="preset">Recommended starting recipe</label><select id="preset"><option value="custom">Custom / keep current settings</option><option value="starter">First laps · mutation 0.3, decay 0.0005</option><option value="explore">Wide exploration · mutation 0.5, decay 0.0005</option><option value="refine">Established driver · mutation 0.1, decay 0.0001</option><option value="speed">Lap refinement · mutation 0.05, decay 0</option></select><small>Starting hypotheses, not proven optimums. Change stages after stable improvement.</small>
17
- <label for="schedule">Mutation schedule for this run</label><select id="schedule"><option value="fixed">Fixed · change stages manually</option><option value="linear">Linear start → end</option><option value="geometric">Geometric start → end</option></select>
18
- <div id="end_rate_group" hidden><label for="mutation_end">End mutation rate</label><input id="mutation_end" type="number" min="0.000001" max="4.2" step="0.01" value="0.1"><small>Both schedule rates must be positive.</small></div>
19
- <label><input id="adaptive_mutation" type="checkbox"> Adaptive mutation · normalize for network size</label><small>Architecture scaling, not progress-aware adjustment. Decay is fixed; adaptive decay isn't exposed by this CLI.</small>
20
- <label for="selection">Parent selection</label><select id="selection"><option value="best">Best</option><option value="tournament">Tournament</option><option value="roulette">Roulette</option></select>
21
- <div class="row"><div><label for="parents">Selected parents</label><input id="parents" type="number" min="1" value="1"><small>1–population; start with 3.</small></div><div><label for="elite">Preserved elites</label><input id="elite" type="number" min="0" value="1"><small>0–population; start with 1.</small></div></div>
22
- <label for="crossover">Crossover</label><select id="crossover"><option value="none">Off</option><option value="single_point">Single point</option><option value="uniform">Uniform</option></select>
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 &amp; deletion policy</a></p>
16
+ <button id="run" disabled>Run short test</button></section><section class="card"><h2>Run &amp; 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 &amp; 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
- spaces==0.51.3
 
 
1
+ fastapi==0.141.1
2
+ uvicorn==0.52.1
server.py CHANGED
@@ -1,290 +1,60 @@
1
- """Loopback UI for the isolated TAO Godot runner. External simulator stays separate."""
2
- import os,base64,copy,gzip,hashlib,json,math,random,subprocess,sys,threading,time,uuid
 
3
  from pathlib import Path
4
- from urllib.parse import unquote
5
- from http.server import ThreadingHTTPServer,BaseHTTPRequestHandler
6
- from concurrent.futures import ThreadPoolExecutor
7
- ROOT=Path(__file__).resolve().parent
8
- sys.path.insert(0,str(ROOT/'vendor'))
9
- import cloud_core as core
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
- def rank(scores,vectors,rows):
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 edit_network(x):
190
- data=base64.b64decode(x.get('data') or base64.b64encode(SAMPLE.read_bytes()).decode(),validate=True)
191
- if data[:2]==b'\x1f\x8b':
192
- import io
193
- with gzip.GzipFile(fileobj=io.BytesIO(data)) as f:data=f.read(8*1024*1024+1)
194
- if len(data)>8*1024*1024:raise ValueError('Network exceeds 8 MB')
195
- template=json.loads(data);n=template['NeuralNetwork']
196
- if x['action']=='inspect':
197
- compatibility={}
198
- for backend in ('ours','github'):
199
- try:parsed,_=parse_network(data);engine_contract(parsed,{'backend':backend});compatibility[backend]='supported'
200
- except (ValueError,KeyError,TypeError,IndexError) as e:compatibility[backend]=str(e)
201
- return {'network':n,'sensors':template.get('Sensors'),'name':template.get('Name'),'outputs':template.get('Outputs'),'vehicle':template.get('Type'),'compatibility':compatibility,'stats':template.get('Stats',{}),'snapshots':template.get('Snapshots',[]),'id':template.get('Id'),'legality':network_legality(template)}
202
- if x['action']=='create':
203
- shape=x['shape']
204
- if not isinstance(shape,list) or not 3<=len(shape)<=20 or shape[0]!=6 or shape[-1]!=2 or any(type(z)!=int or not 1<=z<=64 for z in shape):raise ValueError('Shape must start with 6, end with 2, and contain 1–18 hidden layers of 1–64 nodes')
205
- if sum(a*b+b for a,b in zip(shape[:-1],shape[1:]))>100000:raise ValueError('Editor limit: 100,000 parameters')
206
- seed=x['seed']
207
- if type(seed)!=int or not 0<=seed<=2147483647:raise ValueError('Seed must be 0–2147483647')
208
- rng=random.Random(seed);weights=[];biases=[]
209
- for a,b in zip(shape[:-1],shape[1:]):
210
- bound=math.sqrt(6/(a+b));weights.append([[rng.uniform(-bound,bound) if x.get('xavier',True) else 0. for _ in range(b)] for _ in range(a)]);biases.append([rng.uniform(-bound,bound) if x.get('xavier_bias',False) else 0. for _ in range(b)])
211
- n={'Shape':shape,'Weights':weights,'Biases':biases}
212
- elif x['action']=='save':n=x['network']
213
- else:raise ValueError('Unknown editor action')
214
- shape=n['Shape']
215
- if not 2<=len(shape)<=20 or any(type(z)!=int or not 1<=z<=64 for z in shape) or len(n['Weights'])!=len(shape)-1 or len(n['Biases'])!=len(shape)-1:raise ValueError('Invalid network shape')
216
- for i,(a,b) in enumerate(zip(shape[:-1],shape[1:])):
217
- w,bs=n['Weights'][i],n['Biases'][i]
218
- if len(w)!=a or any(len(row)!=b for row in w) or len(bs)!=b:raise ValueError('Malformed weight dimensions')
219
- if any(type(z) not in (int,float) or not math.isfinite(z) for z in [v for row in w for v in row]+bs):raise ValueError('All weights and biases must be finite numbers')
220
- n['InputShape']=shape[0];n['OutputShape']=shape[-1];n['NeuronsCount']=sum(shape);n['TotalParams']=sum(a*b+b for a,b in zip(shape[:-1],shape[1:]));template['NeuralNetwork']=n;template['Name']=str(x.get('name','TAO editor network'))[:80];template['Id']=str(uuid.uuid4());template['Stats']={'Generations':[]};template['Snapshots']=[];template['OpponentId']=None;template['SteamWorkshopId']=None;template['Description']='Locally edited research network; no game or leaderboard validation claimed.'
221
- legality=network_legality(template)
222
- if legality['errors']:raise ValueError('Cannot export structurally invalid network: '+'; '.join(legality['errors']))
223
- payload=gzip.compress(json.dumps(template).encode(),mtime=0)
224
- return {'filename':template['Name']+'.evolution','data':base64.b64encode(payload).decode(),'network':n,'trainable':template.get('Outputs')==[0,1] and template.get('Type')==0,'stats':template['Stats'],'sensors':template['Sensors'],'outputs':template['Outputs'],'vehicle':template.get('Type'),'legality':network_legality(template)}
 
 
 
 
 
 
 
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
- for p in sorted(RUNS.glob('*/status.json')):
280
- j=json.loads(p.read_text());j['cancel']=threading.Event();j['processes']=[]
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)