diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..d079320fc0089792fda7151a5caa51a08cb62721 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +trackio/assets/badge.png filter=lfs diff=lfs merge=lfs -text +trackio/assets/trackio_logo_old.png filter=lfs diff=lfs merge=lfs -text diff --git a/trackio/CHANGELOG.md b/trackio/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..4a4abe2a804f4c40bae6e8f1fdafc1000ae2ab29 --- /dev/null +++ b/trackio/CHANGELOG.md @@ -0,0 +1,517 @@ +# trackio + +## 0.41.0 + +### Features + +- [#703](https://github.com/gradio-app/trackio/pull/703) [`6bdb2d1`](https://github.com/gradio-app/trackio/commit/6bdb2d1f857473c191e76ef011ecc0c14fa01926) - Add `trackio bump` to upgrade a Space to the local Trackio version. Thanks @abidlabs! + +## 0.40.0 + +### Features + +- [#700](https://github.com/gradio-app/trackio/pull/700) [`9f8215d`](https://github.com/gradio-app/trackio/commit/9f8215dcebf3544338c58265d768d113c70f10b2) - Invalidate the Spaces logs cache on every committed write. Thanks @abidlabs! + +## 0.39.0 + +### Features + +- [#697](https://github.com/gradio-app/trackio/pull/697) [`89c9b1b`](https://github.com/gradio-app/trackio/commit/89c9b1b8e644a4ee8e5ae3e86a77d54e8a09360b) - Let an embedding page ask the dashboard for its current view. Thanks @abidlabs! +- [#693](https://github.com/gradio-app/trackio/pull/693) [`3db5e38`](https://github.com/gradio-app/trackio/commit/3db5e38a9fd731959b96793c7aebba6c3481615e) - Add overwrite option to log_artifact. Thanks @abidlabs! +- [#696](https://github.com/gradio-app/trackio/pull/696) [`81ae55f`](https://github.com/gradio-app/trackio/commit/81ae55fe1fe94e3c4205f9bd892a85cfc73b0ee2) - Hide empty dashboard tabs on narrow viewports. Thanks @abidlabs! + +## 0.38.1 + +### Features + +- [#692](https://github.com/gradio-app/trackio/pull/692) [`217a949`](https://github.com/gradio-app/trackio/commit/217a94957cce97a3822a908560f49c435b5b5117) - Fix CommitOperationAdd import with huggingface_hub 1.32. Thanks @abidlabs! + +## 0.38.0 + +### Features + +- [#684](https://github.com/gradio-app/trackio/pull/684) [`10011b0`](https://github.com/gradio-app/trackio/commit/10011b0590e2a7ebd552837b16f253a665c2128d) - Reduce dashboard canvas GPU memory and render churn. Thanks @abidlabs! +- [#688](https://github.com/gradio-app/trackio/pull/688) [`e332a26`](https://github.com/gradio-app/trackio/commit/e332a262a45facc371443d7f16e4f7d39e5869a3) - Show Trackio version beneath dashboard logo. Thanks @abidlabs! + +## 0.37.1 + +### Features + +- [#678](https://github.com/gradio-app/trackio/pull/678) [`df9168e`](https://github.com/gradio-app/trackio/commit/df9168e2d0de2b284413a7e81915e7c1f76e49ae) - Fix dotted metric names in line plots. Thanks @abidlabs! +- [#685](https://github.com/gradio-app/trackio/pull/685) [`b7b349b`](https://github.com/gradio-app/trackio/commit/b7b349b838dcba093e15bc329717ccbb2e3e8f92) - Fix OAuth token authorization for Space logging. Thanks @abidlabs! +- [#686](https://github.com/gradio-app/trackio/pull/686) [`ddcd855`](https://github.com/gradio-app/trackio/commit/ddcd8557f6b55a7569700a083bd5ab2253f65e61) - Responsive auto layout for charts, sidebar, and tabs. Thanks @pngwn! + +### Fixes + +- [#680](https://github.com/gradio-app/trackio/pull/680) [`c81e537`](https://github.com/gradio-app/trackio/commit/c81e53765ea3e58bc36e3016f98b74d265926196) - decode UTF-8 blobs in `trackio query` output instead of hex-encoding them, so JSON columns like `config` and `metrics` are readable. Thanks @B1aZer! + +## 0.37.0 + +### Features + +- [#672](https://github.com/gradio-app/trackio/pull/672) [`784819b`](https://github.com/gradio-app/trackio/commit/784819b610bbd872bf39ed9ef5d62ece2a5966b5) - Add hierarchical execution spans and a trace inspector with per-operation latency, status, token usage, cost, and input/output details. Thanks @abidlabs! + +## 0.36.0 + +### Features + +- [#674](https://github.com/gradio-app/trackio/pull/674) [`ac5f06c`](https://github.com/gradio-app/trackio/commit/ac5f06c551a051ac435913f3210472d6971b1ed5) - fix: make 3D object cards behave like image cards and fix Reset camera. Thanks @abidlabs! + +## 0.35.0 + +### Features + +- [#645](https://github.com/gradio-app/trackio/pull/645) [`8fe869f`](https://github.com/gradio-app/trackio/commit/8fe869f956515344fef68ee69230b2e192ea7707) - artifact registry storage and publishing API — link artifact versions into typed, cross-project collections with `Run.link_artifact` or `Artifact.link` (and remove links with `Artifact.unlink`), and manage them via `trackio.Registry`. Thanks @adrien-grl! +- [#651](https://github.com/gradio-app/trackio/pull/651) [`ae43b85`](https://github.com/gradio-app/trackio/commit/ae43b85ca625a056280278148bab6e34a5c520fa) - add artifact lineage graph. Thanks @Saba9! +- [#667](https://github.com/gradio-app/trackio/pull/667) [`e029b80`](https://github.com/gradio-app/trackio/commit/e029b8068b43dea4cd97647d86642abb0b587a1f) - add CLI commands to create, inspect, publish to, promote, and unlink from artifact registries. Thanks @abidlabs! +- [#668](https://github.com/gradio-app/trackio/pull/668) [`ac9073f`](https://github.com/gradio-app/trackio/commit/ac9073f283e5d28975246f91879b1e5ed4dbf590) - Add standalone read-only registry dashboard. Thanks @abidlabs! +- [#606](https://github.com/gradio-app/trackio/pull/606) [`1dc2dd9`](https://github.com/gradio-app/trackio/commit/1dc2dd9cab0e1603522c43935ebbec6b1f5efb67) - feat(UI): Run comparer - compare (potentially nested) run configs side by side. Thanks @adrien-grl! + +## 0.34.1 + +### Features + +- [#658](https://github.com/gradio-app/trackio/pull/658) [`ad4373c`](https://github.com/gradio-app/trackio/commit/ad4373c521a84092f8bf7b12d87d6d9c65bc3078) - Fix remote media upload filenames. Thanks @MiloDavis! +- [#659](https://github.com/gradio-app/trackio/pull/659) [`e9b6e06`](https://github.com/gradio-app/trackio/commit/e9b6e0601ed41d51d9cffc143d21d282d25ca73a) - Fix unstable metric sampling in charts that happens in `trl>=1.7.0`. Thanks @abidlabs! + +## 0.34.0 + +### Features + +- [#656](https://github.com/gradio-app/trackio/pull/656) [`bb888ca`](https://github.com/gradio-app/trackio/commit/bb888ca79dcb1e99572ffb62bdd3b0020d77dc07) - Remove beta status from logbooks. Thanks @abidlabs! +- [#652](https://github.com/gradio-app/trackio/pull/652) [`b1e0731`](https://github.com/gradio-app/trackio/commit/b1e07316962fefce71856cafcee7b38e1f477a52) - Log arbitrary HTML and Plotly/Matplotlib figures via `trackio.Html`. Thanks @greysonlalonde! + +## 0.33.0 + +### Features + +- [#647](https://github.com/gradio-app/trackio/pull/647) [`1d25383`](https://github.com/gradio-app/trackio/commit/1d253830e302d4bcd67f3aae5458e6c70c5f5144) - Increase smoothing upper bound to 100. Thanks @abidlabs! +- [#648](https://github.com/gradio-app/trackio/pull/648) [`6a1063f`](https://github.com/gradio-app/trackio/commit/6a1063f7c834f850390866b80608ca17c8563199) - Deprecate persisting data to HF Datasets in favor of Buckets. Thanks @abidlabs! + +### Fixes + +- [#649](https://github.com/gradio-app/trackio/pull/649) [`8506189`](https://github.com/gradio-app/trackio/commit/8506189b69f6c0361e6420ef545abaad4a773cb3) - render logged histograms in the dashboard's Metrics tab. Thanks @abidlabs! + +## 0.32.2 + +### Features + +- [#641](https://github.com/gradio-app/trackio/pull/641) [`2d1c01a`](https://github.com/gradio-app/trackio/commit/2d1c01a5ce0307ec5c81660c512bf6f25dd3120a) - Improve logbook layout and public repository views. Thanks @abidlabs! +- [#643](https://github.com/gradio-app/trackio/pull/643) [`e8292c5`](https://github.com/gradio-app/trackio/commit/e8292c5c26baa0e7e806144fa1bbf5ecfb492f3d) - docs: document the artifact tables in the storage schema page. Thanks @adrien-grl! + +## 0.32.1 + +### Features + +- [#639](https://github.com/gradio-app/trackio/pull/639) [`73971e6`](https://github.com/gradio-app/trackio/commit/73971e66a4f1daf5c42341727349e6f82ab4e887) - Fix logbook code overflow, CLI command truncation, Hub link contrast, resource ID validation, and live visibility detection for referenced trace datasets and Buckets. Thanks @abidlabs! + +## 0.32.0 + +### Features + +- [#635](https://github.com/gradio-app/trackio/pull/635) [`b4587b0`](https://github.com/gradio-app/trackio/commit/b4587b0ebce18756a0f38e3721ed2880a7307137) - Add Code & Markdown, Traces, and Workspace logbook views with live local refresh, guided empty states, and privacy-aware manual publishing to Agent Traces Datasets and HF Buckets. Thanks @NielsRogge! + +## 0.31.5 + +### Features + +- [#630](https://github.com/gradio-app/trackio/pull/630) [`bed508c`](https://github.com/gradio-app/trackio/commit/bed508c04c487686dea3dea70c67b058dd895f96) - Fix automatic dashboard cells to use the active logbook page instead of creating a page named after the Trackio project. Thanks @abidlabs! + +## 0.31.4 + +### Features + +- [#628](https://github.com/gradio-app/trackio/pull/628) [`3eeaa99`](https://github.com/gradio-app/trackio/commit/3eeaa99bd05bef69053b9a1b5c7af3abf56c360c) - let `logbook cell figure` embed image files directly. Thanks @abidlabs!/n `trackio logbook cell figure` now accepts an image path via a new `--image`/n flag, and `--html ` transparently embeds the file when it points at an/n image. Previously the only way to add a PNG/JPG figure was to hand-encode it/n into an `` data-URI, and passing an image path to `--html` crashed with a/n `UnicodeDecodeError` (the binary file was read as UTF-8 text). Images are/n embedded as responsive base64 data URIs. The Python API `add_figure_cell` gains/n a matching `image=` parameter. + +## 0.31.3 + +### Features + +- [#626](https://github.com/gradio-app/trackio/pull/626) [`174aecf`](https://github.com/gradio-app/trackio/commit/174aecff33e6d1cc8d71965a73538c0510abed46) - Fix logbook Markdown escaping and figure hotspot navigation. Thanks @abidlabs! + +## 0.31.2 + +### Features + +- [#624](https://github.com/gradio-app/trackio/pull/624) [`a0918ed`](https://github.com/gradio-app/trackio/commit/a0918edc7594bb211f9e6871546b9d36939f0658) - Fix poster hotspot navigation after iframe load. Thanks @abidlabs! + +## 0.31.1 + +### Features + +- [#621](https://github.com/gradio-app/trackio/pull/621) [`a40f1a4`](https://github.com/gradio-app/trackio/commit/a40f1a464c51652ecdeab04d999659d71b76047e) - add native fullscreen mode for logbook figures. Thanks @abidlabs! +- [#623](https://github.com/gradio-app/trackio/pull/623) [`31fd4d7`](https://github.com/gradio-app/trackio/commit/31fd4d7b4004f0c511484f089e6a550da1b4d71e) - let interactive logbook figures navigate to logbook pages. Thanks @abidlabs! + +## 0.31.0 + +### Features + +- [#601](https://github.com/gradio-app/trackio/pull/601) [`8c49140`](https://github.com/gradio-app/trackio/commit/8c491406c54685369d43f3a9c4d4a0e99108bc49) - browse artifacts from the UI. Thanks @Saba9! +- [#602](https://github.com/gradio-app/trackio/pull/602) [`1ae1558`](https://github.com/gradio-app/trackio/commit/1ae155803b5450b181a47767255421499074c70b) - feat: add Artifact.add_reference to reference external objects without copy. Thanks @adrien-grl! + +## 0.30.4 + +### Features + +- [#618](https://github.com/gradio-app/trackio/pull/618) [`64c18f6`](https://github.com/gradio-app/trackio/commit/64c18f65bf98d6f54040395a1ca0473347ba6a2b) - Logbook: share button on figures + `logbook pin` CLI. Thanks @abidlabs! + +## 0.30.3 + +### Features + +- [#616](https://github.com/gradio-app/trackio/pull/616) [`645498b`](https://github.com/gradio-app/trackio/commit/645498be37bafab28f884a36e80c5bc0eab06cd9) - Logbook single page. Thanks @abidlabs! + +## 0.30.2 + +### Features + +- [#614](https://github.com/gradio-app/trackio/pull/614) [`eba80c7`](https://github.com/gradio-app/trackio/commit/eba80c7464af250519a90b8ac3179ff6652b651a) - Render logbooks as single-page documents with section navigation and contextual resources. Thanks @abidlabs! +- [#612](https://github.com/gradio-app/trackio/pull/612) [`2c1b943`](https://github.com/gradio-app/trackio/commit/2c1b9436b093e9f9335c99c10fe4b6afa622e314) - Warn that logbook open is experimental. Thanks @abidlabs! +- [#611](https://github.com/gradio-app/trackio/pull/611) [`96b1d94`](https://github.com/gradio-app/trackio/commit/96b1d94def7596aa1302864cbfc57942dd63822b) - Logbook: auto-capture output artifacts and live-embedded dashboard cells. Thanks @abidlabs! + +## 0.30.1 + +### Features + +- [#609](https://github.com/gradio-app/trackio/pull/609) [`2ed1295`](https://github.com/gradio-app/trackio/commit/2ed12959c73aa9c04ca1096cf7a8315ce2886ed1) - Serve live logbook preview from open. Thanks @abidlabs! + +## 0.30.0 + +### Features + +- [#603](https://github.com/gradio-app/trackio/pull/603) [`a793fd8`](https://github.com/gradio-app/trackio/commit/a793fd88827482134bce295c6228e117de64566a) - Move default artifact downloads under `./.trackio/artifact-downloads/` so materialized files stay out of the project root. Thanks @abidlabs! +- [#604](https://github.com/gradio-app/trackio/pull/604) [`dd3b8ee`](https://github.com/gradio-app/trackio/commit/dd3b8eeef4e0da8bf0df6238617e20ae81f3e564) - docs: clarify artifact types. Thanks @abidlabs! +- [#586](https://github.com/gradio-app/trackio/pull/586) [`7db4c3b`](https://github.com/gradio-app/trackio/commit/7db4c3b6de6ff4903daf7dabed8842f212c8b7fd) - artifact tracking API — `log_artifact`, `use_artifact`, and an `Artifact` class for versioned, named file collections with aliases, content-addressed deduplication, and producer/consumer run lineage; works offline and syncs blobs to Hugging Face Spaces/Datasets. Thanks @Saba9! +- [#597](https://github.com/gradio-app/trackio/pull/597) [`5ba561e`](https://github.com/gradio-app/trackio/commit/5ba561e1b4023abdf63624f4fb396367ed6e9e76) - Add experiment logbooks: shareable static-Space lab notebooks. Thanks @abidlabs! + +## 0.29.0 + +### Features + +- [#593](https://github.com/gradio-app/trackio/pull/593) [`ca4722c`](https://github.com/gradio-app/trackio/commit/ca4722c19d2219a3a78b5977149873b9dff73f34) - Add CLI command to list Trackio Spaces. Thanks @abidlabs! + +## 0.28.1 + +### Features + +- [#589](https://github.com/gradio-app/trackio/pull/589) [`5e9905d`](https://github.com/gradio-app/trackio/commit/5e9905d7815bb063f2d9bb38aa76177405c5e2b4) - Reduce Metrics page log payloads by requesting scalar-only metrics and add URL x-axis initialization/fallback handling. Thanks @evalstate! +- [#590](https://github.com/gradio-app/trackio/pull/590) [`56493a6`](https://github.com/gradio-app/trackio/commit/56493a6e9a0114b8655ab44b8c3dec7bae2a5df5) - Use TRACKIO_WRITE_TOKEN for server write token. Thanks @abidlabs! + +## 0.28.0 + +### Features + +- [#582](https://github.com/gradio-app/trackio/pull/582) [`d9dabb5`](https://github.com/gradio-app/trackio/commit/d9dabb5bda8147ad7c9203646358bd0880fa6521) - Add JSONL inbox fragments: durable logging when the Space is unreachable and on network filesystems. Thanks @abidlabs! + +## 0.27.0 + +### Features + +- [#567](https://github.com/gradio-app/trackio/pull/567) [`8c0ce21`](https://github.com/gradio-app/trackio/commit/8c0ce21ff9746d7c6a250684b1b5f0f3f04e0520) - Add CPU, RAM, disk, network and sensor metrics monitoring for non-Apple devices. Thanks @yannsadowski! +- [#571](https://github.com/gradio-app/trackio/pull/571) [`57bc7df`](https://github.com/gradio-app/trackio/commit/57bc7df27ecd7b3ff543dbd2bff5c94182bfb69e) - Fix IndexError when importing CSV with no numeric metric columns. Thanks @Ckal! +- [#577](https://github.com/gradio-app/trackio/pull/577) [`db0e730`](https://github.com/gradio-app/trackio/commit/db0e730463103142688c1c0efef9ff68a52e281d) - Sort and paginate media items. Thanks @abidlabs! +- [#576](https://github.com/gradio-app/trackio/pull/576) [`6f635e2`](https://github.com/gradio-app/trackio/commit/6f635e22553ce2deaece6a0d6289475ab8683040) - Render plot/metric titles as searchable DOM text. Thanks @abidlabs! +- [#579](https://github.com/gradio-app/trackio/pull/579) [`cf7759e`](https://github.com/gradio-app/trackio/commit/cf7759efaab73ce1e98a8a2355dc1f65d54bc775) - Merge image viewer with current media page, credit to @catwell. Thanks @abidlabs! + +## 0.26.0 + +### Features + +- [#553](https://github.com/gradio-app/trackio/pull/553) [`06011ac`](https://github.com/gradio-app/trackio/commit/06011acc9c73341fd234f9cd8eaf96d5a34ad8ce) - fix: serve static-Space assets at /static/trackio. Thanks @abidlabs! +- [#563](https://github.com/gradio-app/trackio/pull/563) [`551569c`](https://github.com/gradio-app/trackio/commit/551569c16fb56ec63249ebdc28348d326ccf7126) - Fix Traces UI a bit. Thanks @abidlabs! +- [#550](https://github.com/gradio-app/trackio/pull/550) [`5690acd`](https://github.com/gradio-app/trackio/commit/5690acda5da303c63ad332451afeab3e9750fd1a) - fix: keep sparse metrics sparse through smoothing. Thanks @abidlabs! +- [#551](https://github.com/gradio-app/trackio/pull/551) [`0ef7266`](https://github.com/gradio-app/trackio/commit/0ef72660695cf932f3906ddbf33d41d087280a22) - add "group by" dropdown to sidebar. Thanks @Saba9! +- [#538](https://github.com/gradio-app/trackio/pull/538) [`a15c1a8`](https://github.com/gradio-app/trackio/commit/a15c1a8877c07514e0596630bb7c7299299994a9) - Subdue empty dashboard tabs. Thanks @abidlabs! +- [#559](https://github.com/gradio-app/trackio/pull/559) [`0b53a41`](https://github.com/gradio-app/trackio/commit/0b53a413909598f92138b6b6395a91c2d5034faf) - Store traces separately from metrics. Thanks @abidlabs! +- [#556](https://github.com/gradio-app/trackio/pull/556) [`d110001`](https://github.com/gradio-app/trackio/commit/d110001dbd9f6b262dfe41f2b702e3a71aa0cfc9) - fix: keep selected x-axis option in dropdown and dismiss dropdown on re-click. Thanks @Saba9! +- [#560](https://github.com/gradio-app/trackio/pull/560) [`aee2923`](https://github.com/gradio-app/trackio/commit/aee2923d3ada4f74d62d065c16f1f6a56a295e48) - Paginate Traces tab with step filter. Thanks @abidlabs! + +### Fixes + +- [#540](https://github.com/gradio-app/trackio/pull/540) [`0b674ac`](https://github.com/gradio-app/trackio/commit/0b674ac6438738de89bc5b3fb700ccfd8a39368c) - raise default metrics sampling cap from 1500 to 3000 so client-side smoothing on the Metrics tab runs over higher-resolution data. Thanks @edbeeching! + +## 0.25.1 + +### Features + +- [#535](https://github.com/gradio-app/trackio/pull/535) [`d7f1b27`](https://github.com/gradio-app/trackio/commit/d7f1b27a98f185d2d97ef54975d5865e0b5243c9) - Avoid HF token leaks in static snapshots. Thanks @abidlabs! + +## 0.25.0 + +### Features + +- [#533](https://github.com/gradio-app/trackio/pull/533) [`08bc5eb`](https://github.com/gradio-app/trackio/commit/08bc5eb090525d3ff5f7fa4233c30c42162aa74c) - Fix Windows-only emoji mojibake when uploading Space README. Thanks @tomaarsen! +- [#518](https://github.com/gradio-app/trackio/pull/518) [`e7ed176`](https://github.com/gradio-app/trackio/commit/e7ed176da53d8b49290fddd890b3d18c0b9b958f) - Traces in Trackio. Thanks @abidlabs! +- [#531](https://github.com/gradio-app/trackio/pull/531) [`27a50a3`](https://github.com/gradio-app/trackio/commit/27a50a37362020304b774344b4c774ff548985b6) - Add configurable custom frontends for Trackio. Thanks @abidlabs! + +## 0.24.2 + +### Features + +- [#527](https://github.com/gradio-app/trackio/pull/527) [`7d1c0b9`](https://github.com/gradio-app/trackio/commit/7d1c0b9c37ce9a9845e6bbe6c083da9d36084caf) - Fix dashboard UX issues: smoothing in share URL, run selection, and run filtering. Thanks @abidlabs! +- [#526](https://github.com/gradio-app/trackio/pull/526) [`643878a`](https://github.com/gradio-app/trackio/commit/643878a82985fab9e6675f769ff0107cb46e042a) - Add emoji to README and deploy README content. Thanks @qgallouedec! +- [#529](https://github.com/gradio-app/trackio/pull/529) [`a77972b`](https://github.com/gradio-app/trackio/commit/a77972b68541ebe9e056824e69c9bbca3979ece4) - Remove pydub dependency. Thanks @abidlabs! + +## 0.24.1 + +### Features + +- [#524](https://github.com/gradio-app/trackio/pull/524) [`65a6897`](https://github.com/gradio-app/trackio/commit/65a6897561b465fc8f05550562f8da1ba3c99060) - Fix `trackio skills add`. Thanks @abidlabs! +- [#522](https://github.com/gradio-app/trackio/pull/522) [`05aaca7`](https://github.com/gradio-app/trackio/commit/05aaca7f166bf7667b60b40656d677532a4bdd6e) - relax `starlette` dependency and fix import style. Thanks @abidlabs! +- [#525](https://github.com/gradio-app/trackio/pull/525) [`32c05c5`](https://github.com/gradio-app/trackio/commit/32c05c5d5e3aa84ca7099a6fa08a9093ccd4b95f) - Restore sidebar share and embed snippets, and fix query parameter regression. Thanks @abidlabs! + +## 0.24.0 + +### Features + +- [#502](https://github.com/gradio-app/trackio/pull/502) [`3b397df`](https://github.com/gradio-app/trackio/commit/3b397dfbaff9de137b088f3cad528117e14faab1) - Add docs on SQL & Parquet schema / format, as well as a new CLI command: `trackio query project --project PROJECT --sql SQL_QUERY`. Thanks @abidlabs! +- [#506](https://github.com/gradio-app/trackio/pull/506) [`498bbc4`](https://github.com/gradio-app/trackio/commit/498bbc47f66cc90cc5776f363d001a5571941c00) - Scope bucket sync to trackio/ subtree to avoid walking the HF cache. Thanks @abidlabs! +- [#505](https://github.com/gradio-app/trackio/pull/505) [`8e26ab9`](https://github.com/gradio-app/trackio/commit/8e26ab93b5d9caa2f81334f6fff42fb9cefbb232) - Add an `id` field to `Run` which is used internally, allowing users to have multiple runs with the same run name. Thanks @abidlabs! +- [#517](https://github.com/gradio-app/trackio/pull/517) [`29e1034`](https://github.com/gradio-app/trackio/commit/29e1034b795567ec5ed6d19c5a946915a6498e2a) - Fix static exports, Space bucket handling, and other misc issues. Thanks @abidlabs! +- [#489](https://github.com/gradio-app/trackio/pull/489) [`1b96db3`](https://github.com/gradio-app/trackio/commit/1b96db39c8fd4326e621ee2336b0fca4f263a18a) - Remove `gradio` dependency in `trackio` -- only `gradio_client` is needed locally anymore. Also lazily import `pandas` and remove it as a dependency. Thanks @abidlabs! +- [#513](https://github.com/gradio-app/trackio/pull/513) [`d54d290`](https://github.com/gradio-app/trackio/commit/d54d290fcb1bb08358b558a43a962f78abe990ea) - Reduce HF Spaces 429s: polling tuning and batched metric logs API. Thanks @abidlabs! +- [#516](https://github.com/gradio-app/trackio/pull/516) [`afe2959`](https://github.com/gradio-app/trackio/commit/afe295988928a3ea3ded38bdb5bb05cca85d3c74) - Fix run list order and legend overflow. Thanks @abidlabs! +- [#515](https://github.com/gradio-app/trackio/pull/515) [`0a242b8`](https://github.com/gradio-app/trackio/commit/0a242b85127b02f532b24c7fd2bb046580cc7641) - Add Gradio-compatible /gradio_api routes on Spaces. Thanks @abidlabs! +- [#510](https://github.com/gradio-app/trackio/pull/510) [`60bbc86`](https://github.com/gradio-app/trackio/commit/60bbc86b4e7f880de72075e5bf31b093709bb5a4) - Add server_url and TRACKIO_SERVER_URL for self-hosted servers; space_id and TRACKIO_SPACE_ID take precedence when both are set. Thanks @abidlabs! +- [#509](https://github.com/gradio-app/trackio/pull/509) [`21c099a`](https://github.com/gradio-app/trackio/commit/21c099aa830a278973fab4c7c58a0139f417caa4) - Fix: Open browser with write_token so trackio show allows mutations. Thanks @abidlabs! + +## 0.23.0 + +### Features + +- [#494](https://github.com/gradio-app/trackio/pull/494) [`e8a897d`](https://github.com/gradio-app/trackio/commit/e8a897d2266d9b2558f72d768b0b21f4d0a8781b) - Add a settings/CLI page to Trackio. Thanks @abidlabs! +- [#481](https://github.com/gradio-app/trackio/pull/481) [`882647e`](https://github.com/gradio-app/trackio/commit/882647ec1599cf04500d03b5ca75ddc2733682e2) - Add multi-GPU system metrics support. Thanks @Saba9! +- [#485](https://github.com/gradio-app/trackio/pull/485) [`46a3cc3`](https://github.com/gradio-app/trackio/commit/46a3cc3758719e171417612efee102a487e71ebd) - Fix/remove flaky E2E space tests. Thanks @abidlabs! +- [#501](https://github.com/gradio-app/trackio/pull/501) [`06ea885`](https://github.com/gradio-app/trackio/commit/06ea8852f5e40ab3f1cf629a0a01af5c17f847a1) - Fix SQLite corruption on bucket-mounted Spaces. Thanks @abidlabs! +- [#496](https://github.com/gradio-app/trackio/pull/496) [`af23d74`](https://github.com/gradio-app/trackio/commit/af23d74438b146c4a3512ace15ea984656e943ed) - Prevent trackio errors from crashing the user's training loop. Thanks @abidlabs! + +## 0.22.0 + +### Features + +- [#484](https://github.com/gradio-app/trackio/pull/484) [`cc05ada`](https://github.com/gradio-app/trackio/commit/cc05ada8e89773f3a894af99b801ef680f64418f) - Fix duplicate columns in parquet export. Thanks @abidlabs! +- [#487](https://github.com/gradio-app/trackio/pull/487) [`853f764`](https://github.com/gradio-app/trackio/commit/853f7646a70d12633afaa4f69db86425aa665413) - Relax `PIL` dependency and remove `plotly` as it's no longer used. Thanks @abidlabs! + +## 0.21.2 + +### Features + +- [#482](https://github.com/gradio-app/trackio/pull/482) [`f62180a`](https://github.com/gradio-app/trackio/commit/f62180a0218bc99a259d5ca110a0384a6cae11c8) - Use server-side bucket copy when freezing Spaces. Thanks @abidlabs! + +## 0.21.1 + +### Features + +- [#475](https://github.com/gradio-app/trackio/pull/475) [`fcb476c`](https://github.com/gradio-app/trackio/commit/fcb476cd37a40923e9679aaf966f41d582a878a8) - Tweaks. Thanks @abidlabs! +- [#477](https://github.com/gradio-app/trackio/pull/477) [`7d52dfd`](https://github.com/gradio-app/trackio/commit/7d52dfdce5b6eff6a34501a6d5a620220663cf09) - Fix `.sync()` and add `.freeze()` as a separate methods. Thanks @abidlabs! + +## 0.21.0 + +### Features + +- [#467](https://github.com/gradio-app/trackio/pull/467) [`f357deb`](https://github.com/gradio-app/trackio/commit/f357debf78957e4c1f2b901bee4f77cf397298b4) - Allow logged metrics as x-axis choices. Thanks @abidlabs! +- [#474](https://github.com/gradio-app/trackio/pull/474) [`655673d`](https://github.com/gradio-app/trackio/commit/655673d4c6b7c8b7ee8f87f2589f2dbbc3d2ef91) - Fix file descriptor leak from `sqlite3.connect`. Thanks @abidlabs! +- [#470](https://github.com/gradio-app/trackio/pull/470) [`bea8c9d`](https://github.com/gradio-app/trackio/commit/bea8c9dcae0b59d071b6c779c97ee525c9bbf6e7) - Restores tooltips to line plots and fixes the call to uses TTL instead of OAuth. Thanks @abidlabs! +- [#471](https://github.com/gradio-app/trackio/pull/471) [`246fce0`](https://github.com/gradio-app/trackio/commit/246fce0a01619e1c2c538c67b3e460883334d500) - Deprecate dataset backend in favor of buckets. Thanks @abidlabs! +- [#465](https://github.com/gradio-app/trackio/pull/465) [`3e11174`](https://github.com/gradio-app/trackio/commit/3e1117438bb8168b802245a33059affa558ae519) - Use HF buckets as backend. Thanks @abidlabs! +- [#469](https://github.com/gradio-app/trackio/pull/469) [`915d170`](https://github.com/gradio-app/trackio/commit/915d17045133172b59195acfdcc70709229668aa) - Make static Spaces work with Buckets and also allow conversion from Gradio SDK to Static Spaces. Thanks @abidlabs! + +## 0.20.2 + +### Features + +- [#464](https://github.com/gradio-app/trackio/pull/464) [`c89ebb3`](https://github.com/gradio-app/trackio/commit/c89ebb3b50f695bc7f16cbc6f46dce86f79a01e9) - Improve rendering of curves. Thanks @abidlabs! +- [#462](https://github.com/gradio-app/trackio/pull/462) [`9160b78`](https://github.com/gradio-app/trackio/commit/9160b78ff6f258f0b87a4f34a24e7d0b5dfbf2fb) - Refactor plot title to display only the metric name without the path. Thanks @qgallouedec! + +## 0.20.1 + +### Features + +- [#454](https://github.com/gradio-app/trackio/pull/454) [`22881db`](https://github.com/gradio-app/trackio/commit/22881dbbbb6b81197a00a19853771007093d61e4) - Bar chart single point. Thanks @abidlabs! +- [#455](https://github.com/gradio-app/trackio/pull/455) [`f8db51a`](https://github.com/gradio-app/trackio/commit/f8db51a20ca61ef703f3f2c2ee1ebd9c4f239cf2) - Adds a static Trackio mode via `trackio.sync(sdk="static")` and support for the `TRACKIO_SPACE_ID` environment variable. Thanks @abidlabs! + +## 0.20.0 + +### Features + +- [#450](https://github.com/gradio-app/trackio/pull/450) [`b0571ef`](https://github.com/gradio-app/trackio/commit/b0571ef6207a1ce346696f858ad2b7b584dd194f) - Use Svelte source for Gradio components directly in Trackio dashboard. Thanks @abidlabs! + +## 0.19.0 + +### Features + +- [#445](https://github.com/gradio-app/trackio/pull/445) [`cef4a58`](https://github.com/gradio-app/trackio/commit/cef4a583cb76f4091fc6c0e5783124ee84f8e243) - Add remote HF Space support to CLI. Thanks @abidlabs! +- [#444](https://github.com/gradio-app/trackio/pull/444) [`358f2a9`](https://github.com/gradio-app/trackio/commit/358f2a9ca238ee8b90b5a8c96220da287e0698fb) - Fix alerts placeholder flashing on reports page. Thanks @abidlabs! + +## 0.18.0 + +### Features + +- [#435](https://github.com/gradio-app/trackio/pull/435) [`4a47112`](https://github.com/gradio-app/trackio/commit/4a471128e18a39e45fad48a67fd711c5ae9e4aed) - feat: allow hiding section header accordions. Thanks @Saba9! +- [#439](https://github.com/gradio-app/trackio/pull/439) [`18e9650`](https://github.com/gradio-app/trackio/commit/18e96503d5a3a7cf926e92782d457e23c19942bd) - Add alerts with webhooks, CLI, and documentation. Thanks @abidlabs! +- [#438](https://github.com/gradio-app/trackio/pull/438) [`0875ccd`](https://github.com/gradio-app/trackio/commit/0875ccd3d8a41b1376f64030f21cfe8cdcc73b05) - Add "share this view" functionality. Thanks @qgallouedec! +- [#409](https://github.com/gradio-app/trackio/pull/409) [`9282403`](https://github.com/gradio-app/trackio/commit/9282403d8896d48679b0f888208a7ba5bdd4271a) - Add Apple Silicon GPU and system monitoring support. Thanks @znation! +- [#434](https://github.com/gradio-app/trackio/pull/434) [`4193223`](https://github.com/gradio-app/trackio/commit/41932230a3a2e1c16405dba08ecba5a42f11d1a8) - fix: table slider crash. Thanks @Saba9! + +### Fixes + +- [#441](https://github.com/gradio-app/trackio/pull/441) [`3a2d11d`](https://github.com/gradio-app/trackio/commit/3a2d11dab0b4b37c925abc30ef84b0e2910321ee) - preserve x-axis step when toggling run checkboxes. Thanks @Saba9! + +## 0.17.0 + +### Features + +- [#428](https://github.com/gradio-app/trackio/pull/428) [`f7dd1ce`](https://github.com/gradio-app/trackio/commit/f7dd1ce2dc8a1936f9983467fcbcf93bfef01e09) - feat: add ability to rename runs. Thanks @Saba9! +- [#437](https://github.com/gradio-app/trackio/pull/437) [`2727c0b`](https://github.com/gradio-app/trackio/commit/2727c0b0755f48f7f186162ea45185c98f6b5516) - Add markdown reports across Trackio. Thanks @abidlabs! +- [#427](https://github.com/gradio-app/trackio/pull/427) [`5aeb9ed`](https://github.com/gradio-app/trackio/commit/5aeb9edcfd2068d309d9d64f172dcbcc327be1ab) - Make Trackio logging much more robust. Thanks @abidlabs! + +## 0.16.1 + +### Features + +- [#431](https://github.com/gradio-app/trackio/pull/431) [`c7ce55b`](https://github.com/gradio-app/trackio/commit/c7ce55b14dd5eb0c2165fb15df17dd60721c9325) - Lazy load the UI when trackio is imported. Thanks @abidlabs! + +## 0.16.0 + +### Features + +- [#426](https://github.com/gradio-app/trackio/pull/426) [`ead4dc8`](https://github.com/gradio-app/trackio/commit/ead4dc8e74ee2d8e47d61bca0a7668456acf49be) - Fix redundant double rendering of group checkboxes. Thanks @abidlabs! +- [#413](https://github.com/gradio-app/trackio/pull/413) [`39c4750`](https://github.com/gradio-app/trackio/commit/39c4750951d554ba6eb4d58847c6bb444b2891a8) - Check `dist-packages` when checking for source installation. Thanks @sergiopaniego! +- [#423](https://github.com/gradio-app/trackio/pull/423) [`2e52ab3`](https://github.com/gradio-app/trackio/commit/2e52ab303e3041718a6a56fbf84d0848aca9ad67) - Fix legend outline visibility issue. Thanks @Raghunath-Balaji! +- [#407](https://github.com/gradio-app/trackio/pull/407) [`c8a384d`](https://github.com/gradio-app/trackio/commit/c8a384ddfe5a295cecf862a26178d40e48acb424) - Fix pytests that were failling locally on MacOS. Thanks @abidlabs! +- [#405](https://github.com/gradio-app/trackio/pull/405) [`35aae4e`](https://github.com/gradio-app/trackio/commit/35aae4e3aa3e2b2888887528478b9dc6a9808bda) - Add conditional padding for HF Space dashboard when not in iframe. Thanks @znation! + +## 0.15.0 + +### Features + +- [#397](https://github.com/gradio-app/trackio/pull/397) [`6b38ad0`](https://github.com/gradio-app/trackio/commit/6b38ad02e5d73a0df49c4eede7e91331282ece04) - Adds `--host` cli option support. Thanks @abidlabs! +- [#396](https://github.com/gradio-app/trackio/pull/396) [`4a4d1ab`](https://github.com/gradio-app/trackio/commit/4a4d1ab85e63d923132a3fa7afa5d90e16431bec) - Fix run selection issue. Thanks @abidlabs! +- [#394](https://github.com/gradio-app/trackio/pull/394) [`c47a3a3`](https://github.com/gradio-app/trackio/commit/c47a3a31f8c4b83bce1aa7fc22eeba3d9021ad3d) - Add wandb-compatible API for trackio. Thanks @abidlabs! +- [#378](https://github.com/gradio-app/trackio/pull/378) [`b02046a`](https://github.com/gradio-app/trackio/commit/b02046a5b0dad7c9854e099a87f884afba4aecb2) - Add JSON export button for line plots and upgrade gradio dependency. Thanks @JamshedAli18! + +## 0.14.2 + +### Features + +- [#386](https://github.com/gradio-app/trackio/pull/386) [`f9452cd`](https://github.com/gradio-app/trackio/commit/f9452cdb8f0819368f3610f7ac0ed08957305275) - Fixing some issues related to deployed Trackio Spaces. Thanks @abidlabs! + +## 0.14.1 + +### Features + +- [#382](https://github.com/gradio-app/trackio/pull/382) [`44fe9bb`](https://github.com/gradio-app/trackio/commit/44fe9bb264fb2aafb0ec302ff15227c045819a2c) - Fix app file path when Trackio is not installed from source. Thanks @abidlabs! +- [#380](https://github.com/gradio-app/trackio/pull/380) [`c3f4cff`](https://github.com/gradio-app/trackio/commit/c3f4cff74bc5676e812773d8571454894fcdc7cc) - Add CLI commands for querying projects, runs, and metrics. Thanks @abidlabs! + +## 0.14.0 + +### Features + +- [#377](https://github.com/gradio-app/trackio/pull/377) [`5c5015b`](https://github.com/gradio-app/trackio/commit/5c5015b68c85c5de51111dad983f735c27b9a05f) - fixed wrapping issue in Runs table. Thanks @gaganchapa! +- [#374](https://github.com/gradio-app/trackio/pull/374) [`388e26b`](https://github.com/gradio-app/trackio/commit/388e26b9e9f24cd7ad203affe9b709be885b3d24) - Save Optimized Parquet files. Thanks @lhoestq! +- [#371](https://github.com/gradio-app/trackio/pull/371) [`fbace9c`](https://github.com/gradio-app/trackio/commit/fbace9cd7732c166f34d268f54b05bb06846cc5d) - Add GPU metrics logging. Thanks @kashif! +- [#367](https://github.com/gradio-app/trackio/pull/367) [`862840c`](https://github.com/gradio-app/trackio/commit/862840c13e30fc960cbee5b9eac4d3c25beba9de) - Add option to only show latest run, and fix the double logo issue. Thanks @abidlabs! + +## 0.13.1 + +### Features + +- [#369](https://github.com/gradio-app/trackio/pull/369) [`767e9fe`](https://github.com/gradio-app/trackio/commit/767e9fe095d7c6ed102016caf927c1517fb8618c) - tiny pr removing unnecessary code. Thanks @abidlabs! + +## 0.13.0 + +### Features + +- [#358](https://github.com/gradio-app/trackio/pull/358) [`073715d`](https://github.com/gradio-app/trackio/commit/073715d1caf8282f68890117f09c3ac301205312) - Improvements to `trackio.sync()`. Thanks @abidlabs! + +## 0.12.0 + +### Features + +- [#357](https://github.com/gradio-app/trackio/pull/357) [`02ba815`](https://github.com/gradio-app/trackio/commit/02ba815358060f1966052de051a5bdb09702920e) - Redesign media and tables to show up on separate page. Thanks @abidlabs! +- [#359](https://github.com/gradio-app/trackio/pull/359) [`08fe9c9`](https://github.com/gradio-app/trackio/commit/08fe9c9ddd7fe99ee811555fdfb62df9ab88e939) - docs: Improve docstrings. Thanks @qgallouedec! + +## 0.11.0 + +### Features + +- [#355](https://github.com/gradio-app/trackio/pull/355) [`ea51f49`](https://github.com/gradio-app/trackio/commit/ea51f4954922f21be76ef828700420fe9a912c4b) - Color code run checkboxes and match with plot lines. Thanks @abidlabs! +- [#353](https://github.com/gradio-app/trackio/pull/353) [`8abe691`](https://github.com/gradio-app/trackio/commit/8abe6919aeefe21fc7a23af814883efbb037c21f) - Remove show_api from demo.launch. Thanks @sergiopaniego! +- [#351](https://github.com/gradio-app/trackio/pull/351) [`8a8957e`](https://github.com/gradio-app/trackio/commit/8a8957e530dd7908d1fef7f2df030303f808101f) - Add `trackio.save()`. Thanks @abidlabs! + +## 0.10.0 + +### Features + +- [#305](https://github.com/gradio-app/trackio/pull/305) [`e64883a`](https://github.com/gradio-app/trackio/commit/e64883a51f7b8b93f7d48b8afe55acdb62238b71) - bump to gradio 6.0, make `trackio` compatible, and fix related issues. Thanks @abidlabs! + +## 0.9.1 + +### Features + +- [#344](https://github.com/gradio-app/trackio/pull/344) [`7e01024`](https://github.com/gradio-app/trackio/commit/7e010241d9a34794e0ce0dc19c1a6f0cf94ba856) - Avoid redundant calls to /whoami-v2. Thanks @Wauplin! + +## 0.9.0 + +### Features + +- [#343](https://github.com/gradio-app/trackio/pull/343) [`51bea30`](https://github.com/gradio-app/trackio/commit/51bea30f2877adff8e6497466d3a799400a0a049) - Sync offline projects to Hugging Face spaces. Thanks @candemircan! +- [#341](https://github.com/gradio-app/trackio/pull/341) [`4fd841f`](https://github.com/gradio-app/trackio/commit/4fd841fa190e15071b02f6fba7683ef4f393a654) - Adds a basic UI test to `trackio`. Thanks @abidlabs! +- [#339](https://github.com/gradio-app/trackio/pull/339) [`011d91b`](https://github.com/gradio-app/trackio/commit/011d91bb6ae266516fd250a349285670a8049d05) - Allow customzing the trackio color palette. Thanks @abidlabs! + +## 0.8.1 + +### Features + +- [#336](https://github.com/gradio-app/trackio/pull/336) [`5f9f51d`](https://github.com/gradio-app/trackio/commit/5f9f51dac8677f240d7c42c3e3b2660a22aee138) - Support a list of `Trackio.Image` in a `trackio.Table` cell. Thanks @abidlabs! + +## 0.8.0 + +### Features + +- [#331](https://github.com/gradio-app/trackio/pull/331) [`2c02d0f`](https://github.com/gradio-app/trackio/commit/2c02d0fd0a5824160528782402bb0dd4083396d5) - Truncate table string values that are greater than 250 characters (configuirable via env variable). Thanks @abidlabs! +- [#324](https://github.com/gradio-app/trackio/pull/324) [`50b2122`](https://github.com/gradio-app/trackio/commit/50b2122e7965ac82a72e6cb3b7d048bc10a2a6b1) - Add log y-axis functionality to UI. Thanks @abidlabs! +- [#326](https://github.com/gradio-app/trackio/pull/326) [`61dc1f4`](https://github.com/gradio-app/trackio/commit/61dc1f40af2f545f8e70395ddf0dbb8aee6b60d5) - Fix: improve table rendering for metrics in Trackio Dashboard. Thanks @vigneshwaran! +- [#328](https://github.com/gradio-app/trackio/pull/328) [`6857cbb`](https://github.com/gradio-app/trackio/commit/6857cbbe557a59a4642f210ec42566d108294e63) - Support trackio.Table with trackio.Image columns. Thanks @abidlabs! +- [#323](https://github.com/gradio-app/trackio/pull/323) [`6857cbb`](https://github.com/gradio-app/trackio/commit/6857cbbe557a59a4642f210ec42566d108294e63) - add Trackio client implementations in Go, Rust, and JS. Thanks @vaibhav-research! + +## 0.7.0 + +### Features + +- [#277](https://github.com/gradio-app/trackio/pull/277) [`db35601`](https://github.com/gradio-app/trackio/commit/db35601b9c023423c4654c9909b8ab73e58737de) - fix: make grouped runs view reflect live updates. Thanks @Saba9! +- [#320](https://github.com/gradio-app/trackio/pull/320) [`24ae739`](https://github.com/gradio-app/trackio/commit/24ae73969b09fb3126acd2f91647cdfbf8cf72a1) - Add additional query parms for xmin, xmax, and smoothing. Thanks @abidlabs! +- [#270](https://github.com/gradio-app/trackio/pull/270) [`cd1dfc3`](https://github.com/gradio-app/trackio/commit/cd1dfc3dc641b4499ac6d4a1b066fa8e2b52c57b) - feature: add support for logging audio. Thanks @Saba9! + +## 0.6.0 + +### Features + +- [#309](https://github.com/gradio-app/trackio/pull/309) [`1df2353`](https://github.com/gradio-app/trackio/commit/1df23534d6c01938c8db9c0f584ffa23e8d6021d) - Add histogram support with wandb-compatible API. Thanks @abidlabs! +- [#315](https://github.com/gradio-app/trackio/pull/315) [`76ba060`](https://github.com/gradio-app/trackio/commit/76ba06055dc43ca8f03b79f3e72d761949bd19a8) - Add guards to avoid silent fails. Thanks @Xmaster6y! +- [#313](https://github.com/gradio-app/trackio/pull/313) [`a606b3e`](https://github.com/gradio-app/trackio/commit/a606b3e1c5edf3d4cf9f31bd50605226a5a1c5d0) - No longer prevent certain keys from being used. Instead, dunderify them to prevent collisions with internal usage. Thanks @abidlabs! +- [#317](https://github.com/gradio-app/trackio/pull/317) [`27370a5`](https://github.com/gradio-app/trackio/commit/27370a595d0dbdf7eebbe7159d2ba778f039da44) - quick fixes for trackio.histogram. Thanks @abidlabs! +- [#312](https://github.com/gradio-app/trackio/pull/312) [`aa0f3bf`](https://github.com/gradio-app/trackio/commit/aa0f3bf372e7a0dd592a38af699c998363830eeb) - Fix video logging by adding TRACKIO_DIR to allowed_paths. Thanks @abidlabs! + +## 0.5.3 + +### Features + +- [#300](https://github.com/gradio-app/trackio/pull/300) [`5e4cacf`](https://github.com/gradio-app/trackio/commit/5e4cacf2e7ce527b4ce60de3a5bc05d2c02c77fb) - Adds more environment variables to allow customization of Trackio dashboard. Thanks @abidlabs! + +## 0.5.2 + +### Features + +- [#293](https://github.com/gradio-app/trackio/pull/293) [`64afc28`](https://github.com/gradio-app/trackio/commit/64afc28d3ea1dfd821472dc6bf0b8ed35a9b74be) - Ensures that the TRACKIO_DIR environment variable is respected. Thanks @abidlabs! +- [#287](https://github.com/gradio-app/trackio/pull/287) [`cd3e929`](https://github.com/gradio-app/trackio/commit/cd3e9294320949e6b8b829239069a43d5d7ff4c1) - fix(sqlite): unify .sqlite extension, allow export when DBs exist, clean WAL sidecars on import. Thanks @vaibhav-research! + +### Fixes + +- [#291](https://github.com/gradio-app/trackio/pull/291) [`3b5adc3`](https://github.com/gradio-app/trackio/commit/3b5adc3d1f452dbab7a714d235f4974782f93730) - Fix the wheel build. Thanks @pngwn! + +## 0.5.1 + +### Fixes + +- [#278](https://github.com/gradio-app/trackio/pull/278) [`314c054`](https://github.com/gradio-app/trackio/commit/314c05438007ddfea3383e06fd19143e27468e2d) - Fix row orientation of metrics plots. Thanks @abidlabs! \ No newline at end of file diff --git a/trackio/__init__.py b/trackio/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7f921eb5f321681e9a1e2665cb6e9a092d6dd6a5 --- /dev/null +++ b/trackio/__init__.py @@ -0,0 +1,1280 @@ +import atexit +import glob +import json +import logging +import os +import shutil +import warnings +import webbrowser +from pathlib import Path +from typing import Any + +import huggingface_hub +from gradio_client import handle_file +from huggingface_hub import SpaceStorage +from huggingface_hub.errors import LocalTokenNotFoundError + +from trackio import context_vars, deploy, utils +from trackio._unsupported import ( + ArtifactTTL, + Classes, + Config, + EvalTable, + Graph, + JoinedTable, + Molecule, + Plotly, + Settings, +) +from trackio.alerts import AlertLevel +from trackio.api import Api +from trackio.apple_gpu import apple_gpu_available +from trackio.apple_gpu import log_apple_gpu as _log_apple_gpu +from trackio.artifact import Artifact +from trackio.cpu import cpu_available +from trackio.cpu import log_cpu as _log_cpu +from trackio.deploy import freeze, sync +from trackio.frontend_config import resolve_frontend_dir +from trackio.gpu import gpu_available +from trackio.gpu import log_gpu as _log_nvidia_gpu +from trackio.histogram import Histogram +from trackio.imports import import_csv, import_tf_events +from trackio.launch import launch_trackio_dashboard +from trackio.markdown import Markdown +from trackio.media import ( + TrackioAudio, + TrackioHtml, + TrackioImage, + TrackioObject3D, + TrackioVideo, + get_project_media_path, +) +from trackio.references import ( + ReferenceHandler, + ResolvedReference, + register_reference_handler, +) +from trackio.registry import Collection as Collection +from trackio.registry import Registry as Registry +from trackio.remote_client import RemoteClient +from trackio.run import Run +from trackio.server import TrackioDashboardApp, build_starlette_app_only +from trackio.sqlite_storage import SQLiteStorage +from trackio.table import Table +from trackio.trace import Trace +from trackio.typehints import UploadEntry +from trackio.utils import TRACKIO_DIR, TRACKIO_LOGO_DIR, _emit_nonfatal_warning + +logging.getLogger("httpx").setLevel(logging.WARNING) + +__version__ = json.loads(Path(__file__).parent.joinpath("package.json").read_text())[ + "version" +] + + +class _TupleNoPrint(tuple): + def __repr__(self) -> str: + return "" + + +__all__ = [ + "init", + "log", + "log_system", + "log_gpu", + "log_artifact", + "use_artifact", + "log_cpu", + "finish", + "alert", + "AlertLevel", + "show", + "sync", + "freeze", + "delete_project", + "import_csv", + "import_tf_events", + "save", + "Artifact", + "ArtifactTTL", + "Classes", + "Config", + "EvalTable", + "Graph", + "JoinedTable", + "Molecule", + "Plotly", + "Settings", + "ReferenceHandler", + "ResolvedReference", + "register_reference_handler", + "Image", + "Video", + "Audio", + "Html", + "Object3D", + "Table", + "Trace", + "Histogram", + "Markdown", + "Api", + "TRACKIO_LOGO_DIR", +] + +Audio = TrackioAudio +Image = TrackioImage +Video = TrackioVideo +Html = TrackioHtml +Object3D = TrackioObject3D + + +config = {} + +_atexit_registered = False +_spaces_created_this_session: set[str] = set() +_projects_notified_auto_log_hw: set[str] = set() + + +def _cleanup_current_run(): + run = context_vars.current_run.get() + if run is not None: + try: + run.finish() + except Exception: + pass + + +def _safe_get_runs_for_init( + project: str, + space_id: str | None, + server_base_url: str | None, + write_token: str | None, + resume: str, + remote_client: RemoteClient | None = None, + check_existing_for_never: bool = False, +) -> list[str]: + if space_id is not None or server_base_url is not None: + if resume == "never" and not check_existing_for_never: + return [] + if remote_client is not None: + source = space_id or server_base_url + try: + runs = remote_client.predict( + project=project, api_name="/get_runs_for_project" + ) + return runs if isinstance(runs, list) else [] + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not inspect existing runs for project '{project}' on '{source}': {e}. Continuing without resume metadata." + ) + return [] + try: + return SQLiteStorage.get_runs(project) + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not inspect existing runs for project '{project}': {e}. Continuing without resume metadata." + ) + return [] + + +def _migrate_local_project_for_init(project: str) -> None: + if utils.get_storage_mode() == "jsonl": + return + if not SQLiteStorage.get_project_db_path(project).exists(): + return + try: + SQLiteStorage.init_db(project) + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not migrate the local database for project '{project}': {e}." + ) + + +def _safe_get_latest_run_for_init( + project: str, + name: str, + space_id: str | None = None, + server_base_url: str | None = None, + write_token: str | None = None, + remote_client: RemoteClient | None = None, +) -> dict | None: + if ( + space_id is not None or server_base_url is not None + ) and remote_client is not None: + source = space_id or server_base_url + try: + runs = remote_client.predict( + project=project, api_name="/get_runs_for_project" + ) + if not isinstance(runs, list): + return None + matches = [r for r in runs if isinstance(r, dict) and r.get("name") == name] + if not matches: + return None + matches.sort(key=lambda r: r.get("created_at") or "", reverse=True) + return matches[0] + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not inspect existing runs for project '{project}' on '{source}': {e}. Continuing without resume metadata." + ) + return None + try: + return SQLiteStorage.get_latest_run_record_by_name(project, name) + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not inspect existing runs for project '{project}': {e}. Continuing without resume metadata." + ) + return None + + +def _safe_get_last_step_for_init( + project: str, + run_name: str, + space_id: str | None, + server_base_url: str | None, + write_token: str | None, + resumed: bool, + run_id: str | None = None, + remote_client: RemoteClient | None = None, +) -> int | None: + if not resumed: + return None + if ( + space_id is not None or server_base_url is not None + ) and remote_client is not None: + source = space_id or server_base_url + try: + summary_kwargs: dict[str, Any] = { + "project": project, + "api_name": "/get_run_summary", + } + if run_id is not None: + summary_kwargs["run_id"] = run_id + else: + summary_kwargs["run"] = run_name + summary = remote_client.predict(**summary_kwargs) + if isinstance(summary, dict): + last_step = summary.get("last_step") + return last_step if isinstance(last_step, int) else None + return None + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not recover the previous step for run '{run_name}' on '{source}': {e}. Continuing from step 0." + ) + return None + try: + return SQLiteStorage.get_max_step_for_run(project, run_name, run_id=run_id) + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not recover the previous step for run '{run_name}': {e}. Continuing from step 0." + ) + return None + + +def init( + project: str, + name: str | None = None, + group: str | None = None, + space_id: str | None = None, + server_url: str | None = None, + space_storage: SpaceStorage | None = None, + dataset_id: str | None = None, + bucket_id: str | None = None, + config: dict | None = None, + resume: str = "never", + settings: Any = None, + private: bool | None = None, + embed: bool = True, + auto_log_gpu: bool | None = None, + gpu_log_interval: float = 10.0, + auto_log_cpu: bool | None = None, + cpu_log_interval: float = 10.0, + webhook_url: str | None = None, + webhook_min_level: AlertLevel | str | None = None, +) -> Run: + """ + Creates a new Trackio project and returns a [`Run`] object. + + Args: + project (`str`): + The name of the project (can be an existing project to continue tracking or + a new project to start tracking from scratch). + name (`str`, *optional*): + The name of the run (if not provided, a default name will be generated). + group (`str`, *optional*): + The name of the group which this run belongs to in order to help organize + related runs together. You can toggle the entire group's visibility in the + dashboard. + space_id (`str`, *optional*): + If provided, the project will be logged to a Hugging Face Space instead of + a local directory. Should be a complete Space name like + `"username/reponame"` or `"orgname/reponame"`, or just `"reponame"` in which + case the Space will be created in the currently-logged-in Hugging Face + user's namespace. If the Space does not exist, it will be created. If the + Space already exists, the project will be logged to it. Can also be set + via the `TRACKIO_SPACE_ID` environment variable. You cannot log to a + Space that has been **frozen** (converted to the static SDK); use + ``trackio.sync(..., sdk="static")`` only after you are done logging. + Takes precedence over `server_url` and `TRACKIO_SERVER_URL` when more than + one is set. + server_url (`str`, *optional*): + Base URL of a self-hosted Trackio server (``http://`` or ``https://``), or the + write-access URL from ``trackio.show()`` which may include a ``write_token`` query + parameter. The client sends that token on each request (``X-Trackio-Write-Token``); + you can also set ``TRACKIO_WRITE_TOKEN`` instead of embedding the token in the URL. + When set, metrics are sent to that server over HTTP instead of creating or syncing + to a Hugging Face Space. Can also be set via the ``TRACKIO_SERVER_URL`` environment + variable. Ignored when ``space_id`` or ``TRACKIO_SPACE_ID`` is set. + space_storage ([`~huggingface_hub.SpaceStorage`], *optional*): + Choice of persistent storage tier. + dataset_id (`str`, *optional*): + Deprecated: persisting trackio data to a Hugging Face Dataset will be + removed in a future version of trackio. Use `bucket_id` (a Hugging + Face Bucket) instead. + bucket_id (`str`, *optional*): + The ID of the Hugging Face Bucket to use for metric persistence. By default, + when a `space_id` is provided and `bucket_id` is not explicitly set, a + bucket is auto-generated from the space_id. Buckets provide + S3-like storage without git overhead - the SQLite database is stored directly + via `hf-mount` in the Space. Specify a Bucket with name like + `"username/bucketname"` or just `"bucketname"`. + config (`dict`, *optional*): + A dictionary of configuration options. Provided for compatibility with + `wandb.init()`. + resume (`str`, *optional*, defaults to `"never"`): + Controls how to handle resuming a run. Can be one of: + + - `"must"`: Must resume the run with the given name, raises error if run + doesn't exist + - `"allow"`: Resume the run if it exists, otherwise create a new run + - `"never"`: Never resume a run, always create a new one + private (`bool`, *optional*): + Whether to make the Space private. If None (default), the repo will be + public unless the organization's default is private. This value is ignored + if the repo already exists. + settings (`Any`, *optional*): + Not used. Provided for compatibility with `wandb.init()`. + embed (`bool`, *optional*, defaults to `True`): + If running inside a Jupyter/Colab notebook, whether the dashboard should + automatically be embedded in the cell when trackio.init() is called. For + local runs, this launches a local Trackio dashboard and embeds it. For Space runs, + this embeds the Space URL. In Colab, the local dashboard will be accessible + via a public share URL when `share=True`. + auto_log_gpu (`bool` or `None`, *optional*, defaults to `None`): + Controls automatic GPU metrics logging. If `None` (default), GPU logging + is automatically enabled when `nvidia-ml-py` is installed and an NVIDIA + GPU or Apple M series is detected. Set to `True` to force enable or + `False` to disable. + gpu_log_interval (`float`, *optional*, defaults to `10.0`): + The interval in seconds between automatic GPU metric logs. + Only used when `auto_log_gpu=True`. + auto_log_cpu (`bool` or `None`, *optional*, defaults to `None`): + Controls automatic CPU and RAM metrics logging (utilization, memory, + disk I/O, network I/O, and sensors). If `None` (default), CPU logging + is automatically enabled when `psutil` is installed. Set to `True` to + force enable or `False` to disable. + cpu_log_interval (`float`, *optional*, defaults to `10.0`): + The interval in seconds between automatic CPU metric logs. + Only used when CPU auto-logging is enabled. + webhook_url (`str`, *optional*): + A webhook URL to POST alert payloads to when `trackio.alert()` is + called. Supports Slack and Discord webhook URLs natively (payloads + are formatted automatically). Can also be set via the + `TRACKIO_WEBHOOK_URL` environment variable. Individual alerts can + override this URL by passing `webhook_url` to `trackio.alert()`. + webhook_min_level (`AlertLevel` or `str`, *optional*): + Minimum alert level that should trigger webhook delivery. + For example, `AlertLevel.WARN` sends only `WARN` and `ERROR` + alerts to the webhook destination. Can also be set via + `TRACKIO_WEBHOOK_MIN_LEVEL`. + Returns: + `Run`: A [`Run`] object that can be used to log metrics and finish the run. + """ + SQLiteStorage.validate_project_name(project) + + if settings is not None: + _emit_nonfatal_warning( + "* Warning: settings is not used. Provided for compatibility with wandb.init(). Please create an issue at: https://github.com/gradio-app/trackio/issues if you need a specific feature implemented." + ) + + previous_run = context_vars.current_run.get() + if previous_run is not None: + try: + previous_run.finish() + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not finish the previous run '{previous_run.name}': {e}. Continuing with new run." + ) + context_vars.current_run.set(None) + + bucket_id_was_explicit = bucket_id is not None + space_id, server_url = utils.resolve_space_id_and_server_url(space_id, server_url) + if bucket_id is None and utils.on_spaces(): + bucket_id = os.environ.get("TRACKIO_BUCKET_ID") + if server_url is not None and not server_url.startswith(("http://", "https://")): + raise ValueError( + f"`server_url` must be a full URL starting with http:// or https://, got: {server_url!r}" + ) + server_base_url: str | None = None + write_token_resolved: str | None = None + if server_url is not None: + server_base_url, tok = utils.parse_trackio_server_url(server_url) + write_token_resolved = tok or os.environ.get("TRACKIO_WRITE_TOKEN") + if not write_token_resolved: + raise ValueError( + "Self-hosted logging requires a write token: add write_token to the server URL, " + "or set the TRACKIO_WRITE_TOKEN environment variable." + ) + if server_url is not None and (dataset_id is not None or bucket_id is not None): + raise ValueError( + "`dataset_id` and `bucket_id` are Hugging Face Spaces concepts and are not " + "compatible with `server_url`. Configure storage on the self-hosted server." + ) + if space_id is None and dataset_id is not None: + raise ValueError("Must provide a `space_id` when `dataset_id` is provided.") + if dataset_id is not None and bucket_id is not None: + raise ValueError("Cannot provide both `dataset_id` and `bucket_id`.") + try: + space_id, dataset_id, bucket_id = utils.preprocess_space_and_dataset_ids( + space_id, dataset_id, bucket_id + ) + if ( + space_id is not None + and dataset_id is None + and bucket_id is not None + and not bucket_id_was_explicit + and not utils.on_spaces() + ): + bucket_id = deploy.resolve_auto_bucket_id(space_id, bucket_id) + except LocalTokenNotFoundError as e: + raise LocalTokenNotFoundError( + f"You must be logged in to Hugging Face locally when `space_id` is provided to deploy to a Space. {e}" + ) from e + + if space_id is None and bucket_id is not None: + _emit_nonfatal_warning( + "trackio.init() has `bucket_id` set but `space_id` is None: metrics will be logged " + "locally only. Pass `space_id` to create or use a Hugging Face Space, which will be " + "attached to the Hugging Face Bucket.", + UserWarning, + stacklevel=2, + ) + + if space_id is not None: + deploy.raise_if_space_is_frozen_for_logging(space_id) + + remote_source = space_id or server_base_url + + if remote_source is not None: + url = remote_source + context_vars.current_server.set(url) + if space_id is not None: + context_vars.current_space_id.set(space_id) + context_vars.current_server_write_token.set(None) + else: + context_vars.current_space_id.set(None) + context_vars.current_server_write_token.set(write_token_resolved) + else: + url = None + context_vars.current_server.set(None) + context_vars.current_space_id.set(None) + context_vars.current_server_write_token.set(None) + + _should_embed_local = False + + newly_created_space = False + if space_id is not None and space_id in _spaces_created_this_session: + if deploy.space_is_running(space_id): + _spaces_created_this_session.discard(space_id) + else: + newly_created_space = True + + if ( + context_vars.current_project.get() is None + or context_vars.current_project.get() != project + ): + print(f"* Trackio project initialized: {project}") + + if bucket_id is not None: + if utils.on_spaces(): + os.environ["TRACKIO_BUCKET_ID"] = bucket_id + bucket_url = f"https://huggingface.co/buckets/{bucket_id}" + print( + f"* Trackio metrics will be synced to Hugging Face Bucket: {bucket_url}" + ) + elif dataset_id is not None: + if utils.on_spaces(): + os.environ["TRACKIO_DATASET_ID"] = dataset_id + print( + f"* Trackio metrics will be synced to Hugging Face Dataset: {dataset_id}" + ) + if remote_source is None: + print(f"* Trackio metrics logged to: {TRACKIO_DIR}") + _should_embed_local = embed and utils.is_in_notebook() + if not _should_embed_local: + utils.print_dashboard_instructions(project) + elif server_base_url is not None: + print( + f"* Trackio metrics will be sent to self-hosted server: {server_base_url}" + ) + if utils.is_in_notebook() and embed: + utils.embed_url_in_notebook(server_base_url) + else: + try: + if deploy.create_space_if_not_exists( + space_id, + space_storage, + dataset_id, + bucket_id, + private, + ): + _spaces_created_this_session.add(space_id) + newly_created_space = True + user_name, space_name = space_id.split("/") + space_url = deploy.SPACE_HOST_URL.format( + user_name=user_name, space_name=space_name + ) + if utils.is_in_notebook() and embed: + utils.embed_url_in_notebook(space_url) + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not prepare Space '{space_id}': {e}. Logging will continue in local fallback mode until the Space is reachable." + ) + context_vars.current_project.set(project) + + remote_client = None + if space_id is not None and not newly_created_space: + try: + remote_client = RemoteClient( + space_id, + hf_token=huggingface_hub.utils.get_token(), + verbose=False, + ) + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not create a remote client for Space '{space_id}': {e}. Continuing with local fallback metadata lookups." + ) + elif server_base_url is not None: + try: + remote_client = RemoteClient( + server_base_url, + hf_token=None, + write_token=write_token_resolved, + verbose=False, + ) + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not create a remote client for '{server_base_url}': {e}. Continuing with local fallback metadata lookups." + ) + + _migrate_local_project_for_init(project) + existing_run_records = _safe_get_runs_for_init( + project, + space_id, + server_base_url, + write_token_resolved, + resume, + remote_client=remote_client, + check_existing_for_never=name is not None, + ) + existing_runs = [ + r["name"] if isinstance(r, dict) else r for r in existing_run_records + ] + + existing_run = ( + _safe_get_latest_run_for_init( + project, + name, + space_id=space_id, + server_base_url=server_base_url, + write_token=write_token_resolved, + remote_client=remote_client, + ) + if name is not None + else None + ) + resolved_run_id = None + + if resume == "must": + if name is None: + raise ValueError("Must provide a run name when resume='must'") + if existing_run is None: + raise ValueError(f"Run '{name}' does not exist in project '{project}'") + resumed = True + resolved_run_id = existing_run["id"] + elif resume == "allow": + resumed = existing_run is not None + if resumed: + resolved_run_id = existing_run["id"] + elif resume == "never": + resumed = False + else: + raise ValueError("resume must be one of: 'must', 'allow', or 'never'") + + initial_last_step = ( + _safe_get_last_step_for_init( + project, + name, + space_id, + server_base_url, + write_token_resolved, + resumed, + run_id=resolved_run_id, + remote_client=remote_client, + ) + if name is not None + else None + ) + + auto_log_cpu_detected = False + if auto_log_cpu is None: + auto_log_cpu_detected = cpu_available() + auto_log_cpu = auto_log_cpu_detected + + auto_log_gpu_detected = False + nvidia_available = False + apple_available = False + if auto_log_gpu is None: + nvidia_available = gpu_available() + apple_available = apple_gpu_available() + auto_log_gpu_detected = nvidia_available or apple_available + auto_log_gpu = auto_log_gpu_detected + + if project not in _projects_notified_auto_log_hw: + if nvidia_available: + print("* NVIDIA GPU detected, enabling automatic GPU metrics logging") + elif apple_available: + print( + "* Apple Silicon detected, enabling automatic GPU/system metrics logging" + ) + if auto_log_cpu_detected: + print("* psutil detected, enabling automatic CPU/system metrics logging") + if auto_log_gpu_detected or auto_log_cpu_detected: + _projects_notified_auto_log_hw.add(project) + + run = Run( + url=url, + project=project, + client=None, + name=name, + run_id=resolved_run_id, + group=group, + config=config, + space_id=space_id, + bucket_id=bucket_id, + server_base_url=server_base_url, + write_token=write_token_resolved, + existing_runs=existing_runs, + initial_last_step=initial_last_step, + auto_log_gpu=auto_log_gpu, + gpu_log_interval=gpu_log_interval, + auto_log_cpu=auto_log_cpu, + cpu_log_interval=cpu_log_interval, + webhook_url=webhook_url, + webhook_min_level=webhook_min_level, + ) + + if space_id is not None: + try: + SQLiteStorage.set_project_metadata(project, "space_id", space_id) + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not persist Space metadata for project '{project}': {e}. Logging will continue." + ) + try: + if SQLiteStorage.has_pending_data(project): + run._has_local_buffer = True + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not inspect pending buffered data for project '{project}': {e}. Logging will continue." + ) + + global _atexit_registered + if not _atexit_registered: + atexit.register(_cleanup_current_run) + _atexit_registered = True + + if resumed: + print(f"* Resumed existing run: {run.name}") + else: + print(f"* Created new run: {run.name}") + + context_vars.current_run.set(run) + globals()["config"] = run.config + + # NOTE: trackio.init() deliberately does NOT mutate any logbook that happens + # to live in the current directory. Auto-noting a dashboard cell here used to + # corrupt curated logbooks (e.g. injecting a stray cell / page). Cells are + # added explicitly via the `trackio logbook` CLI instead. + + if _should_embed_local: + try: + show(project=project, open_browser=False, block_thread=False) + except Exception as e: + _emit_nonfatal_warning( + f"trackio.init() could not auto-launch the dashboard: {e}. Logging will continue." + ) + + return run + + +def log(metrics: dict, step: int | None = None) -> None: + """ + Logs metrics to the current run. + + Args: + metrics (`dict`): + A dictionary of metrics to log. + step (`int`, *optional*): + The step number. If not provided, the step will be incremented + automatically. + """ + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.log().") + run.log( + metrics=metrics, + step=step, + ) + + +def log_system(metrics: dict) -> None: + """ + Logs system metrics (GPU, etc.) to the current run using timestamps instead of steps. + + Args: + metrics (`dict`): + A dictionary of system metrics to log. + """ + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.log_system().") + run.log_system(metrics=metrics) + + +def log_artifact( + artifact_or_path: Artifact | str | Path, + name: str | None = None, + type: str | None = None, + aliases: list[str] | None = None, + overwrite: bool = False, +) -> Artifact: + """ + Logs an artifact as an output of the current run. + + Args: + artifact_or_path (`Artifact`, `str`, or `Path`): + The artifact to log (must have at least one file added via + `add_file` or `add_dir`), or a path to a file or directory to + log as a new artifact. + name (`str`, *optional*): + Artifact name when logging a path. Defaults to + `run--`. Must not be passed with an `Artifact`. + type (`str`, *optional*): + Artifact type when logging a path (e.g. `"model"`, `"dataset"`). + Defaults to `"unspecified"`. Must not be passed with an + `Artifact`. + aliases (`list[str]`, *optional*): + Aliases to rotate onto the resulting version, alongside `latest` + (assigned automatically whenever a new version is created). Your + aliases rotate onto the version even when identical content is + de-duplicated. + overwrite (`bool`, *optional*): + If `True`, removes every other version of this artifact after the + new content is logged. Blobs still used by other artifacts are + preserved, and version numbers continue increasing. Defaults to + `False`. + + Returns: + The logged `Artifact` instance, hydrated with `version`, `aliases`, + `size`, `manifest`, and `project` set. + """ + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.log_artifact().") + return run.log_artifact( + artifact_or_path, + name=name, + type=type, + aliases=aliases, + overwrite=overwrite, + ) + + +def use_artifact( + artifact_or_name: Artifact | str, + type: str | None = None, +) -> Artifact: + """ + Fetches an artifact and records it as an input to the current run. + + Args: + artifact_or_name (`Artifact` or `str`): + An already-logged `Artifact`, or an artifact name. A bare name + (`"my-model"`) resolves to `:latest`; you can also pin a version + (`"my-model:v3"`) or resolve an alias (`"my-model:prod"`). + type (`str`, *optional*): + If given, checked against the stored artifact type, raising if it + does not match. + + Returns: + The fetched `Artifact`, hydrated and ready to `download()`. + """ + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.use_artifact().") + return run.use_artifact(artifact_or_name, type=type) + + +def log_gpu(run: Run | None = None, device: int | None = None) -> dict: + """ + Log GPU metrics to the current or specified run as system metrics. + Automatically detects whether an NVIDIA or Apple GPU is available and calls + the appropriate logging method. + + Args: + run: Optional Run instance. If None, uses current run from context. + device: CUDA device index to collect metrics from (NVIDIA GPUs only). + If None, collects from all GPUs visible to this process. + This parameter is ignored for Apple GPUs. + + Returns: + dict: The GPU metrics that were logged. + + Example: + ```python + import trackio + + run = trackio.init(project="my-project") + trackio.log({"loss": 0.5}) + trackio.log_gpu() + trackio.log_gpu(device=0) + ``` + """ + if run is None: + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.log_gpu().") + + if gpu_available(): + return _log_nvidia_gpu(run=run, device=device) + elif apple_gpu_available(): + return _log_apple_gpu(run=run) + else: + _emit_nonfatal_warning( + "No GPU detected. Install nvidia-ml-py for NVIDIA GPU support " + "or psutil for Apple Silicon support." + ) + return {} + + +def log_cpu(run: Run | None = None) -> dict: + """ + Log CPU, RAM, disk, network, and sensor metrics to the current or specified run + as system metrics. + + Args: + run: Optional Run instance. If None, uses current run from context. + + Returns: + dict: The CPU and system metrics that were logged. + + Example: + ```python + import trackio + + run = trackio.init(project="my-project") + trackio.log({"loss": 0.5}) + trackio.log_cpu() + ``` + """ + if run is None: + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.log_cpu().") + + return _log_cpu(run=run) + + +def finish(): + """ + Finishes the current run. + """ + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.finish().") + try: + run.finish() + finally: + context_vars.current_run.set(None) + + +def alert( + title: str, + text: str | None = None, + level: AlertLevel = AlertLevel.WARN, + webhook_url: str | None = None, +) -> None: + """ + Fires an alert immediately on the current run. The alert is printed to the + terminal, stored in the database, and displayed in the dashboard. If a + webhook URL is configured (via `trackio.init()`, the `TRACKIO_WEBHOOK_URL` + environment variable, or the `webhook_url` parameter here), the alert is + also POSTed to that URL. + + Args: + title (`str`): + A short title for the alert. + text (`str`, *optional*): + A longer description with details about the alert. + level (`AlertLevel`, *optional*, defaults to `AlertLevel.WARN`): + The severity level. One of `AlertLevel.INFO`, `AlertLevel.WARN`, + or `AlertLevel.ERROR`. + webhook_url (`str`, *optional*): + A webhook URL to send this specific alert to. Overrides any + URL set in `trackio.init()` or the `TRACKIO_WEBHOOK_URL` + environment variable. Supports Slack and Discord webhook + URLs natively. + """ + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.alert().") + run.alert(title=title, text=text, level=level, webhook_url=webhook_url) + + +def delete_project(project: str, force: bool = False) -> bool: + """ + Deletes a project by removing its local SQLite database. + + Args: + project (`str`): + The name of the project to delete. + force (`bool`, *optional*, defaults to `False`): + If `True`, deletes the project without prompting for confirmation. + If `False`, prompts the user to confirm before deleting. + + Returns: + `bool`: `True` if the project was deleted, `False` otherwise. + """ + db_path = SQLiteStorage.get_project_db_path(project) + + if not db_path.exists(): + print(f"* Project '{project}' does not exist.") + return False + + if not force: + response = input( + f"Are you sure you want to delete project '{project}'? " + f"This will permanently delete all runs and metrics. (y/N): " + ) + if response.lower() not in ["y", "yes"]: + print("* Deletion cancelled.") + return False + + try: + db_path.unlink() + + for suffix in ("-wal", "-shm"): + sidecar = Path(str(db_path) + suffix) + if sidecar.exists(): + sidecar.unlink() + + for parquet_path in SQLiteStorage._project_parquet_paths(db_path): + if parquet_path.exists(): + parquet_path.unlink() + + for asset_dir in ( + utils.project_artifacts_dir(project), + utils.project_media_dir(project), + ): + if asset_dir.exists(): + shutil.rmtree(asset_dir, ignore_errors=True) + + print(f"* Project '{project}' has been deleted.") + return True + except Exception as e: + print(f"* Error deleting project '{project}': {e}") + return False + + +def save( + glob_str: str | Path, + project: str | None = None, +) -> str: + """ + Saves files to a project (not linked to a specific run). If Trackio is running + locally, the file(s) will be copied to the project's files directory. If Trackio is + running in a Space, the file(s) will be uploaded to the Space's files directory. + + Args: + glob_str (`str` or `Path`): + The file path or glob pattern to save. Can be a single file or a pattern + matching multiple files (e.g., `"*.py"`, `"models/**/*.pth"`). + project (`str`, *optional*): + The name of the project to save files to. If not provided, uses the current + project from `trackio.init()`. If no project is initialized, raises an + error. + + Returns: + `str`: The path where the file(s) were saved (project's files directory). + + Example: + ```python + import trackio + + trackio.init(project="my-project") + trackio.save("config.yaml") + trackio.save("models/*.pth") + ``` + """ + if project is None: + project = context_vars.current_project.get() + if project is None: + raise RuntimeError( + "No project specified. Either call trackio.init() first or provide a " + "project parameter to trackio.save()." + ) + + glob_str = Path(glob_str) + base_path = Path.cwd().resolve() + + matched_files = [] + if glob_str.is_file(): + matched_files = [glob_str.resolve()] + else: + pattern = str(glob_str) + if not glob_str.is_absolute(): + pattern = str((Path.cwd() / glob_str).resolve()) + matched_files = [ + Path(f).resolve() + for f in glob.glob(pattern, recursive=True) + if Path(f).is_file() + ] + + if not matched_files: + raise ValueError(f"No files found matching pattern: {glob_str}") + + current_run = context_vars.current_run.get() + is_local = ( + current_run._is_local + if current_run is not None + else ( + context_vars.current_space_id.get() is None + and context_vars.current_server.get() is None + ) + ) + + if is_local: + for file_path in matched_files: + try: + relative_to_base = file_path.relative_to(base_path) + except ValueError: + relative_to_base = Path(file_path.name) + + if current_run is not None: + current_run._queue_upload( + file_path, + step=None, + relative_path=str(relative_to_base.parent), + use_run_name=False, + ) + else: + media_path = get_project_media_path( + project=project, + run=None, + step=None, + relative_path=str(relative_to_base), + ) + shutil.copy(str(file_path), str(media_path)) + else: + url = context_vars.current_server.get() + + upload_entries = [] + for file_path in matched_files: + try: + relative_to_base = file_path.relative_to(base_path) + except ValueError: + relative_to_base = Path(file_path.name) + + if current_run is not None: + current_run._queue_upload( + file_path, + step=None, + relative_path=str(relative_to_base.parent), + use_run_name=False, + ) + else: + upload_entry: UploadEntry = { + "project": project, + "run": None, + "step": None, + "relative_path": str(relative_to_base), + "uploaded_file": handle_file(file_path), + } + upload_entries.append(upload_entry) + + if upload_entries: + if url is None: + raise RuntimeError( + "No server available. Call trackio.init() before trackio.save() to start the server." + ) + + try: + wt = context_vars.current_server_write_token.get() + if wt is not None: + client = RemoteClient( + url, + hf_token=None, + write_token=wt, + httpx_kwargs={"timeout": 90}, + ) + else: + client = RemoteClient( + url, + hf_token=huggingface_hub.utils.get_token(), + httpx_kwargs={"timeout": 90}, + ) + client.predict( + api_name="/bulk_upload_media", + uploads=upload_entries, + hf_token=huggingface_hub.utils.get_token() if wt is None else None, + ) + except Exception as e: + _emit_nonfatal_warning( + f"Failed to upload files: {e}. " + "Files may not be available in the dashboard." + ) + + return str(utils.project_media_dir(project) / "files") + + +def show( + project: str | None = None, + *, + dashboard: str | None = None, + theme: Any = None, + mcp_server: bool | None = None, + footer: bool = True, + color_palette: list[str] | None = None, + open_browser: bool = True, + block_thread: bool | None = None, + host: str | None = None, + share: bool | None = None, + server_port: int | None = None, + frontend_dir: str | Path | None = None, +): + """ + Launches the Trackio dashboard. + + Args: + project (`str`, *optional*): + The name of the project whose runs to show. If not provided, all projects + will be shown and the user can select one. + dashboard (`str`, *optional*): + Set to `"registry"` to show the standalone registry dashboard. The + default is the project dashboard. + theme (`Any`, *optional*): + Ignored. Kept for backward compatibility; Trackio no longer uses Gradio themes. + mcp_server (`bool`, *optional*): + If `True`, the dashboard exposes an MCP server at `/mcp` when the optional + `trackio[mcp]` dependency is installed. If `None` (default), the + `GRADIO_MCP_SERVER` environment variable is used (e.g. on Spaces). + footer (`bool`, *optional*, defaults to `True`): + Whether to include `footer=false` in the write-token URL when `False`. + This can also be controlled via the `footer` query parameter in the URL. + color_palette (`list[str]`, *optional*): + A list of hex color codes to use for plot lines. If not provided, the + `TRACKIO_COLOR_PALETTE` environment variable will be used (comma-separated + hex codes), or if that is not set, the default color palette will be used. + Example: `['#FF0000', '#00FF00', '#0000FF']` + open_browser (`bool`, *optional*, defaults to `True`): + If `True` and not in a notebook, a new browser tab will be opened with the + dashboard. If `False`, the browser will not be opened. + block_thread (`bool`, *optional*): + If `True`, the main thread will be blocked until the dashboard is closed. + If `None` (default behavior), then the main thread will not be blocked if the + dashboard is launched in a notebook, otherwise the main thread will be blocked. + host (`str`, *optional*): + The host to bind the server to. If not provided, defaults to `'127.0.0.1'` + (localhost only). Set to `'0.0.0.0'` to allow remote access. + share (`bool`, *optional*): + If `True`, creates a temporary public URL (Gradio-compatible tunnel). On Colab + or hosted notebooks, defaults to `True` unless overridden. + server_port (`int`, *optional*): + Port to bind. If not set, scans from `GRADIO_SERVER_PORT` (default 7860). + frontend_dir (`str | Path`, *optional*): + Directory containing a custom static frontend. Must contain `index.html`. + If not provided, Trackio checks `TRACKIO_FRONTEND_DIR`, then the persistent + Trackio config, then the bundled frontend. If an explicit `frontend_dir` + points to a missing or empty directory, Trackio copies in the starter + template and serves that directory. + + Returns: + `app`: The dashboard handle (`.close()` stops the server). + `url`: The local URL of the dashboard. + `share_url`: The public share URL, if any. + `full_url`: The full URL including the write token (share URL when sharing, else local). + """ + if dashboard not in (None, "registry"): + raise ValueError("dashboard must be None or 'registry'.") + if dashboard == "registry" and project is not None: + raise ValueError("The registry dashboard does not accept a project.") + + if theme is not None and theme != "default": + warnings.warn( + "The theme argument is ignored; Trackio no longer depends on Gradio themes.", + UserWarning, + stacklevel=2, + ) + + if color_palette is not None: + os.environ["TRACKIO_COLOR_PALETTE"] = ",".join(color_palette) + + _mcp_server = ( + mcp_server + if mcp_server is not None + else os.environ.get("GRADIO_MCP_SERVER", "False") == "True" + ) + + resolved_frontend = resolve_frontend_dir(frontend_dir, announce=True) + starlette_app, wt = build_starlette_app_only( + mcp_server=_mcp_server, + frontend_dir=str(resolved_frontend.path), + ) + local_url, share_url, _local_api_url, uv_server = launch_trackio_dashboard( + starlette_app, + server_name=host, + server_port=server_port, + share=share, + mcp_server=_mcp_server, + quiet=True, + ) + server = TrackioDashboardApp(starlette_app, uv_server, wt) + + base_root = (share_url or local_url).rstrip("/") + base_url = base_root + "/" + dashboard_url = base_url if dashboard is None else f"{base_root}/registry" + if dashboard is None and project: + dashboard_url += f"?project={project}" + full_url_base = base_root if dashboard is None else f"{base_root}/registry" + full_url = utils.get_full_url( + full_url_base, + project=project, + write_token=wt, + footer=footer, + ) + + if not utils.is_in_notebook(): + label = "Trackio registry UI" if dashboard == "registry" else "Trackio UI" + print(f"\033[1m\033[38;5;208m* {label} launched at: {dashboard_url}\033[0m") + utils.print_write_token_instructions(full_url) + if open_browser: + webbrowser.open(full_url) + block_thread = block_thread if block_thread is not None else True + else: + utils.embed_url_in_notebook(full_url) + block_thread = block_thread if block_thread is not None else False + + if block_thread: + utils.block_main_thread_until_keyboard_interrupt() + return _TupleNoPrint((server, local_url, share_url, full_url)) diff --git a/trackio/_unsupported.py b/trackio/_unsupported.py new file mode 100644 index 0000000000000000000000000000000000000000..d4f024ca44de5f341897f2f5aea7c522a1b8902f --- /dev/null +++ b/trackio/_unsupported.py @@ -0,0 +1,48 @@ +"""Placeholders for W&B feature classes that Trackio does not yet implement.""" + +from typing import Any + + +class _UnsupportedFeature: + def __init__(self, *args: Any, **kwargs: Any) -> None: + raise NotImplementedError( + f"trackio.{type(self).__name__} is not implemented yet. " + "If you need this feature, please open a GitHub issue in the Trackio " + "repository: https://github.com/gradio-app/trackio/issues/new" + ) + + +class ArtifactTTL(_UnsupportedFeature): + """Placeholder for artifact time-to-live policies.""" + + +class Classes(_UnsupportedFeature): + """Placeholder for class labels used in media annotations.""" + + +class Config(_UnsupportedFeature): + """Placeholder for the W&B configuration container.""" + + +class EvalTable(_UnsupportedFeature): + """Placeholder for evaluation tables.""" + + +class Graph(_UnsupportedFeature): + """Placeholder for model graphs.""" + + +class JoinedTable(_UnsupportedFeature): + """Placeholder for joining tables by a shared key.""" + + +class Molecule(_UnsupportedFeature): + """Placeholder for molecular structure visualizations.""" + + +class Plotly(_UnsupportedFeature): + """Placeholder for Plotly visualizations.""" + + +class Settings(_UnsupportedFeature): + """Placeholder for W&B SDK settings.""" diff --git a/trackio/_vendor/__init__.py b/trackio/_vendor/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/trackio/_vendor/gradio_exceptions.py b/trackio/_vendor/gradio_exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..34696b9e534896b66557bab78a2d78709c07e351 --- /dev/null +++ b/trackio/_vendor/gradio_exceptions.py @@ -0,0 +1,6 @@ +class ChecksumMismatchError(Exception): + pass + + +class ShareCertificateWriteError(Exception): + pass diff --git a/trackio/_vendor/networking.py b/trackio/_vendor/networking.py new file mode 100644 index 0000000000000000000000000000000000000000..04ef35763f89eb813059ea6fc91a7316ecd2f2de --- /dev/null +++ b/trackio/_vendor/networking.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import os +import time +import warnings +from pathlib import Path +from urllib.parse import urlparse, urlunparse + +import httpx + +from trackio._vendor.gradio_exceptions import ShareCertificateWriteError +from trackio._vendor.tunneling import CERTIFICATE_PATH, Tunnel + +GRADIO_API_SERVER = "https://api.gradio.app/v3/tunnel-request" +GRADIO_SHARE_SERVER_ADDRESS = os.getenv("GRADIO_SHARE_SERVER_ADDRESS") + + +def setup_tunnel( + local_host: str, + local_port: int, + share_token: str, + share_server_address: str | None, + share_server_tls_certificate: str | None, +) -> str: + share_server_address = ( + GRADIO_SHARE_SERVER_ADDRESS + if share_server_address is None + else share_server_address + ) + if share_server_address is None: + try: + response = httpx.get(GRADIO_API_SERVER, timeout=30) + payload = response.json()[0] + remote_host, remote_port = payload["host"], int(payload["port"]) + certificate = payload["root_ca"] + except Exception as e: + raise RuntimeError( + "Could not get share link from Gradio API Server." + ) from e + try: + Path(CERTIFICATE_PATH).parent.mkdir(parents=True, exist_ok=True) + with open(CERTIFICATE_PATH, "w") as f: + f.write(certificate) + except Exception as e: + raise ShareCertificateWriteError( + f"{e}. This can happen if the current working directory is read-only." + ) from e + share_server_tls_certificate = CERTIFICATE_PATH + + else: + remote_host, remote_port = share_server_address.split(":") + remote_port = int(remote_port) + tunnel = Tunnel( + remote_host, + remote_port, + local_host, + local_port, + share_token, + share_server_tls_certificate, + ) + address = tunnel.start_tunnel() + return address + + +def url_ok(url: str) -> bool: + try: + for _ in range(5): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + r = httpx.head(url, timeout=3, verify=False) + if r.status_code in (200, 401, 302, 303, 307): + return True + time.sleep(0.500) + except (ConnectionError, httpx.ConnectError, httpx.TimeoutException): + return False + return False + + +def normalize_share_url(share_url: str, share_server_protocol: str) -> str: + parsed_url = urlparse(share_url) + return urlunparse((share_server_protocol,) + parsed_url[1:]) diff --git a/trackio/_vendor/tunneling.py b/trackio/_vendor/tunneling.py new file mode 100644 index 0000000000000000000000000000000000000000..6bd6258ed043ba70b07bb1e928bf009864206000 --- /dev/null +++ b/trackio/_vendor/tunneling.py @@ -0,0 +1,191 @@ +import atexit +import hashlib +import os +import platform +import re +import stat +import subprocess +import sys +import time +from pathlib import Path + +import httpx +from huggingface_hub.constants import HF_HOME + +from trackio._vendor.gradio_exceptions import ChecksumMismatchError + +VERSION = "0.3" +CURRENT_TUNNELS: list["Tunnel"] = [] + +machine = platform.machine() +if machine == "x86_64": + machine = "amd64" +elif machine == "aarch64": + machine = "arm64" + +BINARY_REMOTE_NAME = f"frpc_{platform.system().lower()}_{machine.lower()}" +EXTENSION = ".exe" if os.name == "nt" else "" +BINARY_URL = f"https://cdn-media.huggingface.co/frpc-gradio-{VERSION}/{BINARY_REMOTE_NAME}{EXTENSION}" + +CHECKSUMS = { + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_windows_amd64.exe": "14bc0ea470be5d67d79a07412bd21de8a0a179c6ac1116d7764f68e942dc9ceb", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_linux_amd64": "c791d1f047b41ff5885772fc4bf20b797c6059bbd82abb9e31de15e55d6a57c4", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_linux_arm64": "823ced25104de6dc3c9f4798dbb43f20e681207279e6ab89c40e2176ccbf70cd", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_darwin_amd64": "930f8face3365810ce16689da81b7d1941fda4466225a7bbcbced9a2916a6e15", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_darwin_arm64": "dfac50c690aca459ed5158fad8bfbe99f9282baf4166cf7c410a6673fbc1f327", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_linux_arm": "4b563beb2e36c448cc688174e20b53af38dc1ff2b5e362d4ddd1401f2affbfb7", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_freebsd_386": "cb0a56c764ecf96dd54ed601d240c564f060ee4e58202d65ffca17c1a51ce19c", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_freebsd_amd64": "516d9e6903513869a011ddcd1ec206167ad1eb5dd6640d21057acc258edecbbb", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_linux_386": "4c2f2a48cd71571498c0ac8a4d42a055f22cb7f14b4b5a2b0d584220fd60a283", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_linux_mips": "b309ecd594d4f0f7f33e556a80d4b67aef9319c00a8334648a618e56b23cb9e0", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_linux_mips64": "0372ef5505baa6f3b64c6295a86541b24b7b0dbe4ef28b344992e21f47624b7b", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_linux_riscv64": "1658eed7e8c14ea76e1d95749d58441ce24147c3d559381832c725c29cfc3df3", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_linux_mipsle": "a2aaba16961d3372b79bd7a28976fcd0f0bbaebc2b50d5a7a71af2240747960f", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_windows_386.exe": "721b90550195a83e15f2176d8f85a48d5a25822757cb872e9723d4bccc4e5bb6", + "https://cdn-media.huggingface.co/frpc-gradio-0.3/frpc_linux_mips64le": "796481edd609f31962b45cc0ab4c9798d040205ae3bf354ed1b72fb432d796b8", +} + +CHUNK_SIZE = 128 + +BINARY_FILENAME = f"{BINARY_REMOTE_NAME}_v{VERSION}" +BINARY_FOLDER = Path(HF_HOME) / "trackio" / "frpc" +BINARY_PATH = str(BINARY_FOLDER / BINARY_FILENAME) + +TUNNEL_TIMEOUT_SECONDS = 30 +TUNNEL_ERROR_MESSAGE = ( + "Could not create share URL. " + "Please check the appended log from frpc for more information:" +) + +CERTIFICATE_PATH = ".trackio/certificate.pem" + + +class Tunnel: + def __init__( + self, + remote_host: str, + remote_port: int, + local_host: str, + local_port: int, + share_token: str, + share_server_tls_certificate: str | None, + ): + self.proc = None + self.url = None + self.remote_host = remote_host + self.remote_port = remote_port + self.local_host = local_host + self.local_port = local_port + self.share_token = share_token + self.share_server_tls_certificate = share_server_tls_certificate + + @staticmethod + def download_binary(): + if not Path(BINARY_PATH).exists(): + Path(BINARY_FOLDER).mkdir(parents=True, exist_ok=True) + resp = httpx.get(BINARY_URL, timeout=30) + + if resp.status_code == 403: + raise OSError( + f"Cannot set up a share link as this platform is incompatible. Please " + f"create a GitHub issue with information about your platform: {platform.uname()}" + ) + + resp.raise_for_status() + + with open(BINARY_PATH, "wb") as file: + file.write(resp.content) + st = os.stat(BINARY_PATH) + os.chmod(BINARY_PATH, st.st_mode | stat.S_IEXEC) + + if BINARY_URL in CHECKSUMS: + sha = hashlib.sha256() + with open(BINARY_PATH, "rb") as f: + for chunk in iter(lambda: f.read(CHUNK_SIZE * sha.block_size), b""): + sha.update(chunk) + calculated_hash = sha.hexdigest() + + if calculated_hash != CHECKSUMS[BINARY_URL]: + raise ChecksumMismatchError() + + def start_tunnel(self) -> str: + self.download_binary() + self.url = self._start_tunnel(BINARY_PATH) + return self.url + + def kill(self): + if self.proc is not None: + print(f"Killing tunnel {self.local_host}:{self.local_port} <> {self.url}") + self.proc.terminate() + self.proc = None + + def _start_tunnel(self, binary: str) -> str: + CURRENT_TUNNELS.append(self) + command = [ + binary, + "http", + "-n", + self.share_token, + "-l", + str(self.local_port), + "-i", + self.local_host, + "--uc", + "--sd", + "random", + "--ue", + "--server_addr", + f"{self.remote_host}:{self.remote_port}", + "--disable_log_color", + ] + if self.share_server_tls_certificate is not None: + command.extend( + [ + "--tls_enable", + "--tls_trusted_ca_file", + self.share_server_tls_certificate, + ] + ) + self.proc = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + atexit.register(self.kill) + return self._read_url_from_tunnel_stream() + + def _read_url_from_tunnel_stream(self) -> str: + start_timestamp = time.time() + + log = [] + url = "" + + def _raise_tunnel_error(): + log_text = "\n".join(log) + print(log_text, file=sys.stderr) + raise ValueError(f"{TUNNEL_ERROR_MESSAGE}\n{log_text}") + + while url == "": + if time.time() - start_timestamp >= TUNNEL_TIMEOUT_SECONDS: + _raise_tunnel_error() + + assert self.proc is not None # noqa: S101 + if self.proc.stdout is None: + continue + + line = self.proc.stdout.readline() + line = line.decode("utf-8") + + if line == "": + continue + + log.append(line.strip()) + + if "start proxy success" in line: + result = re.search("start proxy success: (.+)\n", line) + if result is None: + _raise_tunnel_error() + else: + url = result.group(1) + elif "login to server failed" in line: + _raise_tunnel_error() + + return url diff --git a/trackio/agent_sessions.py b/trackio/agent_sessions.py new file mode 100644 index 0000000000000000000000000000000000000000..3d0197f379ef0e9d05c5dda1abc5235c0e100dc0 --- /dev/null +++ b/trackio/agent_sessions.py @@ -0,0 +1,533 @@ +"""Agent session serialization for the Hugging Face Hub's trace viewer. + +The Hub renders `.jsonl` agent sessions in a dedicated trace viewer, for both +Datasets and Storage Buckets. This module is the single place Trackio produces +that format: the logbook's attached agent sessions and the `traces` table both +serialize through it. + +Trackio emits **Pi's session format** (version 3), which the Hub supports +natively. The Hub also documents a Session Trace Simple Format (STS), but as of +this writing files written to that spec are not rendered by the viewer — a +controlled test (same bucket, same folder, one variable) showed a Codex-native +and a Pi-format session rendering while a spec-compliant STS session displayed +as raw JSONL, and none of the public datasets tagged `format:agent-traces` use +STS. Pi's format is also the better fit: `id`/`parentId`, token `usage`, `cost`, +and `isError` all have native homes, where STS had none. + +A session file is a header line followed by one entry per line: + + {"type":"session","version":3,"id":"...","timestamp":"...","cwd":"..."} + {"type":"message","id":"...","parentId":null,"timestamp":"...", + "message":{"role":"user","content":[{"type":"text","text":"..."}]}} + +Entries chain through `parentId`. Message content is a list of typed blocks +(`text`, `thinking`, `toolCall`); tool results are their own message with +`role: "toolResult"`, a `toolCallId`, a `toolName`, and an optional `isError`. + +Format reference: +https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/session-format.md +""" + +from __future__ import annotations + +import hashlib +import json +import re +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +HARNESS = "trackio" +SESSION_VERSION = 3 +SESSION_NAME_MAX_CHARS = 120 + +# Formats the Hub renders without conversion. +HUB_NATIVE_PROVIDERS = {"Codex", "Claude Code", "Pi"} + +# Pi roles. Trackio's OpenAI-style `system` maps onto `developer`, which is what +# the sessions that render in the viewer use. +ROLE_BY_OPENAI_ROLE = { + "system": "developer", + "developer": "developer", + "user": "user", + "assistant": "assistant", +} + +_NUMERIC_RE = re.compile(r"\d+(?:\.\d+)?") +_UNSAFE_ID_RE = re.compile(r"[^a-zA-Z0-9._-]+") + + +def parse_time(value: str | int | float | None) -> datetime | None: + """Parse an ISO-8601 string or an epoch number (seconds or milliseconds).""" + if value in (None, ""): + return None + if isinstance(value, (int, float)) or ( + isinstance(value, str) and _NUMERIC_RE.fullmatch(value) + ): + numeric = float(value) + if numeric > 10_000_000_000: + numeric /= 1000 + try: + return datetime.fromtimestamp(numeric, tz=timezone.utc) + except (OverflowError, OSError, ValueError): + return None + try: + return datetime.fromisoformat(str(value).replace("Z", "+00:00")) + except ValueError: + return None + + +def iso_z(value: str | int | float | None) -> str | None: + """An ISO-8601 UTC timestamp with the `Z` suffix Pi sessions use.""" + parsed = parse_time(value) + if parsed is None: + return None + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + return ( + parsed.astimezone(timezone.utc) + .isoformat(timespec="milliseconds") + .replace("+00:00", "Z") + ) + + +def entry_id(*parts: Any) -> str: + """A stable 8-character entry id, matching the width Pi sessions use.""" + seed = ":".join(str(part) for part in parts) + return hashlib.sha1(seed.encode("utf-8")).hexdigest()[:8] + + +def safe_id(value: str) -> str: + """A filesystem- and bucket-safe form of an identifier.""" + cleaned = _UNSAFE_ID_RE.sub("-", str(value)).strip("-.") + return cleaned[:100] or f"session-{uuid.uuid4().hex[:12]}" + + +def is_hub_native_jsonl(path: Path, provider: str | None = None) -> bool: + """Whether the Hub's trace viewer renders this file without conversion. + + Only the harness-native formats qualify. A session header carrying a + `harness` but no `version` is STS, which the viewer does not render, so it + still needs converting. + """ + if path.suffix.lower() != ".jsonl": + return False + if provider in HUB_NATIVE_PROVIDERS: + return True + try: + with path.open(encoding="utf-8") as handle: + first_line = next((line for line in handle if line.strip()), "") + header = json.loads(first_line) + except (OSError, json.JSONDecodeError): + return False + if not isinstance(header, dict): + return False + if header.get("type") == "session" and header.get("version") is not None: + return True + # Codex sessions open with a `session_meta` envelope rather than `session`. + return header.get("type") == "session_meta" and isinstance( + header.get("payload"), dict + ) + + +def session_header( + session_id: str, + timestamp: str | None = None, + name: str | None = None, + cwd: str | None = None, + **extra: Any, +) -> dict[str, Any]: + header: dict[str, Any] = { + "type": "session", + "version": SESSION_VERSION, + "id": str(session_id), + "timestamp": iso_z(timestamp) or iso_z(datetime.now(timezone.utc)), + "cwd": cwd or "/", + "harness": HARNESS, + } + if name: + header["name"] = name + header.update({key: value for key, value in extra.items() if value is not None}) + return header + + +def dumps(records: list[dict[str, Any]]) -> str: + return ( + "\n".join(json.dumps(record, ensure_ascii=False) for record in records) + "\n" + ) + + +def write_session(path: Path, records: list[dict[str, Any]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(dumps(records), encoding="utf-8") + + +def content_text(value: Any) -> str: + """Flatten an OpenAI-style `content` value to text. + + Content may be a plain string, a list of typed blocks, or a serialized + Trackio media object. Non-text blocks keep a readable placeholder rather + than being dropped, so the viewer shows that something was there. + """ + if value is None: + return "" + if isinstance(value, str): + return value + if isinstance(value, list): + parts = [content_text(item) for item in value] + return "\n".join(part for part in parts if part) + if isinstance(value, dict): + media_type = value.get("_type") + if isinstance(media_type, str) and media_type.startswith("trackio."): + label = media_type.split(".", 1)[1] + caption = value.get("caption") or value.get("file_path") or "" + return f"[{label}{': ' + str(caption) if caption else ''}]" + for key in ("text", "content", "output", "value"): + if isinstance(value.get(key), str): + return value[key] + return json.dumps(value, ensure_ascii=False) + return str(value) + + +def text_block(text: str) -> dict[str, Any]: + return {"type": "text", "text": text} + + +def thinking_block(text: str) -> dict[str, Any]: + return {"type": "thinking", "thinking": text} + + +def tool_call_block(call_id: str, name: str, arguments: Any) -> dict[str, Any]: + """A `toolCall` block. Unlike STS, `arguments` stays a real object.""" + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError: + arguments = {"input": arguments} + if not isinstance(arguments, dict): + arguments = {} if arguments is None else {"input": arguments} + return { + "type": "toolCall", + "id": str(call_id), + "name": str(name or "tool"), + "arguments": arguments, + } + + +class _Chain: + """Accumulates entries, chaining each to the one before via `parentId`.""" + + def __init__(self, session_id: str, default_timestamp: str | None): + self.session_id = session_id + self.default_timestamp = default_timestamp + self.records: list[dict[str, Any]] = [] + self._parent: str | None = None + + def add(self, entry: dict[str, Any], timestamp: str | None = None) -> str: + entry["parentId"] = self._parent + entry["timestamp"] = iso_z(timestamp) or self.default_timestamp + self._parent = entry["id"] + self.records.append(entry) + return entry["id"] + + def message( + self, index: Any, message: dict[str, Any], timestamp: str | None = None + ) -> str: + return self.add( + { + "type": "message", + "id": entry_id(self.session_id, "msg", index), + "message": message, + }, + timestamp, + ) + + +def span_usage(span: dict[str, Any]) -> dict[str, Any] | None: + """Pi's `usage` block from a Trackio span's `usage` and `cost_usd`.""" + usage = span.get("usage") or {} + raw_input = usage.get("input_tokens", usage.get("prompt_tokens")) + raw_output = usage.get("output_tokens", usage.get("completion_tokens")) + cost = span.get("cost_usd") + if raw_input is None and raw_output is None and cost is None: + return None + try: + input_tokens = int(raw_input or 0) + output_tokens = int(raw_output or 0) + except (TypeError, ValueError): + input_tokens = output_tokens = 0 + total = usage.get("total_tokens") or (input_tokens + output_tokens) + block: dict[str, Any] = { + "input": input_tokens, + "output": output_tokens, + "cacheRead": int(usage.get("cache_read_input_tokens") or 0), + "cacheWrite": int(usage.get("cache_creation_input_tokens") or 0), + "totalTokens": total, + } + if cost is not None: + try: + block["cost"] = { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0, + "total": float(cost), + } + except (TypeError, ValueError): + pass + return block + + +def _span_is_error(span: dict[str, Any]) -> bool: + status = str(span.get("status") or "").lower() + return status in {"error", "failed"} or bool(span.get("error")) + + +def _message_is_error(message: dict[str, Any]) -> bool: + if message.get("is_error") is True or message.get("isError") is True: + return True + if message.get("error"): + return True + return str(message.get("status") or "").lower() in {"error", "failed"} + + +def _generation_output_text(span: dict[str, Any]) -> str: + output = span.get("output") + if isinstance(output, dict): + for key in ("text", "content", "message", "output"): + if output.get(key) is not None: + return content_text(output[key]) + return content_text(output) + + +def _span_sort_key(indexed_span: tuple[int, dict[str, Any]]) -> tuple[float, int]: + index, span = indexed_span + started = parse_time(span.get("start_time") or span.get("startTime")) + return (started.timestamp() if started else float("inf"), index) + + +def _ordered_spans(spans: list[dict[str, Any]], kind: str) -> list[dict[str, Any]]: + indexed = [ + (index, span) + for index, span in enumerate(spans) + if str(span.get("kind") or "").lower() == kind + ] + return [span for _, span in sorted(indexed, key=_span_sort_key)] + + +def _session_name(records: list[dict[str, Any]], fallback: str) -> str: + """Title for the session: the request, when there is one.""" + for wanted_role in ("user", None): + for record in records: + message = record.get("message") or {} + role = str(message.get("role") or "") + if wanted_role is not None and role != wanted_role: + continue + text = " ".join(content_text(message.get("content")).split()) + if text: + return text[:SESSION_NAME_MAX_CHARS] + return fallback + + +def _messages_to_entries( + chain: _Chain, + messages: list[dict[str, Any]], + generations: list[dict[str, Any]], + tool_spans: list[dict[str, Any]], +) -> None: + """Emit message entries, enriched with what the spans know. + + Spans supply what the message layer cannot: token usage and cost on the + generation that produced each assistant turn, and failure status on a tool + result. Tool call ids and span ids come from different namespaces, so a + result is matched to its span by call id first and by order second. + """ + tool_names: dict[str, str] = {} + tool_span_by_id = {str(span.get("id")): span for span in tool_spans} + remaining_tool_spans = list(tool_spans) + generation_index = 0 + + for index, message in enumerate(messages): + role = str(message.get("role") or "assistant").lower() + timestamp = message.get("timestamp") or message.get("created_at") + + if role == "tool": + call_id = str( + message.get("tool_call_id") + or message.get("toolCallId") + or f"call-{index}" + ) + span = tool_span_by_id.get(call_id) + if span is None and remaining_tool_spans: + span = remaining_tool_spans[0] + if span is not None and span in remaining_tool_spans: + remaining_tool_spans.remove(span) + result: dict[str, Any] = { + "role": "toolResult", + "toolCallId": call_id, + "toolName": tool_names.get(call_id) + or str((span or {}).get("name") or "tool"), + "content": [text_block(content_text(message.get("content")))], + } + if _message_is_error(message) or ( + span is not None and _span_is_error(span) + ): + result["isError"] = True + chain.message(index, result, timestamp) + continue + + blocks: list[dict[str, Any]] = [] + reasoning = ( + message.get("reasoning_content") + or message.get("reasoningContent") + or message.get("reasoning") + ) + if reasoning: + blocks.append(thinking_block(content_text(reasoning))) + text = content_text(message.get("content")) + if text: + blocks.append(text_block(text)) + + calls = list(message.get("tool_calls") or message.get("toolCalls") or []) + if message.get("function_call"): + calls.append({"function": message["function_call"]}) + for call_index, call in enumerate(calls): + if not isinstance(call, dict): + continue + function = ( + call.get("function") if isinstance(call.get("function"), dict) else call + ) + call_id = str(call.get("id") or f"call-{index}-{call_index}") + name = str(function.get("name") or "tool") + tool_names[call_id] = name + blocks.append(tool_call_block(call_id, name, function.get("arguments"))) + + entry_message: dict[str, Any] = { + "role": ROLE_BY_OPENAI_ROLE.get(role, "assistant"), + "content": blocks, + } + if entry_message["role"] == "assistant": + # Pair the Nth assistant turn with the Nth generation span rather + # than repeating one span's totals on every turn, which would make + # the viewer double-count tokens and cost. + if generation_index < len(generations): + usage = span_usage(generations[generation_index]) + if usage: + entry_message["usage"] = usage + generation_index += 1 + chain.message(index, entry_message, timestamp) + + +def _spans_to_entries( + chain: _Chain, spans: list[dict[str, Any]], generations: list[dict[str, Any]] +) -> None: + """Emit entries for a trace that carries spans but no messages.""" + ordered = sorted( + ((index, span) for index, span in enumerate(spans) if isinstance(span, dict)), + key=_span_sort_key, + ) + for index, span in ordered: + kind = str(span.get("kind") or "span").lower() + timestamp = span.get("start_time") or span.get("startTime") + if kind == "generation": + message: dict[str, Any] = { + "role": "assistant", + "content": [text_block(_generation_output_text(span))], + } + usage = span_usage(span) + if usage: + message["usage"] = usage + chain.message(f"span-{index}", message, timestamp) + elif kind == "tool": + call_id = str(span.get("id") or f"span-tool-{index}") + name = str(span.get("name") or "tool") + chain.message( + f"span-{index}-call", + { + "role": "assistant", + "content": [tool_call_block(call_id, name, span.get("input"))], + }, + timestamp, + ) + result: dict[str, Any] = { + "role": "toolResult", + "toolCallId": call_id, + "toolName": name, + "content": [ + text_block(content_text(span.get("error") or span.get("output"))) + ], + } + if _span_is_error(span): + result["isError"] = True + chain.message(f"span-{index}-result", result, timestamp) + else: + chain.message( + f"span-{index}", + { + "role": "developer", + "content": [text_block(str(span.get("name") or kind))], + }, + timestamp, + ) + + +def trace_to_records( + trace: dict[str, Any], project: str | None = None +) -> list[dict[str, Any]]: + """Serialize one Trackio trace row into a Pi-format agent session. + + The entries form the linear conversation the viewer renders, enriched from + the spans with model, token usage, cost, and failure status. The full span + tree — hierarchy, `duration_ms`, per-span `metadata`, and the `kind` of + non-tool spans — stays in SQLite, which remains authoritative for every + Trackio reader. + """ + messages = [m for m in (trace.get("messages") or []) if isinstance(m, dict)] + spans = [s for s in (trace.get("spans") or []) if isinstance(s, dict)] + generations = _ordered_spans(spans, "generation") + tool_spans = _ordered_spans(spans, "tool") + + trace_id = str(trace.get("id") or uuid.uuid4().hex) + timestamp = iso_z(trace.get("timestamp")) + chain = _Chain(trace_id, timestamp) + + model = next( + (str(span["model"]) for span in generations if span.get("model")), + None, + ) + if model: + chain.add( + { + "type": "model_change", + "id": entry_id(trace_id, "model"), + "modelId": model, + }, + trace.get("timestamp"), + ) + + if messages: + _messages_to_entries(chain, messages, generations, tool_spans) + else: + _spans_to_entries(chain, spans, generations) + + header = session_header( + trace_id, + timestamp=trace.get("timestamp"), + name=_session_name(chain.records, trace_id), + cwd=f"/{project}" if project else "/", + trackio={ + "project": project, + "run_id": trace.get("run_id"), + "run_name": trace.get("run_name"), + "step": trace.get("step"), + "key": trace.get("key"), + "trace_index": trace.get("trace_index"), + "metadata": trace.get("metadata") or {}, + }, + ) + return [header, *chain.records] + + +def trace_session_filename(trace_id: Any) -> str: + return f"{safe_id(str(trace_id))}.jsonl" diff --git a/trackio/alerts.py b/trackio/alerts.py new file mode 100644 index 0000000000000000000000000000000000000000..646b9786eacb91fca5270189a78280293e045fc0 --- /dev/null +++ b/trackio/alerts.py @@ -0,0 +1,184 @@ +import json +import logging +import ssl +import urllib.request +from enum import Enum + +try: + import certifi + + _SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where()) +except ImportError: + _SSL_CONTEXT = None + +logger = logging.getLogger(__name__) + + +class AlertLevel(str, Enum): + INFO = "info" + WARN = "warn" + ERROR = "error" + + +ALERT_LEVEL_ORDER = { + AlertLevel.INFO: 0, + AlertLevel.WARN: 1, + AlertLevel.ERROR: 2, +} + +ALERT_COLORS = { + AlertLevel.INFO: "\033[94m", + AlertLevel.WARN: "\033[93m", + AlertLevel.ERROR: "\033[91m", +} +RESET_COLOR = "\033[0m" + +LEVEL_EMOJI = { + AlertLevel.INFO: "ℹ️", + AlertLevel.WARN: "⚠️", + AlertLevel.ERROR: "🚨", +} + + +def format_alert_terminal( + level: AlertLevel, title: str, text: str | None, step: int | None +) -> str: + color = ALERT_COLORS.get(level, "") + step_str = f" (step {step})" if step is not None else "" + if text: + return f"{color}[TRACKIO {level.value.upper()}]{RESET_COLOR} {title}: {text}{step_str}" + return f"{color}[TRACKIO {level.value.upper()}]{RESET_COLOR} {title}{step_str}" + + +def _is_slack_url(url: str) -> bool: + return "hooks.slack.com" in url + + +def _is_discord_url(url: str) -> bool: + return "discord.com/api/webhooks" in url or "discordapp.com/api/webhooks" in url + + +def _build_slack_payload( + level: AlertLevel, + title: str, + text: str | None, + project: str, + run: str, + step: int | None, +) -> dict: + emoji = LEVEL_EMOJI.get(level, "") + step_str = f" • Step {step}" if step is not None else "" + header = f"{emoji} *[{level.value.upper()}] {title}*" + context = f"Project: {project} • Run: {run}{step_str}" + blocks = [ + {"type": "section", "text": {"type": "mrkdwn", "text": header}}, + ] + if text: + blocks.append({"type": "section", "text": {"type": "mrkdwn", "text": text}}) + blocks.append( + {"type": "context", "elements": [{"type": "mrkdwn", "text": context}]} + ) + return {"blocks": blocks} + + +def _build_discord_payload( + level: AlertLevel, + title: str, + text: str | None, + project: str, + run: str, + step: int | None, +) -> dict: + color_map = { + AlertLevel.INFO: 3447003, + AlertLevel.WARN: 16776960, + AlertLevel.ERROR: 15158332, + } + emoji = LEVEL_EMOJI.get(level, "") + step_str = f" • Step {step}" if step is not None else "" + embed = { + "title": f"{emoji} [{level.value.upper()}] {title}", + "color": color_map.get(level, 0), + "footer": {"text": f"Project: {project} • Run: {run}{step_str}"}, + } + if text: + embed["description"] = text + return {"embeds": [embed]} + + +def _build_generic_payload( + level: AlertLevel, + title: str, + text: str | None, + project: str, + run: str, + step: int | None, + timestamp: str | None, +) -> dict: + return { + "level": level.value, + "title": title, + "text": text, + "project": project, + "run": run, + "step": step, + "timestamp": timestamp, + } + + +def parse_alert_level(level: AlertLevel | str) -> AlertLevel: + if isinstance(level, AlertLevel): + return level + normalized = level.lower().strip() + try: + return AlertLevel(normalized) + except ValueError as e: + allowed = ", ".join(lvl.value for lvl in AlertLevel) + raise ValueError( + f"Invalid alert level '{level}'. Expected one of: {allowed}." + ) from e + + +def resolve_webhook_min_level( + webhook_min_level: AlertLevel | str | None, +) -> AlertLevel | None: + if webhook_min_level is None: + return None + return parse_alert_level(webhook_min_level) + + +def should_send_webhook( + level: AlertLevel, webhook_min_level: AlertLevel | None +) -> bool: + if webhook_min_level is None: + return True + return ALERT_LEVEL_ORDER[level] >= ALERT_LEVEL_ORDER[webhook_min_level] + + +def send_webhook( + url: str, + level: AlertLevel, + title: str, + text: str | None, + project: str, + run: str, + step: int | None, + timestamp: str | None = None, +) -> None: + if _is_slack_url(url): + payload = _build_slack_payload(level, title, text, project, run, step) + elif _is_discord_url(url): + payload = _build_discord_payload(level, title, text, project, run, step) + else: + payload = _build_generic_payload( + level, title, text, project, run, step, timestamp + ) + + data = json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + url, data=data, headers={"Content-Type": "application/json"} + ) + try: + urllib.request.urlopen(req, timeout=10, context=_SSL_CONTEXT) + except Exception as e: + logger.warning(f"Failed to send webhook to {url}: {e}") diff --git a/trackio/api.py b/trackio/api.py new file mode 100644 index 0000000000000000000000000000000000000000..bdecb92eb6a63966d421c9aae2e0a6943d6cdfb9 --- /dev/null +++ b/trackio/api.py @@ -0,0 +1,149 @@ +from typing import Iterator + +from trackio.registry import Registry +from trackio.sqlite_storage import SQLiteStorage + + +class Run: + def __init__(self, project: str, name: str, run_id: str | None = None): + self.project = project + self.name = name + self._id = run_id or name + self._config = None + + @property + def id(self) -> str: + return self._id + + @property + def config(self) -> dict | None: + if self._config is None: + self._config = SQLiteStorage.get_run_config( + self.project, self.name, run_id=self.id + ) + return self._config + + def alerts(self, level: str | None = None, since: str | None = None) -> list[dict]: + return SQLiteStorage.get_alerts( + self.project, run_name=self.name, run_id=self.id, level=level, since=since + ) + + def delete(self) -> bool: + return SQLiteStorage.delete_run(self.project, self.name, run_id=self.id) + + def move(self, new_project: str) -> bool: + success = SQLiteStorage.move_run( + self.project, self.name, new_project, run_id=self.id + ) + if success: + self.project = new_project + return success + + def rename(self, new_name: str) -> "Run": + SQLiteStorage.rename_run(self.project, self.name, new_name, run_id=self.id) + self.name = new_name + return self + + def __repr__(self) -> str: + return f"" + + +class Runs: + def __init__(self, project: str): + self.project = project + self._runs = None + + def _load_runs(self): + if self._runs is None: + records = SQLiteStorage.get_run_records(self.project) + self._runs = [ + Run( + self.project, + str(record["name"]), + run_id=str(record["id"]) if record["id"] is not None else None, + ) + for record in records + ] + + def __iter__(self) -> Iterator[Run]: + self._load_runs() + return iter(self._runs) + + def __getitem__(self, index: int) -> Run: + self._load_runs() + return self._runs[index] + + def __len__(self) -> int: + self._load_runs() + return len(self._runs) + + def __repr__(self) -> str: + self._load_runs() + return f"" + + +class Api: + def runs(self, project: str) -> Runs: + if not SQLiteStorage.get_project_db_path(project).exists(): + raise ValueError(f"Project '{project}' does not exist") + return Runs(project) + + def alerts( + self, + project: str, + run: str | None = None, + level: str | None = None, + since: str | None = None, + ) -> list[dict]: + if not SQLiteStorage.get_project_db_path(project).exists(): + raise ValueError(f"Project '{project}' does not exist") + return SQLiteStorage.get_alerts(project, run_name=run, level=level, since=since) + + def create_registry( + self, + name: str, + description: str | None = None, + bucket_id: str | None = None, + ) -> Registry: + """Create a new registry and return a handle on it. + + Raises `ValueError` if a registry with this name already exists. + Registries are never created implicitly: linking into a registry + that does not exist raises an error. + + Args: + name (`str`): + Registry name, e.g. `"models"`. Must match + `^[A-Za-z0-9_-]+$`. + description (`str`, *optional*): + Human-readable description of the registry. + bucket_id (`str`, *optional*): + Hugging Face bucket to hold the registry, e.g. + `"my-org/models-registry"`. The bucket is created (private) if + it does not exist. A bucket-backed registry is reachable from + any machine with access to it, including runs that log to a + Space. Omit for a local registry; defaults to + `TRACKIO_REGISTRY_BUCKET_ID` when that is set. + + Returns: + A [`Registry`] handle on the new registry. + """ + registry = Registry(name, bucket_id=bucket_id) + registry._storage.create_registry(name, description=description) + return registry + + def registry(self, name: str, bucket_id: str | None = None) -> Registry: + """Fetch a handle on an existing registry. + + Pass `bucket_id` (or set `TRACKIO_REGISTRY_BUCKET_ID`) for a + bucket-backed registry. Raises `ValueError` if no registry with this + name exists.""" + registry = Registry(name, bucket_id=bucket_id) + if not registry._storage.registry_exists(name): + where = ( + "" + if registry.bucket_id is None + else f" in bucket '{registry.bucket_id}'" + ) + raise ValueError(f"Registry '{name}' does not exist{where}") + return registry diff --git a/trackio/apple_gpu.py b/trackio/apple_gpu.py new file mode 100644 index 0000000000000000000000000000000000000000..3373bf6ddc8dcde19b2d6b15e7ce46e86df00dc1 --- /dev/null +++ b/trackio/apple_gpu.py @@ -0,0 +1,206 @@ +import platform +import subprocess +import sys +import threading +import warnings +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from trackio.run import Run + +psutil: Any = None +PSUTIL_AVAILABLE = False +_monitor_lock = threading.Lock() + + +def _ensure_psutil(): + global PSUTIL_AVAILABLE, psutil + if PSUTIL_AVAILABLE: + return psutil + try: + import psutil as _psutil + + psutil = _psutil + PSUTIL_AVAILABLE = True + return psutil + except ImportError: + raise ImportError( + "psutil is required for Apple Silicon monitoring. " + "Install it with: pip install psutil" + ) + + +def is_apple_silicon() -> bool: + """Check if running on Apple Silicon (M1/M2/M3/M4).""" + if platform.system() != "Darwin": + return False + + try: + result = subprocess.run( + ["sysctl", "-n", "machdep.cpu.brand_string"], + capture_output=True, + text=True, + timeout=1, + ) + cpu_brand = result.stdout.strip() + return "Apple" in cpu_brand + except Exception: + return False + + +def get_gpu_info() -> dict[str, Any]: + """Get Apple GPU information using ioreg.""" + try: + result = subprocess.run( + ["ioreg", "-r", "-d", "1", "-w", "0", "-c", "IOAccelerator"], + capture_output=True, + text=True, + timeout=2, + ) + + if result.returncode == 0 and result.stdout: + lines = result.stdout.strip().split("\n") + for line in lines: + if "IOAccelerator" in line and "class" in line: + return {"detected": True, "type": "Apple GPU"} + else: + print("Error collecting Apple GPU info. ioreg stdout was:", file=sys.stderr) + print(result.stdout, file=sys.stderr) + print("ioreg stderr was:", file=sys.stderr) + print(result.stderr, file=sys.stderr) + + result = subprocess.run( + ["system_profiler", "SPDisplaysDataType"], + capture_output=True, + text=True, + timeout=3, + ) + + if result.returncode == 0 and "Apple" in result.stdout: + for line in result.stdout.split("\n"): + if "Chipset Model:" in line: + model = line.split(":")[-1].strip() + return {"detected": True, "type": model} + + except Exception: + pass + + return {"detected": False} + + +def apple_gpu_available() -> bool: + """ + Check if Apple GPU monitoring is available. + + Returns True if running on Apple Silicon (M-series chips) and psutil is installed. + """ + try: + _ensure_psutil() + return is_apple_silicon() + except ImportError: + return False + except Exception: + return False + + +def collect_apple_metrics(include_cpu_metrics: bool = True) -> dict: + """ + Collect system metrics for Apple Silicon. + + Returns: + Dictionary of system metrics including CPU, memory, and GPU info. + """ + metrics = {} + + if include_cpu_metrics: + from trackio.cpu import collect_cpu_metrics + + metrics.update(collect_cpu_metrics(include_static=True)) + + gpu_info = get_gpu_info() + if gpu_info.get("detected"): + metrics["gpu/detected"] = 1 + + return metrics + + +class AppleGpuMonitor: + def __init__( + self, run: "Run", interval: float = 10.0, include_cpu_metrics: bool = True + ): + self._run = run + self._interval = interval + self._include_cpu_metrics = include_cpu_metrics + self._stop_flag = threading.Event() + self._thread: "threading.Thread | None" = None + + def start(self): + if not is_apple_silicon(): + warnings.warn( + "auto_log_gpu=True but not running on Apple Silicon. " + "Apple GPU logging disabled." + ) + return + + if not PSUTIL_AVAILABLE: + try: + _ensure_psutil() + except ImportError: + warnings.warn( + "auto_log_gpu=True but psutil not installed. " + "Install with: pip install psutil" + ) + return + + self._thread = threading.Thread(target=self._monitor_loop, daemon=True) + self._thread.start() + + def stop(self): + self._stop_flag.set() + if self._thread is not None: + self._thread.join(timeout=2.0) + + def _monitor_loop(self): + while not self._stop_flag.is_set(): + try: + metrics = collect_apple_metrics( + include_cpu_metrics=self._include_cpu_metrics + ) + if metrics: + self._run.log_system(metrics) + except Exception: + pass + + self._stop_flag.wait(timeout=self._interval) + + +def log_apple_gpu(run: "Run | None" = None) -> dict: + """ + Log Apple Silicon system metrics to the current or specified run. + + Args: + run: Optional Run instance. If None, uses current run from context. + + Returns: + dict: The system metrics that were logged. + + Example: + ```python + import trackio + + run = trackio.init(project="my-project") + trackio.log({"loss": 0.5}) + trackio.log_apple_gpu() + ``` + """ + from trackio import context_vars + + if run is None: + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.log_apple_gpu().") + + metrics = collect_apple_metrics() + if metrics: + run.log_system(metrics) + return metrics diff --git a/trackio/artifact.py b/trackio/artifact.py new file mode 100644 index 0000000000000000000000000000000000000000..3a1055233e7de5b89fd024e50f18329a62154c02 --- /dev/null +++ b/trackio/artifact.py @@ -0,0 +1,774 @@ +import os +import shutil +from pathlib import Path + +import httpx +from huggingface_hub.utils import get_token + +from trackio import cas, references, utils +from trackio.registry import registry_backend +from trackio.registry_storage import ( + parse_collection_target, + registry_project_name, +) +from trackio.remote_client import _merge_client_headers, _resolve_src_url +from trackio.typehints import ETag, Manifest, ManifestEntry, Sha256Digest, URIStr + + +def _materialize(blob: Path, dst: Path, size: int) -> None: + """Copy `blob` to `dst` unless `dst` already matches the blob's size and + mtime. + """ + dst.parent.mkdir(parents=True, exist_ok=True) + blob_stat = blob.stat() + if dst.is_file(): + dst_stat = dst.stat() + if dst_stat.st_size == size and dst_stat.st_mtime_ns == blob_stat.st_mtime_ns: + return + partial = dst.parent / cas.partial_blob_name(dst.name) + try: + shutil.copyfile(blob, partial) + os.utime(partial, ns=(blob_stat.st_atime_ns, blob_stat.st_mtime_ns)) + os.replace(partial, dst) + except Exception: + partial.unlink(missing_ok=True) + raise + + +def _fetch_blob_from_remote( + remote_source: dict, + project: str, + digest: Sha256Digest, + target_path: Path, +) -> None: + space_id = remote_source.get("space_id") + src = space_id or remote_source.get("server_base_url") + if not src: + raise RuntimeError( + "Artifact has _remote_source set but neither space_id nor " + "server_base_url is populated." + ) + base_url = _resolve_src_url(src).rstrip("/") + url = f"{base_url}/artifact_blob/{utils.canonical_project_name(project)}/{digest}" + headers = _merge_client_headers( + get_token() if space_id else None, + remote_source.get("write_token"), + ) + with httpx.stream( + "GET", + url, + headers=headers, + timeout=httpx.Timeout(connect=10.0, read=300.0, write=10.0, pool=10.0), + ) as response: + if response.status_code == 404: + raise FileNotFoundError( + f"Artifact blob {digest} not available on remote at {url}" + ) + response.raise_for_status() + cas.stage_blob_from_chunks( + response.iter_bytes(), + claimed_digest=digest, + target_path=target_path, + ) + + +def _materialize_reference( + uri: str, scheme: str, dest: Path, digest: Sha256Digest | ETag | URIStr +) -> None: + """Fetch the object at `uri` into `dest`, idempotently and atomically. + + If `dest` already exists it is left untouched and nothing is fetched: + reference digests are opaque, so a file already present is assumed current + (mirroring the size/mtime idempotence of `_materialize`), letting a repeated + `download()` skip references already on disk. + + Otherwise, when a real checksum was recorded, the source is re-probed and + compared. When the digest doesn't exist, or when it cannot be re-probed, + the comparison is skipped and the artifact is still materialized. + """ + if dest.is_file(): + return + if len(digest) > 0 and digest != uri: + try: + resolved = references.resolve_reference( + uri, scheme, checksum=True, max_objects=1 + )[0] + except Exception: + resolved = None + current_digest = resolved.digest if resolved is not None else None + if current_digest is not None and current_digest != digest: + utils._emit_nonfatal_warning( + f"Reference {uri!r} may have changed since it was logged " + f"(recorded digest {digest}, current {current_digest})." + ) + dest.parent.mkdir(parents=True, exist_ok=True) + partial = dest.parent / cas.partial_blob_name(dest.name) + try: + references.fetch_reference(uri, scheme, partial) + os.replace(partial, dest) + except Exception: + partial.unlink(missing_ok=True) + raise + + +class Artifact: + """ + A versioned, named bundle of files and references attached to a project. + + Construct an `Artifact`, add files to it with `add_file` / `add_dir` + (bytes staged into content-addressed storage) or `add_reference` (an + external URI recorded without copying any bytes), then persist it with + `trackio.log_artifact`. Logging freezes the artifact and populates its + read-only `version`, `aliases`, `size`, and `manifest`. + + Args: + name (`str`): + Artifact name, unique within the project. Must match + `^[A-Za-z0-9._-]+$` (letters, digits, dot, underscore, hyphen). + type (`str`): + Free-form category such as `"model"` or `"dataset"`, used to group + artifacts. + description (`str`, *optional*): + Human-readable description of the artifact. + metadata (`dict`, *optional*): + Arbitrary JSON-serializable metadata stored alongside the version. + """ + + def __init__( + self, + name: str, + type: str, + description: str | None = None, + metadata: dict | None = None, + ): + cas.validate_artifact_name(name) + self._name = name + self._type = type + self._description = description + self._metadata: dict = dict(metadata) if metadata else {} + self._pending_files: list[tuple[Path, str]] = [] + self._pending_references: list[ + tuple[str, str, Sha256Digest | ETag | URIStr, int] + ] = [] + self._logged = False + self._version: int | None = None + self._aliases: tuple[str, ...] = () + self._size: int | None = None + self._manifest: Manifest | None = None + self._manifest_digest: Sha256Digest | None = None + self._project: str | None = None + self._remote_source: dict | None = None + self._registry: str | None = None + self._registry_bucket_id: str | None = None + self._source_project: str | None = None + self._source_artifact: str | None = None + self._source_version: int | None = None + + @property + def name(self) -> str: + """Artifact name, unique within its project.""" + return self._name + + @property + def type(self) -> str: + """Free-form category such as `"model"` or `"dataset"`.""" + return self._type + + @property + def description(self) -> str | None: + """Human-readable description, or None if unset.""" + return self._description + + @description.setter + def description(self, value: str | None) -> None: + self._description = value + + @property + def metadata(self) -> dict: + """Live, mutable metadata dict; assignments like `art.metadata["k"] = v` + made before logging are preserved.""" + return self._metadata + + @metadata.setter + def metadata(self, value: dict | None) -> None: + self._metadata = dict(value) if value else {} + + @property + def version(self) -> str | None: + """Version string in `"v"` form (e.g. `"v3"`), or None if the + artifact has not been logged or fetched yet.""" + return None if self._version is None else f"v{self._version}" + + @property + def aliases(self) -> tuple[str, ...]: + """Aliases pointing at this version, e.g. `("latest", "prod")`.""" + return self._aliases + + @property + def size(self) -> int | None: + """Total size in bytes of the artifact's files plus any referenced data + whose size is known, or None if not yet logged.""" + return self._size + + @property + def manifest(self) -> Manifest | None: + """List of manifest entries describing the artifact's contents, or None + if not yet logged or fetched. File entries carry `path`, `digest`, and + `size`; reference entries carry `path`, `ref` (a URI), `size`, and a + `digest` — a sha256 for local/LFS references, the provider's opaque ETag + for HTTP or object-store references, or the URI itself when no checksum + is available. Returns a fresh copy on each access.""" + return None if self._manifest is None else [dict(e) for e in self._manifest] + + @property + def manifest_digest(self) -> Sha256Digest | None: + """Digest over the manifest (file paths plus their content), used to + de-duplicate re-logged content; None until logged or fetched.""" + return self._manifest_digest + + @property + def digest(self) -> Sha256Digest | None: + """Alias of `manifest_digest`.""" + return self._manifest_digest + + @property + def qualified_name(self) -> str: + """`"/:v"` for a logged or fetched artifact.""" + if self._project is None or self._version is None: + raise RuntimeError( + "Artifact has no qualified name until it is logged or fetched." + ) + return f"{self._project}/{self._name}:v{self._version}" + + @property + def project(self) -> str | None: + """Project the artifact belongs to, or None until logged or fetched.""" + return self._project + + @property + def is_link(self) -> bool: + """True if this artifact is a registry link, i.e. the pointer + returned by `Run.link_artifact`, rather than a source artifact + version.""" + return self._registry is not None + + @property + def source_name(self) -> str | None: + """Name of the source artifact version this artifact's content comes + from. Equal to `name` unless this artifact is a registry link.""" + return self._source_artifact if self.is_link else self._name + + @property + def source_version(self) -> str | None: + """Version string (`"v"`) of the source artifact version. Equal to + `version` unless this artifact is a registry link.""" + if self.is_link: + return f"v{self._source_version}" + return self.version + + @property + def source_project(self) -> str | None: + """Project the source artifact version was logged in. Equal to + `project` unless this artifact is a registry link.""" + return self._source_project if self.is_link else self._project + + @property + def source_qualified_name(self) -> str: + """`"/:v"` of the source artifact version. Equal to + `qualified_name` unless this artifact is a registry link.""" + if self.is_link: + return ( + f"{self._source_project}/{self._source_artifact}" + f":v{self._source_version}" + ) + return self.qualified_name + + def wait(self, timeout: int | None = None) -> "Artifact": + """No-op: trackio logs artifacts synchronously, so the artifact is + already committed by the time `log_artifact` returns.""" + if not self._logged: + raise RuntimeError( + "Cannot wait on an Artifact that has not been logged; " + "call log_artifact first." + ) + return self + + def add_file(self, local_path: str | Path, name: str | None = None) -> None: + """Stage a single file for inclusion in the artifact. + + Args: + local_path (`str` or `Path`): + Path to an existing regular file to add. + name (`str`, *optional*): + Logical path the file is stored under inside the artifact. + Defaults to the file's basename. + """ + if self._logged: + raise RuntimeError( + "Cannot add files to an Artifact that has already been logged or fetched." + ) + src = Path(local_path).resolve() + if not src.is_file(): + raise ValueError(f"Not a regular file: {local_path}") + logical = cas.validate_logical_path(name if name is not None else src.name) + self._pending_files.append((src, logical)) + + def add_dir(self, local_dir: str | Path, name: str | None = None) -> None: + """Stage every regular file under a directory, recursively. + + Symlinks are skipped. + + Args: + local_dir (`str` or `Path`): + Path to an existing directory whose files are added. + name (`str`, *optional*): + Logical path prefix for the added files. Defaults to no prefix, + so files keep their paths relative to `local_dir`. + """ + if self._logged: + raise RuntimeError( + "Cannot add files to an Artifact that has already been logged or fetched." + ) + root = Path(local_dir).resolve() + if not root.is_dir(): + raise ValueError(f"Not a directory: {local_dir}") + prefix = cas.validate_logical_path(name.rstrip("/")) + "/" if name else "" + for dirpath, dirnames, filenames in os.walk(root, followlinks=False): + dirnames.sort() + for filename in sorted(filenames): + entry = Path(dirpath) / filename + if entry.is_symlink() or not entry.is_file(): + continue + rel = entry.relative_to(root).as_posix() + logical = cas.validate_logical_path(prefix + rel) + self._pending_files.append((entry, logical)) + + def add_reference( + self, + uri: str, + name: str | None = None, + checksum: bool = True, + max_objects: int | None = None, + ) -> None: + """Stage a reference to external data, without copying any bytes. + + Unlike `add_file`/`add_dir`, a reference records the data's URI (plus, + when available, a size and checksum) *without staging the bytes into + the content-addressed store*. This is how you version and track + lineage over large or already-durably-stored datasets that must not be + duplicated. + + A URI that denotes a directory (`file://`, `hf://`, or a prefix + expanded by a custom handler) is expanded into one entry per object, + capped by `max_objects`. Reference bytes are never uploaded to a Space + or bucket. + + The URI is stored verbatim in the artifact manifest, and manifests may + be synced to a shared Hugging Face Dataset — so a signed URI triggers a + warning for security reasons; please prefer the object's canonical + unsigned URI (e.g. `s3://bucket/key`). + + Args: + uri (`str`): + Location of the referenced data, percent-encoded (build local + URIs with `pathlib.Path.as_uri()`). `file://`, `http(s)://`, + and `hf://` are resolved and fetched natively; any other scheme + (`s3://`, `gs://`, Azure blob URLs, ...) requires a custom + `ReferenceHandler` registered via + `trackio.register_reference_handler` — see the artifacts + documentation for complete examples — and raises otherwise. + name (`str`, *optional*): + Logical path the reference is stored under. Defaults to the last + segment of `uri`; for an expanded prefix (ending with /) it is the + prefix under which each object's relative path is nested. + checksum (`bool`, *optional*, defaults to `True`): + When `True`, probes the source to derive its size and a + checksum (and to expand a directory/prefix): `file://` is + stream-hashed to a sha256, `hf://` uses its LFS sha256 or git + blob id, and `http(s)://` uses the server `ETag`. This may read + the filesystem or make network requests. When no checksum can + be obtained for a *remote* source (e.g. no ETag), the URI + itself is used as the `digest` and a warning is emitted. A + local `file://` path that does not exist instead raises. Pass + `checksum=False` to skip remote probing entirely. + max_objects (`int`, *optional*): + Cap on how many objects a directory/prefix expands into (default + 10,000); exceeding it raises. + """ + if self._logged: + raise RuntimeError( + "Cannot add references to an Artifact that has already been " + "logged or fetched." + ) + scheme, normalized = references.validate_reference_uri(uri) + if references.looks_signed(normalized): + utils._emit_nonfatal_warning( + f"Reference {uri!r} looks like a signed URL, and the full URI " + "(embedded credential included) is stored in the artifact " + "manifest, which may be synced to a shared HF Dataset. Prefer " + "the object's canonical unsigned URI (e.g. s3://bucket/key or " + "the URL without its signed query string)." + ) + cap = references.DEFAULT_MAX_OBJECTS if max_objects is None else max_objects + resolved = references.resolve_reference(normalized, scheme, checksum, cap) + if not resolved: + raise ValueError(f"Reference URI {uri!r} matched no objects.") + used_uri_fallback = False + name_prefix = f"{name.rstrip('/')}/" if name is not None else "" + for entry in resolved: + if entry.relkey is None: + logical = self._reference_logical_name(name, entry.uri, uri) + else: + logical = cas.validate_logical_path(name_prefix + entry.relkey) + ref_digest: Sha256Digest | ETag | URIStr | None = entry.digest + if ref_digest is None: + ref_digest = URIStr(entry.uri) + used_uri_fallback = True + ref_size = 0 if entry.size is None else entry.size + self._pending_references.append((logical, entry.uri, ref_digest, ref_size)) + if checksum and used_uri_fallback: + utils._emit_nonfatal_warning( + f"Reference {uri!r} was recorded without a content checksum, so its " + "version identity is its URI; re-logging changed content at the same " + "location will reuse the existing version instead of creating a new " + f"one. {references.checksum_hint(scheme, normalized)}" + ) + + @staticmethod + def _reference_logical_name(name: str | None, resolved_uri: str, uri: str) -> str: + if name is not None: + return cas.validate_logical_path(name) + derived = references.default_reference_name(resolved_uri) + if not derived: + raise ValueError( + f"Could not derive a name from URI {uri!r}; pass name= explicitly." + ) + return cas.validate_logical_path(derived) + + def _build_manifest(self, project: str) -> Manifest: + if not self._pending_files and not self._pending_references: + raise ValueError( + f"Artifact {self._name!r} has no files or references; " + "call add_file/add_dir/add_reference first." + ) + cas.assert_manifest_paths_compatible( + [logical for _, logical in self._pending_files] + + [logical for logical, *_ in self._pending_references] + ) + + entries: Manifest = [] + for src, logical in self._pending_files: + digest, size = cas.stage_blob_into_project(src, project) + entries.append({"path": logical, "digest": digest, "size": size}) + for logical, uri, ref_digest, ref_size in self._pending_references: + entry: ManifestEntry = { + "path": logical, + "ref": uri, + "digest": ref_digest, + "size": ref_size, + } + entries.append(entry) + return entries + + def _hydrate_from_db( + self, + *, + project: str, + version: int, + aliases: list[str], + manifest: Manifest, + manifest_digest: Sha256Digest, + size_bytes: int, + description: str | None = None, + metadata: dict | None = None, + ) -> None: + if description is not None: + self._description = description + if metadata is not None: + self._metadata = dict(metadata) + self._project = project + self._version = version + self._aliases = tuple(aliases) + self._manifest = [dict(e) for e in manifest] + self._manifest_digest = manifest_digest + self._size = size_bytes + self._logged = True + + def download(self, root: str | Path | None = None) -> str: + """Materialize the artifact's files into a local directory. + + Files are copied from Trackio's content-addressed cache (and fetched + from the remote when missing locally), so repeated calls are cheap and + idempotent. + + Reference entries added via `add_reference` are fetched here too: the + object at each reference URI is downloaded into the directory (via the + bundled `httpx` for `http(s)://`, `huggingface_hub` for `hf://`, and + the registered custom `ReferenceHandler` for any other scheme). Each + object is written atomically and calls are idempotent. Because reference digests + are opaque (a provider ETag or the URI itself), a present file is assumed + current and is not re-verified against the source, and a source whose + re-probed checksum no longer matches the recorded one is downloaded + anyway with a warning. Downloading can transfer a large amount of data; + to work with the URIs without downloading, read them from the `references` + property or `get_entry_uri` instead. A missing source, an unreachable + server, or a scheme with no registered handler raises. + + Args: + root (`str` or `Path`, *optional*): + Directory to write the files into. Defaults to + `./.trackio/artifact-downloads//_v/`, + keyed by project so same-named artifacts from different projects + never collide, and by the resolved version so a moving alias + like `latest` never leaves behind stale files from a previous + version. + + Returns: + The absolute path to the download directory, as a string. + """ + if not self._logged: + raise RuntimeError( + "Cannot download an Artifact that has not been logged or fetched." + ) + if self._manifest is None or self._project is None or self._version is None: + raise RuntimeError("Artifact is missing manifest, project, or version.") + blob_project = self.source_project or self._project + + if root is None: + project = utils.canonical_project_name(self._project) + root_path = ( + Path.cwd() + / ".trackio" + / "artifact-downloads" + / project + / f"{self._name}_v{self._version}" + ) + else: + root_path = Path(root) + root_path.mkdir(parents=True, exist_ok=True) + + for entry in self._manifest: + logical = cas.validate_logical_path(entry["path"]) + if references.is_reference_entry(entry): + scheme, uri = references.validate_reference_uri(entry["ref"]) + _materialize_reference( + uri, scheme, root_path / logical, entry["digest"] + ) + continue + digest = cas.validate_digest(entry["digest"]) + blob = cas.blob_path(blob_project, digest) + if not blob.is_file(): + if self._remote_source is None: + raise FileNotFoundError( + f"Artifact blob {digest} not available locally or " + "remotely. The producer machine may not have shipped " + "this blob yet." + ) + _fetch_blob_from_remote(self._remote_source, blob_project, digest, blob) + _materialize(blob, root_path / logical, entry["size"]) + + return str(root_path) + + def link( + self, + target_path: str, + aliases: list[str] | None = None, + bucket_id: str | None = None, + ) -> "Artifact": + """Link this artifact version into a registry collection. + + A link is a pointer to this version: no files are copied. See + `Run.link_artifact` for the full semantics (collection versioning, + aliases, and the returned linked artifact). + + The artifact must already be logged or fetched; unlike + `Run.link_artifact`, this does not log a draft artifact for you, and + the link records no publishing run. An artifact fetched from a Space or + a self-hosted server can only be linked into a bucket-backed registry, + since a local registry is not reachable from where its bytes live. + + Args: + target_path (`str`): + The collection to link into, as + `"registry-/"`. The registry must + already exist. + aliases (`list[str]`, *optional*): + Aliases to place on the linked version. `latest` is managed + automatically; passing it is a no-op. + bucket_id (`str`, *optional*): + Bucket holding the registry, e.g. `"my-org/models-registry"`. + Omit for a local registry; defaults to + `TRACKIO_REGISTRY_BUCKET_ID` when that is set. + + Returns: + The linked artifact at its registry location. + """ + if not self._logged: + raise RuntimeError( + "Cannot link an Artifact that has not been logged or fetched. " + "Log it first with log_artifact, or fetch a version with " + "use_artifact, then link the result." + ) + registry, collection = parse_collection_target(target_path) + return self._link_version( + registry, collection, aliases, None, None, bucket_id=bucket_id + ) + + def unlink(self) -> None: + """Remove this registry link. + + Call this on the linked artifact returned by `link` or + `Run.link_artifact`, not on a source artifact version. The source + artifact and its files are untouched; only the collection membership + is removed. Aliases pointing at the link go with it, and the + collection version number is never reused. If the version held + `latest`, that alias moves to the highest remaining version. + """ + if not self.is_link: + raise ValueError( + "unlink() removes a registry link; call it on the linked " + "artifact returned by link() or Run.link_artifact, not on a " + "source artifact version." + ) + registry_backend(self._registry_bucket_id).unlink( + self._registry, self._name, self._version + ) + + def _resolve_link_source(self) -> tuple[str, str, int]: + """Coordinates ``(project, artifact, version)`` a link operation + records for this artifact. A registry-linked artifact links its + source version directly, so links never chain.""" + if self.is_link: + return self._source_project, self._source_artifact, self._source_version + return self._project, self._name, self._version + + def _link_version( + self, + registry: str, + collection: str, + aliases: list[str] | None, + run_name: str | None, + run_id: str | None, + bucket_id: str | None = None, + ) -> "Artifact": + """Link this logged artifact version into `registry`/`collection` and + return the linked artifact hydrated at its registry location. + + A bucket-backed registry accepts a version whose bytes live on a Space + or a self-hosted server, because the link records where the source is + and every reader of the bucket can follow it. A local registry cannot: + it is reachable only from this machine, so linking remote bytes into it + would produce a pointer nobody else can resolve. + """ + source_project, source_artifact, source_version = self._resolve_link_source() + if self.is_link: + utils._emit_nonfatal_warning( + f"Linking a linked artifact links its source version " + f"({self.source_qualified_name}) directly; registry links " + "never chain." + ) + bucket_id = utils.resolve_registry_bucket_id(bucket_id) + if self._remote_source is not None and bucket_id is None: + raise NotImplementedError( + "Linking an artifact fetched from a Space or a self-hosted " + "server into a local registry is not supported. Pass " + "bucket_id= (or set TRACKIO_REGISTRY_BUCKET_ID) to publish " + "into a bucket-backed registry both sides can reach." + ) + manifest = self._manifest or [] + remote_source = self._remote_source or {} + link = registry_backend(bucket_id).link_artifact_version( + registry=registry, + collection=collection, + type=self._type, + source_project=source_project, + source_artifact=source_artifact, + source_version=source_version, + aliases=aliases, + run_name=run_name, + run_id=run_id, + source_space_id=remote_source.get("space_id"), + source_bucket_id=remote_source.get("bucket_id"), + source_server_base_url=remote_source.get("server_base_url"), + ) + linked = Artifact._from_registry_link( + registry, + collection, + { + **link, + "source": { + "type": self._type, + "description": self._description, + "metadata": self._metadata, + "manifest": manifest, + "manifest_digest": self._manifest_digest, + "size_bytes": self._size or 0, + }, + }, + bucket_id=bucket_id, + ) + if self._remote_source is not None: + linked._remote_source = dict(self._remote_source) + return linked + + @classmethod + def _from_registry_link( + cls, + registry: str, + collection: str, + link: dict, + bucket_id: str | None = None, + ) -> "Artifact": + """Build the linked artifact at its registry location from a link + record. `bucket_id` is remembered so `unlink()` goes back to the same + registry the link was written to.""" + source = link["source"] + linked = cls( + name=collection, + type=source["type"], + description=source.get("description"), + metadata=source.get("metadata"), + ) + linked._hydrate_from_db( + project=registry_project_name(registry), + version=int(link["collection_version"]), + aliases=link["aliases"], + manifest=source["manifest"], + manifest_digest=source["manifest_digest"], + size_bytes=int(source["size_bytes"]), + ) + linked._registry = registry + linked._registry_bucket_id = bucket_id + linked._source_project = link["source_project"] + linked._source_artifact = link["source_artifact"] + linked._source_version = int(link["source_version"]) + return linked + + @property + def references(self) -> list[dict]: + """Reference entries (each with `path`, `ref`, `size`, and a `digest`) + recorded via `add_reference`. These point at external data not stored in + Trackio; read them to work with the URIs without downloading (unlike + `download()`, which fetches them). Returns a fresh copy on each access; + empty if the artifact has no references or has not been logged yet.""" + if self._manifest is None: + return [] + return [dict(e) for e in self._manifest if references.is_reference_entry(e)] + + def get_entry_uri(self, name: str) -> str | None: + """Return the URI recorded for the reference entry at logical path + `name`, or None if there is no reference entry with that name.""" + if self._manifest is None: + return None + for entry in self._manifest: + if entry.get("path") == name and references.is_reference_entry(entry): + return entry["ref"] + return None + + def __repr__(self) -> str: + parts: list[str] = [f"name={self._name!r}", f"type={self._type!r}"] + if self._version is not None: + parts.append(f"version=v{self._version}") + if self._aliases: + parts.append(f"aliases={list(self._aliases)!r}") + return f"Artifact({', '.join(parts)})" diff --git a/trackio/asgi_app.py b/trackio/asgi_app.py new file mode 100644 index 0000000000000000000000000000000000000000..d6e395fd5490e3634546b9514ac61832e0eb2a84 --- /dev/null +++ b/trackio/asgi_app.py @@ -0,0 +1,538 @@ +from __future__ import annotations + +import inspect +import json +import logging +import math +import secrets +import tempfile +import threading +from collections.abc import Callable +from pathlib import Path +from typing import Any, get_args, get_origin +from urllib.parse import unquote + +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import FileResponse, JSONResponse, Response, StreamingResponse +from starlette.routing import Route + +from trackio import utils +from trackio.exceptions import TrackioAPIError +from trackio.remote_client import HTTP_API_VERSION + +logger = logging.getLogger("trackio.asgi_app") + +_PACKAGE_JSON_PATH = Path(__file__).parent / "package.json" +_TRACKIO_PACKAGE_VERSION = json.loads(_PACKAGE_JSON_PATH.read_text())["version"] + + +def _normalize_allowed_file_roots( + allowed_file_roots: list[str | Path] | None, +) -> tuple[Path, ...]: + roots = [] + for root in allowed_file_roots or []: + roots.append(Path(root).resolve()) + return tuple(roots) + + +def _is_allowed_file_path(path: Path, allowed_roots: tuple[Path, ...]) -> bool: + resolved_path = path.resolve(strict=False) + for root in allowed_roots: + try: + resolved_path.relative_to(root) + return True + except ValueError: + continue + return False + + +def _json_safe(data: Any) -> Any: + if data is None or isinstance(data, (str, bool, int)): + return data + if isinstance(data, float): + return data if math.isfinite(data) else None + if isinstance(data, dict): + return {k: _json_safe(v) for k, v in data.items()} + if isinstance(data, (list, tuple)): + return [_json_safe(v) for v in data] + if hasattr(data, "item"): + try: + return _json_safe(data.item()) + except Exception: + pass + return str(data) + + +def register_uploaded_temp_file(request: Request, file_path: str | Path) -> None: + resolved_path = Path(file_path).resolve(strict=False) + with request.app.state.uploaded_temp_files_lock: + request.app.state.uploaded_temp_files.add(resolved_path) + + +def consume_uploaded_temp_file(request: Request, file_data: Any) -> Path: + file_path = file_data.get("path") if isinstance(file_data, dict) else None + if not isinstance(file_path, str) or not file_path: + raise TrackioAPIError("Expected uploaded file metadata with a valid path.") + + resolved_path = Path(file_path).resolve(strict=False) + with request.app.state.uploaded_temp_files_lock: + if resolved_path not in request.app.state.uploaded_temp_files: + raise TrackioAPIError( + "Uploaded file was not created by this Trackio server." + ) + request.app.state.uploaded_temp_files.remove(resolved_path) + + if not resolved_path.is_file(): + raise TrackioAPIError("Uploaded file is missing.") + + return resolved_path + + +def cleanup_uploaded_temp_file(file_path: str | Path) -> None: + try: + Path(file_path).unlink(missing_ok=True) + except Exception: + pass + + +def _invoke_handler( + fn: Any, + request: Request, + args: list[Any] | None = None, + kwargs: dict[str, Any] | None = None, +) -> Any: + sig = inspect.signature(fn) + params = list(sig.parameters.values()) + positional_args: list[Any] = [] + keyword_args: dict[str, Any] = {} + args = list(args or []) + kwargs = dict(kwargs or {}) + data_index = 0 + + for param in params: + if param.name == "request": + keyword_args["request"] = request + elif param.kind == inspect.Parameter.VAR_POSITIONAL: + positional_args.extend(args[data_index:]) + data_index = len(args) + elif param.kind == inspect.Parameter.VAR_KEYWORD: + keyword_args.update(kwargs) + kwargs.clear() + elif param.name in kwargs: + keyword_args[param.name] = kwargs.pop(param.name) + elif param.kind in ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + ) and data_index < len(args): + positional_args.append(args[data_index]) + data_index += 1 + elif param.default is inspect.Signature.empty and param.kind not in ( + inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.VAR_KEYWORD, + ): + raise TrackioAPIError(f"Missing required parameter: {param.name}") + + return fn(*positional_args, **keyword_args) + + +async def version_handler(request: Request) -> Response: + mcp_enabled = bool(getattr(request.app.state, "mcp_enabled", False)) + return JSONResponse( + { + "version": _TRACKIO_PACKAGE_VERSION, + "api_version": HTTP_API_VERSION, + "writes_paused": utils.writes_paused(), + "api_transport": "http", + "mcp_enabled": mcp_enabled, + "mcp_path": "/mcp" if mcp_enabled else None, + } + ) + + +def _json_schema_and_python_type(annotation: Any) -> tuple[dict[str, Any], str]: + if annotation is inspect.Parameter.empty: + return {"type": "object"}, "Any" + origin = get_origin(annotation) + args = get_args(annotation) + if origin is not None and args: + non_none = tuple(a for a in args if a is not type(None)) + if len(non_none) == 1 and len(args) > 1: + return _json_schema_and_python_type(non_none[0]) + if origin is list: + inner, py_inner = _json_schema_and_python_type( + args[0] if args else inspect.Parameter.empty + ) + return {"type": "array", "items": inner}, f"list[{py_inner}]" + if origin is dict: + return {"type": "object"}, "dict" + if annotation in (str, bytes): + return {"type": "string"}, "str" + if annotation is int: + return {"type": "integer"}, "int" + if annotation is float: + return {"type": "number"}, "float" + if annotation is bool: + return {"type": "boolean"}, "bool" + name = getattr(annotation, "__name__", None) + if name: + return {"type": "object"}, name + return {"type": "object"}, "Any" + + +def build_gradio_api_info(api_registry: dict[str, Any]) -> dict[str, Any]: + named_endpoints: dict[str, Any] = {} + for name in sorted(api_registry.keys()): + fn = api_registry[name] + if not callable(fn): + continue + sig = inspect.signature(fn) + parameters: list[dict[str, Any]] = [] + for pname, param in sig.parameters.items(): + if pname == "request": + continue + jtype, pytype = _json_schema_and_python_type(param.annotation) + has_default = param.default is not inspect.Parameter.empty + parameters.append( + { + "label": pname, + "parameter_name": pname, + "parameter_has_default": has_default, + "parameter_default": None if not has_default else param.default, + "type": jtype, + "python_type": {"type": pytype, "description": ""}, + "component": "Api", + "example_input": None, + } + ) + ret_ann = sig.return_annotation + if ret_ann is inspect.Signature.empty: + ret_ann = Any + rjtype, rpytype = _json_schema_and_python_type(ret_ann) + returns = [ + { + "label": "result", + "type": rjtype, + "python_type": {"type": rpytype, "description": ""}, + "component": "Api", + } + ] + named_endpoints[f"/{name}"] = { + "parameters": parameters, + "returns": returns, + "api_visibility": "public", + } + return {"named_endpoints": named_endpoints, "unnamed_endpoints": {}} + + +_MAX_GRADIO_CALL_EVENTS = 256 + + +def _hf_token_value_is_unset(value: Any) -> bool: + if value is None: + return True + if isinstance(value, str) and value.strip() == "": + return True + return False + + +def _authorization_bearer_token(request: Request) -> str | None: + auth = request.headers.get("authorization") or request.headers.get("Authorization") + if not auth: + return None + parts = auth.split() + if len(parts) != 2 or parts[0].lower() != "bearer": + return None + tok = parts[1].strip() + return tok or None + + +def _maybe_apply_hf_token_from_authorization( + request: Request, fn: Any, args: list[Any], kwargs: dict[str, Any] +) -> None: + if not utils.on_spaces(): + return + token = _authorization_bearer_token(request) + if not token: + return + sig = inspect.signature(fn) + if "hf_token" not in sig.parameters: + return + params = [p for p in sig.parameters.values() if p.name != "request"] + names = [p.name for p in params] + if "hf_token" not in names: + return + idx = names.index("hf_token") + if "hf_token" in kwargs: + if _hf_token_value_is_unset(kwargs["hf_token"]): + kwargs["hf_token"] = token + return + if idx < len(args): + if _hf_token_value_is_unset(args[idx]): + args[idx] = token + return + kwargs["hf_token"] = token + + +def _store_gradio_call_result( + request: Request, event_id: str, api_name: str, data: Any +) -> None: + with request.app.state.gradio_call_events_lock: + d = request.app.state.gradio_call_events + while len(d) >= _MAX_GRADIO_CALL_EVENTS: + d.pop(next(iter(d))) + d[event_id] = {"api_name": api_name, "data": data} + + +async def run_api_request(request: Request, api_name: str) -> Response: + api_registry = request.app.state.api_registry + fn = api_registry.get(api_name) + if fn is None: + return JSONResponse({"error": f"Unknown API: {api_name}"}, status_code=404) + if api_name in request.app.state.write_apis and utils.writes_paused(): + return JSONResponse({"error": utils.WRITES_PAUSED_MESSAGE}, status_code=503) + + try: + body = await request.json() + except Exception: + body = {} + + args: list[Any] = [] + kwargs: dict[str, Any] = {} + if isinstance(body, dict): + if "args" in body or "kwargs" in body: + args = body.get("args") or [] + kwargs = body.get("kwargs") or {} + elif "data" in body and isinstance(body["data"], list): + args = body["data"] + else: + kwargs = body + elif isinstance(body, list): + args = body + elif body is not None: + args = [body] + + if not isinstance(args, list): + args = [args] + if not isinstance(kwargs, dict): + kwargs = {} + + _maybe_apply_hf_token_from_authorization(request, fn, args, kwargs) + + try: + result = _invoke_handler(fn, request, args=args, kwargs=kwargs) + if inspect.isawaitable(result): + result = await result + return JSONResponse({"data": _json_safe(result)}) + except TrackioAPIError as e: + return JSONResponse({"error": str(e)}, status_code=400) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + + +async def api_handler(request: Request) -> Response: + return await run_api_request(request, request.path_params["api_name"]) + + +async def gradio_api_info_handler(request: Request) -> Response: + return JSONResponse( + build_gradio_api_info(request.app.state.api_registry), + headers={"Cache-Control": "no-store"}, + ) + + +async def gradio_call_post_handler(request: Request) -> Response: + api_name = request.path_params["api_name"] + resp = await run_api_request(request, api_name) + if resp.status_code != 200: + return resp + body = json.loads(bytes(resp.body).decode()) + event_id = secrets.token_urlsafe(16) + _store_gradio_call_result(request, event_id, api_name, body["data"]) + return JSONResponse({"event_id": event_id}) + + +async def gradio_call_poll_handler(request: Request) -> Response: + api_name = request.path_params["api_name"] + event_id = request.path_params["event_id"] + with request.app.state.gradio_call_events_lock: + event = request.app.state.gradio_call_events.pop(event_id, None) + if event is None: + logger.info("gradio_api poll: unknown or expired event_id") + return JSONResponse({"error": "Unknown or expired event_id"}, status_code=404) + if event.get("api_name") != api_name: + logger.info( + "gradio_api poll: api_name mismatch (path=%r, stored=%r)", + api_name, + event.get("api_name"), + ) + with request.app.state.gradio_call_events_lock: + d = request.app.state.gradio_call_events + while len(d) >= _MAX_GRADIO_CALL_EVENTS: + d.pop(next(iter(d))) + d[event_id] = event + return JSONResponse({"error": "Unknown or expired event_id"}, status_code=404) + + data = event["data"] + payload = json.dumps(_json_safe([data])) + + async def sse() -> Any: + yield f"event: complete\ndata: {payload}\n\n" + + return StreamingResponse( + sse(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-store", + "X-Accel-Buffering": "no", + }, + ) + + +async def upload_handler(request: Request) -> Response: + upload_authorizer = getattr(request.app.state, "upload_authorizer", None) + if callable(upload_authorizer): + try: + upload_authorizer(request) + except TrackioAPIError as e: + return JSONResponse({"error": str(e)}, status_code=400) + + form = await request.form() + uploads = form.getlist("files") + saved_paths = [] + for upload in uploads: + suffix = Path(getattr(upload, "filename", "") or "").suffix + with tempfile.NamedTemporaryFile( + delete=False, + prefix="trackio-upload-", + suffix=suffix, + ) as tmp: + tmp.write(await upload.read()) + register_uploaded_temp_file(request, tmp.name) + saved_paths.append(tmp.name) + return JSONResponse({"paths": saved_paths}) + + +async def gradio_upload_alias_handler(request: Request) -> Response: + return await upload_handler(request) + + +_DISALLOWED_FILE_SUFFIXES = frozenset( + {".db", ".db-journal", ".db-wal", ".db-shm", ".sqlite", ".sqlite3"} +) + + +async def file_handler(request: Request) -> Response: + fs_path = request.query_params.get("path") + if fs_path is None: + return Response("Missing path", status_code=400) + fp = Path(unquote(fs_path)).resolve(strict=False) + if fp.suffix.lower() in _DISALLOWED_FILE_SUFFIXES: + return Response("Not found", status_code=404) + if _is_allowed_file_path(fp, (utils.ARTIFACTS_DIR.resolve(),)): + return Response("Not found", status_code=404) + allowed_roots = getattr(request.app.state, "allowed_file_roots", ()) + if fp.is_file() and _is_allowed_file_path(fp, allowed_roots): + return FileResponse(str(fp)) + return Response("Not found", status_code=404) + + +async def artifact_blob_handler(request: Request) -> Response: + from trackio import cas # noqa: PLC0415 + from trackio import server as _server # noqa: PLC0415 + + project = request.path_params.get("project") + digest = request.path_params.get("digest") + try: + project = _server._validate_project_name(project) + digest = _server._validate_sha256_digest(digest) + except TrackioAPIError: + return Response("Not found", status_code=404) + blob = cas.blob_path(project, digest) + if not blob.is_file(): + return Response("Not found", status_code=404) + return FileResponse(str(blob)) + + +def create_trackio_starlette_app( + oauth_routes: list[Route], + api_registry: dict[str, Any], + extra_routes: list[Any] | None = None, + write_apis: frozenset[str] = frozenset(), + mcp_lifespan: Any = None, + mcp_enabled: bool = False, + allowed_file_roots: list[str | Path] | None = None, + upload_authorizer: Callable[[Request], None] | None = None, +) -> Starlette: + routes: list[Any] = list(oauth_routes) + routes.extend( + [ + Route("/version", endpoint=version_handler, methods=["GET"]), + Route("/api/upload", endpoint=upload_handler, methods=["POST"]), + Route("/api/{api_name:str}", endpoint=api_handler, methods=["POST"]), + Route("/file", endpoint=file_handler, methods=["GET"]), + Route( + "/artifact_blob/{project:str}/{digest:str}", + endpoint=artifact_blob_handler, + methods=["GET"], + ), + ] + ) + if utils.on_spaces(): + routes.extend( + [ + Route( + "/gradio_api/info", + endpoint=gradio_api_info_handler, + methods=["GET"], + ), + Route( + "/gradio_api/info/", + endpoint=gradio_api_info_handler, + methods=["GET"], + ), + Route( + "/gradio_api/upload", + endpoint=gradio_upload_alias_handler, + methods=["POST"], + ), + Route( + "/gradio_api/upload/", + endpoint=gradio_upload_alias_handler, + methods=["POST"], + ), + Route( + "/gradio_api/call/{api_name:str}", + endpoint=gradio_call_post_handler, + methods=["POST"], + ), + Route( + "/gradio_api/call/{api_name:str}/", + endpoint=gradio_call_post_handler, + methods=["POST"], + ), + Route( + "/gradio_api/call/{api_name:str}/{event_id:str}", + endpoint=gradio_call_poll_handler, + methods=["GET"], + ), + Route( + "/gradio_api/call/{api_name:str}/{event_id:str}/", + endpoint=gradio_call_poll_handler, + methods=["GET"], + ), + ] + ) + routes.extend(extra_routes or []) + app = Starlette(routes=routes, lifespan=mcp_lifespan) + app.state.api_registry = api_registry + app.state.write_apis = write_apis + app.state.mcp_enabled = mcp_enabled + app.state.allowed_file_roots = _normalize_allowed_file_roots(allowed_file_roots) + app.state.upload_authorizer = upload_authorizer + app.state.uploaded_temp_files = set() + app.state.uploaded_temp_files_lock = threading.Lock() + if utils.on_spaces(): + app.state.gradio_call_events = {} + app.state.gradio_call_events_lock = threading.Lock() + return app diff --git a/trackio/assets/badge.png b/trackio/assets/badge.png new file mode 100644 index 0000000000000000000000000000000000000000..03e0340873223b3ecb45f3ebf09512f6d6746ec2 --- /dev/null +++ b/trackio/assets/badge.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:206b7847247e83279f498510a2760338a03116bb5141a658d71ec14429f9ea9e +size 169669 diff --git a/trackio/assets/trackio_logo_dark.png b/trackio/assets/trackio_logo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e6ff7e0a3580bca040d9ec3ea8c91e8cca469e61 Binary files /dev/null and b/trackio/assets/trackio_logo_dark.png differ diff --git a/trackio/assets/trackio_logo_light.png b/trackio/assets/trackio_logo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..b3438262c61989e6c6d16df4801a8935136115b3 Binary files /dev/null and b/trackio/assets/trackio_logo_light.png differ diff --git a/trackio/assets/trackio_logo_old.png b/trackio/assets/trackio_logo_old.png new file mode 100644 index 0000000000000000000000000000000000000000..48a07d40b83e23c9cc9dc0cb6544a3c6ad62b57f --- /dev/null +++ b/trackio/assets/trackio_logo_old.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3922c4d1e465270ad4d8abb12023f3beed5d9f7f338528a4c0ac21dcf358a1c8 +size 487101 diff --git a/trackio/assets/trackio_logo_type_dark.png b/trackio/assets/trackio_logo_type_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..6f80a3191e514a8a0beaa6ab2011e5baf8df5eda Binary files /dev/null and b/trackio/assets/trackio_logo_type_dark.png differ diff --git a/trackio/assets/trackio_logo_type_dark_transparent.png b/trackio/assets/trackio_logo_type_dark_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..2495f610c975917290b57706133b38621a58cb8b Binary files /dev/null and b/trackio/assets/trackio_logo_type_dark_transparent.png differ diff --git a/trackio/assets/trackio_logo_type_light.png b/trackio/assets/trackio_logo_type_light.png new file mode 100644 index 0000000000000000000000000000000000000000..f07866d245ea897b9aba417b29403f7f91cc8bbd Binary files /dev/null and b/trackio/assets/trackio_logo_type_light.png differ diff --git a/trackio/assets/trackio_logo_type_light_transparent.png b/trackio/assets/trackio_logo_type_light_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..a20b4d5e64c61c91546577645310593fe3493508 Binary files /dev/null and b/trackio/assets/trackio_logo_type_light_transparent.png differ diff --git a/trackio/bucket_storage.py b/trackio/bucket_storage.py new file mode 100644 index 0000000000000000000000000000000000000000..432ff550be03aacf1b8f2909a0844a70eba8d28f --- /dev/null +++ b/trackio/bucket_storage.py @@ -0,0 +1,214 @@ +import tempfile +from pathlib import Path + +import huggingface_hub + +from trackio.cas import is_partial_blob +from trackio.sqlite_storage import SQLiteStorage +from trackio.utils import ( + TRACKIO_DIR, + canonical_project_name, + project_artifacts_dir, + project_media_dir, +) + + +def create_bucket_if_not_exists(bucket_id: str, private: bool | None = None) -> None: + huggingface_hub.create_bucket(bucket_id, private=private, exist_ok=True) + + +LOGBOOK_FILES_BUCKET_PREFIX = "logbook-files" + + +def upload_logbook_path_artifacts_to_bucket( + bucket_id: str, entries: list[dict] +) -> list[str]: + """Uploads local files referenced by path-artifact cells, preserving their + relative path under a stable bucket prefix. Returns the relative paths that + were actually uploaded (missing files are skipped).""" + additions: list[tuple[str, str]] = [] + uploaded: list[str] = [] + for entry in entries: + local_path = Path(entry["abs_path"]) + if not local_path.is_file(): + continue + remote_path = f"{LOGBOOK_FILES_BUCKET_PREFIX}/{entry['path']}" + additions.append((str(local_path), remote_path)) + uploaded.append(entry["path"]) + if additions: + huggingface_hub.batch_bucket_files( + bucket_id, add=additions, token=huggingface_hub.utils.get_token() + ) + return uploaded + + +def _list_bucket_file_paths( + bucket_id: str, + prefix: str | None = None, + token: str | bool | None = None, +) -> list[str]: + items = huggingface_hub.list_bucket_tree( + bucket_id, prefix=prefix, recursive=True, token=token + ) + return [ + item.path + for item in items + if getattr(item, "type", None) == "file" and getattr(item, "path", None) + ] + + +def download_bucket_to_trackio_dir(bucket_id: str) -> None: + TRACKIO_DIR.mkdir(parents=True, exist_ok=True) + huggingface_hub.sync_bucket( + source=f"hf://buckets/{bucket_id}/trackio", + dest=str(TRACKIO_DIR), + quiet=True, + ) + + +def upload_project_to_bucket(project: str, bucket_id: str) -> None: + db_path = SQLiteStorage.get_project_db_path(project) + if not db_path.exists(): + raise FileNotFoundError(f"No database found for project '{project}'") + + with SQLiteStorage._get_connection( + db_path, configure_pragmas=False, row_factory=None + ) as conn: + conn.execute("PRAGMA wal_checkpoint(TRUNCATE)") + + files_to_add = [(str(db_path), f"trackio/{db_path.name}")] + + media_dir = project_media_dir(project) + if media_dir.exists(): + for media_file in media_dir.rglob("*"): + if media_file.is_file(): + rel = media_file.relative_to(TRACKIO_DIR) + files_to_add.append((str(media_file), f"trackio/{rel.as_posix()}")) + + artifacts_dir = project_artifacts_dir(project) + if artifacts_dir.exists(): + for blob_file in artifacts_dir.rglob("*"): + if not blob_file.is_file(): + continue + if is_partial_blob(blob_file.name): + continue + rel = blob_file.relative_to(TRACKIO_DIR) + files_to_add.append((str(blob_file), f"trackio/{rel.as_posix()}")) + + huggingface_hub.batch_bucket_files(bucket_id, add=files_to_add) + + +def _download_db_from_bucket( + project: str, bucket_id: str, dest_path: Path | None = None +) -> bool: + db_filename = SQLiteStorage.get_project_db_filename(project) + remote_path = f"trackio/{db_filename}" + local_path = dest_path or SQLiteStorage.get_project_db_path(project) + local_path.parent.mkdir(parents=True, exist_ok=True) + try: + huggingface_hub.download_bucket_files( + bucket_id, + files=[(remote_path, str(local_path))], + token=huggingface_hub.utils.get_token(), + ) + return local_path.exists() + except Exception: + return False + + +def _local_db_has_data(project: str) -> bool: + db_path = SQLiteStorage.get_project_db_path(project) + if not db_path.exists() or db_path.stat().st_size == 0: + return False + try: + with SQLiteStorage._get_connection( + db_path, configure_pragmas=False, row_factory=None + ) as conn: + count = conn.execute("SELECT COUNT(*) FROM metrics").fetchone()[0] + return count > 0 + except Exception: + return False + + +def _export_and_upload_static( + project: str, + dest_bucket_id: str, + db_path: Path, + media_dir: Path | None = None, + artifacts_dir: Path | None = None, +) -> None: + with tempfile.TemporaryDirectory() as tmp_dir: + output_dir = Path(tmp_dir) + SQLiteStorage.export_for_static_space( + project, output_dir, db_path_override=db_path + ) + + files_to_add = [] + for f in output_dir.rglob("*"): + if f.is_file(): + rel = f.relative_to(output_dir) + files_to_add.append((str(f), rel.as_posix())) + + if media_dir and media_dir.exists(): + for f in media_dir.rglob("*"): + if f.is_file(): + rel = f.relative_to(media_dir) + files_to_add.append((str(f), f"media/{rel.as_posix()}")) + + if artifacts_dir and artifacts_dir.exists(): + for f in artifacts_dir.rglob("*"): + if f.is_file() and not is_partial_blob(f.name): + rel = f.relative_to(artifacts_dir) + files_to_add.append((str(f), f"artifacts/{rel.as_posix()}")) + + huggingface_hub.batch_bucket_files(dest_bucket_id, add=files_to_add) + + +def _copy_project_prefix_between_buckets( + source_bucket_id: str, dest_bucket_id: str, project: str, kind: str +) -> None: + source_prefix = f"trackio/{kind}/{canonical_project_name(project)}/" + files_to_copy = _list_bucket_file_paths(source_bucket_id, prefix=source_prefix) + if not files_to_copy: + return + + huggingface_hub.copy_files( + f"hf://buckets/{source_bucket_id}/{source_prefix}", + f"hf://buckets/{dest_bucket_id}/{kind}/", + ) + + +def upload_project_to_bucket_for_static(project: str, bucket_id: str) -> None: + if not _local_db_has_data(project): + _download_db_from_bucket(project, bucket_id) + + db_path = SQLiteStorage.get_project_db_path(project) + _export_and_upload_static( + project, + bucket_id, + db_path, + project_media_dir(project), + project_artifacts_dir(project), + ) + + +def export_from_bucket_for_static( + source_bucket_id: str, + dest_bucket_id: str, + project: str, +) -> None: + with tempfile.TemporaryDirectory() as work_dir: + work_path = Path(work_dir) + db_path = work_path / SQLiteStorage.get_project_db_filename(project) + + if not _download_db_from_bucket(project, source_bucket_id, dest_path=db_path): + raise FileNotFoundError( + f"Could not download database for project '{project}' " + f"from bucket '{source_bucket_id}'." + ) + + _export_and_upload_static(project, dest_bucket_id, db_path) + for kind in ("media", "artifacts"): + _copy_project_prefix_between_buckets( + source_bucket_id, dest_bucket_id, project, kind + ) diff --git a/trackio/bump.py b/trackio/bump.py new file mode 100644 index 0000000000000000000000000000000000000000..3289b7ec1b0e772136e2f0079f6a4051be0d5c83 --- /dev/null +++ b/trackio/bump.py @@ -0,0 +1,614 @@ +"""Upgrade an existing Trackio Space to the locally installed Trackio version. + +`bump()` either upgrades a Space in place or duplicates it (and its bucket) into +a new Space running the local version. The Space's SQLite databases live in the +Hugging Face Bucket mounted at `/data`; the new Trackio server migrates them when +it opens them on boot. While an in-place bump is in progress the Space is paused, +so training clients write their logs to the bucket inbox as JSONL fragments, +which the upgraded Space imports once it is running again. +""" + +import io +import json +import re +import sqlite3 +import tempfile +import time +from datetime import datetime, timezone +from pathlib import Path + +import httpx +import huggingface_hub +from huggingface_hub.errors import ( + BucketNotFoundError, + HfHubHTTPError, + RepositoryNotFoundError, +) + +import trackio +from trackio import deploy +from trackio.bucket_storage import _list_bucket_file_paths, create_bucket_if_not_exists +from trackio.remote_client import RemoteClient, _space_id_to_url +from trackio.sqlite_storage import DB_EXT, SCHEMA_VERSION, SQLiteStorage +from trackio.utils import PAUSE_WRITES_VARIABLE, preprocess_space_and_dataset_ids + +MIN_BUMPABLE_VERSION = "0.21.0" +BACKUP_PREFIX = "trackio-backups" +DB_PREFIX = "trackio/" +_JOURNAL_SUFFIX = "-journal" +_REQUIREMENT_PATTERN = re.compile( + r"^\s*trackio(?:\[[^\]]*\])?\s*==\s*([0-9][0-9A-Za-z.+\-]*)\s*$" +) +_VERSION_PATTERN = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") +_MANAGED_VARIABLES = frozenset({"TRACKIO_DIR", "TRACKIO_BUCKET_ID"}) +_RUN_COUNT_PAGE_SIZE = 5000 +_FAILURE_STAGES = frozenset( + ("NO_APP_FILE", "CONFIG_ERROR", "BUILD_ERROR", "RUNTIME_ERROR") +) + + +class BumpError(RuntimeError): + pass + + +def _version_tuple(version: str) -> tuple[int, int, int] | None: + match = _VERSION_PATTERN.match(version) + if match is None: + return None + return tuple(int(part) for part in match.groups()) + + +def get_space_trackio_version( + space_id: str, hf_api: huggingface_hub.HfApi | None = None +) -> str | None: + """ + Returns the Trackio version a Space is configured to run, read from its + `requirements.txt` pin, or from `trackio/package.json` for Spaces deployed from + a source install. Returns `None` when the version cannot be determined. + """ + hf_api = hf_api or huggingface_hub.HfApi() + try: + path = hf_api.hf_hub_download(space_id, "requirements.txt", repo_type="space") + for line in Path(path).read_text(encoding="utf-8").splitlines(): + if match := _REQUIREMENT_PATTERN.match(line): + return match.group(1) + except (HfHubHTTPError, OSError): + pass + try: + path = hf_api.hf_hub_download( + space_id, "trackio/package.json", repo_type="space" + ) + return json.loads(Path(path).read_text(encoding="utf-8")).get("version") + except (HfHubHTTPError, OSError, ValueError): + return None + + +def _check_bumpable(space_id: str, space_version: str | None) -> str: + local = _version_tuple(trackio.__version__) + if local is None: + raise BumpError( + f"The local Trackio version {trackio.__version__!r} is not a final " + "release, so it cannot be compared safely with the Space's version." + ) + if space_version is None: + raise BumpError( + f"Could not determine which Trackio version Space '{space_id}' runs. " + "`trackio bump` only supports Spaces deployed by Trackio." + ) + remote = _version_tuple(space_version) + if remote is None: + raise BumpError( + f"Space '{space_id}' pins Trackio {space_version!r}, which is not a final " + "release that `trackio bump` can compare safely." + ) + if remote < _version_tuple(MIN_BUMPABLE_VERSION): + raise BumpError( + f"Space '{space_id}' runs Trackio {space_version}, which is older than " + f"{MIN_BUMPABLE_VERSION}, the oldest version `trackio bump` supports." + ) + if remote > local: + raise BumpError( + f"Space '{space_id}' runs Trackio {space_version}, which is newer than the " + f"local Trackio {trackio.__version__}. Upgrade Trackio locally instead of downgrading the Space." + ) + return space_version + + +def _bucket_db_paths(bucket_id: str, prefix: str = DB_PREFIX) -> list[str]: + return sorted( + path + for path in _list_bucket_file_paths(bucket_id, prefix=prefix) + if "/" not in path[len(prefix) :] + and (path.endswith(DB_EXT) or path.endswith(DB_EXT + _JOURNAL_SUFFIX)) + ) + + +def _db_inventory(db_path: Path) -> dict: + conn = sqlite3.connect(f"file:{db_path}?mode=ro&immutable=1", uri=True) + try: + tables = [ + row[0] + for row in conn.execute( + "SELECT name FROM sqlite_master " + "WHERE type = 'table' AND name NOT LIKE 'sqlite_%'" + ) + ] + counts = { + table: conn.execute(f'SELECT COUNT(*) FROM "{table}"').fetchone()[0] + for table in tables + } + runs = {} + if "metrics" in tables: + runs = dict( + conn.execute("SELECT run_name, COUNT(*) FROM metrics GROUP BY run_name") + ) + return {"tables": counts, "metric_rows_per_run": runs} + finally: + conn.close() + + +def bucket_inventory(bucket_id: str, prefix: str = DB_PREFIX) -> dict[str, dict]: + """ + Downloads every project database under `prefix` in a bucket and returns, per + database filename, the row count of each table and the metric rows per run. + """ + db_paths = [p for p in _bucket_db_paths(bucket_id, prefix) if p.endswith(DB_EXT)] + inventory = {} + with tempfile.TemporaryDirectory() as work_dir: + for remote_path in db_paths: + filename = remote_path.rsplit("/", 1)[-1] + local_path = Path(work_dir) / filename + huggingface_hub.download_bucket_files( + bucket_id, + files=[(remote_path, str(local_path))], + token=huggingface_hub.utils.get_token(), + ) + inventory[filename] = _db_inventory(local_path) + return inventory + + +def _served_version_info(space_id: str) -> dict: + headers = {} + if token := huggingface_hub.utils.get_token(): + headers["Authorization"] = f"Bearer {token}" + try: + response = httpx.get( + _space_id_to_url(space_id) + "version", headers=headers, timeout=30 + ) + response.raise_for_status() + return response.json() + except (httpx.HTTPError, ValueError): + return {} + + +def _wait_for_version_info(space_id: str, expected: dict, timeout: int = 300) -> None: + deadline = time.time() + timeout + info = {} + while time.time() < deadline: + info = _served_version_info(space_id) + if all(info.get(key) == value for key, value in expected.items()): + return + time.sleep(5) + raise BumpError( + f"Space '{space_id}' reports {info or 'nothing'}, expected {expected}." + ) + + +def _metric_rows_per_run(client: RemoteClient, project: str) -> dict[str, int]: + counts = {} + offset = 0 + while True: + rows = client.predict( + project, + "SELECT run_name, COUNT(*) AS n FROM metrics GROUP BY run_name " + f"ORDER BY run_name LIMIT {_RUN_COUNT_PAGE_SIZE} OFFSET {offset}", + api_name="/query_project", + )["rows"] + counts.update((row["run_name"], row["n"]) for row in rows) + if len(rows) < _RUN_COUNT_PAGE_SIZE: + return counts + offset += _RUN_COUNT_PAGE_SIZE + + +def verify_space_serves_inventory(space_id: str, inventory: dict[str, dict]) -> None: + """ + Checks that a running Space serves the local Trackio version, that every + project database reached the local `SCHEMA_VERSION`, and that every table + still holds at least the rows recorded in `inventory`. Rows can only grow, + since the Space may import inbox fragments. + """ + _wait_for_version_info(space_id, {"version": trackio.__version__}) + client = RemoteClient( + space_id, hf_token=huggingface_hub.utils.get_token(), verbose=False + ) + projects = client.predict(api_name="/get_all_projects") + by_filename = {SQLiteStorage.get_project_db_filename(p): p for p in projects} + for filename, expected in inventory.items(): + project = by_filename.get(filename) + if project is None: + raise BumpError( + f"Space '{space_id}' no longer lists the project stored in {filename}." + ) + result = client.predict( + project, "PRAGMA user_version", api_name="/query_project" + ) + schema_version = result["rows"][0]["user_version"] + if schema_version != SCHEMA_VERSION: + raise BumpError( + f"Project '{project}' is at schema version {schema_version} after the " + f"bump, expected {SCHEMA_VERSION}." + ) + for table, count in expected["tables"].items(): + result = client.predict( + project, + f'SELECT COUNT(*) AS n FROM "{table}"', + api_name="/query_project", + ) + actual = result["rows"][0]["n"] + if actual < count: + raise BumpError( + f"Project '{project}' table '{table}' has {actual} rows after the " + f"bump, expected at least {count}." + ) + actual_runs = _metric_rows_per_run(client, project) + for run, count in expected["metric_rows_per_run"].items(): + if actual_runs.get(run, 0) < count: + raise BumpError( + f"Run '{run}' in project '{project}' has {actual_runs.get(run, 0)} " + f"metric rows after the bump, expected at least {count}." + ) + + +def _wait_for_stage( + space_id: str, + targets: frozenset[str], + timeout: int, + hf_api: huggingface_hub.HfApi, +) -> None: + deadline = time.time() + timeout + stage = None + while time.time() < deadline: + try: + runtime = hf_api.get_space_runtime(space_id) + stage = str(runtime.stage) + except (HfHubHTTPError, httpx.RequestError): + stage = None + if stage in targets: + return + if stage in _FAILURE_STAGES and not targets & _FAILURE_STAGES: + raise BumpError( + f"Space '{space_id}' entered {stage}. Check its logs on the Hub." + ) + time.sleep(5) + raise BumpError( + f"Space '{space_id}' did not reach {'/'.join(sorted(targets))} within " + f"{timeout}s (last stage: {stage})." + ) + + +def _runtime_operations( + space_id: str, hf_api: huggingface_hub.HfApi, revision: str +) -> list[huggingface_hub.CommitOperationAdd]: + repo_files = set( + hf_api.list_repo_files(space_id, repo_type="space", revision=revision) + ) + has_custom_frontend = any( + f.startswith(deploy._CUSTOM_SPACE_FRONTEND_DIR + "/") for f in repo_files + ) + if deploy._is_trackio_installed_from_source(): + requirements = deploy._get_source_install_dependencies() + else: + requirements = deploy._get_space_install_requirement() + app_py = deploy._space_app_py( + deploy._CUSTOM_SPACE_FRONTEND_DIR if has_custom_frontend else None + ) + operations = [ + huggingface_hub.CommitOperationAdd( + "requirements.txt", io.BytesIO(requirements.encode("utf-8")) + ), + huggingface_hub.CommitOperationAdd( + "app.py", io.BytesIO(app_py.encode("utf-8")) + ), + ] + if ( + "trackio/__init__.py" in repo_files + and not deploy._is_trackio_installed_from_source() + ): + operations.append( + huggingface_hub.CommitOperationDelete("trackio/", is_folder=True) + ) + return operations + + +def _copy_bucket_paths(bucket_id: str, pairs: list[tuple[str, str]]) -> None: + for source, destination in pairs: + huggingface_hub.copy_files( + f"hf://buckets/{bucket_id}/{source}", + f"hf://buckets/{bucket_id}/{destination}", + ) + + +def _bump_in_place( + space_id: str, + bucket_id: str, + space_version: str, + hf_api: huggingface_hub.HfApi, + timeout: int, +) -> str: + local_version = trackio.__version__ + stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + backup_prefix = f"{BACKUP_PREFIX}/{space_version}-to-{local_version}-{stamp}/" + + base_commit = hf_api.repo_info(space_id, repo_type="space").sha + with tempfile.TemporaryDirectory() as snapshot_dir: + hf_api.snapshot_download( + space_id, + repo_type="space", + revision=base_commit, + local_dir=snapshot_dir, + max_workers=4, + ) + print(f"* Pausing {space_id}; new logs will queue in the bucket inbox") + hf_api.pause_space(space_id) + _wait_for_stage(space_id, frozenset({"PAUSED"}), 300, hf_api) + + try: + db_paths = _bucket_db_paths(bucket_id) + backups = [ + (path, backup_prefix + path[len(DB_PREFIX) :]) for path in db_paths + ] + _copy_bucket_paths(bucket_id, backups) + print( + f"* Backed up {len(db_paths)} database files to " + f"hf://buckets/{bucket_id}/{backup_prefix}" + ) + inventory = bucket_inventory(bucket_id, prefix=backup_prefix) + except Exception as e: + hf_api.restart_space(space_id) + raise BumpError( + f"Backing up '{space_id}' failed before anything was changed, so the " + f"Space was restarted on Trackio {space_version}: {e}" + ) from e + + bump_commit = None + try: + print( + f"* Updating {space_id} from Trackio {space_version} to {local_version}" + ) + bump_commit = hf_api.create_commit( + space_id, + operations=_runtime_operations(space_id, hf_api, base_commit), + commit_message=f"Bump Trackio to {local_version}", + repo_type="space", + parent_commit=base_commit, + ).oid + if deploy._is_trackio_installed_from_source(): + bump_commit = deploy._upload_source_tree( + hf_api, space_id, parent_commit=bump_commit + ).oid + hf_api.add_space_variable(space_id, PAUSE_WRITES_VARIABLE, "1") + hf_api.restart_space(space_id) + print("* Waiting for the Space to rebuild and migrate its data") + _wait_for_stage(space_id, frozenset({"RUNNING"}), timeout, hf_api) + _wait_for_version_info(space_id, {"writes_paused": True}) + verify_space_serves_inventory(space_id, inventory) + except Exception as e: + print(f"* Bump failed ({e}); restoring Trackio {space_version}") + _rollback( + space_id, + bucket_id, + backups, + Path(snapshot_dir), + bump_commit, + hf_api, + timeout, + ) + raise BumpError( + f"Bumping '{space_id}' failed and was rolled back to Trackio " + f"{space_version}: {e}" + ) from e + + print("* Verified; resuming writes and importing the queued inbox") + hf_api.delete_space_variable(space_id, PAUSE_WRITES_VARIABLE) + try: + _wait_for_stage(space_id, frozenset({"RUNNING"}), timeout, hf_api) + _wait_for_version_info( + space_id, {"version": local_version, "writes_paused": False}, timeout + ) + except BumpError as e: + raise BumpError( + f"{space_id} was upgraded to Trackio {local_version} and verified, but " + f"did not resume writes: {e} Restart the Space once it is healthy." + ) from e + print( + f"* {space_id} now runs Trackio {local_version}: " + f"{deploy.SPACE_URL.format(space_id=space_id)}" + ) + return space_id + + +def _rollback( + space_id: str, + bucket_id: str, + backups: list[tuple[str, str]], + snapshot_dir: Path, + bump_commit: str | None, + hf_api: huggingface_hub.HfApi, + timeout: int, +) -> None: + hf_api.pause_space(space_id) + _wait_for_stage(space_id, frozenset({"PAUSED"}), 300, hf_api) + _copy_bucket_paths(bucket_id, [(backup, path) for path, backup in backups]) + try: + _delete_space_variable(space_id, PAUSE_WRITES_VARIABLE, hf_api) + if bump_commit is not None: + hf_api.upload_folder( + repo_id=space_id, + repo_type="space", + folder_path=snapshot_dir, + ignore_patterns=[".cache/**"], + delete_patterns=["*", "**/*"], + commit_message="Roll back failed Trackio bump", + parent_commit=bump_commit, + ) + finally: + hf_api.restart_space(space_id) + _wait_for_stage(space_id, frozenset({"RUNNING"}) | _FAILURE_STAGES, timeout, hf_api) + + +def _delete_space_variable( + space_id: str, key: str, hf_api: huggingface_hub.HfApi +) -> None: + if key in hf_api.get_space_variables(space_id): + hf_api.delete_space_variable(space_id, key) + + +def _bump_into_new_space( + space_id: str, + bucket_id: str, + new_space_id: str, + new_bucket_id: str, + private: bool, + hf_api: huggingface_hub.HfApi, + timeout: int, +) -> str: + try: + hf_api.space_info(new_space_id) + raise BumpError(f"Space '{new_space_id}' already exists.") + except RepositoryNotFoundError: + pass + try: + if _list_bucket_file_paths(new_bucket_id): + raise BumpError( + f"Bucket '{new_bucket_id}' already exists and is not empty." + ) + except BucketNotFoundError: + pass + + created_bucket = not deploy._bucket_exists(new_bucket_id, hf_api) + try: + create_bucket_if_not_exists( + new_bucket_id, private=bool(hf_api.bucket_info(bucket_id).private) + ) + if _list_bucket_file_paths(bucket_id): + print(f"* Copying hf://buckets/{bucket_id} to hf://buckets/{new_bucket_id}") + huggingface_hub.copy_files( + f"hf://buckets/{bucket_id}/", + f"hf://buckets/{new_bucket_id}/", + ) + inventory = bucket_inventory(new_bucket_id) + + print(f"* Creating {new_space_id} with Trackio {trackio.__version__}") + deploy.deploy_as_space(new_space_id, bucket_id=new_bucket_id, private=private) + for key, variable in hf_api.get_space_variables(space_id).items(): + if key not in _MANAGED_VARIABLES: + hf_api.add_space_variable(new_space_id, key, variable.value) + _wait_for_stage(new_space_id, frozenset({"RUNNING"}), timeout, hf_api) + verify_space_serves_inventory(new_space_id, inventory) + except Exception as e: + print(f"* Bump failed ({e}); removing {new_space_id} and its copied data") + try: + _remove_new_space(new_space_id, new_bucket_id, created_bucket, hf_api) + except Exception as cleanup_error: + print(f"* Could not remove {new_space_id}: {cleanup_error}") + raise BumpError( + f"Copying '{space_id}' into '{new_space_id}' failed and the new Space " + f"was removed; '{space_id}' was not modified: {e}" + ) from e + print( + f"* {new_space_id} runs Trackio {trackio.__version__} with a copy of " + f"{space_id}'s data: {deploy.SPACE_URL.format(space_id=new_space_id)}" + ) + return new_space_id + + +def _remove_new_space( + new_space_id: str, + new_bucket_id: str, + created_bucket: bool, + hf_api: huggingface_hub.HfApi, +) -> None: + hf_api.delete_repo(new_space_id, repo_type="space", missing_ok=True) + if created_bucket: + hf_api.delete_bucket(new_bucket_id, missing_ok=True) + return + copied = _list_bucket_file_paths(new_bucket_id) + if copied: + huggingface_hub.batch_bucket_files(new_bucket_id, delete=copied) + + +def bump( + space_id: str, + new_space_id: str | None = None, + new_bucket_id: str | None = None, + timeout: int = 1800, +) -> str: + """ + Upgrades a Trackio Space to the locally installed Trackio version. + + Without `new_space_id`, the Space is upgraded in place: it is paused (clients + keep logging to the bucket inbox), its databases are backed up inside its + bucket, its Trackio pin is updated and it is restarted so the new server + migrates the data. If the Space fails to come back or loses data, the backup + and the previous Space files are restored. + + With `new_space_id`, the source Space is left untouched and its bucket data is + copied into `new_bucket_id`, which backs a new Space running the local version. + + Args: + space_id (`str`): + The Trackio Space to upgrade. Must run Trackio 0.21.0 or newer with a + bucket mounted at `/data`. + new_space_id (`str`, *optional*): + Create this Space from a copy of `space_id` instead of upgrading in place. + new_bucket_id (`str`, *optional*): + The bucket for `new_space_id`. Defaults to `{new_space_id}-bucket`. + timeout (`int`, *optional*, defaults to `1800`): + Seconds to wait for the upgraded Space to build and start. + + Returns: + `str`: The ID of the Space now running the local Trackio version. + """ + if new_bucket_id is not None and new_space_id is None: + raise BumpError("new_bucket_id requires new_space_id.") + space_id, _, _ = preprocess_space_and_dataset_ids(space_id, None, None) + hf_api = huggingface_hub.HfApi() + try: + info = hf_api.space_info(space_id) + except RepositoryNotFoundError: + raise BumpError(f"Space '{space_id}' not found.") + if info.sdk != "gradio": + raise BumpError( + f"Space '{space_id}' is not a Trackio Gradio Space (sdk={info.sdk!r})." + ) + bucket_id = deploy._get_existing_space_bucket(space_id, hf_api=hf_api) + if bucket_id is None: + raise BumpError( + f"Space '{space_id}' has no bucket mounted at '/data'. " + "`trackio bump` requires bucket storage." + ) + space_version = _check_bumpable( + space_id, get_space_trackio_version(space_id, hf_api) + ) + + if new_space_id is not None: + new_space_id, _, auto_bucket_id = preprocess_space_and_dataset_ids( + new_space_id, None, new_bucket_id + ) + return _bump_into_new_space( + space_id, + bucket_id, + new_space_id, + auto_bucket_id, + bool(info.private), + hf_api, + timeout, + ) + + if ( + space_version == trackio.__version__ + and not deploy._is_trackio_installed_from_source() + ): + print(f"* {space_id} already runs Trackio {space_version}") + return space_id + return _bump_in_place(space_id, bucket_id, space_version, hf_api, timeout) diff --git a/trackio/cas.py b/trackio/cas.py new file mode 100644 index 0000000000000000000000000000000000000000..92f797078ae5d74a78d921ccebf1384a34314b44 --- /dev/null +++ b/trackio/cas.py @@ -0,0 +1,214 @@ +"""Content-addressed storage (CAS) helpers shared by the artifact client, +server, and storage layers. +""" + +import hashlib +import os +import re +import uuid +from collections.abc import Iterable, Iterator +from pathlib import Path + +from trackio import utils +from trackio.typehints import Sha256Digest + +SHA256_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$") +ARTIFACT_VERSION_SPEC_RE = re.compile(r"^v(\d+)$") +ARTIFACT_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+\Z") +HASH_CHUNK_SIZE = 1024 * 1024 +PARTIAL_BLOB_MARKER = ".partial." +PARTIAL_BLOB_GLOB = f"*{PARTIAL_BLOB_MARKER}*" + + +def partial_blob_name(base: str = "") -> str: + """Temp filename for an in-flight blob write, excluded from sync/export.""" + return f"{base}{PARTIAL_BLOB_MARKER}{uuid.uuid4().hex}" + + +def is_partial_blob(filename: str) -> bool: + return PARTIAL_BLOB_MARKER in filename + + +def validate_artifact_name(name: str) -> str: + if not isinstance(name, str) or not ARTIFACT_NAME_RE.match(name): + raise ValueError( + f"Artifact name {name!r} must match ^[A-Za-z0-9._-]+$ " + "(letters, digits, dot, underscore, hyphen)." + ) + return name + + +def validate_aliases(aliases: list[str] | None) -> list[str]: + if aliases is None: + return [] + if isinstance(aliases, str) or not isinstance(aliases, (list, tuple)): + raise ValueError( + f"aliases must be a list of strings, got {type(aliases).__name__}." + ) + validated: list[str] = [] + for alias in aliases: + if not isinstance(alias, str) or not alias: + raise ValueError(f"Alias {alias!r} must be a non-empty string.") + if not ARTIFACT_NAME_RE.match(alias): + raise ValueError( + f"Alias {alias!r} must match ^[A-Za-z0-9._-]+$ " + "(letters, digits, dot, underscore, hyphen)." + ) + if ARTIFACT_VERSION_SPEC_RE.match(alias): + raise ValueError( + f"Alias {alias!r} is reserved for version pointers (vN); " + "choose another." + ) + validated.append(alias) + return validated + + +def _project_blobs_root(project: str) -> Path: + return utils.project_artifacts_dir(project) / "blobs" / "sha256" + + +def blob_path(project: str, digest: Sha256Digest) -> Path: + return _project_blobs_root(project) / digest[:2] / digest + + +def hash_file(path: Path) -> tuple[Sha256Digest, int]: + size = path.stat().st_size + file_digest = getattr(hashlib, "file_digest", None) + if file_digest is not None: + with path.open("rb") as f: + sha = file_digest(f, "sha256") + else: + sha = hashlib.sha256() + with path.open("rb") as f: + while chunk := f.read(HASH_CHUNK_SIZE): + sha.update(chunk) + return Sha256Digest(sha.hexdigest()), size + + +def validate_logical_path(logical: str) -> str: + """Validate a manifest logical path: relative, no `.`/`..` segments, no + backslashes, no drive letters, non-empty. Returns the path unchanged. + """ + if not isinstance(logical, str) or not logical: + raise ValueError(f"Invalid artifact path: {logical!r} (must be non-empty)") + if "\\" in logical or "\x00" in logical: + raise ValueError( + f"Invalid artifact path {logical!r}: backslashes and NUL are not allowed" + ) + if logical.startswith("/") or re.match(r"^[A-Za-z]:", logical): + raise ValueError(f"Invalid artifact path {logical!r}: must be relative") + parts = logical.split("/") + if any(part in ("", ".", "..") for part in parts): + raise ValueError( + f"Invalid artifact path {logical!r}: " + "'.', '..', and empty segments are not allowed" + ) + return logical + + +def assert_manifest_paths_compatible(paths: Iterable[str]) -> None: + """Validate a manifest's logical paths as a set: reject exact duplicates and + file/directory prefix collisions. A path cannot be both a file and a + directory, so `sub` and `sub/x` (or `a/b` and `a/b/c`) cannot coexist; + siblings like `a/b` and `a/c` are fine. + """ + seen: set[str] = set() + for p in paths: + if p in seen: + raise ValueError(f"Duplicate logical path in manifest: {p!r}") + seen.add(p) + for p in seen: + parts = p.split("/") + for i in range(1, len(parts)): + ancestor = "/".join(parts[:i]) + if ancestor in seen: + raise ValueError( + f"Artifact path {p!r} collides with {ancestor!r}: a path " + "cannot be both a file and a directory in the same artifact." + ) + + +def validate_digest(digest: str) -> Sha256Digest: + """Validate that `digest` is a 64-char lowercase sha256 hex string and + return it unchanged. + """ + if not isinstance(digest, str) or not SHA256_DIGEST_RE.match(digest): + raise ValueError(f"Invalid artifact blob digest: {digest!r}") + return Sha256Digest(digest) + + +def stage_blob_from_chunks( + chunks: Iterable[bytes], + claimed_digest: Sha256Digest, + target_path: Path, +) -> None: + """Stream `chunks` to `target_path`, verifying they hash to `claimed_digest`. + No-op if `target_path` already exists (the chunks are not consumed). + """ + if target_path.is_file(): + return + target_path.parent.mkdir(parents=True, exist_ok=True) + partial = target_path.parent / partial_blob_name(target_path.name) + sha = hashlib.sha256() + try: + with partial.open("wb") as dst: + for chunk in chunks: + if not chunk: + continue + sha.update(chunk) + dst.write(chunk) + actual = sha.hexdigest() + if actual != claimed_digest: + raise ValueError( + f"Digest mismatch: claimed {claimed_digest}, computed {actual}" + ) + os.replace(partial, target_path) + except Exception: + partial.unlink(missing_ok=True) + raise + + +def stage_blob_from_file( + src_path: Path, + claimed_digest: Sha256Digest, + target_path: Path, +) -> None: + """Copy `src_path` to `target_path`, verifying it hashes to `claimed_digest`.""" + if target_path.is_file(): + return + + def _file_chunks() -> Iterator[bytes]: + with src_path.open("rb") as src: + while chunk := src.read(HASH_CHUNK_SIZE): + yield chunk + + stage_blob_from_chunks(_file_chunks(), claimed_digest, target_path) + + +def stage_blob_into_project(src_path: Path, project: str) -> tuple[Sha256Digest, int]: + """Copy `src_path` into the project's CAS in a single pass, computing its + sha256 and size while writing. Returns `(digest, size)`. If the blob is + already present, the copy is discarded. + """ + blobs_root = _project_blobs_root(project) + blobs_root.mkdir(parents=True, exist_ok=True) + partial = blobs_root / partial_blob_name() + sha = hashlib.sha256() + size = 0 + try: + with src_path.open("rb") as src, partial.open("wb") as dst: + while chunk := src.read(HASH_CHUNK_SIZE): + sha.update(chunk) + dst.write(chunk) + size += len(chunk) + digest = Sha256Digest(sha.hexdigest()) + target = blob_path(project, digest) + if target.is_file(): + partial.unlink(missing_ok=True) + return digest, size + target.parent.mkdir(parents=True, exist_ok=True) + os.replace(partial, target) + return digest, size + except Exception: + partial.unlink(missing_ok=True) + raise diff --git a/trackio/cli.py b/trackio/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..2ad53c609b10ed4fa22783f8ca4c621e5076fe24 --- /dev/null +++ b/trackio/cli.py @@ -0,0 +1,3048 @@ +import argparse +import json +import os +import re +import shutil +import sys +from pathlib import Path + +import huggingface_hub +from huggingface_hub.utils import get_session + +import trackio +from trackio import freeze, show, sync +from trackio import logbook as lb +from trackio.bump import BumpError, bump +from trackio.cli_helpers import ( + error_exit, + format_alerts, + format_artifact, + format_artifacts, + format_json, + format_list, + format_metric_values, + format_project_summary, + format_query_result, + format_registry_collection, + format_registry_collections, + format_registry_events, + format_run_summary, + format_snapshot, + format_spaces, + format_system_metric_names, + format_system_metrics, + format_trace, + format_trace_summary, + format_traces, + trace_id_matches, + trace_rollup, +) +from trackio.deploy import sync_incremental +from trackio.frontend_config import ( + TRACKIO_CONFIG_PATH, + get_persisted_frontend_dir, + set_persisted_frontend_dir, + unset_persisted_frontend_dir, +) +from trackio.markdown import Markdown +from trackio.registry_storage import parse_collection_target +from trackio.remote_client import RemoteClient +from trackio.server import get_project_summary, get_run_summary +from trackio.sqlite_storage import SQLiteStorage + + +def _get_space(args): + return getattr(args, "space", None) + + +def _get_remote(args): + space = _get_space(args) + if not space: + return None + hf_token = getattr(args, "hf_token", None) + return RemoteClient(space, hf_token=hf_token) + + +def _handle_status(): + print("Reading local Trackio projects...\n") + projects = SQLiteStorage.get_projects() + if not projects: + print("No Trackio projects found.") + return + + local_projects = [] + synced_projects = [] + unsynced_projects = [] + + for project in projects: + space_id = SQLiteStorage.get_space_id(project) + if space_id is None: + local_projects.append(project) + elif SQLiteStorage.has_pending_data(project): + unsynced_projects.append(project) + else: + synced_projects.append(project) + + print("Finished reading Trackio projects") + if local_projects: + print(f" * {len(local_projects)} local trackio project(s) [OK]") + if synced_projects: + print(f" * {len(synced_projects)} trackio project(s) synced to Spaces [OK]") + if unsynced_projects: + print( + f" * {len(unsynced_projects)} trackio project(s) with unsynced changes [WARNING]:" + ) + for p in unsynced_projects: + print(f" - {p}") + + if unsynced_projects: + print( + f"\nRun `trackio sync --project {unsynced_projects[0]}` to sync. " + "Or run `trackio sync --all` to sync all unsynced changes." + ) + + +def _handle_sync(args): + if args.sync_all and args.project: + error_exit("Cannot use --all and --project together.") + if not args.sync_all and not args.project: + error_exit("Must provide either --project or --all.") + + if args.sync_all: + projects = SQLiteStorage.get_projects() + synced_any = False + for project in projects: + space_id = SQLiteStorage.get_space_id(project) + if space_id and SQLiteStorage.has_pending_data(project): + sync_incremental( + project, + space_id, + private=args.private, + pending_only=True, + frontend_dir=args.frontend, + ) + synced_any = True + if not synced_any: + print("No projects with unsynced data found.") + else: + space_id = args.space_id + if space_id is None: + space_id = SQLiteStorage.get_space_id(args.project) + sync( + project=args.project, + space_id=space_id, + private=args.private, + force=args.force, + sdk=args.sdk, + frontend_dir=args.frontend, + ) + + +def _handle_config(args): + if args.config_command == "get": + frontend_dir = get_persisted_frontend_dir() + if frontend_dir is None: + print("No Trackio frontend config is set.") + print(f"Config file: {TRACKIO_CONFIG_PATH}") + return + print(f"frontend: {frontend_dir}") + print(f"config: {TRACKIO_CONFIG_PATH}") + return + + if args.config_command == "set": + try: + frontend_dir = set_persisted_frontend_dir(args.frontend) + except ValueError as e: + error_exit(str(e)) + print(f"Saved Trackio default frontend: {frontend_dir}") + print("Reset with `trackio config unset frontend`.") + return + + if args.config_command == "unset": + removed = unset_persisted_frontend_dir() + if removed: + print("Removed Trackio default frontend.") + else: + print("No Trackio default frontend was set.") + return + + +def _extract_reports( + run: str, logs: list[dict], report_name: str | None = None +) -> list[dict]: + reports = [] + for log in logs: + timestamp = log.get("timestamp") + step = log.get("step") + for key, value in log.items(): + if report_name is not None and key != report_name: + continue + if isinstance(value, dict) and value.get("_type") == Markdown.TYPE: + content = value.get("_value") + if isinstance(content, str): + reports.append( + { + "run": run, + "report": key, + "step": step, + "timestamp": timestamp, + "content": content, + } + ) + return reports + + +TRACE_SORTS = { + "request_time_desc", + "request_time_asc", + "step_asc", + "step_desc", +} + +_TRACE_SUMMARY_SQL = """ +SELECT json_extract(s.value, '$.name') AS operation, + json_extract(s.value, '$.kind') AS kind, + COUNT(*) AS calls, + SUM(CASE WHEN LOWER(COALESCE(json_extract(s.value, '$.status'), '')) IN ('error', 'failed') + OR json_extract(s.value, '$.error') IS NOT NULL + THEN 1 ELSE 0 END) AS errors, + ROUND(AVG(COALESCE(json_extract(s.value, '$.duration_ms'), + (julianday(json_extract(s.value, '$.end_time')) + - julianday(json_extract(s.value, '$.start_time'))) * 86400000)), 1) AS avg_ms, + ROUND(MAX(COALESCE(json_extract(s.value, '$.duration_ms'), + (julianday(json_extract(s.value, '$.end_time')) + - julianday(json_extract(s.value, '$.start_time'))) * 86400000)), 1) AS max_ms, + SUM(COALESCE(json_extract(s.value, '$.usage.input_tokens'), 0)) AS input_tokens, + SUM(COALESCE(json_extract(s.value, '$.usage.output_tokens'), 0)) AS output_tokens, + ROUND(SUM(COALESCE(json_extract(s.value, '$.cost_usd'), 0)), 4) AS cost_usd +FROM traces, json_each(traces.spans) AS s +{where} +GROUP BY operation, kind +ORDER BY cost_usd DESC, calls DESC +""" + + +def _trace_sort_key(sort): + if sort in ("step_asc", "step_desc"): + return lambda trace: trace.get("step") or 0, sort == "step_desc" + return lambda trace: str(trace.get("timestamp") or ""), sort != "request_time_asc" + + +def _project_run_names(args, remote) -> list[str]: + if remote: + records = remote.predict(args.project, api_name="/get_runs_for_project") + return [r["name"] if isinstance(r, dict) else r for r in records] + return SQLiteStorage.get_runs(args.project) + + +def _fetch_traces_for_run(args, remote, run, *, limit, offset, search, sort, step): + if remote: + return remote.predict( + args.project, + run, + None, + search, + sort, + limit, + offset, + step, + api_name="/get_traces", + ) + return SQLiteStorage.get_traces( + args.project, + run, + search=search, + sort=sort, + limit=limit, + offset=offset, + step=step, + ) + + +def _fetch_traces(args, remote, *, limit, offset, search=None, sort=None, step=None): + """Fetch traces for one run, or merge across every run in the project. + + Traces are stored per run, so with no `--run` this merges each run's page + the way the dashboard does, then re-sorts and slices the combined result. + """ + if not remote: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + + run = getattr(args, "run", None) + if run: + return _fetch_traces_for_run( + args, + remote, + run, + limit=limit, + offset=offset, + search=search, + sort=sort, + step=step, + ) + + runs = _project_run_names(args, remote) + if not runs: + return [] + per_run_limit = None if limit is None else limit + max(0, offset) + merged: list[dict] = [] + for name in runs: + merged.extend( + _fetch_traces_for_run( + args, + remote, + name, + limit=per_run_limit, + offset=0, + search=search, + sort=sort, + step=step, + ) + or [] + ) + key, reverse = _trace_sort_key(sort) + merged.sort(key=key, reverse=reverse) + if offset: + merged = merged[offset:] + return merged if limit is None else merged[:limit] + + +def _handle_list_traces(args, remote): + traces = _fetch_traces( + args, + remote, + limit=args.limit, + offset=args.offset, + search=args.search, + sort=args.sort, + step=args.step, + ) + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "traces": [ + {**trace, "summary": trace_rollup(trace)} for trace in traces + ], + } + ) + ) + else: + print(format_traces(traces)) + + +def _handle_get_trace(args, remote): + traces = _fetch_traces(args, remote, limit=None, offset=0) + match = next( + (trace for trace in traces if trace_id_matches(trace, args.trace_id)), None + ) + if match is None: + error_exit( + f"Trace '{args.trace_id}' not found in project '{args.project}'. " + "Use `trackio list traces` to see available trace ids." + ) + if args.json: + print(format_json({**match, "summary": trace_rollup(match)})) + else: + print(format_trace(match)) + + +def _handle_get_trace_summary(args, remote): + where = "" + if args.run: + escaped = args.run.replace("'", "''") + where = f"WHERE traces.run_name = '{escaped}'" + sql = _TRACE_SUMMARY_SQL.format(where=where) + try: + if remote: + result = remote.predict(args.project, sql, api_name="/query_project") + else: + result = SQLiteStorage.query_project(args.project, sql) + except FileNotFoundError as e: + error_exit(str(e)) + except ValueError as e: + error_exit(str(e)) + if args.json: + print(format_json({"project": args.project, "run": args.run, **result})) + else: + print(format_trace_summary(result)) + + +def _handle_query(args): + remote = _get_remote(args) + try: + if remote: + result = remote.predict(args.project, args.sql, api_name="/query_project") + else: + result = SQLiteStorage.query_project(args.project, args.sql) + except FileNotFoundError as e: + error_exit(str(e)) + except ValueError as e: + error_exit(str(e)) + + if args.json: + print(format_json(result)) + else: + print(format_query_result(result)) + + +def _datetime_to_iso(value): + return value.isoformat() if value is not None else None + + +def _space_url(space) -> str | None: + host = getattr(space, "host", None) + if host: + return host if host.startswith("http") else f"https://{host}" + + subdomain = getattr(space, "subdomain", None) + if subdomain: + return f"https://{subdomain}.hf.space" + + space_id = getattr(space, "id", None) + if not space_id: + return None + return f"https://huggingface.co/spaces/{space_id}" + + +def _serialize_space(space) -> dict: + space_id = getattr(space, "id", "") + namespace, _, name = space_id.partition("/") + return { + "id": space_id, + "namespace": namespace, + "name": name or space_id, + "author": getattr(space, "author", None) or namespace, + "private": bool(getattr(space, "private", False)), + "sdk": getattr(space, "sdk", None), + "url": _space_url(space), + "last_modified": _datetime_to_iso( + getattr(space, "last_modified", None) + or getattr(space, "lastModified", None) + ), + "created_at": _datetime_to_iso(getattr(space, "created_at", None)), + "tags": getattr(space, "tags", None) or [], + } + + +def _maybe_handle_logbook_run_argv() -> bool: + argv = sys.argv[1:] + try: + logbook_idx = argv.index("logbook") + except ValueError: + return False + if len(argv) <= logbook_idx + 1 or argv[logbook_idx + 1] != "run": + return False + + run_argv = argv[logbook_idx + 2 :] + + run_parser = argparse.ArgumentParser( + prog=f"{os.path.basename(sys.argv[0])} logbook run", + description=( + "Run a command; log the command, its scripts, and output to a page" + ), + ) + run_parser.add_argument("--page", help="Page title or slug") + run_parser.add_argument("--title", help="Cell title") + run_parser.add_argument( + "--no-artifacts", + action="store_true", + help="Do not record output model/data files as artifact cells", + ) + if "--" in run_argv: + sep = run_argv.index("--") + opts = run_parser.parse_args(run_argv[:sep]) + command = run_argv[sep + 1 :] + else: + opts, command = run_parser.parse_known_args(run_argv) + if not command: + run_parser.error("No command provided. Use: trackio logbook run -- ") + args = argparse.Namespace( + logbook_action="run", + page=opts.page, + title=opts.title, + no_artifacts=opts.no_artifacts, + command=command, + ) + _handle_logbook(args) + return True + + +def _maybe_rewrite_logbook_read_argv() -> None: + argv = sys.argv + for i in range(1, len(argv) - 1): + if argv[i] == "logbook" and argv[i + 1] == "read": + j = i + 2 + if ( + j < len(argv) + and not argv[j].startswith("-") + and argv[j] not in ("pages", "page", "cell") + ): + argv[j : j + 1] = ["--path", argv[j]] + return + + +def _trackio_space_namespaces(api, token: str | None, author: str | None) -> list[str]: + if author: + return [author] + + if not token: + error_exit( + "Log in with `huggingface-cli login`, pass `--hf-token`, or provide `--author`." + ) + + try: + whoami = api.whoami(token=token, cache=True) + except Exception as e: + error_exit(f"Failed to read Hugging Face account information: {e}") + + namespaces = [whoami["name"]] + for org in whoami.get("orgs", []): + org_name = org.get("name") if isinstance(org, dict) else org + if org_name: + namespaces.append(org_name) + + return list(dict.fromkeys(namespaces)) + + +def _handle_list_spaces(args): + if _get_space(args): + error_exit("The 'list spaces' command does not support --space.") + if args.limit is not None and args.limit < 0: + error_exit("--limit must be zero or greater.") + + token = args.hf_token or huggingface_hub.utils.get_token() + api = huggingface_hub.HfApi(token=token) + namespaces = _trackio_space_namespaces(api, token, args.author) + + spaces_by_id = {} + try: + for namespace in namespaces: + spaces = api.list_spaces( + author=namespace, + filter="trackio", + full=True, + token=token, + ) + for space in spaces: + space_id = getattr(space, "id", None) + if space_id: + spaces_by_id[space_id] = _serialize_space(space) + except Exception as e: + error_exit(f"Failed to list Trackio Spaces: {e}") + + spaces = sorted( + spaces_by_id.values(), + key=lambda space: space.get("last_modified") or "", + reverse=True, + ) + if args.limit is not None: + spaces = spaces[: args.limit] + + if args.json: + print(format_json({"spaces": spaces})) + else: + print(format_spaces(spaces)) + + +def _registry_target(value: str) -> tuple[str, str]: + target = value if value.startswith("registry-") else f"registry-{value}" + try: + return parse_collection_target(target) + except ValueError as e: + raise argparse.ArgumentTypeError(str(e)) from e + + +def _collection_version(value: str) -> int: + raw = value[1:] if value.startswith("v") else value + try: + version = int(raw) + except ValueError as e: + raise argparse.ArgumentTypeError( + f"Collection version must be an integer or vN, got {value!r}." + ) from e + if version < 0: + raise argparse.ArgumentTypeError("Collection version cannot be negative.") + return version + + +def _source_artifact(value: str) -> tuple[str, str, str | None]: + if "/" not in value: + raise argparse.ArgumentTypeError( + "Source must be '/[:]'." + ) + project, artifact_spec = value.split("/", 1) + if not project or not artifact_spec: + raise argparse.ArgumentTypeError( + "Source must be '/[:]'." + ) + if ":" in artifact_spec: + artifact, spec = artifact_spec.split(":", 1) + if not artifact or not spec: + raise argparse.ArgumentTypeError( + "Source must be '/[:]'." + ) + else: + artifact, spec = artifact_spec, None + return project, artifact, spec + + +def _collection_record(collection) -> dict: + return { + "name": collection.name, + "type": collection.type, + "description": collection.description, + "created_at": collection.created_at, + "num_links": collection.num_links, + "latest_version": collection.latest_version, + "links": collection.links, + } + + +def _handle_registry(args): + try: + if args.registry_action == "create": + registry = trackio.Api().create_registry( + args.name, + description=args.description, + bucket_id=args.bucket_id, + ) + result = { + "name": registry.name, + "description": registry.description, + "bucket_id": registry.bucket_id, + } + if args.json: + print(format_json(result)) + else: + where = ( + f" in bucket '{registry.bucket_id}'" if registry.bucket_id else "" + ) + print(f"Created registry '{registry.name}'{where}.") + return + + if args.registry_action == "create-collection": + registry_name, collection_name = args.target + registry = trackio.Api().registry(registry_name, bucket_id=args.bucket_id) + collection = registry.create_collection( + collection_name, + artifact_type=args.type, + description=args.description, + ) + result = _collection_record(collection) + if args.json: + print(format_json(result)) + else: + print( + f"Collection '{registry_name}/{collection_name}' " + f"is ready ({collection.type})." + ) + return + + if args.registry_action == "list": + registry = trackio.Api().registry(args.name, bucket_id=args.bucket_id) + collections = [_collection_record(c) for c in registry.collections()] + if args.json: + print( + format_json({"registry": registry.name, "collections": collections}) + ) + else: + print(format_registry_collections(registry.name, collections)) + return + + if args.registry_action == "events": + registry = trackio.Api().registry(args.name, bucket_id=args.bucket_id) + events = registry.events() + if args.json: + print(format_json({"registry": registry.name, "events": events})) + else: + print(format_registry_events(events)) + return + + registry_name, collection_name = args.target + registry = trackio.Api().registry(registry_name, bucket_id=args.bucket_id) + + if args.registry_action == "show": + collection = registry.collection(collection_name) + if collection is None: + error_exit( + f"Collection '{collection_name}' not found in registry " + f"'{registry_name}'." + ) + result = _collection_record(collection) + if args.json: + print(format_json(result)) + else: + print(format_registry_collection(registry_name, result)) + return + + if args.registry_action == "link": + source_project, source_artifact, source_spec = args.source + record = SQLiteStorage.get_artifact_manifest( + source_project, source_artifact, source_spec + ) + if record is None: + suffix = f":{source_spec}" if source_spec else "" + error_exit( + f"Artifact '{source_project}/{source_artifact}{suffix}' " + "not found locally." + ) + result = registry._storage.link_artifact_version( + registry=registry_name, + collection=collection_name, + type=record["type"], + source_project=source_project, + source_artifact=record["name"], + source_version=int(record["version"]), + aliases=args.alias, + ) + if args.json: + print(format_json(result)) + else: + action = "Linked" if result["created"] else "Updated" + print( + f"{action} '{source_project}/{record['name']}:v{record['version']}' " + f"as '{registry_name}/{collection_name}:" + f"v{result['collection_version']}'." + ) + return + + collection = registry.collection(collection_name) + if collection is None: + error_exit( + f"Collection '{collection_name}' not found in registry " + f"'{registry_name}'." + ) + + if args.registry_action == "promote": + if args.alias == "latest": + error_exit( + "The 'latest' alias is managed automatically by the registry." + ) + link = next( + ( + link + for link in collection.links + if link["collection_version"] == args.version + ), + None, + ) + if link is None: + error_exit( + f"Version v{args.version} not found in " + f"'{registry_name}/{collection_name}'." + ) + result = registry._storage.link_artifact_version( + registry=registry_name, + collection=collection_name, + type=collection.type, + source_project=link["source_project"], + source_artifact=link["source_artifact"], + source_version=int(link["source_version"]), + aliases=[args.alias], + source_space_id=link.get("source_space_id"), + source_bucket_id=link.get("source_bucket_id"), + source_server_base_url=link.get("source_server_base_url"), + ) + if args.json: + print(format_json(result)) + else: + print( + f"Promoted '{registry_name}/{collection_name}:v{args.version}' " + f"to alias '{args.alias}'." + ) + return + + if args.registry_action == "unlink": + result = registry._storage.unlink( + registry_name, collection_name, args.version + ) + if args.json: + print(format_json(result)) + else: + print(f"Unlinked '{registry_name}/{collection_name}:v{args.version}'.") + return + + except ValueError as e: + error_exit(str(e)) + + +def main(): + if _maybe_handle_logbook_run_argv(): + return + _maybe_rewrite_logbook_read_argv() + + parser = argparse.ArgumentParser(description="Trackio CLI") + parser.add_argument( + "--version", + action="version", + version=f"trackio {trackio.__version__}", + ) + parser.add_argument( + "--space", + required=False, + help="HF Space ID (e.g. 'user/space') or Space URL to query remotely.", + ) + parser.add_argument( + "--hf-token", + required=False, + help="HF token for accessing private Spaces.", + ) + subparsers = parser.add_subparsers(dest="command") + + ui_parser = subparsers.add_parser("show", help="Show a Trackio dashboard") + ui_parser.add_argument( + "dashboard", + nargs="?", + choices=["registry"], + help="Dashboard to show. Omit for the project dashboard.", + ) + ui_parser.add_argument( + "--project", required=False, help="Project name to show in the dashboard" + ) + ui_parser.add_argument( + "--theme", + required=False, + default="default", + help="A Gradio Theme to use for the dashboard instead of the default, can be a built-in theme (e.g. 'soft', 'citrus'), or a theme from the Hub (e.g. 'gstaff/xkcd').", + ) + ui_parser.add_argument( + "--mcp-server", + action="store_true", + help="Enable MCP server functionality. The Trackio dashboard will be set up as an MCP server and certain functions will be exposed as MCP tools.", + ) + ui_parser.add_argument( + "--footer", + action="store_true", + default=True, + help="Show the Gradio footer. Use --no-footer to hide it.", + ) + ui_parser.add_argument( + "--no-footer", + dest="footer", + action="store_false", + help="Hide the Gradio footer.", + ) + ui_parser.add_argument( + "--color-palette", + required=False, + help="Comma-separated list of hex color codes for plot lines (e.g. '#FF0000,#00FF00,#0000FF'). If not provided, the TRACKIO_COLOR_PALETTE environment variable will be used, or the default palette if not set.", + ) + ui_parser.add_argument( + "--host", + required=False, + help="Host to bind the server to (e.g. '0.0.0.0' for remote access). If not provided, defaults to '127.0.0.1' (localhost only).", + ) + ui_parser.add_argument( + "--frontend", + required=False, + help="Custom frontend directory to serve. Must contain index.html.", + ) + + subparsers.add_parser( + "status", + help="Show the status of all local Trackio projects, including sync status.", + ) + + sync_parser = subparsers.add_parser( + "sync", + help="Sync a local project's database to a Hugging Face Space. If the Space does not exist, it will be created.", + ) + sync_parser.add_argument( + "--project", + required=False, + help="The name of the local project.", + ) + sync_parser.add_argument( + "--space-id", + required=False, + help="The Hugging Face Space ID where the project will be synced (e.g. username/space_id). If not provided, uses the previously-configured Space.", + ) + sync_parser.add_argument( + "--all", + action="store_true", + dest="sync_all", + help="Sync all projects that have unsynced data to their configured Spaces.", + ) + sync_parser.add_argument( + "--private", + action="store_true", + help="Make the Hugging Face Space private if creating a new Space. By default, the repo will be public unless the organization's default is private. This value is ignored if the repo already exists.", + ) + sync_parser.add_argument( + "--force", + action="store_true", + help="Overwrite the existing database without prompting for confirmation.", + ) + sync_parser.add_argument( + "--sdk", + choices=["gradio", "static"], + default="gradio", + help="The type of Space to deploy. 'gradio' (default) deploys a live Gradio server. 'static' deploys a static Space that reads from an HF Bucket.", + ) + sync_parser.add_argument( + "--frontend", + required=False, + help="Custom frontend directory to deploy. Must contain index.html.", + ) + + freeze_parser = subparsers.add_parser( + "freeze", + help="Create a one-time static Space snapshot from a project's data.", + ) + freeze_parser.add_argument( + "--space-id", + required=True, + help="The source Gradio Space ID (e.g. username/space_id).", + ) + freeze_parser.add_argument( + "--project", + required=True, + help="The name of the project to freeze into a static snapshot.", + ) + freeze_parser.add_argument( + "--new-space-id", + required=False, + help="The Space ID for the new static Space. Defaults to {space_id}_static.", + ) + freeze_parser.add_argument( + "--private", + action="store_true", + help="Make the new static Space private.", + ) + freeze_parser.add_argument( + "--frontend", + required=False, + help="Custom frontend directory to deploy to the frozen static Space.", + ) + + bump_parser = subparsers.add_parser( + "bump", + help="Upgrade a Trackio Space to the local Trackio version and migrate its data.", + ) + bump_parser.add_argument( + "space_id", + nargs="?", + help="The Trackio Space to upgrade (e.g. username/space_id). Can also be passed as --space.", + ) + bump_parser.add_argument( + "--space", + dest="space", + default=argparse.SUPPRESS, + help="The Trackio Space to upgrade, as an alternative to the positional argument.", + ) + bump_parser.add_argument( + "--new-space", + required=False, + help="Leave the Space untouched and create this Space from a copy of its data.", + ) + bump_parser.add_argument( + "--new-bucket", + required=False, + help="The bucket for --new-space. Defaults to {new_space}-bucket.", + ) + + config_parser = subparsers.add_parser( + "config", + help="Manage persistent Trackio configuration.", + ) + config_subparsers = config_parser.add_subparsers( + dest="config_command", + required=True, + ) + config_subparsers.add_parser("get", help="Show current Trackio config.") + config_set_parser = config_subparsers.add_parser( + "set", + help="Set a persistent Trackio config value.", + ) + config_set_parser.add_argument( + "key", + choices=["frontend"], + help="Config key to set.", + ) + config_set_parser.add_argument( + "frontend", + help="Frontend directory to persist.", + ) + config_unset_parser = config_subparsers.add_parser( + "unset", + help="Unset a persistent Trackio config value.", + ) + config_unset_parser.add_argument( + "key", + choices=["frontend"], + help="Config key to unset.", + ) + + list_parser = subparsers.add_parser( + "list", + help="List projects, runs, or metrics", + ) + list_subparsers = list_parser.add_subparsers(dest="list_type", required=True) + + list_projects_parser = list_subparsers.add_parser( + "projects", + help="List all projects", + ) + list_projects_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + list_spaces_parser = list_subparsers.add_parser( + "spaces", + help="List Trackio Spaces for your Hugging Face account and organizations", + ) + list_spaces_parser.add_argument( + "--author", + required=False, + help="Only list Trackio Spaces under this user or organization namespace.", + ) + list_spaces_parser.add_argument( + "--limit", + type=int, + required=False, + help="Maximum number of Spaces to return.", + ) + list_spaces_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + list_runs_parser = list_subparsers.add_parser( + "runs", + help="List runs for a project", + ) + list_runs_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + list_runs_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + list_metrics_parser = list_subparsers.add_parser( + "metrics", + help="List metrics for a run", + ) + list_metrics_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + list_metrics_parser.add_argument( + "--run", + required=True, + help="Run name", + ) + list_metrics_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + list_system_metrics_parser = list_subparsers.add_parser( + "system-metrics", + help="List system metrics for a run", + ) + list_system_metrics_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + list_system_metrics_parser.add_argument( + "--run", + required=True, + help="Run name", + ) + list_system_metrics_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + list_alerts_parser = list_subparsers.add_parser( + "alerts", + help="List alerts for a project or run", + ) + list_alerts_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + list_alerts_parser.add_argument( + "--run", + required=False, + help="Run name (optional)", + ) + list_alerts_parser.add_argument( + "--level", + required=False, + help="Filter by alert level (info, warn, error)", + ) + list_alerts_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + list_alerts_parser.add_argument( + "--since", + required=False, + help="Only show alerts after this ISO 8601 timestamp", + ) + + list_traces_parser = list_subparsers.add_parser( + "traces", + help="List agent/conversation traces for a project or run", + ) + list_traces_parser.add_argument("--project", required=True, help="Project name") + list_traces_parser.add_argument("--run", required=False, help="Run name (optional)") + list_traces_parser.add_argument( + "--search", + required=False, + help="Only show traces matching this text (messages, metadata, span names)", + ) + list_traces_parser.add_argument( + "--step", required=False, type=int, help="Only show traces at this step" + ) + list_traces_parser.add_argument( + "--sort", + required=False, + choices=sorted(TRACE_SORTS), + default="request_time_desc", + help="Sort order (default: request_time_desc)", + ) + list_traces_parser.add_argument( + "--limit", required=False, type=int, default=50, help="Maximum traces to return" + ) + list_traces_parser.add_argument( + "--offset", required=False, type=int, default=0, help="Traces to skip" + ) + list_traces_parser.add_argument( + "--json", action="store_true", help="Output in JSON format" + ) + + list_reports_parser = list_subparsers.add_parser( + "reports", + help="List markdown reports for a project or run", + ) + list_reports_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + list_reports_parser.add_argument( + "--run", + required=False, + help="Run name (optional)", + ) + list_reports_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + list_artifacts_parser = list_subparsers.add_parser( + "artifacts", + help="List artifacts for a project", + ) + list_artifacts_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + list_artifacts_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + get_parser = subparsers.add_parser( + "get", + help="Get project, run, or metric information", + ) + get_subparsers = get_parser.add_subparsers(dest="get_type", required=True) + + get_project_parser = get_subparsers.add_parser( + "project", + help="Get project summary", + ) + get_project_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + get_project_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + get_run_parser = get_subparsers.add_parser( + "run", + help="Get run summary", + ) + get_run_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + get_run_parser.add_argument( + "--run", + required=True, + help="Run name", + ) + get_run_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + get_artifact_parser = get_subparsers.add_parser( + "artifact", + help="Get an artifact version (manifest, aliases, metadata)", + ) + get_artifact_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + get_artifact_parser.add_argument( + "--name", + required=True, + help="Artifact name", + ) + get_artifact_parser.add_argument( + "--version", + required=False, + help="Version or alias to resolve (e.g. 'v2' or 'best'). Defaults to latest.", + ) + get_artifact_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + get_metric_parser = get_subparsers.add_parser( + "metric", + help="Get metric values for a run", + ) + get_metric_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + get_metric_parser.add_argument( + "--run", + required=True, + help="Run name", + ) + get_metric_parser.add_argument( + "--metric", + required=True, + help="Metric name", + ) + get_metric_parser.add_argument( + "--step", + type=int, + required=False, + help="Get metric at exactly this step", + ) + get_metric_parser.add_argument( + "--around", + type=int, + required=False, + help="Get metrics around this step (use with --window)", + ) + get_metric_parser.add_argument( + "--at-time", + required=False, + help="Get metrics around this ISO 8601 timestamp (use with --window)", + ) + get_metric_parser.add_argument( + "--window", + type=int, + required=False, + default=10, + help="Window size: ±steps for --around, ±seconds for --at-time (default: 10)", + ) + get_metric_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + get_snapshot_parser = get_subparsers.add_parser( + "snapshot", + help="Get all metrics at/around a step or timestamp", + ) + get_snapshot_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + get_snapshot_parser.add_argument( + "--run", + required=True, + help="Run name", + ) + get_snapshot_parser.add_argument( + "--step", + type=int, + required=False, + help="Get all metrics at exactly this step", + ) + get_snapshot_parser.add_argument( + "--around", + type=int, + required=False, + help="Get all metrics around this step (use with --window)", + ) + get_snapshot_parser.add_argument( + "--at-time", + required=False, + help="Get all metrics around this ISO 8601 timestamp (use with --window)", + ) + get_snapshot_parser.add_argument( + "--window", + type=int, + required=False, + default=10, + help="Window size: ±steps for --around, ±seconds for --at-time (default: 10)", + ) + get_snapshot_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + get_system_metric_parser = get_subparsers.add_parser( + "system-metric", + help="Get system metric values for a run", + ) + get_system_metric_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + get_system_metric_parser.add_argument( + "--run", + required=True, + help="Run name", + ) + get_system_metric_parser.add_argument( + "--metric", + required=False, + help="System metric name (optional, if not provided returns all system metrics)", + ) + get_system_metric_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + get_trace_parser = get_subparsers.add_parser( + "trace", + help="Get one trace with its full execution span tree", + ) + get_trace_parser.add_argument("--project", required=True, help="Project name") + get_trace_parser.add_argument( + "--trace-id", required=True, help="Trace id (from `trackio list traces`)" + ) + get_trace_parser.add_argument("--run", required=False, help="Run name (optional)") + get_trace_parser.add_argument( + "--json", action="store_true", help="Output in JSON format" + ) + + get_trace_summary_parser = get_subparsers.add_parser( + "trace-summary", + help="Aggregate trace spans by operation (calls, errors, latency, tokens, cost)", + ) + get_trace_summary_parser.add_argument( + "--project", required=True, help="Project name" + ) + get_trace_summary_parser.add_argument( + "--run", required=False, help="Run name (optional)" + ) + get_trace_summary_parser.add_argument( + "--json", action="store_true", help="Output in JSON format" + ) + + get_alerts_parser = get_subparsers.add_parser( + "alerts", + help="Get alerts for a project or run", + ) + get_alerts_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + get_alerts_parser.add_argument( + "--run", + required=False, + help="Run name (optional)", + ) + get_alerts_parser.add_argument( + "--level", + required=False, + help="Filter by alert level (info, warn, error)", + ) + get_alerts_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + get_alerts_parser.add_argument( + "--since", + required=False, + help="Only show alerts after this ISO 8601 timestamp", + ) + + get_report_parser = get_subparsers.add_parser( + "report", + help="Get markdown report entries for a run", + ) + get_report_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + get_report_parser.add_argument( + "--run", + required=True, + help="Run name", + ) + get_report_parser.add_argument( + "--report", + required=True, + help="Report metric name", + ) + get_report_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + query_parser = subparsers.add_parser( + "query", + help="Run a read-only SQL query against a project database", + ) + query_subparsers = query_parser.add_subparsers(dest="query_type", required=True) + query_project_parser = query_subparsers.add_parser( + "project", + help="Run a read-only SQL query against a project's SQLite database", + ) + query_project_parser.add_argument( + "--project", + required=True, + help="Project name", + ) + query_project_parser.add_argument( + "--sql", + required=True, + help="Read-only SQL query to execute", + ) + query_project_parser.add_argument( + "--json", + action="store_true", + help="Output in JSON format", + ) + + skills_parser = subparsers.add_parser( + "skills", + help="Manage Trackio skills for AI coding assistants", + ) + skills_subparsers = skills_parser.add_subparsers( + dest="skills_action", required=True + ) + skills_add_parser = skills_subparsers.add_parser( + "add", + help=( + "Install the Trackio skill to the central .agents/skills location; " + "pass agent flags to also symlink it for specific assistants" + ), + ) + skills_add_parser.add_argument( + "--cursor", + action="store_true", + help="Install for Cursor", + ) + skills_add_parser.add_argument( + "--claude", + action="store_true", + help="Install for Claude Code", + ) + skills_add_parser.add_argument( + "--codex", + action="store_true", + help="Install for Codex", + ) + skills_add_parser.add_argument( + "--opencode", + action="store_true", + help="Install for OpenCode", + ) + skills_add_parser.add_argument( + "--pi", + action="store_true", + help="Install for pi", + ) + skills_add_parser.add_argument( + "--global", + dest="global_", + action="store_true", + help="Install globally (user-level) instead of in the current project directory", + ) + skills_add_parser.add_argument( + "--dest", + type=str, + required=False, + help="Install into a custom destination (path to skills directory)", + ) + skills_add_parser.add_argument( + "--force", + action="store_true", + help="Overwrite existing skill if it already exists", + ) + skills_add_parser.add_argument( + "--no-command", + dest="no_command", + action="store_true", + help="Do not install the /logbook slash command alongside the skill", + ) + skills_add_parser.add_argument( + "--no-hook", + dest="no_hook", + action="store_true", + help="Do not install the Claude Code hook that mirrors todos into the logbook", + ) + + registry_parser = subparsers.add_parser( + "registry", + help="Create and manage artifact registries", + ) + registry_subparsers = registry_parser.add_subparsers( + dest="registry_action", required=True + ) + + def add_registry_output_options(command_parser): + command_parser.add_argument( + "--bucket-id", + help="HF bucket holding the registry. Omit for a local registry.", + ) + command_parser.add_argument( + "--json", action="store_true", help="Output in JSON format" + ) + + registry_create = registry_subparsers.add_parser("create", help="Create a registry") + registry_create.add_argument("name", help="Registry name") + registry_create.add_argument("--description", help="Registry description") + add_registry_output_options(registry_create) + + registry_create_collection = registry_subparsers.add_parser( + "create-collection", help="Create a typed collection" + ) + registry_create_collection.add_argument( + "target", + type=_registry_target, + help="Registry collection as registry/collection", + ) + registry_create_collection.add_argument( + "--type", required=True, help="Artifact type accepted by the collection" + ) + registry_create_collection.add_argument( + "--description", help="Collection description" + ) + add_registry_output_options(registry_create_collection) + + registry_list = registry_subparsers.add_parser( + "list", help="List collections in a registry" + ) + registry_list.add_argument("name", help="Registry name") + add_registry_output_options(registry_list) + + registry_show = registry_subparsers.add_parser( + "show", help="Show a collection and its linked versions" + ) + registry_show.add_argument( + "target", + type=_registry_target, + help="Registry collection as registry/collection", + ) + add_registry_output_options(registry_show) + + registry_link = registry_subparsers.add_parser( + "link", help="Link a local artifact version into a collection" + ) + registry_link.add_argument( + "target", + type=_registry_target, + help="Registry collection as registry/collection", + ) + registry_link.add_argument( + "source", + type=_source_artifact, + help="Local source as project/artifact[:version-or-alias]", + ) + registry_link.add_argument( + "--alias", action="append", default=[], help="Alias to assign; repeatable" + ) + add_registry_output_options(registry_link) + + registry_promote = registry_subparsers.add_parser( + "promote", help="Move an alias onto a collection version" + ) + registry_promote.add_argument( + "target", + type=_registry_target, + help="Registry collection as registry/collection", + ) + registry_promote.add_argument("alias", help="Alias to move") + registry_promote.add_argument( + "version", type=_collection_version, help="Collection version (vN or N)" + ) + add_registry_output_options(registry_promote) + + registry_unlink = registry_subparsers.add_parser( + "unlink", help="Remove a linked collection version" + ) + registry_unlink.add_argument( + "target", + type=_registry_target, + help="Registry collection as registry/collection", + ) + registry_unlink.add_argument( + "version", type=_collection_version, help="Collection version (vN or N)" + ) + add_registry_output_options(registry_unlink) + + registry_events = registry_subparsers.add_parser( + "events", help="Show the registry audit log" + ) + registry_events.add_argument("name", help="Registry name") + add_registry_output_options(registry_events) + + logbook_parser = subparsers.add_parser( + "logbook", + help="Create and publish a shareable experiment logbook", + ) + logbook_sub = logbook_parser.add_subparsers( + dest="logbook_action", + required=True, + metavar="{open,cell,run,page,attach,remove,read,serve,publish,pin,sync,sync-todos}", + ) + + lb_open = logbook_sub.add_parser( + "open", help="Start or attach to the logbook in this directory" + ) + lb_open.add_argument( + "space_id", nargs="?", help="Optional HF Space id to publish to" + ) + lb_open.add_argument("--title", help="Logbook title (only when creating)") + lb_open.add_argument( + "--no-serve", + action="store_true", + help="Open or attach without launching the local logbook preview", + ) + lb_open.add_argument("--port", type=int, default=7861) + lb_open.add_argument("--no-browser", action="store_true") + + lb_cell = logbook_sub.add_parser( + "cell", help="Append a typed notebook-style cell to a logbook page" + ) + lb_cell_sub = lb_cell.add_subparsers(dest="cell_type", required=True) + + lb_cell_md = lb_cell_sub.add_parser("markdown", help="Append a markdown cell") + lb_cell_md.add_argument( + "body", + help="Markdown body (literal \\n escape sequences are converted to line breaks)", + ) + lb_cell_md.add_argument("--title", help="Cell title") + lb_cell_md.add_argument("--page", help="Page title or slug") + lb_cell_art = lb_cell_sub.add_parser( + "artifact", help="Append an artifact cell referencing a Trackio artifact" + ) + lb_cell_art.add_argument("name", help="Artifact reference project/name:vN") + lb_cell_art.add_argument("--title", help="Cell title") + lb_cell_art.add_argument("--page", help="Page title or slug") + lb_cell_art.add_argument( + "--type", + dest="artifact_type", + help='Artifact type, e.g. "dataset" or "model"', + ) + + lb_cell_code = lb_cell_sub.add_parser("code", help="Append a code cell") + lb_cell_code.add_argument("--title", help="Cell title") + lb_cell_code.add_argument("--page", help="Page title or slug") + lb_cell_code.add_argument( + "--code", action="append", default=[], help="Path to code/config to include" + ) + lb_cell_code.add_argument("--code-text", help="Inline code to include") + lb_cell_code.add_argument("--language", help="Language for --code-text") + lb_cell_code.add_argument( + "--output", + default="", + help="Output text (optional; omit for a command-only code cell)", + ) + + lb_cell_figure = lb_cell_sub.add_parser("figure", help="Append a figure cell") + lb_cell_figure.add_argument("--title", help="Cell title") + lb_cell_figure.add_argument("--page", help="Page title or slug") + lb_cell_figure.add_argument( + "--html", help="Path to an HTML or image file, or inline HTML text" + ) + lb_cell_figure.add_argument("--html-text", help="Inline HTML text") + lb_cell_figure.add_argument( + "--image", help="Path to an image file (PNG, JPG, GIF, WEBP, SVG, ...)" + ) + lb_cell_figure.add_argument("--raw", help="Path/URL/text for raw data") + lb_cell_figure.add_argument("--raw-text", help="Inline raw data") + lb_cell_figure.add_argument( + "--inline-plotlyjs", + action="store_true", + help=( + "Embed the full Plotly.js library in the page (can be several MB). " + "By default an inlined Plotly.js bundle is rewritten to a CDN " + "reference to keep pages small." + ), + ) + + lb_cell_dash = lb_cell_sub.add_parser( + "dashboard", help="Embed a Trackio dashboard for a project" + ) + lb_cell_dash.add_argument("project", help="Trackio project name") + lb_cell_dash.add_argument( + "--space", + dest="space_id", + help="HF Space id (owner/name) hosting the dashboard", + ) + lb_cell_dash.add_argument("--title", help="Cell title") + lb_cell_dash.add_argument("--page", help="Page title or slug") + + lb_cell_remove = lb_cell_sub.add_parser( + "remove", help="Remove a cell from a page by its cell id" + ) + lb_cell_remove.add_argument("cell_id", help="Cell id to remove") + lb_cell_remove.add_argument("--page", help="Page title or slug to scope the search") + + lb_run = logbook_sub.add_parser( + "run", + help="Run a command; log the command, its scripts, and output to a page", + ) + lb_run.add_argument("--page", help="Page title or slug") + lb_run.add_argument("--title", help="Cell title") + lb_run.add_argument( + "--no-artifacts", + action="store_true", + help="Do not record output model/data files as artifact cells", + ) + lb_run.add_argument("command", nargs="*") + + lb_page = logbook_sub.add_parser( + "page", help="Create or select a page and make it the default target" + ) + lb_page.add_argument("title", help="Page title") + + lb_attach = logbook_sub.add_parser( + "attach", help="Attach external data to this logbook" + ) + lb_attach_sub = lb_attach.add_subparsers(dest="attach_type", required=True) + lb_attach_trace = lb_attach_sub.add_parser( + "trace", help="Attach an agent session JSON or JSONL trace" + ) + lb_attach_trace.add_argument("filepath", help="Path to the agent session file") + lb_attach_trace.add_argument("--title", help="Display title for this session") + lb_attach_trace.add_argument( + "--no-scrub", + action="store_true", + help=( + "Do not scrub secrets from the trace before storing it. By default " + "tokens, keys and passwords are redacted." + ), + ) + + lb_remove = logbook_sub.add_parser( + "remove", help="Remove attached data from this logbook" + ) + lb_remove_sub = lb_remove.add_subparsers(dest="remove_type", required=True) + lb_remove_trace = lb_remove_sub.add_parser( + "trace", help="Remove an attached agent session" + ) + lb_remove_trace.add_argument("session_id", help="Attached session id") + + lb_read = logbook_sub.add_parser( + "read", help="Read logbook pages/cells in an agent-friendly form" + ) + lb_read.add_argument( + "--path", + help=( + "Logbook to read: local path, HF Space id, or URL " + "(can also be passed positionally: trackio logbook read )" + ), + ) + lb_read.add_argument("--json", action="store_true", help="Output JSON") + lb_read.add_argument( + "--head", + type=int, + default=None, + help="Lines of code shown per code cell (default 3; 0 hides code)", + ) + lb_read.add_argument( + "--tail", + type=int, + default=None, + help="Lines of output shown per code cell (default 3; 0 hides output)", + ) + lb_read.add_argument( + "--raw-limit", + type=int, + default=None, + help="Inline figure raw data up to this many chars (default 500; 0 disables)", + ) + lb_read_sub = lb_read.add_subparsers(dest="read_target") + lb_read_pages = lb_read_sub.add_parser("pages", help="List logbook pages") + lb_read_pages.add_argument("--json", action="store_true", help="Output JSON") + lb_read_page = lb_read_sub.add_parser("page", help="Read a page for agents") + lb_read_page.add_argument("page", nargs="?", help="Page title or slug") + lb_read_page.add_argument("--json", action="store_true", help="Output JSON") + lb_read_page.add_argument( + "--head", type=int, default=argparse.SUPPRESS, help="Code lines per code cell" + ) + lb_read_page.add_argument( + "--tail", type=int, default=argparse.SUPPRESS, help="Output lines per code cell" + ) + lb_read_page.add_argument( + "--raw-limit", + type=int, + default=argparse.SUPPRESS, + help="Inline figure raw data up to this many chars", + ) + lb_read_cell = lb_read_sub.add_parser("cell", help="Read one cell by id") + lb_read_cell.add_argument("cell_id", help="Cell id") + lb_read_cell.add_argument("--json", action="store_true", help="Output JSON") + lb_read_cell.add_argument("--full", action="store_true", help="Include full body") + lb_read_cell.add_argument( + "--raw", action="store_true", help="Include figure raw data" + ) + lb_read_cell.add_argument("--html", action="store_true", help="Include figure HTML") + + lb_serve = logbook_sub.add_parser("serve", help="Preview the logbook locally") + lb_serve.add_argument("path", nargs="?", help="Logbook workspace/path to serve") + lb_serve.add_argument("--port", type=int, default=7861) + lb_serve.add_argument("--no-browser", action="store_true") + + lb_pub = logbook_sub.add_parser( + "publish", help="Publish the current logbook state to Hugging Face" + ) + lb_pub.add_argument("space_id", nargs="?", help="HF Space id (username/space)") + lb_pub.add_argument( + "--private", + action="store_true", + help="Make the published logbook Space itself private.", + ) + lb_pub.add_argument( + "--public", + action="store_true", + help=( + "Publish the trace Dataset and artifacts Bucket as PUBLIC (they are " + "private by default) and embed trace/workspace content inline in the " + "static Space. By default the Space stores references only." + ), + ) + + lb_pin = logbook_sub.add_parser( + "pin", + help="Pin (or unpin) a cell so it surfaces on the logbook intro", + ) + lb_pin.add_argument( + "cell_id", + nargs="?", + help="Cell id to pin (default: the most recent cell on the target page)", + ) + lb_pin.add_argument( + "--page", + help="Page title or slug to scope the search / pick the last cell from", + ) + lb_pin.add_argument( + "--unpin", action="store_true", help="Unpin the cell instead of pinning it" + ) + + logbook_sub.add_parser( + "sync", + help="Regenerate the logbook site files from the current page sources", + ) + + logbook_sub.add_parser("sync-todos") + + args, unknown_args = parser.parse_known_args() + if unknown_args: + trailing_global_parser = argparse.ArgumentParser(add_help=False) + trailing_global_parser.add_argument("--space", required=False) + trailing_global_parser.add_argument("--hf-token", required=False) + trailing_globals, remaining_unknown = trailing_global_parser.parse_known_args( + unknown_args + ) + if remaining_unknown: + parser.error(f"unrecognized arguments: {' '.join(remaining_unknown)}") + if trailing_globals.space is not None: + args.space = trailing_globals.space + if trailing_globals.hf_token is not None: + args.hf_token = trailing_globals.hf_token + + if args.command in ( + "show", + "status", + "sync", + "freeze", + "skills", + "registry", + ) and _get_space(args): + error_exit( + f"The '{args.command}' command does not support --space (remote mode)." + ) + + if args.command == "show": + if args.dashboard == "registry" and args.project: + parser.error("trackio show registry does not accept --project") + color_palette = None + if args.color_palette: + color_palette = [color.strip() for color in args.color_palette.split(",")] + show( + project=args.project, + theme=args.theme, + mcp_server=args.mcp_server, + footer=args.footer, + color_palette=color_palette, + host=args.host, + frontend_dir=args.frontend, + dashboard=args.dashboard, + ) + elif args.command == "status": + _handle_status() + elif args.command == "sync": + _handle_sync(args) + elif args.command == "freeze": + freeze( + space_id=args.space_id, + project=args.project, + new_space_id=args.new_space_id, + private=args.private, + frontend_dir=args.frontend, + ) + elif args.command == "bump": + space_id = args.space_id or _get_space(args) + if not space_id: + parser.error( + "trackio bump requires a Space ID, e.g. trackio bump user/space" + ) + if args.space_id and _get_space(args) and args.space_id != _get_space(args): + parser.error( + "Pass the Space ID either as an argument or with --space, not both" + ) + try: + bump( + space_id, + new_space_id=args.new_space, + new_bucket_id=args.new_bucket, + ) + except BumpError as e: + error_exit(str(e)) + elif args.command == "config": + _handle_config(args) + elif args.command == "list": + if args.list_type == "spaces": + _handle_list_spaces(args) + return + + remote = _get_remote(args) + if args.list_type == "projects": + if remote: + projects = remote.predict(api_name="/get_all_projects") + else: + projects = SQLiteStorage.get_projects() + if args.json: + print(format_json({"projects": projects})) + else: + print(format_list(projects, "Projects")) + elif args.list_type == "runs": + if remote: + run_records = remote.predict( + args.project, api_name="/get_runs_for_project" + ) + runs = [r["name"] if isinstance(r, dict) else r for r in run_records] + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + runs = SQLiteStorage.get_runs(args.project) + if args.json: + print(format_json({"project": args.project, "runs": runs})) + else: + print(format_list(runs, f"Runs in '{args.project}'")) + elif args.list_type == "metrics": + if remote: + metrics = remote.predict( + args.project, args.run, api_name="/get_metrics_for_run" + ) + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + runs = SQLiteStorage.get_runs(args.project) + if args.run not in runs: + error_exit( + f"Run '{args.run}' not found in project '{args.project}'." + ) + metrics = SQLiteStorage.get_all_metrics_for_run(args.project, args.run) + if args.json: + print( + format_json( + {"project": args.project, "run": args.run, "metrics": metrics} + ) + ) + else: + print( + format_list( + metrics, f"Metrics for '{args.run}' in '{args.project}'" + ) + ) + elif args.list_type == "system-metrics": + if remote: + system_metrics = remote.predict( + args.project, args.run, api_name="/get_system_metrics_for_run" + ) + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + runs = SQLiteStorage.get_runs(args.project) + if args.run not in runs: + error_exit( + f"Run '{args.run}' not found in project '{args.project}'." + ) + system_metrics = SQLiteStorage.get_all_system_metrics_for_run( + args.project, args.run + ) + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "system_metrics": system_metrics, + } + ) + ) + else: + print(format_system_metric_names(system_metrics)) + elif args.list_type == "alerts": + if remote: + alerts = remote.predict( + args.project, + args.run, + args.level, + args.since, + api_name="/get_alerts", + ) + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + alerts = SQLiteStorage.get_alerts( + args.project, + run_name=args.run, + level=args.level, + since=args.since, + ) + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "level": args.level, + "since": args.since, + "alerts": alerts, + } + ) + ) + else: + print(format_alerts(alerts)) + elif args.list_type == "traces": + _handle_list_traces(args, remote) + elif args.list_type == "reports": + if remote: + run_records = remote.predict( + args.project, api_name="/get_runs_for_project" + ) + runs = [r["name"] if isinstance(r, dict) else r for r in run_records] + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + runs = SQLiteStorage.get_runs(args.project) + if args.run and args.run not in runs: + error_exit(f"Run '{args.run}' not found in project '{args.project}'.") + + target_runs = [args.run] if args.run else runs + all_reports = [] + for run_name in target_runs: + if remote: + logs = remote.predict(args.project, run_name, api_name="/get_logs") + else: + logs = SQLiteStorage.get_logs(args.project, run_name) + all_reports.extend(_extract_reports(run_name, logs)) + + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "reports": all_reports, + } + ) + ) + else: + report_lines = [ + f"{entry['run']} | {entry['report']} | step={entry['step']} | {entry['timestamp']}" + for entry in all_reports + ] + if args.run: + print( + format_list( + report_lines, + f"Reports for '{args.run}' in '{args.project}'", + ) + ) + else: + print(format_list(report_lines, f"Reports in '{args.project}'")) + elif args.list_type == "artifacts": + if remote: + artifacts = remote.predict(args.project, api_name="/get_artifacts") + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + artifacts = SQLiteStorage.get_artifacts(args.project) + if args.json: + print(format_json({"project": args.project, "artifacts": artifacts})) + else: + print(format_artifacts(artifacts, args.project)) + elif args.command == "get": + remote = _get_remote(args) + if args.get_type == "artifact": + if remote: + record = remote.predict( + args.project, + args.name, + args.version, + api_name="/get_artifact_manifest", + ) + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + record = SQLiteStorage.get_artifact_manifest( + args.project, args.name, args.version + ) + if record is None: + spec = f":{args.version}" if args.version else "" + error_exit( + f"Artifact '{args.name}{spec}' not found in project " + f"'{args.project}'." + ) + if args.json: + print(format_json(record)) + else: + print(format_artifact(record)) + elif args.get_type == "project": + if remote: + summary = remote.predict(args.project, api_name="/get_project_summary") + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + summary = get_project_summary(args.project) + if args.json: + print(format_json(summary)) + else: + print(format_project_summary(summary)) + elif args.get_type == "run": + if remote: + summary = remote.predict( + args.project, args.run, api_name="/get_run_summary" + ) + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + runs = SQLiteStorage.get_runs(args.project) + if args.run not in runs: + error_exit( + f"Run '{args.run}' not found in project '{args.project}'." + ) + summary = get_run_summary(args.project, args.run) + if args.json: + print(format_json(summary)) + else: + print(format_run_summary(summary)) + elif args.get_type == "metric": + at_time = getattr(args, "at_time", None) + if remote: + values = remote.predict( + args.project, + args.run, + args.metric, + args.step, + args.around, + at_time, + args.window, + api_name="/get_metric_values", + ) + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + runs = SQLiteStorage.get_runs(args.project) + if args.run not in runs: + error_exit( + f"Run '{args.run}' not found in project '{args.project}'." + ) + metrics = SQLiteStorage.get_all_metrics_for_run(args.project, args.run) + if args.metric not in metrics: + error_exit( + f"Metric '{args.metric}' not found in run '{args.run}' of project '{args.project}'." + ) + values = SQLiteStorage.get_metric_values( + args.project, + args.run, + args.metric, + step=args.step, + around_step=args.around, + at_time=at_time, + window=args.window, + ) + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "metric": args.metric, + "values": values, + } + ) + ) + else: + print(format_metric_values(values)) + elif args.get_type == "snapshot": + if not args.step and not args.around and not getattr(args, "at_time", None): + error_exit( + "Provide --step, --around (with --window), or --at-time (with --window)." + ) + at_time = getattr(args, "at_time", None) + if remote: + snapshot = remote.predict( + args.project, + args.run, + args.step, + args.around, + at_time, + args.window, + api_name="/get_snapshot", + ) + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + runs = SQLiteStorage.get_runs(args.project) + if args.run not in runs: + error_exit( + f"Run '{args.run}' not found in project '{args.project}'." + ) + snapshot = SQLiteStorage.get_snapshot( + args.project, + args.run, + step=args.step, + around_step=args.around, + at_time=at_time, + window=args.window, + ) + if args.json: + result = { + "project": args.project, + "run": args.run, + "metrics": snapshot, + } + if args.step is not None: + result["step"] = args.step + if args.around is not None: + result["around"] = args.around + result["window"] = args.window + if at_time is not None: + result["at_time"] = at_time + result["window"] = args.window + print(format_json(result)) + else: + print(format_snapshot(snapshot)) + elif args.get_type == "system-metric": + if remote: + system_metrics = remote.predict( + args.project, args.run, api_name="/get_system_logs" + ) + if args.metric: + all_system_metric_names = remote.predict( + args.project, + args.run, + api_name="/get_system_metrics_for_run", + ) + if args.metric not in all_system_metric_names: + error_exit( + f"System metric '{args.metric}' not found in run '{args.run}' of project '{args.project}'." + ) + filtered_metrics = [ + { + k: v + for k, v in entry.items() + if k == "timestamp" or k == args.metric + } + for entry in system_metrics + if args.metric in entry + ] + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "metric": args.metric, + "values": filtered_metrics, + } + ) + ) + else: + print(format_system_metrics(filtered_metrics)) + else: + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "system_metrics": system_metrics, + } + ) + ) + else: + print(format_system_metrics(system_metrics)) + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + runs = SQLiteStorage.get_runs(args.project) + if args.run not in runs: + error_exit( + f"Run '{args.run}' not found in project '{args.project}'." + ) + if args.metric: + system_metrics = SQLiteStorage.get_system_logs( + args.project, args.run + ) + all_system_metric_names = ( + SQLiteStorage.get_all_system_metrics_for_run( + args.project, args.run + ) + ) + if args.metric not in all_system_metric_names: + error_exit( + f"System metric '{args.metric}' not found in run '{args.run}' of project '{args.project}'." + ) + filtered_metrics = [ + { + k: v + for k, v in entry.items() + if k == "timestamp" or k == args.metric + } + for entry in system_metrics + if args.metric in entry + ] + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "metric": args.metric, + "values": filtered_metrics, + } + ) + ) + else: + print(format_system_metrics(filtered_metrics)) + else: + system_metrics = SQLiteStorage.get_system_logs( + args.project, args.run + ) + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "system_metrics": system_metrics, + } + ) + ) + else: + print(format_system_metrics(system_metrics)) + elif args.get_type == "trace": + _handle_get_trace(args, remote) + elif args.get_type == "trace-summary": + _handle_get_trace_summary(args, remote) + elif args.get_type == "alerts": + if remote: + alerts = remote.predict( + args.project, + args.run, + args.level, + args.since, + api_name="/get_alerts", + ) + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + alerts = SQLiteStorage.get_alerts( + args.project, + run_name=args.run, + level=args.level, + since=args.since, + ) + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "level": args.level, + "since": args.since, + "alerts": alerts, + } + ) + ) + else: + print(format_alerts(alerts)) + elif args.get_type == "report": + if remote: + logs = remote.predict(args.project, args.run, api_name="/get_logs") + else: + db_path = SQLiteStorage.get_project_db_path(args.project) + if not db_path.exists(): + error_exit(f"Project '{args.project}' not found.") + runs = SQLiteStorage.get_runs(args.project) + if args.run not in runs: + error_exit( + f"Run '{args.run}' not found in project '{args.project}'." + ) + logs = SQLiteStorage.get_logs(args.project, args.run) + + reports = _extract_reports(args.run, logs, report_name=args.report) + if not reports: + error_exit( + f"Report '{args.report}' not found in run '{args.run}' of project '{args.project}'." + ) + + if args.json: + print( + format_json( + { + "project": args.project, + "run": args.run, + "report": args.report, + "values": reports, + } + ) + ) + else: + output = [] + for idx, entry in enumerate(reports, start=1): + output.append( + f"Entry {idx} | step={entry['step']} | timestamp={entry['timestamp']}" + ) + output.append(entry["content"]) + if idx < len(reports): + output.append("-" * 80) + print("\n".join(output)) + elif args.command == "query": + if args.query_type == "project": + _handle_query(args) + elif args.command == "skills": + if args.skills_action == "add": + _handle_skills_add(args) + elif args.command == "registry": + _handle_registry(args) + elif args.command == "logbook": + _handle_logbook(args) + else: + parser.print_help() + + +def _logbook_cell_target(lb, proj, args): + return lb.resolve_page(proj, getattr(args, "page", None)) + + +def _print_logbook_pages(pages): + if not pages: + print("No pages.") + return + print("Pages:") + for page in pages: + count = page["cell_count"] + print( + f"- {page['slug']} · {page['title']} · " + f"{count} cell{'' if count == 1 else 's'}" + ) + + +def _print_logbook_page_outline(page): + print(f"Page: {page['title']} ({page['slug']})") + if not page["cells"]: + print("No cells.") + return + for cell in page["cells"]: + created = ( + f" · {cell['created_at'].replace('T', ' ')[:16]}" + if cell.get("created_at") + else "" + ) + print(f"\n### {cell['title']} · {cell['type']} · {cell['id']}{created}") + if cell.get("preview"): + print(cell["preview"].rstrip()) + print( + "\nFetch full payloads with: trackio logbook read cell " + "[--full|--raw|--html]" + ) + + +def _print_logbook_cell(cell): + print(f"Cell: {cell['title']} ({cell['id']})") + print(f"Page: {cell['page_title']} ({cell['page']})") + print(f"Type: {cell['type']}") + if cell.get("created_at"): + print(f"Created: {cell['created_at']}") + content_keys = [key for key in ("body", "raw", "html") if key in cell] + if not content_keys: + if cell["type"] == "code": + print("\nBody omitted. Use --full to include code/output.") + elif cell["type"] == "figure": + print("\nFigure content omitted. Use --raw, --html, or --full.") + return + for key in content_keys: + label = key.upper() + print(f"\n--- {label} ---\n") + print(cell[key]) + + +def _read_logbook_payload(path_or_text, inline_text): + if inline_text is not None: + return inline_text + if not path_or_text: + return "" + path = Path(path_or_text) + if path.is_file(): + return path.read_text(encoding="utf-8") + return path_or_text + + +def _handle_logbook(args): + action = args.logbook_action + try: + if action == "open": + proj = lb.find_project_dir() + if proj is not None: + if args.space_id: + metadata = lb.read_metadata(proj) + metadata["space_id"] = args.space_id + lb.write_metadata(proj, metadata) + print(f"Attached to existing logbook at {lb.logbook_root(proj)}") + if args.title: + print( + "Note: --title was ignored because this logbook already " + "exists. To retitle it, edit the '# ...' heading in " + f"{lb.logbook_root(proj) / 'pages' / 'index.md'}." + ) + elif args.space_id: + proj = lb.clone_logbook(args.space_id) + if proj is not None: + print( + f"Cloned logbook from {args.space_id} into " + f"{lb.logbook_root(proj)}" + ) + if proj is None: + proj = lb.create_logbook( + args.title or os.path.basename(os.getcwd()), space_id=args.space_id + ) + print(f"Opened logbook at {lb.logbook_root(proj)}") + if args.space_id: + print(f"Will publish to: {args.space_id}") + if not args.no_serve: + lb.start_preview(proj, port=args.port, open_browser=not args.no_browser) + elif action == "run": + proj = lb.require_project_dir() + command = list(args.command or []) + if command and command[0] == "--": + command = command[1:] + if not command: + error_exit("No command provided. Use: trackio logbook run -- ") + rc = lb.run_and_log( + proj, + command, + page=args.page, + title=args.title, + capture_artifacts=not getattr(args, "no_artifacts", False), + ) + slug = lb.read_metadata(proj).get("last_page", "?") + print(f"Logged run to page '{slug}'.") + sys.exit(rc) + elif action == "cell" and args.cell_type == "remove": + proj = lb.require_project_dir() + result = lb.remove_cell(proj, args.cell_id, page=args.page) + print( + f"Removed {result['type']} cell {args.cell_id} " + f"from page '{result['page']}'." + ) + elif action == "cell": + proj = lb.require_project_dir() + slug = _logbook_cell_target(lb, proj, args) + if args.cell_type == "markdown": + # Agent/tool callers often send a single argument containing + # escaped newlines. Treat those as Markdown line breaks while + # preserving normal shell-provided newlines unchanged. + body = args.body.replace("\\r\\n", "\n").replace("\\n", "\n") + lb.add_markdown_cell(proj, slug, body, title=args.title) + elif args.cell_type == "artifact": + lb.add_artifact_cell( + proj, + slug, + args.name, + title=args.title, + artifact_type=args.artifact_type, + ) + elif args.cell_type == "code": + lb.add_code_cell( + proj, + slug, + args.output, + title=args.title, + code_paths=args.code, + code_text=args.code_text, + language=args.language, + ) + elif args.cell_type == "figure": + if args.image: + html = lb.figure_html_from_image(args.image) + elif ( + args.html + and args.html_text is None + and Path(args.html).is_file() + and lb.is_figure_image_path(args.html) + ): + # `--html plot.png` — embed the image instead of trying to + # read a binary file as UTF-8 text. + html = lb.figure_html_from_image(args.html) + else: + html = _read_logbook_payload(args.html, args.html_text) + raw = _read_logbook_payload(args.raw, args.raw_text) + lb.add_figure_cell( + proj, + slug, + html=html, + raw=raw, + title=args.title, + inline_plotlyjs=args.inline_plotlyjs, + ) + elif args.cell_type == "dashboard": + lb.add_dashboard_cell( + proj, + slug, + args.project, + space_id=args.space_id, + title=args.title, + ) + print(f"Logged {args.cell_type} cell to page '{slug}'.") + elif action == "page": + proj = lb.require_project_dir() + page_slug = lb.ensure_page(proj, args.title) + print(f"Selected page '{page_slug}' as default.") + elif action == "attach": + proj = lb.require_project_dir() + if args.attach_type == "trace": + scrub = not args.no_scrub + trace = lb.attach_trace( + proj, args.filepath, title=args.title, scrub=scrub + ) + print( + f"Attached {trace.get('provider', 'agent')} trace " + f"'{trace['id']}' ({trace.get('event_count', 0)} events)." + ) + if scrub: + redactions = trace.get("scrub_redactions", 0) + print( + f"Scrubbed secrets before storing: {redactions} " + f"redaction{'' if redactions == 1 else 's'}." + ) + else: + print( + "Warning: --no-scrub set; secrets were NOT redacted from " + "this trace." + ) + elif action == "remove": + proj = lb.require_project_dir() + if args.remove_type == "trace": + lb.remove_trace(proj, args.session_id) + print(f"Removed attached trace '{args.session_id}'.") + elif action == "pin": + proj = lb.require_project_dir() + cell_id = args.cell_id or lb.last_cell_id(proj, page=args.page) + if not cell_id: + error_exit( + "No cell to pin. Pass a cell id, or add a cell first " + "(optionally scope with --page)." + ) + res = lb.set_cell_pinned( + proj, cell_id, pinned=not args.unpin, page=args.page + ) + verb = "Unpinned" if args.unpin else "Pinned" + print(f"{verb} cell {cell_id} on page '{res['page']}'.") + elif action == "read": + source, view = lb.split_read_view(args.path) + proj = lb.resolve_read_source(source) + preview_opts = { + "head": lb.DEFAULT_HEAD + if getattr(args, "head", None) is None + else args.head, + "tail": lb.DEFAULT_TAIL + if getattr(args, "tail", None) is None + else args.tail, + "raw_limit": lb.DEFAULT_RAW_LIMIT + if getattr(args, "raw_limit", None) is None + else args.raw_limit, + } + if args.read_target is None and view == "trace": + text = lb.read_traces(proj) + print( + format_json({"view": "trace", "text": text}) if args.json else text + ) + elif args.read_target is None and view == "workspace": + text = lb.read_workspace_tree(proj) + print( + format_json({"view": "workspace", "text": text}) + if args.json + else text + ) + elif args.read_target is None: + if args.json: + print(format_json(lb.read_logbook_data(proj, **preview_opts))) + else: + print(lb.read_logbook(proj, **preview_opts)) + elif args.read_target == "pages": + pages = lb.list_pages(proj) + if args.json: + print(format_json({"pages": pages})) + else: + _print_logbook_pages(pages) + elif args.read_target == "page": + page = lb.read_page_outline(proj, args.page, **preview_opts) + if args.json: + print(format_json(page)) + else: + _print_logbook_page_outline(page) + elif args.read_target == "cell": + cell = lb.read_cell( + proj, + args.cell_id, + include_full=args.full, + include_raw=args.raw, + include_html=args.html, + ) + if args.json: + print(format_json(cell)) + else: + _print_logbook_cell(cell) + elif action == "sync": + proj = lb.require_project_dir() + lb.write_site_files(proj) + print(f"Synced logbook site files at {lb.logbook_root(proj)}.") + elif action == "sync-todos": + lb.sync_todos_from_stdin() + elif action == "serve": + lb.serve(path=args.path, port=args.port, open_browser=not args.no_browser) + elif action == "publish": + proj = lb.require_project_dir() + metadata = lb.read_metadata(proj) + if not (args.space_id or metadata.get("space_id")): + raise lb.LogbookError( + "No Space id. Provide one: trackio logbook publish " + ) + inventory = lb.publication_inventory(proj) + visibility = "PUBLIC" if args.public else "PRIVATE" + if inventory["trace_count"] or inventory["workspace_file_count"]: + print( + f"Attached traces ({inventory['trace_count']}) and Workspace " + f"files ({inventory['workspace_file_count']}) will be published " + f"to {visibility} repos." + ) + if args.public: + print( + " --public: trace/workspace content will also be embedded " + "inline in the static Space." + ) + else: + print( + " The static Space will store references only " + "(no trace/workspace content or names)." + ) + url = lb.publish( + space_id=args.space_id, + hf_token=args.hf_token, + private=args.private, + public=args.public, + ) + print(f"Published: {url}") + space_id = lb.read_metadata(lb.require_project_dir()).get("space_id") + if space_id: + subdomain = re.sub(r"[^a-z0-9]+", "-", space_id.lower()).strip("-") + print(f"Rendered at: https://{subdomain}.static.hf.space/") + except lb.LogbookError as e: + error_exit(str(e)) + + +def _handle_skills_add(args): + CENTRAL_LOCAL = Path(".agents/skills") + CENTRAL_GLOBAL = Path("~/.agents/skills") + CLAUDE_LOCAL = Path(".claude/skills") + CLAUDE_GLOBAL = Path("~/.claude/skills") + + SKILL_ID = "trackio" + GITHUB_RAW = "https://raw.githubusercontent.com/gradio-app/trackio/main" + SKILL_PREFIX = ".agents/skills/trackio" + SKILL_FILES = [ + "SKILL.md", + "alerts.md", + "logging_metrics.md", + "retrieving_metrics.md", + "storage_schema.md", + "traces.md", + "logbook.md", + ] + COMMAND_PREFIX = ".agents/commands" + COMMAND_FILE = "logbook.md" + + REPO_ROOT = Path(__file__).resolve().parent.parent + USE_LOCAL = (REPO_ROOT / SKILL_PREFIX / "SKILL.md").is_file() + + if USE_LOCAL: + print(f"Using local Trackio source at {REPO_ROOT}") + + def download(url: str) -> str: + try: + response = get_session().get(url) + response.raise_for_status() + except Exception as e: + error_exit( + f"Failed to download {url}\n{e}\n\n" + "Make sure you have internet access. The skill files are fetched from " + "the Trackio GitHub repository." + ) + return response.text + + def get_content(prefix: str, fname: str) -> str: + if USE_LOCAL: + local = REPO_ROOT / prefix / fname + if local.is_file(): + return local.read_text(encoding="utf-8") + return download(f"{GITHUB_RAW}/{prefix}/{fname}") + + def remove_existing(path: Path, force: bool): + if not (path.exists() or path.is_symlink()): + return + if not force: + error_exit( + f"Skill already exists at {path}.\nRe-run with --force to overwrite." + ) + if path.is_dir() and not path.is_symlink(): + shutil.rmtree(path) + else: + path.unlink() + + def install_to(skills_dir: Path, force: bool) -> Path: + skills_dir = skills_dir.expanduser().resolve() + skills_dir.mkdir(parents=True, exist_ok=True) + dest = skills_dir / SKILL_ID + remove_existing(dest, force) + dest.mkdir() + for fname in SKILL_FILES: + (dest / fname).write_text( + get_content(SKILL_PREFIX, fname), encoding="utf-8" + ) + return dest + + def create_symlink( + agent_skills_dir: Path, central_skill_path: Path, force: bool + ) -> Path: + agent_skills_dir = agent_skills_dir.expanduser().resolve() + agent_skills_dir.mkdir(parents=True, exist_ok=True) + link_path = agent_skills_dir / SKILL_ID + remove_existing(link_path, force) + link_path.symlink_to(os.path.relpath(central_skill_path, agent_skills_dir)) + return link_path + + def install_command( + command_dir: Path, force: bool, strip_frontmatter: bool = False + ) -> Path: + command_dir = command_dir.expanduser().resolve() + command_dir.mkdir(parents=True, exist_ok=True) + dest = command_dir / COMMAND_FILE + if dest.exists() and not force: + error_exit( + f"Command already exists at {dest}.\nRe-run with --force to overwrite." + ) + content = get_content(COMMAND_PREFIX, COMMAND_FILE) + if strip_frontmatter: + content = re.sub(r"\A---\n.*?\n---\n+", "", content, flags=re.S) + dest.write_text(content, encoding="utf-8") + return dest + + global_targets = { + "cursor": Path("~/.cursor/skills"), + "claude": CLAUDE_GLOBAL, + "codex": Path("~/.codex/skills"), + "opencode": Path("~/.opencode/skills"), + "pi": Path("~/.pi/agent/skills"), + } + local_targets = { + "cursor": Path(".cursor/skills"), + "claude": CLAUDE_LOCAL, + "codex": Path(".codex/skills"), + "opencode": Path(".opencode/skills"), + "pi": Path(".pi/skills"), + } + targets_dict = global_targets if args.global_ else local_targets + + command_targets_global = { + "cursor": Path("~/.cursor/commands"), + "claude": Path("~/.claude/commands"), + "codex": Path("~/.codex/prompts"), + "opencode": Path("~/.config/opencode/commands"), + "pi": Path("~/.pi/agent/prompts"), + } + command_targets_local = { + "cursor": Path(".cursor/commands"), + "claude": Path(".claude/commands"), + "codex": Path("~/.codex/prompts"), + "opencode": Path(".opencode/commands"), + "pi": Path(".pi/prompts"), + } + command_dict = command_targets_global if args.global_ else command_targets_local + COMMAND_NOTES = { + "codex": " (Codex only reads ~/.codex/prompts; restart Codex to pick it up)" + } + STRIP_FRONTMATTER = {"pi"} + + if args.dest: + if ( + args.cursor + or args.claude + or args.codex + or args.opencode + or args.pi + or args.global_ + ): + error_exit("--dest cannot be combined with agent flags or --global.") + skill_dest = install_to(Path(args.dest), args.force) + print(f"Installed '{SKILL_ID}' to {skill_dest}") + return + + selected = [ + name + for name, flag in ( + ("cursor", args.cursor), + ("claude", args.claude), + ("codex", args.codex), + ("opencode", args.opencode), + ("pi", args.pi), + ) + if flag + ] + + central_path = CENTRAL_GLOBAL if args.global_ else CENTRAL_LOCAL + central_skill_path = install_to(central_path, args.force) + print(f"Installed '{SKILL_ID}' to central location: {central_skill_path}") + + for name in selected: + link_path = create_symlink(targets_dict[name], central_skill_path, args.force) + print(f"Created symlink: {link_path}") + + if not args.no_command: + for name in selected: + if name in command_dict: + cmd_path = install_command( + command_dict[name], + args.force, + strip_frontmatter=name in STRIP_FRONTMATTER, + ) + note = COMMAND_NOTES.get(name, "") + print(f"Installed '/logbook' command: {cmd_path}{note}") + else: + print( + f"Skipped '/logbook' command for {name} " + "(no slash-command directory convention)." + ) + + if args.claude and not args.no_hook: + hook_path = _install_claude_logbook_hook(args.global_) + if hook_path: + print(f"Installed logbook todo-sync hook: {hook_path}") + + +def _install_claude_logbook_hook(global_: bool): + settings = ( + Path("~/.claude/settings.json") if global_ else Path(".claude/settings.json") + ).expanduser() + settings.parent.mkdir(parents=True, exist_ok=True) + data = {} + if settings.exists(): + try: + data = json.loads(settings.read_text(encoding="utf-8")) + except Exception: + print(f"Could not parse {settings}; skipping hook install.") + return None + command = "trackio logbook sync-todos" + hooks = data.setdefault("hooks", {}) + post = hooks.setdefault("PostToolUse", []) + for entry in post: + for h in entry.get("hooks", []): + if h.get("command") == command: + return settings + post.append( + { + "matcher": "TodoWrite", + "hooks": [{"type": "command", "command": command}], + } + ) + settings.write_text(json.dumps(data, indent=2), encoding="utf-8") + return settings + + +if __name__ == "__main__": + main() diff --git a/trackio/cli_helpers.py b/trackio/cli_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..c1cd5c22dd77a0ca74da7d3cd5c6e254b47cf423 --- /dev/null +++ b/trackio/cli_helpers.py @@ -0,0 +1,673 @@ +import json +import re +import sys +from datetime import datetime, timezone +from typing import Any + +from trackio import references + + +def format_json(data: Any) -> str: + """Format data as JSON.""" + return json.dumps(data, indent=2) + + +def format_list(items: list[str], title: str | None = None) -> str: + """Format a list of items in human-readable format.""" + if not items: + return f"No {title.lower() if title else 'items'} found." + + output = [] + if title: + output.append(f"{title}:") + + for item in items: + output.append(f" - {item}") + + return "\n".join(output) + + +def format_artifacts(artifacts: list[dict], project: str | None = None) -> str: + """Format a project's artifacts in human-readable format.""" + if not artifacts: + return "No artifacts found." + + title = f"Artifacts in '{project}'" if project else "Artifacts" + output = [f"{title}:"] + for art in artifacts: + latest = art.get("latest_version") + version = f"v{latest}" if latest is not None else "(no versions)" + aliases = art.get("aliases") or [] + alias_str = f" [{', '.join(aliases)}]" if aliases else "" + output.append( + f" - {art['name']} ({art.get('type', '?')}) " + f"latest={version}{alias_str} " + f"versions={art.get('num_versions', 0)}" + ) + if art.get("description"): + output.append(f" {art['description']}") + return "\n".join(output) + + +def format_artifact(record: dict) -> str: + """Format a single resolved artifact version in human-readable format.""" + lines = [ + f"Artifact: {record['name']} (v{record['version']})", + f" type: {record.get('type')}", + ] + if record.get("description"): + lines.append(f" description: {record['description']}") + aliases = record.get("aliases") or [] + if aliases: + lines.append(f" aliases: {', '.join(aliases)}") + if record.get("metadata"): + lines.append(f" metadata: {record['metadata']}") + lines.append(f" size: {record.get('size_bytes')} bytes") + lines.append(f" digest: {record.get('manifest_digest')}") + manifest = record.get("manifest") or [] + lines.append(f" files ({len(manifest)}):") + for entry in manifest: + if references.is_reference_entry(entry): + lines.append(f" - {entry['path']} -> {entry['ref']} (reference)") + else: + lines.append(f" - {entry['path']} ({entry['size']} bytes)") + return "\n".join(lines) + + +def format_registry_collections(registry: str, collections: list[dict]) -> str: + """Format the collections in an artifact registry.""" + if not collections: + return f"No collections found in registry '{registry}'." + + output = [f"Collections in registry '{registry}':"] + for collection in collections: + latest = collection.get("latest_version") + latest_display = f"v{latest}" if latest is not None else "(empty)" + output.append( + f" - {collection['name']} ({collection['type']}) " + f"latest={latest_display} versions={collection['num_links']}" + ) + if collection.get("description"): + output.append(f" {collection['description']}") + return "\n".join(output) + + +def format_registry_collection(registry: str, collection: dict) -> str: + """Format one registry collection and its linked versions.""" + output = [ + f"Collection: {registry}/{collection['name']}", + f" type: {collection['type']}", + f" versions: {collection['num_links']}", + ] + if collection.get("description"): + output.append(f" description: {collection['description']}") + if not collection["links"]: + output.append(" No linked versions.") + return "\n".join(output) + + output.append(" Linked versions:") + for link in collection["links"]: + aliases = link.get("aliases") or [] + alias_display = f" [{', '.join(aliases)}]" if aliases else "" + output.append( + f" - v{link['collection_version']} -> " + f"{link['source_project']}/{link['source_artifact']}" + f":v{link['source_version']}{alias_display}" + ) + return "\n".join(output) + + +def format_registry_events(events: list[dict]) -> str: + """Format a registry audit log.""" + if not events: + return "No registry events found." + return "\n".join( + f"{event['ts']} | {event['kind']} | " + f"{json.dumps(event['payload'], sort_keys=True)}" + for event in events + ) + + +def format_spaces(spaces: list[dict]) -> str: + """Format HF Spaces in human-readable format.""" + if not spaces: + return "No Trackio Spaces found." + + output = ["Trackio Spaces:"] + for space in spaces: + visibility = "private" if space.get("private") else "public" + output.append(f" - {space['id']} ({visibility})") + if space.get("url"): + output.append(f" {space['url']}") + + return "\n".join(output) + + +def format_project_summary(summary: dict) -> str: + """Format project summary in human-readable format.""" + output = [f"Project: {summary['project']}"] + output.append(f"Number of runs: {summary['num_runs']}") + + if summary["runs"]: + output.append("\nRuns:") + for run in summary["runs"]: + output.append(f" - {run}") + else: + output.append("\nNo runs found.") + + if summary.get("last_activity"): + output.append(f"\nLast activity (max step): {summary['last_activity']}") + + return "\n".join(output) + + +def format_run_summary(summary: dict) -> str: + """Format run summary in human-readable format.""" + output = [f"Project: {summary['project']}"] + output.append(f"Run: {summary['run']}") + output.append(f"Number of logs: {summary['num_logs']}") + + if summary.get("last_step") is not None: + output.append(f"Last step: {summary['last_step']}") + + if summary.get("metrics"): + output.append("\nMetrics:") + for metric in summary["metrics"]: + output.append(f" - {metric}") + else: + output.append("\nNo metrics found.") + + config = summary.get("config") + if config: + output.append("\nConfig:") + config_display = {k: v for k, v in config.items() if not k.startswith("_")} + if config_display: + for key, value in config_display.items(): + output.append(f" {key}: {value}") + else: + output.append(" (no config)") + else: + output.append("\nConfig: (no config)") + + return "\n".join(output) + + +def format_metric_values(values: list[dict]) -> str: + """Format metric values in human-readable format.""" + if not values: + return "No metric values found." + + output = [f"Found {len(values)} value(s):\n"] + output.append("Step | Timestamp | Value") + output.append("-" * 50) + + for value in values: + step = value.get("step", "N/A") + timestamp = value.get("timestamp", "N/A") + val = value.get("value", "N/A") + output.append(f"{step} | {timestamp} | {val}") + + return "\n".join(output) + + +def format_system_metrics(metrics: list[dict]) -> str: + """Format system metrics in human-readable format.""" + if not metrics: + return "No system metrics found." + + output = [f"Found {len(metrics)} system metric entry/entries:\n"] + + for i, entry in enumerate(metrics): + timestamp = entry.get("timestamp", "N/A") + output.append(f"\nEntry {i + 1} (Timestamp: {timestamp}):") + for key, value in entry.items(): + if key != "timestamp": + output.append(f" {key}: {value}") + + return "\n".join(output) + + +def format_system_metric_names(names: list[str]) -> str: + """Format system metric names in human-readable format.""" + return format_list(names, "System Metrics") + + +def format_snapshot(snapshot: dict[str, list[dict]]) -> str: + """Format a metrics snapshot in human-readable format.""" + if not snapshot: + return "No metrics found in the specified range." + + output = [] + for metric_name, values in sorted(snapshot.items()): + output.append(f"\n{metric_name}:") + output.append(" Step | Timestamp | Value") + output.append(" " + "-" * 48) + for v in values: + step = v.get("step", "N/A") + ts = v.get("timestamp", "N/A") + val = v.get("value", "N/A") + output.append(f" {step} | {ts} | {val}") + + return "\n".join(output) + + +def format_alerts(alerts: list[dict]) -> str: + """Format alerts in human-readable format.""" + if not alerts: + return "No alerts found." + + output = [f"Found {len(alerts)} alert(s):\n"] + output.append("Timestamp | Run | Level | Title | Text | Step") + output.append("-" * 80) + + for a in alerts: + ts = a.get("timestamp", "N/A") + run = a.get("run", "N/A") + level = a.get("level", "N/A").upper() + title = a.get("title", "") + text = a.get("text", "") or "" + step = a.get("step", "N/A") + output.append(f"{ts} | {run} | {level} | {title} | {text} | {step}") + + return "\n".join(output) + + +def format_query_result(result: dict[str, Any]) -> str: + """Format SQL query results in human-readable format.""" + columns = result.get("columns", []) + rows = result.get("rows", []) + row_count = result.get("row_count", 0) + + if not columns: + return f"Query returned {row_count} row(s)." + + rendered_rows = [] + for row in rows: + rendered_rows.append( + [ + "" if row.get(column) is None else str(row.get(column)) + for column in columns + ] + ) + + widths = [] + for idx, column in enumerate(columns): + cell_width = max( + (len(rendered_row[idx]) for rendered_row in rendered_rows), default=0 + ) + widths.append(max(len(column), cell_width)) + + header = " | ".join( + column.ljust(width) for column, width in zip(columns, widths, strict=False) + ) + separator = "-+-".join("-" * width for width in widths) + output = [f"Query returned {row_count} row(s).", header, separator] + + if not rendered_rows: + output.append("(no rows)") + return "\n".join(output) + + for rendered_row in rendered_rows: + output.append( + " | ".join( + value.ljust(width) + for value, width in zip(rendered_row, widths, strict=False) + ) + ) + + return "\n".join(output) + + +def error_exit(message: str, code: int = 1) -> None: + """Print error message and exit.""" + print(f"Error: {message}", file=sys.stderr) + sys.exit(code) + + +def format_duration_ms(value: Any) -> str: + """Render a millisecond duration the way the trace dashboard does.""" + if value is None or value == "": + return "-" + try: + ms = float(value) + except (TypeError, ValueError): + return "-" + if ms < 1000: + return f"{round(ms)}ms" + if ms < 60_000: + return f"{ms / 1000:.2f}s" if ms < 10_000 else f"{ms / 1000:.1f}s" + minutes = int(ms // 60_000) + seconds = round((ms % 60_000) / 1000) + return f"{minutes}m {seconds}s" + + +def format_cost_usd(value: Any) -> str: + if value is None or value == "": + return "-" + try: + cost = float(value) + except (TypeError, ValueError): + return "-" + if cost == 0: + return "$0" + if cost < 0.0001: + return "<$0.0001" + if cost < 0.01: + return f"${cost:.4f}" + return f"${cost:.2f}" + + +_ISO_FRACTION_RE = re.compile(r"\.(\d+)") + + +def parse_span_timestamp(value: Any) -> datetime | None: + """Parse a span timestamp, tolerating any number of fractional digits. + + `datetime.fromisoformat` accepts only 3 or 6 fractional digits before + Python 3.11, and instrumentation commonly emits fewer (or a `Z` suffix), + so those are normalized first. Naive timestamps are assumed to be UTC so + that spans from mixed sources stay comparable. + """ + if not value: + return None + text = str(value).strip() + if text.endswith(("Z", "z")): + text = text[:-1] + "+00:00" + text = _ISO_FRACTION_RE.sub( + lambda match: "." + (match.group(1) + "000000")[:6], text, count=1 + ) + try: + parsed = datetime.fromisoformat(text) + except ValueError: + return None + return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc) + + +def _span_duration_ms(span: dict) -> float | None: + explicit = span.get("duration_ms") + if explicit is not None: + try: + return max(0.0, float(explicit)) + except (TypeError, ValueError): + pass + started = parse_span_timestamp(span.get("start_time")) + ended = parse_span_timestamp(span.get("end_time")) + if started is None or ended is None: + return None + return max(0.0, (ended - started).total_seconds() * 1000) + + +def _span_is_error(span: dict) -> bool: + status = str(span.get("status") or "").lower() + return status in {"error", "failed"} or bool(span.get("error")) + + +def trace_rollup(trace: dict) -> dict: + """Latency, cost, token, and status totals for one trace.""" + spans = trace.get("spans") or [] + starts, ends, durations = [], [], [] + cost = 0.0 + has_cost = False + input_tokens = output_tokens = 0 + has_usage = has_error = False + + for span in spans: + if not isinstance(span, dict): + continue + for field, sink in (("start_time", starts), ("end_time", ends)): + parsed = parse_span_timestamp(span.get(field)) + if parsed is not None: + sink.append(parsed) + duration = _span_duration_ms(span) + if duration is not None: + durations.append(duration) + span_cost = span.get("cost_usd") + if span_cost is not None: + try: + cost += float(span_cost) + has_cost = True + except (TypeError, ValueError): + pass + usage = span.get("usage") or {} + for key, add in (("input_tokens", "in"), ("output_tokens", "out")): + value = usage.get(key) + if value is None: + continue + try: + if add == "in": + input_tokens += int(value) + else: + output_tokens += int(value) + has_usage = True + except (TypeError, ValueError): + pass + if _span_is_error(span): + has_error = True + + metadata = trace.get("metadata") or {} + if starts and ends: + duration_ms = max(0.0, (max(ends) - min(starts)).total_seconds() * 1000) + elif durations: + duration_ms = max(durations) + else: + duration_ms = metadata.get("duration_ms") or metadata.get("latency_ms") + + all_success = bool(spans) and all( + str(span.get("status") or "").lower() == "success" + for span in spans + if isinstance(span, dict) + ) + metadata_status = metadata.get("status") + status = ( + "error" + if has_error + else ( + metadata_status + if isinstance(metadata_status, str) and metadata_status + else ("success" if all_success else None) + ) + ) + return { + "duration_ms": duration_ms, + "cost_usd": cost if has_cost else metadata.get("cost_usd"), + "input_tokens": input_tokens if has_usage else None, + "output_tokens": output_tokens if has_usage else None, + "status": status, + "span_count": len(spans), + } + + +def short_trace_id(trace_id: Any) -> str: + """Shorten a stored trace id for display, as the dashboard does. + + Stored ids are `run_id:log_id:key[:index]`; the `log_id` (plus the list + index, when the key held several traces) identifies the trace on its own. + """ + text = str(trace_id or "") + parts = text.split(":") + if len(parts) < 3: + return text + short = parts[1] + last = parts[-1] + if len(parts) >= 4 and last.lstrip("-").isdigit(): + return f"{short}:{last}" + return short + + +def trace_id_matches(trace: dict, wanted: str) -> bool: + stored = str(trace.get("id") or "") + return wanted in (stored, short_trace_id(stored)) + + +def _first_message_text(trace: dict, role: str) -> str: + for message in trace.get("messages") or []: + if not isinstance(message, dict) or message.get("role") != role: + continue + content = message.get("content") + if isinstance(content, str): + return content + if isinstance(content, list): + for part in content: + if isinstance(part, dict) and isinstance(part.get("text"), str): + return part["text"] + return "" + + +def format_traces(traces: list[dict]) -> str: + """Format a trace index in human-readable format.""" + if not traces: + return "No traces found." + + output = [f"Found {len(traces)} trace(s):\n"] + output.append("ID | Run | Step | Status | Latency | Cost | Ops | Request") + output.append("-" * 100) + for trace in traces: + rollup = trace_rollup(trace) + request = _first_message_text(trace, "user").replace("\n", " ") + if len(request) > 48: + request = request[:45] + "..." + output.append( + " | ".join( + [ + short_trace_id(trace.get("id")) or "-", + str(trace.get("run", "-")), + str(trace.get("step", "-")), + rollup["status"] or "-", + format_duration_ms(rollup["duration_ms"]), + format_cost_usd(rollup["cost_usd"]), + str(rollup["span_count"]), + request or "-", + ] + ) + ) + return "\n".join(output) + + +def _ordered_span_tree(spans: list[dict]) -> list[tuple[int, dict]]: + """Depth-annotated spans, parents before children, siblings by start time.""" + normalized = [span for span in spans if isinstance(span, dict)] + by_id: dict[str, dict] = {} + for index, span in enumerate(normalized): + span_id = str(span.get("id") or f"span-{index + 1}") + by_id.setdefault(span_id, span) + + children: dict[int, list[dict]] = {} + roots: list[dict] = [] + for span in normalized: + parent_id = span.get("parent_id") + parent = by_id.get(str(parent_id)) if parent_id else None + if parent is None or parent is span: + roots.append(span) + else: + children.setdefault(id(parent), []).append(span) + + def sort_key(span: dict) -> tuple: + started = parse_span_timestamp(span.get("start_time")) + return ( + 0 if started else 1, + started.timestamp() if started else 0.0, + normalized.index(span), + ) + + rows: list[tuple[int, dict]] = [] + seen: set[int] = set() + + def walk(span: dict, depth: int) -> None: + if id(span) in seen: + return + seen.add(id(span)) + rows.append((depth, span)) + for child in sorted(children.get(id(span), []), key=sort_key): + walk(child, depth + 1) + + for root in sorted(roots, key=sort_key): + walk(root, 0) + for span in normalized: + walk(span, 0) + return rows + + +def format_trace(trace: dict) -> str: + """Format a single trace, including its span tree, in human-readable format.""" + rollup = trace_rollup(trace) + output = [f"Trace {short_trace_id(trace.get('id')) or '-'}"] + output.append(f" Full id: {trace.get('id', '-')}") + output.append(f" Run: {trace.get('run', '-')}") + output.append(f" Step: {trace.get('step', '-')}") + output.append( + f" Logged: {trace.get('timestamp', '-')} (key: {trace.get('key', '-')})" + ) + output.append( + f" Totals: {rollup['status'] or 'no status'} | " + f"{format_duration_ms(rollup['duration_ms'])} | " + f"{format_cost_usd(rollup['cost_usd'])} | " + f"{rollup['input_tokens'] if rollup['input_tokens'] is not None else '-'} in / " + f"{rollup['output_tokens'] if rollup['output_tokens'] is not None else '-'} out tokens | " + f"{rollup['span_count']} operation(s)" + ) + metadata = trace.get("metadata") or {} + if metadata: + rendered = ", ".join( + f"{key}={value}" + for key, value in metadata.items() + if not isinstance(value, (dict, list)) + ) + if rendered: + output.append(f" Metadata: {rendered}") + + spans = trace.get("spans") or [] + if spans: + output.append("\nExecution:") + for depth, span in _ordered_span_tree(spans): + indent = " " * (depth + 1) + bits = [format_duration_ms(_span_duration_ms(span))] + if span.get("model"): + bits.append(str(span["model"])) + usage = span.get("usage") or {} + if ( + usage.get("input_tokens") is not None + or usage.get("output_tokens") is not None + ): + bits.append( + f"{usage.get('input_tokens', 0)}->{usage.get('output_tokens', 0)} tok" + ) + if span.get("cost_usd") is not None: + bits.append(format_cost_usd(span.get("cost_usd"))) + marker = "ERROR " if _span_is_error(span) else "" + output.append( + f"{indent}{marker}{span.get('name', span.get('kind', 'span'))} " + f"[{span.get('kind', 'span')}] {' '.join(bits)}" + ) + error = span.get("error") + if error: + detail = error if isinstance(error, str) else json.dumps(error) + output.append(f"{indent} -> {detail[:160]}") + else: + output.append("\nExecution: no spans logged for this trace.") + + messages = trace.get("messages") or [] + if messages: + output.append("\nConversation:") + for message in messages: + if not isinstance(message, dict): + continue + role = message.get("role", "message") + content = message.get("content") + if not isinstance(content, str): + content = json.dumps(content) if content is not None else "" + content = content.replace("\n", " ") + if len(content) > 160: + content = content[:157] + "..." + output.append(f" {role}: {content}") + return "\n".join(output) + + +def format_trace_summary(result: dict[str, Any]) -> str: + """Format the per-operation trace rollup in human-readable format.""" + rows = result.get("rows") or [] + if not rows: + return "No trace spans found." + columns = result.get("columns") or [] + return format_query_result( + {"columns": columns, "rows": rows, "row_count": len(rows)} + ) diff --git a/trackio/commit_scheduler.py b/trackio/commit_scheduler.py new file mode 100644 index 0000000000000000000000000000000000000000..b1c880bbf992db3db9d085d81c4fca94d2928c05 --- /dev/null +++ b/trackio/commit_scheduler.py @@ -0,0 +1,306 @@ +# Originally copied from https://github.com/huggingface/huggingface_hub/blob/d0a948fc2a32ed6e557042a95ef3e4af97ec4a7c/src/huggingface_hub/_commit_scheduler.py + +import atexit +import logging +import time +from concurrent.futures import Future +from dataclasses import dataclass +from pathlib import Path +from threading import Lock, Thread +from typing import Callable, Dict, List, Union + +from huggingface_hub import CommitOperationAdd +from huggingface_hub.hf_api import DEFAULT_IGNORE_PATTERNS, CommitInfo, HfApi +from huggingface_hub.utils import filter_repo_objects + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class _FileToUpload: + """Temporary dataclass to store info about files to upload. Not meant to be used directly.""" + + local_path: Path + path_in_repo: str + size_limit: int + last_modified: float + + +class CommitScheduler: + """ + Scheduler to upload a local folder to the Hub at regular intervals (e.g. push to hub every 5 minutes). + + The recommended way to use the scheduler is to use it as a context manager. This ensures that the scheduler is + properly stopped and the last commit is triggered when the script ends. The scheduler can also be stopped manually + with the `stop` method. Checkout the [upload guide](https://huggingface.co/docs/huggingface_hub/guides/upload#scheduled-uploads) + to learn more about how to use it. + + Args: + repo_id (`str`): + The id of the repo to commit to. + folder_path (`str` or `Path`): + Path to the local folder to upload regularly. + every (`int` or `float`, *optional*): + The number of minutes between each commit. Defaults to 5 minutes. + path_in_repo (`str`, *optional*): + Relative path of the directory in the repo, for example: `"checkpoints/"`. Defaults to the root folder + of the repository. + repo_type (`str`, *optional*): + The type of the repo to commit to. Defaults to `model`. + revision (`str`, *optional*): + The revision of the repo to commit to. Defaults to `main`. + private (`bool`, *optional*): + Whether to make the repo private. If `None` (default), the repo will be public unless the organization's default is private. This value is ignored if the repo already exists. + token (`str`, *optional*): + The token to use to commit to the repo. Defaults to the token saved on the machine. + allow_patterns (`List[str]` or `str`, *optional*): + If provided, only files matching at least one pattern are uploaded. + ignore_patterns (`List[str]` or `str`, *optional*): + If provided, files matching any of the patterns are not uploaded. + squash_history (`bool`, *optional*): + Whether to squash the history of the repo after each commit. Defaults to `False`. Squashing commits is + useful to avoid degraded performances on the repo when it grows too large. + hf_api (`HfApi`, *optional*): + The [`HfApi`] client to use to commit to the Hub. Can be set with custom settings (user agent, token,...). + on_before_commit (`Callable[[], None]`, *optional*): + If specified, a function that will be called before the CommitScheduler lists files to create a commit. + + Example: + ```py + >>> from pathlib import Path + >>> from huggingface_hub import CommitScheduler + + # Scheduler uploads every 10 minutes + >>> csv_path = Path("watched_folder/data.csv") + >>> CommitScheduler(repo_id="test_scheduler", repo_type="dataset", folder_path=csv_path.parent, every=10) + + >>> with csv_path.open("a") as f: + ... f.write("first line") + + # Some time later (...) + >>> with csv_path.open("a") as f: + ... f.write("second line") + ``` + + Example using a context manager: + ```py + >>> from pathlib import Path + >>> from huggingface_hub import CommitScheduler + + >>> with CommitScheduler(repo_id="test_scheduler", repo_type="dataset", folder_path="watched_folder", every=10) as scheduler: + ... csv_path = Path("watched_folder/data.csv") + ... with csv_path.open("a") as f: + ... f.write("first line") + ... (...) + ... with csv_path.open("a") as f: + ... f.write("second line") + + # Scheduler is now stopped and last commit have been triggered + ``` + """ + + def __init__( + self, + *, + repo_id: str, + folder_path: Union[str, Path], + every: Union[int, float] = 5, + path_in_repo: str | None = None, + repo_type: str | None = None, + revision: str | None = None, + private: bool | None = None, + token: str | None = None, + allow_patterns: list[str] | str | None = None, + ignore_patterns: list[str] | str | None = None, + squash_history: bool = False, + hf_api: HfApi | None = None, + on_before_commit: Callable[[], None] | None = None, + ) -> None: + self.api = hf_api or HfApi(token=token) + self.on_before_commit = on_before_commit + + # Folder + self.folder_path = Path(folder_path).expanduser().resolve() + self.path_in_repo = path_in_repo or "" + self.allow_patterns = allow_patterns + + if ignore_patterns is None: + ignore_patterns = [] + elif isinstance(ignore_patterns, str): + ignore_patterns = [ignore_patterns] + self.ignore_patterns = ignore_patterns + DEFAULT_IGNORE_PATTERNS + + if self.folder_path.is_file(): + raise ValueError( + f"'folder_path' must be a directory, not a file: '{self.folder_path}'." + ) + self.folder_path.mkdir(parents=True, exist_ok=True) + + # Repository + repo_url = self.api.create_repo( + repo_id=repo_id, private=private, repo_type=repo_type, exist_ok=True + ) + self.repo_id = repo_url.repo_id + self.repo_type = repo_type + self.revision = revision + self.token = token + + self.last_uploaded: Dict[Path, float] = {} + self.last_push_time: float | None = None + + if not every > 0: + raise ValueError(f"'every' must be a positive integer, not '{every}'.") + self.lock = Lock() + self.every = every + self.squash_history = squash_history + + logger.info( + f"Scheduled job to push '{self.folder_path}' to '{self.repo_id}' every {self.every} minutes." + ) + self._scheduler_thread = Thread(target=self._run_scheduler, daemon=True) + self._scheduler_thread.start() + atexit.register(self._push_to_hub) + + self.__stopped = False + + def stop(self) -> None: + """Stop the scheduler. + + A stopped scheduler cannot be restarted. Mostly for tests purposes. + """ + self.__stopped = True + + def __enter__(self) -> "CommitScheduler": + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + # Upload last changes before exiting + self.trigger().result() + self.stop() + return + + def _run_scheduler(self) -> None: + """Dumb thread waiting between each scheduled push to Hub.""" + while True: + self.last_future = self.trigger() + time.sleep(self.every * 60) + if self.__stopped: + break + + def trigger(self) -> Future: + """Trigger a `push_to_hub` and return a future. + + This method is automatically called every `every` minutes. You can also call it manually to trigger a commit + immediately, without waiting for the next scheduled commit. + """ + return self.api.run_as_future(self._push_to_hub) + + def _push_to_hub(self) -> CommitInfo | None: + if self.__stopped: # If stopped, already scheduled commits are ignored + return None + + logger.info("(Background) scheduled commit triggered.") + try: + value = self.push_to_hub() + if self.squash_history: + logger.info("(Background) squashing repo history.") + self.api.super_squash_history( + repo_id=self.repo_id, repo_type=self.repo_type, branch=self.revision + ) + return value + except Exception as e: + logger.error( + f"Error while pushing to Hub: {e}" + ) # Depending on the setup, error might be silenced + raise + + def push_to_hub(self) -> CommitInfo | None: + """ + Push folder to the Hub and return the commit info. + + + + This method is not meant to be called directly. It is run in the background by the scheduler, respecting a + queue mechanism to avoid concurrent commits. Making a direct call to the method might lead to concurrency + issues. + + + + The default behavior of `push_to_hub` is to assume an append-only folder. It lists all files in the folder and + uploads only changed files. If no changes are found, the method returns without committing anything. If you want + to change this behavior, you can inherit from [`CommitScheduler`] and override this method. This can be useful + for example to compress data together in a single file before committing. For more details and examples, check + out our [integration guide](https://huggingface.co/docs/huggingface_hub/main/en/guides/upload#scheduled-uploads). + """ + # Check files to upload (with lock) + with self.lock: + if self.on_before_commit is not None: + self.on_before_commit() + + logger.debug("Listing files to upload for scheduled commit.") + + # List files from folder (taken from `_prepare_upload_folder_additions`) + relpath_to_abspath = { + path.relative_to(self.folder_path).as_posix(): path + for path in sorted( + self.folder_path.glob("**/*") + ) # sorted to be deterministic + if path.is_file() + } + prefix = f"{self.path_in_repo.strip('/')}/" if self.path_in_repo else "" + + # Filter with pattern + filter out unchanged files + retrieve current file size + files_to_upload: List[_FileToUpload] = [] + for relpath in filter_repo_objects( + relpath_to_abspath.keys(), + allow_patterns=self.allow_patterns, + ignore_patterns=self.ignore_patterns, + ): + local_path = relpath_to_abspath[relpath] + stat = local_path.stat() + if ( + self.last_uploaded.get(local_path) is None + or self.last_uploaded[local_path] != stat.st_mtime + ): + files_to_upload.append( + _FileToUpload( + local_path=local_path, + path_in_repo=prefix + relpath, + size_limit=stat.st_size, + last_modified=stat.st_mtime, + ) + ) + + # Return if nothing to upload + if len(files_to_upload) == 0: + logger.debug("Dropping schedule commit: no changed file to upload.") + return None + + # Convert `_FileToUpload` as `CommitOperationAdd` (=> compute file shas + limit to file size) + logger.debug("Removing unchanged files since previous scheduled commit.") + add_operations = [ + CommitOperationAdd( + # TODO: Cap the file to its current size, even if the user append data to it while a scheduled commit is happening + # (requires an upstream fix for XET-535: `hf_xet` should support `BinaryIO` for upload) + path_or_fileobj=file_to_upload.local_path, + path_in_repo=file_to_upload.path_in_repo, + ) + for file_to_upload in files_to_upload + ] + + # Upload files (append mode expected - no need for lock) + logger.debug("Uploading files for scheduled commit.") + commit_info = self.api.create_commit( + repo_id=self.repo_id, + repo_type=self.repo_type, + operations=add_operations, + commit_message="Scheduled Commit", + revision=self.revision, + ) + + for file in files_to_upload: + self.last_uploaded[file.local_path] = file.last_modified + + self.last_push_time = time.time() + + return commit_info diff --git a/trackio/compression.py b/trackio/compression.py new file mode 100644 index 0000000000000000000000000000000000000000..a6b0a59e46d56237c6cc5da742cfb653fd72648a --- /dev/null +++ b/trackio/compression.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +import gzip +import io +from typing import NoReturn + +from starlette.datastructures import Headers, MutableHeaders +from starlette.types import ASGIApp, Message, Receive, Scope, Send + +try: + import brotli + + HAS_BROTLI = True +except ImportError: + brotli = None + HAS_BROTLI = False + +COMPRESSIBLE_CONTENT_TYPES = ( + "application/json", + "application/javascript", + "application/manifest+json", + "application/xml", + "application/xhtml+xml", + "image/svg+xml", + "text/", +) + + +class CompressionMiddleware: + """Negotiates Brotli, then gzip, then identity based on Accept-Encoding. + + Only text-like content types are compressed, so already-compressed media + (images, audio, video) and zero-copy file sends pass through untouched. + """ + + def __init__( + self, + app: ASGIApp, + minimum_size: int = 512, + gzip_level: int = 6, + brotli_quality: int = 5, + ) -> None: + self.app = app + self.minimum_size = minimum_size + self.gzip_level = gzip_level + self.brotli_quality = brotli_quality + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + + accept_encoding = Headers(scope=scope).get("Accept-Encoding", "") + responder: _CompressionResponder + if HAS_BROTLI and "br" in accept_encoding: + responder = _BrotliResponder( + self.app, self.minimum_size, self.brotli_quality + ) + elif "gzip" in accept_encoding: + responder = _GZipResponder(self.app, self.minimum_size, self.gzip_level) + else: + responder = _CompressionResponder(self.app, self.minimum_size) + + await responder(scope, receive, send) + + +class _CompressionResponder: + content_encoding: str = "" + + def __init__(self, app: ASGIApp, minimum_size: int) -> None: + self.app = app + self.minimum_size = minimum_size + self.send: Send = _unattached_send + self.initial_message: Message = {} + self.started = False + self.passthrough = True + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + self.send = send + await self.app(scope, receive, self.send_with_compression) + + async def send_with_compression(self, message: Message) -> None: + message_type = message["type"] + if message_type == "http.response.start": + self.initial_message = message + headers = Headers(raw=message["headers"]) + content_type = headers.get("content-type", "") + already_encoded = "content-encoding" in headers + self.passthrough = already_encoded or not content_type.startswith( + COMPRESSIBLE_CONTENT_TYPES + ) + elif message_type == "http.response.body" and self.passthrough: + if not self.started: + self.started = True + await self.send(self.initial_message) + await self.send(message) + elif message_type == "http.response.body" and not self.started: + self.started = True + body = message.get("body", b"") + more_body = message.get("more_body", False) + if len(body) < self.minimum_size and not more_body: + await self.send(self.initial_message) + await self.send(message) + else: + compressed = self.apply_compression(body, more_body=more_body) + headers = MutableHeaders(raw=self.initial_message["headers"]) + headers.add_vary_header("Accept-Encoding") + if compressed != body: + headers["Content-Encoding"] = self.content_encoding + if more_body: + del headers["Content-Length"] + else: + headers["Content-Length"] = str(len(compressed)) + message["body"] = compressed + await self.send(self.initial_message) + await self.send(message) + elif message_type == "http.response.body": + body = message.get("body", b"") + more_body = message.get("more_body", False) + message["body"] = self.apply_compression(body, more_body=more_body) + await self.send(message) + elif message_type == "http.response.pathsend": + await self.send(self.initial_message) + await self.send(message) + + def apply_compression(self, body: bytes, *, more_body: bool) -> bytes: + return body + + +class _GZipResponder(_CompressionResponder): + content_encoding = "gzip" + + def __init__(self, app: ASGIApp, minimum_size: int, compresslevel: int) -> None: + super().__init__(app, minimum_size) + self.buffer = io.BytesIO() + self.file = gzip.GzipFile( + mode="wb", fileobj=self.buffer, compresslevel=compresslevel + ) + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + with self.buffer, self.file: + await super().__call__(scope, receive, send) + + def apply_compression(self, body: bytes, *, more_body: bool) -> bytes: + self.file.write(body) + if not more_body: + self.file.close() + data = self.buffer.getvalue() + self.buffer.seek(0) + self.buffer.truncate() + return data + + +class _BrotliResponder(_CompressionResponder): + content_encoding = "br" + + def __init__(self, app: ASGIApp, minimum_size: int, quality: int) -> None: + super().__init__(app, minimum_size) + self.compressor = brotli.Compressor(quality=quality) + + def apply_compression(self, body: bytes, *, more_body: bool) -> bytes: + data = self.compressor.process(body) + if not more_body: + data += self.compressor.finish() + return data + + +async def _unattached_send(message: Message) -> NoReturn: + raise RuntimeError("send awaitable not set") diff --git a/trackio/context_vars.py b/trackio/context_vars.py new file mode 100644 index 0000000000000000000000000000000000000000..ef1404001b8856446ec2940eee81578d634a51fd --- /dev/null +++ b/trackio/context_vars.py @@ -0,0 +1,21 @@ +import contextvars +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from trackio.run import Run + +current_run: contextvars.ContextVar["Run | None"] = contextvars.ContextVar( + "current_run", default=None +) +current_project: contextvars.ContextVar[str | None] = contextvars.ContextVar( + "current_project", default=None +) +current_server: contextvars.ContextVar[str | None] = contextvars.ContextVar( + "current_server", default=None +) +current_space_id: contextvars.ContextVar[str | None] = contextvars.ContextVar( + "current_space_id", default=None +) +current_server_write_token: contextvars.ContextVar[str | None] = contextvars.ContextVar( + "current_server_write_token", default=None +) diff --git a/trackio/cpu.py b/trackio/cpu.py new file mode 100644 index 0000000000000000000000000000000000000000..e11274786d1f17d7d5415286f2fd79a96f7ae2dc --- /dev/null +++ b/trackio/cpu.py @@ -0,0 +1,303 @@ +import importlib +import threading +import time +import warnings +from typing import TYPE_CHECKING, Any + +from trackio import context_vars + +if TYPE_CHECKING: + from trackio.run import Run + +psutil: Any = None +PSUTIL_AVAILABLE = False +_psutil_lock = threading.Lock() + + +def _ensure_psutil(): + global PSUTIL_AVAILABLE, psutil + if PSUTIL_AVAILABLE: + return psutil + with _psutil_lock: + if PSUTIL_AVAILABLE: + return psutil + try: + psutil = importlib.import_module("psutil") + PSUTIL_AVAILABLE = True + return psutil + except ImportError: + raise ImportError( + "psutil is required for CPU and RAM monitoring. " + "Install it with: pip install psutil" + ) + + +def cpu_available() -> bool: + """ + Check if CPU and RAM monitoring is available. + + Returns True if psutil is installed. + """ + try: + _ensure_psutil() + return True + except ImportError: + return False + except Exception: + return False + + +def collect_cpu_metrics( + prev_disk_counters: Any = None, + prev_net_counters: Any = None, + elapsed: float | None = None, + include_static: bool = True, +) -> dict: + """ + Collect CPU, RAM, disk, network, and sensor metrics using psutil. + + Args: + prev_disk_counters: Previous disk I/O counters for computing read/write rates. + If None, only cumulative values are reported. + prev_net_counters: Previous network I/O counters for computing send/recv rates. + If None, only cumulative values are reported. + elapsed: Seconds since prev_disk_counters and prev_net_counters were captured. + If None or non-positive, cumulative values are reported instead of rates. + include_static: Whether to include mostly static metrics such as CPU + frequency and core counts. + + Returns: + Dictionary of system metrics. + """ + if not PSUTIL_AVAILABLE: + try: + _ensure_psutil() + except ImportError: + return {} + + metrics = {} + + try: + per_core = psutil.cpu_percent(interval=0.1, percpu=True) + for i, pct in enumerate(per_core): + metrics[f"cpu/{i}/utilization"] = pct + if per_core: + metrics["cpu/utilization"] = sum(per_core) / len(per_core) + except Exception: + pass + + if include_static: + try: + cpu_freq = psutil.cpu_freq() + if cpu_freq: + metrics["cpu/frequency"] = cpu_freq.current + if cpu_freq.max > 0: + metrics["cpu/frequency_max"] = cpu_freq.max + except Exception: + pass + + try: + cpu_count_logical = psutil.cpu_count(logical=True) + if cpu_count_logical is not None: + metrics["cpu/count_logical"] = cpu_count_logical + cpu_count_physical = psutil.cpu_count(logical=False) + if cpu_count_physical is not None: + metrics["cpu/count_physical"] = cpu_count_physical + except Exception: + pass + + try: + mem = psutil.virtual_memory() + metrics["memory/used"] = mem.used / (1024**3) + metrics["memory/total"] = mem.total / (1024**3) + metrics["memory/available"] = mem.available / (1024**3) + metrics["memory/percent"] = mem.percent + except Exception: + pass + + try: + swap = psutil.swap_memory() + metrics["swap/used"] = swap.used / (1024**3) + metrics["swap/total"] = swap.total / (1024**3) + metrics["swap/percent"] = swap.percent + except Exception: + pass + + try: + disk = psutil.disk_io_counters() + if disk is not None: + if prev_disk_counters is not None and elapsed and elapsed > 0: + metrics["disk/read_mb_per_sec"] = ( + (disk.read_bytes - prev_disk_counters.read_bytes) + / elapsed + / (1024**2) + ) + metrics["disk/write_mb_per_sec"] = ( + (disk.write_bytes - prev_disk_counters.write_bytes) + / elapsed + / (1024**2) + ) + metrics["disk/read_iops"] = ( + disk.read_count - prev_disk_counters.read_count + ) / elapsed + metrics["disk/write_iops"] = ( + disk.write_count - prev_disk_counters.write_count + ) / elapsed + else: + metrics["disk/read_bytes"] = disk.read_bytes + metrics["disk/write_bytes"] = disk.write_bytes + except Exception: + pass + + try: + net = psutil.net_io_counters() + if net is not None: + if prev_net_counters is not None and elapsed and elapsed > 0: + metrics["network/sent_mb_per_sec"] = ( + (net.bytes_sent - prev_net_counters.bytes_sent) + / elapsed + / (1024**2) + ) + metrics["network/recv_mb_per_sec"] = ( + (net.bytes_recv - prev_net_counters.bytes_recv) + / elapsed + / (1024**2) + ) + else: + metrics["network/sent_bytes"] = net.bytes_sent + metrics["network/recv_bytes"] = net.bytes_recv + except Exception: + pass + + try: + sensors = psutil.sensors_temperatures() + if sensors: + for chip_name, entries in sensors.items(): + for i, entry in enumerate(entries): + label = ( + entry.label.strip() + if entry.label and entry.label.strip() + else f"{chip_name}_{i}" + ) + metrics[f"temp/{label}"] = entry.current + except Exception: + pass + + try: + battery = psutil.sensors_battery() + if battery is not None: + metrics["battery/percent"] = battery.percent + metrics["battery/power_plugged"] = int(battery.power_plugged) + except Exception: + pass + + return metrics + + +class CpuMonitor: + def __init__(self, run: "Run", interval: float = 10.0): + self._run = run + self._interval = interval + self._stop_flag = threading.Event() + self._thread: "threading.Thread | None" = None + self._last_disk_counters: Any = None + self._last_net_counters: Any = None + self._last_time: float | None = None + self._include_static_next = True + + def start(self): + if not PSUTIL_AVAILABLE: + try: + _ensure_psutil() + except ImportError: + warnings.warn( + "auto_log_cpu=True but psutil is not installed. " + "CPU and RAM logging disabled. Install with: pip install psutil" + ) + return + + self._thread = threading.Thread(target=self._monitor_loop, daemon=True) + self._thread.start() + + def stop(self): + self._stop_flag.set() + if self._thread is not None: + self._thread.join(timeout=2.0) + + def _monitor_loop(self): + try: + self._last_disk_counters = psutil.disk_io_counters() + except Exception: + pass + try: + self._last_net_counters = psutil.net_io_counters() + except Exception: + pass + self._last_time = time.monotonic() + + while not self._stop_flag.is_set(): + self._stop_flag.wait(timeout=self._interval) + if self._stop_flag.is_set(): + break + try: + now = time.monotonic() + elapsed = now - self._last_time if self._last_time is not None else None + metrics = collect_cpu_metrics( + prev_disk_counters=self._last_disk_counters, + prev_net_counters=self._last_net_counters, + elapsed=elapsed, + include_static=self._include_static_next, + ) + self._include_static_next = False + try: + self._last_disk_counters = psutil.disk_io_counters() + except Exception: + self._last_disk_counters = None + try: + self._last_net_counters = psutil.net_io_counters() + except Exception: + self._last_net_counters = None + self._last_time = now + if metrics: + self._run.log_system(metrics) + except Exception: + pass + + +def log_cpu(run: "Run | None" = None) -> dict: + """ + Log CPU, RAM, disk, network, and sensor metrics to the current or specified run + as system metrics. + + Args: + run: Optional Run instance. If None, uses current run from context. + + Returns: + dict: The system metrics that were logged. + + Example: + ```python + import trackio + + run = trackio.init(project="my-project") + trackio.log({"loss": 0.5}) + trackio.log_cpu() + ``` + """ + if run is None: + run = context_vars.current_run.get() + if run is None: + raise RuntimeError("Call trackio.init() before trackio.log_cpu().") + + try: + _ensure_psutil() + except ImportError: + warnings.warn( + "trackio.log_cpu() requires psutil. Install it with: pip install trackio[cpu]" + ) + return {} + + metrics = collect_cpu_metrics() + if metrics: + run.log_system(metrics) + return metrics diff --git a/trackio/deploy.py b/trackio/deploy.py new file mode 100644 index 0000000000000000000000000000000000000000..d646cc05e2f0b4f9cc3b0b5999b48a57ec4621c1 --- /dev/null +++ b/trackio/deploy.py @@ -0,0 +1,1332 @@ +import importlib.metadata +import io +import json as json_mod +import os +import tempfile +import threading +import time +import warnings +from collections import Counter +from importlib.resources import files +from pathlib import Path + +import httpx +import huggingface_hub +from gradio_client import handle_file +from httpx import ReadTimeout +from huggingface_hub import Volume +from huggingface_hub.errors import ( + BucketNotFoundError, + HfHubHTTPError, + RepositoryNotFoundError, +) + +import trackio +from trackio.bucket_storage import ( + create_bucket_if_not_exists, + export_from_bucket_for_static, + upload_project_to_bucket, + upload_project_to_bucket_for_static, +) +from trackio.cas import PARTIAL_BLOB_GLOB +from trackio.frontend_config import resolve_frontend_dir +from trackio.pending_uploads import replay_pending_uploads +from trackio.remote_client import RemoteClient +from trackio.sqlite_storage import SQLiteStorage +from trackio.utils import ( + get_or_create_project_hash, + on_spaces, + preprocess_space_and_dataset_ids, + project_artifacts_dir, + project_media_dir, + warn_dataset_persistence_deprecated, +) + +SPACE_HOST_URL = "https://{user_name}-{space_name}.hf.space/" +SPACE_URL = "https://huggingface.co/spaces/{space_id}" +_BOLD_ORANGE = "\033[1m\033[38;5;208m" +_RESET = "\033[0m" + + +def raise_if_space_is_frozen_for_logging(space_id: str) -> None: + try: + info = huggingface_hub.HfApi().space_info(space_id) + except RepositoryNotFoundError: + return + if getattr(info, "sdk", None) == "static": + raise RuntimeError( + f"Cannot log to Hugging Face Space '{space_id}' because it has been frozen " + f"(it uses the static SDK: a read-only dashboard with no live Trackio server).\n\n" + f"Use a different space_id for training, or create a new Gradio Trackio Space. " + f"Freezing converts a live Gradio Space to static after a run; a frozen Space " + f'cannot accept new logs. See trackio.sync(..., sdk="static") in the Trackio docs.' + ) + + +def _readme_linked_hub_yaml(dataset_id: str | None) -> str: + if dataset_id is not None: + return f"datasets:\n - {dataset_id}\n" + return "" + + +_CUSTOM_SPACE_FRONTEND_DIR = "trackio_custom_frontend" + + +def _space_app_py(frontend_dir: str | None = None) -> str: + if frontend_dir is None: + return "import trackio\ntrackio.show()\n" + return f'import trackio\ntrackio.show(frontend_dir="{frontend_dir}")\n' + + +def _upload_frontend_folder( + hf_api: huggingface_hub.HfApi, + *, + repo_id: str, + repo_type: str, + folder_path: str | Path, + path_in_repo: str | None = None, +) -> None: + kwargs = { + "repo_id": repo_id, + "repo_type": repo_type, + "folder_path": str(folder_path), + } + if path_in_repo is not None: + kwargs["path_in_repo"] = path_in_repo + hf_api.upload_folder(**kwargs) + + +def _retry_hf_write(op_name: str, fn, retries: int = 4, initial_delay: float = 1.5): + delay = initial_delay + for attempt in range(1, retries + 1): + try: + return fn() + except ReadTimeout: + if attempt == retries: + raise + print( + f"* {op_name} timed out (attempt {attempt}/{retries}). Retrying in {delay:.1f}s..." + ) + time.sleep(delay) + delay = min(delay * 2, 12) + except HfHubHTTPError as e: + status = e.response.status_code if e.response is not None else None + if status is None or status < 500 or attempt == retries: + raise + print( + f"* {op_name} failed with HTTP {status} (attempt {attempt}/{retries}). Retrying in {delay:.1f}s..." + ) + time.sleep(delay) + delay = min(delay * 2, 12) + + +def _get_space_volumes( + space_id: str, hf_api: huggingface_hub.HfApi | None = None +) -> list[Volume]: + """ + Return mounted volumes for a Space. + + `HfApi.get_space_runtime()` does not always populate `volumes`, even when the + mount exists. Fall back to `space_info().runtime.volumes`, which currently + carries the volume metadata for running Spaces. + """ + hf_api = hf_api or huggingface_hub.HfApi() + runtime = hf_api.get_space_runtime(space_id) + if runtime.volumes: + return list(runtime.volumes) + + info = hf_api.space_info(space_id) + if info.runtime and info.runtime.volumes: + return list(info.runtime.volumes) + + return [] + + +def _get_space_bucket_at_data_mount( + space_id: str, hf_api: huggingface_hub.HfApi | None = None +) -> str | None: + for volume in _get_space_volumes(space_id, hf_api=hf_api): + if volume.type == "bucket" and volume.mount_path == "/data": + return volume.source + return None + + +def _get_existing_space_bucket( + space_id: str, hf_api: huggingface_hub.HfApi | None = None +) -> str | None: + """Return the Trackio bucket for a Space, preferring the canonical /data mount.""" + bucket_at_data = _get_space_bucket_at_data_mount(space_id, hf_api=hf_api) + if bucket_at_data is not None: + return bucket_at_data + + for volume in _get_space_volumes(space_id, hf_api=hf_api): + if volume.type == "bucket": + return volume.source + return None + + +def _get_existing_static_space_bucket( + space_id: str, hf_api: huggingface_hub.HfApi | None = None +) -> str | None: + hf_api = hf_api or huggingface_hub.HfApi() + try: + config_path = hf_api.hf_hub_download( + repo_id=space_id, + repo_type="space", + filename="config.json", + ) + except (FileNotFoundError, HfHubHTTPError, OSError, ValueError): + return None + + try: + with open(config_path, encoding="utf-8") as config_file: + config = json_mod.load(config_file) + except (OSError, ValueError, TypeError): + return None + + bucket_id = config.get("bucket_id") + if isinstance(bucket_id, str) and bucket_id: + return bucket_id + return None + + +def _ensure_bucket_mounted_at_data( + space_id: str, + bucket_id: str, + hf_api: huggingface_hub.HfApi | None = None, +) -> None: + hf_api = hf_api or huggingface_hub.HfApi() + existing = _get_space_volumes(space_id, hf_api=hf_api) + already_mounted = any( + v.type == "bucket" and v.source == bucket_id and v.mount_path == "/data" + for v in existing + ) + if not already_mounted: + preserved = [ + v + for v in existing + if not ( + v.type == "bucket" + and (v.source == bucket_id or v.mount_path == "/data") + ) + ] + hf_api.set_space_volumes( + space_id, + preserved + [Volume(type="bucket", source=bucket_id, mount_path="/data")], + ) + print(f"* Attached bucket {bucket_id} at '/data'") + + existing_variables = hf_api.get_space_variables(space_id) + current_trackio_dir = getattr(existing_variables.get("TRACKIO_DIR"), "value", None) + if current_trackio_dir != "/data/trackio": + huggingface_hub.add_space_variable(space_id, "TRACKIO_DIR", "/data/trackio") + current_bucket_id = getattr( + existing_variables.get("TRACKIO_BUCKET_ID"), "value", None + ) + if current_bucket_id != bucket_id: + huggingface_hub.add_space_variable(space_id, "TRACKIO_BUCKET_ID", bucket_id) + + +def _bucket_exists(bucket_id: str, hf_api: huggingface_hub.HfApi | None = None) -> bool: + hf_api = hf_api or huggingface_hub.HfApi() + try: + hf_api.bucket_info(bucket_id) + return True + except BucketNotFoundError: + return False + + +def _find_available_bucket_id( + preferred_bucket_id: str, hf_api: huggingface_hub.HfApi | None = None +) -> str: + hf_api = hf_api or huggingface_hub.HfApi() + if not _bucket_exists(preferred_bucket_id, hf_api): + return preferred_bucket_id + + suffix = 2 + while True: + candidate = f"{preferred_bucket_id}-{suffix}" + if not _bucket_exists(candidate, hf_api): + return candidate + suffix += 1 + + +def resolve_auto_bucket_id( + space_id: str, + preferred_bucket_id: str, + hf_api: huggingface_hub.HfApi | None = None, +) -> str: + """ + Resolve the bucket to use for an auto-generated bucket ID. + + Rules: + - Existing Space with a bucket mounted at /data -> reuse that bucket. + - Existing static Space with a bucket_id in config.json -> reuse that bucket. + - Otherwise -> use the preferred auto bucket ID if free, or a suffixed variant. + """ + hf_api = hf_api or huggingface_hub.HfApi() + try: + info = hf_api.space_info(space_id) + except RepositoryNotFoundError: + pass + else: + existing_bucket_id = _get_existing_space_bucket(space_id, hf_api=hf_api) + if existing_bucket_id is None and getattr(info, "sdk", None) == "static": + existing_bucket_id = _get_existing_static_space_bucket( + space_id, hf_api=hf_api + ) + if existing_bucket_id is not None: + return existing_bucket_id + + bucket_id = _find_available_bucket_id(preferred_bucket_id, hf_api) + if bucket_id != preferred_bucket_id: + print( + f"* Auto-generated bucket {preferred_bucket_id} already exists; " + f"using {bucket_id} instead" + ) + return bucket_id + + +def _get_source_install_dependencies() -> str: + """Get trackio dependencies from pyproject.toml for source installs.""" + try: + import tomllib # noqa: PLC0415 + except ModuleNotFoundError: + import tomli as tomllib # noqa: PLC0415 + + trackio_path = files("trackio") + pyproject_path = Path(trackio_path).parent / "pyproject.toml" + with open(pyproject_path, "rb") as f: + pyproject = tomllib.load(f) + deps = pyproject["project"]["dependencies"] + spaces_deps = ( + pyproject["project"].get("optional-dependencies", {}).get("spaces", []) + ) + mcp_deps = pyproject["project"].get("optional-dependencies", {}).get("mcp", []) + return "\n".join(deps + spaces_deps + mcp_deps) + + +def _get_space_install_requirement() -> str: + return f"trackio[spaces,mcp]=={trackio.__version__}" + + +def _is_trackio_installed_from_source() -> bool: + """Check if trackio is installed from source/editable install vs PyPI.""" + try: + trackio_file = trackio.__file__ + if "site-packages" not in trackio_file and "dist-packages" not in trackio_file: + return True + + dist = importlib.metadata.distribution("trackio") + if dist.files: + files = list(dist.files) + has_pth = any(".pth" in str(f) for f in files) + if has_pth: + return True + + return False + except ( + AttributeError, + importlib.metadata.PackageNotFoundError, + importlib.metadata.MetadataError, + ValueError, + TypeError, + ): + return True + + +def _upload_source_tree( + hf_api: huggingface_hub.HfApi, space_id: str, parent_commit: str | None = None +) -> huggingface_hub.CommitInfo: + return hf_api.upload_folder( + repo_id=space_id, + repo_type="space", + folder_path=files("trackio"), + path_in_repo="trackio", + parent_commit=parent_commit, + ignore_patterns=[ + "README.md", + "frontend/node_modules/**", + "frontend/src/**", + "frontend/.gitignore", + "frontend/package.json", + "frontend/package-lock.json", + "frontend/vite.config.js", + "frontend/svelte.config.js", + "**/__pycache__/**", + "*.pyc", + ], + ) + + +def deploy_as_space( + space_id: str, + space_storage: huggingface_hub.SpaceStorage | None = None, + dataset_id: str | None = None, + bucket_id: str | None = None, + private: bool | None = None, + frontend_dir: str | Path | None = None, +): + if on_spaces(): # in case a repo with this function is uploaded to spaces + return + + if dataset_id is not None and bucket_id is not None: + raise ValueError( + "Cannot use bucket volume options together with dataset_id; use one persistence mode." + ) + if dataset_id is not None: + warn_dataset_persistence_deprecated() + + trackio_path = files("trackio") + + hf_api = huggingface_hub.HfApi() + + try: + huggingface_hub.create_repo( + space_id, + private=private, + space_sdk="gradio", + space_storage=space_storage, + repo_type="space", + exist_ok=True, + ) + except HfHubHTTPError as e: + if e.response.status_code in [401, 403]: # unauthorized or forbidden + print("Need 'write' access token to create a Spaces repo.") + huggingface_hub.login(add_to_git_credential=False) + huggingface_hub.create_repo( + space_id, + private=private, + space_sdk="gradio", + space_storage=space_storage, + repo_type="space", + exist_ok=True, + ) + else: + raise ValueError(f"Failed to create Space: {e}") + + # We can assume huggingface-hub is available; requirements.txt pins trackio. + # Make sure necessary dependencies are installed by creating a requirements.txt. + is_source_install = _is_trackio_installed_from_source() + resolved_frontend = resolve_frontend_dir(frontend_dir, announce=True) + + if bucket_id is not None: + create_bucket_if_not_exists(bucket_id, private=private) + + with open(Path(trackio_path, "README.md"), "r", encoding="utf-8") as f: + readme_content = f.read() + readme_content = readme_content.replace("sdk_version: {GRADIO_VERSION}\n", "") + readme_content = readme_content.replace("{APP_FILE}", "app.py") + readme_content = readme_content.replace( + "{LINKED_HUB_METADATA}", _readme_linked_hub_yaml(dataset_id) + ) + readme_buffer = io.BytesIO(readme_content.encode("utf-8")) + hf_api.upload_file( + path_or_fileobj=readme_buffer, + path_in_repo="README.md", + repo_id=space_id, + repo_type="space", + ) + + if is_source_install: + requirements_content = _get_source_install_dependencies() + else: + requirements_content = _get_space_install_requirement() + + requirements_buffer = io.BytesIO(requirements_content.encode("utf-8")) + hf_api.upload_file( + path_or_fileobj=requirements_buffer, + path_in_repo="requirements.txt", + repo_id=space_id, + repo_type="space", + ) + + huggingface_hub.utils.disable_progress_bars() + + if is_source_install: + dist_index = ( + Path(trackio.__file__).resolve().parent / "frontend" / "dist" / "index.html" + ) + if not dist_index.is_file() and not resolved_frontend.is_custom: + raise ValueError( + "The Trackio frontend build is missing. From the repository root run " + "`cd trackio/frontend && npm ci && npm run build`, then deploy again." + ) + _upload_source_tree(hf_api, space_id) + + if resolved_frontend.is_custom: + _upload_frontend_folder( + hf_api, + repo_id=space_id, + repo_type="space", + folder_path=resolved_frontend.path, + path_in_repo=_CUSTOM_SPACE_FRONTEND_DIR, + ) + + app_file_content = _space_app_py( + _CUSTOM_SPACE_FRONTEND_DIR if resolved_frontend.is_custom else None + ) + app_file_buffer = io.BytesIO(app_file_content.encode("utf-8")) + hf_api.upload_file( + path_or_fileobj=app_file_buffer, + path_in_repo="app.py", + repo_id=space_id, + repo_type="space", + ) + + if hf_token := huggingface_hub.utils.get_token(): + huggingface_hub.add_space_secret(space_id, "HF_TOKEN", hf_token) + if bucket_id is not None: + _ensure_bucket_mounted_at_data(space_id, bucket_id, hf_api) + elif dataset_id is not None: + huggingface_hub.add_space_variable(space_id, "TRACKIO_DATASET_ID", dataset_id) + if logo_light_url := os.environ.get("TRACKIO_LOGO_LIGHT_URL"): + huggingface_hub.add_space_variable( + space_id, "TRACKIO_LOGO_LIGHT_URL", logo_light_url + ) + if logo_dark_url := os.environ.get("TRACKIO_LOGO_DARK_URL"): + huggingface_hub.add_space_variable( + space_id, "TRACKIO_LOGO_DARK_URL", logo_dark_url + ) + if plot_order := os.environ.get("TRACKIO_PLOT_ORDER"): + huggingface_hub.add_space_variable(space_id, "TRACKIO_PLOT_ORDER", plot_order) + if theme := os.environ.get("TRACKIO_THEME"): + huggingface_hub.add_space_variable(space_id, "TRACKIO_THEME", theme) + huggingface_hub.add_space_variable(space_id, "GRADIO_MCP_SERVER", "True") + + +def create_space_if_not_exists( + space_id: str, + space_storage: huggingface_hub.SpaceStorage | None = None, + dataset_id: str | None = None, + bucket_id: str | None = None, + private: bool | None = None, + frontend_dir: str | Path | None = None, +) -> bool: + """ + Creates a new Hugging Face Space if it does not exist. Returns True if a + new Space was created, False if an existing Space was found. + + Args: + space_id (`str`): + The ID of the Space to create. + space_storage ([`~huggingface_hub.SpaceStorage`], *optional*): + Choice of persistent storage tier for the Space. + dataset_id (`str`, *optional*): + Deprecated: persisting trackio data to a Hugging Face Dataset will be + removed in a future version of trackio. Use `bucket_id` (a Hugging + Face Bucket) instead. + bucket_id (`str`, *optional*): + Full Hub bucket id (`namespace/name`) to attach via the Hub volumes API (platform mount). + Sets `TRACKIO_DIR` to the mount path. + private (`bool`, *optional*): + Whether to make the Space private. If `None` (default), the repo will be + public unless the organization's default is private. This value is ignored + if the repo already exists. + """ + if "/" not in space_id: + raise ValueError( + f"Invalid space ID: {space_id}. Must be in the format: username/reponame or orgname/reponame." + ) + if dataset_id is not None and "/" not in dataset_id: + raise ValueError( + f"Invalid dataset ID: {dataset_id}. Must be in the format: username/datasetname or orgname/datasetname." + ) + if bucket_id is not None and "/" not in bucket_id: + raise ValueError( + f"Invalid bucket ID: {bucket_id}. Must be in the format: username/bucketname or orgname/bucketname." + ) + if dataset_id is not None: + warn_dataset_persistence_deprecated() + try: + huggingface_hub.repo_info(space_id, repo_type="space") + print( + f"* Found existing space: {_BOLD_ORANGE}{SPACE_URL.format(space_id=space_id)}{_RESET}" + ) + if bucket_id is not None: + create_bucket_if_not_exists(bucket_id, private=private) + _ensure_bucket_mounted_at_data(space_id, bucket_id) + elif dataset_id is not None: + huggingface_hub.add_space_variable( + space_id, "TRACKIO_DATASET_ID", dataset_id + ) + resolved_frontend = resolve_frontend_dir(frontend_dir, announce=False) + if resolved_frontend.is_custom: + deploy_as_space( + space_id, + space_storage, + dataset_id, + bucket_id, + private, + frontend_dir=frontend_dir, + ) + return False + except RepositoryNotFoundError: + pass + except HfHubHTTPError as e: + if e.response.status_code in [401, 403]: # unauthorized or forbidden + print("Need 'write' access token to create a Spaces repo.") + huggingface_hub.login(add_to_git_credential=False) + else: + raise ValueError(f"Failed to create Space: {e}") + + print( + f"* Creating new space: {_BOLD_ORANGE}{SPACE_URL.format(space_id=space_id)}{_RESET}" + ) + deploy_as_space( + space_id, + space_storage, + dataset_id, + bucket_id, + private, + frontend_dir=frontend_dir, + ) + threading.Thread( + target=_warn_if_space_build_fails, args=(space_id,), daemon=True + ).start() + return True + + +def space_is_running(space_id: str) -> bool: + try: + info = huggingface_hub.HfApi().space_info(space_id, timeout=30) + return bool(info.runtime) and str(info.runtime.stage) == "RUNNING" + except Exception: + return False + + +def _warn_if_space_build_fails(space_id: str, timeout: int = 600) -> None: + hf_api = huggingface_hub.HfApi() + failure_stages = frozenset( + ("NO_APP_FILE", "CONFIG_ERROR", "BUILD_ERROR", "RUNTIME_ERROR") + ) + start = time.time() + while time.time() - start < timeout: + try: + info = hf_api.space_info(space_id, timeout=30) + if info.runtime: + stage = str(info.runtime.stage) + if stage in failure_stages: + print( + f"\n* WARNING: Space {space_id} failed to build (stage: {stage}). " + f"Logs are being saved to the Bucket and will appear once the Space " + f"is fixed; see the build logs at {SPACE_URL.format(space_id=space_id)}" + ) + return + if stage == "RUNNING": + return + except Exception: + pass + time.sleep(10) + + +def _wait_until_space_running(space_id: str, timeout: int = 300) -> None: + hf_api = huggingface_hub.HfApi() + start = time.time() + delay = 2 + request_timeout = 45.0 + failure_stages = frozenset( + ("NO_APP_FILE", "CONFIG_ERROR", "BUILD_ERROR", "RUNTIME_ERROR") + ) + while time.time() - start < timeout: + try: + info = hf_api.space_info(space_id, timeout=request_timeout) + if info.runtime: + stage = str(info.runtime.stage) + if stage in failure_stages: + raise RuntimeError( + f"Space {space_id} entered terminal stage {stage}. " + "Fix README.md or app files; see build logs on the Hub." + ) + if stage == "RUNNING": + return + except RuntimeError: + raise + except (huggingface_hub.utils.HfHubHTTPError, httpx.RequestError): + pass + time.sleep(delay) + delay = min(delay * 1.5, 15) + raise TimeoutError( + f"Space {space_id} did not reach RUNNING within {timeout}s. " + "Check status and build logs on the Hub." + ) + + +def wait_until_space_exists( + space_id: str, +) -> None: + """ + Blocks the current thread until the Space exists. + + Args: + space_id (`str`): + The ID of the Space to wait for. + + Raises: + `TimeoutError`: If waiting for the Space takes longer than expected. + """ + hf_api = huggingface_hub.HfApi() + delay = 1 + for _ in range(30): + try: + hf_api.space_info(space_id) + return + except (huggingface_hub.utils.HfHubHTTPError, httpx.RequestError): + time.sleep(delay) + delay = min(delay * 2, 60) + raise TimeoutError("Waiting for space to exist took longer than expected") + + +def upload_db_to_space(project: str, space_id: str, force: bool = False) -> None: + """ + Uploads the database of a local Trackio project to a Hugging Face Space. + + This uses the Trackio remote client so newer Trackio Spaces can speak the direct + HTTP API while older Gradio-based Spaces still work through `gradio_client`. + + Args: + project (`str`): + The name of the project to upload. + space_id (`str`): + The ID of the Space to upload to. + force (`bool`, *optional*, defaults to `False`): + If `True`, overwrites the existing database without prompting. If `False`, + prompts for confirmation. + """ + db_path = SQLiteStorage.get_project_db_path(project) + client = RemoteClient( + space_id, + hf_token=huggingface_hub.utils.get_token(), + httpx_kwargs={"timeout": 90}, + ) + + if not force: + try: + existing_projects = client.predict(api_name="/get_all_projects") + if project in existing_projects: + response = input( + f"Database for project '{project}' already exists on Space '{space_id}'. " + f"Overwrite it? (y/N): " + ) + if response.lower() not in ["y", "yes"]: + print("* Upload cancelled.") + return + except Exception as e: + print(f"* Warning: Could not check if project exists on Space: {e}") + print("* Proceeding with upload...") + + client.predict( + api_name="/upload_db_to_space", + project=project, + uploaded_db=handle_file(db_path), + hf_token=huggingface_hub.utils.get_token(), + ) + + +SYNC_BATCH_SIZE = 500 + + +def _replay_pending_uploads( + project: str, + client: "RemoteClient", + hf_token: str | None, +) -> None: + """Replay queued `pending_uploads` rows for `project` via `client` — the + out-of-Run sync path (`trackio sync`); see `replay_pending_uploads` for the + per-kind routing and clearing semantics. + """ + pending_uploads = SQLiteStorage.get_pending_uploads(project) + if not pending_uploads: + return + + def _warn_missing(count: int, sample: str) -> None: + print( + f" Warning: dropping {count} pending upload(s) whose local files " + f"no longer exist (e.g. {sample!r})." + ) + + replay_pending_uploads( + pending_uploads, + project, + predict=client.predict, + hf_token=hf_token, + warn_missing=_warn_missing, + verbose=True, + ) + + +def sync_incremental( + project: str, + space_id: str, + private: bool | None = None, + pending_only: bool = False, + frontend_dir: str | Path | None = None, +) -> None: + """ + Syncs a local Trackio project to a Space via the bulk_log API endpoints + instead of uploading the entire DB file. Supports incremental sync. + + Args: + project: The name of the project to sync. + space_id: The HF Space ID to sync to. + private: Whether to make the Space private if creating. + pending_only: If True, only sync rows tagged with space_id (pending data). + """ + print( + f"* Syncing project '{project}' to: {SPACE_URL.format(space_id=space_id)} (please wait...)" + ) + create_space_if_not_exists(space_id, private=private, frontend_dir=frontend_dir) + wait_until_space_exists(space_id) + hf_token = huggingface_hub.utils.get_token() + expected_run_counts: Counter[str] = Counter() + + client = RemoteClient( + space_id, + hf_token=hf_token, + httpx_kwargs={"timeout": 90}, + ) + + if pending_only: + pending_logs = SQLiteStorage.get_pending_logs(project) + if pending_logs: + logs = pending_logs["logs"] + expected_run_counts.update(log["run"] for log in logs) + for i in range(0, len(logs), SYNC_BATCH_SIZE): + batch = logs[i : i + SYNC_BATCH_SIZE] + print( + f" Syncing metrics: {min(i + SYNC_BATCH_SIZE, len(logs))}/{len(logs)}..." + ) + client.predict(api_name="/bulk_log", logs=batch, hf_token=hf_token) + SQLiteStorage.clear_pending_logs(project, pending_logs["ids"]) + + pending_sys = SQLiteStorage.get_pending_system_logs(project) + if pending_sys: + logs = pending_sys["logs"] + for i in range(0, len(logs), SYNC_BATCH_SIZE): + batch = logs[i : i + SYNC_BATCH_SIZE] + print( + f" Syncing system metrics: {min(i + SYNC_BATCH_SIZE, len(logs))}/{len(logs)}..." + ) + client.predict( + api_name="/bulk_log_system", logs=batch, hf_token=hf_token + ) + SQLiteStorage.clear_pending_system_logs(project, pending_sys["ids"]) + + _replay_pending_uploads(project, client, hf_token) + else: + all_logs = SQLiteStorage.get_all_logs_for_sync(project) + if all_logs: + expected_run_counts.update(log["run"] for log in all_logs) + for i in range(0, len(all_logs), SYNC_BATCH_SIZE): + batch = all_logs[i : i + SYNC_BATCH_SIZE] + print( + f" Syncing metrics: {min(i + SYNC_BATCH_SIZE, len(all_logs))}/{len(all_logs)}..." + ) + client.predict(api_name="/bulk_log", logs=batch, hf_token=hf_token) + + all_sys_logs = SQLiteStorage.get_all_system_logs_for_sync(project) + if all_sys_logs: + for i in range(0, len(all_sys_logs), SYNC_BATCH_SIZE): + batch = all_sys_logs[i : i + SYNC_BATCH_SIZE] + print( + f" Syncing system metrics: {min(i + SYNC_BATCH_SIZE, len(all_sys_logs))}/{len(all_sys_logs)}..." + ) + client.predict( + api_name="/bulk_log_system", logs=batch, hf_token=hf_token + ) + + _wait_for_remote_sync(client, project, expected_run_counts) + SQLiteStorage.set_project_metadata(project, "space_id", space_id) + print( + f"* Synced successfully to space: {_BOLD_ORANGE}{SPACE_URL.format(space_id=space_id)}{_RESET}" + ) + + +def _build_remote_client_with_retry( + space_id: str, + timeout: int = 360, + verbose: bool = False, +) -> RemoteClient: + deadline = time.time() + timeout + delay = 2 + last_error: Exception | None = None + while time.time() < deadline: + try: + return RemoteClient( + space_id, + hf_token=huggingface_hub.utils.get_token(), + verbose=verbose, + httpx_kwargs={"timeout": 90}, + ) + except (ValueError, ConnectionError) as e: + last_error = e + time.sleep(delay) + delay = min(delay * 1.5, 15) + raise ConnectionError( + f"Could not connect to Space '{space_id}' within {timeout}s: {last_error}" + ) + + +def _wait_for_remote_sync( + client: RemoteClient, + project: str, + expected_run_counts: Counter[str], + timeout: int = 180, +) -> None: + if not expected_run_counts: + return + + deadline = time.time() + timeout + delay = 2 + last_error: Exception | None = None + pending = dict(expected_run_counts) + + while time.time() < deadline and pending: + completed = [] + for run_name, expected_num_logs in pending.items(): + try: + summary = client.predict( + project=project, run=run_name, api_name="/get_run_summary" + ) + if summary.get("num_logs") == expected_num_logs: + completed.append(run_name) + except Exception as e: + last_error = e + for run_name in completed: + pending.pop(run_name, None) + if pending: + time.sleep(delay) + delay = min(delay * 1.5, 15) + + if pending: + raise TimeoutError( + f"Remote sync for project '{project}' did not become visible for runs " + f"{sorted(pending.items())} within {timeout}s. " + f"Last error: {last_error!r}" + ) + + +def upload_dataset_for_static( + project: str, + dataset_id: str, + private: bool | None = None, +) -> None: + hf_api = huggingface_hub.HfApi() + + try: + huggingface_hub.create_repo( + dataset_id, + private=private, + repo_type="dataset", + exist_ok=True, + ) + except HfHubHTTPError as e: + if e.response.status_code in [401, 403]: + print("Need 'write' access token to create a Dataset repo.") + huggingface_hub.login(add_to_git_credential=False) + huggingface_hub.create_repo( + dataset_id, + private=private, + repo_type="dataset", + exist_ok=True, + ) + else: + raise ValueError(f"Failed to create Dataset: {e}") + + def _upload_folder(folder: Path, path_in_repo: str, ignore_patterns=None) -> None: + if not folder.exists(): + return + _retry_hf_write( + f"Dataset {path_in_repo} upload", + lambda: hf_api.upload_folder( + repo_id=dataset_id, + repo_type="dataset", + folder_path=str(folder), + path_in_repo=path_in_repo, + ignore_patterns=ignore_patterns, + ), + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + output_dir = Path(tmp_dir) + SQLiteStorage.export_for_static_space(project, output_dir) + + _upload_folder(project_media_dir(project), "media") + _upload_folder(project_artifacts_dir(project), "artifacts", [PARTIAL_BLOB_GLOB]) + + _retry_hf_write( + "Dataset upload", + lambda: hf_api.upload_folder( + repo_id=dataset_id, + repo_type="dataset", + folder_path=str(output_dir), + ), + ) + + print(f"* Dataset uploaded: https://huggingface.co/datasets/{dataset_id}") + + +def deploy_as_static_space( + space_id: str, + dataset_id: str | None, + project: str, + bucket_id: str | None = None, + private: bool | None = None, + hf_token: str | None = None, + frontend_dir: str | Path | None = None, +) -> None: + if on_spaces(): + return + + if private is True: + raise ValueError( + "private=True is not supported for static Trackio Spaces. Static Spaces " + "run entirely in the browser, so their snapshot data must be public. " + "Use sdk='gradio' for a private dashboard." + ) + if dataset_id is not None: + warn_dataset_persistence_deprecated() + hf_api = huggingface_hub.HfApi() + + try: + huggingface_hub.create_repo( + space_id, + private=False, + space_sdk="static", + repo_type="space", + exist_ok=True, + ) + except HfHubHTTPError as e: + if e.response.status_code in [401, 403]: + print("Need 'write' access token to create a Spaces repo.") + huggingface_hub.login(add_to_git_credential=False) + huggingface_hub.create_repo( + space_id, + private=False, + space_sdk="static", + repo_type="space", + exist_ok=True, + ) + else: + raise ValueError(f"Failed to create Space: {e}") + + linked = _readme_linked_hub_yaml(dataset_id) + readme_content = ( + f"---\nemoji: 🎯\nsdk: static\npinned: false\ntags:\n - trackio\n{linked}---\n" + ) + _retry_hf_write( + "Static Space README upload", + lambda: hf_api.upload_file( + path_or_fileobj=io.BytesIO(readme_content.encode("utf-8")), + path_in_repo="README.md", + repo_id=space_id, + repo_type="space", + ), + ) + + resolved_frontend = resolve_frontend_dir(frontend_dir, announce=True) + + _retry_hf_write( + "Static Space frontend upload", + lambda: hf_api.upload_folder( + repo_id=space_id, + repo_type="space", + folder_path=str(resolved_frontend.path), + ), + ) + + config = { + "mode": "static", + "project": project, + "private": bool(private), + "version": trackio.__version__, + } + if bucket_id is not None: + config["bucket_id"] = bucket_id + if dataset_id is not None: + config["dataset_id"] = dataset_id + if hf_token is not None: + warnings.warn( + "`hf_token` is ignored by deploy_as_static_space() for static Space " + "deployment and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) + + _retry_hf_write( + "Static Space config upload", + lambda: hf_api.upload_file( + path_or_fileobj=io.BytesIO(json_mod.dumps(config).encode("utf-8")), + path_in_repo="config.json", + repo_id=space_id, + repo_type="space", + ), + ) + + assets_dir = Path(trackio.__file__).resolve().parent / "assets" + if assets_dir.is_dir(): + _retry_hf_write( + "Static Space assets upload", + lambda: hf_api.upload_folder( + repo_id=space_id, + repo_type="space", + folder_path=str(assets_dir), + path_in_repo="static/trackio", + ), + ) + + print( + f"* Static Space deployed: {_BOLD_ORANGE}{SPACE_URL.format(space_id=space_id)}{_RESET}" + ) + + +def sync( + project: str, + space_id: str | None = None, + private: bool | None = None, + force: bool = False, + run_in_background: bool = False, + sdk: str = "gradio", + dataset_id: str | None = None, + bucket_id: str | None = None, + frontend_dir: str | Path | None = None, +) -> str: + """ + Syncs a local Trackio project's database to a Hugging Face Space. + If the Space does not exist, it will be created. Local data is never deleted. + + **Freezing:** Passing ``sdk="static"`` deploys a static Space backed by an HF Bucket + (read-only dashboard, no Gradio server). You can sync the same project again later to + refresh that static Space. If you want a one-time snapshot of an existing Gradio Space, + use ``freeze()`` instead. + + Args: + project (`str`): The name of the project to upload. + space_id (`str`, *optional*): The ID of the Space to upload to (e.g., `"username/space_id"`). + If not provided, checks project metadata first, then generates a random space_id. + private (`bool`, *optional*): + Whether to make the Space private. If None (default), the repo will be + public unless the organization's default is private. This value is ignored + if the repo already exists. Not supported with ``sdk="static"`` because + static Trackio dashboards read snapshot data directly from the browser. + force (`bool`, *optional*, defaults to `False`): + If `True`, overwrite the existing database without prompting for confirmation. + If `False`, prompt the user before overwriting an existing database. + run_in_background (`bool`, *optional*, defaults to `False`): + If `True`, the Space creation and database upload will be run in a background thread. + If `False`, all the steps will be run synchronously. + sdk (`str`, *optional*, defaults to `"gradio"`): + The type of Space to deploy. `"gradio"` deploys a Gradio Space with a live + server. `"static"` freezes the Space: deploys a static Space that reads from an HF Bucket + (no server needed). + dataset_id (`str`, *optional*): + Deprecated: persisting trackio data to a Hugging Face Dataset will be + removed in a future version of trackio. Use `bucket_id` (a Hugging + Face Bucket) instead. + bucket_id (`str`, *optional*): + The ID of the HF Bucket to sync to. By default, a bucket is auto-generated + from the space_id. + Returns: + `str`: The Space ID of the synced project. + """ + if sdk not in ("gradio", "static"): + raise ValueError(f"sdk must be 'gradio' or 'static', got '{sdk}'") + if sdk == "static" and private is True: + raise ValueError( + "private=True is not supported for static Trackio Spaces. Static Spaces " + "run entirely in the browser, so their snapshot data must be public. " + "Use sdk='gradio' for a private dashboard." + ) + bucket_id_was_explicit = bucket_id is not None + + if space_id is None: + space_id = SQLiteStorage.get_space_id(project) + if space_id is None: + space_id = f"{project}-{get_or_create_project_hash(project)}" + space_id, dataset_id, bucket_id = preprocess_space_and_dataset_ids( + space_id, dataset_id, bucket_id + ) + if dataset_id is None and bucket_id is not None and not bucket_id_was_explicit: + bucket_id = resolve_auto_bucket_id(space_id, bucket_id) + + def _do_sync(): + try: + info = huggingface_hub.HfApi().space_info(space_id) + existing_sdk = info.sdk + if existing_sdk and existing_sdk != sdk: + raise ValueError( + f"Space '{space_id}' is a '{existing_sdk}' Space but sdk='{sdk}' was requested. " + f"The sdk must match the existing Space type." + ) + except RepositoryNotFoundError: + pass + + if sdk == "static": + if dataset_id is not None: + upload_dataset_for_static(project, dataset_id, private=False) + deploy_as_static_space( + space_id, + dataset_id, + project, + private=False, + frontend_dir=frontend_dir, + ) + elif bucket_id is not None: + create_bucket_if_not_exists(bucket_id, private=False) + upload_project_to_bucket_for_static(project, bucket_id) + print( + f"* Project data uploaded to bucket: https://huggingface.co/buckets/{bucket_id}" + ) + deploy_as_static_space( + space_id, + None, + project, + bucket_id=bucket_id, + private=False, + frontend_dir=frontend_dir, + ) + else: + if bucket_id is not None: + create_bucket_if_not_exists(bucket_id, private=private) + upload_project_to_bucket(project, bucket_id) + print( + f"* Project data uploaded to bucket: https://huggingface.co/buckets/{bucket_id}" + ) + create_space_if_not_exists( + space_id, + bucket_id=bucket_id, + private=private, + frontend_dir=frontend_dir, + ) + _wait_until_space_running(space_id) + _wait_for_remote_sync( + _build_remote_client_with_retry(space_id), + project, + Counter( + log["run"] + for log in SQLiteStorage.get_all_logs_for_sync(project) + ), + ) + else: + sync_incremental( + project, + space_id, + private=private, + pending_only=False, + frontend_dir=frontend_dir, + ) + SQLiteStorage.set_project_metadata(project, "space_id", space_id) + + if run_in_background: + threading.Thread(target=_do_sync).start() + else: + _do_sync() + return space_id + + +def _get_source_bucket(space_id: str) -> str: + bucket_id = _get_existing_space_bucket(space_id) + if bucket_id is not None: + _ensure_bucket_mounted_at_data(space_id, bucket_id) + return bucket_id + raise ValueError( + f"Space '{space_id}' has no bucket mounted at '/data'. " + f"freeze() requires the source Space to use bucket storage." + ) + + +def freeze( + space_id: str, + project: str, + new_space_id: str | None = None, + private: bool | None = None, + bucket_id: str | None = None, + frontend_dir: str | Path | None = None, +) -> str: + """ + Creates a new static Hugging Face Space containing a read-only snapshot of + the data for the specified project from the source Gradio Space. The data is + read from the bucket attached to the source Space at freeze time. The original + Space is not modified, and the new static Space does not automatically reflect + metrics uploaded to the original Gradio Space after the freeze completes. + + Args: + space_id (`str`): + The ID of the source Gradio Space (e.g., `"username/my-space"` or a + short repo name with the logged-in namespace inferred, like `init()`). + Must be a Gradio Space with a bucket mounted at `/data`. + project (`str`): + The name of the project whose data to include in the frozen Space. + new_space_id (`str`, *optional*): + The ID for the new static Space. If not provided, defaults to + `"{space_id}_static"`. + private (`bool`, *optional*): + Not supported. Frozen static dashboards read snapshot data directly + from the browser, so the destination snapshot must be public. + bucket_id (`str`, *optional*): + The ID of the HF Bucket for the new static Space's data storage. + If not provided, one is auto-generated from the new Space ID. + + Returns: + `str`: The Space ID of the newly created static Space. + """ + if private is True: + raise ValueError( + "private=True is not supported for frozen static Trackio Spaces. Static " + "Spaces run entirely in the browser, so their snapshot data must be " + "public. Use a Gradio Space if the frozen dashboard must stay private." + ) + space_id, _, _ = preprocess_space_and_dataset_ids(space_id, None, None) + + try: + info = huggingface_hub.HfApi().space_info(space_id) + if info.sdk != "gradio": + raise ValueError( + f"Space '{space_id}' is not a Gradio Space (sdk='{info.sdk}'). " + f"freeze() requires a Gradio Space as the source." + ) + except RepositoryNotFoundError: + raise ValueError( + f"Space '{space_id}' not found. Provide an existing Gradio Space ID." + ) + + source_bucket_id = _get_source_bucket(space_id) + print(f"* Reading project data from bucket: {source_bucket_id}") + + bucket_id_was_explicit = bucket_id is not None + + if new_space_id is None: + new_space_id = f"{space_id}_static" + new_space_id, _dataset_id, bucket_id = preprocess_space_and_dataset_ids( + new_space_id, None, bucket_id + ) + if bucket_id is not None and not bucket_id_was_explicit: + bucket_id = resolve_auto_bucket_id(new_space_id, bucket_id) + + hf_api = huggingface_hub.HfApi() + try: + dest_info = hf_api.space_info(new_space_id) + tags = dest_info.tags or [] + if dest_info.sdk != "static" or "trackio" not in tags: + raise ValueError( + f"Space '{new_space_id}' already exists and is not a Trackio static Space " + f"(sdk='{dest_info.sdk}', tags={tags}). Choose a different new_space_id " + f"or delete the existing Space first." + ) + except RepositoryNotFoundError: + pass + + create_bucket_if_not_exists(bucket_id, private=False) + export_from_bucket_for_static(source_bucket_id, bucket_id, project) + print( + f"* Project data uploaded to bucket: https://huggingface.co/buckets/{bucket_id}" + ) + deploy_as_static_space( + new_space_id, + None, + project, + bucket_id=bucket_id, + private=False, + frontend_dir=frontend_dir, + ) + return new_space_id diff --git a/trackio/dummy_commit_scheduler.py b/trackio/dummy_commit_scheduler.py new file mode 100644 index 0000000000000000000000000000000000000000..6068fe8a36b8647ade92dd300af0855f4338826f --- /dev/null +++ b/trackio/dummy_commit_scheduler.py @@ -0,0 +1,19 @@ +from concurrent.futures import Future + + +class DummyCommitSchedulerLock: + def __enter__(self): + return None + + def __exit__(self, exception_type, exception_value, exception_traceback): + pass + + +class DummyCommitScheduler: + def __init__(self): + self.lock = DummyCommitSchedulerLock() + + def trigger(self) -> Future: + fut: Future = Future() + fut.set_result(None) + return fut diff --git a/trackio/exceptions.py b/trackio/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..189f56d718245b65790ff43e14005b846aad2402 --- /dev/null +++ b/trackio/exceptions.py @@ -0,0 +1,2 @@ +class TrackioAPIError(Exception): + pass diff --git a/trackio/fragments.py b/trackio/fragments.py new file mode 100644 index 0000000000000000000000000000000000000000..23906bb2b3fbd5755b7fbaa35444b6408a841cae --- /dev/null +++ b/trackio/fragments.py @@ -0,0 +1,389 @@ +"""Append-only JSONL fragments used as a durable fallback for metric logging. + +Fragments are immutable JSONL files, one writer (process) per subdirectory, so +concurrent training processes never contend on a shared file. They are written +either to a Hugging Face Bucket inbox (when a Space is unreachable) or to a +local inbox directory (when SQLite is unsafe, e.g. on network filesystems), and +are later imported into the project SQLite database by the process that owns it +(the Space or the dashboard server). Records carry the same ``log_id``/ +``alert_id`` UUIDs as the HTTP logging endpoints, and imports use +``INSERT OR IGNORE``, so importing a fragment is idempotent: fragments are +deleted only after a successful import, and re-importing after a crash is +harmless. +""" + +import tempfile +import threading +import uuid +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import huggingface_hub +import orjson + +from trackio import utils +from trackio.typehints import AlertEntry, LogEntry, SystemLogEntry + +FRAGMENT_VERSION = 1 +INBOX_DIR_NAME = "inbox" +BUCKET_INBOX_PREFIX = "trackio/inbox" +BUCKET_MEDIA_PREFIX = "trackio/media" + +METRIC_KIND = "metric" +SYSTEM_METRIC_KIND = "system_metric" +ALERT_KIND = "alert" +KINDS = {METRIC_KIND, SYSTEM_METRIC_KIND, ALERT_KIND} + + +def local_inbox_dir() -> Path: + return utils.TRACKIO_DIR / INBOX_DIR_NAME + + +def metric_record(entry: LogEntry | dict) -> dict: + return { + "v": FRAGMENT_VERSION, + "kind": METRIC_KIND, + "project": entry["project"], + "run": entry["run"], + "run_id": entry.get("run_id"), + "metrics": utils.serialize_values(entry.get("metrics") or {}), + "step": entry.get("step"), + "timestamp": entry.get("timestamp"), + "config": utils.serialize_values(entry.get("config")) + if entry.get("config") + else None, + "log_id": entry.get("log_id"), + } + + +def system_metric_record(entry: SystemLogEntry | dict) -> dict: + return { + "v": FRAGMENT_VERSION, + "kind": SYSTEM_METRIC_KIND, + "project": entry["project"], + "run": entry["run"], + "run_id": entry.get("run_id"), + "metrics": utils.serialize_values(entry.get("metrics") or {}), + "timestamp": entry.get("timestamp"), + "log_id": entry.get("log_id"), + } + + +def alert_record(entry: AlertEntry | dict) -> dict: + return { + "v": FRAGMENT_VERSION, + "kind": ALERT_KIND, + "project": entry["project"], + "run": entry["run"], + "run_id": entry.get("run_id"), + "title": entry["title"], + "text": entry.get("text"), + "level": entry.get("level"), + "step": entry.get("step"), + "timestamp": entry.get("timestamp"), + "alert_id": entry.get("alert_id"), + } + + +def bucket_media_path( + project: str, + run: str | None, + step: int | None, + relative_path: str | None, + filename: str, +) -> str: + parts = [BUCKET_MEDIA_PREFIX, utils.canonical_project_name(project)] + if run: + parts.append(run) + if step is not None: + parts.append(str(step)) + else: + parts.append("files") + if relative_path: + parts.append(str(relative_path)) + parts.append(filename) + return "/".join(parts) + + +class FragmentWriter: + """Writes immutable JSONL fragments for a single writer (process).""" + + def __init__(self, writer_id: str | None = None): + self.writer_id = writer_id or uuid.uuid4().hex[:16] + self._seq = 0 + self._lock = threading.Lock() + + def _next_fragment_name(self) -> str: + with self._lock: + name = f"{self._seq:08d}.jsonl" + self._seq += 1 + return name + + @staticmethod + def serialize_records(records: list[dict]) -> bytes: + return b"".join(orjson.dumps(record) + b"\n" for record in records) + + def write_local( + self, records: list[dict], inbox_dir: Path | None = None + ) -> Path | None: + if not records: + return None + inbox = inbox_dir or local_inbox_dir() + writer_dir = inbox / self.writer_id + fragment_path = writer_dir / self._next_fragment_name() + data = self.serialize_records(records) + for attempt in range(3): + writer_dir.mkdir(parents=True, exist_ok=True) + try: + with tempfile.NamedTemporaryFile( + mode="wb", dir=writer_dir, suffix=".tmp", delete=False + ) as tmp: + tmp.write(data) + tmp_path = Path(tmp.name) + tmp_path.replace(fragment_path) + return fragment_path + except FileNotFoundError: + if attempt == 2 or writer_dir.exists(): + raise + + def write_to_bucket(self, records: list[dict], bucket_id: str) -> str | None: + if not records: + return None + remote_path = ( + f"{BUCKET_INBOX_PREFIX}/{self.writer_id}/{self._next_fragment_name()}" + ) + huggingface_hub.batch_bucket_files( + bucket_id, + add=[(self.serialize_records(records), remote_path)], + token=huggingface_hub.utils.get_token(), + ) + return remote_path + + +def parse_fragment_bytes(data: bytes) -> list[dict]: + records = [] + for line in data.splitlines(): + line = line.strip() + if not line: + continue + try: + record = orjson.loads(line) + except orjson.JSONDecodeError: + continue + if isinstance(record, dict) and record.get("kind") in KINDS: + records.append(record) + return records + + +def _group_by_run(records: list[dict]) -> dict[tuple, list[dict]]: + grouped: dict[tuple, list[dict]] = {} + for record in records: + key = (record.get("project"), record.get("run"), record.get("run_id")) + grouped.setdefault(key, []).append(record) + return grouped + + +def import_records(records: list[dict]) -> int: + from trackio.sqlite_storage import SQLiteStorage # noqa: PLC0415 + + metric_records = [r for r in records if r.get("kind") == METRIC_KIND] + system_records = [r for r in records if r.get("kind") == SYSTEM_METRIC_KIND] + alert_records = [r for r in records if r.get("kind") == ALERT_KIND] + imported = 0 + + for (project, run, run_id), group in _group_by_run(metric_records).items(): + if not project or not run: + continue + config = next((r["config"] for r in reversed(group) if r.get("config")), None) + has_timestamps = all(r.get("timestamp") for r in group) + SQLiteStorage.bulk_log( + project=project, + run=run, + run_id=run_id, + metrics_list=[r.get("metrics") or {} for r in group], + steps=[r.get("step") for r in group], + timestamps=[r["timestamp"] for r in group] if has_timestamps else None, + config=config, + log_ids=[r.get("log_id") for r in group], + ) + imported += len(group) + + for (project, run, run_id), group in _group_by_run(system_records).items(): + if not project or not run: + continue + has_timestamps = all(r.get("timestamp") for r in group) + SQLiteStorage.bulk_log_system( + project=project, + run=run, + run_id=run_id, + metrics_list=[r.get("metrics") or {} for r in group], + timestamps=[r["timestamp"] for r in group] if has_timestamps else None, + log_ids=[r.get("log_id") for r in group], + ) + imported += len(group) + + for (project, run, run_id), group in _group_by_run(alert_records).items(): + if not project or not run: + continue + has_timestamps = all(r.get("timestamp") for r in group) + SQLiteStorage.bulk_alert( + project=project, + run=run, + run_id=run_id, + titles=[r.get("title") or "" for r in group], + texts=[r.get("text") for r in group], + levels=[r.get("level") or "WARN" for r in group], + steps=[r.get("step") for r in group], + timestamps=[r["timestamp"] for r in group] if has_timestamps else None, + alert_ids=[r.get("alert_id") for r in group], + ) + imported += len(group) + + return imported + + +def import_inbox_dir(inbox_dir: Path | None = None, batch_size: int = 100) -> int: + inbox = inbox_dir or local_inbox_dir() + if not inbox.exists(): + return 0 + imported = 0 + batch_records: list[dict] = [] + batch_paths: list[Path] = [] + + def _flush_batch() -> int: + nonlocal batch_records, batch_paths + if not batch_paths: + return 0 + count = 0 + if batch_records: + count = import_records(batch_records) + for p in batch_paths: + try: + p.unlink() + except OSError: + pass + batch_records = [] + batch_paths = [] + return count + + for fragment_path in sorted(inbox.rglob("*.jsonl")): + try: + data = fragment_path.read_bytes() + except OSError: + continue + records = parse_fragment_bytes(data) + if records: + batch_records.extend(records) + batch_paths.append(fragment_path) + + if len(batch_paths) >= batch_size: + imported += _flush_batch() + + imported += _flush_batch() + for writer_dir in inbox.glob("*"): + if writer_dir.is_dir(): + try: + writer_dir.rmdir() + except OSError: + pass + return imported + + +def list_bucket_inbox_paths(bucket_id: str) -> list[str]: + try: + items = huggingface_hub.list_bucket_tree( + bucket_id, + prefix=BUCKET_INBOX_PREFIX, + recursive=True, + token=huggingface_hub.utils.get_token(), + ) + except Exception: + return [] + return sorted( + item.path + for item in items + if getattr(item, "type", None) == "file" + and getattr(item, "path", "").endswith(".jsonl") + ) + + +def import_inbox_from_bucket(bucket_id: str) -> int: + paths = list_bucket_inbox_paths(bucket_id) + if not paths: + return 0 + imported = 0 + consumed: list[str] = [] + with tempfile.TemporaryDirectory() as tmp_dir: + for i, remote_path in enumerate(paths): + local_path = Path(tmp_dir) / f"{i}.jsonl" + try: + huggingface_hub.download_bucket_files( + bucket_id, + files=[(remote_path, str(local_path))], + token=huggingface_hub.utils.get_token(), + ) + records = parse_fragment_bytes(local_path.read_bytes()) + except Exception: + continue + if records: + imported += import_records(records) + consumed.append(remote_path) + if consumed: + try: + huggingface_hub.batch_bucket_files( + bucket_id, + delete=consumed, + token=huggingface_hub.utils.get_token(), + ) + except Exception: + pass + return imported + + +def _add_files_to_bucket(bucket_id: str, additions: list[tuple[str, str]]) -> None: + if additions: + huggingface_hub.batch_bucket_files( + bucket_id, + add=additions, + token=huggingface_hub.utils.get_token(), + ) + + +def _upload_files_to_bucket( + bucket_id: str, + uploads: list[dict[str, Any]], + remote_path: Callable[[dict[str, Any], Path], str], +) -> None: + _add_files_to_bucket( + bucket_id, + [ + (str(p), remote_path(upload, p)) + for upload in uploads + if (p := Path(upload["file_path"])).exists() + ], + ) + + +def upload_media_files_to_bucket(bucket_id: str, uploads: list[dict[str, Any]]) -> None: + _upload_files_to_bucket( + bucket_id, + uploads, + lambda upload, p: bucket_media_path( + project=upload["project"], + run=upload.get("run"), + step=upload.get("step"), + relative_path=upload.get("relative_path"), + filename=p.name, + ), + ) + + +def upload_artifact_blobs_to_bucket( + bucket_id: str, uploads: list[dict[str, Any]] +) -> None: + _upload_files_to_bucket( + bucket_id, + uploads, + lambda upload, p: f"trackio/{p.relative_to(utils.TRACKIO_DIR).as_posix()}", + ) diff --git a/trackio/frontend/dist/assets/EXT_lights_area-DpkQYXlQ.js b/trackio/frontend/dist/assets/EXT_lights_area-DpkQYXlQ.js new file mode 100644 index 0000000000000000000000000000000000000000..5091cf3553692b6844d6e6a59c3bbfe75f0a63b0 --- /dev/null +++ b/trackio/frontend/dist/assets/EXT_lights_area-DpkQYXlQ.js @@ -0,0 +1 @@ +import{V as a,Q as R}from"./math.vector-C1xyPDEl.js";import{Color3 as L}from"./math.color-CzLaKyb6.js";import{L as c}from"./light-DT53MGnW.js";import{L as D,_ as b,s as w}from"./bufferUtils-DTRdUD8L.js";import{N as S}from"./node-C-Iu0ALa.js";import{R as M}from"./math.scalar.functions-DkeaWAWz.js";import{R as y}from"./rawTexture-Cp8_XuDs.js";import{T as d}from"./texture-BEqhDl5Y.js";import{T as A}from"./tools-D01N7ptR.js";import{T as B}from"./mesh-Fj-9677A.js";import{A as E,G as C}from"./glTFLoader-BPeiodqB.js";import{u as U,r as z}from"./dynamic-BUKysMyT.js";import"./lightConstants-B1TSTh-k.js";import"./decorators.serialization-C7d7ykGG.js";import"./baseTexture-CgAggsey.js";import"./math.plane-Ce_75-g5.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./index-Bx1QoKVU.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./objectModelMapping-DWcnlpHl.js";async function F(){const i=new Uint16Array(16384),t=new Uint16Array(4096*4),r=await A.LoadFileAsync(A.GetAssetUrl("https://assets.babylonjs.com/core/areaLights/areaLightsLTC.bin")),e=new Uint16Array(r),n=e.length/8;for(let s=0;s{var n,s;i._ltcTextures&&(((n=i._ltcTextures)==null?void 0:n.LTC1).update(e[0]),((s=i._ltcTextures)==null?void 0:s.LTC2).update(e[1]),i.onDisposeObservable.addOnce(()=>{var o,l;(o=i._ltcTextures)==null||o.LTC1.dispose(),(l=i._ltcTextures)==null||l.LTC2.dispose()}))}).catch(e=>{D.Error(`Area Light fail to get LTC textures data. Error: ${e}`)})}class O extends c{constructor(t,r,e,n){super(t,e,n),this.position=r,this._scene._ltcTextures||P(this._scene)}transferTexturesToEffect(t,r){return this._scene._ltcTextures&&(t.setTexture("areaLightsLTC1Sampler",this._scene._ltcTextures.LTC1),t.setTexture("areaLightsLTC2Sampler",this._scene._ltcTextures.LTC2)),this}prepareLightSpecificDefines(t,r){t["AREALIGHT"+r]=!0,t.AREALIGHTUSED=!0}_isReady(){return this._scene._ltcTextures?this._scene._ltcTextures.LTC1.isReady()&&this._scene._ltcTextures.LTC2.isReady():!1}}S.AddNodeConstructor("Light_Type_4",(i,t)=>()=>new u(i,a.Zero(),1,1,t));class u extends O{get emissionTexture(){return this._emissionTextureTexture}set emissionTexture(t){this._emissionTextureTexture!==t&&(this._emissionTextureTexture=t,this._emissionTextureTexture&&(this._emissionTextureTexture.wrapU=0,this._emissionTextureTexture.wrapV=0),this._emissionTextureTexture&&u._IsTexture(this._emissionTextureTexture)&&this._emissionTextureTexture.onLoadObservable.addOnce(()=>{this._markMeshesAsLightDirty()}))}get width(){return this._width.x}set width(t){this._width.x=t}get height(){return this._height.y}set height(t){this._height.y=t}constructor(t,r,e,n,s,f){super(t,r,s,f),this._emissionTextureTexture=null,this._width=new a(e,0,0),this._height=new a(0,n,0),this._pointTransformedPosition=a.Zero(),this._pointTransformedWidth=a.Zero(),this._pointTransformedHeight=a.Zero()}getClassName(){return"RectAreaLight"}getTypeID(){return c.LIGHTTYPEID_RECT_AREALIGHT}_buildUniformLayout(){this._uniformBuffer.addUniform("vLightData",4),this._uniformBuffer.addUniform("vLightDiffuse",4),this._uniformBuffer.addUniform("vLightSpecular",4),this._uniformBuffer.addUniform("vLightWidth",4),this._uniformBuffer.addUniform("vLightHeight",4),this._uniformBuffer.addUniform("shadowsInfo",3),this._uniformBuffer.addUniform("depthValues",2),this._uniformBuffer.create()}_computeTransformedInformation(){return this.parent&&this.parent.getWorldMatrix?(a.TransformCoordinatesToRef(this.position,this.parent.getWorldMatrix(),this._pointTransformedPosition),a.TransformNormalToRef(this._width,this.parent.getWorldMatrix(),this._pointTransformedWidth),a.TransformNormalToRef(this._height,this.parent.getWorldMatrix(),this._pointTransformedHeight),!0):!1}static _IsTexture(t){return t.onLoadObservable!==void 0}transferToEffect(t,r){const e=this._scene.floatingOriginOffset;return this._computeTransformedInformation()?(this._uniformBuffer.updateFloat4("vLightData",this._pointTransformedPosition.x-e.x,this._pointTransformedPosition.y-e.y,this._pointTransformedPosition.z-e.z,0,r),this._uniformBuffer.updateFloat4("vLightWidth",this._pointTransformedWidth.x/2,this._pointTransformedWidth.y/2,this._pointTransformedWidth.z/2,0,r),this._uniformBuffer.updateFloat4("vLightHeight",this._pointTransformedHeight.x/2,this._pointTransformedHeight.y/2,this._pointTransformedHeight.z/2,0,r)):(this._uniformBuffer.updateFloat4("vLightData",this.position.x-e.x,this.position.y-e.y,this.position.z-e.z,0,r),this._uniformBuffer.updateFloat4("vLightWidth",this._width.x/2,this._width.y/2,this._width.z/2,0,r),this._uniformBuffer.updateFloat4("vLightHeight",this._height.x/2,this._height.y/2,this._height.z/2,0,r)),this}transferTexturesToEffect(t,r){return super.transferTexturesToEffect(t,r),this._emissionTextureTexture&&this._emissionTextureTexture.isReady()&&t.setTexture("rectAreaLightEmissionTexture"+r,this._emissionTextureTexture),this}transferToNodeMaterialEffect(t,r){const e=this._scene.floatingOriginOffset;return this._computeTransformedInformation()?t.setFloat3(r,this._pointTransformedPosition.x-e.x,this._pointTransformedPosition.y-e.y,this._pointTransformedPosition.z-e.z):t.setFloat3(r,this.position.x-e.x,this.position.y-e.y,this.position.z-e.z),this}prepareLightSpecificDefines(t,r){super.prepareLightSpecificDefines(t,r),t["RECTAREALIGHTEMISSIONTEXTURE"+r]=!!(this._emissionTextureTexture&&this._emissionTextureTexture.isReady())}}b([w()],u.prototype,"width",null);b([w()],u.prototype,"height",null);M("BABYLON.RectAreaLight",u);const m="EXT_lights_area";class v{constructor(t){this.name=m,this._loader=t,this.enabled=this._loader.isExtensionUsed(m)}dispose(){this._loader=null,delete this._lights}onLoading(){const t=this._loader.gltf.extensions;if(t&&t[this.name]){const r=t[this.name];this._lights=r.lights,E.Assign(this._lights)}}loadNodeAsync(t,r,e){return C.LoadExtensionAsync(t,r,this.name,async(n,s)=>(this._loader._allMaterialsDirtyRequired=!0,await this._loader.loadNodeAsync(t,r,f=>{var x;let h;const o=E.Get(n,this._lights,s.light),l=o.name||f.name;this._loader.babylonScene._blockEntityCollection=!!this._loader._assetContainer;const T=o.size!==void 0?o.size:1;switch(o.type){case"rect":{const _=((x=o.rect)==null?void 0:x.aspect)!==void 0?o.rect.aspect*T:T,g=T;h=new u(l,a.Zero(),_,g,this._loader.babylonScene);break}case"disk":{const _=Math.sqrt(T*T*.25*Math.PI);h=new u(l,a.Zero(),_,_,this._loader.babylonScene);break}default:throw this._loader.babylonScene._blockEntityCollection=!1,new Error(`${n}: Invalid area light type (${o.type})`)}h._parentContainer=this._loader._assetContainer,this._loader.babylonScene._blockEntityCollection=!1,o._babylonLight=h,h.falloffType=c.FALLOFF_GLTF,h.diffuse=o.color?L.FromArray(o.color):L.White(),h.intensity=o.intensity==null?1:o.intensity;const p=new B(`${l}_orientation`,this._loader.babylonScene);p.rotationQuaternion=R.RotationAxis(a.Up(),Math.PI),p.parent=f,h.parent=p,this._loader._babylonLights.push(h),C.AddPointerMetadata(h,n),e(f)})))}}U(m);z(m,!0,i=>new v(i));export{v as EXT_lights_area}; diff --git a/trackio/frontend/dist/assets/EXT_lights_ies-Qe4ehPsC.js b/trackio/frontend/dist/assets/EXT_lights_ies-Qe4ehPsC.js new file mode 100644 index 0000000000000000000000000000000000000000..6370b62d0971f1ecf863a952e7431baf0cc7d33e --- /dev/null +++ b/trackio/frontend/dist/assets/EXT_lights_ies-Qe4ehPsC.js @@ -0,0 +1 @@ +import{V as d}from"./math.vector-C1xyPDEl.js";import{Color3 as f}from"./math.color-CzLaKyb6.js";import{c as g}from"./objectModelMapping-DWcnlpHl.js";import{L as b}from"./light-DT53MGnW.js";import{A as l,G as h}from"./glTFLoader-BPeiodqB.js";import{u as y,r as L}from"./dynamic-BUKysMyT.js";import{T as A}from"./texture-BEqhDl5Y.js";import"./math.scalar.functions-DkeaWAWz.js";import"./bufferUtils-DTRdUD8L.js";import"./node-C-Iu0ALa.js";import"./decorators.serialization-C7d7ykGG.js";import"./math.axis-Ck7JfPPj.js";import"./lightConstants-B1TSTh-k.js";import"./tools-D01N7ptR.js";import"./index-Bx1QoKVU.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./material-CrEsrP1R.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./baseTexture-CgAggsey.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";const a="EXT_lights_ies";class w{constructor(t){this.name=a,this._loader=t,this.enabled=this._loader.isExtensionUsed(a)}dispose(){this._loader=null,delete this._lights}onLoading(){const t=this._loader.gltf.extensions;if(t&&t[this.name]){const o=t[this.name];this._lights=o.lights,l.Assign(this._lights)}}loadNodeAsync(t,o,_){return h.LoadExtensionAsync(t,o,this.name,async(p,s)=>{this._loader._allMaterialsDirtyRequired=!0;let i,e;const u=await this._loader.loadNodeAsync(t,o,r=>{e=l.Get(p,this._lights,s.light);const c=e.name||r.name;this._loader.babylonScene._blockEntityCollection=!!this._loader._assetContainer,i=new g(c,d.Zero(),d.Backward(),0,1,this._loader.babylonScene),i.angle=Math.PI/2,i.innerAngle=0,i._parentContainer=this._loader._assetContainer,this._loader.babylonScene._blockEntityCollection=!1,e._babylonLight=i,i.falloffType=b.FALLOFF_GLTF,i.diffuse=s.color?f.FromArray(s.color):f.White(),i.intensity=s.multiplier||1,i.range=Number.MAX_VALUE,i.parent=r,this._loader._babylonLights.push(i),h.AddPointerMetadata(i,p),_(r)});let n;if(e.uri)n=await this._loader.loadUriAsync(t,e,e.uri);else{const r=l.Get(`${t}/bufferView`,this._loader.gltf.bufferViews,e.bufferView);n=await this._loader.loadBufferViewAsync(`/bufferViews/${r.index}`,r)}return i.iesProfileTexture=new A(name+"_iesProfile",this._loader.babylonScene,!0,!1,void 0,null,null,n,!0,void 0,void 0,void 0,void 0,".ies"),u})}}y(a);L(a,!0,m=>new w(m));export{w as EXT_lights_ies}; diff --git a/trackio/frontend/dist/assets/EXT_lights_image_based-CEXtWEZ3.js b/trackio/frontend/dist/assets/EXT_lights_image_based-CEXtWEZ3.js new file mode 100644 index 0000000000000000000000000000000000000000..595634985064e99930c2f64b6e610013a6f46a9d --- /dev/null +++ b/trackio/frontend/dist/assets/EXT_lights_image_based-CEXtWEZ3.js @@ -0,0 +1 @@ +import{a as G,S as z}from"./engine-DaBDhQ20.js";import{M as c,O as D,V as S,T as M,Q as w}from"./math.vector-C1xyPDEl.js";import{S as A}from"./decorators.serialization-C7d7ykGG.js";import{_ as F}from"./environmentTextureTools-CDrTJ7w-.js";import{_ as h,s as _,a as E,v as P}from"./bufferUtils-DTRdUD8L.js";import{T as Y}from"./tools-D01N7ptR.js";import{B as $}from"./baseTexture-CgAggsey.js";import{T as B}from"./texture-BEqhDl5Y.js";import{G as V,R as H}from"./math.scalar.functions-DkeaWAWz.js";import"./abstractEngine.cubeTexture-C895Ky-E.js";import{G as j,A as C}from"./glTFLoader-BPeiodqB.js";import{u as q,r as O}from"./dynamic-BUKysMyT.js";import"./thinEngine-D5ynu8Dd.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./dataBuffer-CgoqiUzT.js";import"./index-Bx1QoKVU.js";import"./math.axis-Ck7JfPPj.js";import"./math.color-CzLaKyb6.js";import"./math.plane-Ce_75-g5.js";import"./math.path-DFucxaHn.js";import"./perfCounter-DusmGz8t.js";import"./scene-gn_hoGMy.js";import"./smartArray-CO6iJ0u5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneComponent-Dflhczui.js";import"./buffer-DqKlo1ts.js";import"./renderingManager-CiY2OUrf.js";import"./deviceInputEvents-BTza-zoR.js";import"./passPostProcess-kvX6RZRb.js";import"./effectRenderer-CSz5fi0n.js";import"./math.viewport-CgkTt1RS.js";import"./drawWrapper-CZVk0xN-.js";import"./renderTargetTexture-B0kOC1Jy.js";import"./dumpTools-phHCiahG.js";import"./baseTexture.polynomial-BRyqQovE.js";import"./cubemapToSphericalPolynomial-BhsWOHWR.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./subMesh-UWDo5wjI.js";import"./node-C-Iu0ALa.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./material-CrEsrP1R.js";import"./cameraInputsManager-WrTH9PVf.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const I=.8;class l extends ${set boundingBoxSize(e){if(this._boundingBoxSize&&this._boundingBoxSize.equals(e))return;this._boundingBoxSize=e;const t=this.getScene();t&&t.markAllMaterialsAsDirty(1)}get boundingBoxSize(){return this._boundingBoxSize}set rotationY(e){this._rotationY=e,this.setReflectionTextureMatrix(c.RotationY(this._rotationY))}get rotationY(){return this._rotationY}get noMipmap(){return this._noMipmap}get forcedExtension(){return this._forcedExtension}static CreateFromImages(e,t,i){let r="";for(const s of e)r+=s;return new l(r,t,null,i,e)}static CreateFromPrefilteredData(e,t,i=null,r=!0){const s=t.useDelayedTextureLoading;t.useDelayedTextureLoading=!1;const o=new l(e,t,null,!1,null,null,null,void 0,!0,i,r);return t.useDelayedTextureLoading=s,o}constructor(e,t,i=null,r=!1,s=null,o=null,n=null,a=5,u=!1,d=null,f=!1,x=I,p=0,y,m){var v;super(t),this.onLoadObservable=new D,this.boundingBoxPosition=S.Zero(),this._rotationY=0,this._files=null,this._forcedExtension=null,this._extensions=null,this._textureMatrixRefraction=new c,this._buffer=null,this.name=e,this.url=e,this._noMipmap=r,this.hasAlpha=!1,this.isCube=!0,this._textureMatrix=c.Identity(),this.coordinatesMode=B.CUBIC_MODE;let b,T=null;i!==null&&!Array.isArray(i)?(b=i.extensions??null,this._noMipmap=i.noMipmap??!1,s=i.files??null,T=i.buffer??null,this._format=i.format??5,u=i.prefiltered??!1,d=i.forcedExtension??null,this._createPolynomials=i.createPolynomials??!1,this._lodScale=i.lodScale??I,this._lodOffset=i.lodOffset??0,this._loaderOptions=i.loaderOptions,this._useSRGBBuffer=i.useSRGBBuffer,o=i.onLoad??null,n=i.onError??null):(this._noMipmap=r,this._format=a,this._createPolynomials=f,b=i,this._loaderOptions=y,this._useSRGBBuffer=m,this._lodScale=x,this._lodOffset=p),!(!e&&!s)&&this.updateURL(e,d,o,u,n,b,(v=this.getScene())==null?void 0:v.useDelayedTextureLoading,s,T)}getClassName(){return"CubeTexture"}updateURL(e,t=null,i=null,r=!1,s=null,o=null,n=!1,a=null,u=null){(!this.name||this.name.startsWith("data:"))&&(this.name=e),this.url=e,t&&(this._forcedExtension=t);const d=e.lastIndexOf("."),f=t||(d>-1?e.substring(d).toLowerCase():""),x=f.indexOf(".dds")===0,p=f.indexOf(".env")===0,y=f.indexOf(".basis")===0;if(p?(this.gammaSpace=!1,this._prefiltered=!1,this.anisotropicFilteringLevel=1):(this._prefiltered=r,r&&(this.gammaSpace=!1,this.anisotropicFilteringLevel=1)),a)this._files=a;else if(!y&&!p&&!x&&!o&&(o=["_px.jpg","_py.jpg","_pz.jpg","_nx.jpg","_ny.jpg","_nz.jpg"]),this._files=this._files||[],this._files.length=0,o){for(let m=0;mn.getActiveTextures().indexOf(this)!==-1)),this._textureMatrix=e,!((o=this.getScene())!=null&&o.useRightHandedSystem)))return;const t=M.Vector3[0],i=M.Quaternion[0],r=M.Vector3[1];this._textureMatrix.decompose(t,i,r),i.z*=-1,i.w*=-1,c.ComposeToRef(t,i,r,this._textureMatrixRefraction)}getRefractionTextureMatrix(){var e;return(e=this.getScene())!=null&&e.useRightHandedSystem?this._textureMatrixRefraction:this._textureMatrix}_loadTexture(e=null,t=null){var n;const i=this.getScene(),r=this._texture;this._texture=this._getFromCache(this.url,this._noMipmap,void 0,void 0,this._useSRGBBuffer,this.isCube);const s=()=>{var a;this.onLoadObservable.notifyObservers(this),r&&(r.dispose(),(a=this.getScene())==null||a.markAllMaterialsAsDirty(1)),e&&e()},o=(a,u)=>{this._loadingError=!0,this._errorObject={message:a,exception:u},t&&t(a,u),B.OnTextureLoadErrorObservable.notifyObservers(this)};this._texture?this._texture.isReady?Y.SetImmediate(()=>s()):this._texture.onLoadedObservable.add(()=>s()):(this._prefiltered?this._texture=this._getEngine().createPrefilteredCubeTexture(this.url,i,this._lodScale,this._lodOffset,e,o,this._format,this._forcedExtension,this._createPolynomials):this._texture=this._getEngine().createCubeTexture(this.url,i,this._files,this._noMipmap,e,o,this._format,this._forcedExtension,!1,this._lodScale,this._lodOffset,null,this._loaderOptions,!!this._useSRGBBuffer,this._buffer),(n=this._texture)==null||n.onLoadedObservable.add(()=>this.onLoadObservable.notifyObservers(this)))}static Parse(e,t,i){const r=A.Parse(()=>{let s=!1;return e.prefiltered&&(s=e.prefiltered),new l(i+(e.url??e.name),t,e.extensions,!1,e.files||null,null,null,void 0,s,e.forcedExtension)},e,t);if(e.boundingBoxPosition&&(r.boundingBoxPosition=S.FromArray(e.boundingBoxPosition)),e.boundingBoxSize&&(r.boundingBoxSize=S.FromArray(e.boundingBoxSize)),e.animations)for(let s=0;s{const i=new l(this.url,this.getScene()||this._getEngine(),this._extensions,this._noMipmap,this._files);return e=i.uniqueId,i},this);return t.uniqueId=e,t}}h([_()],l.prototype,"url",void 0);h([E()],l.prototype,"boundingBoxPosition",void 0);h([E()],l.prototype,"boundingBoxSize",null);h([_("rotationY")],l.prototype,"rotationY",null);h([_("files")],l.prototype,"_files",void 0);h([_("forcedExtension")],l.prototype,"_forcedExtension",void 0);h([_("extensions")],l.prototype,"_extensions",void 0);h([P("textureMatrix")],l.prototype,"_textureMatrix",void 0);h([P("textureMatrixRefraction")],l.prototype,"_textureMatrixRefraction",void 0);B._CubeTextureParser=l.Parse;H("BABYLON.CubeTexture",l);class R extends l{constructor(e,t,i,r=5,s=0,o=!1,n=!1,a=3,u=null){super("",e),this._texture=e.getEngine().createRawCubeTexture(t,i,r,s,o,n,a,u)}update(e,t,i,r,s=null){this._texture.getEngine().updateRawCubeTexture(this._texture,e,t,i,r,s)}updateRGBDAsync(e,t=null,i=.8,r=0){return F(this._texture,e,t,i,r).then(()=>{})}clone(){return A.Clone(()=>{const e=this.getScene(),t=this._texture,i=new R(e,t._bufferViewArray,t.width,t.format,t.type,t.generateMipMaps,t.invertY,t.samplingMode,t._compression);return t.source===13&&i.updateRGBDAsync(t._bufferViewArrayArray,t._sphericalPolynomial,t._lodGenerationScale,t._lodGenerationOffset),i},this)}}const g="EXT_lights_image_based";class N{constructor(e){this.name=g,this._loader=e,this.enabled=this._loader.isExtensionUsed(g)}dispose(){this._loader=null,delete this._lights}onLoading(){const e=this._loader.gltf.extensions;if(e&&e[this.name]){const t=e[this.name];this._lights=t.lights}}loadSceneAsync(e,t){return j.LoadExtensionAsync(e,t,this.name,async(i,r)=>{this._loader._allMaterialsDirtyRequired=!0;const s=new Array;s.push(this._loader.loadSceneAsync(e,t)),this._loader.logOpen(`${i}`);const o=C.Get(`${i}/light`,this._lights,r.light);return s.push(this._loadLightAsync(`/extensions/${this.name}/lights/${r.light}`,o).then(n=>{this._loader.babylonScene.environmentTexture=n})),this._loader.logClose(),await Promise.all(s).then(()=>{})})}_loadLightAsync(e,t){if(!t._loaded){const i=new Array;this._loader.logOpen(`${e}`);const r=new Array(t.specularImages.length);for(let s=0;s{r[s][n]=f})),this._loader.logClose()}}this._loader.logClose(),t._loaded=Promise.all(i).then(async()=>{const s=new R(this._loader.babylonScene,null,t.specularImageSize);if(s.name=t.name||"environment",t._babylonTexture=s,t.intensity!=null&&(s.level=t.intensity),t.rotation){let u=w.FromArray(t.rotation);this._loader.babylonScene.useRightHandedSystem||(u=w.Inverse(u)),c.FromQuaternionToRef(u,s.getReflectionTextureMatrix())}if(!t.irradianceCoefficients)throw new Error(`${e}: Irradiance coefficients are missing`);const o=G.FromArray(t.irradianceCoefficients);o.scaleInPlace(t.intensity),o.convertIrradianceToLambertianRadiance();const n=z.FromHarmonics(o),a=(r.length-1)/Math.log2(t.specularImageSize);return await s.updateRGBDAsync(r,n,a)})}return t._loaded.then(()=>t._babylonTexture)}}q(g);O(g,!0,L=>new N(L));export{N as EXT_lights_image_based}; diff --git a/trackio/frontend/dist/assets/EXT_mesh_gpu_instancing-B3VxVLJg.js b/trackio/frontend/dist/assets/EXT_mesh_gpu_instancing-B3VxVLJg.js new file mode 100644 index 0000000000000000000000000000000000000000..0b5fd51def346e3b03b202d80eb8a6244f8a9a2b --- /dev/null +++ b/trackio/frontend/dist/assets/EXT_mesh_gpu_instancing-B3VxVLJg.js @@ -0,0 +1 @@ +import{T as t,V as A,Q as F,M as b}from"./math.vector-C1xyPDEl.js";import{L as g}from"./bufferUtils-DTRdUD8L.js";import{G as L,A as M}from"./glTFLoader-BPeiodqB.js";import{u as V,r as w}from"./dynamic-BUKysMyT.js";import"./thinInstanceMesh-CYnaUZKe.js";import"./math.scalar.functions-DkeaWAWz.js";import"./index-Bx1QoKVU.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const m="EXT_mesh_gpu_instancing";class B{constructor(p){this.name=m,this._loader=p,this.enabled=this._loader.isExtensionUsed(m)}dispose(){this._loader=null}loadNodeAsync(p,i,T){return L.LoadExtensionAsync(p,i,this.name,async(f,u)=>{this._loader._disableInstancedMesh++;const h=this._loader.loadNodeAsync(`/nodes/${i.index}`,i,T);if(this._loader._disableInstancedMesh--,!i._primitiveBabylonMeshes)return await h;const c=new Array;let o=0;const n=s=>{if(u.attributes[s]==null){c.push(Promise.resolve(null));return}const e=M.Get(`${f}/attributes/${s}`,this._loader.gltf.accessors,u.attributes[s]);if(c.push(this._loader._loadFloatAccessorAsync(`/accessors/${e.bufferView}`,e)),o===0)o=e.count;else if(o!==e.count)throw new Error(`${f}/attributes: Instance buffer accessors do not have the same count.`)};return n("TRANSLATION"),n("ROTATION"),n("SCALE"),n("_COLOR_0"),await h.then(async s=>{const[e,d,_,a]=await Promise.all(c),y=new Float32Array(o*16);t.Vector3[0].copyFromFloats(0,0,0),t.Quaternion[0].copyFromFloats(0,0,0,1),t.Vector3[1].copyFromFloats(1,1,1);for(let r=0;rnew B(l));export{B as EXT_mesh_gpu_instancing}; diff --git a/trackio/frontend/dist/assets/EXT_meshopt_compression-Pa499AYZ.js b/trackio/frontend/dist/assets/EXT_meshopt_compression-Pa499AYZ.js new file mode 100644 index 0000000000000000000000000000000000000000..e03a8203a7c79f9bd4e22a0be821dd09940dfcff --- /dev/null +++ b/trackio/frontend/dist/assets/EXT_meshopt_compression-Pa499AYZ.js @@ -0,0 +1 @@ +import{G as n,A as c}from"./glTFLoader-BPeiodqB.js";import{T as f}from"./tools-D01N7ptR.js";import{u as h,r as y}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./mesh-Fj-9677A.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";let p=0,m=null;class e{static get Default(){return e._Default||(e._Default=new e),e._Default}constructor(){const t=e.Configuration.decoder;this._decoderModulePromise=f.LoadBabylonScriptAsync(t.url).then(()=>MeshoptDecoder.ready)}dispose(){delete this._decoderModulePromise}async decodeGltfBufferAsync(t,i,d,r,o){await this._decoderModulePromise,p===0&&(MeshoptDecoder.useWorkers(1),p=1);const s=await MeshoptDecoder.decodeGltfBufferAsync(i,d,t,r,o);return m!==null&&clearTimeout(m),m=setTimeout(()=>{MeshoptDecoder.useWorkers(0),p=0,m=null},1e3),s}}e.Configuration={decoder:{url:`${f._DefaultCdnUrl}/meshopt_decoder.js`}};e._Default=null;const a="EXT_meshopt_compression";class _{constructor(t){this.name=a,this.enabled=t.isExtensionUsed(a),this._loader=t}dispose(){this._loader=null}loadBufferViewAsync(t,i){return n.LoadExtensionAsync(t,i,this.name,async(d,r)=>{const o=i;if(o._meshOptData)return await o._meshOptData;const s=c.Get(`${t}/buffer`,this._loader.gltf.buffers,r.buffer);return o._meshOptData=this._loader.loadBufferAsync(`/buffers/${s.index}`,s,r.byteOffset||0,r.byteLength).then(async l=>await e.Default.decodeGltfBufferAsync(l,r.count,r.byteStride,r.mode,r.filter)),await o._meshOptData})}}h(a);y(a,!0,u=>new _(u));export{_ as EXT_meshopt_compression}; diff --git a/trackio/frontend/dist/assets/EXT_texture_avif-Nv7VBjPQ.js b/trackio/frontend/dist/assets/EXT_texture_avif-Nv7VBjPQ.js new file mode 100644 index 0000000000000000000000000000000000000000..d01d17f27feca46825eeae121f82967ea99a1d74 --- /dev/null +++ b/trackio/frontend/dist/assets/EXT_texture_avif-Nv7VBjPQ.js @@ -0,0 +1 @@ +import{G as m,A as p}from"./glTFLoader-BPeiodqB.js";import{u,r as _}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const o="EXT_texture_avif";class c{constructor(r){this.name=o,this._loader=r,this.enabled=r.isExtensionUsed(o)}dispose(){this._loader=null}_loadTextureAsync(r,t,e){return m.LoadExtensionAsync(r,t,this.name,async(s,a)=>{const n=t.sampler==null?m.DefaultSampler:p.Get(`${r}/sampler`,this._loader.gltf.samplers,t.sampler),l=p.Get(`${s}/source`,this._loader.gltf.images,a.source);return await this._loader._createTextureAsync(r,n,l,d=>{e(d)},void 0,!t._textureInfo.nonColorData)})}}u(o);_(o,!0,i=>new c(i));export{c as EXT_texture_avif}; diff --git a/trackio/frontend/dist/assets/EXT_texture_webp-BDYrjNRo.js b/trackio/frontend/dist/assets/EXT_texture_webp-BDYrjNRo.js new file mode 100644 index 0000000000000000000000000000000000000000..1103f895e14c57f5f404f945439485f0fca499a3 --- /dev/null +++ b/trackio/frontend/dist/assets/EXT_texture_webp-BDYrjNRo.js @@ -0,0 +1 @@ +import{G as m,A as p}from"./glTFLoader-BPeiodqB.js";import{u,r as _}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const o="EXT_texture_webp";class c{constructor(r){this.name=o,this._loader=r,this.enabled=r.isExtensionUsed(o)}dispose(){this._loader=null}_loadTextureAsync(r,t,e){return m.LoadExtensionAsync(r,t,this.name,async(s,a)=>{const n=t.sampler==null?m.DefaultSampler:p.Get(`${r}/sampler`,this._loader.gltf.samplers,t.sampler),l=p.Get(`${s}/source`,this._loader.gltf.images,a.source);return await this._loader._createTextureAsync(r,n,l,d=>{e(d)},void 0,!t._textureInfo.nonColorData)})}}u(o);_(o,!0,i=>new c(i));export{c as EXT_texture_webp}; diff --git a/trackio/frontend/dist/assets/ExtrasAsMetadata-Cp9P1Lmx.js b/trackio/frontend/dist/assets/ExtrasAsMetadata-Cp9P1Lmx.js new file mode 100644 index 0000000000000000000000000000000000000000..86ec3539e2f8f88e0dcfffa9c612a856bca45cd1 --- /dev/null +++ b/trackio/frontend/dist/assets/ExtrasAsMetadata-Cp9P1Lmx.js @@ -0,0 +1 @@ +import{u as o,r as n}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./sceneLoader-CutT-qCn.js";import"./tools-D01N7ptR.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./math.color-CzLaKyb6.js";import"./scene-gn_hoGMy.js";import"./smartArray-CO6iJ0u5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./renderingManager-CiY2OUrf.js";import"./deviceInputEvents-BTza-zoR.js";import"./perfCounter-DusmGz8t.js";import"./sceneLoaderFlags-wywyb9ml.js";const i="ExtrasAsMetadata";class m{_assignExtras(r,t){if(t.extras&&Object.keys(t.extras).length>0){const s=r.metadata=r.metadata||{},a=s.gltf=s.gltf||{};a.extras=t.extras}}constructor(r){this.name=i,this.enabled=!0,this._loader=r}dispose(){this._loader=null}loadNodeAsync(r,t,s){return this._loader.loadNodeAsync(r,t,a=>{this._assignExtras(a,t),s(a)})}loadCameraAsync(r,t,s){return this._loader.loadCameraAsync(r,t,a=>{this._assignExtras(a,t),s(a)})}createMaterial(r,t,s){const a=this._loader.createMaterial(r,t,s);return this._assignExtras(a,t),a}loadAnimationAsync(r,t){return this._loader.loadAnimationAsync(r,t).then(s=>(this._assignExtras(s,t),s))}}o(i);n(i,!1,e=>new m(e));export{m as ExtrasAsMetadata}; diff --git a/trackio/frontend/dist/assets/KHR_animation_pointer-DbEjDTwi.js b/trackio/frontend/dist/assets/KHR_animation_pointer-DbEjDTwi.js new file mode 100644 index 0000000000000000000000000000000000000000..091bcb0ad22761621cf7d35161fcc513fa004eae --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_animation_pointer-DbEjDTwi.js @@ -0,0 +1 @@ +import{L as K}from"./bufferUtils-DTRdUD8L.js";import{u as B,r as Z}from"./dynamic-BUKysMyT.js";import{S as e,a as G}from"./objectModelMapping-DWcnlpHl.js";import{A as t}from"./animation-IftAdLIx.js";import{AnimationPropertyInfo as p}from"./glTFLoaderAnimation-QPpM8WOy.js";import{Color3 as V}from"./math.color-CzLaKyb6.js";import"./index-Bx1QoKVU.js";import"./sceneLoader-CutT-qCn.js";import"./tools-D01N7ptR.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./decorators.serialization-C7d7ykGG.js";import"./scene-gn_hoGMy.js";import"./smartArray-CO6iJ0u5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./renderingManager-CiY2OUrf.js";import"./deviceInputEvents-BTza-zoR.js";import"./perfCounter-DusmGz8t.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./node-C-Iu0ALa.js";import"./light-DT53MGnW.js";import"./math.axis-Ck7JfPPj.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";function u(l,r,a,i){return V.FromArray(r,a).scale(i)}function W(l,r,a,i){return r[a+3]*i}function s(l,r,a,i){return r[a]*i}function O(l,r,a,i){return-r[a]*i}function A(l,r,a,i){return r[a+1]*i}function z(l,r,a,i){return r[a]*i*2}function o(l){return{scale:[new n(t.ANIMATIONTYPE_FLOAT,`${l}.uScale`,s,()=>2),new n(t.ANIMATIONTYPE_FLOAT,`${l}.vScale`,A,()=>2)],offset:[new n(t.ANIMATIONTYPE_FLOAT,`${l}.uOffset`,s,()=>2),new n(t.ANIMATIONTYPE_FLOAT,`${l}.vOffset`,A,()=>2)],rotation:[new n(t.ANIMATIONTYPE_FLOAT,`${l}.wAng`,O,()=>1)]}}class c extends p{buildAnimations(r,a,i,m){return[{babylonAnimatable:r._babylonCamera,babylonAnimation:this._buildAnimation(a,i,m)}]}}class n extends p{buildAnimations(r,a,i,m){const x=[];for(const T in r._data)x.push({babylonAnimatable:r._data[T].babylonMaterial,babylonAnimation:this._buildAnimation(a,i,m)});return x}}class _ extends p{buildAnimations(r,a,i,m){return[{babylonAnimatable:r._babylonLight,babylonAnimation:this._buildAnimation(a,i,m)}]}}class D extends p{buildAnimations(r,a,i,m){return r._primitiveBabylonMeshes?r._primitiveBabylonMeshes.map(x=>({babylonAnimatable:x,babylonAnimation:this._buildAnimation(a,i,m)})):[]}}e("/cameras/{}/orthographic/xmag",[new c(t.ANIMATIONTYPE_FLOAT,"orthoLeft",O,()=>1),new c(t.ANIMATIONTYPE_FLOAT,"orthoRight",A,()=>1)]);e("/cameras/{}/orthographic/ymag",[new c(t.ANIMATIONTYPE_FLOAT,"orthoBottom",O,()=>1),new c(t.ANIMATIONTYPE_FLOAT,"orthoTop",A,()=>1)]);e("/cameras/{}/orthographic/zfar",[new c(t.ANIMATIONTYPE_FLOAT,"maxZ",s,()=>1)]);e("/cameras/{}/orthographic/znear",[new c(t.ANIMATIONTYPE_FLOAT,"minZ",s,()=>1)]);e("/cameras/{}/perspective/yfov",[new c(t.ANIMATIONTYPE_FLOAT,"fov",s,()=>1)]);e("/cameras/{}/perspective/zfar",[new c(t.ANIMATIONTYPE_FLOAT,"maxZ",s,()=>1)]);e("/cameras/{}/perspective/znear",[new c(t.ANIMATIONTYPE_FLOAT,"minZ",s,()=>1)]);e("/materials/{}/pbrMetallicRoughness/baseColorFactor",[new n(t.ANIMATIONTYPE_COLOR3,"albedoColor",u,()=>4),new n(t.ANIMATIONTYPE_FLOAT,"alpha",W,()=>4)]);e("/materials/{}/pbrMetallicRoughness/metallicFactor",[new n(t.ANIMATIONTYPE_FLOAT,"metallic",s,()=>1)]);e("/materials/{}/pbrMetallicRoughness/metallicFactor",[new n(t.ANIMATIONTYPE_FLOAT,"roughness",s,()=>1)]);const H=o("albedoTexture");e("/materials/{}/pbrMetallicRoughness/baseColorTexture/extensions/KHR_texture_transform/scale",H.scale);e("/materials/{}/pbrMetallicRoughness/baseColorTexture/extensions/KHR_texture_transform/offset",H.offset);e("/materials/{}/pbrMetallicRoughness/baseColorTexture/extensions/KHR_texture_transform/rotation",H.rotation);const N=o("metallicTexture");e("//materials/{}/pbrMetallicRoughness/metallicRoughnessTexture/scale",N.scale);e("//materials/{}/pbrMetallicRoughness/metallicRoughnessTexture/offset",N.offset);e("//materials/{}/pbrMetallicRoughness/metallicRoughnessTexture/rotation",N.rotation);e("/materials/{}/emissiveFactor",[new n(t.ANIMATIONTYPE_COLOR3,"emissiveColor",u,()=>3)]);const g=o("bumpTexture");e("/materials/{}/normalTexture/scale",[new n(t.ANIMATIONTYPE_FLOAT,"bumpTexture.level",s,()=>1)]);e("/materials/{}/normalTexture/extensions/KHR_texture_transform/scale",g.scale);e("/materials/{}/normalTexture/extensions/KHR_texture_transform/offset",g.offset);e("/materials/{}/normalTexture/extensions/KHR_texture_transform/rotation",g.rotation);e("/materials/{}/occlusionTexture/strength",[new n(t.ANIMATIONTYPE_FLOAT,"ambientTextureStrength",s,()=>1)]);const d=o("ambientTexture");e("/materials/{}/occlusionTexture/extensions/KHR_texture_transform/scale",d.scale);e("/materials/{}/occlusionTexture/extensions/KHR_texture_transform/offset",d.offset);e("/materials/{}/occlusionTexture/extensions/KHR_texture_transform/rotation",d.rotation);const b=o("emissiveTexture");e("/materials/{}/emissiveTexture/extensions/KHR_texture_transform/scale",b.scale);e("/materials/{}/emissiveTexture/extensions/KHR_texture_transform/offset",b.offset);e("/materials/{}/emissiveTexture/extensions/KHR_texture_transform/rotation",b.rotation);e("/materials/{}/extensions/KHR_materials_anisotropy/anisotropyStrength",[new n(t.ANIMATIONTYPE_FLOAT,"anisotropy.intensity",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_anisotropy/anisotropyRotation",[new n(t.ANIMATIONTYPE_FLOAT,"anisotropy.angle",s,()=>1)]);const M=o("anisotropy.texture");e("/materials/{}/extensions/KHR_materials_anisotropy/anisotropyTexture/extensions/KHR_texture_transform/scale",M.scale);e("/materials/{}/extensions/KHR_materials_anisotropy/anisotropyTexture/extensions/KHR_texture_transform/offset",M.offset);e("/materials/{}/extensions/KHR_materials_anisotropy/anisotropyTexture/extensions/KHR_texture_transform/rotation",M.rotation);e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatFactor",[new n(t.ANIMATIONTYPE_FLOAT,"clearCoat.intensity",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatRoughnessFactor",[new n(t.ANIMATIONTYPE_FLOAT,"clearCoat.roughness",s,()=>1)]);const F=o("clearCoat.texture");e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatTexture/extensions/KHR_texture_transform/scale",F.scale);e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatTexture/extensions/KHR_texture_transform/offset",F.offset);e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatTexture/extensions/KHR_texture_transform/rotation",F.rotation);const E=o("clearCoat.bumpTexture");e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatNormalTexture/scale",[new n(t.ANIMATIONTYPE_FLOAT,"clearCoat.bumpTexture.level",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatNormalTexture/extensions/KHR_texture_transform/scale",E.scale);e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatNormalTexture/extensions/KHR_texture_transform/offset",E.offset);e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatNormalTexture/extensions/KHR_texture_transform/rotation",E.rotation);const w=o("clearCoat.textureRoughness");e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatRoughnessTexture/extensions/KHR_texture_transform/scale",w.scale);e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatRoughnessTexture/extensions/KHR_texture_transform/offset",w.offset);e("/materials/{}/extensions/KHR_materials_clearcoat/clearcoatRoughnessTexture/extensions/KHR_texture_transform/rotation",w.rotation);e("/materials/{}/extensions/KHR_materials_dispersion/dispersionFactor",[new n(t.ANIMATIONTYPE_FLOAT,"subSurface.dispersion",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_emissive_strength/emissiveStrength",[new n(t.ANIMATIONTYPE_FLOAT,"emissiveIntensity",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_ior/ior",[new n(t.ANIMATIONTYPE_FLOAT,"indexOfRefraction",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceFactor",[new n(t.ANIMATIONTYPE_FLOAT,"iridescence.intensity",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceIor",[new n(t.ANIMATIONTYPE_FLOAT,"iridescence.indexOfRefraction",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceThicknessMinimum",[new n(t.ANIMATIONTYPE_FLOAT,"iridescence.minimumThickness",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceThicknessMaximum",[new n(t.ANIMATIONTYPE_FLOAT,"iridescence.maximumThickness",s,()=>1)]);const L=o("iridescence.texture");e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceTexture/extensions/KHR_texture_transform/scale",L.scale);e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceTexture/extensions/KHR_texture_transform/offset",L.offset);e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceTexture/extensions/KHR_texture_transform/rotation",L.rotation);const P=o("iridescence.thicknessTexture");e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceThicknessTexture/extensions/KHR_texture_transform/scale",P.scale);e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceThicknessTexture/extensions/KHR_texture_transform/offset",P.offset);e("/materials/{}/extensions/KHR_materials_iridescence/iridescenceThicknessTexture/extensions/KHR_texture_transform/rotation",P.rotation);e("/materials/{}/extensions/KHR_materials_sheen/sheenColorFactor",[new n(t.ANIMATIONTYPE_COLOR3,"sheen.color",u,()=>3)]);e("/materials/{}/extensions/KHR_materials_sheen/sheenRoughnessFactor",[new n(t.ANIMATIONTYPE_FLOAT,"sheen.roughness",s,()=>1)]);const y=o("sheen.texture");e("/materials/{}/extensions/KHR_materials_sheen/sheenColorTexture/extensions/KHR_texture_transform/scale",y.scale);e("/materials/{}/extensions/KHR_materials_sheen/sheenColorTexture/extensions/KHR_texture_transform/offset",y.offset);e("/materials/{}/extensions/KHR_materials_sheen/sheenColorTexture/extensions/KHR_texture_transform/rotation",y.rotation);const C=o("sheen.textureRoughness");e("/materials/{}/extensions/KHR_materials_sheen/sheenRoughnessTexture/extensions/KHR_texture_transform/scale",C.scale);e("/materials/{}/extensions/KHR_materials_sheen/sheenRoughnessTexture/extensions/KHR_texture_transform/offset",C.offset);e("/materials/{}/extensions/KHR_materials_sheen/sheenRoughnessTexture/extensions/KHR_texture_transform/rotation",C.rotation);e("/materials/{}/extensions/KHR_materials_specular/specularFactor",[new n(t.ANIMATIONTYPE_FLOAT,"metallicF0Factor",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_specular/specularColorFactor",[new n(t.ANIMATIONTYPE_COLOR3,"metallicReflectanceColor",u,()=>3)]);const Y=o("metallicReflectanceTexture");e("/materials/{}/extensions/KHR_materials_specular/specularTexture/extensions/KHR_texture_transform/scale",Y.scale);e("/materials/{}/extensions/KHR_materials_specular/specularTexture/extensions/KHR_texture_transform/offset",Y.offset);e("/materials/{}/extensions/KHR_materials_specular/specularTexture/extensions/KHR_texture_transform/rotation",Y.rotation);const v=o("reflectanceTexture");e("/materials/{}/extensions/KHR_materials_specular/specularColorTexture/extensions/KHR_texture_transform/scale",v.scale);e("/materials/{}/extensions/KHR_materials_specular/specularColorTexture/extensions/KHR_texture_transform/offset",v.offset);e("/materials/{}/extensions/KHR_materials_specular/specularColorTexture/extensions/KHR_texture_transform/rotation",v.rotation);e("/materials/{}/extensions/KHR_materials_transmission/transmissionFactor",[new n(t.ANIMATIONTYPE_FLOAT,"subSurface.refractionIntensity",s,()=>1)]);const S=o("subSurface.refractionIntensityTexture");e("/materials/{}/extensions/KHR_materials_transmission/transmissionTexture/extensions/KHR_texture_transform/scale",S.scale);e("/materials/{}/extensions/KHR_materials_transmission/transmissionTexture/extensions/KHR_texture_transform/offset",S.offset);e("/materials/{}/extensions/KHR_materials_transmission/transmissionTexture/extensions/KHR_texture_transform/rotation",S.rotation);e("/materials/{}/extensions/KHR_materials_volume/attenuationColor",[new n(t.ANIMATIONTYPE_COLOR3,"subSurface.tintColor",u,()=>3)]);e("/materials/{}/extensions/KHR_materials_volume/attenuationDistance",[new n(t.ANIMATIONTYPE_FLOAT,"subSurface.tintColorAtDistance",s,()=>1)]);e("/materials/{}/extensions/KHR_materials_volume/thicknessFactor",[new n(t.ANIMATIONTYPE_FLOAT,"subSurface.maximumThickness",s,()=>1)]);const $=o("subSurface.thicknessTexture");e("/materials/{}/extensions/KHR_materials_volume/thicknessTexture/extensions/KHR_texture_transform/scale",$.scale);e("/materials/{}/extensions/KHR_materials_volume/thicknessTexture/extensions/KHR_texture_transform/offset",$.offset);e("/materials/{}/extensions/KHR_materials_volume/thicknessTexture/extensions/KHR_texture_transform/rotation",$.rotation);e("/materials/{}/extensions/KHR_materials_diffuse_transmission/diffuseTransmissionFactor",[new n(t.ANIMATIONTYPE_FLOAT,"subSurface.translucencyIntensity",s,()=>1)]);const k=o("subSurface.translucencyIntensityTexture");e("materials/{}/extensions/KHR_materials_diffuse_transmission/diffuseTransmissionTexture/extensions/KHR_texture_transform/scale",k.scale);e("materials/{}/extensions/KHR_materials_diffuse_transmission/diffuseTransmissionTexture/extensions/KHR_texture_transform/offset",k.offset);e("materials/{}/extensions/KHR_materials_diffuse_transmission/diffuseTransmissionTexture/extensions/KHR_texture_transform/rotation",k.rotation);e("/materials/{}/extensions/KHR_materials_diffuse_transmission/diffuseTransmissionColorFactor",[new n(t.ANIMATIONTYPE_COLOR3,"subSurface.translucencyColor",u,()=>3)]);const j=o("subSurface.translucencyColorTexture");e("materials/{}/extensions/KHR_materials_diffuse_transmission/diffuseTransmissionColorTexture/extensions/KHR_texture_transform/scale",j.scale);e("materials/{}/extensions/KHR_materials_diffuse_transmission/diffuseTransmissionColorTexture/extensions/KHR_texture_transform/offset",j.offset);e("materials/{}/extensions/KHR_materials_diffuse_transmission/diffuseTransmissionColorTexture/extensions/KHR_texture_transform/rotation",j.rotation);e("/extensions/KHR_lights_punctual/lights/{}/color",[new _(t.ANIMATIONTYPE_COLOR3,"diffuse",u,()=>3)]);e("/extensions/KHR_lights_punctual/lights/{}/intensity",[new _(t.ANIMATIONTYPE_FLOAT,"intensity",s,()=>1)]);e("/extensions/KHR_lights_punctual/lights/{}/range",[new _(t.ANIMATIONTYPE_FLOAT,"range",s,()=>1)]);e("/extensions/KHR_lights_punctual/lights/{}/spot/innerConeAngle",[new _(t.ANIMATIONTYPE_FLOAT,"innerAngle",z,()=>1)]);e("/extensions/KHR_lights_punctual/lights/{}/spot/outerConeAngle",[new _(t.ANIMATIONTYPE_FLOAT,"angle",z,()=>1)]);e("/extensions/EXT_lights_area/lights/{}/color",[new _(t.ANIMATIONTYPE_COLOR3,"diffuse",u,()=>3)]);e("/extensions/EXT_lights_area/lights/{}/intensity",[new _(t.ANIMATIONTYPE_FLOAT,"intensity",s,()=>1)]);e("/extensions/EXT_lights_area/lights/{}/size",[new _(t.ANIMATIONTYPE_FLOAT,"radius",s,()=>1)]);e("/extensions/EXT_lights_area/lights/{}/rect/aspect",[new _(t.ANIMATIONTYPE_FLOAT,"radius",s,()=>1)]);e("/nodes/{}/extensions/EXT_lights_ies/color",[new _(t.ANIMATIONTYPE_COLOR3,"diffuse",u,()=>3)]);e("/nodes/{}/extensions/EXT_lights_ies/multiplier",[new _(t.ANIMATIONTYPE_FLOAT,"intensity",s,()=>1)]);e("/nodes/{}/extensions/KHR_node_visibility/visible",[new D(t.ANIMATIONTYPE_FLOAT,"isVisible",s,()=>1)]);const R="KHR_animation_pointer";class U{constructor(r){this.name=R,this._loader=r,this._pathToObjectConverter=G(this._loader.gltf)}get enabled(){return this._loader.isExtensionUsed(R)}dispose(){this._loader=null,delete this._pathToObjectConverter}_loadAnimationChannelAsync(r,a,i,m,x){var X;const T=(X=m.target.extensions)==null?void 0:X.KHR_animation_pointer;if(!T||!this._pathToObjectConverter)return null;m.target.path!=="pointer"&&K.Warn(`${r}/target/path: Value (${m.target.path}) must be (pointer) when using the ${this.name} extension`),m.target.node!=null&&K.Warn(`${r}/target/node: Value (${m.target.node}) must not be present when using the ${this.name} extension`);const I=`${r}/extensions/${this.name}`,h=T.pointer;if(!h)throw new Error(`${I}: Pointer is missing`);try{const f=this._pathToObjectConverter.convert(h);if(!f.info.interpolation)throw new Error(`${I}/pointer: Interpolation is missing`);return this._loader._loadAnimationChannelFromTargetInfoAsync(r,a,i,m,{object:f.object,info:f.info.interpolation},x)}catch{return K.Warn(`${I}/pointer: Invalid pointer (${h}) skipped`),null}}}B(R);Z(R,!0,l=>new U(l));export{U as KHR_animation_pointer}; diff --git a/trackio/frontend/dist/assets/KHR_draco_mesh_compression-BEx33bus.js b/trackio/frontend/dist/assets/KHR_draco_mesh_compression-BEx33bus.js new file mode 100644 index 0000000000000000000000000000000000000000..629f21cd587a218277432be33ad66b50babb31df --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_draco_mesh_compression-BEx33bus.js @@ -0,0 +1 @@ +import{T as A}from"./tools-D01N7ptR.js";import{A as v}from"./workerPool-CfMXSLnf.js";import{G as C}from"./mesh-Fj-9677A.js";import{V as _}from"./buffer-DqKlo1ts.js";import{L as V}from"./bufferUtils-DTRdUD8L.js";import{G as k,A as g,L as R}from"./glTFLoader-BPeiodqB.js";import{u as F,r as N}from"./dynamic-BUKysMyT.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./decorators.serialization-C7d7ykGG.js";import"./math.color-CzLaKyb6.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./index-Bx1QoKVU.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./dataBuffer-CgoqiUzT.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";function U(c,t,n,a,m){const e=c;let r=null,o=null,s=null;try{r=new e.Decoder,o=new e.DecoderBuffer,o.Init(t,t.byteLength);let i;const l=r.GetEncodedGeometryType(o);switch(l){case e.TRIANGULAR_MESH:{const d=new e.Mesh;if(i=r.DecodeBufferToMesh(o,d),!i.ok()||d.ptr===0)throw new Error(i.error_msg());const y=d.num_faces()*3,p=y*4,u=e._malloc(p);try{r.GetTrianglesUInt32Array(d,p,u);const P=new Uint32Array(y);P.set(new Uint32Array(e.HEAPF32.buffer,u,y)),a(P)}finally{e._free(u)}s=d;break}case e.POINT_CLOUD:{const d=new e.PointCloud;if(i=r.DecodeBufferToPointCloud(o,d),!i.ok()||!d.ptr)throw new Error(i.error_msg());s=d;break}default:throw new Error(`Draco: Cannot decode invalid geometry type ${l}`)}const h=s.num_points(),b=(d,f,y,p)=>{const u=p.data_type(),P=p.num_components(),M=p.normalized(),B=p.byte_stride(),O=p.byte_offset(),T={[e.DT_FLOAT32]:{typedArrayConstructor:Float32Array,heap:e.HEAPF32},[e.DT_INT8]:{typedArrayConstructor:Int8Array,heap:e.HEAP8},[e.DT_INT16]:{typedArrayConstructor:Int16Array,heap:e.HEAP16},[e.DT_INT32]:{typedArrayConstructor:Int32Array,heap:e.HEAP32},[e.DT_UINT8]:{typedArrayConstructor:Uint8Array,heap:e.HEAPU8},[e.DT_UINT16]:{typedArrayConstructor:Uint16Array,heap:e.HEAPU16},[e.DT_UINT32]:{typedArrayConstructor:Uint32Array,heap:e.HEAPU32}}[u];if(!T)throw new Error(`Draco: Cannot decode invalid data type ${u}`);const I=h*P,L=I*T.typedArrayConstructor.BYTES_PER_ELEMENT,D=e._malloc(L);try{d.GetAttributeDataArrayForAllPoints(f,p,u,L,D);const G=new T.typedArrayConstructor(T.heap.buffer,D,I);m(y,G.slice(),P,O,B,M)}finally{e._free(D)}};if(n)for(const d in n){const f=n[d],y=r.GetAttributeByUniqueId(s,f);b(r,s,d,y)}else{const d={position:e.POSITION,normal:e.NORMAL,color:e.COLOR,uv:e.TEX_COORD};for(const f in d){const y=r.GetAttributeId(s,d[f]);if(y!==-1){const p=r.GetAttribute(s,y);b(r,s,f,p)}}}return h}finally{s&&e.destroy(s),o&&e.destroy(o),r&&e.destroy(r)}}function S(){let c;onmessage=t=>{const n=t.data;switch(n.id){case"init":{n.url&&importScripts(n.url);const a=n.wasmBinary?{wasmBinary:n.wasmBinary}:{};c=DracoDecoderModule(a),postMessage({id:"initDone"});break}case"decodeMesh":{if(!c)throw new Error("Draco: Decoder module is not available");c.then(a=>{const m=U(a,n.dataView,n.attributes,e=>{postMessage({id:"indices",data:e},[e.buffer])},(e,r,o,s,i,l)=>{postMessage({id:"attribute",kind:e,data:r,size:o,byteOffset:s,byteStride:i,normalized:l},[r.buffer])});postMessage({id:"decodeMeshDone",totalVertices:m})});break}}}}async function W(c,t,n){return await new Promise((a,m)=>{const e=o=>{c.removeEventListener("error",e),c.removeEventListener("message",r),m(o)},r=o=>{o.data.id==="initDone"&&(c.removeEventListener("error",e),c.removeEventListener("message",r),a(c))};if(c.addEventListener("error",e),c.addEventListener("message",r),!t)c.postMessage({id:"init",url:n});else{const o=t.slice(0);c.postMessage({id:"init",url:n,wasmBinary:o},[o])}})}function K(){return typeof navigator!="object"||!navigator.hardwareConcurrency?1:Math.min(Math.floor(navigator.hardwareConcurrency*.5),4)}function $(c){return!!(c.wasmUrl&&(c.wasmBinary||c.wasmBinaryUrl)&&typeof WebAssembly=="object"||c.fallbackUrl)}class z{constructor(t){if(t.workerPool){this._workerPoolPromise=Promise.resolve(t.workerPool);return}const n=t.wasmBinary,a=t.numWorkers??K(),m=a&&typeof Worker=="function"&&typeof URL=="function",e=m||!t.jsModule,r=t.wasmUrl&&t.wasmBinaryUrl&&typeof WebAssembly=="object"?{url:e?A.GetBabylonScriptURL(t.wasmUrl,!0):"",wasmBinaryPromise:n?Promise.resolve(n):A.LoadFileAsync(A.GetBabylonScriptURL(t.wasmBinaryUrl,!0))}:{url:e?A.GetBabylonScriptURL(t.fallbackUrl):"",wasmBinaryPromise:Promise.resolve(void 0)};m?this._workerPoolPromise=r.wasmBinaryPromise.then(o=>{const s=this._getWorkerContent(),i=URL.createObjectURL(new Blob([s],{type:"application/javascript"}));return new v(a,()=>{const l=new Worker(i);return W(l,o,r.url)})}):this._modulePromise=r.wasmBinaryPromise.then(async o=>{if(!this._isModuleAvailable()&&!t.jsModule){if(!r.url)throw new Error("Draco codec module is not available");await A.LoadBabylonScriptAsync(r.url)}return await this._createModuleAsync(o,t.jsModule)})}async whenReadyAsync(){if(this._workerPoolPromise){await this._workerPoolPromise;return}if(this._modulePromise){await this._modulePromise;return}}dispose(){this._workerPoolPromise&&this._workerPoolPromise.then(t=>{t.dispose()}),delete this._workerPoolPromise,delete this._modulePromise}}class w extends z{static get DefaultAvailable(){return $(w.DefaultConfiguration)}static get Default(){return w._Default??(w._Default=new w),w._Default}static ResetDefault(t){w._Default&&(t||w._Default.dispose(),w._Default=null)}_isModuleAvailable(){return typeof DracoDecoderModule<"u"}async _createModuleAsync(t,n){return{module:await(n||DracoDecoderModule)({wasmBinary:t})}}_getWorkerContent(){return`${U}(${S})()`}constructor(t=w.DefaultConfiguration){super(t)}decodeMeshToMeshDataAsync(t,n,a){const m=t instanceof ArrayBuffer?new Int8Array(t):new Int8Array(t.buffer,t.byteOffset,t.byteLength),e=(r,o)=>a&&a[r]!==void 0?(o!==a[r]&&V.Warn(`Normalized flag from Draco data (${o}) does not match normalized flag from glTF accessor (${a[r]}). Using flag from glTF accessor.`),a[r]):o;if(this._workerPoolPromise)return this._workerPoolPromise.then(async r=>await new Promise((o,s)=>{r.push((i,l)=>{let h=null;const b=[],d=p=>{i.removeEventListener("error",d),i.removeEventListener("message",f),s(p),l()},f=p=>{const u=p.data;switch(u.id){case"indices":{h=u.data;break}case"attribute":{b.push({kind:u.kind,data:u.data,size:u.size,byteOffset:u.byteOffset,byteStride:u.byteStride,normalized:e(u.kind,u.normalized)});break}case"decodeMeshDone":{i.removeEventListener("error",d),i.removeEventListener("message",f),o({indices:h,attributes:b,totalVertices:u.totalVertices}),l();break}}};i.addEventListener("error",d),i.addEventListener("message",f);const y=m.slice();i.postMessage({id:"decodeMesh",dataView:y,attributes:n},[y.buffer])})}));if(this._modulePromise)return this._modulePromise.then(r=>{let o=null;const s=[],i=U(r.module,m,n,l=>{o=l},(l,h,b,d,f,y)=>{s.push({kind:l,data:h,size:b,byteOffset:d,byteStride:f,normalized:y})});return{indices:o,attributes:s,totalVertices:i}});throw new Error("Draco: Decoder module is not available")}async decodeMeshToGeometryAsync(t,n,a,m){const e=await this.decodeMeshToMeshDataAsync(a,m),r=new C(t,n);e.indices&&r.setIndices(e.indices);for(const o of e.attributes)r.setVerticesBuffer(new _(n.getEngine(),o.data,o.kind,!1,void 0,o.byteStride,void 0,o.byteOffset,o.size,void 0,o.normalized,!0),e.totalVertices);return r}async _decodeMeshToGeometryForGltfAsync(t,n,a,m,e,r){const o=await this.decodeMeshToMeshDataAsync(a,m,e),s=new C(t,n);r&&(s._boundingInfo=r,s.useBoundingInfoFromGeometry=!0),o.indices&&s.setIndices(o.indices);for(const i of o.attributes)s.setVerticesBuffer(new _(n.getEngine(),i.data,i.kind,!1,void 0,i.byteStride,void 0,i.byteOffset,i.size,void 0,i.normalized,!0),o.totalVertices);return s}}w.DefaultConfiguration={wasmUrl:`${A._DefaultCdnUrl}/draco_wasm_wrapper_gltf.js`,wasmBinaryUrl:`${A._DefaultCdnUrl}/draco_decoder_gltf.wasm`,fallbackUrl:`${A._DefaultCdnUrl}/draco_decoder_gltf.js`};w._Default=null;const E="KHR_draco_mesh_compression";class H{constructor(t){this.name=E,this.useNormalizedFlagFromAccessor=!0,this._loader=t,this.enabled=w.DefaultAvailable&&this._loader.isExtensionUsed(E)}dispose(){delete this.dracoDecoder,this._loader=null}_loadVertexDataAsync(t,n,a){return k.LoadExtensionAsync(t,n,this.name,async(m,e)=>{if(n.mode!=null&&n.mode!==4&&n.mode!==5)throw new Error(`${t}: Unsupported mode ${n.mode}`);const r={},o={},s=(l,h)=>{const b=e.attributes[l];if(b!=null&&(a._delayInfo=a._delayInfo||[],a._delayInfo.indexOf(h)===-1&&a._delayInfo.push(h),r[h]=b,this.useNormalizedFlagFromAccessor)){const d=g.TryGet(this._loader.gltf.accessors,n.attributes[l]);d&&(o[h]=d.normalized||!1)}};s("POSITION",_.PositionKind),s("NORMAL",_.NormalKind),s("TANGENT",_.TangentKind),s("TEXCOORD_0",_.UVKind),s("TEXCOORD_1",_.UV2Kind),s("TEXCOORD_2",_.UV3Kind),s("TEXCOORD_3",_.UV4Kind),s("TEXCOORD_4",_.UV5Kind),s("TEXCOORD_5",_.UV6Kind),s("JOINTS_0",_.MatricesIndicesKind),s("WEIGHTS_0",_.MatricesWeightsKind),s("COLOR_0",_.ColorKind);const i=g.Get(m,this._loader.gltf.bufferViews,e.bufferView);return i._dracoBabylonGeometry||(i._dracoBabylonGeometry=this._loader.loadBufferViewAsync(`/bufferViews/${i.index}`,i).then(async l=>{const h=this.dracoDecoder||w.Default,b=g.TryGet(this._loader.gltf.accessors,n.attributes.POSITION),d=!this._loader.parent.alwaysComputeBoundingBox&&!a.skeleton&&b?R(b):null;return await h._decodeMeshToGeometryForGltfAsync(a.name,this._loader.babylonScene,l,r,o,d).catch(f=>{throw new Error(`${t}: ${f.message}`)})})),await i._dracoBabylonGeometry})}}F(E);N(E,!0,c=>new H(c));export{H as KHR_draco_mesh_compression}; diff --git a/trackio/frontend/dist/assets/KHR_interactivity-K8Kioa6L.js b/trackio/frontend/dist/assets/KHR_interactivity-K8Kioa6L.js new file mode 100644 index 0000000000000000000000000000000000000000..d4e9812be4170ad7cc015cf2822d8ff28626c3db --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_interactivity-K8Kioa6L.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/flowGraphPlayAnimationBlock-B9FP0RfS.js","assets/declarationMapper-oEUQrCmY.js","assets/bufferUtils-DTRdUD8L.js","assets/math.vector-C1xyPDEl.js","assets/math.scalar.functions-DkeaWAWz.js","assets/math.color-CzLaKyb6.js","assets/animationGroup-BP7VB8mY.js","assets/animation-IftAdLIx.js","assets/node-C-Iu0ALa.js","assets/decorators.serialization-C7d7ykGG.js","assets/baseTexture-CgAggsey.js","assets/bone-B35odufe.js","assets/scene-gn_hoGMy.js","assets/tools-D01N7ptR.js","assets/smartArray-CO6iJ0u5.js","assets/lightConstants-B1TSTh-k.js","assets/sceneComponent-Dflhczui.js","assets/math.plane-Ce_75-g5.js","assets/buffer-DqKlo1ts.js","assets/dataBuffer-CgoqiUzT.js","assets/renderingManager-CiY2OUrf.js","assets/deviceInputEvents-BTza-zoR.js","assets/perfCounter-DusmGz8t.js","assets/index-Bx1QoKVU.js","assets/index-BfQ3YfM4.css","assets/dynamic-BUKysMyT.js","assets/sceneLoader-CutT-qCn.js","assets/sceneLoaderFlags-wywyb9ml.js","assets/objectModelMapping-DWcnlpHl.js","assets/light-DT53MGnW.js","assets/math.axis-Ck7JfPPj.js","assets/texture-BEqhDl5Y.js","assets/flowGraphStopAnimationBlock-ChXcK4cb.js","assets/flowGraphPauseAnimationBlock-DAoY0eJc.js","assets/flowGraphInterpolationBlock-CYcMKCGO.js","assets/flowGraphSceneReadyEventBlock-Crn-rAIL.js","assets/flowGraphSceneTickEventBlock-gf3YDJRe.js","assets/flowGraphSendCustomEventBlock-Ba8XUM8G.js","assets/flowGraphReceiveCustomEventBlock-CId8KX4K.js","assets/flowGraphMeshPickEventBlock-WsLHiXtY.js","assets/flowGraphMathBlocks-HeZhspxf.js","assets/flowGraphBinaryOperationBlock-CNTF2Ejc.js","assets/flowGraphCachedOperationBlock-cxKp5Gdq.js","assets/flowGraphUnaryOperationBlock-CI7NJ04I.js","assets/flowGraphVectorMathBlocks-BGV5dKz9.js","assets/flowGraphMatrixMathBlocks-Dg7VLPFP.js","assets/flowGraphBranchBlock-CPQ1Lj8X.js","assets/flowGraphSetDelayBlock-BA13Fleb.js","assets/flowGraphCancelDelayBlock-CiPuCgGz.js","assets/flowGraphCounterBlock-OjaFZjW5.js","assets/flowGraphDebounceBlock-fxe8WoxT.js","assets/flowGraphThrottleBlock-B-yO7rgE.js","assets/flowGraphDoNBlock-C4vVyovK.js","assets/flowGraphFlipFlopBlock-BSjrwcqq.js","assets/flowGraphForLoopBlock-jI9z_K5X.js","assets/flowGraphMultiGateBlock-Cy-tdSZ3.js","assets/flowGraphSequenceBlock-Bcye5mDB.js","assets/flowGraphSwitchBlock-CfuObCnO.js","assets/flowGraphWaitAllBlock-VXxeVnO_.js","assets/flowGraphWhileLoopBlock-BYmEfSWz.js","assets/flowGraphConsoleLogBlock-Cz7IRQyV.js","assets/flowGraphConditionalDataBlock-Btzamqcx.js","assets/flowGraphConstantBlock-BV1ij11S.js","assets/flowGraphTransformCoordinatesSystemBlock-D3zEJ4A-.js","assets/flowGraphGetAssetBlock-BeMIiO3t.js","assets/flowGraphGetPropertyBlock-CAOmAIk8.js","assets/flowGraphSetPropertyBlock-DxJBj-a9.js","assets/flowGraphGetVariableBlock-BaRkHNJ_.js","assets/flowGraphSetVariableBlock-BG0Kfa6g.js","assets/flowGraphJsonPointerParserBlock-CZ93bSRG.js","assets/flowGraphMathCombineExtractBlocks-Dg02KNQM.js","assets/flowGraphTypeToTypeBlocks-DdLy9OXZ.js","assets/flowGraphEasingBlock-C_-3ZhLh.js","assets/easing-Wc0jPSIm.js","assets/math.path-DFucxaHn.js","assets/flowGraphBezierCurveEasingBlock--1XIKCm2.js","assets/flowGraphPointerOverEventBlock-wKWlgMPO.js","assets/flowGraphPointerOutEventBlock-BpW1yJKn.js","assets/flowGraphContextBlock-DeEwvFEY.js","assets/flowGraphArrayIndexBlock-3XRt4h8L.js","assets/flowGraphCodeExecutionBlock-BpjYYwgf.js","assets/flowGraphIndexOfBlock-D1XW4kc4.js","assets/flowGraphFunctionReferenceBlock-CLhl7P_Q.js","assets/flowGraphDataSwitchBlock-BUqG5JEg.js","assets/flowGraphGLTFDataProvider-By82y__G.js"])))=>i.map(i=>d[i]); +import{_ as r}from"./index-Bx1QoKVU.js";import{M as kt,a as Gt,V as ut,b as Bt,Q as X,O as U}from"./math.vector-C1xyPDEl.js";import{L as w,_ as Ft,s as Et}from"./bufferUtils-DTRdUD8L.js";import{R as D}from"./decorators.serialization-C7d7ykGG.js";import{Color3 as vt,Color4 as It}from"./math.color-CzLaKyb6.js";import{F as Tt,a as Vt,b as Ct,g as Pt,c as bt,d as at}from"./declarationMapper-oEUQrCmY.js";import{R as ht}from"./math.scalar.functions-DkeaWAWz.js";import{P as rt}from"./deviceInputEvents-BTza-zoR.js";import{u as Ot,r as xt}from"./dynamic-BUKysMyT.js";import{a as Dt,A as O}from"./objectModelMapping-DWcnlpHl.js";function At(a){return a==="Mesh"||a==="AbstractMesh"||a==="GroundMesh"||a==="InstanceMesh"||a==="LinesMesh"||a==="GoldbergMesh"||a==="GreasedLineMesh"||a==="TrailMesh"}function pt(a){return a==="Vector2"||a==="Vector3"||a==="Vector4"||a==="Quaternion"||a==="Color3"||a==="Color4"}function Rt(a){return a==="Matrix"||a==="Matrix2D"||a==="Matrix3D"}function Lt(a){return a==="AnimationGroup"}function St(a,t,e=!1){if(a==="Vector2")return Gt.FromArray(t);if(a==="Vector3")return e&&(t[2]*=-1),ut.FromArray(t);if(a==="Vector4")return Bt.FromArray(t);if(a==="Quaternion")return e&&(t[2]*=-1,t[3]*=-1),X.FromArray(t);if(a==="Color3")return new vt(t[0],t[1],t[2]);if(a==="Color4")return new It(t[0],t[1],t[2],t[3]);throw new Error(`Unknown vector class name ${a}`)}function tt(a,t,e){var n;const o=((n=t==null?void 0:t.getClassName)==null?void 0:n.call(t))??"";if(pt(o)||Rt(o))e[a]={value:t.asArray(),className:o};else if(o==="FlowGraphInteger")e[a]={value:t.value,className:o};else if(o&&(t.id||t.name))e[a]={id:t.id,name:t.name,className:o};else if(typeof t!="object")e[a]=t;else throw new Error(`Could not serialize value ${t}`)}function H(a,t,e,o){const n=t[a];let s;const i=(n==null?void 0:n.type)??(n==null?void 0:n.className);if(At(i)){let c=o.meshes.filter(l=>n.id?l.id===n.id:l.name===n.name);c.length===0&&(c=o.transformNodes.filter(l=>n.id?l.id===n.id:l.name===n.name)),s=n.uniqueId?c.find(l=>l.uniqueId===n.uniqueId):c[0]}else if(pt(i))s=St(i,n.value);else if(Lt(i)){const c=o.animationGroups.filter(l=>l.name===n.name);s=c.length===1?c[0]:c.find(l=>l.uniqueId===n.uniqueId)}else i==="Matrix"?s=kt.FromArray(n.value):i==="Matrix2D"?s=new Tt(n.value):i==="Matrix3D"?s=new Vt(n.value):i==="FlowGraphInteger"?s=Ct.FromValue(n.value):i==="number"||i==="string"||i==="boolean"?s=n.value[0]:n&&n.value!==void 0?s=n.value:Array.isArray(n)?s=n.reduce((c,l)=>(l.eventData&&(c[l.id]={type:Pt(l.type)},typeof l.value<"u"&&(c[l.id].value=H("value",l,e,o))),c),{}):s=n;return s}function Nt(a){return a==="FlowGraphJsonPointerParserBlock"}var st;(function(a){a.Animation="Animation",a.AnimationGroup="AnimationGroup",a.Mesh="Mesh",a.Material="Material",a.Camera="Camera",a.Light="Light"})(st||(st={}));function Mt(a,t,e,o){switch(t){case"Animation":return o?a.animations.find(n=>n.uniqueId===e)??null:a.animations[e]??null;case"AnimationGroup":return o?a.animationGroups.find(n=>n.uniqueId===e)??null:a.animationGroups[e]??null;case"Mesh":return o?a.meshes.find(n=>n.uniqueId===e)??null:a.meshes[e]??null;case"Material":return o?a.materials.find(n=>n.uniqueId===e)??null:a.materials[e]??null;case"Camera":return o?a.cameras.find(n=>n.uniqueId===e)??null:a.cameras[e]??null;case"Light":return o?a.lights.find(n=>n.uniqueId===e)??null:a.lights[e]??null;default:return null}}var it;(function(a){a.ExecuteBlock="ExecuteBlock",a.ExecuteEvent="ExecuteEvent",a.TriggerConnection="TriggerConnection",a.ContextVariableSet="ContextVariableSet",a.GlobalVariableSet="GlobalVariableSet",a.GlobalVariableDelete="GlobalVariableDelete",a.GlobalVariableGet="GlobalVariableGet",a.AddConnection="AddConnection",a.GetConnectionValue="GetConnectionValue",a.SetConnectionValue="SetConnectionValue",a.ActivateSignal="ActivateSignal",a.ContextVariableGet="ContextVariableGet"})(it||(it={}));class qt{constructor(){this.logToConsole=!1,this.log=[]}addLogItem(t){var e;if(t.time||(t.time=Date.now()),this.log.push(t),this.logToConsole){const o=(e=t.payload)==null?void 0:e.value;typeof o=="object"&&o.getClassName?w.Log(`[FGLog] ${t.className}:${t.uniqueId.split("-")[0]} ${t.action} - ${JSON.stringify(o.getClassName())}: ${o.toString()}`):w.Log(`[FGLog] ${t.className}:${t.uniqueId.split("-")[0]} ${t.action} - ${JSON.stringify(t.payload)}`)}}getItemsOfType(t){return this.log.filter(e=>e.action===t)}}class _t{get enableLogging(){return this._enableLogging}set enableLogging(t){this._enableLogging!==t&&(this._enableLogging=t,this._enableLogging?(this.logger=new qt,this.logger.logToConsole=!0):this.logger=null)}constructor(t){this.uniqueId=D(),this._userVariables={},this._executionVariables={},this._globalContextVariables={},this._connectionValues={},this._pendingBlocks=[],this._executionId=0,this.onNodeExecutedObservable=new U,this.treatDataAsRightHanded=!1,this._enableLogging=!1,this._configuration=t,this.assetsContext=t.assetsContext??t.scene}hasVariable(t){return t in this._userVariables}setVariable(t,e){var o;this._userVariables[t]=e,(o=this.logger)==null||o.addLogItem({time:Date.now(),className:this.getClassName(),uniqueId:this.uniqueId,action:"ContextVariableSet",payload:{name:t,value:e}})}getAsset(t,e){return Mt(this.assetsContext,t,e)}getVariable(t){var e;return(e=this.logger)==null||e.addLogItem({time:Date.now(),className:this.getClassName(),uniqueId:this.uniqueId,action:"ContextVariableGet",payload:{name:t,value:this._userVariables[t]}}),this._userVariables[t]}get userVariables(){return this._userVariables}getScene(){return this._configuration.scene}_getUniqueIdPrefixedName(t,e){return`${t.uniqueId}_${e}`}_getGlobalContextVariable(t,e){var o;return(o=this.logger)==null||o.addLogItem({time:Date.now(),className:this.getClassName(),uniqueId:this.uniqueId,action:"GlobalVariableGet",payload:{name:t,defaultValue:e,possibleValue:this._globalContextVariables[t]}}),this._hasGlobalContextVariable(t)?this._globalContextVariables[t]:e}_setGlobalContextVariable(t,e){var o;(o=this.logger)==null||o.addLogItem({time:Date.now(),className:this.getClassName(),uniqueId:this.uniqueId,action:"GlobalVariableSet",payload:{name:t,value:e}}),this._globalContextVariables[t]=e}_deleteGlobalContextVariable(t){var e;(e=this.logger)==null||e.addLogItem({time:Date.now(),className:this.getClassName(),uniqueId:this.uniqueId,action:"GlobalVariableDelete",payload:{name:t}}),delete this._globalContextVariables[t]}_hasGlobalContextVariable(t){return t in this._globalContextVariables}_setExecutionVariable(t,e,o){this._executionVariables[this._getUniqueIdPrefixedName(t,e)]=o}_getExecutionVariable(t,e,o){return this._hasExecutionVariable(t,e)?this._executionVariables[this._getUniqueIdPrefixedName(t,e)]:o}_deleteExecutionVariable(t,e){delete this._executionVariables[this._getUniqueIdPrefixedName(t,e)]}_hasExecutionVariable(t,e){return this._getUniqueIdPrefixedName(t,e)in this._executionVariables}_hasConnectionValue(t){return t.uniqueId in this._connectionValues}_setConnectionValue(t,e){var o;this._connectionValues[t.uniqueId]=e,(o=this.logger)==null||o.addLogItem({time:Date.now(),className:this.getClassName(),uniqueId:this.uniqueId,action:"SetConnectionValue",payload:{connectionPointId:t.uniqueId,value:e}})}_setConnectionValueByKey(t,e){this._connectionValues[t]=e}_getConnectionValue(t){var e;return(e=this.logger)==null||e.addLogItem({time:Date.now(),className:this.getClassName(),uniqueId:this.uniqueId,action:"GetConnectionValue",payload:{connectionPointId:t.uniqueId,value:this._connectionValues[t.uniqueId]}}),this._connectionValues[t.uniqueId]}get configuration(){return this._configuration}get hasPendingBlocks(){return this._pendingBlocks.length>0}_addPendingBlock(t){this._pendingBlocks.includes(t)||(this._pendingBlocks.push(t),this._pendingBlocks.sort((e,o)=>e.priority-o.priority))}_removePendingBlock(t){const e=this._pendingBlocks.indexOf(t);e!==-1&&this._pendingBlocks.splice(e,1)}_clearPendingBlocks(){for(const t of this._pendingBlocks)t._cancelPendingTasks(this);this._pendingBlocks.length=0}_notifyExecuteNode(t){var e;this.onNodeExecutedObservable.notifyObservers(t),(e=this.logger)==null||e.addLogItem({time:Date.now(),className:t.getClassName(),uniqueId:t.uniqueId,action:"ExecuteBlock"})}_notifyOnTick(t){var e;this._setGlobalContextVariable("timeSinceStart",t.timeSinceStart),this._setGlobalContextVariable("deltaTime",t.deltaTime);for(const o of this._pendingBlocks)(e=o._executeOnTick)==null||e.call(o,this)}_increaseExecutionId(){this._executionId++}get executionId(){return this._executionId}serialize(t={},e=tt){var o;t.uniqueId=this.uniqueId,t._userVariables={};for(const n in this._userVariables)e(n,this._userVariables[n],t._userVariables);t._connectionValues={};for(const n in this._connectionValues)e(n,this._connectionValues[n],t._connectionValues);this.assetsContext!==this.getScene()&&(t._assetsContext={meshes:this.assetsContext.meshes.map(n=>n.id),materials:this.assetsContext.materials.map(n=>n.id),textures:this.assetsContext.textures.map(n=>n.name),animations:this.assetsContext.animations.map(n=>n.name),lights:this.assetsContext.lights.map(n=>n.id),cameras:this.assetsContext.cameras.map(n=>n.id),sounds:(o=this.assetsContext.sounds)==null?void 0:o.map(n=>n.name),skeletons:this.assetsContext.skeletons.map(n=>n.id),particleSystems:this.assetsContext.particleSystems.map(n=>n.name),geometries:this.assetsContext.geometries.map(n=>n.id),multiMaterials:this.assetsContext.multiMaterials.map(n=>n.id),transformNodes:this.assetsContext.transformNodes.map(n=>n.id)})}getClassName(){return"FlowGraphContext"}}Ft([Et()],_t.prototype,"uniqueId",void 0);var ct;(function(a){a[a.Input=0]="Input",a[a.Output=1]="Output"})(ct||(ct={}));class wt{constructor(t,e,o){this._ownerBlock=o,this._connectedPoint=[],this.uniqueId=D(),this.connectedPointIds=[],this.name=t,this._connectionType=e}get connectionType(){return this._connectionType}_isSingularConnection(){return!0}isConnected(){return this._connectedPoint.length>0}connectTo(t){if(this._connectionType===t._connectionType)throw new Error(`Cannot connect two points of type ${this.connectionType}`);if(this._isSingularConnection()&&this._connectedPoint.length>0||t._isSingularConnection()&&t._connectedPoint.length>0)throw new Error("Max number of connections for point reached");this._connectedPoint.push(t),t._connectedPoint.push(this)}disconnectFrom(t,e=!0){const o=this._connectedPoint.indexOf(t),n=t._connectedPoint.indexOf(this);o===-1||n===-1||(e&&this._connectedPoint.splice(o,1),t._connectedPoint.splice(n,1))}disconnectFromAll(){for(const t of this._connectedPoint)this.disconnectFrom(t,!1);this._connectedPoint.length=0}dispose(){for(const t of this._connectedPoint)this.disconnectFrom(t)}serialize(t={}){t.uniqueId=this.uniqueId,t.name=this.name,t._connectionType=this._connectionType,t.connectedPointIds=[],t.className=this.getClassName();for(const e of this._connectedPoint)t.connectedPointIds.push(e.uniqueId)}getClassName(){return"FGConnection"}deserialize(t){this.uniqueId=t.uniqueId,this.name=t.name,this._connectionType=t._connectionType,this.connectedPointIds=t.connectedPointIds}}class Y extends wt{constructor(t,e,o,n,s=n.defaultValue,i=!1){super(t,e,o),this.richType=n,this._defaultValue=s,this._optional=i,this._isDisabled=!1,this._lastValue=null,this.dataTransformer=null,this.onValueChangedObservable=new U}get optional(){return this._optional}get isDisabled(){return this._isDisabled}set isDisabled(t){this._isDisabled!==t&&(this._isDisabled=t,this._isDisabled&&this.disconnectFromAll())}_isSingularConnection(){return this.connectionType===0}setValue(t,e){e._getConnectionValue(this)!==t&&(e._setConnectionValue(this,t),this.onValueChangedObservable.notifyObservers(t))}resetToDefaultValue(t){t._setConnectionValue(this,this._defaultValue)}connectTo(t){this._isDisabled||super.connectTo(t)}_getValueOrDefault(t){const e=t._getConnectionValue(this)??this._defaultValue;return this.dataTransformer?this.dataTransformer(e):e}getValue(t){if(this.connectionType===1){t._notifyExecuteNode(this._ownerBlock),this._ownerBlock._updateOutputs(t);const o=this._getValueOrDefault(t);return this._lastValue=o,this.richType.typeTransformer?this.richType.typeTransformer(o):o}const e=this.isConnected()?this._connectedPoint[0].getValue(t):this._getValueOrDefault(t);return this._lastValue=e,this.richType.typeTransformer?this.richType.typeTransformer(e):e}_getLastValue(){return this._lastValue}getClassName(){return"FlowGraphDataConnection"}serialize(t={}){super.serialize(t),t.richType={},this.richType.serialize(t.richType),t.optional=this._optional,tt("defaultValue",this._defaultValue,t)}}ht("FlowGraphDataConnection",Y);class $t{constructor(t){var e;this.config=t,this.uniqueId=D(),this.name=((e=this.config)==null?void 0:e.name)??this.getClassName(),this.dataInputs=[],this.dataOutputs=[]}_updateOutputs(t){}registerDataInput(t,e,o){const n=new Y(t,0,this,e,o);return this.dataInputs.push(n),n}registerDataOutput(t,e,o){const n=new Y(t,1,this,e,o);return this.dataOutputs.push(n),n}getDataInput(t){return this.dataInputs.find(e=>e.name===t)}getDataOutput(t){return this.dataOutputs.find(e=>e.name===t)}serialize(t={},e=tt){if(t.uniqueId=this.uniqueId,t.config={},this.config){const o=this.config,n=Object.keys(o);for(const s of n)e(s,o[s],t.config)}t.dataInputs=[],t.dataOutputs=[],t.className=this.getClassName();for(const o of this.dataInputs){const n={};o.serialize(n),t.dataInputs.push(n)}for(const o of this.dataOutputs){const n={};o.serialize(n),t.dataOutputs.push(n)}}deserialize(t){}_log(t,e,o){var n;(n=t.logger)==null||n.addLogItem({action:e,payload:o,className:this.getClassName(),uniqueId:this.uniqueId})}getClassName(){return"FlowGraphBlock"}}class z extends wt{constructor(){super(...arguments),this.priority=0}_isSingularConnection(){return!1}connectTo(t){super.connectTo(t),this._connectedPoint.sort((e,o)=>o.priority-e.priority)}_activateSignal(t){var e;if((e=t.logger)==null||e.addLogItem({action:"ActivateSignal",className:this._ownerBlock.getClassName(),uniqueId:this._ownerBlock.uniqueId,payload:{connectionType:this.connectionType,name:this.name}}),this.connectionType===0)t._notifyExecuteNode(this._ownerBlock),this._ownerBlock._execute(t,this),t._increaseExecutionId();else for(const o of this._connectedPoint)o._activateSignal(t)}}ht("FlowGraphSignalConnection",z);class J extends $t{constructor(t){super(t),this.priority=0,this.signalInputs=[],this.signalOutputs=[],this.in=this._registerSignalInput("in"),this.error=this._registerSignalOutput("error")}_registerSignalInput(t){const e=new z(t,0,this);return this.signalInputs.push(e),e}_registerSignalOutput(t){const e=new z(t,1,this);return this.signalOutputs.push(e),e}_unregisterSignalInput(t){const e=this.signalInputs.findIndex(o=>o.name===t);e!==-1&&(this.signalInputs[e].dispose(),this.signalInputs.splice(e,1))}_unregisterSignalOutput(t){const e=this.signalOutputs.findIndex(o=>o.name===t);e!==-1&&(this.signalOutputs[e].dispose(),this.signalOutputs.splice(e,1))}_reportError(t,e){this.error.payload=typeof e=="string"?new Error(e):e,this.error._activateSignal(t)}getSignalInput(t){return this.signalInputs.find(e=>e.name===t)}getSignalOutput(t){return this.signalOutputs.find(e=>e.name===t)}serialize(t={}){super.serialize(t),t.signalInputs=[],t.signalOutputs=[];for(const e of this.signalInputs){const o={};e.serialize(o),t.signalInputs.push(o)}for(const e of this.signalOutputs){const o={};e.serialize(o),t.signalOutputs.push(o)}}deserialize(t){for(let e=0;e{this.sceneReadyTriggered||(this.onEventTriggeredObservable.notifyObservers({type:"SceneReady"}),this.sceneReadyTriggered=!0)}),this._sceneDisposeObserver=this._scene.onDisposeObservable.add(()=>{this.onEventTriggeredObservable.notifyObservers({type:"SceneDispose"})}),this._sceneOnBeforeRenderObserver=this._scene.onBeforeRenderObservable.add(()=>{const t=this._scene.getEngine().getDeltaTime()/1e3;this.onEventTriggeredObservable.notifyObservers({type:"SceneBeforeRender",payload:{timeSinceStart:this._startingTime,deltaTime:t}}),this._startingTime+=t}),this._meshPickedObserver=this._scene.onPointerObservable.add(t=>{this.onEventTriggeredObservable.notifyObservers({type:"MeshPick",payload:t})},rt.POINTERPICK),this._meshUnderPointerObserver=this._scene.onMeshUnderPointerUpdatedObservable.add(t=>{const e=t.pointerId,o=t.mesh,n=this._pointerUnderMeshState[e];!n&&o?this.onEventTriggeredObservable.notifyObservers({type:"PointerOver",payload:{pointerId:e,mesh:o}}):n&&!o?this.onEventTriggeredObservable.notifyObservers({type:"PointerOut",payload:{pointerId:e,mesh:n}}):n&&o&&n!==o&&(this.onEventTriggeredObservable.notifyObservers({type:"PointerOut",payload:{pointerId:e,mesh:n,over:o}}),this.onEventTriggeredObservable.notifyObservers({type:"PointerOver",payload:{pointerId:e,mesh:o,out:n}})),this._pointerUnderMeshState[e]=o},rt.POINTERMOVE)}dispose(){var t,e,o,n,s;(t=this._sceneDisposeObserver)==null||t.remove(),(e=this._sceneReadyObserver)==null||e.remove(),(o=this._sceneOnBeforeRenderObserver)==null||o.remove(),(n=this._meshPickedObserver)==null||n.remove(),(s=this._meshUnderPointerObserver)==null||s.remove(),this.onEventTriggeredObservable.clear()}}function dt(a,t){return!!(a.parent&&(a.parent===t||dt(a.parent,t)))}function de(a){if(a.getClassName)return a.getClassName()}function fe(a,t){return a===t&&(a==="Vector2"||a==="Vector3"||a==="Vector4"||a==="Quaternion")}function ye(a,t){return a===t&&(a==="Matrix"||a==="Matrix2D"||a==="Matrix3D")}function ge(a,t){return a==="FlowGraphInteger"&&t==="FlowGraphInteger"}function me(a,t){const e=typeof a=="number"||typeof(a==null?void 0:a.value)=="number";return e&&!t?!isNaN(Wt(a)):e}function Wt(a){return typeof a=="number"?a:a.value}var lt;(function(a){a[a.Stopped=0]="Stopped",a[a.Started=1]="Started"})(lt||(lt={}));class Ut{get state(){return this._state}set state(t){this._state=t,this.onStateChangedObservable.notifyObservers(t)}constructor(t){this.onStateChangedObservable=new U,this._eventBlocks={SceneReady:[],SceneDispose:[],SceneBeforeRender:[],MeshPick:[],PointerDown:[],PointerUp:[],PointerMove:[],PointerOver:[],PointerOut:[],SceneAfterRender:[],NoTrigger:[]},this._executionContexts=[],this._state=0,this._scene=t.scene,this._sceneEventCoordinator=new Kt(this._scene),this._coordinator=t.coordinator,this._eventObserver=this._sceneEventCoordinator.onEventTriggeredObservable.add(e=>{for(const o of this._executionContexts){const n=this._getContextualOrder(e.type,o);for(const s of n)if(!s._executeEvent(o,e.payload))break}switch(e.type){case"SceneReady":this._sceneEventCoordinator.sceneReadyTriggered=!0;break;case"SceneBeforeRender":for(const o of this._executionContexts)o._notifyOnTick(e.payload);break;case"SceneDispose":this.dispose();break}})}createContext(){const t=new _t({scene:this._scene,coordinator:this._coordinator});return this._executionContexts.push(t),t}getContext(t){return this._executionContexts[t]}addEventBlock(t){if((t.type==="PointerOver"||t.type==="PointerOut")&&(this._scene.constantlyUpdateMeshUnderPointer=!0),t.type!=="NoTrigger"&&this._eventBlocks[t.type].push(t),this.state===1)for(const e of this._executionContexts)t._startPendingTasks(e);else this.onStateChangedObservable.addOnce(e=>{if(e===1)for(const o of this._executionContexts)t._startPendingTasks(o)})}start(){this.state!==1&&(this._executionContexts.length===0&&this.createContext(),this.onStateChangedObservable.add(t=>{t===1&&(this._startPendingEvents(),this._scene.isReady(!0)&&this._sceneEventCoordinator.onEventTriggeredObservable.notifyObservers({type:"SceneReady"}))}),this.state=1)}_startPendingEvents(){for(const t of this._executionContexts)for(const e in this._eventBlocks){const o=this._getContextualOrder(e,t);for(const n of o)n._startPendingTasks(t)}}_getContextualOrder(t,e){const o=this._eventBlocks[t].sort((n,s)=>s.initPriority-n.initPriority);if(t==="MeshPick"){const n=[];for(const s of o){const i=s.asset.getValue(e);let c=0;for(;c0;){const n=e.pop();t(n);for(const s of n.dataInputs)for(const i of s._connectedPoint)o.has(i._ownerBlock.uniqueId)||(e.push(i._ownerBlock),o.add(i._ownerBlock.uniqueId));if(n instanceof J)for(const s of n.signalOutputs)for(const i of s._connectedPoint)o.has(i._ownerBlock.uniqueId)||(e.push(i._ownerBlock),o.add(i._ownerBlock.uniqueId))}}serialize(t={},e){t.allBlocks=[],this.visitAllBlocks(o=>{const n={};o.serialize(n),t.allBlocks.push(n)}),t.executionContexts=[];for(const o of this._executionContexts){const n={};o.serialize(n,e),t.executionContexts.push(n)}}}class E{constructor(t){this.config=t,this.dispatchEventsSynchronously=!0,this._flowGraphs=[],this._customEventsMap=new Map,this._eventExecutionCounter=new Map,this._executeOnNextFrame=[],this._eventUniqueId=0,this._disposeObserver=this.config.scene.onDisposeObservable.add(()=>{this.dispose()}),this._onBeforeRenderObserver=this.config.scene.onBeforeRenderObservable.add(()=>{this._eventExecutionCounter.clear();const o=this._executeOnNextFrame.slice(0);if(o.length)for(const n of o){this.notifyCustomEvent(n.id,n.data,!1);const s=this._executeOnNextFrame.findIndex(i=>i.uniqueId===n.uniqueId);s!==-1&&this._executeOnNextFrame.splice(s,1)}});let e=E.SceneCoordinators.get(this.config.scene);e||(e=[],E.SceneCoordinators.set(this.config.scene,e)),e.push(this)}createGraph(){const t=new Ut({scene:this.config.scene,coordinator:this});return this._flowGraphs.push(t),t}removeGraph(t){const e=this._flowGraphs.indexOf(t);e!==-1&&(t.dispose(),this._flowGraphs.splice(e,1))}start(){for(const t of this._flowGraphs)t.start()}dispose(){var o,n;for(const s of this._flowGraphs)s.dispose();this._flowGraphs.length=0,(o=this._disposeObserver)==null||o.remove(),(n=this._onBeforeRenderObserver)==null||n.remove();const t=E.SceneCoordinators.get(this.config.scene)??[],e=t.indexOf(this);e!==-1&&t.splice(e,1)}serialize(t,e){t._flowGraphs=[];for(const o of this._flowGraphs){const n={};o.serialize(n,e),t._flowGraphs.push(n)}t.dispatchEventsSynchronously=this.dispatchEventsSynchronously}get flowGraphs(){return this._flowGraphs}getCustomEventObservable(t){let e=this._customEventsMap.get(t);return e||(e=new U,this._customEventsMap.set(t,e)),e}notifyCustomEvent(t,e,o=!this.dispatchEventsSynchronously){if(o){this._executeOnNextFrame.push({id:t,data:e,uniqueId:this._eventUniqueId++});return}if(this._eventExecutionCounter.has(t)){const s=this._eventExecutionCounter.get(t);if(this._eventExecutionCounter.set(t,s+1),s>=E.MaxEventTypeExecutionPerFrame){s===E.MaxEventTypeExecutionPerFrame&&w.Warn(`FlowGraphCoordinator: Too many executions of event "${t}".`);return}}else this._eventExecutionCounter.set(t,1);const n=this._customEventsMap.get(t);n&&n.notifyObservers(e)}}E.MaxEventsPerType=30;E.MaxEventTypeExecutionPerFrame=30;E.SceneCoordinators=new Map;const j={};function Qt(a,t,e){j[`${a}/${t}`]=e}function Ht(a){switch(a){case"FlowGraphPlayAnimationBlock":return async()=>(await r(async()=>{const{FlowGraphPlayAnimationBlock:t}=await import("./flowGraphPlayAnimationBlock-B9FP0RfS.js");return{FlowGraphPlayAnimationBlock:t}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]))).FlowGraphPlayAnimationBlock;case"FlowGraphStopAnimationBlock":return async()=>(await r(async()=>{const{FlowGraphStopAnimationBlock:t}=await import("./flowGraphStopAnimationBlock-ChXcK4cb.js");return{FlowGraphStopAnimationBlock:t}},__vite__mapDeps([32,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphStopAnimationBlock;case"FlowGraphPauseAnimationBlock":return async()=>(await r(async()=>{const{FlowGraphPauseAnimationBlock:t}=await import("./flowGraphPauseAnimationBlock-DAoY0eJc.js");return{FlowGraphPauseAnimationBlock:t}},__vite__mapDeps([33,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphPauseAnimationBlock;case"FlowGraphInterpolationBlock":return async()=>(await r(async()=>{const{FlowGraphInterpolationBlock:t}=await import("./flowGraphInterpolationBlock-CYcMKCGO.js");return{FlowGraphInterpolationBlock:t}},__vite__mapDeps([34,1,2,3,4,5,7,8,9,10,23,24,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,29,30,31]))).FlowGraphInterpolationBlock;case"FlowGraphSceneReadyEventBlock":return async()=>(await r(async()=>{const{FlowGraphSceneReadyEventBlock:t}=await import("./flowGraphSceneReadyEventBlock-Crn-rAIL.js");return{FlowGraphSceneReadyEventBlock:t}},__vite__mapDeps([35,4,23,24,3,2,9,5,1,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSceneReadyEventBlock;case"FlowGraphSceneTickEventBlock":return async()=>(await r(async()=>{const{FlowGraphSceneTickEventBlock:t}=await import("./flowGraphSceneTickEventBlock-gf3YDJRe.js");return{FlowGraphSceneTickEventBlock:t}},__vite__mapDeps([36,4,1,2,3,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSceneTickEventBlock;case"FlowGraphSendCustomEventBlock":return async()=>(await r(async()=>{const{FlowGraphSendCustomEventBlock:t}=await import("./flowGraphSendCustomEventBlock-Ba8XUM8G.js");return{FlowGraphSendCustomEventBlock:t}},__vite__mapDeps([37,4,23,24,3,2,9,5,1,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSendCustomEventBlock;case"FlowGraphReceiveCustomEventBlock":return async()=>(await r(async()=>{const{FlowGraphReceiveCustomEventBlock:t}=await import("./flowGraphReceiveCustomEventBlock-CId8KX4K.js");return{FlowGraphReceiveCustomEventBlock:t}},__vite__mapDeps([38,13,3,4,9,2,5,23,24,1,21,25,26,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphReceiveCustomEventBlock;case"FlowGraphMeshPickEventBlock":return async()=>(await r(async()=>{const{FlowGraphMeshPickEventBlock:t}=await import("./flowGraphMeshPickEventBlock-WsLHiXtY.js");return{FlowGraphMeshPickEventBlock:t}},__vite__mapDeps([39,21,3,4,1,2,5,23,24,9,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphMeshPickEventBlock;case"FlowGraphEBlock":return async()=>(await r(async()=>{const{FlowGraphEBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphEBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphEBlock;case"FlowGraphPIBlock":return async()=>(await r(async()=>{const{FlowGraphPiBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphPiBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphPiBlock;case"FlowGraphInfBlock":return async()=>(await r(async()=>{const{FlowGraphInfBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphInfBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphInfBlock;case"FlowGraphNaNBlock":return async()=>(await r(async()=>{const{FlowGraphNaNBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphNaNBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphNaNBlock;case"FlowGraphRandomBlock":return async()=>(await r(async()=>{const{FlowGraphRandomBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphRandomBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphRandomBlock;case"FlowGraphAddBlock":return async()=>(await r(async()=>{const{FlowGraphAddBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphAddBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAddBlock;case"FlowGraphSubtractBlock":return async()=>(await r(async()=>{const{FlowGraphSubtractBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphSubtractBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSubtractBlock;case"FlowGraphMultiplyBlock":return async()=>(await r(async()=>{const{FlowGraphMultiplyBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphMultiplyBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphMultiplyBlock;case"FlowGraphDivideBlock":return async()=>(await r(async()=>{const{FlowGraphDivideBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphDivideBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphDivideBlock;case"FlowGraphAbsBlock":return async()=>(await r(async()=>{const{FlowGraphAbsBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphAbsBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAbsBlock;case"FlowGraphSignBlock":return async()=>(await r(async()=>{const{FlowGraphSignBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphSignBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSignBlock;case"FlowGraphTruncBlock":return async()=>(await r(async()=>{const{FlowGraphTruncBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphTruncBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphTruncBlock;case"FlowGraphFloorBlock":return async()=>(await r(async()=>{const{FlowGraphFloorBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphFloorBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphFloorBlock;case"FlowGraphCeilBlock":return async()=>(await r(async()=>{const{FlowGraphCeilBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphCeilBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCeilBlock;case"FlowGraphRoundBlock":return async()=>(await r(async()=>{const{FlowGraphRoundBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphRoundBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphRoundBlock;case"FlowGraphFractBlock":return async()=>(await r(async()=>{const{FlowGraphFractionBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphFractionBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphFractionBlock;case"FlowGraphNegationBlock":return async()=>(await r(async()=>{const{FlowGraphNegationBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphNegationBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphNegationBlock;case"FlowGraphModuloBlock":return async()=>(await r(async()=>{const{FlowGraphModuloBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphModuloBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphModuloBlock;case"FlowGraphMinBlock":return async()=>(await r(async()=>{const{FlowGraphMinBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphMinBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphMinBlock;case"FlowGraphMaxBlock":return async()=>(await r(async()=>{const{FlowGraphMaxBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphMaxBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphMaxBlock;case"FlowGraphClampBlock":return async()=>(await r(async()=>{const{FlowGraphClampBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphClampBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphClampBlock;case"FlowGraphSaturateBlock":return async()=>(await r(async()=>{const{FlowGraphSaturateBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphSaturateBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSaturateBlock;case"FlowGraphMathInterpolationBlock":return async()=>(await r(async()=>{const{FlowGraphMathInterpolationBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphMathInterpolationBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphMathInterpolationBlock;case"FlowGraphEqualityBlock":return async()=>(await r(async()=>{const{FlowGraphEqualityBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphEqualityBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphEqualityBlock;case"FlowGraphLessThanBlock":return async()=>(await r(async()=>{const{FlowGraphLessThanBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphLessThanBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphLessThanBlock;case"FlowGraphLessThanOrEqualBlock":return async()=>(await r(async()=>{const{FlowGraphLessThanOrEqualBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphLessThanOrEqualBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphLessThanOrEqualBlock;case"FlowGraphGreaterThanBlock":return async()=>(await r(async()=>{const{FlowGraphGreaterThanBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphGreaterThanBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphGreaterThanBlock;case"FlowGraphGreaterThanOrEqualBlock":return async()=>(await r(async()=>{const{FlowGraphGreaterThanOrEqualBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphGreaterThanOrEqualBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphGreaterThanOrEqualBlock;case"FlowGraphIsNaNBlock":return async()=>(await r(async()=>{const{FlowGraphIsNanBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphIsNanBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphIsNanBlock;case"FlowGraphIsInfBlock":return async()=>(await r(async()=>{const{FlowGraphIsInfinityBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphIsInfinityBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphIsInfinityBlock;case"FlowGraphDegToRadBlock":return async()=>(await r(async()=>{const{FlowGraphDegToRadBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphDegToRadBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphDegToRadBlock;case"FlowGraphRadToDegBlock":return async()=>(await r(async()=>{const{FlowGraphRadToDegBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphRadToDegBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphRadToDegBlock;case"FlowGraphSinBlock":return async()=>(await r(async()=>{const{FlowGraphSinBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphSinBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSinBlock;case"FlowGraphCosBlock":return async()=>(await r(async()=>{const{FlowGraphCosBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphCosBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCosBlock;case"FlowGraphTanBlock":return async()=>(await r(async()=>{const{FlowGraphTanBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphTanBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphTanBlock;case"FlowGraphASinBlock":return async()=>(await r(async()=>{const{FlowGraphAsinBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphAsinBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAsinBlock;case"FlowGraphACosBlock":return async()=>(await r(async()=>{const{FlowGraphAcosBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphAcosBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAcosBlock;case"FlowGraphATanBlock":return async()=>(await r(async()=>{const{FlowGraphAtanBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphAtanBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAtanBlock;case"FlowGraphATan2Block":return async()=>(await r(async()=>{const{FlowGraphAtan2Block:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphAtan2Block:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAtan2Block;case"FlowGraphSinhBlock":return async()=>(await r(async()=>{const{FlowGraphSinhBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphSinhBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSinhBlock;case"FlowGraphCoshBlock":return async()=>(await r(async()=>{const{FlowGraphCoshBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphCoshBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCoshBlock;case"FlowGraphTanhBlock":return async()=>(await r(async()=>{const{FlowGraphTanhBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphTanhBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphTanhBlock;case"FlowGraphASinhBlock":return async()=>(await r(async()=>{const{FlowGraphAsinhBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphAsinhBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAsinhBlock;case"FlowGraphACoshBlock":return async()=>(await r(async()=>{const{FlowGraphAcoshBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphAcoshBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAcoshBlock;case"FlowGraphATanhBlock":return async()=>(await r(async()=>{const{FlowGraphAtanhBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphAtanhBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAtanhBlock;case"FlowGraphExponentialBlock":return async()=>(await r(async()=>{const{FlowGraphExpBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphExpBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphExpBlock;case"FlowGraphLogBlock":return async()=>(await r(async()=>{const{FlowGraphLogBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphLogBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphLogBlock;case"FlowGraphLog2Block":return async()=>(await r(async()=>{const{FlowGraphLog2Block:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphLog2Block:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphLog2Block;case"FlowGraphLog10Block":return async()=>(await r(async()=>{const{FlowGraphLog10Block:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphLog10Block:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphLog10Block;case"FlowGraphSquareRootBlock":return async()=>(await r(async()=>{const{FlowGraphSquareRootBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphSquareRootBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSquareRootBlock;case"FlowGraphPowerBlock":return async()=>(await r(async()=>{const{FlowGraphPowerBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphPowerBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphPowerBlock;case"FlowGraphCubeRootBlock":return async()=>(await r(async()=>{const{FlowGraphCubeRootBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphCubeRootBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCubeRootBlock;case"FlowGraphBitwiseAndBlock":return async()=>(await r(async()=>{const{FlowGraphBitwiseAndBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphBitwiseAndBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBitwiseAndBlock;case"FlowGraphBitwiseOrBlock":return async()=>(await r(async()=>{const{FlowGraphBitwiseOrBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphBitwiseOrBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBitwiseOrBlock;case"FlowGraphBitwiseNotBlock":return async()=>(await r(async()=>{const{FlowGraphBitwiseNotBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphBitwiseNotBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBitwiseNotBlock;case"FlowGraphBitwiseXorBlock":return async()=>(await r(async()=>{const{FlowGraphBitwiseXorBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphBitwiseXorBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBitwiseXorBlock;case"FlowGraphBitwiseLeftShiftBlock":return async()=>(await r(async()=>{const{FlowGraphBitwiseLeftShiftBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphBitwiseLeftShiftBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBitwiseLeftShiftBlock;case"FlowGraphBitwiseRightShiftBlock":return async()=>(await r(async()=>{const{FlowGraphBitwiseRightShiftBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphBitwiseRightShiftBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBitwiseRightShiftBlock;case"FlowGraphLengthBlock":return async()=>(await r(async()=>{const{FlowGraphLengthBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphLengthBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphLengthBlock;case"FlowGraphNormalizeBlock":return async()=>(await r(async()=>{const{FlowGraphNormalizeBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphNormalizeBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphNormalizeBlock;case"FlowGraphDotBlock":return async()=>(await r(async()=>{const{FlowGraphDotBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphDotBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphDotBlock;case"FlowGraphCrossBlock":return async()=>(await r(async()=>{const{FlowGraphCrossBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphCrossBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCrossBlock;case"FlowGraphRotate2DBlock":return async()=>(await r(async()=>{const{FlowGraphRotate2DBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphRotate2DBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphRotate2DBlock;case"FlowGraphRotate3DBlock":return async()=>(await r(async()=>{const{FlowGraphRotate3DBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphRotate3DBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphRotate3DBlock;case"FlowGraphTransposeBlock":return async()=>(await r(async()=>{const{FlowGraphTransposeBlock:t}=await import("./flowGraphMatrixMathBlocks-Dg7VLPFP.js");return{FlowGraphTransposeBlock:t}},__vite__mapDeps([45,1,2,3,4,5,43,42,41,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphTransposeBlock;case"FlowGraphDeterminantBlock":return async()=>(await r(async()=>{const{FlowGraphDeterminantBlock:t}=await import("./flowGraphMatrixMathBlocks-Dg7VLPFP.js");return{FlowGraphDeterminantBlock:t}},__vite__mapDeps([45,1,2,3,4,5,43,42,41,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphDeterminantBlock;case"FlowGraphInvertMatrixBlock":return async()=>(await r(async()=>{const{FlowGraphInvertMatrixBlock:t}=await import("./flowGraphMatrixMathBlocks-Dg7VLPFP.js");return{FlowGraphInvertMatrixBlock:t}},__vite__mapDeps([45,1,2,3,4,5,43,42,41,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphInvertMatrixBlock;case"FlowGraphMatrixMultiplicationBlock":return async()=>(await r(async()=>{const{FlowGraphMatrixMultiplicationBlock:t}=await import("./flowGraphMatrixMathBlocks-Dg7VLPFP.js");return{FlowGraphMatrixMultiplicationBlock:t}},__vite__mapDeps([45,1,2,3,4,5,43,42,41,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphMatrixMultiplicationBlock;case"FlowGraphBranchBlock":return async()=>(await r(async()=>{const{FlowGraphBranchBlock:t}=await import("./flowGraphBranchBlock-CPQ1Lj8X.js");return{FlowGraphBranchBlock:t}},__vite__mapDeps([46,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBranchBlock;case"FlowGraphSetDelayBlock":return async()=>(await r(async()=>{const{FlowGraphSetDelayBlock:t}=await import("./flowGraphSetDelayBlock-BA13Fleb.js");return{FlowGraphSetDelayBlock:t}},__vite__mapDeps([47,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSetDelayBlock;case"FlowGraphCancelDelayBlock":return async()=>(await r(async()=>{const{FlowGraphCancelDelayBlock:t}=await import("./flowGraphCancelDelayBlock-CiPuCgGz.js");return{FlowGraphCancelDelayBlock:t}},__vite__mapDeps([48,4,1,2,3,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCancelDelayBlock;case"FlowGraphCallCounterBlock":return async()=>(await r(async()=>{const{FlowGraphCallCounterBlock:t}=await import("./flowGraphCounterBlock-OjaFZjW5.js");return{FlowGraphCallCounterBlock:t}},__vite__mapDeps([49,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCallCounterBlock;case"FlowGraphDebounceBlock":return async()=>(await r(async()=>{const{FlowGraphDebounceBlock:t}=await import("./flowGraphDebounceBlock-fxe8WoxT.js");return{FlowGraphDebounceBlock:t}},__vite__mapDeps([50,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphDebounceBlock;case"FlowGraphThrottleBlock":return async()=>(await r(async()=>{const{FlowGraphThrottleBlock:t}=await import("./flowGraphThrottleBlock-B-yO7rgE.js");return{FlowGraphThrottleBlock:t}},__vite__mapDeps([51,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphThrottleBlock;case"FlowGraphDoNBlock":return async()=>(await r(async()=>{const{FlowGraphDoNBlock:t}=await import("./flowGraphDoNBlock-C4vVyovK.js");return{FlowGraphDoNBlock:t}},__vite__mapDeps([52,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphDoNBlock;case"FlowGraphFlipFlopBlock":return async()=>(await r(async()=>{const{FlowGraphFlipFlopBlock:t}=await import("./flowGraphFlipFlopBlock-BSjrwcqq.js");return{FlowGraphFlipFlopBlock:t}},__vite__mapDeps([53,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphFlipFlopBlock;case"FlowGraphForLoopBlock":return async()=>(await r(async()=>{const{FlowGraphForLoopBlock:t}=await import("./flowGraphForLoopBlock-jI9z_K5X.js");return{FlowGraphForLoopBlock:t}},__vite__mapDeps([54,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphForLoopBlock;case"FlowGraphMultiGateBlock":return async()=>(await r(async()=>{const{FlowGraphMultiGateBlock:t}=await import("./flowGraphMultiGateBlock-Cy-tdSZ3.js");return{FlowGraphMultiGateBlock:t}},__vite__mapDeps([55,4,1,2,3,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphMultiGateBlock;case"FlowGraphSequenceBlock":return async()=>(await r(async()=>{const{FlowGraphSequenceBlock:t}=await import("./flowGraphSequenceBlock-Bcye5mDB.js");return{FlowGraphSequenceBlock:t}},__vite__mapDeps([56,4,23,24,3,2,9,5,1,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSequenceBlock;case"FlowGraphSwitchBlock":return async()=>(await r(async()=>{const{FlowGraphSwitchBlock:t}=await import("./flowGraphSwitchBlock-CfuObCnO.js");return{FlowGraphSwitchBlock:t}},__vite__mapDeps([57,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSwitchBlock;case"FlowGraphWaitAllBlock":return async()=>(await r(async()=>{const{FlowGraphWaitAllBlock:t}=await import("./flowGraphWaitAllBlock-VXxeVnO_.js");return{FlowGraphWaitAllBlock:t}},__vite__mapDeps([58,4,1,2,3,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphWaitAllBlock;case"FlowGraphWhileLoopBlock":return async()=>(await r(async()=>{const{FlowGraphWhileLoopBlock:t}=await import("./flowGraphWhileLoopBlock-BYmEfSWz.js");return{FlowGraphWhileLoopBlock:t}},__vite__mapDeps([59,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphWhileLoopBlock;case"FlowGraphConsoleLogBlock":return async()=>(await r(async()=>{const{FlowGraphConsoleLogBlock:t}=await import("./flowGraphConsoleLogBlock-Cz7IRQyV.js");return{FlowGraphConsoleLogBlock:t}},__vite__mapDeps([60,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphConsoleLogBlock;case"FlowGraphConditionalBlock":return async()=>(await r(async()=>{const{FlowGraphConditionalDataBlock:t}=await import("./flowGraphConditionalDataBlock-Btzamqcx.js");return{FlowGraphConditionalDataBlock:t}},__vite__mapDeps([61,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphConditionalDataBlock;case"FlowGraphConstantBlock":return async()=>(await r(async()=>{const{FlowGraphConstantBlock:t}=await import("./flowGraphConstantBlock-BV1ij11S.js");return{FlowGraphConstantBlock:t}},__vite__mapDeps([62,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphConstantBlock;case"FlowGraphTransformCoordinatesSystemBlock":return async()=>(await r(async()=>{const{FlowGraphTransformCoordinatesSystemBlock:t}=await import("./flowGraphTransformCoordinatesSystemBlock-D3zEJ4A-.js");return{FlowGraphTransformCoordinatesSystemBlock:t}},__vite__mapDeps([63,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphTransformCoordinatesSystemBlock;case"FlowGraphGetAssetBlock":return async()=>(await r(async()=>{const{FlowGraphGetAssetBlock:t}=await import("./flowGraphGetAssetBlock-BeMIiO3t.js");return{FlowGraphGetAssetBlock:t}},__vite__mapDeps([64,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphGetAssetBlock;case"FlowGraphGetPropertyBlock":return async()=>(await r(async()=>{const{FlowGraphGetPropertyBlock:t}=await import("./flowGraphGetPropertyBlock-CAOmAIk8.js");return{FlowGraphGetPropertyBlock:t}},__vite__mapDeps([65,1,2,3,4,5,42,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphGetPropertyBlock;case"FlowGraphSetPropertyBlock":return async()=>(await r(async()=>{const{FlowGraphSetPropertyBlock:t}=await import("./flowGraphSetPropertyBlock-DxJBj-a9.js");return{FlowGraphSetPropertyBlock:t}},__vite__mapDeps([66,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSetPropertyBlock;case"FlowGraphGetVariableBlock":return async()=>(await r(async()=>{const{FlowGraphGetVariableBlock:t}=await import("./flowGraphGetVariableBlock-BaRkHNJ_.js");return{FlowGraphGetVariableBlock:t}},__vite__mapDeps([67,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphGetVariableBlock;case"FlowGraphSetVariableBlock":return async()=>(await r(async()=>{const{FlowGraphSetVariableBlock:t}=await import("./flowGraphSetVariableBlock-BG0Kfa6g.js");return{FlowGraphSetVariableBlock:t}},__vite__mapDeps([68,4,1,2,3,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphSetVariableBlock;case"FlowGraphJsonPointerParserBlock":return async()=>(await r(async()=>{const{FlowGraphJsonPointerParserBlock:t}=await import("./flowGraphJsonPointerParserBlock-CZ93bSRG.js");return{FlowGraphJsonPointerParserBlock:t}},__vite__mapDeps([69,1,2,3,4,5,42,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphJsonPointerParserBlock;case"FlowGraphLeadingZerosBlock":return async()=>(await r(async()=>{const{FlowGraphLeadingZerosBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphLeadingZerosBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphLeadingZerosBlock;case"FlowGraphTrailingZerosBlock":return async()=>(await r(async()=>{const{FlowGraphTrailingZerosBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphTrailingZerosBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphTrailingZerosBlock;case"FlowGraphOneBitsCounterBlock":return async()=>(await r(async()=>{const{FlowGraphOneBitsCounterBlock:t}=await import("./flowGraphMathBlocks-HeZhspxf.js");return{FlowGraphOneBitsCounterBlock:t}},__vite__mapDeps([40,4,1,2,3,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphOneBitsCounterBlock;case"FlowGraphCombineVector2Block":return async()=>(await r(async()=>{const{FlowGraphCombineVector2Block:t}=await import("./flowGraphMathCombineExtractBlocks-Dg02KNQM.js");return{FlowGraphCombineVector2Block:t}},__vite__mapDeps([70,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCombineVector2Block;case"FlowGraphCombineVector3Block":return async()=>(await r(async()=>{const{FlowGraphCombineVector3Block:t}=await import("./flowGraphMathCombineExtractBlocks-Dg02KNQM.js");return{FlowGraphCombineVector3Block:t}},__vite__mapDeps([70,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCombineVector3Block;case"FlowGraphCombineVector4Block":return async()=>(await r(async()=>{const{FlowGraphCombineVector4Block:t}=await import("./flowGraphMathCombineExtractBlocks-Dg02KNQM.js");return{FlowGraphCombineVector4Block:t}},__vite__mapDeps([70,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCombineVector4Block;case"FlowGraphCombineMatrixBlock":return async()=>(await r(async()=>{const{FlowGraphCombineMatrixBlock:t}=await import("./flowGraphMathCombineExtractBlocks-Dg02KNQM.js");return{FlowGraphCombineMatrixBlock:t}},__vite__mapDeps([70,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCombineMatrixBlock;case"FlowGraphExtractVector2Block":return async()=>(await r(async()=>{const{FlowGraphExtractVector2Block:t}=await import("./flowGraphMathCombineExtractBlocks-Dg02KNQM.js");return{FlowGraphExtractVector2Block:t}},__vite__mapDeps([70,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphExtractVector2Block;case"FlowGraphExtractVector3Block":return async()=>(await r(async()=>{const{FlowGraphExtractVector3Block:t}=await import("./flowGraphMathCombineExtractBlocks-Dg02KNQM.js");return{FlowGraphExtractVector3Block:t}},__vite__mapDeps([70,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphExtractVector3Block;case"FlowGraphExtractVector4Block":return async()=>(await r(async()=>{const{FlowGraphExtractVector4Block:t}=await import("./flowGraphMathCombineExtractBlocks-Dg02KNQM.js");return{FlowGraphExtractVector4Block:t}},__vite__mapDeps([70,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphExtractVector4Block;case"FlowGraphExtractMatrixBlock":return async()=>(await r(async()=>{const{FlowGraphExtractMatrixBlock:t}=await import("./flowGraphMathCombineExtractBlocks-Dg02KNQM.js");return{FlowGraphExtractMatrixBlock:t}},__vite__mapDeps([70,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphExtractMatrixBlock;case"FlowGraphTransformVectorBlock":return async()=>(await r(async()=>{const{FlowGraphTransformBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphTransformBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphTransformBlock;case"FlowGraphTransformCoordinatesBlock":return async()=>(await r(async()=>{const{FlowGraphTransformCoordinatesBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphTransformCoordinatesBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphTransformCoordinatesBlock;case"FlowGraphConjugateBlock":return async()=>(await r(async()=>{const{FlowGraphConjugateBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphConjugateBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphConjugateBlock;case"FlowGraphAngleBetweenBlock":return async()=>(await r(async()=>{const{FlowGraphAngleBetweenBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphAngleBetweenBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAngleBetweenBlock;case"FlowGraphQuaternionFromAxisAngleBlock":return async()=>(await r(async()=>{const{FlowGraphQuaternionFromAxisAngleBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphQuaternionFromAxisAngleBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphQuaternionFromAxisAngleBlock;case"FlowGraphAxisAngleFromQuaternionBlock":return async()=>(await r(async()=>{const{FlowGraphAxisAngleFromQuaternionBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphAxisAngleFromQuaternionBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphAxisAngleFromQuaternionBlock;case"FlowGraphQuaternionFromDirectionsBlock":return async()=>(await r(async()=>{const{FlowGraphQuaternionFromDirectionsBlock:t}=await import("./flowGraphVectorMathBlocks-BGV5dKz9.js");return{FlowGraphQuaternionFromDirectionsBlock:t}},__vite__mapDeps([44,1,2,3,4,5,41,42,43,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphQuaternionFromDirectionsBlock;case"FlowGraphMatrixDecompose":return async()=>(await r(async()=>{const{FlowGraphMatrixDecomposeBlock:t}=await import("./flowGraphMatrixMathBlocks-Dg7VLPFP.js");return{FlowGraphMatrixDecomposeBlock:t}},__vite__mapDeps([45,1,2,3,4,5,43,42,41,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphMatrixDecomposeBlock;case"FlowGraphMatrixCompose":return async()=>(await r(async()=>{const{FlowGraphMatrixComposeBlock:t}=await import("./flowGraphMatrixMathBlocks-Dg7VLPFP.js");return{FlowGraphMatrixComposeBlock:t}},__vite__mapDeps([45,1,2,3,4,5,43,42,41,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphMatrixComposeBlock;case"FlowGraphBooleanToFloat":return async()=>(await r(async()=>{const{FlowGraphBooleanToFloat:t}=await import("./flowGraphTypeToTypeBlocks-DdLy9OXZ.js");return{FlowGraphBooleanToFloat:t}},__vite__mapDeps([71,43,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBooleanToFloat;case"FlowGraphBooleanToInt":return async()=>(await r(async()=>{const{FlowGraphBooleanToInt:t}=await import("./flowGraphTypeToTypeBlocks-DdLy9OXZ.js");return{FlowGraphBooleanToInt:t}},__vite__mapDeps([71,43,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBooleanToInt;case"FlowGraphFloatToBoolean":return async()=>(await r(async()=>{const{FlowGraphFloatToBoolean:t}=await import("./flowGraphTypeToTypeBlocks-DdLy9OXZ.js");return{FlowGraphFloatToBoolean:t}},__vite__mapDeps([71,43,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphFloatToBoolean;case"FlowGraphIntToBoolean":return async()=>(await r(async()=>{const{FlowGraphIntToBoolean:t}=await import("./flowGraphTypeToTypeBlocks-DdLy9OXZ.js");return{FlowGraphIntToBoolean:t}},__vite__mapDeps([71,43,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphIntToBoolean;case"FlowGraphIntToFloat":return async()=>(await r(async()=>{const{FlowGraphIntToFloat:t}=await import("./flowGraphTypeToTypeBlocks-DdLy9OXZ.js");return{FlowGraphIntToFloat:t}},__vite__mapDeps([71,43,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphIntToFloat;case"FlowGraphFloatToInt":return async()=>(await r(async()=>{const{FlowGraphFloatToInt:t}=await import("./flowGraphTypeToTypeBlocks-DdLy9OXZ.js");return{FlowGraphFloatToInt:t}},__vite__mapDeps([71,43,42,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphFloatToInt;case"FlowGraphEasingBlock":return async()=>(await r(async()=>{const{FlowGraphEasingBlock:t}=await import("./flowGraphEasingBlock-C_-3ZhLh.js");return{FlowGraphEasingBlock:t}},__vite__mapDeps([72,73,74,4,3,1,2,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphEasingBlock;case"FlowGraphBezierCurveEasing":return async()=>(await r(async()=>{const{FlowGraphBezierCurveEasingBlock:t}=await import("./flowGraphBezierCurveEasingBlock--1XIKCm2.js");return{FlowGraphBezierCurveEasingBlock:t}},__vite__mapDeps([75,73,74,4,3,1,2,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphBezierCurveEasingBlock;case"FlowGraphPointerOverEventBlock":return async()=>(await r(async()=>{const{FlowGraphPointerOverEventBlock:t}=await import("./flowGraphPointerOverEventBlock-wKWlgMPO.js");return{FlowGraphPointerOverEventBlock:t}},__vite__mapDeps([76,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphPointerOverEventBlock;case"FlowGraphPointerOutEventBlock":return async()=>(await r(async()=>{const{FlowGraphPointerOutEventBlock:t}=await import("./flowGraphPointerOutEventBlock-BpW1yJKn.js");return{FlowGraphPointerOutEventBlock:t}},__vite__mapDeps([77,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphPointerOutEventBlock;case"FlowGraphContextBlock":return async()=>(await r(async()=>{const{FlowGraphContextBlock:t}=await import("./flowGraphContextBlock-DeEwvFEY.js");return{FlowGraphContextBlock:t}},__vite__mapDeps([78,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphContextBlock;case"FlowGraphArrayIndexBlock":return async()=>(await r(async()=>{const{FlowGraphArrayIndexBlock:t}=await import("./flowGraphArrayIndexBlock-3XRt4h8L.js");return{FlowGraphArrayIndexBlock:t}},__vite__mapDeps([79,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphArrayIndexBlock;case"FlowGraphCodeExecutionBlock":return async()=>(await r(async()=>{const{FlowGraphCodeExecutionBlock:t}=await import("./flowGraphCodeExecutionBlock-BpjYYwgf.js");return{FlowGraphCodeExecutionBlock:t}},__vite__mapDeps([80,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphCodeExecutionBlock;case"FlowGraphIndexOfBlock":return async()=>(await r(async()=>{const{FlowGraphIndexOfBlock:t}=await import("./flowGraphIndexOfBlock-D1XW4kc4.js");return{FlowGraphIndexOfBlock:t}},__vite__mapDeps([81,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphIndexOfBlock;case"FlowGraphFunctionReference":return async()=>(await r(async()=>{const{FlowGraphFunctionReferenceBlock:t}=await import("./flowGraphFunctionReferenceBlock-CLhl7P_Q.js");return{FlowGraphFunctionReferenceBlock:t}},__vite__mapDeps([82,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphFunctionReferenceBlock;case"FlowGraphDataSwitchBlock":return async()=>(await r(async()=>{const{FlowGraphDataSwitchBlock:t}=await import("./flowGraphDataSwitchBlock-BUqG5JEg.js");return{FlowGraphDataSwitchBlock:t}},__vite__mapDeps([83,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphDataSwitchBlock;default:if(j[a])return j[a];throw new Error(`Unknown block name ${a}`)}}class Jt extends J{constructor(t){super(t),this.out=this._registerSignalOutput("out")}}class Zt extends Jt{constructor(t,e){if(super(t),this._eventsSignalOutputs={},this.done=this._registerSignalOutput("done"),e)for(const o of e)this._eventsSignalOutputs[o]=this._registerSignalOutput(o+"Event")}_executeOnTick(t){}_startPendingTasks(t){t._getExecutionVariable(this,"_initialized",!1)&&(this._cancelPendingTasks(t),this._resetAfterCanceled(t)),this._preparePendingTasks(t),t._addPendingBlock(this),this.out._activateSignal(t),t._setExecutionVariable(this,"_initialized",!0)}_resetAfterCanceled(t){t._deleteExecutionVariable(this,"_initialized"),t._removePendingBlock(this)}}class Xt extends Zt{constructor(){super(...arguments),this.initPriority=0,this.type="NoTrigger"}_execute(t){t._notifyExecuteNode(this),this.done._activateSignal(t)}}function Yt(a,t){for(const e of a)for(const o of e.dataOutputs)if(o.uniqueId===t)return o;throw new Error("Could not find data out connection with unique id "+t)}function zt(a,t){for(const e of a)if(e instanceof J){for(const o of e.signalInputs)if(o.uniqueId===t)return o}throw new Error("Could not find signal in connection with unique id "+t)}async function jt(a,t){const e=await Promise.all(a.allBlocks.map(async o=>await Ht(o.className)()));return te(a,t,e)}function te(a,t,e){const o=t.coordinator.createGraph(),n=[],s=t.valueParseFunction??H;for(let i=0;is.getMeshById(h)),lights:(c=p.lights)==null?void 0:c.map(h=>s.getLightByName(h)),cameras:(l=p.cameras)==null?void 0:l.map(h=>s.getCameraByName(h)),materials:(u=p.materials)==null?void 0:u.map(h=>s.getMaterialById(h)),textures:(m=p.textures)==null?void 0:m.map(h=>s.getTextureByName(h)),animations:(f=p.animations)==null?void 0:f.map(h=>s.animations.find(Q=>Q.name===h)),skeletons:(A=p.skeletons)==null?void 0:A.map(h=>s.getSkeletonByName(h)),particleSystems:(R=p.particleSystems)==null?void 0:R.map(h=>s.getParticleSystemById(h)),animationGroups:(L=p.animationGroups)==null?void 0:L.map(h=>s.getAnimationGroupByName(h)),transformNodes:(S=p.transformNodes)==null?void 0:S.map(h=>s.getTransformNodeById(h)),rootNodes:[],multiMaterials:[],morphTargetManagers:[],geometries:[],actionManagers:[],environmentTexture:null,postProcesses:[],sounds:null,effectLayers:[],layers:[],reflectionProbes:[],lensFlareSystems:[],proceduralTextures:[],getNodes:function(){throw new Error("Function not implemented.")}};o.assetsContext=v}for(const p in a._userVariables){const v=n(p,a._userVariables,o.assetsContext,s);o.userVariables[p]=v}for(const p in a._connectionValues){const v=n(p,a._connectionValues,o.assetsContext,s);o._setConnectionValueByKey(p,v)}return o}function oe(a,t,e){const o={},n=t.valueParseFunction??H;if(a.config)for(const i in a.config)o[i]=n(i,a.config,t.assetsContainer||t.scene,t.scene);if(Nt(a.className)){if(!t.pathConverter)throw new Error("Path converter is required for this block");o.pathConverter=t.pathConverter}const s=new e(o);s.uniqueId=a.uniqueId;for(let i=0;i{var c;const n=(c=t.values)==null?void 0:c[o];if(!n)throw w.Error(["No value found for event key",o]),new Error("Error parsing events");const s=this._types[n.type];if(!s)throw w.Error(["No type found for event value",n]),new Error("Error parsing events");const i=typeof n.value<"u"?this._parseVariable(n):void 0;return{id:o,type:s.flowGraphType,eventData:!0,value:i}})),this._events.push(e)}}_parseNodes(){var t;if(this._interactivityGraph.nodes)for(const e of this._interactivityGraph.nodes){if(typeof e.declaration!="number")throw w.Error(["No declaration found for node",e]),new Error("Error parsing nodes");const o=this._mappings[e.declaration];if(!o)throw w.Error(["No mapping found for node",e]),new Error("Error parsing nodes");if(o.flowGraphMapping.validation){const s=o.flowGraphMapping.validation(e,this._interactivityGraph,this._gltf);if(!s.valid)throw new Error(`Error validating interactivity node ${(t=this._interactivityGraph.declarations)==null?void 0:t[e.declaration].op} - ${s.error}`)}const n=[];for(const s of o.flowGraphMapping.blocks){const i=this._getEmptyBlock(s,o.fullOperationName);this._parseNodeConfiguration(e,i,o.flowGraphMapping,s),n.push(i)}this._nodes.push({blocks:n,fullOperationName:o.fullOperationName})}}_getEmptyBlock(t,e){return{uniqueId:D(),className:t,dataInputs:[],dataOutputs:[],signalInputs:[],signalOutputs:[],config:{},type:e,metadata:{}}}_parseNodeConfiguration(t,e,o,n){var i;const s=t.configuration;if(s)for(const c in s){const l=s[c];if(!l)throw new Error("Error parsing node configuration");const u=(i=o.configuration)==null?void 0:i[c];if(u&&u.toBlock?u.toBlock===n:o.blocks.indexOf(n)===0){let f=u==null?void 0:u.defaultValue;l!=null&&l.value&&(f=l.value),u!=null&&u.isArray||(f.length!==1&&w.Warn(`Invalid non-array value length: ${f.length}`),f=f[0]),u!=null&&u.dataTransformer&&(f=u.dataTransformer(f,this)),f!==void 0&&(e.config[(u==null?void 0:u.name)||c]={value:f})}}}_parseNodeConnections(t){var e,o,n,s,i,c,l,u,m,f,A,R,L,S,p,v,h,Q,et;for(let N=0;NB.className===_.toBlock)||T.blocks[0]).signalOutputs.push(M);const Z=y.node,g=this._nodes[Z];if(!g)throw w.Error(["No node found for input node id",Z]),new Error("Error parsing node connections");const V=at(g.fullOperationName);if(!V)throw w.Error(["No mapping found for input node",g]),new Error("Error parsing node connections");let k=(i=(s=V.inputs)==null?void 0:s.flows)==null?void 0:i[y.socket||"in"],P=!1;if(!k)for(const B in(c=V.inputs)==null?void 0:c.flows)B.startsWith("[")&&B.endsWith("]")&&(P=!0,k=(u=(l=V.inputs)==null?void 0:l.flows)==null?void 0:u[B]);const G=k?P?k.name.replace("$1",y.socket||""):k.name:y.socket||"in",$=k&&k.toBlock&&g.blocks.find(B=>B.className===k.toBlock)||g.blocks[0];let C=$.signalInputs.find(B=>B.name===G);C||(C=this._createNewSocketConnection(G),$.signalInputs.push(C)),C.connectedPointIds.push(M.uniqueId),M.connectedPointIds.push(C.uniqueId)}const nt=I.values||{},mt=Object.keys(nt);for(const d of mt){const y=nt[d];let _=(f=(m=F.flowGraphMapping.inputs)==null?void 0:m.values)==null?void 0:f[d],x=!1;if(!_)for(const g in(A=F.flowGraphMapping.inputs)==null?void 0:A.values)g.startsWith("[")&&g.endsWith("]")&&(x=!0,_=(L=(R=F.flowGraphMapping.inputs)==null?void 0:R.values)==null?void 0:L[g]);const M=_?x?_.name.replace("$1",d):_.name:d,q=this._createNewSocketConnection(M);if((_&&_.toBlock&&T.blocks.find(g=>g.className===_.toBlock)||T.blocks[0]).dataInputs.push(q),y.value!==void 0){const g=this._parseVariable(y,_&&_.dataTransformer);t._connectionValues[q.uniqueId]=g}else if(typeof y.node<"u"){const g=y.node,V=y.socket||"value",k=this._nodes[g];if(!k)throw w.Error(["No node found for output socket reference",y]),new Error("Error parsing node connections");const P=at(k.fullOperationName);if(!P)throw w.Error(["No mapping found for output socket reference",y]),new Error("Error parsing node connections");let G=(p=(S=P.outputs)==null?void 0:S.values)==null?void 0:p[V],$=!1;if(!G)for(const b in(v=P.outputs)==null?void 0:v.values)b.startsWith("[")&&b.endsWith("]")&&($=!0,G=(Q=(h=P.outputs)==null?void 0:h.values)==null?void 0:Q[b]);const C=G?$?G.name.replace("$1",V):G==null?void 0:G.name:V,B=G&&G.toBlock&&k.blocks.find(b=>b.className===G.toBlock)||k.blocks[0];let K=B.dataOutputs.find(b=>b.name===C);K||(K=this._createNewSocketConnection(C,!0),B.dataOutputs.push(K)),q.connectedPointIds.push(K.uniqueId),K.connectedPointIds.push(q.uniqueId)}else throw w.Error(["Invalid value for value connection",y]),new Error("Error parsing node connections")}if(F.flowGraphMapping.interBlockConnectors)for(const d of F.flowGraphMapping.interBlockConnectors){const y=d.input,_=d.output,x=d.isVariable;this._connectFlowGraphNodes(y,_,T.blocks[d.inputBlockIndex],T.blocks[d.outputBlockIndex],x)}if(F.flowGraphMapping.extraProcessor){const d=(et=this._interactivityGraph.declarations)==null?void 0:et[I.declaration];if(!d)throw w.Error(["No declaration found for extra processor",I]),new Error("Error parsing node connections");T.blocks=F.flowGraphMapping.extraProcessor(I,d,F.flowGraphMapping,this,T.blocks,t,this._gltf)}}}_createNewSocketConnection(t,e){return{uniqueId:D(),name:t,_connectionType:e?1:0,connectedPointIds:[]}}_connectFlowGraphNodes(t,e,o,n,s){const i=s?o.dataInputs:o.signalInputs,c=s?n.dataOutputs:n.signalOutputs,l=i.find(m=>m.name===t)||this._createNewSocketConnection(t),u=c.find(m=>m.name===e)||this._createNewSocketConnection(e,!0);i.find(m=>m.name===t)||i.push(l),c.find(m=>m.name===e)||c.push(u),l.connectedPointIds.push(u.uniqueId),u.connectedPointIds.push(l.uniqueId)}getVariableName(t){return"staticVariable_"+t}serializeToFlowGraph(){const t={uniqueId:D(),_userVariables:{},_connectionValues:{}};this._parseNodeConnections(t);for(let o=0;oo.concat(n.blocks),[]),executionContexts:[t]}}}const W="KHR_interactivity";class ft{constructor(t){this._loader=t,this.name=W,this.enabled=this._loader.isExtensionUsed(W),this._pathConverter=Dt(this._loader.gltf),t._skipStartAnimationStep=!0;const e=t.babylonScene;e&&yt(e)}dispose(){this._loader=null,delete this._pathConverter}async onReady(){var s;if(!this._loader.babylonScene||!this._pathConverter)return;const t=this._loader.babylonScene,e=(s=this._loader.gltf.extensions)==null?void 0:s.KHR_interactivity;if(!e)return;const o=new E({scene:t});o.dispatchEventsSynchronously=!1;const n=e.graphs.map(i=>new ae(i,this._loader.gltf,this._loader.parent.targetFps).serializeToFlowGraph());await Promise.all(n.map(async i=>await jt(i,{coordinator:o,pathConverter:this._pathConverter}))),o.start()}}function yt(a){O("/extensions/KHR_interactivity/?/activeCamera/rotation",{get:()=>{if(!a.activeCamera)return new X(NaN,NaN,NaN,NaN);const t=X.FromRotationMatrix(a.activeCamera.getWorldMatrix()).normalize();return a.useRightHandedSystem||(t.w*=-1,t.x*=-1),t},type:"Quaternion",getTarget:()=>a.activeCamera}),O("/extensions/KHR_interactivity/?/activeCamera/position",{get:()=>{if(!a.activeCamera)return new ut(NaN,NaN,NaN);const t=a.activeCamera.getWorldMatrix().getTranslation();return a.useRightHandedSystem||(t.x*=-1),t},type:"Vector3",getTarget:()=>a.activeCamera}),O("/animations/{}/extensions/KHR_interactivity/isPlaying",{get:t=>{var e;return((e=t._babylonAnimationGroup)==null?void 0:e.isPlaying)??!1},type:"boolean",getTarget:t=>t._babylonAnimationGroup}),O("/animations/{}/extensions/KHR_interactivity/minTime",{get:t=>{var e;return(((e=t._babylonAnimationGroup)==null?void 0:e.from)??0)/60},type:"number",getTarget:t=>t._babylonAnimationGroup}),O("/animations/{}/extensions/KHR_interactivity/maxTime",{get:t=>{var e;return(((e=t._babylonAnimationGroup)==null?void 0:e.to)??0)/60},type:"number",getTarget:t=>t._babylonAnimationGroup}),O("/animations/{}/extensions/KHR_interactivity/playhead",{get:t=>{var e;return(((e=t._babylonAnimationGroup)==null?void 0:e.getCurrentFrame())??0)/60},type:"number",getTarget:t=>t._babylonAnimationGroup}),O("/animations/{}/extensions/KHR_interactivity/virtualPlayhead",{get:t=>{var e;return(((e=t._babylonAnimationGroup)==null?void 0:e.getCurrentFrame())??0)/60},type:"number",getTarget:t=>t._babylonAnimationGroup})}Qt(W,"FlowGraphGLTFDataProvider",async()=>(await r(async()=>{const{FlowGraphGLTFDataProvider:a}=await import("./flowGraphGLTFDataProvider-By82y__G.js");return{FlowGraphGLTFDataProvider:a}},__vite__mapDeps([84,1,2,3,4,5,23,24,9,21,25,26,13,12,14,15,16,17,18,19,20,22,27,28,8,29,30,31,10]))).FlowGraphGLTFDataProvider);Ot(W);xt(W,!0,a=>new ft(a));const ke=Object.freeze(Object.defineProperty({__proto__:null,KHR_interactivity:ft,_AddInteractivityObjectModel:yt},Symbol.toStringTag,{value:"Module"}));export{$t as F,Mt as G,ke as K,dt as _,Zt as a,Jt as b,Xt as c,E as d,de as e,fe as f,Wt as g,ye as h,me as i,ge as j,J as k,tt as l}; diff --git a/trackio/frontend/dist/assets/KHR_lights_punctual-CCzRzwty.js b/trackio/frontend/dist/assets/KHR_lights_punctual-CCzRzwty.js new file mode 100644 index 0000000000000000000000000000000000000000..3d2b3fab3910ee79b8e133789f13246f2c9daa30 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_lights_punctual-CCzRzwty.js @@ -0,0 +1 @@ +import{M as x,V as h}from"./math.vector-C1xyPDEl.js";import{Color3 as b}from"./math.color-CzLaKyb6.js";import{_ as d,s as u}from"./bufferUtils-DTRdUD8L.js";import{N as T}from"./node-C-Iu0ALa.js";import{L as S}from"./light-DT53MGnW.js";import{b as E,c as v}from"./objectModelMapping-DWcnlpHl.js";import{R as Z}from"./math.scalar.functions-DkeaWAWz.js";import{A as D,G as M}from"./glTFLoader-BPeiodqB.js";import{u as C,r as R}from"./dynamic-BUKysMyT.js";import"./decorators.serialization-C7d7ykGG.js";import"./lightConstants-B1TSTh-k.js";import"./tools-D01N7ptR.js";import"./math.axis-Ck7JfPPj.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./math.plane-Ce_75-g5.js";import"./index-Bx1QoKVU.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./material-CrEsrP1R.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";T.AddNodeConstructor("Light_Type_1",(m,t)=>()=>new a(m,h.Zero(),t));class a extends E{get shadowFrustumSize(){return this._shadowFrustumSize}set shadowFrustumSize(t){this._shadowFrustumSize=t,this.forceProjectionMatrixCompute()}get shadowOrthoScale(){return this._shadowOrthoScale}set shadowOrthoScale(t){this._shadowOrthoScale=t,this.forceProjectionMatrixCompute()}get orthoLeft(){return this._orthoLeft}set orthoLeft(t){this._orthoLeft=t}get orthoRight(){return this._orthoRight}set orthoRight(t){this._orthoRight=t}get orthoTop(){return this._orthoTop}set orthoTop(t){this._orthoTop=t}get orthoBottom(){return this._orthoBottom}set orthoBottom(t){this._orthoBottom=t}constructor(t,o,e,i){super(t,e,i),this._shadowFrustumSize=0,this._shadowOrthoScale=.1,this.autoUpdateExtends=!0,this.autoCalcShadowZBounds=!1,this._orthoLeft=Number.MAX_VALUE,this._orthoRight=Number.MIN_VALUE,this._orthoTop=Number.MIN_VALUE,this._orthoBottom=Number.MAX_VALUE,this.position=o.scale(-1),this.direction=o}getClassName(){return"DirectionalLight"}getTypeID(){return S.LIGHTTYPEID_DIRECTIONALLIGHT}_setDefaultShadowProjectionMatrix(t,o,e){this.shadowFrustumSize>0?this._setDefaultFixedFrustumShadowProjectionMatrix(t):this._setDefaultAutoExtendShadowProjectionMatrix(t,o,e)}_setDefaultFixedFrustumShadowProjectionMatrix(t){const o=this.getScene().activeCamera;x.OrthoLHToRef(this.shadowFrustumSize,this.shadowFrustumSize,this.shadowMinZ!==void 0?this.shadowMinZ:o?o.minZ:0,this.shadowMaxZ!==void 0?this.shadowMaxZ:o?o.maxZ:1e4,t,this.getScene().getEngine().isNDCHalfZRange)}_setDefaultAutoExtendShadowProjectionMatrix(t,o,e){const i=this.getScene().activeCamera;if(this.autoUpdateExtends||this._orthoLeft===Number.MAX_VALUE){const r=h.Zero();this._orthoLeft=Number.MAX_VALUE,this._orthoRight=-Number.MAX_VALUE,this._orthoTop=-Number.MAX_VALUE,this._orthoBottom=Number.MAX_VALUE;let g=Number.MAX_VALUE,L=-Number.MAX_VALUE;for(let w=0;wthis._orthoRight&&(this._orthoRight=r.x),r.y>this._orthoTop&&(this._orthoTop=r.y),this.autoCalcShadowZBounds&&(r.zL&&(L=r.z))}this.autoCalcShadowZBounds&&(this._shadowMinZ=g,this._shadowMaxZ=L)}const c=this._orthoRight-this._orthoLeft,f=this._orthoTop-this._orthoBottom,s=this.shadowMinZ!==void 0?this.shadowMinZ:(i==null?void 0:i.minZ)||0,n=this.shadowMaxZ!==void 0?this.shadowMaxZ:(i==null?void 0:i.maxZ)||1e4,l=this.getScene().getEngine().useReverseDepthBuffer;x.OrthoOffCenterLHToRef(this._orthoLeft-c*this.shadowOrthoScale,this._orthoRight+c*this.shadowOrthoScale,this._orthoBottom-f*this.shadowOrthoScale,this._orthoTop+f*this.shadowOrthoScale,l?n:s,l?s:n,t,this.getScene().getEngine().isNDCHalfZRange)}_buildUniformLayout(){this._uniformBuffer.addUniform("vLightData",4),this._uniformBuffer.addUniform("vLightDiffuse",4),this._uniformBuffer.addUniform("vLightSpecular",4),this._uniformBuffer.addUniform("shadowsInfo",3),this._uniformBuffer.addUniform("depthValues",2),this._uniformBuffer.create()}transferToEffect(t,o){return this.computeTransformedInformation()?(this._uniformBuffer.updateFloat4("vLightData",this.transformedDirection.x,this.transformedDirection.y,this.transformedDirection.z,1,o),this):(this._uniformBuffer.updateFloat4("vLightData",this.direction.x,this.direction.y,this.direction.z,1,o),this)}transferToNodeMaterialEffect(t,o){return this.computeTransformedInformation()?(t.setFloat3(o,this.transformedDirection.x,this.transformedDirection.y,this.transformedDirection.z),this):(t.setFloat3(o,this.direction.x,this.direction.y,this.direction.z),this)}getDepthMinZ(t){const o=this._scene.getEngine();return!o.useReverseDepthBuffer&&o.isNDCHalfZRange?0:1}getDepthMaxZ(t){const o=this._scene.getEngine();return o.useReverseDepthBuffer&&o.isNDCHalfZRange?0:1}prepareLightSpecificDefines(t,o){t["DIRLIGHT"+o]=!0}}d([u()],a.prototype,"shadowFrustumSize",null);d([u()],a.prototype,"shadowOrthoScale",null);d([u()],a.prototype,"autoUpdateExtends",void 0);d([u()],a.prototype,"autoCalcShadowZBounds",void 0);d([u("orthoLeft")],a.prototype,"_orthoLeft",void 0);d([u("orthoRight")],a.prototype,"_orthoRight",void 0);d([u("orthoTop")],a.prototype,"_orthoTop",void 0);d([u("orthoBottom")],a.prototype,"_orthoBottom",void 0);Z("BABYLON.DirectionalLight",a);T.AddNodeConstructor("Light_Type_0",(m,t)=>()=>new _(m,h.Zero(),t));class _ extends E{get shadowAngle(){return this._shadowAngle}set shadowAngle(t){this._shadowAngle=t,this.forceProjectionMatrixCompute()}get direction(){return this._direction}set direction(t){const o=this.needCube();if(this._direction=t,this.needCube()!==o&&this._shadowGenerators){const e=this._shadowGenerators.values();for(let i=e.next();i.done!==!0;i=e.next())i.value.recreateShadowMap()}}constructor(t,o,e,i){super(t,e,i),this._shadowAngle=Math.PI/2,this.position=o}getClassName(){return"PointLight"}getTypeID(){return S.LIGHTTYPEID_POINTLIGHT}needCube(){return!this.direction}getShadowDirection(t){if(this.direction)return super.getShadowDirection(t);switch(t){case 0:return new h(1,0,0);case 1:return new h(-1,0,0);case 2:return new h(0,-1,0);case 3:return new h(0,1,0);case 4:return new h(0,0,1);case 5:return new h(0,0,-1)}return h.Zero()}_setDefaultShadowProjectionMatrix(t,o,e){const i=this.getScene().activeCamera,c=this.getDepthMinZ(i),f=this.getDepthMaxZ(i),s=this.getScene().getEngine().useReverseDepthBuffer;x.PerspectiveFovLHToRef(this.shadowAngle,1,s?f:c,s?c:f,t,!0,this._scene.getEngine().isNDCHalfZRange,void 0,s)}_buildUniformLayout(){this._uniformBuffer.addUniform("vLightData",4),this._uniformBuffer.addUniform("vLightDiffuse",4),this._uniformBuffer.addUniform("vLightSpecular",4),this._uniformBuffer.addUniform("vLightFalloff",4),this._uniformBuffer.addUniform("shadowsInfo",3),this._uniformBuffer.addUniform("depthValues",2),this._uniformBuffer.create()}transferToEffect(t,o){const e=this._scene.floatingOriginOffset;return this.computeTransformedInformation()?this._uniformBuffer.updateFloat4("vLightData",this.transformedPosition.x-e.x,this.transformedPosition.y-e.y,this.transformedPosition.z-e.z,0,o):this._uniformBuffer.updateFloat4("vLightData",this.position.x-e.x,this.position.y-e.y,this.position.z-e.z,0,o),this._uniformBuffer.updateFloat4("vLightFalloff",this.range,this._inverseSquaredRange,0,0,o),this}transferToNodeMaterialEffect(t,o){const e=this._scene.floatingOriginOffset;return this.computeTransformedInformation()?t.setFloat3(o,this.transformedPosition.x-e.x,this.transformedPosition.y-e.y,this.transformedPosition.z-e.z):t.setFloat3(o,this.position.x-e.x,this.position.y-e.y,this.position.z-e.z),this}prepareLightSpecificDefines(t,o){t["POINTLIGHT"+o]=!0}}d([u()],_.prototype,"shadowAngle",null);Z("BABYLON.PointLight",_);const p="KHR_lights_punctual";class N{constructor(t){this.name=p,this._loader=t,this.enabled=this._loader.isExtensionUsed(p)}dispose(){this._loader=null,delete this._lights}onLoading(){const t=this._loader.gltf.extensions;if(t&&t[this.name]){const o=t[this.name];this._lights=o.lights,D.Assign(this._lights)}}loadNodeAsync(t,o,e){return M.LoadExtensionAsync(t,o,this.name,async(i,c)=>(this._loader._allMaterialsDirtyRequired=!0,await this._loader.loadNodeAsync(t,o,f=>{let s;const n=D.Get(i,this._lights,c.light),l=n.name||f.name;switch(this._loader.babylonScene._blockEntityCollection=!!this._loader._assetContainer,n.type){case"directional":{const r=new a(l,h.Backward(),this._loader.babylonScene);r.position.setAll(0),s=r;break}case"point":{s=new _(l,h.Zero(),this._loader.babylonScene);break}case"spot":{const r=new v(l,h.Zero(),h.Backward(),0,1,this._loader.babylonScene);r.angle=(n.spot&&n.spot.outerConeAngle||Math.PI/4)*2,r.innerAngle=(n.spot&&n.spot.innerConeAngle||0)*2,s=r;break}default:throw this._loader.babylonScene._blockEntityCollection=!1,new Error(`${i}: Invalid light type (${n.type})`)}s._parentContainer=this._loader._assetContainer,this._loader.babylonScene._blockEntityCollection=!1,n._babylonLight=s,s.falloffType=S.FALLOFF_GLTF,s.diffuse=n.color?b.FromArray(n.color):b.White(),s.intensity=n.intensity==null?1:n.intensity,s.range=n.range==null?Number.MAX_VALUE:n.range,s.parent=f,this._loader._babylonLights.push(s),M.AddPointerMetadata(s,i),e(f)})))}}C(p);R(p,!0,m=>new N(m));export{N as KHR_lights}; diff --git a/trackio/frontend/dist/assets/KHR_materials_anisotropy-DeuhwPRx.js b/trackio/frontend/dist/assets/KHR_materials_anisotropy-DeuhwPRx.js new file mode 100644 index 0000000000000000000000000000000000000000..70e24d6b7c99984102d0db2260bd6769094e0e0d --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_anisotropy-DeuhwPRx.js @@ -0,0 +1 @@ +import{G as l}from"./glTFLoader-BPeiodqB.js";import{u as d,r as c}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const n="KHR_materials_anisotropy";class u{constructor(t){this.name=n,this.order=195,this._loader=t,this.enabled=this._loader.isExtensionUsed(n)}dispose(){this._loader=null}loadMaterialPropertiesAsync(t,o,i){return l.LoadExtensionAsync(t,o,this.name,async(r,e)=>{const s=new Array;s.push(this._loader.loadMaterialPropertiesAsync(t,o,i)),s.push(this._loadAnisotropyPropertiesAsync(r,e,i)),await Promise.all(s)})}async _loadAnisotropyPropertiesAsync(t,o,i){const r=this._loader._getOrCreateMaterialAdapter(i),e=new Array,s=o.anisotropyStrength??0,y=o.anisotropyRotation??0;r.specularRoughnessAnisotropy=s,r.geometryTangentAngle=y;const a=o.extensions??{};(!a.EXT_materials_anisotropy_openpbr||!a.EXT_materials_anisotropy_openpbr.openPbrAnisotropyEnabled)&&r.configureGltfStyleAnisotropy(!0),o.anisotropyTexture&&(o.anisotropyTexture.nonColorData=!0,e.push(this._loader.loadTextureInfoAsync(`${t}/anisotropyTexture`,o.anisotropyTexture,m=>{m.name=`${i.name} (Anisotropy Intensity)`,r.geometryTangentTexture=m}))),await Promise.all(e)}}d(n);c(n,!0,p=>new u(p));export{u as KHR_materials_anisotropy}; diff --git a/trackio/frontend/dist/assets/KHR_materials_clearcoat-CqejipXp.js b/trackio/frontend/dist/assets/KHR_materials_clearcoat-CqejipXp.js new file mode 100644 index 0000000000000000000000000000000000000000..e60ee526220b8ca9816bd837489cee16a77eedf6 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_clearcoat-CqejipXp.js @@ -0,0 +1 @@ +import{G as n}from"./glTFLoader-BPeiodqB.js";import{u as l,r as u}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const m="KHR_materials_clearcoat";class d{constructor(a){this.name=m,this.order=190,this._loader=a,this.enabled=this._loader.isExtensionUsed(m)}dispose(){this._loader=null}loadMaterialPropertiesAsync(a,o,r){return n.LoadExtensionAsync(a,o,this.name,async(t,s)=>{const e=new Array;e.push(this._loader.loadMaterialPropertiesAsync(a,o,r)),e.push(this._loadClearCoatPropertiesAsync(t,s,r)),await Promise.all(e)})}_loadClearCoatPropertiesAsync(a,o,r){const t=this._loader._getOrCreateMaterialAdapter(r),s=new Array;return t.configureCoat(),t.coatWeight=o.clearcoatFactor!==void 0?o.clearcoatFactor:0,t.coatRoughness=o.clearcoatRoughnessFactor!==void 0?o.clearcoatRoughnessFactor:0,o.clearcoatTexture&&s.push(this._loader.loadTextureInfoAsync(`${a}/clearcoatTexture`,o.clearcoatTexture,e=>{e.name=`${r.name} (ClearCoat)`,t.coatWeightTexture=e})),o.clearcoatRoughnessTexture&&(o.clearcoatRoughnessTexture.nonColorData=!0,s.push(this._loader.loadTextureInfoAsync(`${a}/clearcoatRoughnessTexture`,o.clearcoatRoughnessTexture,e=>{e.name=`${r.name} (ClearCoat Roughness)`,t.coatRoughnessTexture=e}))),o.clearcoatNormalTexture&&(o.clearcoatNormalTexture.nonColorData=!0,s.push(this._loader.loadTextureInfoAsync(`${a}/clearcoatNormalTexture`,o.clearcoatNormalTexture,e=>{var c;e.name=`${r.name} (ClearCoat Normal)`,t.geometryCoatNormalTexture=e,((c=o.clearcoatNormalTexture)==null?void 0:c.scale)!=null&&(t.geometryCoatNormalTextureScale=o.clearcoatNormalTexture.scale)})),t.setNormalMapInversions(!r.getScene().useRightHandedSystem,r.getScene().useRightHandedSystem)),Promise.all(s).then(()=>{})}}l(m);u(m,!0,i=>new d(i));export{d as KHR_materials_clearcoat}; diff --git a/trackio/frontend/dist/assets/KHR_materials_coat-DOoUAE9E.js b/trackio/frontend/dist/assets/KHR_materials_coat-DOoUAE9E.js new file mode 100644 index 0000000000000000000000000000000000000000..19adc8f482bf5986ac94139910e0669ade944bd5 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_coat-DOoUAE9E.js @@ -0,0 +1 @@ +import{G as d}from"./glTFLoader-BPeiodqB.js";import{u as h,r as T}from"./dynamic-BUKysMyT.js";import{Color3 as g}from"./math.color-CzLaKyb6.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const i="KHR_materials_coat";class f{constructor(r){this.name=i,this.order=191,this.useOpenPBR=!1,this._loader=r,this.enabled=this._loader.isExtensionUsed(i)}dispose(){this._loader=null}loadMaterialPropertiesAsync(r,o,a){return d.LoadExtensionAsync(r,o,this.name,async(t,n)=>{const s=new Array;s.push(this._loader.loadMaterialPropertiesAsync(r,o,a)),o.extensions&&o.extensions.KHR_materials_openpbr&&(this.useOpenPBR=!0),s.push(this._loadCoatPropertiesAsync(t,n,a)),await Promise.all(s)})}_loadCoatPropertiesAsync(r,o,a){const t=this._loader._getOrCreateMaterialAdapter(a),n=new Array;t.configureCoat(),t.coatWeight=o.coatFactor!==void 0?o.coatFactor:0,t.coatRoughness=o.coatRoughnessFactor!==void 0?o.coatRoughnessFactor:0,o.coatTexture&&n.push(this._loader.loadTextureInfoAsync(`${r}/coatTexture`,o.coatTexture,e=>{e.name=`${a.name} (Coat)`,t.coatWeightTexture=e})),o.coatRoughnessTexture&&(o.coatRoughnessTexture.nonColorData=!0,n.push(this._loader.loadTextureInfoAsync(`${r}/coatRoughnessTexture`,o.coatRoughnessTexture,e=>{e.name=`${a.name} (Coat Roughness)`,t.coatRoughnessTexture=e}))),o.coatNormalTexture&&(o.coatNormalTexture.nonColorData=!0,n.push(this._loader.loadTextureInfoAsync(`${r}/coatNormalTexture`,o.coatNormalTexture,e=>{var m;e.name=`${a.name} (Coat Normal)`,t.geometryCoatNormalTexture=e,((m=o.coatNormalTexture)==null?void 0:m.scale)!=null&&(t.geometryCoatNormalTextureScale=o.coatNormalTexture.scale)})),t.setNormalMapInversions(!a.getScene().useRightHandedSystem,a.getScene().useRightHandedSystem)),t.coatDarkening=o.coatDarkeningFactor!==void 0?o.coatDarkeningFactor:1,t.coatIor=o.coatIor!==void 0?o.coatIor:1.5;const s=g.White();o.coatColorFactor!==void 0&&s.fromArray(o.coatColorFactor),t.coatColor=s,o.coatColorTexture&&n.push(this._loader.loadTextureInfoAsync(`${r}/coatColorTexture`,o.coatColorTexture,e=>{e.name=`${a.name} (Coat Color)`,t.coatColorTexture=e}));const u=o.coatAnisotropyStrength??0,l=o.coatAnisotropyRotation??0;return t.coatRoughnessAnisotropy=u,t.geometryCoatTangentAngle=l,this.useOpenPBR||t.configureGltfStyleAnisotropy(!0),o.coatAnisotropyTexture&&(o.coatAnisotropyTexture.nonColorData=!0,n.push(this._loader.loadTextureInfoAsync(`${r}/coatAnisotropyTexture`,o.coatAnisotropyTexture,e=>{e.name=`${a.name} (Coat Anisotropy)`,t.geometryCoatTangentTexture=e}))),Promise.all(n).then(()=>{})}}h(i);T(i,!0,c=>new f(c));export{f as KHR_materials_coat}; diff --git a/trackio/frontend/dist/assets/KHR_materials_diffuse_roughness-3tU76Z0C.js b/trackio/frontend/dist/assets/KHR_materials_diffuse_roughness-3tU76Z0C.js new file mode 100644 index 0000000000000000000000000000000000000000..c7cd9876294acfec6d82a03dbbc36d85f3dc740d --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_diffuse_roughness-3tU76Z0C.js @@ -0,0 +1 @@ +import{G as n}from"./glTFLoader-BPeiodqB.js";import{u as a,r as u}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const m="KHR_materials_diffuse_roughness";class h{constructor(r){this.name=m,this.order=190,this._loader=r,this.enabled=this._loader.isExtensionUsed(m)}dispose(){this._loader=null}loadMaterialPropertiesAsync(r,s,t){return n.LoadExtensionAsync(r,s,this.name,async(i,e)=>{const o=new Array;return o.push(this._loader.loadMaterialPropertiesAsync(r,s,t)),o.push(this._loadDiffuseRoughnessPropertiesAsync(i,e,t)),await Promise.all(o).then(()=>{})})}_loadDiffuseRoughnessPropertiesAsync(r,s,t){const i=this._loader._getOrCreateMaterialAdapter(t),e=new Array;return i.baseDiffuseRoughness=s.diffuseRoughnessFactor??0,s.diffuseRoughnessTexture&&e.push(this._loader.loadTextureInfoAsync(`${r}/diffuseRoughnessTexture`,s.diffuseRoughnessTexture,o=>{o.name=`${t.name} (Diffuse Roughness)`,i.baseDiffuseRoughnessTexture=o})),Promise.all(e).then(()=>{})}}a(m);u(m,!0,p=>new h(p));export{h as KHR_materials_diffuse_roughness}; diff --git a/trackio/frontend/dist/assets/KHR_materials_diffuse_transmission-CfRoCwmd.js b/trackio/frontend/dist/assets/KHR_materials_diffuse_transmission-CfRoCwmd.js new file mode 100644 index 0000000000000000000000000000000000000000..46f25ec4b4a2feec04113f0ae412f10e4038a5da --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_diffuse_transmission-CfRoCwmd.js @@ -0,0 +1 @@ +import{G as u}from"./glTFLoader-BPeiodqB.js";import{Color3 as p}from"./math.color-CzLaKyb6.js";import{u as f,r as d}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const n="KHR_materials_diffuse_transmission";class T{constructor(s){this.name=n,this.order=174,this._loader=s,this.enabled=this._loader.isExtensionUsed(n),this.enabled&&(s.parent.transparencyAsCoverage=!0)}dispose(){this._loader=null}loadMaterialPropertiesAsync(s,m,t){return u.LoadExtensionAsync(s,m,this.name,async(r,i)=>{const o=new Array;return o.push(this._loader.loadMaterialPropertiesAsync(s,m,t)),o.push(this._loadTranslucentPropertiesAsync(r,m,t,i)),await Promise.all(o).then(()=>{})})}_loadTranslucentPropertiesAsync(s,m,t,r){const i=this._loader._getOrCreateMaterialAdapter(t);i.configureSubsurface(),i.subsurfaceWeight=r.diffuseTransmissionFactor??0,i.subsurfaceConstantTint=r.diffuseTransmissionColorFactor!==void 0?p.FromArray(r.diffuseTransmissionColorFactor):p.White();const o=new Array;return r.diffuseTransmissionTexture&&(r.diffuseTransmissionTexture.nonColorData=!0,o.push(this._loader.loadTextureInfoAsync(`${s}/diffuseTransmissionTexture`,r.diffuseTransmissionTexture).then(e=>{e.name=`${t.name} (Diffuse Transmission)`,i.subsurfaceWeightTexture=e}))),r.diffuseTransmissionColorTexture&&o.push(this._loader.loadTextureInfoAsync(`${s}/diffuseTransmissionColorTexture`,r.diffuseTransmissionColorTexture).then(e=>{e.name=`${t.name} (Diffuse Transmission Color)`,i.subsurfaceConstantTintTexture=e})),Promise.all(o).then(()=>{})}}f(n);d(n,!0,a=>new T(a));export{T as KHR_materials_diffuse_transmission}; diff --git a/trackio/frontend/dist/assets/KHR_materials_dispersion-fuNIvVrP.js b/trackio/frontend/dist/assets/KHR_materials_dispersion-fuNIvVrP.js new file mode 100644 index 0000000000000000000000000000000000000000..7db6dd410b15431fdd812896eec250e0bae234d5 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_dispersion-fuNIvVrP.js @@ -0,0 +1 @@ +import{G as n}from"./glTFLoader-BPeiodqB.js";import{u as a,r as d}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const e="KHR_materials_dispersion";class l{constructor(r){this.name=e,this.order=174,this._loader=r,this.enabled=this._loader.isExtensionUsed(e)}dispose(){this._loader=null}loadMaterialPropertiesAsync(r,o,t){return n.LoadExtensionAsync(r,o,this.name,async(s,i)=>{const p=new Array;return p.push(this._loader.loadMaterialPropertiesAsync(r,o,t)),p.push(this._loadDispersionPropertiesAsync(s,o,t,i)),await Promise.all(p).then(()=>{})})}_loadDispersionPropertiesAsync(r,o,t,s){const i=this._loader._getOrCreateMaterialAdapter(t);return i.transmissionWeight==0||!s.dispersion||(i.transmissionDispersionAbbeNumber=20,i.transmissionDispersionScale=s.dispersion),Promise.resolve()}}a(e);d(e,!0,m=>new l(m));export{l as KHR_materials_dispersion}; diff --git a/trackio/frontend/dist/assets/KHR_materials_emissive_strength-rOKORGq9.js b/trackio/frontend/dist/assets/KHR_materials_emissive_strength-rOKORGq9.js new file mode 100644 index 0000000000000000000000000000000000000000..8791dbf6f8cfa49feab609a2a7e5dc925bda8b18 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_emissive_strength-rOKORGq9.js @@ -0,0 +1 @@ +import{G as p}from"./glTFLoader-BPeiodqB.js";import{u as a,r as n}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const o="KHR_materials_emissive_strength";class d{constructor(i){this.name=o,this.order=170,this._loader=i,this.enabled=this._loader.isExtensionUsed(o)}dispose(){this._loader=null}loadMaterialPropertiesAsync(i,t,r){return p.LoadExtensionAsync(i,t,this.name,async(e,s)=>(await this._loader.loadMaterialPropertiesAsync(i,t,r),this._loadEmissiveProperties(e,s,r),await Promise.resolve()))}_loadEmissiveProperties(i,t,r){if(t.emissiveStrength!==void 0){const e=this._loader._getOrCreateMaterialAdapter(r);e.emissionLuminance=t.emissiveStrength}}}a(o);n(o,!0,m=>new d(m));export{d as KHR_materials_emissive_strength}; diff --git a/trackio/frontend/dist/assets/KHR_materials_fuzz-Clhu1IVg.js b/trackio/frontend/dist/assets/KHR_materials_fuzz-Clhu1IVg.js new file mode 100644 index 0000000000000000000000000000000000000000..e4308e4ff28ea8c3c7ea51f835bd01662ff09698 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_fuzz-Clhu1IVg.js @@ -0,0 +1 @@ +import{G as n}from"./glTFLoader-BPeiodqB.js";import{Color3 as m}from"./math.color-CzLaKyb6.js";import{u as a,r as f}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const s="KHR_materials_fuzz";class h{constructor(e){this.name=s,this.order=190,this._loader=e,this.enabled=this._loader.isExtensionUsed(s)}dispose(){this._loader=null}loadMaterialPropertiesAsync(e,o,u){return n.LoadExtensionAsync(e,o,this.name,async(t,i)=>{const r=new Array;return r.push(this._loader.loadMaterialPropertiesAsync(e,o,u)),r.push(this._loadFuzzPropertiesAsync(t,i,u)),await Promise.all(r).then(()=>{})})}_loadFuzzPropertiesAsync(e,o,u){const t=this._loader._getOrCreateMaterialAdapter(u),i=new Array;return t.configureFuzz(),t.fuzzWeight=o.fuzzFactor!==void 0?o.fuzzFactor:0,t.fuzzColor=o.fuzzColorFactor!==void 0?m.FromArray(o.fuzzColorFactor):m.White(),t.fuzzRoughness=o.fuzzRoughnessFactor!==void 0?o.fuzzRoughnessFactor:.5,o.fuzzTexture&&i.push(this._loader.loadTextureInfoAsync(`${e}/fuzzTexture`,o.fuzzTexture,r=>{r.name=`${u.name} (Fuzz)`,t.fuzzWeightTexture=r})),o.fuzzColorTexture&&i.push(this._loader.loadTextureInfoAsync(`${e}/fuzzColorTexture`,o.fuzzColorTexture,r=>{r.name=`${u.name} (Fuzz Color)`,t.fuzzColorTexture=r})),o.fuzzRoughnessTexture&&(o.fuzzRoughnessTexture.nonColorData=!0,i.push(this._loader.loadTextureInfoAsync(`${e}/fuzzRoughnessTexture`,o.fuzzRoughnessTexture,r=>{r.name=`${u.name} (Fuzz Roughness)`,t.fuzzRoughnessTexture=r}))),Promise.all(i).then(()=>{})}}a(s);f(s,!0,z=>new h(z));export{h as KHR_materials_fuzz}; diff --git a/trackio/frontend/dist/assets/KHR_materials_ior-B1oqd8B6.js b/trackio/frontend/dist/assets/KHR_materials_ior-B1oqd8B6.js new file mode 100644 index 0000000000000000000000000000000000000000..139eedbae88886df929ae45ffa4cd4b288656205 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_ior-B1oqd8B6.js @@ -0,0 +1 @@ +import{G as a}from"./glTFLoader-BPeiodqB.js";import{u as d,r as l}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const i="KHR_materials_ior";class e{constructor(r){this.name=i,this.order=180,this._loader=r,this.enabled=this._loader.isExtensionUsed(i)}dispose(){this._loader=null}loadMaterialPropertiesAsync(r,o,t){return a.LoadExtensionAsync(r,o,this.name,async(p,m)=>{const s=new Array;return s.push(this._loader.loadMaterialPropertiesAsync(r,o,t)),s.push(this._loadIorPropertiesAsync(p,m,t)),await Promise.all(s).then(()=>{})})}_loadIorPropertiesAsync(r,o,t){const p=this._loader._getOrCreateMaterialAdapter(t),m=o.ior!==void 0?o.ior:e._DEFAULT_IOR;return p.specularIor=m,Promise.resolve()}}e._DEFAULT_IOR=1.5;d(i);l(i,!0,n=>new e(n));export{e as KHR_materials_ior}; diff --git a/trackio/frontend/dist/assets/KHR_materials_iridescence-acCc2TH6.js b/trackio/frontend/dist/assets/KHR_materials_iridescence-acCc2TH6.js new file mode 100644 index 0000000000000000000000000000000000000000..7bc5a1cfa281554e711d112c5320f11994b7d805 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_iridescence-acCc2TH6.js @@ -0,0 +1 @@ +import{G as c}from"./glTFLoader-BPeiodqB.js";import{u as a,r as p}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const n="KHR_materials_iridescence";class d{constructor(t){this.name=n,this.order=195,this._loader=t,this.enabled=this._loader.isExtensionUsed(n)}dispose(){this._loader=null}loadMaterialPropertiesAsync(t,i,s){return c.LoadExtensionAsync(t,i,this.name,async(r,o)=>{const e=new Array;return e.push(this._loader.loadMaterialPropertiesAsync(t,i,s)),e.push(this._loadIridescencePropertiesAsync(r,o,s)),await Promise.all(e).then(()=>{})})}_loadIridescencePropertiesAsync(t,i,s){const r=this._loader._getOrCreateMaterialAdapter(s),o=new Array;return r.thinFilmWeight=i.iridescenceFactor??0,r.thinFilmIor=i.iridescenceIor??i.iridescenceIOR??1.3,r.thinFilmThicknessMinimum=i.iridescenceThicknessMinimum??100,r.thinFilmThicknessMaximum=i.iridescenceThicknessMaximum??400,i.iridescenceTexture&&o.push(this._loader.loadTextureInfoAsync(`${t}/iridescenceTexture`,i.iridescenceTexture,e=>{e.name=`${s.name} (Iridescence)`,r.thinFilmWeightTexture=e})),i.iridescenceThicknessTexture&&o.push(this._loader.loadTextureInfoAsync(`${t}/iridescenceThicknessTexture`,i.iridescenceThicknessTexture,e=>{e.name=`${s.name} (Iridescence Thickness)`,r.thinFilmThicknessTexture=e})),Promise.all(o).then(()=>{})}}a(n);p(n,!0,m=>new d(m));export{d as KHR_materials_iridescence}; diff --git a/trackio/frontend/dist/assets/KHR_materials_pbrSpecularGlossiness-C3yScQoz.js b/trackio/frontend/dist/assets/KHR_materials_pbrSpecularGlossiness-C3yScQoz.js new file mode 100644 index 0000000000000000000000000000000000000000..b028d2e50081bf69147957f1d1119bfa725fb362 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_pbrSpecularGlossiness-C3yScQoz.js @@ -0,0 +1 @@ +import{Color3 as e}from"./math.color-CzLaKyb6.js";import{PBRMaterial as l}from"./pbrMaterial-BY72QmxQ.js";import{G as n}from"./glTFLoader-BPeiodqB.js";import{u as c,r as a}from"./dynamic-BUKysMyT.js";import"./math.scalar.functions-DkeaWAWz.js";import"./bufferUtils-DTRdUD8L.js";import"./brdfTextureTools-BcdKBp2Y.js";import"./texture-BEqhDl5Y.js";import"./math.vector-C1xyPDEl.js";import"./baseTexture-CgAggsey.js";import"./decorators.serialization-C7d7ykGG.js";import"./tools-D01N7ptR.js";import"./math.plane-Ce_75-g5.js";import"./index-Bx1QoKVU.js";import"./passPostProcess-kvX6RZRb.js";import"./smartArray-CO6iJ0u5.js";import"./effectRenderer-CSz5fi0n.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./math.viewport-CgkTt1RS.js";import"./drawWrapper-CZVk0xN-.js";import"./engine-DaBDhQ20.js";import"./thinEngine-D5ynu8Dd.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./math.axis-Ck7JfPPj.js";import"./math.path-DFucxaHn.js";import"./perfCounter-DusmGz8t.js";import"./textureTools-Dam1Xl1N.js";import"./renderTargetTexture-B0kOC1Jy.js";import"./renderingManager-CiY2OUrf.js";import"./lightConstants-B1TSTh-k.js";import"./scene-gn_hoGMy.js";import"./sceneComponent-Dflhczui.js";import"./deviceInputEvents-BTza-zoR.js";import"./imageProcessing-2WziWti-.js";import"./material.detailMapConfiguration-D1Pl6X8T.js";import"./material-CrEsrP1R.js";import"./subMesh-UWDo5wjI.js";import"./pushMaterial-B8MG1fVH.js";import"./baseTexture.polynomial-BRyqQovE.js";import"./cubemapToSphericalPolynomial-BhsWOHWR.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./node-C-Iu0ALa.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./cameraInputsManager-WrTH9PVf.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const m="KHR_materials_pbrSpecularGlossiness";class d{constructor(s){this.name=m,this.order=200,this._loader=s,this.enabled=this._loader.isExtensionUsed(m)}dispose(){this._loader=null}loadMaterialPropertiesAsync(s,o,r){return n.LoadExtensionAsync(s,o,this.name,async(t,i)=>{const p=new Array;return p.push(this._loader.loadMaterialBasePropertiesAsync(s,o,r)),p.push(this._loadSpecularGlossinessPropertiesAsync(t,i,r)),this._loader.loadMaterialAlphaProperties(s,o,r),await Promise.all(p).then(()=>{})})}_loadSpecularGlossinessPropertiesAsync(s,o,r){if(!(r instanceof l))throw new Error(`${s}: Material type not supported`);const t=new Array;return r.metallic=null,r.roughness=null,o.diffuseFactor?(r.albedoColor=e.FromArray(o.diffuseFactor),r.alpha=o.diffuseFactor[3]):r.albedoColor=e.White(),r.reflectivityColor=o.specularFactor?e.FromArray(o.specularFactor):e.White(),r.microSurface=o.glossinessFactor==null?1:o.glossinessFactor,o.diffuseTexture&&t.push(this._loader.loadTextureInfoAsync(`${s}/diffuseTexture`,o.diffuseTexture,i=>{i.name=`${r.name} (Diffuse)`,r.albedoTexture=i})),o.specularGlossinessTexture&&(t.push(this._loader.loadTextureInfoAsync(`${s}/specularGlossinessTexture`,o.specularGlossinessTexture,i=>{i.name=`${r.name} (Specular Glossiness)`,r.reflectivityTexture=i,r.reflectivityTexture.hasAlpha=!0})),r.useMicroSurfaceFromReflectivityMapAlpha=!0),Promise.all(t).then(()=>{})}}c(m);a(m,!0,u=>new d(u));export{d as KHR_materials_pbrSpecularGlossiness}; diff --git a/trackio/frontend/dist/assets/KHR_materials_sheen-DWUKP0Pn.js b/trackio/frontend/dist/assets/KHR_materials_sheen-DWUKP0Pn.js new file mode 100644 index 0000000000000000000000000000000000000000..f71cd689a5d84a82e8dec9f82806299aa87f80bb --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_sheen-DWUKP0Pn.js @@ -0,0 +1 @@ +import{G as p}from"./glTFLoader-BPeiodqB.js";import{Color3 as a}from"./math.color-CzLaKyb6.js";import{u as l,r as d}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const m="KHR_materials_sheen";class c{constructor(r){this.name=m,this.order=190,this._loader=r,this.enabled=this._loader.isExtensionUsed(m)}dispose(){this._loader=null}loadMaterialPropertiesAsync(r,e,t){return p.LoadExtensionAsync(r,e,this.name,async(o,s)=>{const n=new Array;return n.push(this._loader.loadMaterialPropertiesAsync(r,e,t)),n.push(this._loadSheenPropertiesAsync(o,s,t)),await Promise.all(n).then(()=>{})})}_loadSheenPropertiesAsync(r,e,t){const o=this._loader._getOrCreateMaterialAdapter(t),s=new Array;o.configureFuzz();const n=e.sheenColorFactor!==void 0?a.FromArray(e.sheenColorFactor):a.Black(),u=e.sheenRoughnessFactor!==void 0?e.sheenRoughnessFactor:0;return o.fuzzWeight=1,o.fuzzColor=n,o.fuzzRoughness=u,e.sheenColorTexture&&s.push(this._loader.loadTextureInfoAsync(`${r}/sheenColorTexture`,e.sheenColorTexture,i=>{i.name=`${t.name} (Sheen Color)`,o.fuzzColorTexture=i})),e.sheenRoughnessTexture&&(e.sheenRoughnessTexture.nonColorData=!0,s.push(this._loader.loadTextureInfoAsync(`${r}/sheenRoughnessTexture`,e.sheenRoughnessTexture,i=>{i.name=`${t.name} (Sheen Roughness)`,o.fuzzRoughnessTexture=i}))),Promise.all(s).then(()=>{})}}l(m);d(m,!0,h=>new c(h));export{c as KHR_materials_sheen}; diff --git a/trackio/frontend/dist/assets/KHR_materials_specular-z6x6B2Wk.js b/trackio/frontend/dist/assets/KHR_materials_specular-z6x6B2Wk.js new file mode 100644 index 0000000000000000000000000000000000000000..2a2347e094f7139fe03889bd21e5ed3157d1f8f2 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_specular-z6x6B2Wk.js @@ -0,0 +1 @@ +import{G as u}from"./glTFLoader-BPeiodqB.js";import{Color3 as l}from"./math.color-CzLaKyb6.js";import{u as n,r as c}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const i="KHR_materials_specular";class d{constructor(t){this.name=i,this.order=190,this._loader=t,this.enabled=this._loader.isExtensionUsed(i)}dispose(){this._loader=null}loadMaterialPropertiesAsync(t,r,s){return u.LoadExtensionAsync(t,r,this.name,async(a,o)=>{const e=new Array;e.push(this._loader.loadMaterialPropertiesAsync(t,r,s)),e.push(this._loadSpecularPropertiesAsync(a,o,s));const m=this._loader._getOrCreateMaterialAdapter(s);return o.extensions&&o.extensions.EXT_materials_specular_edge_color&&o.extensions.EXT_materials_specular_edge_color.specularEdgeColorEnabled&&m.enableSpecularEdgeColor(!0),await Promise.all(e).then(()=>{})})}_loadSpecularPropertiesAsync(t,r,s){const a=this._loader._getOrCreateMaterialAdapter(s),o=new Array;return a.specularWeight=r.specularFactor??1,a.specularColor=r.specularColorFactor!==void 0?l.FromArray(r.specularColorFactor):new l(1,1,1),r.specularTexture&&(r.specularTexture.nonColorData=!0,o.push(this._loader.loadTextureInfoAsync(`${t}/specularTexture`,r.specularTexture,e=>{e.name=`${s.name} (Specular)`,a.specularWeightTexture=e}))),r.specularColorTexture&&o.push(this._loader.loadTextureInfoAsync(`${t}/specularColorTexture`,r.specularColorTexture,e=>{e.name=`${s.name} (Specular Color)`,a.specularColorTexture=e})),Promise.all(o).then(()=>{})}}n(i);c(i,!0,p=>new d(p));export{d as KHR_materials_specular}; diff --git a/trackio/frontend/dist/assets/KHR_materials_transmission-BOMnlenZ.js b/trackio/frontend/dist/assets/KHR_materials_transmission-BOMnlenZ.js new file mode 100644 index 0000000000000000000000000000000000000000..ad475d7c93715c8ec39dae8101c2032fef16e452 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_transmission-BOMnlenZ.js @@ -0,0 +1 @@ +import{G as u}from"./glTFLoader-BPeiodqB.js";import{R as c}from"./renderTargetTexture-B0kOC1Jy.js";import{O as m}from"./math.vector-C1xyPDEl.js";import{C as g}from"./objectModelMapping-DWcnlpHl.js";import{T}from"./tools-D01N7ptR.js";import{u as f,r as M}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.color-CzLaKyb6.js";import"./math.scalar.functions-DkeaWAWz.js";import"./mesh-Fj-9677A.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";class p{static _GetDefaultOptions(){return{renderSize:1024,samples:4,lodGenerationScale:1,lodGenerationOffset:-4,renderTargetTextureType:g.TEXTURETYPE_HALF_FLOAT,generateMipmaps:!0}}constructor(e,s,t){this._opaqueRenderTarget=null,this._opaqueMeshesCache=[],this._transparentMeshesCache=[],this._materialObservers={},this._options={...p._GetDefaultOptions(),...e},this._scene=s,this._scene._transmissionHelper=this,this._loader=t,this.onErrorObservable=new m,this._scene.onDisposeObservable.addOnce(()=>{this.dispose()}),this._parseScene(),this._setupRenderTargets()}updateOptions(e){if(!Object.keys(e).filter(i=>this._options[i]!==e[i]).length)return;const t={...this._options,...e},r=this._options;this._options=t,t.renderSize!==r.renderSize||t.renderTargetTextureType!==r.renderTargetTextureType||t.generateMipmaps!==r.generateMipmaps||!this._opaqueRenderTarget?this._setupRenderTargets():(this._opaqueRenderTarget.samples=t.samples,this._opaqueRenderTarget.lodGenerationScale=t.lodGenerationScale,this._opaqueRenderTarget.lodGenerationOffset=t.lodGenerationOffset)}getOpaqueTarget(){return this._opaqueRenderTarget}_addMesh(e){this._materialObservers[e.uniqueId]=e.onMaterialChangedObservable.add(this._onMeshMaterialChanged.bind(this)),T.SetImmediate(()=>{if(e.material){if(!this._loader.isMatchingMaterialType(e.material))return;const s=this._loader._getOrCreateMaterialAdapter(e.material);s.transmissionWeight>0?(s.refractionBackgroundTexture=this._opaqueRenderTarget,this._transparentMeshesCache.indexOf(e)===-1&&this._transparentMeshesCache.push(e)):this._opaqueMeshesCache.indexOf(e)===-1&&this._opaqueMeshesCache.push(e)}})}_removeMesh(e){e.onMaterialChangedObservable.remove(this._materialObservers[e.uniqueId]),delete this._materialObservers[e.uniqueId];let s=this._transparentMeshesCache.indexOf(e);s!==-1&&this._transparentMeshesCache.splice(s,1),s=this._opaqueMeshesCache.indexOf(e),s!==-1&&this._opaqueMeshesCache.splice(s,1)}_parseScene(){this._scene.meshes.forEach(this._addMesh.bind(this)),this._scene.onNewMeshAddedObservable.add(this._addMesh.bind(this)),this._scene.onMeshRemovedObservable.add(this._removeMesh.bind(this))}_onMeshMaterialChanged(e){const s=this._transparentMeshesCache.indexOf(e),t=this._opaqueMeshesCache.indexOf(e);if(!this._loader.isMatchingMaterialType(e.material))return;const r=e.material?this._loader._getOrCreateMaterialAdapter(e.material):null;(r?r.transmissionWeight>0:!1)?(r&&(r.refractionBackgroundTexture=this._opaqueRenderTarget),t!==-1?(this._opaqueMeshesCache.splice(t,1),this._transparentMeshesCache.push(e)):s===-1&&this._transparentMeshesCache.push(e)):s!==-1?(this._transparentMeshesCache.splice(s,1),this._opaqueMeshesCache.push(e)):t===-1&&this._opaqueMeshesCache.push(e)}_isRenderTargetValid(){var e;return((e=this._opaqueRenderTarget)==null?void 0:e.getInternalTexture())!==null}_setupRenderTargets(){var s;this._opaqueRenderTarget&&this._opaqueRenderTarget.dispose(),this._opaqueRenderTarget=new c("opaqueSceneTexture",this._options.renderSize,this._scene,this._options.generateMipmaps,void 0,this._options.renderTargetTextureType),this._opaqueRenderTarget.ignoreCameraViewport=!0,this._opaqueRenderTarget.renderList=this._opaqueMeshesCache,this._opaqueRenderTarget.clearColor=((s=this._options.clearColor)==null?void 0:s.clone())??this._scene.clearColor.clone(),this._opaqueRenderTarget.gammaSpace=!1,this._opaqueRenderTarget.lodGenerationScale=this._options.lodGenerationScale,this._opaqueRenderTarget.lodGenerationOffset=this._options.lodGenerationOffset,this._opaqueRenderTarget.samples=this._options.samples,this._opaqueRenderTarget.renderSprites=!0,this._opaqueRenderTarget.renderParticles=!0,this._opaqueRenderTarget.disableImageProcessing=!0;let e;this._opaqueRenderTarget.onBeforeBindObservable.add(t=>{e=this._scene.environmentIntensity,this._scene.environmentIntensity=1,this._options.clearColor?t.clearColor.copyFrom(this._options.clearColor):this._scene.clearColor.toLinearSpaceToRef(t.clearColor,this._scene.getEngine().useExactSrgbConversions)}),this._opaqueRenderTarget.onAfterUnbindObservable.add(()=>{this._scene.environmentIntensity=e});for(const t of this._transparentMeshesCache)if(t.material){if(!this._loader.isMatchingMaterialType(t.material))return;const r=this._loader._getOrCreateMaterialAdapter(t.material);r.transmissionWeight>0&&(r.refractionBackgroundTexture=this._opaqueRenderTarget)}}dispose(){this._scene._transmissionHelper=void 0,this._opaqueRenderTarget&&(this._opaqueRenderTarget.dispose(),this._opaqueRenderTarget=null),this._transparentMeshesCache=[],this._opaqueMeshesCache=[]}}const o="KHR_materials_transmission";class q{constructor(e){this.name=o,this.order=175,this._loader=e,this.enabled=this._loader.isExtensionUsed(o),this.enabled&&(e.parent.transparencyAsCoverage=!0)}dispose(){this._loader=null}loadMaterialPropertiesAsync(e,s,t){return u.LoadExtensionAsync(e,s,this.name,async(r,i)=>{const a=new Array;return a.push(this._loader.loadMaterialPropertiesAsync(e,s,t)),a.push(this._loadTransparentPropertiesAsync(r,s,t,i)),await Promise.all(a).then(()=>{})})}_loadTransparentPropertiesAsync(e,s,t,r){var l,_;const i=this._loader._getOrCreateMaterialAdapter(t),a=r.transmissionFactor!==void 0?r.transmissionFactor:0;if(a===0||!i)return Promise.resolve();if(i.configureTransmission(),i.transmissionWeight=a,a>0&&!this._loader.parent.dontUseTransmissionHelper){const n=t.getScene();n._transmissionHelper?(l=n._transmissionHelper)!=null&&l._isRenderTargetValid()||(_=n._transmissionHelper)==null||_._setupRenderTargets():new p({},t.getScene(),this._loader)}let d=Promise.resolve(null);return r.transmissionTexture&&(r.transmissionTexture.nonColorData=!0,d=this._loader.loadTextureInfoAsync(`${e}/transmissionTexture`,r.transmissionTexture,n=>{n.name=`${t.name} (Transmission)`,i.transmissionWeightTexture=n})),d.then(()=>{})}}f(o);M(o,!0,h=>new q(h));export{q as KHR_materials_transmission}; diff --git a/trackio/frontend/dist/assets/KHR_materials_unlit-CTd-ysfe.js b/trackio/frontend/dist/assets/KHR_materials_unlit-CTd-ysfe.js new file mode 100644 index 0000000000000000000000000000000000000000..7c3257b430f9c091e591f0b0140ec54e63d938ae --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_unlit-CTd-ysfe.js @@ -0,0 +1 @@ +import{Color3 as l}from"./math.color-CzLaKyb6.js";import{G as n}from"./glTFLoader-BPeiodqB.js";import{u as d,r as u}from"./dynamic-BUKysMyT.js";import"./math.scalar.functions-DkeaWAWz.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const s="KHR_materials_unlit";class c{constructor(r){this.name=s,this.order=210,this._loader=r,this.enabled=this._loader.isExtensionUsed(s)}dispose(){this._loader=null}loadMaterialPropertiesAsync(r,o,e){return n.LoadExtensionAsync(r,o,this.name,async()=>await this._loadUnlitPropertiesAsync(r,o,e))}_loadUnlitPropertiesAsync(r,o,e){const t=this._loader._getOrCreateMaterialAdapter(e),m=new Array,i=o.pbrMetallicRoughness;return i&&(i.baseColorFactor&&(t.baseColor=l.FromArray(i.baseColorFactor),t.geometryOpacity=i.baseColorFactor[3]),i.baseColorTexture&&m.push(this._loader.loadTextureInfoAsync(`${r}/baseColorTexture`,i.baseColorTexture,a=>{a.name=`${e.name} (Base Color)`,t.baseColorTexture=a}))),t.isUnlit=!0,o.doubleSided&&(t.backFaceCulling=!1,t.twoSidedLighting=!0),this._loader.loadMaterialAlphaProperties(r,o,e),Promise.all(m).then(()=>{})}}d(s);u(s,!0,p=>new c(p));export{c as KHR_materials_unlit}; diff --git a/trackio/frontend/dist/assets/KHR_materials_variants-CR-weLGR.js b/trackio/frontend/dist/assets/KHR_materials_variants-CR-weLGR.js new file mode 100644 index 0000000000000000000000000000000000000000..9b7a68590c46fd2ddd2162ecd4068c4f46abc190 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_variants-CR-weLGR.js @@ -0,0 +1 @@ +import{G as S,A as L}from"./glTFLoader-BPeiodqB.js";import{M as O}from"./mesh-Fj-9677A.js";import{u as C,r as I}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./cameraInputsManager-WrTH9PVf.js";import"./math.axis-Ck7JfPPj.js";import"./node-C-Iu0ALa.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./math.plane-Ce_75-g5.js";import"./animation-IftAdLIx.js";import"./material-CrEsrP1R.js";import"./subMesh-UWDo5wjI.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./drawWrapper-CZVk0xN-.js";import"./lightConstants-B1TSTh-k.js";import"./sceneComponent-Dflhczui.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./smartArray-CO6iJ0u5.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./math.viewport-CgkTt1RS.js";const r="KHR_materials_variants";class s{constructor(t){this.name=r,this._loader=t,this.enabled=this._loader.isExtensionUsed(r)&&!this._loader.parent.skipMaterials}dispose(){this._loader=null}static GetAvailableVariants(t){const a=this._GetExtensionMetadata(t);return a?Object.keys(a.variants):[]}getAvailableVariants(t){return s.GetAvailableVariants(t)}static SelectVariant(t,a){const e=this._GetExtensionMetadata(t);if(!e)throw new Error(`Cannot select variant on a glTF mesh that does not have the ${r} extension`);const o=c=>{const p=e.variants[c];if(p)for(const h of p)h.mesh.material=h.material};if(a instanceof Array)for(const c of a)o(c);else o(a);e.lastSelected=a}selectVariant(t,a){s.SelectVariant(t,a)}static Reset(t){const a=this._GetExtensionMetadata(t);if(!a)throw new Error(`Cannot reset on a glTF mesh that does not have the ${r} extension`);for(const e of a.original)e.mesh.material=e.material;a.lastSelected=null}reset(t){s.Reset(t)}static GetLastSelectedVariant(t){const a=this._GetExtensionMetadata(t);if(!a)throw new Error(`Cannot get the last selected variant on a glTF mesh that does not have the ${r} extension`);return a.lastSelected}getLastSelectedVariant(t){return s.GetLastSelectedVariant(t)}static _GetExtensionMetadata(t){var a,e;return((e=(a=t==null?void 0:t._internalMetadata)==null?void 0:a.gltf)==null?void 0:e[r])||null}onLoading(){const t=this._loader.gltf.extensions;if(t&&t[this.name]){const a=t[this.name];this._variants=a.variants}}onReady(){var a;const t=this._loader.rootBabylonMesh;if(t){const e=this._loader.parent.extensionOptions[r];e!=null&&e.defaultVariant&&s.SelectVariant(t,e.defaultVariant),(a=e==null?void 0:e.onLoaded)==null||a.call(e,{get variants(){return s.GetAvailableVariants(t)},get selectedVariant(){const o=s.GetLastSelectedVariant(t);return o?Array.isArray(o)?o[0]:o:s.GetAvailableVariants(t)[0]},set selectedVariant(o){s.SelectVariant(t,o)}})}}_loadMeshPrimitiveAsync(t,a,e,o,c,p){return S.LoadExtensionAsync(t,c,this.name,async(h,V)=>{const x=new Array;return x.push(this._loader._loadMeshPrimitiveAsync(t,a,e,o,c,l=>{if(p(l),l instanceof O){const $=S._GetDrawMode(t,c.mode),d=this._loader.rootBabylonMesh,A=d?d._internalMetadata=d._internalMetadata||{}:{},E=A.gltf=A.gltf||{},f=E[r]=E[r]||{lastSelected:null,original:[],variants:{}};f.original.push({mesh:l,material:l.material});for(let g=0;g{for(let v=0;v{const G=k;let m,i=G;do{if(i=i.parent,!i)return;m=s._GetExtensionMetadata(i)}while(m===null);if(d&&m===s._GetExtensionMetadata(d)){i._internalMetadata={};for(const n in d._internalMetadata)i._internalMetadata[n]=d._internalMetadata[n];i._internalMetadata.gltf=[];for(const n in d._internalMetadata.gltf)i._internalMetadata.gltf[n]=d._internalMetadata.gltf[n];i._internalMetadata.gltf[r]={lastSelected:null,original:[],variants:{}};for(const n of m.original)i._internalMetadata.gltf[r].original.push({mesh:n.mesh,material:n.material});for(const n in m.variants)if(Object.prototype.hasOwnProperty.call(m.variants,n)){i._internalMetadata.gltf[r].variants[n]=[];for(const y of m.variants[n])i._internalMetadata.gltf[r].variants[n].push({mesh:y.mesh,material:y.material})}m=i._internalMetadata.gltf[r]}for(const n of m.original)n.mesh===l&&(n.mesh=G);for(const n of m.variants[M.name])n.mesh===l&&(n.mesh=G)})}}))}}})),await Promise.all(x).then(([l])=>l)})}}C(r);I(r,!0,_=>new s(_));export{s as KHR_materials_variants}; diff --git a/trackio/frontend/dist/assets/KHR_materials_volume-DyryNepY.js b/trackio/frontend/dist/assets/KHR_materials_volume-DyryNepY.js new file mode 100644 index 0000000000000000000000000000000000000000..b5905f87e0fe90a9adf0cbc2541a6317a41e0e9a --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_volume-DyryNepY.js @@ -0,0 +1 @@ +import{Color3 as h}from"./math.color-CzLaKyb6.js";import{G as u}from"./glTFLoader-BPeiodqB.js";import{u as d,r as _}from"./dynamic-BUKysMyT.js";import{V as f}from"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./index-Bx1QoKVU.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const n="KHR_materials_volume";class A{constructor(o){this.name=n,this.order=173,this._loader=o,this.enabled=this._loader.isExtensionUsed(n),this.enabled&&this._loader._disableInstancedMesh++}dispose(){this.enabled&&this._loader._disableInstancedMesh--,this._loader=null}loadMaterialPropertiesAsync(o,s,e){return u.LoadExtensionAsync(o,s,this.name,async(t,r)=>{const i=new Array;return i.push(this._loader.loadMaterialPropertiesAsync(o,s,e)),i.push(this._loadVolumePropertiesAsync(t,s,e,r)),await Promise.all(i).then(()=>{})})}_loadVolumePropertiesAsync(o,s,e,t){const r=this._loader._getOrCreateMaterialAdapter(e);if(r.transmissionWeight===0&&r.subsurfaceWeight===0||!t.thicknessFactor)return Promise.resolve();const i=t.attenuationDistance!==void 0?t.attenuationDistance:Number.MAX_VALUE,a=t.attenuationColor!==void 0&&t.attenuationColor.length==3?h.FromArray(t.attenuationColor):h.White(),p=new f(-Math.log(a.r),-Math.log(a.g),-Math.log(a.b));p.scaleInPlace(1/Math.max(i,.001)),r.extinctionCoefficient=p,r.transmissionDepth=i,r.transmissionColor=a,r.volumeThickness=t.thicknessFactor??0;const l=new Array;return t.thicknessTexture&&(t.thicknessTexture.nonColorData=!0,l.push(this._loader.loadTextureInfoAsync(`${o}/thicknessTexture`,t.thicknessTexture,c=>{c.name=`${e.name} (Thickness)`,r.volumeThicknessTexture=c}))),Promise.all(l).then(()=>{})}}d(n);_(n,!0,m=>new A(m));export{A as KHR_materials_volume}; diff --git a/trackio/frontend/dist/assets/KHR_materials_volume_scatter-Dilc3FVg.js b/trackio/frontend/dist/assets/KHR_materials_volume_scatter-Dilc3FVg.js new file mode 100644 index 0000000000000000000000000000000000000000..71f93cd96202033bf1c6334f7766de4a0adec281 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_materials_volume_scatter-Dilc3FVg.js @@ -0,0 +1 @@ +import{Color3 as p}from"./math.color-CzLaKyb6.js";import{V as s}from"./math.vector-C1xyPDEl.js";import{G as y}from"./glTFLoader-BPeiodqB.js";import{u as C,r as _}from"./dynamic-BUKysMyT.js";import"./math.scalar.functions-DkeaWAWz.js";import"./index-Bx1QoKVU.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const c="KHR_materials_volume_scatter";function x(n){const r=new s(n.r,n.g,n.b),i=new s(4.09712,4.09712,4.09712);i.multiplyInPlace(r),i.addInPlace(new s(4.20863,4.20863,4.20863));const e=new s(9.59217,9.59217,9.59217);return e.addInPlace(new s(41.6808,41.6808,41.6808).multiplyInPlace(r)),e.addInPlace(new s(17.7126,17.7126,17.7126).multiplyInPlace(r.multiply(r))),i.subtractInPlace(new s(Math.sqrt(e.x),Math.sqrt(e.y),Math.sqrt(e.z))),new s(1,1,1).subtract(i.multiply(i))}class A{constructor(r){this.name=c,this.order=172,this._loader=r,this.enabled=this._loader.isExtensionUsed(c),this.enabled&&this._loader._disableInstancedMesh++}dispose(){this.enabled&&this._loader._disableInstancedMesh--,this._loader=null}loadMaterialPropertiesAsync(r,i,e){return y.LoadExtensionAsync(r,i,this.name,async(o,t)=>{const l=new Array;return l.push(this._loader.loadMaterialPropertiesAsync(r,i,e)),l.push(this._loadVolumePropertiesAsync(o,i,e,t)),await Promise.all(l).then(()=>{})})}_loadVolumePropertiesAsync(r,i,e,o){const t=this._loader._getOrCreateMaterialAdapter(e);if(t.transmissionWeight===0&&t.subsurfaceWeight===0)return Promise.resolve();const l=o.multiscatterColor!==void 0&&o.multiscatterColor.length==3?p.FromArray(o.multiscatterColor):p.Black(),u=o.scatterAnisotropy!==void 0?o.scatterAnisotropy:0;if(t.transmissionWeight>0){const a=x(l),d=new s(-Math.log(t.transmissionColor.r),-Math.log(t.transmissionColor.g),-Math.log(t.transmissionColor.b));d.scaleInPlace(1/Math.max(t.transmissionDepth,.001));const m=d.multiply(a);m.scaleInPlace(t.transmissionDepth),t.transmissionScatter.set(m.x,m.y,m.z);let f=Promise.resolve(null);return o.multiscatterColorTexture&&(o.multiscatterColorTexture.nonColorData=!0,f=this._loader.loadTextureInfoAsync(`${r}/multiscatterColorTexture`,o.multiscatterColorTexture,h=>{h.name=`${e.name} (Transmission)`,t.transmissionWeightTexture=h})),t.transmissionScatterAnisotropy=u,f.then(()=>{})}if(t.subsurfaceWeight>0){t.subsurfaceScatterAnisotropy=u,t.subsurfaceColor=l;const a=new s(t.extinctionCoefficient.x!==0?1/t.extinctionCoefficient.x:1,t.extinctionCoefficient.y!==0?1/t.extinctionCoefficient.y:1,t.extinctionCoefficient.z!==0?1/t.extinctionCoefficient.z:1);t.subsurfaceRadius=Math.max(a.x,a.y,a.z),t.subsurfaceRadiusScale=new p(a.x/t.subsurfaceRadius,a.y/t.subsurfaceRadius,a.z/t.subsurfaceRadius)}return Promise.resolve()}}C(c);_(c,!0,n=>new A(n));export{A as KHR_materials_volume_scatter}; diff --git a/trackio/frontend/dist/assets/KHR_mesh_quantization-DRY1BM2Q.js b/trackio/frontend/dist/assets/KHR_mesh_quantization-DRY1BM2Q.js new file mode 100644 index 0000000000000000000000000000000000000000..d7e6b0fffd78fdf0f958de843d90a84894482549 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_mesh_quantization-DRY1BM2Q.js @@ -0,0 +1 @@ +import{u as r,r as m}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./sceneLoader-CutT-qCn.js";import"./tools-D01N7ptR.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./math.color-CzLaKyb6.js";import"./scene-gn_hoGMy.js";import"./smartArray-CO6iJ0u5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./renderingManager-CiY2OUrf.js";import"./deviceInputEvents-BTza-zoR.js";import"./perfCounter-DusmGz8t.js";import"./sceneLoaderFlags-wywyb9ml.js";const t="KHR_mesh_quantization";class p{constructor(o){this.name=t,this.enabled=o.isExtensionUsed(t)}dispose(){}}r(t);m(t,!0,i=>new p(i));export{p as KHR_mesh_quantization}; diff --git a/trackio/frontend/dist/assets/KHR_node_hoverability-k_keZPJw.js b/trackio/frontend/dist/assets/KHR_node_hoverability-k_keZPJw.js new file mode 100644 index 0000000000000000000000000000000000000000..5d05bd19fa3fef9be0fc6c8bb7fb8a281f37ca47 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_node_hoverability-k_keZPJw.js @@ -0,0 +1 @@ +import{u as k,r as B}from"./dynamic-BUKysMyT.js";import{e as b}from"./declarationMapper-oEUQrCmY.js";import{A as y}from"./objectModelMapping-DWcnlpHl.js";import"./index-Bx1QoKVU.js";import"./sceneLoader-CutT-qCn.js";import"./tools-D01N7ptR.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./math.color-CzLaKyb6.js";import"./scene-gn_hoGMy.js";import"./smartArray-CO6iJ0u5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./renderingManager-CiY2OUrf.js";import"./deviceInputEvents-BTza-zoR.js";import"./perfCounter-DusmGz8t.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./node-C-Iu0ALa.js";import"./light-DT53MGnW.js";import"./math.axis-Ck7JfPPj.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";const p="KHR_node_hoverability",I="targetMeshPointerOver_";b("event/onHoverIn",p,{blocks:["FlowGraphPointerOverEventBlock","FlowGraphGetVariableBlock","FlowGraphIndexOfBlock","KHR_interactivity/FlowGraphGLTFDataProvider"],configuration:{stopPropagation:{name:"stopPropagation"},nodeIndex:{name:"variable",toBlock:"FlowGraphGetVariableBlock",dataTransformer(o){return I+o}}},outputs:{values:{hoverNodeIndex:{name:"index",toBlock:"FlowGraphIndexOfBlock"},controllerIndex:{name:"pointerId"}},flows:{out:{name:"done"}}},interBlockConnectors:[{input:"targetMesh",output:"value",inputBlockIndex:0,outputBlockIndex:1,isVariable:!0},{input:"array",output:"nodes",inputBlockIndex:2,outputBlockIndex:3,isVariable:!0},{input:"object",output:"meshUnderPointer",inputBlockIndex:2,outputBlockIndex:0,isVariable:!0}],extraProcessor(o,n,i,s,t,u,e){var c,l,m,h,f,v,x;const a=t[t.length-1];a.config=a.config||{},a.config.glTF=e;const r=(m=(l=(c=o.configuration)==null?void 0:c.nodeIndex)==null?void 0:l.value)==null?void 0:m[0];if(r===void 0||typeof r!="number")throw new Error("nodeIndex not found in configuration");const d=I+r;return t[1].config.variable=d,u._userVariables[d]={className:"Mesh",id:(f=(h=e==null?void 0:e.nodes)==null?void 0:h[r]._babylonTransformNode)==null?void 0:f.id,uniqueId:(x=(v=e==null?void 0:e.nodes)==null?void 0:v[r]._babylonTransformNode)==null?void 0:x.uniqueId},t}});const _="targetMeshPointerOut_";b("event/onHoverOut",p,{blocks:["FlowGraphPointerOutEventBlock","FlowGraphGetVariableBlock","FlowGraphIndexOfBlock","KHR_interactivity/FlowGraphGLTFDataProvider"],configuration:{stopPropagation:{name:"stopPropagation"},nodeIndex:{name:"variable",toBlock:"FlowGraphGetVariableBlock",dataTransformer(o){return _+o}}},outputs:{values:{hoverNodeIndex:{name:"index",toBlock:"FlowGraphIndexOfBlock"},controllerIndex:{name:"pointerId"}},flows:{out:{name:"done"}}},interBlockConnectors:[{input:"targetMesh",output:"value",inputBlockIndex:0,outputBlockIndex:1,isVariable:!0},{input:"array",output:"nodes",inputBlockIndex:2,outputBlockIndex:3,isVariable:!0},{input:"object",output:"meshOutOfPointer",inputBlockIndex:2,outputBlockIndex:0,isVariable:!0}],extraProcessor(o,n,i,s,t,u,e){var c,l,m,h,f,v,x;const a=t[t.length-1];a.config=a.config||{},a.config.glTF=e;const r=(m=(l=(c=o.configuration)==null?void 0:c.nodeIndex)==null?void 0:l.value)==null?void 0:m[0];if(r===void 0||typeof r!="number")throw new Error("nodeIndex not found in configuration");const d=_+r;return t[1].config.variable=d,u._userVariables[d]={className:"Mesh",id:(f=(h=e==null?void 0:e.nodes)==null?void 0:h[r]._babylonTransformNode)==null?void 0:f.id,uniqueId:(x=(v=e==null?void 0:e.nodes)==null?void 0:v[r]._babylonTransformNode)==null?void 0:x.uniqueId},t}});y("/nodes/{}/extensions/KHR_node_hoverability/hoverable",{get:o=>{const n=o._babylonTransformNode;return n&&n.pointerOverDisableMeshTesting!==void 0?n.pointerOverDisableMeshTesting:!0},set:(o,n)=>{var i;(i=n._primitiveBabylonMeshes)==null||i.forEach(s=>{s.pointerOverDisableMeshTesting=!o})},getTarget:o=>o._babylonTransformNode,getPropertyName:[()=>"pointerOverDisableMeshTesting"],type:"boolean"});class g{constructor(n){this.name=p,this._loader=n,this.enabled=n.isExtensionUsed(p)}async onReady(){var n;(n=this._loader.gltf.nodes)==null||n.forEach(i=>{var s,t,u;(s=i.extensions)!=null&&s.KHR_node_hoverability&&((t=i.extensions)==null?void 0:t.KHR_node_hoverability.hoverable)===!1&&((u=i._babylonTransformNode)==null||u.getChildMeshes().forEach(e=>{e.pointerOverDisableMeshTesting=!0}))})}dispose(){this._loader=null}}k(p);B(p,!0,o=>new g(o));export{g as KHR_node_hoverability}; diff --git a/trackio/frontend/dist/assets/KHR_node_selectability-CWoTfyY_.js b/trackio/frontend/dist/assets/KHR_node_selectability-CWoTfyY_.js new file mode 100644 index 0000000000000000000000000000000000000000..7e6e5ef9aba564237e0d2bf2106e07b0afc52851 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_node_selectability-CWoTfyY_.js @@ -0,0 +1 @@ +import{u as h,r as x}from"./dynamic-BUKysMyT.js";import{e as y}from"./declarationMapper-oEUQrCmY.js";import{A as I}from"./objectModelMapping-DWcnlpHl.js";import"./index-Bx1QoKVU.js";import"./sceneLoader-CutT-qCn.js";import"./tools-D01N7ptR.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./math.color-CzLaKyb6.js";import"./scene-gn_hoGMy.js";import"./smartArray-CO6iJ0u5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./renderingManager-CiY2OUrf.js";import"./deviceInputEvents-BTza-zoR.js";import"./perfCounter-DusmGz8t.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./node-C-Iu0ALa.js";import"./light-DT53MGnW.js";import"./math.axis-Ck7JfPPj.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";const s="KHR_node_selectability";y("event/onSelect",s,{blocks:["FlowGraphMeshPickEventBlock","FlowGraphGetVariableBlock","FlowGraphIndexOfBlock","KHR_interactivity/FlowGraphGLTFDataProvider"],configuration:{stopPropagation:{name:"stopPropagation"},nodeIndex:{name:"variable",toBlock:"FlowGraphGetVariableBlock",dataTransformer(e){return"pickedMesh_"+e}}},outputs:{values:{selectedNodeIndex:{name:"index",toBlock:"FlowGraphIndexOfBlock"},controllerIndex:{name:"pointerId"},selectionPoint:{name:"pickedPoint"},selectionRayOrigin:{name:"pickOrigin"}},flows:{out:{name:"done"}}},interBlockConnectors:[{input:"asset",output:"value",inputBlockIndex:0,outputBlockIndex:1,isVariable:!0},{input:"array",output:"nodes",inputBlockIndex:2,outputBlockIndex:3,isVariable:!0},{input:"object",output:"pickedMesh",inputBlockIndex:2,outputBlockIndex:0,isVariable:!0}],extraProcessor(e,o,i,r,n,p,t){var l,m,u,f,b,k,_;const c=n[n.length-1];c.config=c.config||{},c.config.glTF=t;const a=(u=(m=(l=e.configuration)==null?void 0:l.nodeIndex)==null?void 0:m.value)==null?void 0:u[0];if(a===void 0||typeof a!="number")throw new Error("nodeIndex not found in configuration");const d="pickedMesh_"+a;return n[1].config.variable=d,p._userVariables[d]={className:"Mesh",id:(b=(f=t==null?void 0:t.nodes)==null?void 0:f[a]._babylonTransformNode)==null?void 0:b.id,uniqueId:(_=(k=t==null?void 0:t.nodes)==null?void 0:k[a]._babylonTransformNode)==null?void 0:_.uniqueId},n}});I("/nodes/{}/extensions/KHR_node_selectability/selectable",{get:e=>{const o=e._babylonTransformNode;return o&&o.isPickable!==void 0?o.isPickable:!0},set:(e,o)=>{var i;(i=o._primitiveBabylonMeshes)==null||i.forEach(r=>{r.isPickable=e})},getTarget:e=>e._babylonTransformNode,getPropertyName:[()=>"isPickable"],type:"boolean"});class B{constructor(o){this.name=s,this._loader=o,this.enabled=o.isExtensionUsed(s)}async onReady(){var o;(o=this._loader.gltf.nodes)==null||o.forEach(i=>{var r,n,p;(r=i.extensions)!=null&&r.KHR_node_selectability&&((n=i.extensions)==null?void 0:n.KHR_node_selectability.selectable)===!1&&((p=i._babylonTransformNode)==null||p.getChildMeshes().forEach(t=>{t.isPickable=!1}))})}dispose(){this._loader=null}}h(s);x(s,!0,e=>new B(e));export{B as KHR_node_selectability}; diff --git a/trackio/frontend/dist/assets/KHR_node_visibility-Dxpqh4YG.js b/trackio/frontend/dist/assets/KHR_node_visibility-Dxpqh4YG.js new file mode 100644 index 0000000000000000000000000000000000000000..3c3412fbccf6b4e14b68086ae2c4e4eac9c4580c --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_node_visibility-Dxpqh4YG.js @@ -0,0 +1 @@ +import{u as n,r as m}from"./dynamic-BUKysMyT.js";import{A as b}from"./objectModelMapping-DWcnlpHl.js";import"./index-Bx1QoKVU.js";import"./sceneLoader-CutT-qCn.js";import"./tools-D01N7ptR.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./math.color-CzLaKyb6.js";import"./scene-gn_hoGMy.js";import"./smartArray-CO6iJ0u5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./renderingManager-CiY2OUrf.js";import"./deviceInputEvents-BTza-zoR.js";import"./perfCounter-DusmGz8t.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./node-C-Iu0ALa.js";import"./light-DT53MGnW.js";import"./math.axis-Ck7JfPPj.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";const s="KHR_node_visibility";b("/nodes/{}/extensions/KHR_node_visibility/visible",{get:o=>{const i=o._babylonTransformNode;return i&&i.isVisible!==void 0?i.isVisible:!0},set:(o,i)=>{var e,t;(e=i._primitiveBabylonMeshes)==null||e.forEach(r=>{r.inheritVisibility=!0}),i._babylonTransformNode&&(i._babylonTransformNode.isVisible=o),(t=i._primitiveBabylonMeshes)==null||t.forEach(r=>{r.isVisible=o})},getTarget:o=>o._babylonTransformNode,getPropertyName:[()=>"isVisible"],type:"boolean"});class l{constructor(i){this.name=s,this._loader=i,this.enabled=i.isExtensionUsed(s)}onReady(){if(!this._loader)return;const i=this._loader.gltf.nodes;if(i)for(const e of i){const t=e._babylonTransformNode;t&&(t.inheritVisibility=!0,e.extensions&&e.extensions.KHR_node_visibility&&e.extensions.KHR_node_visibility.visible===!1&&(t.isVisible=!1))}}dispose(){delete this._loader}}n(s);m(s,!0,o=>new l(o));export{l as KHR_node_visibility}; diff --git a/trackio/frontend/dist/assets/KHR_texture_basisu-wb_QWhaW.js b/trackio/frontend/dist/assets/KHR_texture_basisu-wb_QWhaW.js new file mode 100644 index 0000000000000000000000000000000000000000..f26895d7f83d3573f7a9ad9a8d7160dc63a2e801 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_texture_basisu-wb_QWhaW.js @@ -0,0 +1 @@ +import{G as m,A as e}from"./glTFLoader-BPeiodqB.js";import{u as d,r as _}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./texture-BEqhDl5Y.js";import"./baseTexture-CgAggsey.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const o="KHR_texture_basisu";class c{constructor(r){this.name=o,this._loader=r,this.enabled=r.isExtensionUsed(o)}dispose(){this._loader=null}_loadTextureAsync(r,t,p){return m.LoadExtensionAsync(r,t,this.name,async(s,a)=>{const n=t.sampler==null?m.DefaultSampler:e.Get(`${r}/sampler`,this._loader.gltf.samplers,t.sampler),l=e.Get(`${s}/source`,this._loader.gltf.images,a.source);return await this._loader._createTextureAsync(r,n,l,u=>{p(u)},t._textureInfo.nonColorData?{useRGBAIfASTCBC7NotAvailableWhenUASTC:!0}:void 0,!t._textureInfo.nonColorData)})}}d(o);_(o,!0,i=>new c(i));export{c as KHR_texture_basisu}; diff --git a/trackio/frontend/dist/assets/KHR_texture_transform-BDMWBq3N.js b/trackio/frontend/dist/assets/KHR_texture_transform-BDMWBq3N.js new file mode 100644 index 0000000000000000000000000000000000000000..4b10028891e446af539c3018616a8d2b2fa412bb --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_texture_transform-BDMWBq3N.js @@ -0,0 +1 @@ +import{T as f}from"./texture-BEqhDl5Y.js";import{G as e}from"./glTFLoader-BPeiodqB.js";import{u as d,r as c}from"./dynamic-BUKysMyT.js";import"./bufferUtils-DTRdUD8L.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./baseTexture-CgAggsey.js";import"./decorators.serialization-C7d7ykGG.js";import"./math.color-CzLaKyb6.js";import"./tools-D01N7ptR.js";import"./math.plane-Ce_75-g5.js";import"./index-Bx1QoKVU.js";import"./mesh-Fj-9677A.js";import"./mesh.vertexData-Cr3zcPIw.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./subMesh-UWDo5wjI.js";import"./drawWrapper-CZVk0xN-.js";import"./smartArray-CO6iJ0u5.js";import"./node-C-Iu0ALa.js";import"./math.viewport-CgkTt1RS.js";import"./sceneComponent-Dflhczui.js";import"./lightConstants-B1TSTh-k.js";import"./sceneLoaderFlags-wywyb9ml.js";import"./math.axis-Ck7JfPPj.js";import"./material-CrEsrP1R.js";import"./drawWrapper.functions-Dzz0ghog.js";import"./cameraInputsManager-WrTH9PVf.js";import"./deviceInputEvents-BTza-zoR.js";import"./bone-B35odufe.js";import"./skeleton-BVUVDN0a.js";import"./rawTexture-Cp8_XuDs.js";import"./animation-IftAdLIx.js";import"./sceneLoader-CutT-qCn.js";import"./scene-gn_hoGMy.js";import"./renderingManager-CiY2OUrf.js";import"./perfCounter-DusmGz8t.js";import"./assetContainer-DOnbxwKX.js";import"./instancedMesh-Cz-e1Hca.js";import"./light-DT53MGnW.js";import"./objectModelMapping-DWcnlpHl.js";const i="KHR_texture_transform";class n{constructor(o){this.name=i,this._loader=o,this.enabled=this._loader.isExtensionUsed(i)}dispose(){this._loader=null}loadTextureInfoAsync(o,p,s){return e.LoadExtensionAsync(o,p,this.name,async(a,r)=>await this._loader.loadTextureInfoAsync(o,p,t=>{if(!(t instanceof f))throw new Error(`${a}: Texture type not supported`);r.offset&&(t.uOffset=r.offset[0],t.vOffset=r.offset[1]),t.uRotationCenter=0,t.vRotationCenter=0,r.rotation&&(t.wAng=-r.rotation),r.scale&&(t.uScale=r.scale[0],t.vScale=r.scale[1]),r.texCoord!=null&&(t.coordinatesIndex=r.texCoord),s(t)}))}}d(i);c(i,!0,m=>new n(m));export{n as KHR_texture_transform}; diff --git a/trackio/frontend/dist/assets/KHR_xmp_json_ld-kzuW6Xgd.js b/trackio/frontend/dist/assets/KHR_xmp_json_ld-kzuW6Xgd.js new file mode 100644 index 0000000000000000000000000000000000000000..06c9584f953cc6087749267d9642d51f6e52c905 --- /dev/null +++ b/trackio/frontend/dist/assets/KHR_xmp_json_ld-kzuW6Xgd.js @@ -0,0 +1 @@ +import{u as n,r as m}from"./dynamic-BUKysMyT.js";import"./index-Bx1QoKVU.js";import"./sceneLoader-CutT-qCn.js";import"./tools-D01N7ptR.js";import"./math.vector-C1xyPDEl.js";import"./math.scalar.functions-DkeaWAWz.js";import"./decorators.serialization-C7d7ykGG.js";import"./bufferUtils-DTRdUD8L.js";import"./math.color-CzLaKyb6.js";import"./scene-gn_hoGMy.js";import"./smartArray-CO6iJ0u5.js";import"./lightConstants-B1TSTh-k.js";import"./sceneComponent-Dflhczui.js";import"./math.plane-Ce_75-g5.js";import"./buffer-DqKlo1ts.js";import"./dataBuffer-CgoqiUzT.js";import"./renderingManager-CiY2OUrf.js";import"./deviceInputEvents-BTza-zoR.js";import"./perfCounter-DusmGz8t.js";import"./sceneLoaderFlags-wywyb9ml.js";const o="KHR_xmp_json_ld";class l{constructor(t){this.name=o,this.order=100,this._loader=t,this.enabled=this._loader.isExtensionUsed(o)}dispose(){this._loader=null}onLoading(){var e,i,a;if(this._loader.rootBabylonMesh===null)return;const t=(e=this._loader.gltf.extensions)==null?void 0:e.KHR_xmp_json_ld,s=(a=(i=this._loader.gltf.asset)==null?void 0:i.extensions)==null?void 0:a.KHR_xmp_json_ld;if(t&&s){const p=+s.packet;t.packets&&pnew l(r));export{l as KHR_xmp_json_ld}; diff --git a/trackio/frontend/dist/assets/LineageSection-CRyR37ec.js b/trackio/frontend/dist/assets/LineageSection-CRyR37ec.js new file mode 100644 index 0000000000000000000000000000000000000000..7ab54c1010216763445d1050cb69fc066892c72e --- /dev/null +++ b/trackio/frontend/dist/assets/LineageSection-CRyR37ec.js @@ -0,0 +1 @@ +var ur=Object.defineProperty;var ut=e=>{throw TypeError(e)};var hr=(e,t,r)=>t in e?ur(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r;var ht=(e,t,r)=>hr(e,typeof t!="symbol"?t+"":t,r),Ve=(e,t,r)=>t.has(e)||ut("Cannot "+r);var oe=(e,t,r)=>(Ve(e,t,"read from private field"),r?r.call(e):t.get(e)),ke=(e,t,r)=>t.has(e)?ut("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,r),Ge=(e,t,r,n)=>(Ve(e,t,"write to private field"),n?n.call(e,r):t.set(e,r),r),ft=(e,t,r)=>(Ve(e,t,"access private method"),r);import{e as fr,u as gr,a as Ue,p as et,b as Z,s as te,c as Tt,d as tt,g as h,f as y,h as O,i as H,j as vr,t as K,k as Be,l as V,m as pr,n as L,o as rt,q as j,r as Le,v as W,w as fe,x as B,y as ee,z as R,A as Ie,B as le,C as Pt,D as Qe,E as br,F as mr,S as _r,G as wr,H as gt,I as vt,J as Er,K as yr}from"./index-Bx1QoKVU.js";var de,be,Ne,Se,St;const Fe=class Fe{constructor(t){ke(this,Se);ke(this,de,new WeakMap);ke(this,be);ke(this,Ne);Ge(this,Ne,t)}observe(t,r){var n=oe(this,de).get(t)||new Set;return n.add(r),oe(this,de).set(t,n),ft(this,Se,St).call(this).observe(t,oe(this,Ne)),()=>{var o=oe(this,de).get(t);o.delete(r),o.size===0&&(oe(this,de).delete(t),oe(this,be).unobserve(t))}}};de=new WeakMap,be=new WeakMap,Ne=new WeakMap,Se=new WeakSet,St=function(){return oe(this,be)??Ge(this,be,new ResizeObserver(t=>{for(var r of t){Fe.entries.set(r.target,r);for(var n of oe(this,de).get(r.target)||[])n(r)}}))},ht(Fe,"entries",new WeakMap);let $e=Fe;var kr=new $e({box:"border-box"});function pt(e,t,r){var n=kr.observe(e,()=>r(e[t]));fr(()=>(gr(()=>r(e[t])),n))}var Ft=Object.defineProperty,xr=(e,t,r)=>t in e?Ft(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,Or=(e,t)=>{for(var r in t)Ft(e,r,{get:t[r],enumerable:!0})},Nr=(e,t,r)=>xr(e,t+"",r),Dt={};Or(Dt,{Graph:()=>J,alg:()=>nt,json:()=>qt,version:()=>jr});var Ir=Object.defineProperty,Rt=(e,t)=>{for(var r in t)Ir(e,r,{get:t[r],enumerable:!0})},J=class{constructor(e){this._isDirected=!0,this._isMultigraph=!1,this._isCompound=!1,this._nodes={},this._in={},this._preds={},this._out={},this._sucs={},this._edgeObjs={},this._edgeLabels={},this._nodeCount=0,this._edgeCount=0,this._defaultNodeLabelFn=()=>{},this._defaultEdgeLabelFn=()=>{},e&&(this._isDirected="directed"in e?e.directed:!0,this._isMultigraph="multigraph"in e?e.multigraph:!1,this._isCompound="compound"in e?e.compound:!1),this._isCompound&&(this._parent={},this._children={},this._children["\0"]={})}isDirected(){return this._isDirected}isMultigraph(){return this._isMultigraph}isCompound(){return this._isCompound}setGraph(e){return this._label=e,this}graph(){return this._label}setDefaultNodeLabel(e){return typeof e!="function"?this._defaultNodeLabelFn=()=>e:this._defaultNodeLabelFn=e,this}nodeCount(){return this._nodeCount}nodes(){return Object.keys(this._nodes)}sources(){return this.nodes().filter(e=>Object.keys(this._in[e]).length===0)}sinks(){return this.nodes().filter(e=>Object.keys(this._out[e]).length===0)}setNodes(e,t){return e.forEach(r=>{t!==void 0?this.setNode(r,t):this.setNode(r)}),this}setNode(e,t){return e in this._nodes?(arguments.length>1&&(this._nodes[e]=t),this):(this._nodes[e]=arguments.length>1?t:this._defaultNodeLabelFn(e),this._isCompound&&(this._parent[e]="\0",this._children[e]={},this._children["\0"][e]=!0),this._in[e]={},this._preds[e]={},this._out[e]={},this._sucs[e]={},++this._nodeCount,this)}node(e){return this._nodes[e]}hasNode(e){return e in this._nodes}removeNode(e){if(e in this._nodes){let t=r=>this.removeEdge(this._edgeObjs[r]);delete this._nodes[e],this._isCompound&&(this._removeFromParentsChildList(e),delete this._parent[e],this.children(e).forEach(r=>{this.setParent(r)}),delete this._children[e]),Object.keys(this._in[e]).forEach(t),delete this._in[e],delete this._preds[e],Object.keys(this._out[e]).forEach(t),delete this._out[e],delete this._sucs[e],--this._nodeCount}return this}setParent(e,t){if(!this._isCompound)throw new Error("Cannot set parent in a non-compound graph");if(t===void 0)t="\0";else{t+="";for(let r=t;r!==void 0;r=this.parent(r))if(r===e)throw new Error("Setting "+t+" as parent of "+e+" would create a cycle");this.setNode(t)}return this.setNode(e),this._removeFromParentsChildList(e),this._parent[e]=t,this._children[t][e]=!0,this}parent(e){if(this._isCompound){let t=this._parent[e];if(t!=="\0")return t}}children(e="\0"){if(this._isCompound){let t=this._children[e];if(t)return Object.keys(t)}else{if(e==="\0")return this.nodes();if(this.hasNode(e))return[]}return[]}predecessors(e){let t=this._preds[e];if(t)return Object.keys(t)}successors(e){let t=this._sucs[e];if(t)return Object.keys(t)}neighbors(e){let t=this.predecessors(e);if(t){let r=new Set(t);for(let n of this.successors(e))r.add(n);return Array.from(r.values())}}isLeaf(e){let t;return this.isDirected()?t=this.successors(e):t=this.neighbors(e),t.length===0}filterNodes(e){let t=new this.constructor({directed:this._isDirected,multigraph:this._isMultigraph,compound:this._isCompound});t.setGraph(this.graph()),Object.entries(this._nodes).forEach(([o,s])=>{e(o)&&t.setNode(o,s)}),Object.values(this._edgeObjs).forEach(o=>{t.hasNode(o.v)&&t.hasNode(o.w)&&t.setEdge(o,this.edge(o))});let r={},n=o=>{let s=this.parent(o);return!s||t.hasNode(s)?(r[o]=s??void 0,s??void 0):s in r?r[s]:n(s)};return this._isCompound&&t.nodes().forEach(o=>t.setParent(o,n(o))),t}setDefaultEdgeLabel(e){return typeof e!="function"?this._defaultEdgeLabelFn=()=>e:this._defaultEdgeLabelFn=e,this}edgeCount(){return this._edgeCount}edges(){return Object.values(this._edgeObjs)}setPath(e,t){return e.reduce((r,n)=>(t!==void 0?this.setEdge(r,n,t):this.setEdge(r,n),n)),this}setEdge(e,t,r,n){let o,s,i,a,d=!1;typeof e=="object"&&e!==null&&"v"in e?(o=e.v,s=e.w,i=e.name,arguments.length===2&&(a=t,d=!0)):(o=e,s=t,i=n,arguments.length>2&&(a=r,d=!0)),o=""+o,s=""+s,i!==void 0&&(i=""+i);let l=xe(this._isDirected,o,s,i);if(l in this._edgeLabels)return d&&(this._edgeLabels[l]=a),this;if(i!==void 0&&!this._isMultigraph)throw new Error("Cannot set a named edge when isMultigraph = false");this.setNode(o),this.setNode(s),this._edgeLabels[l]=d?a:this._defaultEdgeLabelFn(o,s,i);let c=Mr(this._isDirected,o,s,i);return o=c.v,s=c.w,Object.freeze(c),this._edgeObjs[l]=c,bt(this._preds[s],o),bt(this._sucs[o],s),this._in[s][l]=c,this._out[o][l]=c,this._edgeCount++,this}edge(e,t,r){let n=arguments.length===1?ze(this._isDirected,e):xe(this._isDirected,e,t,r);return this._edgeLabels[n]}edgeAsObj(e,t,r){let n=arguments.length===1?this.edge(e):this.edge(e,t,r);return typeof n!="object"?{label:n}:n}hasEdge(e,t,r){return(arguments.length===1?ze(this._isDirected,e):xe(this._isDirected,e,t,r))in this._edgeLabels}removeEdge(e,t,r){let n=arguments.length===1?ze(this._isDirected,e):xe(this._isDirected,e,t,r),o=this._edgeObjs[n];if(o){let s=o.v,i=o.w;delete this._edgeLabels[n],delete this._edgeObjs[n],mt(this._preds[i],s),mt(this._sucs[s],i),delete this._in[i][n],delete this._out[s][n],this._edgeCount--}return this}inEdges(e,t){return this.isDirected()?this.filterEdges(this._in[e],e,t):this.nodeEdges(e,t)}outEdges(e,t){return this.isDirected()?this.filterEdges(this._out[e],e,t):this.nodeEdges(e,t)}nodeEdges(e,t){if(e in this._nodes)return this.filterEdges({...this._in[e],...this._out[e]},e,t)}_removeFromParentsChildList(e){delete this._children[this._parent[e]][e]}filterEdges(e,t,r){if(!e)return;let n=Object.values(e);return r?n.filter(o=>o.v===t&&o.w===r||o.v===r&&o.w===t):n}};function bt(e,t){e[t]?e[t]++:e[t]=1}function mt(e,t){e[t]!==void 0&&!--e[t]&&delete e[t]}function xe(e,t,r,n){let o=""+t,s=""+r;if(!e&&o>s){let i=o;o=s,s=i}return o+""+s+""+(n===void 0?"\0":n)}function Mr(e,t,r,n){let o=""+t,s=""+r;if(!e&&o>s){let a=o;o=s,s=a}let i={v:o,w:s};return n&&(i.name=n),i}function ze(e,t){return xe(e,t.v,t.w,t.name)}var jr="4.0.1",qt={};Rt(qt,{read:()=>Pr,write:()=>Lr});function Lr(e){let t={options:{directed:e.isDirected(),multigraph:e.isMultigraph(),compound:e.isCompound()},nodes:Cr(e),edges:Tr(e)},r=e.graph();return r!==void 0&&(t.value=structuredClone(r)),t}function Cr(e){return e.nodes().map(t=>{let r=e.node(t),n=e.parent(t),o={v:t};return r!==void 0&&(o.value=r),n!==void 0&&(o.parent=n),o})}function Tr(e){return e.edges().map(t=>{let r=e.edge(t),n={v:t.v,w:t.w};return t.name!==void 0&&(n.name=t.name),r!==void 0&&(n.value=r),n})}function Pr(e){let t=new J(e.options);return e.value!==void 0&&t.setGraph(e.value),e.nodes.forEach(r=>{t.setNode(r.v,r.value),r.parent&&t.setParent(r.v,r.parent)}),e.edges.forEach(r=>{t.setEdge({v:r.v,w:r.w,name:r.name},r.value)}),t}var nt={};Rt(nt,{CycleException:()=>Te,bellmanFord:()=>Yt,components:()=>Dr,dijkstra:()=>Ce,dijkstraAll:()=>Yr,findCycles:()=>Ar,floydWarshall:()=>Gr,isAcyclic:()=>zr,postorder:()=>Wr,preorder:()=>Hr,prim:()=>Kr,shortestPaths:()=>Zr,tarjan:()=>Vt,topsort:()=>Gt});var Sr=()=>1;function Yt(e,t,r,n){return Fr(e,String(t),r||Sr,n||function(o){return e.outEdges(o)})}function Fr(e,t,r,n){let o={},s,i=0,a=e.nodes(),d=function(u){let g=r(u);o[u.v].distance+ge.key)}has(e){return e in this._keyIndices}priority(e){let t=this._keyIndices[e];if(t!==void 0)return this._arr[t].priority}min(){if(this.size()===0)throw new Error("Queue underflow");return this._arr[0].key}add(e,t){let r=this._keyIndices,n=String(e);if(!(n in r)){let o=this._arr,s=o.length;return r[n]=s,o.push({key:n,priority:t}),this._decrease(s),!0}return!1}removeMin(){this._swap(0,this._arr.length-1);let e=this._arr.pop();return delete this._keyIndices[e.key],this._heapify(0),e.key}decrease(e,t){let r=this._keyIndices[e];if(r===void 0)throw new Error(`Key not found: ${e}`);let n=this._arr[r].priority;if(t>n)throw new Error(`New priority is greater than current priority. Key: ${e} Old: ${n} New: ${t}`);this._arr[r].priority=t,this._decrease(r)}_heapify(e){let t=this._arr,r=2*e,n=r+1,o=e;r>1,!(t[n].priority1;function Ce(e,t,r,n){let o=function(s){return e.outEdges(s)};return qr(e,String(t),r||Rr,n||o)}function qr(e,t,r,n){let o={},s=new At,i,a,d=function(l){let c=l.v!==i?l.v:l.w,u=o[c],g=r(l),v=a.distance+g;if(g<0)throw new Error("dijkstra does not allow negative edge weights. Bad edge: "+l+" Weight: "+g);v0&&(i=s.removeMin(),a=o[i],a.distance!==Number.POSITIVE_INFINITY);)n(i).forEach(d);return o}function Yr(e,t,r){return e.nodes().reduce(function(n,o){return n[o]=Ce(e,o,t,r),n},{})}function Vt(e){let t=0,r=[],n={},o=[];function s(i){let a=n[i]={onStack:!0,lowlink:t,index:t++};if(r.push(i),e.successors(i).forEach(function(d){d in n?n[d].onStack&&(a.lowlink=Math.min(a.lowlink,n[d].index)):(s(d),a.lowlink=Math.min(a.lowlink,n[d].lowlink))}),a.lowlink===a.index){let d=[],l;do l=r.pop(),n[l].onStack=!1,d.push(l);while(i!==l);o.push(d)}}return e.nodes().forEach(function(i){i in n||s(i)}),o}function Ar(e){return Vt(e).filter(function(t){return t.length>1||t.length===1&&e.hasEdge(t[0],t[0])})}var Vr=()=>1;function Gr(e,t,r){return Br(e,t||Vr,r||function(n){return e.outEdges(n)})}function Br(e,t,r){let n={},o=e.nodes();return o.forEach(function(s){n[s]={},n[s][s]={distance:0,predecessor:""},o.forEach(function(i){s!==i&&(n[s][i]={distance:Number.POSITIVE_INFINITY,predecessor:""})}),r(s).forEach(function(i){let a=i.v===s?i.w:i.v,d=t(i);n[s][a]={distance:d,predecessor:s}})}),o.forEach(function(s){let i=n[s];o.forEach(function(a){let d=n[a];o.forEach(function(l){let c=d[s],u=i[l],g=d[l],v=c.distance+u.distance;v{var d;return(d=e.isDirected()?e.successors(a):e.neighbors(a))!=null?d:[]}),i={};return t.forEach(function(a){if(!e.hasNode(a))throw new Error("Graph does not have node: "+a);o=Bt(e,a,r==="post",i,s,n,o)}),o}function Bt(e,t,r,n,o,s,i){return t in n||(n[t]=!0,r||(i=s(i,t)),o(t).forEach(function(a){i=Bt(e,a,r,n,o,s,i)}),r&&(i=s(i,t))),i}function zt(e,t,r){return Xr(e,t,r,function(n,o){return n.push(o),n},[])}function Wr(e,t){return zt(e,t,"post")}function Hr(e,t){return zt(e,t,"pre")}function Kr(e,t){let r=new J,n={},o=new At,s;function i(d){let l=d.v===s?d.w:d.v,c=o.priority(l);if(c!==void 0){let u=t(d);u0;){if(s=o.removeMin(),s in n)r.setEdge(s,n[s]);else{if(a)throw new Error("Input graph is not connected: "+e);a=!0}e.nodeEdges(s).forEach(i)}return r}function Zr(e,t,r,n){return Ur(e,t,r,n??(o=>{let s=e.outEdges(o);return s??[]}))}function Ur(e,t,r,n){if(r===void 0)return Ce(e,t,r,n);let o=!1,s=e.nodes();for(let i=0;it.setNode(r,e.node(r))),e.edges().forEach(r=>{let n=t.edge(r.v,r.w)||{weight:0,minlen:1},o=e.edge(r);t.setEdge(r.v,r.w,{weight:n.weight+o.weight,minlen:Math.max(n.minlen,o.minlen)})}),t}function Xt(e){let t=new J({multigraph:e.isMultigraph()}).setGraph(e.graph());return e.nodes().forEach(r=>{e.children(r).length||t.setNode(r,e.node(r))}),e.edges().forEach(r=>{t.setEdge(r,e.edge(r))}),t}function _t(e,t){let r=e.x,n=e.y,o=t.x-r,s=t.y-n,i=e.width/2,a=e.height/2;if(!o&&!s)throw new Error("Not possible to find intersection inside of the rectangle");let d,l;return Math.abs(s)*i>Math.abs(o)*a?(s<0&&(a=-a),d=a*o/s,l=a):(o<0&&(i=-i),d=i,l=i*s/o),{x:r+d,y:n+l}}function De(e){let t=Oe(Ht(e)+1).map(()=>[]);return e.nodes().forEach(r=>{let n=e.node(r),o=n.rank;o!==void 0&&(t[o]||(t[o]=[]),t[o][n.order]=r)}),t}function $r(e){let t=e.nodes().map(n=>{let o=e.node(n).rank;return o===void 0?Number.MAX_VALUE:o}),r=re(Math.min,t);e.nodes().forEach(n=>{let o=e.node(n);Object.hasOwn(o,"rank")&&(o.rank-=r)})}function Jr(e){let t=e.nodes().map(i=>e.node(i).rank).filter(i=>i!==void 0),r=re(Math.min,t),n=[];e.nodes().forEach(i=>{let a=e.node(i).rank-r;n[a]||(n[a]=[]),n[a].push(i)});let o=0,s=e.graph().nodeRankFactor;Array.from(n).forEach((i,a)=>{i===void 0&&a%s!==0?--o:i!==void 0&&o&&i.forEach(d=>e.node(d).rank+=o)})}function wt(e,t,r,n){let o={width:0,height:0};return arguments.length>=4&&(o.rank=r,o.order=n),_e(e,"border",o,t)}function en(e,t=Wt){let r=[];for(let n=0;nWt){let r=en(t);return e(...r.map(n=>e(...n)))}else return e(...t)}function Ht(e){let t=e.nodes().map(r=>{let n=e.node(r).rank;return n===void 0?Number.MIN_VALUE:n});return re(Math.max,t)}function tn(e,t){let r={lhs:[],rhs:[]};return e.forEach(n=>{t(n)?r.lhs.push(n):r.rhs.push(n)}),r}function rn(e,t){let r=Date.now();try{return t()}finally{console.log(e+" time: "+(Date.now()-r)+"ms")}}function nn(e,t){return t()}var on=0;function ot(e){let t=++on;return e+(""+t)}function Oe(e,t,r=1){t==null&&(t=e,e=0);let n=s=>stn[t]:r=t,Object.entries(e).reduce((n,[o,s])=>(n[o]=r(s,o),n),{})}function sn(e,t){return e.reduce((r,n,o)=>(r[n]=t[o],r),{})}var qe="\0",an=class{constructor(){Nr(this,"_sentinel");let e={};e._next=e._prev=e,this._sentinel=e}dequeue(){let e=this._sentinel,t=e._prev;if(t!==e)return Et(t),t}enqueue(e){let t=this._sentinel;e._prev&&e._next&&Et(e),e._next=t._next,t._next._prev=e,t._next=e,e._prev=t}toString(){let e=[],t=this._sentinel,r=t._prev;for(;r!==t;)e.push(JSON.stringify(r,ln)),r=r._prev;return"["+e.join(", ")+"]"}};function Et(e){e._prev._next=e._next,e._next._prev=e._prev,delete e._next,delete e._prev}function ln(e,t){if(e!=="_next"&&e!=="_prev")return t}var dn=an,cn=()=>1;function un(e,t){if(e.nodeCount()<=1)return[];let r=fn(e,t||cn);return hn(r.graph,r.buckets,r.zeroIdx).flatMap(n=>e.outEdges(n.v,n.w)||[])}function hn(e,t,r){var n;let o=[],s=t[t.length-1],i=t[0],a;for(;e.nodeCount();){for(;a=i.dequeue();)Xe(e,t,r,a);for(;a=s.dequeue();)Xe(e,t,r,a);if(e.nodeCount()){for(let d=t.length-2;d>0;--d)if(a=(n=t[d])==null?void 0:n.dequeue(),a){o=o.concat(Xe(e,t,r,a,!0)||[]);break}}}return o}function Xe(e,t,r,n,o){let s=[],i=o?s:void 0;return(e.inEdges(n.v)||[]).forEach(a=>{let d=e.edge(a),l=e.node(a.v);o&&s.push({v:a.v,w:a.w}),l.out-=d,Je(t,r,l)}),(e.outEdges(n.v)||[]).forEach(a=>{let d=e.edge(a),l=a.w,c=e.node(l);c.in-=d,Je(t,r,c)}),e.removeNode(n.v),i}function fn(e,t){let r=new J,n=0,o=0;e.nodes().forEach(a=>{r.setNode(a,{v:a,in:0,out:0})}),e.edges().forEach(a=>{let d=r.edge(a.v,a.w)||0,l=t(a),c=d+l;r.setEdge(a.v,a.w,c);let u=r.node(a.v),g=r.node(a.w);o=Math.max(o,u.out+=l),n=Math.max(n,g.in+=l)});let s=gn(o+n+3).map(()=>new dn),i=n+1;return r.nodes().forEach(a=>{Je(s,i,r.node(a))}),{graph:r,buckets:s,zeroIdx:i}}function Je(e,t,r){var n,o,s;r.out?r.in?(s=e[r.out-r.in+t])==null||s.enqueue(r):(o=e[e.length-1])==null||o.enqueue(r):(n=e[0])==null||n.enqueue(r)}function gn(e){let t=[];for(let r=0;r{let n=e.edge(r);e.removeEdge(r),n.forwardName=r.name,n.reversed=!0,e.setEdge(r.w,r.v,n,ot("rev"))});function t(r){return n=>r.edge(n).weight}}function pn(e){let t=[],r={},n={};function o(s){Object.hasOwn(n,s)||(n[s]=!0,r[s]=!0,e.outEdges(s).forEach(i=>{Object.hasOwn(r,i.w)?t.push(i):o(i.w)}),delete r[s])}return e.nodes().forEach(o),t}function bn(e){e.edges().forEach(t=>{let r=e.edge(t);if(r.reversed){e.removeEdge(t);let n=r.forwardName;delete r.reversed,delete r.forwardName,e.setEdge(t.w,t.v,r,n)}})}function mn(e){e.graph().dummyChains=[],e.edges().forEach(t=>_n(e,t))}function _n(e,t){let r=t.v,n=e.node(r).rank,o=t.w,s=e.node(o).rank,i=t.name,a=e.edge(t),d=a.labelRank;if(s===n+1)return;e.removeEdge(t);let l,c,u;for(u=0,++n;n{let r=e.node(t),n=r.edgeLabel,o;for(e.setEdge(r.edgeObj,n);r.dummy;)o=e.successors(t)[0],e.removeNode(t),n.points.push({x:r.x,y:r.y}),r.dummy==="edge-label"&&(n.x=r.x,n.y=r.y,n.width=r.width,n.height=r.height),t=o,r=e.node(t)})}function st(e){let t={};function r(n){let o=e.node(n);if(Object.hasOwn(t,n))return o.rank;t[n]=!0;let s=e.outEdges(n),i=s?s.map(d=>d==null?Number.POSITIVE_INFINITY:r(d.w)-e.edge(d).minlen):[],a=re(Math.min,i);return a===Number.POSITIVE_INFINITY&&(a=0),o.rank=a}e.sources().forEach(r)}function me(e,t){return e.node(t.w).rank-e.node(t.v).rank-e.edge(t).minlen}var Kt=En;function En(e){let t=new J({directed:!1}),r=e.nodes();if(r.length===0)throw new Error("Graph must have at least one node");let n=r[0],o=e.nodeCount();t.setNode(n,{});let s,i;for(;yn(t,e){let i=s.v,a=n===i?s.w:i;!e.hasNode(a)&&!me(t,s)&&(e.setNode(a,{}),e.setEdge(n,a,{}),r(a))})}return e.nodes().forEach(r),e.nodeCount()}function kn(e,t){return t.edges().reduce((r,n)=>{let o=Number.POSITIVE_INFINITY;return e.hasNode(n.v)!==e.hasNode(n.w)&&(o=me(t,n)),ot.node(n).rank+=r)}var{preorder:On,postorder:Nn}=nt,In=ge;ge.initLowLimValues=at;ge.initCutValues=it;ge.calcCutValue=Zt;ge.leaveEdge=Qt;ge.enterEdge=$t;ge.exchangeEdges=Jt;function ge(e){e=Qr(e),st(e);let t=Kt(e);at(t),it(t,e);let r,n;for(;r=Qt(t);)n=$t(t,e,r),Jt(t,e,r,n)}function it(e,t){let r=Nn(e,e.nodes());r=r.slice(0,r.length-1),r.forEach(n=>Mn(e,t,n))}function Mn(e,t,r){let n=e.node(r).parent,o=e.edge(r,n);o.cutvalue=Zt(e,t,r)}function Zt(e,t,r){let n=e.node(r).parent,o=!0,s=t.edge(r,n),i=0;s||(o=!1,s=t.edge(n,r)),i=s.weight;let a=t.nodeEdges(r);return a&&a.forEach(d=>{let l=d.v===r,c=l?d.w:d.v;if(c!==n){let u=l===o,g=t.edge(d).weight;if(i+=u?g:-g,Ln(e,r,c)){let v=e.edge(r,c).cutvalue;i+=u?-v:v}}}),i}function at(e,t){arguments.length<2&&(t=e.nodes()[0]),Ut(e,{},1,t)}function Ut(e,t,r,n,o){let s=r,i=e.node(n);t[n]=!0;let a=e.neighbors(n);return a&&a.forEach(d=>{Object.hasOwn(t,d)||(r=Ut(e,t,r,d,n))}),i.low=s,i.lim=r++,o?i.parent=o:delete i.parent,r}function Qt(e){return e.edges().find(t=>e.edge(t).cutvalue<0)}function $t(e,t,r){let n=r.v,o=r.w;t.hasEdge(n,o)||(n=r.w,o=r.v);let s=e.node(n),i=e.node(o),a=s,d=!1;return s.lim>i.lim&&(a=i,d=!0),t.edges().filter(l=>d===yt(e,e.node(l.v),a)&&d!==yt(e,e.node(l.w),a)).reduce((l,c)=>me(t,c)!e.node(o).parent);if(!r)return;let n=On(e,[r]);n=n.slice(1),n.forEach(o=>{let s=e.node(o).parent,i=t.edge(o,s),a=!1;i||(i=t.edge(s,o),a=!0),t.node(o).rank=t.node(s).rank+(a?i.minlen:-i.minlen)})}function Ln(e,t,r){return e.hasEdge(t,r)}function yt(e,t,r){return r.low<=t.lim&&t.lim<=r.lim}var Cn=Tn;function Tn(e){let t=e.graph().ranker;if(typeof t=="function")return t(e);switch(t){case"network-simplex":kt(e);break;case"tight-tree":Sn(e);break;case"longest-path":Pn(e);break;case"none":break;default:kt(e)}}var Pn=st;function Sn(e){st(e),Kt(e)}function kt(e){In(e)}var Fn=Dn;function Dn(e){let t=qn(e);e.graph().dummyChains.forEach(r=>{let n=e.node(r),o=n.edgeObj,s=Rn(e,t,o.v,o.w),i=s.path,a=s.lca,d=0,l=i[d],c=!0;for(;r!==o.w;){if(n=e.node(r),c){for(;(l=i[d])!==a&&e.node(l).maxRanki||a>t[d].lim));let l=d,c=n;for(;(c=e.parent(c))!==l;)s.push(c);return{path:o.concat(s.reverse()),lca:l}}function qn(e){let t={},r=0;function n(o){let s=r;e.children(o).forEach(n),t[o]={low:s,lim:r++}}return e.children(qe).forEach(n),t}function Yn(e){let t=_e(e,"root",{},"_root"),r=An(e),n=Object.values(r),o=re(Math.max,n)-1,s=2*o+1;e.graph().nestingRoot=t,e.edges().forEach(a=>e.edge(a).minlen*=s);let i=Vn(e)+1;e.children(qe).forEach(a=>er(e,t,s,i,o,r,a)),e.graph().nodeRankFactor=s}function er(e,t,r,n,o,s,i){var a;let d=e.children(i);if(!d.length){i!==t&&e.setEdge(t,i,{weight:0,minlen:r});return}let l=wt(e,"_bt"),c=wt(e,"_bb"),u=e.node(i);e.setParent(l,i),u.borderTop=l,e.setParent(c,i),u.borderBottom=c,d.forEach(g=>{var v;er(e,t,r,n,o,s,g);let p=e.node(g),b=p.borderTop?p.borderTop:g,k=p.borderBottom?p.borderBottom:g,N=p.borderTop?n:2*n,q=b!==k?1:o-((v=s[i])!=null?v:0)+1;e.setEdge(l,b,{weight:N,minlen:q,nestingEdge:!0}),e.setEdge(k,c,{weight:N,minlen:q,nestingEdge:!0})}),e.parent(i)||e.setEdge(t,l,{weight:0,minlen:o+((a=s[i])!=null?a:0)})}function An(e){let t={};function r(n,o){let s=e.children(n);s&&s.length&&s.forEach(i=>r(i,o+1)),t[n]=o}return e.children(qe).forEach(n=>r(n,1)),t}function Vn(e){return e.edges().reduce((t,r)=>t+e.edge(r).weight,0)}function Gn(e){let t=e.graph();e.removeNode(t.nestingRoot),delete t.nestingRoot,e.edges().forEach(r=>{e.edge(r).nestingEdge&&e.removeEdge(r)})}var Bn=zn;function zn(e){function t(r){let n=e.children(r),o=e.node(r);if(n.length&&n.forEach(t),Object.hasOwn(o,"minRank")){o.borderLeft=[],o.borderRight=[];for(let s=o.minRank,i=o.maxRank+1;sOt(e.node(t))),e.edges().forEach(t=>Ot(e.edge(t)))}function Ot(e){let t=e.width;e.width=e.height,e.height=t}function Hn(e){e.nodes().forEach(t=>We(e.node(t))),e.edges().forEach(t=>{var r;let n=e.edge(t);(r=n.points)==null||r.forEach(We),Object.hasOwn(n,"y")&&We(n)})}function We(e){e.y=-e.y}function Kn(e){e.nodes().forEach(t=>He(e.node(t))),e.edges().forEach(t=>{var r;let n=e.edge(t);(r=n.points)==null||r.forEach(He),Object.hasOwn(n,"x")&&He(n)})}function He(e){let t=e.x;e.x=e.y,e.y=t}function Zn(e){let t={},r=e.nodes().filter(a=>!e.children(a).length),n=r.map(a=>e.node(a).rank),o=re(Math.max,n),s=Oe(o+1).map(()=>[]);function i(a){if(t[a])return;t[a]=!0;let d=e.node(a);s[d.rank].push(a);let l=e.successors(a);l&&l.forEach(i)}return r.sort((a,d)=>e.node(a).rank-e.node(d).rank).forEach(i),s}function Un(e,t){let r=0;for(let n=1;nc)),o=t.flatMap(l=>{let c=e.outEdges(l);return c?c.map(u=>({pos:n[u.w],weight:e.edge(u).weight})).sort((u,g)=>u.pos-g.pos):[]}),s=1;for(;s{let c=l.pos+s;a[c]+=l.weight;let u=0;for(;c>0;)c%2&&(u+=a[c+1]),c=c-1>>1,a[c]+=l.weight;d+=l.weight*u}),d}function $n(e,t=[]){return t.map(r=>{let n=e.inEdges(r);if(!n||!n.length)return{v:r};{let o=n.reduce((s,i)=>{let a=e.edge(i),d=e.node(i.v);return{sum:s.sum+a.weight*d.order,weight:s.weight+a.weight}},{sum:0,weight:0});return{v:r,barycenter:o.sum/o.weight,weight:o.weight}}})}function Jn(e,t){let r={};e.forEach((o,s)=>{let i={indegree:0,in:[],out:[],vs:[o.v],i:s};o.barycenter!==void 0&&(i.barycenter=o.barycenter,i.weight=o.weight),r[o.v]=i}),t.edges().forEach(o=>{let s=r[o.v],i=r[o.w];s!==void 0&&i!==void 0&&(i.indegree++,s.out.push(i))});let n=Object.values(r).filter(o=>!o.indegree);return eo(n)}function eo(e){let t=[];function r(o){return s=>{s.merged||(s.barycenter===void 0||o.barycenter===void 0||s.barycenter>=o.barycenter)&&to(o,s)}}function n(o){return s=>{s.in.push(o),--s.indegree===0&&e.push(s)}}for(;e.length;){let o=e.pop();t.push(o),o.in.reverse().forEach(r(o)),o.out.forEach(n(o))}return t.filter(o=>!o.merged).map(o=>Pe(o,["vs","i","barycenter","weight"]))}function to(e,t){let r=0,n=0;e.weight&&(r+=e.barycenter*e.weight,n+=e.weight),t.weight&&(r+=t.barycenter*t.weight,n+=t.weight),e.vs=t.vs.concat(e.vs),e.barycenter=r/n,e.weight=n,e.i=Math.min(t.i,e.i),t.merged=!0}function ro(e,t){let r=tn(e,c=>Object.hasOwn(c,"barycenter")),n=r.lhs,o=r.rhs.sort((c,u)=>u.i-c.i),s=[],i=0,a=0,d=0;n.sort(no(!!t)),d=Nt(s,o,d),n.forEach(c=>{d+=c.vs.length,s.push(c.vs),i+=c.barycenter*c.weight,a+=c.weight,d=Nt(s,o,d)});let l={vs:s.flat(1)};return a&&(l.barycenter=i/a,l.weight=a),l}function Nt(e,t,r){let n;for(;t.length&&(n=t[t.length-1]).i<=r;)t.pop(),e.push(n.vs),r++;return r}function no(e){return(t,r)=>t.barycenterr.barycenter?1:e?r.i-t.i:t.i-r.i}function rr(e,t,r,n){let o=e.children(t),s=e.node(t),i=s?s.borderLeft:void 0,a=s?s.borderRight:void 0,d={};i&&(o=o.filter(g=>g!==i&&g!==a));let l=$n(e,o);l.forEach(g=>{if(e.children(g.v).length){let v=rr(e,g.v,r,n);d[g.v]=v,Object.hasOwn(v,"barycenter")&&so(g,v)}});let c=Jn(l,r);oo(c,d);let u=ro(c,n);if(i&&a){u.vs=[i,u.vs,a].flat(1);let g=e.predecessors(i);if(g&&g.length){let v=e.node(g[0]),p=e.predecessors(a),b=e.node(p[0]);Object.hasOwn(u,"barycenter")||(u.barycenter=0,u.weight=0),u.barycenter=(u.barycenter*u.weight+v.order+b.order)/(u.weight+2),u.weight+=2}}return u}function oo(e,t){e.forEach(r=>{r.vs=r.vs.flatMap(n=>t[n]?t[n].vs:n)})}function so(e,t){e.barycenter!==void 0?(e.barycenter=(e.barycenter*e.weight+t.barycenter*t.weight)/(e.weight+t.weight),e.weight+=t.weight):(e.barycenter=t.barycenter,e.weight=t.weight)}function io(e,t,r,n){n||(n=e.nodes());let o=ao(e),s=new J({compound:!0}).setGraph({root:o}).setDefaultNodeLabel(i=>e.node(i));return n.forEach(i=>{let a=e.node(i),d=e.parent(i);if(a.rank===t||a.minRank<=t&&t<=a.maxRank){s.setNode(i),s.setParent(i,d||o);let l=e[r](i);l&&l.forEach(c=>{let u=c.v===i?c.w:c.v,g=s.edge(u,i),v=g!==void 0?g.weight:0;s.setEdge(u,i,{weight:e.edge(c).weight+v})}),Object.hasOwn(a,"minRank")&&s.setNode(i,{borderLeft:a.borderLeft[t],borderRight:a.borderRight[t]})}}),s}function ao(e){let t;for(;e.hasNode(t=ot("_root")););return t}function lo(e,t,r){let n={},o;r.forEach(s=>{let i=e.parent(s),a,d;for(;i;){if(a=e.parent(i),a?(d=n[a],n[a]=i):(d=o,o=i),d&&d!==i){t.setEdge(d,i);return}i=a}})}function nr(e,t={}){if(typeof t.customOrder=="function"){t.customOrder(e,nr);return}let r=Ht(e),n=It(e,Oe(1,r+1),"inEdges"),o=It(e,Oe(r-1,-1,-1),"outEdges"),s=Zn(e);if(Mt(e,s),t.disableOptimalOrderHeuristic)return;let i=Number.POSITIVE_INFINITY,a,d=t.constraints||[];for(let l=0,c=0;c<4;++l,++c){co(l%2?n:o,l%4>=2,d),s=De(e);let u=Un(e,s);u{n.has(s)||n.set(s,[]),n.get(s).push(i)};for(let s of e.nodes()){let i=e.node(s);if(typeof i.rank=="number"&&o(i.rank,s),typeof i.minRank=="number"&&typeof i.maxRank=="number")for(let a=i.minRank;a<=i.maxRank;a++)a!==i.rank&&o(a,s)}return t.map(function(s){return io(e,s,r,n.get(s)||[])})}function co(e,t,r){let n=new J;e.forEach(function(o){r.forEach(a=>n.setEdge(a.left,a.right));let s=o.graph().root,i=rr(o,s,n,t);i.vs.forEach((a,d)=>o.node(a).order=d),lo(o,n,i.vs)})}function Mt(e,t){Object.values(t).forEach(r=>r.forEach((n,o)=>e.node(n).order=o))}function uo(e,t){let r={};function n(o,s){let i=0,a=0,d=o.length,l=s[s.length-1];return s.forEach((c,u)=>{let g=fo(e,c),v=g?e.node(g).order:d;(g||c===l)&&(s.slice(a,u+1).forEach(p=>{let b=e.predecessors(p);b&&b.forEach(k=>{let N=e.node(k),q=N.order;(q{let u=s[c];if(u!==void 0&&e.node(u).dummy){let g=e.predecessors(u);g&&g.forEach(v=>{if(v===void 0)return;let p=e.node(v);p.dummy&&(p.orderl)&&or(r,v,u)})}})}function o(s,i){let a=-1,d=-1,l=0;return i.forEach((c,u)=>{if(e.node(c).dummy==="border"){let g=e.predecessors(c);if(g&&g.length){let v=g[0];if(v===void 0)return;d=e.node(v).order,n(i,l,u,a,d),l=u,a=d}}n(i,l,i.length,d,s.length)}),i}return t.length&&t.reduce(o),r}function fo(e,t){if(e.node(t).dummy){let r=e.predecessors(t);if(r)return r.find(n=>e.node(n).dummy)}}function or(e,t,r){if(t>r){let o=t;t=r,r=o}let n=e[t];n||(e[t]=n={}),n[r]=!0}function go(e,t,r){if(t>r){let o=t;t=r,r=o}let n=e[t];return n!==void 0&&Object.hasOwn(n,r)}function vo(e,t,r,n){let o={},s={},i={};return t.forEach(a=>{a.forEach((d,l)=>{o[d]=d,s[d]=d,i[d]=l})}),t.forEach(a=>{let d=-1;a.forEach(l=>{let c=n(l);if(c&&c.length){let u=c.sort((v,p)=>{let b=i[v],k=i[p];return(b!==void 0?b:0)-(k!==void 0?k:0)}),g=(u.length-1)/2;for(let v=Math.floor(g),p=Math.ceil(g);v<=p;++v){let b=u[v];if(b===void 0)continue;let k=i[b];if(k!==void 0&&s[l]===l&&d{var N;let q=(N=s[k.v])!=null?N:0,S=i.edge(k);return Math.max(b,q+(S!==void 0?S:0))},0):s[v]=0}function c(v){let p=i.outEdges(v),b=Number.POSITIVE_INFINITY;p&&(b=p.reduce((N,q)=>{let S=s[q.w],ne=i.edge(q);return Math.min(N,(S!==void 0?S:0)-(ne!==void 0?ne:0))},Number.POSITIVE_INFINITY));let k=e.node(v);b!==Number.POSITIVE_INFINITY&&k.borderType!==a&&(s[v]=Math.max(s[v]!==void 0?s[v]:0,b))}function u(v){return i.predecessors(v)||[]}function g(v){return i.successors(v)||[]}return d(l,u),d(c,g),Object.keys(n).forEach(v=>{var p;let b=r[v];b!==void 0&&(s[v]=(p=s[b])!=null?p:0)}),s}function bo(e,t,r,n){let o=new J,s=e.graph(),i=yo(s.nodesep,s.edgesep,n);return t.forEach(a=>{let d;a.forEach(l=>{let c=r[l];if(c!==void 0){if(o.setNode(c),d!==void 0){let u=r[d];if(u!==void 0){let g=o.edge(u,c);o.setEdge(u,c,Math.max(i(e,l,d),g||0))}}d=l}})}),o}function mo(e,t){return Object.values(t).reduce((r,n)=>{let o=Number.NEGATIVE_INFINITY,s=Number.POSITIVE_INFINITY;Object.entries(n).forEach(([a,d])=>{let l=ko(e,a)/2;o=Math.max(d+l,o),s=Math.min(d-l,s)});let i=o-s;return i{["l","r"].forEach(i=>{let a=s+i,d=e[a];if(!d||d===t)return;let l=Object.values(d),c=n-re(Math.min,l);i!=="l"&&(c=o-re(Math.max,l)),c&&(e[a]=Re(d,u=>u+c))})})}function wo(e,t=void 0){let r=e.ul;return r?Re(r,(n,o)=>{var s,i;if(t){let d=t.toLowerCase(),l=e[d];if(l&&l[o]!==void 0)return l[o]}let a=Object.values(e).map(d=>{let l=d[o];return l!==void 0?l:0}).sort((d,l)=>d-l);return(((s=a[1])!=null?s:0)+((i=a[2])!=null?i:0))/2}):{}}function Eo(e){let t=De(e),r=Object.assign(uo(e,t),ho(e,t)),n={},o;["u","d"].forEach(i=>{o=i==="u"?t:Object.values(t).reverse(),["l","r"].forEach(a=>{a==="r"&&(o=o.map(c=>Object.values(c).reverse()));let d=vo(e,o,r,c=>(i==="u"?e.predecessors(c):e.successors(c))||[]),l=po(e,o,d.root,d.align,a==="r");a==="r"&&(l=Re(l,c=>-c)),n[i+a]=l})});let s=mo(e,n);return _o(n,s),wo(n,e.graph().align)}function yo(e,t,r){return(n,o,s)=>{let i=n.node(o),a=n.node(s),d=0,l;if(d+=i.width/2,Object.hasOwn(i,"labelpos"))switch(i.labelpos.toLowerCase()){case"l":l=-i.width/2;break;case"r":l=i.width/2;break}if(l&&(d+=r?l:-l),l=void 0,d+=(i.dummy?t:e)/2,d+=(a.dummy?t:e)/2,d+=a.width/2,Object.hasOwn(a,"labelpos"))switch(a.labelpos.toLowerCase()){case"l":l=a.width/2;break;case"r":l=-a.width/2;break}return l&&(d+=r?l:-l),d}}function ko(e,t){return e.node(t).width}function xo(e){e=Xt(e),Oo(e),Object.entries(Eo(e)).forEach(([t,r])=>e.node(t).x=r)}function Oo(e){let t=De(e),r=e.graph(),n=r.ranksep,o=r.rankalign,s=0;t.forEach(i=>{let a=i.reduce((d,l)=>{var c;let u=(c=e.node(l).height)!=null?c:0;return d>u?d:u},0);i.forEach(d=>{let l=e.node(d);o==="top"?l.y=s+l.height/2:o==="bottom"?l.y=s+a-l.height/2:l.y=s+a/2}),s+=a+n})}function No(e,t={}){let r=t.debugTiming?rn:nn;return r("layout",()=>{let n=r(" buildLayoutGraph",()=>Do(e));return r(" runLayout",()=>Io(n,r,t)),r(" updateInputGraph",()=>Mo(e,n)),n})}function Io(e,t,r){t(" makeSpaceForEdgeLabels",()=>Ro(e)),t(" removeSelfEdges",()=>Wo(e)),t(" acyclic",()=>vn(e)),t(" nestingGraph.run",()=>Yn(e)),t(" rank",()=>Cn(Xt(e))),t(" injectEdgeLabelProxies",()=>qo(e)),t(" removeEmptyRanks",()=>Jr(e)),t(" nestingGraph.cleanup",()=>Gn(e)),t(" normalizeRanks",()=>$r(e)),t(" assignRankMinMax",()=>Yo(e)),t(" removeEdgeLabelProxies",()=>Ao(e)),t(" normalize.run",()=>mn(e)),t(" parentDummyChains",()=>Fn(e)),t(" addBorderSegments",()=>Bn(e)),t(" order",()=>nr(e,r)),t(" insertSelfEdges",()=>Ho(e)),t(" adjustCoordinateSystem",()=>Xn(e)),t(" position",()=>xo(e)),t(" positionSelfEdges",()=>Ko(e)),t(" removeBorderNodes",()=>Xo(e)),t(" normalize.undo",()=>wn(e)),t(" fixupEdgeLabelCoords",()=>Bo(e)),t(" undoCoordinateSystem",()=>Wn(e)),t(" translateGraph",()=>Vo(e)),t(" assignNodeIntersects",()=>Go(e)),t(" reversePoints",()=>zo(e)),t(" acyclic.undo",()=>bn(e))}function Mo(e,t){e.nodes().forEach(r=>{let n=e.node(r),o=t.node(r);n&&(n.x=o.x,n.y=o.y,n.order=o.order,n.rank=o.rank,t.children(r).length&&(n.width=o.width,n.height=o.height))}),e.edges().forEach(r=>{let n=e.edge(r),o=t.edge(r);n.points=o.points,Object.hasOwn(o,"x")&&(n.x=o.x,n.y=o.y)}),e.graph().width=t.graph().width,e.graph().height=t.graph().height}var jo=["nodesep","edgesep","ranksep","marginx","marginy"],Lo={ranksep:50,edgesep:20,nodesep:50,rankdir:"TB",rankalign:"center"},Co=["acyclicer","ranker","rankdir","align","rankalign"],To=["width","height","rank"],jt={width:0,height:0},Po=["minlen","weight","width","height","labeloffset"],So={minlen:1,weight:1,width:0,height:0,labeloffset:10,labelpos:"r"},Fo=["labelpos"];function Do(e){let t=new J({multigraph:!0,compound:!0}),r=Ze(e.graph());return t.setGraph(Object.assign({},Lo,Ke(r,jo),Pe(r,Co))),e.nodes().forEach(n=>{let o=Ze(e.node(n)),s=Ke(o,To);Object.keys(jt).forEach(a=>{s[a]===void 0&&(s[a]=jt[a])}),t.setNode(n,s);let i=e.parent(n);i!==void 0&&t.setParent(n,i)}),e.edges().forEach(n=>{let o=Ze(e.edge(n));t.setEdge(n,Object.assign({},So,Ke(o,Po),Pe(o,Fo)))}),t}function Ro(e){let t=e.graph();t.ranksep/=2,e.edges().forEach(r=>{let n=e.edge(r);n.minlen*=2,n.labelpos.toLowerCase()!=="c"&&(t.rankdir==="TB"||t.rankdir==="BT"?n.width+=n.labeloffset:n.height+=n.labeloffset)})}function qo(e){e.edges().forEach(t=>{let r=e.edge(t);if(r.width&&r.height){let n=e.node(t.v),o={rank:(e.node(t.w).rank-n.rank)/2+n.rank,e:t};_e(e,"edge-proxy",o,"_ep")}})}function Yo(e){let t=0;e.nodes().forEach(r=>{let n=e.node(r);n.borderTop&&(n.minRank=e.node(n.borderTop).rank,n.maxRank=e.node(n.borderBottom).rank,t=Math.max(t,n.maxRank))}),e.graph().maxRank=t}function Ao(e){e.nodes().forEach(t=>{let r=e.node(t);if(r.dummy==="edge-proxy"){let n=r;e.edge(n.e).labelRank=r.rank,e.removeNode(t)}})}function Vo(e){let t=Number.POSITIVE_INFINITY,r=0,n=Number.POSITIVE_INFINITY,o=0,s=e.graph(),i=s.marginx||0,a=s.marginy||0;function d(l){let c=l.x,u=l.y,g=l.width,v=l.height;t=Math.min(t,c-g/2),r=Math.max(r,c+g/2),n=Math.min(n,u-v/2),o=Math.max(o,u+v/2)}e.nodes().forEach(l=>d(e.node(l))),e.edges().forEach(l=>{let c=e.edge(l);Object.hasOwn(c,"x")&&d(c)}),t-=i,n-=a,e.nodes().forEach(l=>{let c=e.node(l);c.x-=t,c.y-=n}),e.edges().forEach(l=>{let c=e.edge(l);c.points.forEach(u=>{u.x-=t,u.y-=n}),Object.hasOwn(c,"x")&&(c.x-=t),Object.hasOwn(c,"y")&&(c.y-=n)}),s.width=r-t+i,s.height=o-n+a}function Go(e){e.edges().forEach(t=>{let r=e.edge(t),n=e.node(t.v),o=e.node(t.w),s,i;r.points?(s=r.points[0],i=r.points[r.points.length-1]):(r.points=[],s=o,i=n),r.points.unshift(_t(n,s)),r.points.push(_t(o,i))})}function Bo(e){e.edges().forEach(t=>{let r=e.edge(t);if(Object.hasOwn(r,"x"))switch((r.labelpos==="l"||r.labelpos==="r")&&(r.width-=r.labeloffset),r.labelpos){case"l":r.x-=r.width/2+r.labeloffset;break;case"r":r.x+=r.width/2+r.labeloffset;break}})}function zo(e){e.edges().forEach(t=>{let r=e.edge(t);r.reversed&&r.points.reverse()})}function Xo(e){e.nodes().forEach(t=>{if(e.children(t).length){let r=e.node(t),n=e.node(r.borderTop),o=e.node(r.borderBottom),s=e.node(r.borderLeft[r.borderLeft.length-1]),i=e.node(r.borderRight[r.borderRight.length-1]);r.width=Math.abs(i.x-s.x),r.height=Math.abs(o.y-n.y),r.x=s.x+r.width/2,r.y=n.y+r.height/2}}),e.nodes().forEach(t=>{e.node(t).dummy==="border"&&e.removeNode(t)})}function Wo(e){e.edges().forEach(t=>{if(t.v===t.w){let r=e.node(t.v);r.selfEdges||(r.selfEdges=[]),r.selfEdges.push({e:t,label:e.edge(t)}),e.removeEdge(t)}})}function Ho(e){De(e).forEach(t=>{let r=0;t.forEach((n,o)=>{let s=e.node(n);s.order=o+r,(s.selfEdges||[]).forEach(i=>{_e(e,"selfedge",{width:i.label.width,height:i.label.height,rank:s.rank,order:o+ ++r,e:i.e,label:i.label},"_se")}),delete s.selfEdges})})}function Ko(e){e.nodes().forEach(t=>{let r=e.node(t);if(r.dummy==="selfedge"){let n=r,o=e.node(n.e.v),s=o.x+o.width/2,i=o.y,a=r.x-s,d=o.height/2;e.setEdge(n.e,n.label),e.removeNode(t),n.label.points=[{x:s+2*a/3,y:i-d},{x:s+5*a/6,y:i-d},{x:s+a,y:i},{x:s+5*a/6,y:i+d},{x:s+2*a/3,y:i+d}],n.label.x=r.x,n.label.y=r.y}})}function Ke(e,t){return Re(Pe(e,t),Number)}function Ze(e){let t={};return e&&Object.entries(e).forEach(([r,n])=>{typeof r=="string"&&(r=r.toLowerCase()),t[r]=n}),t}/*! For license information please see dagre.esm.js.LEGAL.txt */const se=140,ie=44,Lt=300,Zo=6,Ct=12;function Uo(e){const t=new Map,r=(n,o)=>{const s=`${n}|${o.side}`;t.has(s)||t.set(s,[]),t.get(s).push(o)};for(const n of e)r(n.edge.source,n.start),r(n.edge.target,n.end);for(const n of t.values())n.sort((o,s)=>o.sortY-s.sortY),n.forEach((o,s)=>{o.y=o.node.y-ie/2+ie*(s+1)/(n.length+1)})}function je(e,t=0){return{x:e.node.x+e.side*(se/2+Zo+t),y:e.y}}function Qo(e){const t=new Map;for(const n of e){const o=[n.source,n.target].sort().join("|");t.has(o)||t.set(o,[]),t.get(o).push(n)}const r=[];for(const n of t.values()){const o=n.find(i=>i.direction==="output"),s=n.find(i=>i.direction==="input");o&&s?r.push({...o,bidirectional:!0}):r.push(...n.map(i=>({...i,bidirectional:!1})))}return r}function $o(e,t){const r=new Dt.Graph({multigraph:!0});r.setGraph({rankdir:"LR",nodesep:14,ranksep:50,edgesep:8,marginx:20,marginy:20}),r.setDefaultEdgeLabel(()=>({}));const n=new Set(e.map(l=>l.id));for(const l of e)r.setNode(l.id,{width:se,height:ie});const o=Qo(t.filter(l=>n.has(l.source)&&n.has(l.target)));for(const l of o)r.setEdge(l.source,l.target,{},Ue(l));No(r);const s=e.map(l=>{const c=r.node(l.id);return{...l,x:c.x,y:c.y,width:se,height:ie}}),i=o.map(l=>{var p,b;const c=r.edge(l.source,l.target,Ue(l)),u=r.node(l.source),g=r.node(l.target),v=((c==null?void 0:c.points)??[]).slice(1,-1);return{edge:l,interior:v,start:{node:u,side:g.x>=u.x?1:-1,sortY:((p=v[0])==null?void 0:p.y)??g.y},end:{node:g,side:u.x>=g.x?1:-1,sortY:((b=v[v.length-1])==null?void 0:b.y)??u.y}}});Uo(i);const a=i.map(({edge:l,interior:c,start:u,end:g})=>({...l,points:[je(u),je(u,Ct),...c,je(g,Ct),je(g)]})),d=r.graph();return{nodes:s,edges:a,width:d.width??0,height:d.height??0}}function Jo(e,t=!0){if(!e||e.length===0)return"";const[r,...n]=e;if(!t||e.length<3)return`M ${r.x} ${r.y}`+n.map(d=>` L ${d.x} ${d.y}`).join("");const o=(d,l)=>({x:(d.x+l.x)/2,y:(d.y+l.y)/2}),s=o(r,e[1]);let i=`M ${r.x} ${r.y} L ${s.x} ${s.y}`;for(let d=1;d'),ts=Ie('',1),rs=Ie('base artifact'),ns=Ie(' '),os=Ie(""),ss=R('
');function is(e,t){et(t,!0);let r=Z(t,"layout",3,null),n=Z(t,"focusId",3,null),o=Z(t,"selectedId",3,null),s=Z(t,"smooth",3,!0),i=Z(t,"onSelect",3,()=>{}),a=te(0),d=te(0),l=te(Tt({x:0,y:0,k:1})),c=null;const u=.25,g=2.5,v=1;function p(){if(!r()||!h(a)||!h(d))return;const f=Math.min(h(a)/Math.max(r().width,1),h(d)/Math.max(r().height,1),1);j(l,{k:f,x:(h(a)-r().width*f)/2,y:(h(d)-r().height*f)/2},!0)}function b(f,m,_){return _<=m?(m-_)/2:Math.min(0,Math.max(m-_,f))}function k(){if(!r()||!h(a)||!h(d))return;const f=r().nodes.find(_=>_.id===n());if(!f){p();return}const m=v;j(l,{k:m,x:b(h(a)/2-f.x*m,h(a),r().width*m),y:b(h(d)/2-f.y*m,h(d),r().height*m)},!0)}tt(()=>{!r()||!h(a)||!h(d)||c!==n()&&(c=n(),k())});function N(f,m=h(a)/2,_=h(d)/2){const Y=Math.min(g,Math.max(u,h(l).k*f)),P=Y/h(l).k;j(l,{k:Y,x:m-(m-h(l).x)*P,y:_-(_-h(l).y)*P},!0)}function q(f){const m=_=>{if(!_.ctrlKey&&!_.metaKey)return;_.preventDefault();const Y=f.getBoundingClientRect();N(Math.exp(-_.deltaY*.002),_.clientX-Y.left,_.clientY-Y.top)};return f.addEventListener("wheel",m,{passive:!1}),{destroy:()=>f.removeEventListener("wheel",m)}}let S=te(null);function ne(f){f.button===0&&(j(S,{x:f.clientX,y:f.clientY,startX:f.clientX,startY:f.clientY},!0),f.currentTarget.setPointerCapture(f.pointerId))}function we(f){h(S)&&(j(l,{...h(l),x:h(l).x+f.clientX-h(S).x,y:h(l).y+f.clientY-h(S).y},!0),j(S,{...h(S),x:f.clientX,y:f.clientY},!0))}function ce(f){if(!h(S))return;const m=Math.hypot(f.clientX-h(S).startX,f.clientY-h(S).startY);j(S,null),f.currentTarget.releasePointerCapture(f.pointerId),m<4&&i()(null)}function ve(f,m=18){if(f==null)return"";const _=String(f);return _.length>m?_.slice(0,m-1)+"…":_}function pe(f){return f.kind==="artifact"?f.artifact_type:f.kind==="cluster"?"cluster":"run"}const $=le(()=>{if(!r()||!o())return null;const f=new Map,m=new Map,_=(x,A,Q)=>{x.has(A)||x.set(A,[]),x.get(A).push(Q)};for(const x of r().edges)_(f,x.source,x.target),_(m,x.target,x.source),x.bidirectional&&(_(f,x.target,x.source),_(m,x.source,x.target));const Y=x=>{const A=new Set([o()]),Q=[o()];for(;Q.length;)for(const ue of x.get(Q.pop())??[])A.has(ue)||(A.add(ue),Q.push(ue));return A},P=Y(f),E=Y(m);return{nodes:new Set([...P,...E]),down:P,up:E}});function I(f){return h($).down.has(f.source)&&h($).down.has(f.target)||h($).up.has(f.source)&&h($).up.has(f.target)}function F(f,m){(f.key==="Enter"||f.key===" ")&&(f.preventDefault(),i()(m.id))}var G=ss(),z=O(G);let ae;var Ee=y(O(z));{var w=f=>{var m=os(),_=O(m);Le(_,17,()=>r().edges,P=>Ue(P),(P,E)=>{var x=es();let A;K((Q,ue)=>{A=Be(x,0,"edge svelte-envflr",null,A,Q),W(x,"d",ue),W(x,"marker-start",h(E).bidirectional?"url(#lineage-arrow-end)":null)},[()=>({dimmed:h($)&&!I(h(E))}),()=>Jo(h(E).points,s())]),L(P,x)});var Y=y(_);Le(Y,17,()=>r().nodes,P=>P.id,(P,E)=>{var x=ns();let A;var Q=O(x);{var ue=U=>{var he=ts(),ye=ee(he),Me=y(ye);K(()=>{W(ye,"width",se-16),W(ye,"height",ie),W(Me,"width",se-8),W(Me,"height",ie)}),L(U,he)};H(Q,U=>{h(E).kind==="cluster"&&U(ue)})}var Ye=y(Q),Ae=y(Ye),lt=y(Ae);{var sr=U=>{var he=rs();K(()=>W(he,"transform",`translate(${se}, 0)`)),L(U,he)};H(lt,U=>{h(E).id===n()&&U(sr)})}var dt=y(lt),ir=O(dt),ar=y(dt),ct=O(ar),lr=y(ct),dr=O(lr);K((U,he,ye,Me,cr)=>{A=Be(x,0,`node ${h(E).kind??""} ${h(E).kind==="cluster"?`cluster-${h(E).member_kind}`:""}`,"svelte-envflr",A,U),W(x,"transform",`translate(${h(E).x-se/2}, ${h(E).y-ie/2})`),W(x,"aria-label",he),W(Ye,"width",se),W(Ye,"height",ie),W(Ae,"width",se+8),W(Ae,"height",ie+8),B(ir,ye),B(ct,`${Me??""} `),B(dr,cr)},[()=>({focused:h(E).id===n(),selected:h(E).id===o(),dimmed:h($)&&!h($).nodes.has(h(E).id)}),()=>fe(h(E)),()=>ve(pe(h(E)),20).toUpperCase(),()=>h(E).kind==="cluster"?fe(h(E)):ve(fe(h(E))),()=>fe(h(E))]),V("pointerdown",x,U=>U.stopPropagation()),V("click",x,()=>i()(h(E).id)),V("keydown",x,U=>F(U,h(E))),L(P,x)}),K(()=>W(m,"transform",`translate(${h(l).x??""}, ${h(l).y??""}) scale(${h(l).k??""})`)),L(f,m)};H(Ee,f=>{r()&&f(w)})}vr(z,f=>q==null?void 0:q(f));var M=y(z,2),D=O(M),C=y(D,2),T=y(C,2),X=y(T,2);K(()=>ae=Be(z,0,"graph svelte-envflr",null,ae,{panning:h(S)})),V("pointerdown",z,ne),V("pointermove",z,we),V("pointerup",z,ce),pr("pointercancel",z,ce),V("click",D,()=>N(1.25)),V("click",C,()=>N(.8)),V("click",T,p),V("click",X,k),pt(G,"clientWidth",f=>j(a,f)),pt(G,"clientHeight",f=>j(d,f)),L(e,G),rt()}Pt(["pointerdown","pointermove","pointerup","click","keydown"]);var as=R(' ',1),ls=R('cluster ',1),ds=R('run ',1),cs=R('
  • '),us=R('
  • No matches.
  • '),hs=R(''),fs=R('
      '),gs=R('Produced by ',1),vs=R('Size Files Created ',1),ps=R('First linked ',1),bs=R(''),ms=R(''),_s=R('
      ');function ws(e,t){et(t,!0);let r=Z(t,"node",3,null),n=Z(t,"focusId",3,null),o=Z(t,"onOpenVersion",3,null),s=Z(t,"onExtract",3,()=>{}),i=Z(t,"onExtractAll",3,()=>{}),a=Z(t,"onClose",3,()=>{});const d=30;let l=te("");tt(()=>{var p;(p=r())==null||p.id,j(l,"")});const c=le(()=>{var b;if(((b=r())==null?void 0:b.kind)!=="cluster")return[];const p=h(l).toLowerCase();return r().members.filter(k=>fe(k).toLowerCase().includes(p))});var u=Qe(),g=ee(u);{var v=p=>{var b=_s(),k=O(b),N=O(k);{var q=w=>{var M=as(),D=ee(M),C=O(D),T=y(D,2),X=O(T),f=y(T,2),m=O(f),_=y(f,2);Le(_,17,()=>r().aliases,mr,(Y,P)=>{br(Y,{get alias(){return h(P)}})}),K(()=>{B(C,r().artifact_type),B(X,r().artifact_name),B(m,`v${r().version??""}`)}),L(w,M)},S=w=>{var M=ls(),D=y(ee(M),2),C=O(D);K(T=>B(C,T),[()=>fe(r())]),L(w,M)},ne=w=>{var M=ds(),D=y(ee(M),2),C=O(D);K(()=>B(C,r().run_name??r().run_id)),L(w,M)};H(N,w=>{r().kind==="artifact"?w(q):r().kind==="cluster"?w(S,1):w(ne,-1)})}var we=y(N,2),ce=y(k,2);{var ve=w=>{var M=fs(),D=O(M);{let f=le(()=>r().member_kind==="run"?"runs":"versions");_r(D,{get placeholder(){return`Search ${h(f)??""}…`},get value(){return h(l)},set value(m){j(l,m,!0)}})}var C=y(D,2);Le(C,21,()=>h(c),f=>f.id,(f,m)=>{var _=cs(),Y=O(_),P=O(Y);K(E=>B(P,E),[()=>fe(h(m))]),V("click",Y,()=>s()(h(m).id)),L(f,_)},f=>{var m=us();L(f,m)});var T=y(C,2);{var X=f=>{var m=hs();V("click",m,()=>i()(r().members.map(_=>_.id))),L(f,m)};H(T,f=>{r().count<=d&&f(X)})}L(w,M)};H(ce,w=>{r().kind==="cluster"&&w(ve)})}var pe=y(ce,2),$=O(pe);{var I=w=>{var M=vs(),D=y(ee(M),2),C=O(D),T=y(D,4),X=O(T),f=y(T,4),m=O(f),_=y(f,2);{var Y=P=>{var E=gs(),x=y(ee(E),2),A=O(x),Q=O(A);K(()=>B(Q,r().producer_run_name)),V("click",A,()=>vt(r().producer_run_name,r().producer_run_id)),L(P,E)};H(_,P=>{r().producer_run_name&&P(Y)})}K((P,E)=>{B(C,P),B(X,r().num_files),B(m,E)},[()=>wr(r().size_bytes),()=>gt(r().created_at)]),L(w,M)},F=w=>{var M=ps(),D=y(ee(M),2),C=O(D);K(T=>B(C,T),[()=>gt(r().created_at)]),L(w,M)};H($,w=>{r().kind==="artifact"?w(I):r().kind==="run"&&w(F,1)})}var G=y(pe,2),z=O(G);{var ae=w=>{var M=Qe(),D=ee(M);{var C=T=>{var X=bs();V("click",X,()=>o()(r().artifact_name,r().version)),L(T,X)};H(D,T=>{o()&&r().id!==n()&&T(C)})}L(w,M)},Ee=w=>{var M=ms();V("click",M,()=>vt(r().run_name,r().run_id)),L(w,M)};H(z,w=>{r().kind==="artifact"?w(ae):r().kind==="run"&&w(Ee,1)})}V("click",we,function(...w){var M;(M=a())==null||M.apply(this,w)}),L(p,b)};H(g,p=>{r()&&p(v)})}L(e,u),rt()}Pt(["click"]);var Es=R('
      Loading lineage…
      '),ys=R('
      Failed to load lineage.
      '),ks=R('
      No lineage recorded for this version.
      '),xs=R('
      Large graph — lineage was truncated.
      '),Os=R('
      '),Ns=R('
      '),Is=R('
      Base Artifact Artifact Run
      Drag to pan · Ctrl/Cmd + scroll to zoom · Click a node for details
      ');function Ls(e,t){et(t,!0);let r=Z(t,"project",3,null),n=Z(t,"versionId",3,null),o=Z(t,"onOpenVersion",3,null),s=te(null),i=te(!0),a=te(!1),d=te(Tt(new Set)),l=te(null),c=0;const u=le(()=>`art:${n()}`);tt(()=>{g()});async function g(){const I=++c,F=r(),G=n(),z=`art:${G}`;if(!F||G==null){j(s,null),j(a,!1),j(d,new Set,!0),j(l,null),j(i,!1);return}j(i,!0),j(a,!1),j(d,new Set,!0),j(l,null);try{const ae=await Er(F,G);if(I!==c)return;j(s,ae,!0),j(l,z)}catch{if(I!==c)return;j(a,!0)}finally{I===c&&j(i,!1)}}const v=le(()=>h(s)?yr(h(s),h(u),{extracted:h(d)}):null),p=le(()=>h(v)&&h(v).nodes.length?$o(h(v).nodes,h(v).edges):null),b=le(()=>!h(v)||h(v).nodes.length<=Lt),k=le(()=>h(v)&&h(l)?h(v).nodes.find(I=>I.id===h(l))??null:null);function N(I){const F=new Set(h(d));F.add(I),j(d,F,!0),j(l,I,!0)}function q(I){const F=new Set(h(d));for(const G of I)F.add(G);j(d,F,!0),j(l,null)}function S(I){j(l,h(l)===I?null:I,!0)}var ne=Qe(),we=ee(ne);{var ce=I=>{var F=Es();L(I,F)},ve=I=>{var F=ys();L(I,F)},pe=I=>{var F=ks();L(I,F)},$=I=>{var F=Is(),G=O(F);{var z=C=>{var T=xs();L(C,T)},ae=C=>{var T=Os(),X=O(T),f=O(X);K(()=>B(f,`Large graph — showing ${h(v).nodes.length??""} nodes.`)),L(C,T)};H(G,C=>{h(s).truncated?C(z):h(v).nodes.length>Lt&&C(ae,1)})}var Ee=y(G,2),w=O(Ee);is(w,{get layout(){return h(p)},get focusId(){return h(u)},get selectedId(){return h(l)},get smooth(){return h(b)},onSelect:S});var M=y(w,6);{var D=C=>{var T=Ns(),X=O(T);ws(X,{get node(){return h(k)},get focusId(){return h(u)},get onOpenVersion(){return o()},onExtract:N,onExtractAll:q,onClose:()=>j(l,null)}),L(C,T)};H(M,C=>{h(k)&&C(D)})}L(I,F)};H(we,I=>{h(i)?I(ce):h(a)?I(ve,1):!h(s)||h(s).edges.length===0?I(pe,2):I($,-1)})}L(e,ne),rt()}export{Ls as default}; diff --git a/trackio/frontend/dist/assets/LineageSection-D4yA5g5T.css b/trackio/frontend/dist/assets/LineageSection-D4yA5g5T.css new file mode 100644 index 0000000000000000000000000000000000000000..eeccf651302a05af9c1617eab8120762adce55b3 --- /dev/null +++ b/trackio/frontend/dist/assets/LineageSection-D4yA5g5T.css @@ -0,0 +1 @@ +.graph-container.svelte-envflr{position:relative;height:100%;min-height:400px;background:var(--background-fill-primary, #ffffff);overflow:hidden}.graph.svelte-envflr{width:100%;height:100%;display:block;cursor:grab;touch-action:none}.graph.panning.svelte-envflr{cursor:grabbing}.edge.svelte-envflr{fill:none;stroke:color-mix(in srgb,var(--body-text-color-subdued, #6b7280) 55%,var(--border-color-primary, #d1d5db));stroke-width:1.5}.arrow-head.svelte-envflr{fill:color-mix(in srgb,var(--body-text-color-subdued, #6b7280) 55%,var(--border-color-primary, #d1d5db))}.node.svelte-envflr{cursor:pointer;outline:none;transition:opacity .15s ease}.edge.svelte-envflr{transition:opacity .15s ease}.node.dimmed.svelte-envflr,.edge.dimmed.svelte-envflr{opacity:.5}.node-box.svelte-envflr{fill:var(--background-fill-primary, #ffffff);stroke-width:1.5}.node.run.svelte-envflr .node-box:where(.svelte-envflr){stroke:var(--lineage-run)}.node.artifact.svelte-envflr .node-box:where(.svelte-envflr){stroke:var(--lineage-artifact)}.node.cluster.svelte-envflr .node-box:where(.svelte-envflr){stroke-dasharray:4 3}.node.cluster-artifact.svelte-envflr .node-box:where(.svelte-envflr){stroke:var(--lineage-artifact)}.node.cluster-run.svelte-envflr .node-box:where(.svelte-envflr){stroke:var(--lineage-run)}.node.cluster.svelte-envflr .node-box.stack:where(.svelte-envflr){fill:var(--background-fill-secondary, #f3f4f6);stroke-dasharray:none;opacity:.7}.selection-ring.svelte-envflr{fill:none;stroke:color-mix(in srgb,var(--body-text-color, #1f2937) 80%,var(--background-fill-primary, #ffffff));stroke-width:2;pointer-events:none;opacity:0}.node.selected.svelte-envflr .selection-ring:where(.svelte-envflr),.node.svelte-envflr:focus-visible .selection-ring:where(.svelte-envflr){opacity:1}.target-ring.svelte-envflr{fill:var(--background-fill-primary, #ffffff);stroke:var(--color-accent, #f97316);stroke-width:2}.target-dot.svelte-envflr{fill:var(--color-accent, #f97316)}.node-chip.svelte-envflr{font-size:9px;font-weight:600;letter-spacing:.05em;fill:var(--body-text-color-subdued, #6b7280)}.node.run.svelte-envflr .node-chip:where(.svelte-envflr),.node.cluster-run.svelte-envflr .node-chip:where(.svelte-envflr){fill:var(--lineage-run)}.node.artifact.svelte-envflr .node-chip:where(.svelte-envflr),.node.cluster-artifact.svelte-envflr .node-chip:where(.svelte-envflr){fill:var(--lineage-artifact)}.node-label.svelte-envflr{font-size:12px;font-weight:500;fill:var(--body-text-color, #1f2937)}.zoom-controls.svelte-envflr{position:absolute;bottom:8px;left:8px;display:flex;gap:4px}.zoom-controls.svelte-envflr button:where(.svelte-envflr){min-width:26px;height:26px;padding:0 7px;border:1px solid var(--border-color-primary, #e5e7eb);border-radius:var(--radius-sm, 4px);background:var(--background-fill-primary, #ffffff);color:var(--body-text-color-subdued, #6b7280);font-size:var(--text-sm, 13px);cursor:pointer}.zoom-controls.svelte-envflr button:where(.svelte-envflr):hover{color:var(--body-text-color, #1f2937);border-color:var(--color-accent, #f97316)}.preview.svelte-h7g2eb{border:1px solid var(--border-color-primary, #e5e7eb);border-radius:var(--radius-md, 6px);background:var(--background-fill-secondary, #f9fafb);padding:12px 14px;display:flex;flex-direction:column;gap:10px}.preview-header.svelte-h7g2eb{position:relative;display:flex;align-items:baseline;flex-wrap:wrap;gap:6px;padding-right:24px}.title.svelte-h7g2eb{font-size:var(--text-md, 14px);font-weight:600;color:var(--body-text-color, #1f2937)}.ver-badge.svelte-h7g2eb{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:var(--text-sm, 12px);font-weight:600;color:var(--body-text-color-subdued, #6b7280);background:var(--background-fill-primary, #ffffff);border-radius:var(--radius-sm, 4px);padding:2px 7px}.kind-chip.svelte-h7g2eb{flex-basis:100%;font-size:var(--text-xs, 11px);text-transform:uppercase;letter-spacing:.05em;font-weight:600;color:var(--lineage-run)}.kind-chip.cluster.svelte-h7g2eb{color:var(--body-text-color-subdued, #6b7280)}.kind-chip.artifact.svelte-h7g2eb{color:var(--lineage-artifact)}.cluster-body.svelte-h7g2eb{min-height:0;display:flex;flex-direction:column;gap:6px}.member-list.svelte-h7g2eb{list-style:none;margin:0;padding:0;min-height:0;overflow-y:auto;border:1px solid var(--border-color-primary, #e5e7eb);border-radius:var(--radius-sm, 4px);background:var(--background-fill-primary, #ffffff)}.member-item.svelte-h7g2eb+.member-item:where(.svelte-h7g2eb){border-top:1px solid var(--border-color-primary, #f3f4f6)}.member-row.svelte-h7g2eb{display:block;width:100%;background:none;border:none;text-align:left;padding:5px 8px;font-size:var(--text-sm, 12px);color:var(--body-text-color, #1f2937);overflow-wrap:anywhere;cursor:pointer}.member-row.svelte-h7g2eb:hover,.member-row.svelte-h7g2eb:focus-visible{background:var(--background-fill-secondary, #f3f4f6)}.member-empty.svelte-h7g2eb{padding:6px 8px;font-size:var(--text-sm, 12px);color:var(--body-text-color-subdued, #6b7280)}.close-btn.svelte-h7g2eb{position:absolute;top:0;right:0;background:none;border:none;color:var(--body-text-color-subdued, #6b7280);font-size:16px;cursor:pointer;padding:0 4px;line-height:1}.detail-grid.svelte-h7g2eb{display:grid;grid-template-columns:auto 1fr;gap:4px 16px;align-items:baseline}.detail-key.svelte-h7g2eb{font-size:var(--text-xs, 11px);color:var(--body-text-color-subdued, #6b7280);white-space:nowrap}.detail-val.svelte-h7g2eb{font-size:var(--text-sm, 12px);color:var(--body-text-color, #1f2937);overflow-wrap:anywhere}.run-link.svelte-h7g2eb{background:none;border:none;padding:0;font-size:var(--text-sm, 12px);color:var(--color-accent, #f97316);cursor:pointer;text-align:left}.preview-footer.svelte-h7g2eb:empty{display:none}.open-btn.svelte-h7g2eb{font-size:var(--text-sm, 12px);padding:4px 12px;border:1px solid var(--border-color-primary, #e5e7eb);border-radius:var(--radius-sm, 4px);background:var(--background-fill-primary, #ffffff);color:var(--body-text-color, #1f2937);cursor:pointer}.open-btn.svelte-h7g2eb:hover{border-color:var(--color-accent, #f97316);color:var(--color-accent, #f97316)}.status.svelte-sqcnye{font-size:var(--text-sm, 12px);color:var(--body-text-color-subdued, #6b7280);padding:6px 0}.lineage.svelte-sqcnye{--lineage-run: #10b981;--lineage-artifact: #3b82f6;flex:1;min-height:0;display:flex;flex-direction:column;gap:8px}.graph-wrap.svelte-sqcnye{flex:1;min-height:0;position:relative}.preview-overlay.svelte-sqcnye{position:absolute;top:8px;right:8px;z-index:5;width:280px;max-width:calc(100% - 16px);max-height:calc(100% - 16px);overflow-y:auto}.preview-overlay.svelte-sqcnye>*{box-sizing:border-box;box-shadow:0 4px 16px #0000002e}.toolbar.svelte-sqcnye{display:flex;align-items:center;gap:12px;min-height:18px}.notice.svelte-sqcnye{font-size:var(--text-xs, 11px);color:var(--color-accent, #f97316)}.legend.svelte-sqcnye{position:absolute;top:8px;left:8px;z-index:4;display:flex;align-items:center;gap:12px;font-size:var(--text-xs, 11px);color:var(--body-text-color-subdued, #6b7280)}.hint.svelte-sqcnye{position:absolute;bottom:8px;right:8px;z-index:4;font-size:var(--text-xs, 11px);color:var(--body-text-color-subdued, #6b7280)}.legend-item.svelte-sqcnye{display:inline-flex;align-items:center;gap:5px}.swatch.svelte-sqcnye{width:10px;height:10px;border-radius:3px;background:var(--background-fill-primary, #ffffff);border:1.5px solid}.swatch.artifact.svelte-sqcnye{border-color:var(--lineage-artifact)}.swatch.run.svelte-sqcnye{border-color:var(--lineage-run)}.target-swatch.svelte-sqcnye{width:11px;height:11px}.target-swatch.svelte-sqcnye .target-ring:where(.svelte-sqcnye){fill:none;stroke:var(--color-accent, #f97316);stroke-width:2}.target-swatch.svelte-sqcnye .target-dot:where(.svelte-sqcnye){fill:var(--color-accent, #f97316)} diff --git a/trackio/frontend/dist/assets/MSFT_audio_emitter-Dn8xGnZi.js b/trackio/frontend/dist/assets/MSFT_audio_emitter-Dn8xGnZi.js new file mode 100644 index 0000000000000000000000000000000000000000..186bdded661181d01db40b8af2827d2a42eea5db --- /dev/null +++ b/trackio/frontend/dist/assets/MSFT_audio_emitter-Dn8xGnZi.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/webAudioBus-7GGmRMcj.js","assets/abstractAudioBus-D0ySXXLP.js","assets/math.vector-C1xyPDEl.js","assets/math.scalar.functions-DkeaWAWz.js","assets/tools-D01N7ptR.js","assets/decorators.serialization-C7d7ykGG.js","assets/bufferUtils-DTRdUD8L.js","assets/math.color-CzLaKyb6.js","assets/glTFLoader-BPeiodqB.js","assets/index-Bx1QoKVU.js","assets/index-BfQ3YfM4.css","assets/mesh-Fj-9677A.js","assets/mesh.vertexData-Cr3zcPIw.js","assets/buffer-DqKlo1ts.js","assets/dataBuffer-CgoqiUzT.js","assets/subMesh-UWDo5wjI.js","assets/drawWrapper-CZVk0xN-.js","assets/smartArray-CO6iJ0u5.js","assets/node-C-Iu0ALa.js","assets/math.viewport-CgkTt1RS.js","assets/sceneComponent-Dflhczui.js","assets/math.plane-Ce_75-g5.js","assets/lightConstants-B1TSTh-k.js","assets/sceneLoaderFlags-wywyb9ml.js","assets/math.axis-Ck7JfPPj.js","assets/material-CrEsrP1R.js","assets/texture-BEqhDl5Y.js","assets/baseTexture-CgAggsey.js","assets/drawWrapper.functions-Dzz0ghog.js","assets/cameraInputsManager-WrTH9PVf.js","assets/deviceInputEvents-BTza-zoR.js","assets/bone-B35odufe.js","assets/skeleton-BVUVDN0a.js","assets/rawTexture-Cp8_XuDs.js","assets/animation-IftAdLIx.js","assets/sceneLoader-CutT-qCn.js","assets/scene-gn_hoGMy.js","assets/renderingManager-CiY2OUrf.js","assets/perfCounter-DusmGz8t.js","assets/assetContainer-DOnbxwKX.js","assets/instancedMesh-Cz-e1Hca.js","assets/light-DT53MGnW.js","assets/dynamic-BUKysMyT.js","assets/objectModelMapping-DWcnlpHl.js","assets/webAudioMainBus-DhWe-5w4.js"])))=>i.map(i=>d[i]); +import{Q as C,V as h,O as y,M as B,E as $}from"./math.vector-C1xyPDEl.js";import{T as k}from"./tools-D01N7ptR.js";import{L as S,f as Ft,e as kt,G as Ut}from"./bufferUtils-DTRdUD8L.js";import{W as Q,P as wt,A as _}from"./decorators.serialization-C7d7ykGG.js";import{R as Bt}from"./math.scalar.functions-DkeaWAWz.js";import{A as O,G as X}from"./glTFLoader-BPeiodqB.js";import{u as Gt,r as zt}from"./dynamic-BUKysMyT.js";import{b as A}from"./sceneComponent-Dflhczui.js";import{S as V}from"./scene-gn_hoGMy.js";import{_ as R}from"./index-Bx1QoKVU.js";class nt{constructor(t,e,i){this.frame=t,this.action=e,this.onlyOnce=i,this.isDone=!1}_clone(){return new nt(this.frame,this.action,this.onlyOnce)}}const u={coneInnerAngle:6.28318530718,coneOuterAngle:6.28318530718,coneOuterVolume:0,distanceModel:"linear",maxDistance:1e4,minDistance:1,orientation:h.Right(),panningModel:"equalpower",position:h.Zero(),rolloffFactor:1,rotation:h.Zero(),rotationQuaternion:new C};function G(s){return s.spatialEnabled||s.spatialAutoUpdate!==void 0||s.spatialConeInnerAngle!==void 0||s.spatialConeOuterAngle!==void 0||s.spatialConeOuterVolume!==void 0||s.spatialDistanceModel!==void 0||s.spatialMaxDistance!==void 0||s.spatialMinDistance!==void 0||s.spatialMinUpdateTime!==void 0||s.spatialOrientation!==void 0||s.spatialPanningModel!==void 0||s.spatialPosition!==void 0||s.spatialRolloffFactor!==void 0||s.spatialRotation!==void 0||s.spatialRotationQuaternion!==void 0}class Wt{}var ht;(function(s){s[s.HAS_INPUTS=1]="HAS_INPUTS",s[s.HAS_OUTPUTS=2]="HAS_OUTPUTS",s[s.HAS_INPUTS_AND_OUTPUTS=3]="HAS_INPUTS_AND_OUTPUTS"})(ht||(ht={}));class ot{constructor(t,e){this.onDisposeObservable=new y,this.engine=t,e&1&&(this._upstreamNodes=new Set),e&2&&(this._downstreamNodes=new Set)}dispose(){if(this._downstreamNodes){for(const t of Array.from(this._downstreamNodes))if(!this._disconnect(t))throw new Error("Disconnect failed");this._downstreamNodes.clear()}if(this._upstreamNodes){for(const t of Array.from(this._upstreamNodes))if(!t._disconnect(this))throw new Error("Disconnect failed");this._upstreamNodes.clear()}this.onDisposeObservable.notifyObservers(this),this.onDisposeObservable.clear()}_connect(t){return!this._downstreamNodes||this._downstreamNodes.has(t)||!t._onConnect(this)?!1:(this._downstreamNodes.add(t),!0)}_disconnect(t){return!this._downstreamNodes||!this._downstreamNodes.delete(t)?!1:t._onDisconnect(this)}_onConnect(t){return!this._upstreamNodes||this._upstreamNodes.has(t)?!1:(this._upstreamNodes.add(t),!0)}_onDisconnect(t){var e;return((e=this._upstreamNodes)==null?void 0:e.delete(t))??!1}}class vt extends ot{constructor(t,e,i){super(e,i),this.onNameChangedObservable=new y,this._name=t}get name(){return this._name}set name(t){if(this._name===t)return;const e=this._name;this._name=t,this.onNameChangedObservable.notifyObservers({newName:t,oldName:e,node:this})}dispose(){super.dispose(),this.onNameChangedObservable.clear()}}class j extends vt{constructor(t,e){super(t,e,3)}connect(t){if(!this._connect(t))throw new Error("Connect failed")}disconnect(t){if(!this._disconnect(t))throw new Error("Disconnect failed")}disconnectAll(){if(!this._downstreamNodes)throw new Error("Disconnect failed");const t=this._downstreamNodes.values();for(let e=t.next();!e.done;e=t.next())if(!this._disconnect(e.value))throw new Error("Disconnect failed")}}const Tt={volume:1};class Lt extends j{constructor(t){super("Volume",t)}setOptions(t){this.setVolume(t.volume??Tt.volume,{shape:"none"})}}function I(s){return s.getSubNode("Volume")}function qt(s,t){var e;return((e=I(s))==null?void 0:e[t])??Tt[t]}const P={fftSize:2048,minDecibels:-100,maxDecibels:-30,smoothing:.8};function Qt(s){return s.analyzerEnabled||s.analyzerFFTSize!==void 0||s.analyzerMinDecibels!==void 0||s.analyzerMaxDecibels!==void 0||s.analyzerSmoothing!==void 0}class Zt{get frequencyBinCount(){return this.fftSize/2}}class $t extends j{constructor(t){super("Analyzer",t)}setOptions(t){this.fftSize=t.analyzerFFTSize??P.fftSize,this.minDecibels=t.analyzerMinDecibels??P.minDecibels,this.maxDecibels=t.analyzerMaxDecibels??P.maxDecibels,this.smoothing=t.analyzerSmoothing??P.smoothing}}function D(s){return s.getSubNode("Analyzer")}function z(s,t,e){s.callOnSubNode("Analyzer",i=>{i[t]=e})}let Y=null,J=null;function Ot(){return Y||(Y=new Uint8Array),Y}function xt(){return J||(J=new Float32Array),J}class jt extends Zt{constructor(t){super(),this._fftSize=P.fftSize,this._maxDecibels=P.maxDecibels,this._minDecibels=P.minDecibels,this._smoothing=P.smoothing,this._subGraph=t}get fftSize(){return this._fftSize}set fftSize(t){this._fftSize=t,z(this._subGraph,"fftSize",t)}get isEnabled(){return D(this._subGraph)!==null}get minDecibels(){return this._minDecibels}set minDecibels(t){this._minDecibels=t,z(this._subGraph,"minDecibels",t)}get maxDecibels(){return this._maxDecibels}set maxDecibels(t){this._maxDecibels=t,z(this._subGraph,"maxDecibels",t)}get smoothing(){return this._smoothing}set smoothing(t){this._smoothing=t,z(this._subGraph,"smoothing",t)}dispose(){const t=D(this._subGraph);t&&(this._subGraph.removeSubNodeAsync(t),t.dispose())}async enableAsync(){D(this._subGraph)||await this._subGraph.createAndAddSubNodeAsync("Analyzer")}getByteFrequencyData(){const t=D(this._subGraph);return t?t.getByteFrequencyData():(S.Warn("AudioAnalyzer not enabled"),this.enableAsync(),Ot())}getFloatFrequencyData(){const t=D(this._subGraph);return t?t.getFloatFrequencyData():(S.Warn("AudioAnalyzer not enabled"),this.enableAsync(),xt())}}class Ht extends vt{constructor(t,e,i){super(t,e,i),this._analyzer=null}get analyzer(){return this._analyzer??(this._analyzer=new jt(this._subGraph))}get volume(){return qt(this._subGraph,"volume")}set volume(t){const e=I(this._subGraph);if(!e)throw new Error("No volume subnode");e.volume=t}dispose(){var t;super.dispose(),(t=this._analyzer)==null||t.dispose(),this._analyzer=null,this._subGraph.dispose()}setVolume(t,e=null){const i=I(this._subGraph);if(!i)throw new Error("No volume subnode");i.setVolume(t,e)}}class Pt extends Ht{constructor(t,e,i,n=2){super(t,e,n),this._spatialAutoUpdate=!0,this._spatialMinUpdateTime=0,this._outBus=null,this._spatial=null,this._onOutBusDisposed=null,typeof i.spatialAutoUpdate=="boolean"&&(this._spatialAutoUpdate=i.spatialAutoUpdate),typeof i.spatialMinUpdateTime=="number"&&(this._spatialMinUpdateTime=i.spatialMinUpdateTime)}get outBus(){return this._outBus}set outBus(t){if(this._outBus!==t){if(this._outBus&&(this._onOutBusDisposed&&(this._outBus.onDisposeObservable.removeCallback(this._onOutBusDisposed),this._onOutBusDisposed=null),!this._disconnect(this._outBus)))throw new Error("Disconnect failed");if(this._outBus=t,this._outBus&&(this._onOutBusDisposed=()=>{this._outBus=null},this._outBus.onDisposeObservable.add(this._onOutBusDisposed),!this._connect(this._outBus)))throw new Error("Connect failed")}}get spatial(){return this._spatial?this._spatial:this._initSpatialProperty()}dispose(){var t;super.dispose(),(t=this._spatial)==null||t.dispose(),this._spatial=null,this._outBus&&this._onOutBusDisposed&&(this._outBus.onDisposeObservable.removeCallback(this._onOutBusDisposed),this._onOutBusDisposed=null),this._outBus=null}_initSpatialProperty(){return this._spatial=this._createSpatialProperty(this._spatialAutoUpdate,this._spatialMinUpdateTime)}get _isSpatial(){return this._spatial!==null}set _isSpatial(t){t&&!this._spatial?this._initSpatialProperty():!t&&this._spatial&&(this._spatial.dispose(),this._spatial=null)}}const Dt={pan:0};function Xt(s){return s.stereoEnabled||s.stereoPan!==void 0}class Yt{}class Jt extends j{constructor(t){super("Stereo",t)}setOptions(t){this.pan=t.stereoPan??Dt.pan}}function lt(s){return s.getSubNode("Stereo")}function Kt(s,t,e){s.callOnSubNode("Stereo",i=>{i[t]=e})}class at extends Yt{constructor(t){super(),this._pan=Dt.pan,this._subGraph=t}get pan(){return this._pan}set pan(t){this._pan=t,Kt(this._subGraph,"pan",t)}}class st{constructor(t){this._attachmentType=3,this._position=new h,this._rotationQuaternion=new C,this._sceneNode=null,this._useBoundingBox=!1,this.dispose=()=>{this.detach()},this._spatialAudioNode=t}get isAttached(){return this._sceneNode!==null}attach(t,e,i){this._sceneNode!==t&&(this.detach(),t&&(this._attachmentType=i,this._sceneNode=t,this._sceneNode.onDisposeObservable.add(this.dispose),this._useBoundingBox=e))}detach(){var t;(t=this._sceneNode)==null||t.onDisposeObservable.removeCallback(this.dispose),this._sceneNode=null}update(){var t,e;this._attachmentType&1&&(this._useBoundingBox&&this._sceneNode.getBoundingInfo?this._position.copyFrom(this._sceneNode.getBoundingInfo().boundingBox.centerWorld):(t=this._sceneNode)==null||t.getWorldMatrix().getTranslationToRef(this._position),this._spatialAudioNode.position.copyFrom(this._position),this._spatialAudioNode._updatePosition()),this._attachmentType&2&&((e=this._sceneNode)==null||e.getWorldMatrix().decompose(void 0,this._rotationQuaternion),this._spatialAudioNode.rotationQuaternion.copyFrom(this._rotationQuaternion),this._spatialAudioNode._updateRotation())}}class te extends j{constructor(t){super("Spatial",t),this._attacherComponent=null}get isAttached(){return this._attacherComponent!==null&&this._attacherComponent.isAttached}attach(t,e,i){this.detach(),this._attacherComponent||(this._attacherComponent=new st(this)),this._attacherComponent.attach(t,e,i)}detach(){var t;(t=this._attacherComponent)==null||t.detach()}dispose(){var t;super.dispose(),(t=this._attacherComponent)==null||t.dispose(),this._attacherComponent=null}setOptions(t){this.coneInnerAngle=t.spatialConeInnerAngle??u.coneInnerAngle,this.coneOuterAngle=t.spatialConeOuterAngle??u.coneOuterAngle,this.coneOuterVolume=t.spatialConeOuterVolume??u.coneOuterVolume,this.distanceModel=t.spatialDistanceModel??u.distanceModel,this.maxDistance=t.spatialMaxDistance??u.maxDistance,this.minDistance=t.spatialMinDistance??u.minDistance,this.orientation=t.spatialOrientation??u.orientation,this.panningModel=t.spatialPanningModel??u.panningModel,this.rolloffFactor=t.spatialRolloffFactor??u.rolloffFactor,t.spatialPosition&&(this.position=t.spatialPosition.clone()),t.spatialRotationQuaternion?this.rotationQuaternion=t.spatialRotationQuaternion.clone():t.spatialRotation?this.rotation=t.spatialRotation.clone():this.rotationQuaternion=u.rotationQuaternion.clone(),this.update()}update(){var t;this.isAttached?(t=this._attacherComponent)==null||t.update():(this._updatePosition(),this._updateRotation())}}function w(s){return s.getSubNode("Spatial")}function x(s,t,e){s.callOnSubNode("Spatial",i=>{i[t]=e})}const ee=new RegExp("\\.(\\w{3,4})($|\\?)"),v=100,K=new Float32Array([0,0]);let F=null,W=null,L=null;function ie(){if(!W){W=new Float32Array(v);const s=1/(v-1);let t=s;for(let e=1;e0&&S.Warn("WebAudioStreamingSound: Custom request headers cannot be applied to a streaming