style: native bun fmt over imported packages, apps and scripts

This commit is contained in:
aphrody-dev committed 2026-10-16 10:08:38 +02:00
1 parent 21d7943af8
commit 01b5395ba8
3426 files changed
+519608 -122443

No files matched your search

+20 -3
View File
@@ -44,7 +44,10 @@ perf = { level = "warn", priority = -1 }
# it never re-executes its own exe as `__llama-daemon`. Without the feature (no libclang at build
# time) the supervisor falls back to the detached daemon, which `src/llama_daemon.rs` still serves.
default = ["in-process"]
in-process = ["aphrody-app-server/in-process", "aphrody-llama-server/in-process"]
in-process = [
"aphrody-app-server/in-process",
"aphrody-llama-server/in-process",
]
# Serve the bundled `frontendDist` (ui-dist) instead of `devUrl`. The Tauri CLI enables it for
# `tauri build`; plain `cargo build`/`cargo run` release builds must pass it explicitly.
custom-protocol = ["tauri/custom-protocol"]
@@ -74,10 +77,24 @@ aphrody-subsystem = { path = "../../crates/aphrody-subsystem", version = "1.4.4"
async-trait = "0.1.92"
yolo-core = "=1.0.0"
base64 = "0.23.1"
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rustls = { version = "0.23", default-features = false, features = [
"ring",
"std",
"tls12",
] }
tauri = { package = "aphrody-tauri", path = "../../toolchains/bun/packages/bun-tauri/crates/aphrody-tauri", version = "1.4.4", default-features = false, features = ["compression", "dynamic-acl", "image-ico", "tray-icon"] }
tauri-runtime-cef = { package = "aphrody-tauri-runtime-cef", path = "../../toolchains/bun/packages/bun-tauri/crates/aphrody-tauri-runtime-cef", version = "1.4.4" }
tokio = { version = "1.53.1", features = ["process", "rt-multi-thread", "macros", "time", "signal", "sync", "fs", "io-util", "full"] }
tokio = { version = "1.53.1", features = [
"process",
"rt-multi-thread",
"macros",
"time",
"signal",
"sync",
"fs",
"io-util",
"full",
] }
serde = { version = "1.0.229", features = ["derive", "std"], default-features = false }
serde_json = "1.0.151"
log = "0.4.34"
+12 -11
View File
@@ -1,4 +1,5 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
# aphrody-app — Aphrody Tauri desktop host
`aphrody-app` is the Tauri 3 (`tauri-runtime-cef`: CEF + tauri-winit + GTK 4) shell in the root Aphrody OS Cargo workspace.
@@ -9,11 +10,11 @@ streams. The browser version talks to the same protocol over loopback HTTP/SSE.
The trusted main window's core commands include:
| Command | Returns | Use |
|---|---|---|
| `aphrody_meta()` | `{ app_version, target_os, target_arch, family }` | header / about info |
| `aphrody_rpc(request)` | JSON-RPC result | app-server sessions, turns and interruption |
| `aphrody_shell_window(action)` | native window state | main-only window controls and navigation |
| Command | Returns | Use |
| ------------------------------ | ------------------------------------------------- | ------------------------------------------- |
| `aphrody_meta()` | `{ app_version, target_os, target_arch, family }` | header / about info |
| `aphrody_rpc(request)` | JSON-RPC result | app-server sessions, turns and interruption |
| `aphrody_shell_window(action)` | native window state | main-only window controls and navigation |
The shell command takes its invoking `WebviewWindow`, accepts no target label and
rejects every label except `main`. Remote CEF previews have no capability and
@@ -35,12 +36,12 @@ not copy or delete them during startup.
## Runtime boundary
| Surface | Owner | Contract |
|---|---|---|
| Agent sessions | `aphrody-app-server` | in-process broker; loopback HTTP/SSE for web clients |
| Web tools | Aphrody kernel | `aphrody_web` Obscura/provider subsystem |
| Web retrieval and crawl | Aphrody kernel / Obscura | backend capability; no embedded browser or crawler-management view in the public webview |
| File libraries | `aphrody-drive` + `sd-daemon` | local socket; daemon stays outside this Cargo graph |
| Surface | Owner | Contract |
| ----------------------- | ----------------------------- | ---------------------------------------------------------------------------------------- |
| Agent sessions | `aphrody-app-server` | in-process broker; loopback HTTP/SSE for web clients |
| Web tools | Aphrody kernel | `aphrody_web` Obscura/provider subsystem |
| Web retrieval and crawl | Aphrody kernel / Obscura | backend capability; no embedded browser or crawler-management view in the public webview |
| File libraries | `aphrody-drive` + `sd-daemon` | local socket; daemon stays outside this Cargo graph |
## Capability surface (default-deny)
@@ -1,25 +1,52 @@
<!doctype html>
<html><body><pre id="out">pending</pre>
<script>window.addEventListener("error",e=>{document.title="ERR "+e.message;window.__TAURI__&&window.__TAURI__.core.invoke("report",{text:"WINERROR "+e.message+" "+e.filename})});window.addEventListener("unhandledrejection",e=>{window.__TAURI__&&window.__TAURI__.core.invoke("report",{text:"REJECT "+e.reason})});</script>
<script type="module">
import init, * as w from "./pkg/aphrody_shell_web.js";
import { attach, localTransport, tauriTransport } from "./index.js";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
try {
const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
globalThis.invoke = invoke;
await init();
const local = localTransport(w, 60, 10);
attach(w, local, 60, 10);
local.send(new TextEncoder().encode("echo tauri-wasm\r"));
const t = tauriTransport({ invoke, listen }, "e2e", 100, 20);
const term = attach(w, t, 100, 20);
await sleep(800);
term.type("echo pty-$((20+22))\r");
await sleep(1500);
await invoke("report", { text: "LOCAL[" + local.snapshot().lines.join("|") + "] REMOTE[" + term.snapshot().lines.join("|") + "]" });
} catch (e) {
await globalThis.invoke?.("report", { text: "ERROR " + e + " " + (e && e.stack) });
}
</script></body></html>
<html>
<body>
<pre id="out">pending</pre>
<script>
window.addEventListener("error", (e) => {
document.title = "ERR " + e.message;
window.__TAURI__ &&
window.__TAURI__.core.invoke("report", {
text: "WINERROR " + e.message + " " + e.filename,
});
});
window.addEventListener("unhandledrejection", (e) => {
window.__TAURI__ &&
window.__TAURI__.core.invoke("report", {
text: "REJECT " + e.reason,
});
});
</script>
<script type="module">
import init, * as w from "./pkg/aphrody_shell_web.js";
import { attach, localTransport, tauriTransport } from "./index.js";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
try {
const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
globalThis.invoke = invoke;
await init();
const local = localTransport(w, 60, 10);
attach(w, local, 60, 10);
local.send(new TextEncoder().encode("echo tauri-wasm\r"));
const t = tauriTransport({ invoke, listen }, "e2e", 100, 20);
const term = attach(w, t, 100, 20);
await sleep(800);
term.type("echo pty-$((20+22))\r");
await sleep(1500);
await invoke("report", {
text:
"LOCAL[" +
local.snapshot().lines.join("|") +
"] REMOTE[" +
term.snapshot().lines.join("|") +
"]",
});
} catch (e) {
await globalThis.invoke?.("report", {
text: "ERROR " + e + " " + (e && e.stack),
});
}
</script>
</body>
</html>
+1 -4
View File
@@ -1,9 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/tauri-v3.0.0-alpha.4/crates/tauri-schema-generator/schemas/config.schema.json",
"bundle": {
"externalBin": [
"binaries/sd-daemon",
"binaries/aphrody"
]
"externalBin": ["binaries/sd-daemon", "binaries/aphrody"]
}
}
File diff suppressed because it is too large. Load diff
+291027 -507
View File
File diff suppressed because it is too large. Load diff
+9 -8
View File
@@ -2,15 +2,16 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
<html lang="en">
<head>
<meta charset="utf-8">
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta charset="utf-8" />
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<title>Aphrody OS</title>
<link rel="icon" type="image/svg+xml" href="./fonts/icon.svg">
<link rel="stylesheet" crossorigin href="./app.css"><script type="module" crossorigin src="./app.js"></script></head>
<link rel="icon" type="image/svg+xml" href="./fonts/icon.svg" />
<link rel="stylesheet" crossorigin href="./app.css" />
<script type="module" crossorigin src="./app.js"></script>
</head>
<body>
<div id="root"></div>
</body>
+95 -95
View File
@@ -1,31 +1,25 @@
[workspace]
exclude = ["crates/json-sample-derive", "extensions/*"]
members = [
# "apps/cloud",
# "apps/desktop/crates/*",
# "apps/desktop/src-tauri",
"apps/cli",
# "apps/gpui-photo-grid",
"core",
"core/benchmarks",
"crates/*",
"xtask"
# "apps/cloud",
# "apps/desktop/crates/*",
# "apps/desktop/src-tauri",
"apps/cli",
# "apps/gpui-photo-grid",
"core",
"core/benchmarks",
"crates/*",
"xtask",
]
# The HTTP site is owned by aphrody-web. This nested FSL workspace builds the
# Drive backend, daemon and CLI without joining the Apache-2.0 OS workspace.
default-members = [
"apps/cli",
"core",
"core/benchmarks",
"crates/*",
"xtask"
]
default-members = ["apps/cli", "core", "core/benchmarks", "crates/*", "xtask"]
resolver = "2"
[workspace.package]
edition = "2021"
license = "FSL-1.1-ALv2"
repository = "https://github.com/spacedriveapp/spacedrive"
edition = "2021"
license = "FSL-1.1-ALv2"
repository = "https://github.com/spacedriveapp/spacedrive"
rust-version = "1.81"
[workspace.dependencies]
@@ -33,64 +27,70 @@ rust-version = "1.81"
# sd-cloud-schema = { git = "https://github.com/spacedriveapp/cloud-services-schema", rev = "515ba740ea" }
# Third party dependencies used by one or more of our crates
anyhow = "1.0.94"
async-channel = "2.3"
async-stream = "0.3.6"
async-trait = "0.1.83"
axum = "0.7.9"
axum-extra = "0.9.6"
base64 = "0.22.1"
blake3 = "1.5.5"
bytes = "1.9.0"
chrono = "0.4.39"
ed25519-dalek = "2.1"
flume = "0.11.0"
futures = "0.3.31"
anyhow = "1.0.94"
async-channel = "2.3"
async-stream = "0.3.6"
async-trait = "0.1.83"
axum = "0.7.9"
axum-extra = "0.9.6"
base64 = "0.22.1"
blake3 = "1.5.5"
bytes = "1.9.0"
chrono = "0.4.39"
ed25519-dalek = "2.1"
flume = "0.11.0"
futures = "0.3.31"
futures-concurrency = "7.6.2"
globset = "0.4.15"
http = "1.2.0"
hyper = "1.5.2"
image = "0.25.5"
iroh = "0.95.1"
itertools = "0.13.0"
lending-stream = "1.0"
libc = "0.2.169"
mimalloc = "0.1.43"
normpath = "1.3"
pin-project-lite = "0.2.15"
quic-rpc = "0.17.3"
rand = "0.9.0-alpha.2"
regex = "1.11.1"
reqwest = { version = "0.12.9", default-features = false }
rmp = "0.8.14"
rmp-serde = "1.3"
rmpv = { version = "1.3", features = ["with-serde"] }
serde = "1.0.216"
serde_json = "1.0.133"
specta = { git = "https://github.com/jamiepine/specta", branch = "main" }
strum = "0.26"
strum_macros = "0.26"
tempfile = "3.14.0"
thiserror = "2.0.8"
tokio = "1.42.0"
tokio-stream = "0.1.17"
tokio-util = "0.7.13"
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
tracing-test = "0.2.5"
uhlc = "0.8.0" # Must follow version used by specta
uuid = "1.10" # Must follow version used by specta
webp = "0.3.0"
zeroize = "1.8"
sqlx = { version = "0.8", default-features = false, features = ["sqlite", "runtime-tokio"] }
lancedb = "0.15"
fastembed = "4"
arrow-array = "53.3"
arrow-schema = "53.3"
indexmap = "2.7"
dashmap = "6.1"
toml = "0.8"
ort = { version = "2.0.0-rc.9", default-features = false, features = ["download-binaries", "tls-native"] }
globset = "0.4.15"
http = "1.2.0"
hyper = "1.5.2"
image = "0.25.5"
iroh = "0.95.1"
itertools = "0.13.0"
lending-stream = "1.0"
libc = "0.2.169"
mimalloc = "0.1.43"
normpath = "1.3"
pin-project-lite = "0.2.15"
quic-rpc = "0.17.3"
rand = "0.9.0-alpha.2"
regex = "1.11.1"
reqwest = { version = "0.12.9", default-features = false }
rmp = "0.8.14"
rmp-serde = "1.3"
rmpv = { version = "1.3", features = ["with-serde"] }
serde = "1.0.216"
serde_json = "1.0.133"
specta = { git = "https://github.com/jamiepine/specta", branch = "main" }
strum = "0.26"
strum_macros = "0.26"
tempfile = "3.14.0"
thiserror = "2.0.8"
tokio = "1.42.0"
tokio-stream = "0.1.17"
tokio-util = "0.7.13"
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
tracing-test = "0.2.5"
uhlc = "0.8.0" # Must follow version used by specta
uuid = "1.10" # Must follow version used by specta
webp = "0.3.0"
zeroize = "1.8"
sqlx = { version = "0.8", default-features = false, features = [
"sqlite",
"runtime-tokio",
] }
lancedb = "0.15"
fastembed = "4"
arrow-array = "53.3"
arrow-schema = "53.3"
indexmap = "2.7"
dashmap = "6.1"
toml = "0.8"
ort = { version = "2.0.0-rc.9", default-features = false, features = [
"download-binaries",
"tls-native",
] }
# Proper IOS Support
# TODO: Fix the fork - it has compilation errors with TimerFuture
@@ -102,22 +102,22 @@ ort = { version = "2.0.0-rc.9", default-features = false, featur
[profile.dev]
# Make compilation faster on macOS
codegen-units = 256
debug = 0
incremental = true
lto = false
opt-level = 0
codegen-units = 256
debug = 0
incremental = true
lto = false
opt-level = 0
split-debuginfo = "unpacked"
strip = "none"
strip = "none"
[profile.dev-debug]
inherits = "dev"
# Enables debugger
codegen-units = 256
debug = "full"
incremental = true
lto = "off"
opt-level = 0
debug = "full"
incremental = true
lto = "off"
opt-level = 0
# split-debuginfo = "none"
strip = "none"
@@ -128,22 +128,22 @@ opt-level = 3
# Set the default for dependencies, except workspace members.
[profile.dev.package."*"]
incremental = false
opt-level = 3
opt-level = 3
# Set the default for dependencies, except workspace members.
[profile.dev-debug.package."*"]
debug = "full"
debug = "full"
incremental = false
inherits = "dev"
opt-level = 3
inherits = "dev"
opt-level = 3
# Optimize release builds
[profile.release]
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size
panic = "unwind" # Sadly we need unwind to avoid unexpected crashes on third party crates
strip = true # Remove debug symbols
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
lto = true # Enables link to optimizations
opt-level = "s" # Optimize for binary size
panic = "unwind" # Sadly we need unwind to avoid unexpected crashes on third party crates
strip = true # Remove debug symbols
# Aphrody local patch: ffmpeg-sys-next 7.1.3 built with bindgen 0.72 (0.70 emits empty structs
# with libclang 21+). See patches/README.md.
+1
View File
@@ -1,4 +1,5 @@
<!-- SPDX-License-Identifier: FSL-1.1-ALv2 -->
# Drive backend migration workspace
This nested Cargo workspace contains the additive OS-owned copy of the
@@ -18,8 +18,8 @@ requires = ["python3 >= 3.9"]
[adapter.oauth]
token_url = "https://oauth2.googleapis.com/token"
scopes = [
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.metadata",
"https://www.googleapis.com/auth/gmail.readonly",
"https://www.googleapis.com/auth/gmail.metadata",
]
token_key = "oauth_token"
@@ -48,5 +48,12 @@ primary_model = "contact"
title = "name"
preview = "notes"
subtitle = "organization"
search_fields = ["name", "organization", "job_title", "emails", "phones", "notes"]
search_fields = [
"name",
"organization",
"job_title",
"emails",
"phones",
"notes",
]
date_field = "modified"
+17 -17
View File
@@ -1,6 +1,6 @@
[package]
edition = "2021"
name = "sd-cli"
name = "sd-cli"
version = "2.0.0-alpha.2"
[features]
@@ -12,20 +12,20 @@ speech-to-text = ["sd-core/speech-to-text"]
ai = ["sd-core/ai"]
[dependencies]
anyhow = "1"
bincode = { version = "2.0.0-rc.3", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
anyhow = "1"
bincode = { version = "2.0.0-rc.3", features = ["serde"] }
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
comfy-table = "7.1"
crossterm = "0.27"
dirs = "5.0"
indicatif = "0.17"
inquire = "0.7"
qr2term = "0.3"
ratatui = "0.26"
reqwest = { version = "0.12", features = ["json"] }
sd-core = { path = "../../core", features = ["cli"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["serde", "v4"] }
crossterm = "0.27"
dirs = "5.0"
indicatif = "0.17"
inquire = "0.7"
qr2term = "0.3"
ratatui = "0.26"
reqwest = { version = "0.12", features = ["json"] }
sd-core = { path = "../../core", features = ["cli"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["serde", "v4"] }
+106 -102
View File
@@ -1,6 +1,6 @@
[package]
edition = "2021"
name = "sd-core"
name = "sd-core"
version = "2.0.0-alpha.2"
autobins = true
@@ -25,20 +25,23 @@ wasm = ["dep:wasmer", "dep:wasmer-middlewares"]
[dependencies]
# Async runtime
async-channel = { workspace = true }
async-trait = "0.1"
futures = "0.3"
tokio = { version = "1.40", features = ["full"] }
async-trait = "0.1"
futures = "0.3"
tokio = { version = "1.40", features = ["full"] }
# Database
sea-orm = { version = "1.1", features = [
"macros",
"runtime-tokio-rustls",
"sqlx-sqlite",
"uuid",
"with-chrono",
"with-json"
"macros",
"runtime-tokio-rustls",
"sqlx-sqlite",
"uuid",
"with-chrono",
"with-json",
] }
sea-orm-migration = { version = "1.1", features = [
"runtime-tokio-rustls",
"sqlx-sqlite",
] }
sea-orm-migration = { version = "1.1", features = ["runtime-tokio-rustls", "sqlx-sqlite"] }
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }
# API (temporarily disabled)
@@ -51,14 +54,7 @@ serde_json = "1.0"
# HTTP client (for model downloads)
reqwest = { version = "0.11", features = ["stream"] }
specta = { git = "https://github.com/jamiepine/specta", branch = "main", features = [
"chrono",
"derive",
"export",
"serde",
"serde_json",
"uuid"
] }
specta = { git = "https://github.com/jamiepine/specta", branch = "main", features = ["chrono", "derive", "export", "serde", "serde_json", "uuid"] }
specta-serde = { git = "https://github.com/jamiepine/specta", branch = "main" }
specta-swift = { git = "https://github.com/jamiepine/specta", branch = "main" }
specta-typescript = { git = "https://github.com/jamiepine/specta", branch = "main" }
@@ -68,7 +64,7 @@ hostname = "^0.4"
# Error handling
anyhow = "1.0"
anyhow = "1.0"
thiserror = "1.0"
# Text processing
@@ -76,41 +72,41 @@ regex = "1.11"
# File operations
blake3 = "1.5" # Content addressing
bytes = "1.7" # Efficient byte buffers for I/O operations
hex = "0.4" # Hex encoding for volume fingerprints
notify = "6.1" # File system watching
sha2 = "0.10" # SHA-256 hashing for CAS IDs
blake3 = "1.5" # Content addressing
bytes = "1.7" # Efficient byte buffers for I/O operations
hex = "0.4" # Hex encoding for volume fingerprints
notify = "6.1" # File system watching
sha2 = "0.10" # SHA-256 hashing for CAS IDs
# Cloud storage integration
opendal = { version = "0.54", features = [
"services-s3",
"services-gdrive",
"services-onedrive",
"services-dropbox",
"services-azblob",
"services-gcs",
"services-s3",
"services-gdrive",
"services-onedrive",
"services-dropbox",
"services-azblob",
"services-gcs",
] }
# Logging
tracing = "0.1"
tracing-appender = "0.2"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# WASM Plugin System
wasmer = { version = "4.2", optional = true }
wasmer = { version = "4.2", optional = true }
wasmer-middlewares = { version = "4.2", optional = true }
# Indexer rules engine
futures-concurrency = "7.6"
gix-ignore = { version = "0.11", features = ["serde"] }
globset = { version = "0.4", features = ["serde1"] }
gix-ignore = { version = "0.11", features = ["serde"] }
globset = { version = "0.4", features = ["serde1"] }
# Job system dependencies
inventory = "0.3" # Automatic job registration
job-derive = { path = "../crates/job-derive" } # Job derive macros
rmp = "0.8" # MessagePack core types
rmp-serde = "1.3" # MessagePack serialization for job state
inventory = "0.3" # Automatic job registration
job-derive = { path = "../crates/job-derive" } # Job derive macros
rmp = "0.8" # MessagePack core types
rmp-serde = "1.3" # MessagePack serialization for job state
sd-task-system = { path = "../crates/task-system" }
# Archive system - indexes external data sources (emails, notes, etc.)
@@ -120,19 +116,19 @@ sd-archive = { path = "../crates/archive" }
# lancedb = "0.15" # Embedded vector database (conflicts with gpui)
# Media processing dependencies
blurhash = "0.2"
image = "0.25"
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
sd-fs-watcher = { path = "../crates/fs-watcher" }
sd-images = { path = "../crates/images" }
blurhash = "0.2"
image = "0.25"
sd-ffmpeg = { path = "../crates/ffmpeg", optional = true }
sd-fs-watcher = { path = "../crates/fs-watcher" }
sd-images = { path = "../crates/images" }
sd-media-metadata = { path = "../crates/media-metadata" }
tokio-rustls = "0.26"
webp = "0.3"
tokio-rustls = "0.26"
webp = "0.3"
# Speech-to-text dependencies (optional, behind whisper feature)
whisper-rs = { version = "0.16", optional = true }
hound = { version = "3.5", optional = true } # WAV file reading
rubato = { version = "0.16", optional = true } # Audio resampling to 16kHz
hound = { version = "3.5", optional = true } # WAV file reading
rubato = { version = "0.16", optional = true } # Audio resampling to 16kHz
# Networking
# Iroh P2P networking
@@ -145,73 +141,73 @@ serde_cbor = "0.11"
ed25519-dalek = "2.1"
# Cryptography and networking utilities
aes-gcm = "0.10" # AES-GCM encryption for secure storage
argon2 = "0.5" # Password derivation
async-stream = "0.3" # File streaming
backoff = "0.4" # Retry logic
bincode = { version = "2.0.0-rc.3", features = ["serde"] } # Efficient encoding with serde compat
ring = "0.16" # Crypto primitives
tokio-stream = "0.1" # Async streams
aes-gcm = "0.10" # AES-GCM encryption for secure storage
argon2 = "0.5" # Password derivation
async-stream = "0.3" # File streaming
backoff = "0.4" # Retry logic
bincode = { version = "2.0.0-rc.3", features = ["serde"] } # Efficient encoding with serde compat
ring = "0.16" # Crypto primitives
tokio-stream = "0.1" # Async streams
# BIP39 wordlist support
bip39 = "2.0"
# Additional cryptography
chacha20poly1305 = "0.10" # Authenticated encryption for chunk-level security
hkdf = "0.12" # Key derivation function for session keys
hmac = "0.12"
x25519-dalek = "2.0"
hkdf = "0.12" # Key derivation function for session keys
hmac = "0.12"
x25519-dalek = "2.0"
# Network utilities
if-watch = "3.0"
if-watch = "3.0"
local-ip-address = "0.5"
# colored already defined above
# Utils
chrono = { version = "0.4", features = ["serde"] }
dirs = "5.0"
chrono = { version = "0.4", features = ["serde"] }
dirs = "5.0"
once_cell = "1.20"
rand = "0.8" # Random number generation for secure delete
sysinfo = "0.31" # Cross-platform system information
tempfile = "3.14" # Temporary directories for testing
uuid = { version = "1.11", features = ["serde", "v4", "v5", "v7"] }
rand = "0.8" # Random number generation for secure delete
sysinfo = "0.31" # Cross-platform system information
tempfile = "3.14" # Temporary directories for testing
uuid = { version = "1.11", features = ["serde", "v4", "v5", "v7"] }
# High-performance ephemeral index
memmap2 = "0.9" # Memory-mapped file support for arena storage
smallvec = { version = "1.13", features = ["serde"] } # Small vector optimization for children arrays
parking_lot = "0.12" # Fast mutex for name cache
num_cpus = "1.16" # CPU count for parallel walker
postcard = { version = "1", features = ["use-std"] } # Zero-copy binary serialization for snapshots
zstd = { version = "0.13", features = ["zstdmt"] } # Multithreaded compression for snapshots
whoami = "1.5"
memmap2 = "0.9" # Memory-mapped file support for arena storage
smallvec = { version = "1.13", features = ["serde"] } # Small vector optimization for children arrays
parking_lot = "0.12" # Fast mutex for name cache
num_cpus = "1.16" # CPU count for parallel walker
postcard = { version = "1", features = ["use-std"] } # Zero-copy binary serialization for snapshots
zstd = { version = "0.13", features = ["zstdmt"] } # Multithreaded compression for snapshots
whoami = "1.5"
# Secure storage
keyring = "3.6" # Only for device key in keychain
redb = "2.2" # Encrypted KV store for library keys, credentials, etc.
keyring = "3.6" # Only for device key in keychain
redb = "2.2" # Encrypted KV store for library keys, credentials, etc.
# CLI dependencies
clap = { version = "4.5", features = ["derive", "env"] }
colored = "2.1"
comfy-table = "7.1"
console = "0.15"
dialoguer = "0.11"
indicatif = "0.17"
owo-colors = "4.1"
clap = { version = "4.5", features = ["derive", "env"] }
colored = "2.1"
comfy-table = "7.1"
console = "0.15"
dialoguer = "0.11"
indicatif = "0.17"
owo-colors = "4.1"
supports-color = "3.0"
[build-dependencies]
chrono = { version = "0.4", features = ["serde"] }
colored = "2.1"
comfy-table = "7.1"
console = "0.15"
crossterm = "0.28"
dialoguer = "0.11"
indicatif = "0.17"
owo-colors = "4.1"
ratatui = "0.29"
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
colored = "2.1"
comfy-table = "7.1"
console = "0.15"
crossterm = "0.28"
dialoguer = "0.11"
indicatif = "0.17"
owo-colors = "4.1"
ratatui = "0.29"
serde_json = "1.0"
supports-color = "3.0"
vergen = { version = "8", features = ["cargo", "git", "gitcl"] }
vergen = { version = "8", features = ["cargo", "git", "gitcl"] }
# Platform specific
[target.'cfg(unix)'.dependencies]
@@ -221,7 +217,15 @@ libc = "0.2"
trash = "3.3" # Native trash/recycle bin support on desktop platforms
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.52", features = ["Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Security", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_Registry", "Win32_System_Power"] }
windows-sys = { version = "0.52", features = [
"Win32_Storage_FileSystem",
"Win32_Foundation",
"Win32_Security",
"Win32_System_Ioctl",
"Win32_System_IO",
"Win32_System_Registry",
"Win32_System_Power",
] }
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
whisper-rs = { version = "0.16", features = ["metal"], optional = true }
@@ -230,13 +234,13 @@ whisper-rs = { version = "0.16", features = ["metal"], optional = true }
[target.'cfg(target_os = "ios")'.dependencies]
objc2 = "0.6"
objc2-foundation = { version = "0.3", features = [
"NSFileManager",
"NSURL",
"NSString",
"NSArray",
"NSValue",
"NSDictionary",
"NSError",
"NSFileManager",
"NSURL",
"NSString",
"NSArray",
"NSValue",
"NSDictionary",
"NSError",
] }
objc2-ui-kit = { version = "0.3", features = ["UIDevice"] }
@@ -251,4 +255,4 @@ path = "src/bin/daemon.rs"
[dev-dependencies]
pretty_assertions = "1.4"
tempfile = "3.14"
tempfile = "3.14"
+25 -23
View File
@@ -1,32 +1,34 @@
[package]
edition = "2021"
name = "sd-bench"
name = "sd-bench"
version = "0.1.0"
[dependencies]
anyhow = "1.0"
async-trait = "0.1"
blake3 = "1.5"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.5", features = ["derive", "env"] }
dirs = "5.0"
humantime = "2.1"
humantime-serde = "1.1"
indicatif = "0.17"
rand = "0.8"
regex = "1.10"
sd-core = { path = ".." }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_with = { version = "3.9", features = ["json"] }
serde_yaml = "0.9"
sysinfo = { version = "0.30", default-features = false, features = ["multithread"] }
tempfile = "3.14"
tokio = { version = "1.40", features = ["full"] }
tracing = "0.1"
anyhow = "1.0"
async-trait = "0.1"
blake3 = "1.5"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.5", features = ["derive", "env"] }
dirs = "5.0"
humantime = "2.1"
humantime-serde = "1.1"
indicatif = "0.17"
rand = "0.8"
regex = "1.10"
sd-core = { path = ".." }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_with = { version = "3.9", features = ["json"] }
serde_yaml = "0.9"
sysinfo = { version = "0.30", default-features = false, features = [
"multithread",
] }
tempfile = "3.14"
tokio = { version = "1.40", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1.11", features = ["serde", "v4"] }
walkdir = "2.5"
uuid = { version = "1.11", features = ["serde", "v4"] }
walkdir = "2.5"
[lib]
name = "sd_bench"
@@ -3,36 +3,99 @@ seed: 42424242
locations:
- path: benchdata/desktop_complex
structure:
depth: 8 # Much deeper nesting like real file systems (Documents/Projects/2024/Client/Assets/Images/Raw/...)
fanout_per_dir: 25 # More directories per level
depth: 8 # Much deeper nesting like real file systems (Documents/Projects/2024/Client/Assets/Images/Raw/...)
fanout_per_dir: 25 # More directories per level
files:
total: 50000 # 50k files - realistic but manageable size (~2-5GB total)
total: 50000 # 50k files - realistic but manageable size (~2-5GB total)
size_buckets:
# Toned down but still realistic distribution
tiny: { range: [0, 4096], share: 0.30 } # Config files, text files, small scripts
small: { range: [4096, 1048576], share: 0.40 } # Documents, code files, small images
medium: { range: [1048576, 10000000], share: 0.25 } # Photos, music, small videos (max 10MB)
large: { range: [10000000, 100000000], share: 0.05 } # Large videos, archives (max 100MB)
tiny: { range: [0, 4096], share: 0.30 } # Config files, text files, small scripts
small: { range: [4096, 1048576], share: 0.40 } # Documents, code files, small images
medium: { range: [1048576, 10000000], share: 0.25 } # Photos, music, small videos (max 10MB)
large: { range: [10000000, 100000000], share: 0.05 } # Large videos, archives (max 100MB)
extensions: [
# Documents
txt, md, pdf, doc, docx, xls, xlsx, ppt, pptx, rtf, odt,
# Images
jpg, jpeg, png, gif, bmp, tiff, webp, svg, raw, cr2, nef, dng,
# Videos
mp4, mkv, avi, mov, wmv, flv, webm, m4v,
# Audio
mp3, flac, wav, aac, ogg, m4a, wma,
# Code
js, ts, py, rs, go, java, cpp, c, h, css, html, json, xml, yaml, toml,
# Archives
zip, rar, 7z, tar, gz, bz2, xz,
# Other
exe, dmg, pkg, deb, rpm, iso, bin, log, db, sqlite
]
duplicate_ratio: 0.15 # Higher duplicate ratio - more realistic for desktops
# Documents
txt,
md,
pdf,
doc,
docx,
xls,
xlsx,
ppt,
pptx,
rtf,
odt,
# Images
jpg,
jpeg,
png,
gif,
bmp,
tiff,
webp,
svg,
raw,
cr2,
nef,
dng,
# Videos
mp4,
mkv,
avi,
mov,
wmv,
flv,
webm,
m4v,
# Audio
mp3,
flac,
wav,
aac,
ogg,
m4a,
wma,
# Code
js,
ts,
py,
rs,
go,
java,
cpp,
c,
h,
css,
html,
json,
xml,
yaml,
toml,
# Archives
zip,
rar,
7z,
tar,
gz,
bz2,
xz,
# Other
exe,
dmg,
pkg,
deb,
rpm,
iso,
bin,
log,
db,
sqlite,
]
duplicate_ratio: 0.15 # Higher duplicate ratio - more realistic for desktops
content_gen:
mode: partial # Faster generation while still providing realistic content hashing
mode: partial # Faster generation while still providing realistic content hashing
sample_block_size: 10240
magic_headers: true
media:
generate_thumbnails: false # Keep generation fast for testing
generate_thumbnails: false # Keep generation fast for testing
@@ -3,35 +3,177 @@ seed: 99999999
locations:
- path: benchdata/desktop_extreme
structure:
depth: 12 # Very deep nesting like real complex file systems
fanout_per_dir: 30 # Many directories per level
depth: 12 # Very deep nesting like real complex file systems
fanout_per_dir: 30 # Many directories per level
files:
total: 100000 # 100k files - still complex but manageable (~5-10GB total)
total: 100000 # 100k files - still complex but manageable (~5-10GB total)
size_buckets:
# Realistic but toned down distribution
tiny: { range: [0, 1024], share: 0.35 } # Lots of small config/cache files
small: { range: [1024, 65536], share: 0.30 } # Text files, small images, code
medium: { range: [65536, 5000000], share: 0.25 } # Documents, photos, music (max 5MB)
large: { range: [5000000, 200000000], share: 0.10 } # Videos, large archives (max 200MB)
tiny: { range: [0, 1024], share: 0.35 } # Lots of small config/cache files
small: { range: [1024, 65536], share: 0.30 } # Text files, small images, code
medium: { range: [65536, 5000000], share: 0.25 } # Documents, photos, music (max 5MB)
large: { range: [5000000, 200000000], share: 0.10 } # Videos, large archives (max 200MB)
extensions: [
# Comprehensive file type list for realistic desktop
txt, md, pdf, doc, docx, xls, xlsx, ppt, pptx, rtf, odt, pages, numbers, keynote,
jpg, jpeg, png, gif, bmp, tiff, webp, svg, raw, cr2, nef, dng, arw, orf, rw2, psd, ai, eps,
mp4, mkv, avi, mov, wmv, flv, webm, m4v, mpg, mpeg, 3gp, f4v, asf, rm, rmvb,
mp3, flac, wav, aac, ogg, m4a, wma, ape, opus, ac3, dts,
js, ts, jsx, tsx, py, rs, go, java, cpp, c, h, hpp, cs, php, rb, swift, kt, dart, scala,
css, scss, sass, less, html, htm, xml, json, yaml, yml, toml, ini, cfg, conf,
zip, rar, 7z, tar, gz, bz2, xz, lz4, zst, cab, ace, arj,
exe, msi, dmg, pkg, deb, rpm, snap, appimage, flatpak, iso, img, bin, app,
log, db, sqlite, sql, csv, tsv, parquet, avro, proto, thrift,
ttf, otf, woff, woff2, eot, fon, pfb, pfm,
blend, fbx, obj, dae, 3ds, max, ma, mb, c4d, skp, dwg, dxf,
sketch, fig, xd, indd, qxd, afdesign, afphoto, afpub
]
duplicate_ratio: 0.20 # High duplicate ratio - very realistic for desktops with backups/copies
# Comprehensive file type list for realistic desktop
txt,
md,
pdf,
doc,
docx,
xls,
xlsx,
ppt,
pptx,
rtf,
odt,
pages,
numbers,
keynote,
jpg,
jpeg,
png,
gif,
bmp,
tiff,
webp,
svg,
raw,
cr2,
nef,
dng,
arw,
orf,
rw2,
psd,
ai,
eps,
mp4,
mkv,
avi,
mov,
wmv,
flv,
webm,
m4v,
mpg,
mpeg,
3gp,
f4v,
asf,
rm,
rmvb,
mp3,
flac,
wav,
aac,
ogg,
m4a,
wma,
ape,
opus,
ac3,
dts,
js,
ts,
jsx,
tsx,
py,
rs,
go,
java,
cpp,
c,
h,
hpp,
cs,
php,
rb,
swift,
kt,
dart,
scala,
css,
scss,
sass,
less,
html,
htm,
xml,
json,
yaml,
yml,
toml,
ini,
cfg,
conf,
zip,
rar,
7z,
tar,
gz,
bz2,
xz,
lz4,
zst,
cab,
ace,
arj,
exe,
msi,
dmg,
pkg,
deb,
rpm,
snap,
appimage,
flatpak,
iso,
img,
bin,
app,
log,
db,
sqlite,
sql,
csv,
tsv,
parquet,
avro,
proto,
thrift,
ttf,
otf,
woff,
woff2,
eot,
fon,
pfb,
pfm,
blend,
fbx,
obj,
dae,
3ds,
max,
ma,
mb,
c4d,
skp,
dwg,
dxf,
sketch,
fig,
xd,
indd,
qxd,
afdesign,
afphoto,
afpub,
]
duplicate_ratio: 0.20 # High duplicate ratio - very realistic for desktops with backups/copies
content_gen:
mode: partial # Keep generation reasonable while providing realistic content
mode: partial # Keep generation reasonable while providing realistic content
sample_block_size: 10240
magic_headers: true
media:
generate_thumbnails: false # Keep generation fast for testing
generate_thumbnails: false # Keep generation fast for testing
@@ -20,9 +20,7 @@
"memory_total_gb": 48
},
"id": "f01681c8-bc82-4e05-a26e-eeaedc3e1239",
"location_paths": [
"/Volumes/Seagate/benchdata/shape_large"
],
"location_paths": ["/Volumes/Seagate/benchdata/shape_large"],
"recipe_name": "shape_large",
"timestamp_utc": "2025-08-10T09:46:25.346840+00:00"
},
@@ -30,4 +28,4 @@
"total_gb": 11882.38
}
]
}
}
@@ -30,4 +30,4 @@
"total_gb": 11882.38
}
]
}
}
@@ -1,33 +1,31 @@
{
"runs": [
{
"dirs": 16507,
"dirs_per_s": 5.647859855612961,
"durations": {
"content_s": 2922.7,
"discovery_s": 2990.7,
"processing_s": 2984.5,
"total_s": 2922.7
},
"errors": 0,
"files": 100000,
"files_per_s": 34.214938,
"meta": {
"hardware_label": "External HDD (Seagate)",
"host": {
"cpu_model": "Apple M3 Max",
"cpu_physical_cores": 16,
"memory_total_gb": 48
},
"id": "f84809a7-b9bd-4647-b724-d837643831d7",
"location_paths": [
"/Volumes/Seagate/benchdata/shape_large"
],
"recipe_name": "shape_large",
"timestamp_utc": "2025-08-10T10:36:35.604531+00:00"
},
"scenario": "content-identification",
"total_gb": 11882.38
}
]
"runs": [
{
"dirs": 16507,
"dirs_per_s": 5.647859855612961,
"durations": {
"content_s": 2922.7,
"discovery_s": 2990.7,
"processing_s": 2984.5,
"total_s": 2922.7
},
"errors": 0,
"files": 100000,
"files_per_s": 34.214938,
"meta": {
"hardware_label": "External HDD (Seagate)",
"host": {
"cpu_model": "Apple M3 Max",
"cpu_physical_cores": 16,
"memory_total_gb": 48
},
"id": "f84809a7-b9bd-4647-b724-d837643831d7",
"location_paths": ["/Volumes/Seagate/benchdata/shape_large"],
"recipe_name": "shape_large",
"timestamp_utc": "2025-08-10T10:36:35.604531+00:00"
},
"scenario": "content-identification",
"total_gb": 11882.38
}
]
}
@@ -30,4 +30,4 @@
"total_gb": 11882.38
}
]
}
}
@@ -20,9 +20,7 @@
"memory_total_gb": 48
},
"id": "8085806f-d7e3-4cfc-9c66-9b0ec782e3be",
"location_paths": [
"/Volumes/Seagate/benchdata/shape_large"
],
"location_paths": ["/Volumes/Seagate/benchdata/shape_large"],
"recipe_name": "shape_large",
"timestamp_utc": "2025-08-10T09:42:47.682209+00:00"
},
@@ -30,4 +28,4 @@
"total_gb": 11882.38
}
]
}
}
@@ -30,4 +30,4 @@
"total_gb": 11882.38
}
]
}
}
@@ -20,9 +20,7 @@
"memory_total_gb": 48
},
"id": "d209e68e-f366-44cb-8249-d6fecc68e6d8",
"location_paths": [
"/Volumes/Seagate/benchdata/shape_medium"
],
"location_paths": ["/Volumes/Seagate/benchdata/shape_medium"],
"recipe_name": "shape_medium",
"timestamp_utc": "2025-08-10T09:46:36.101659+00:00"
},
@@ -30,4 +28,4 @@
"total_gb": 375.63
}
]
}
}
@@ -30,4 +30,4 @@
"total_gb": 375.63
}
]
}
}
@@ -20,9 +20,7 @@
"memory_total_gb": 48
},
"id": "209bd316-ca8e-472e-995b-723b94a5b666",
"location_paths": [
"/Volumes/Seagate/benchdata/shape_medium"
],
"location_paths": ["/Volumes/Seagate/benchdata/shape_medium"],
"recipe_name": "shape_medium",
"timestamp_utc": "2025-08-10T10:40:35.582507+00:00"
},
@@ -30,4 +28,4 @@
"total_gb": 375.63
}
]
}
}
@@ -30,4 +30,4 @@
"total_gb": 375.63
}
]
}
}
@@ -20,9 +20,7 @@
"memory_total_gb": 48
},
"id": "0b40d364-b963-436a-88fd-5f9f2ba9de3b",
"location_paths": [
"/Volumes/Seagate/benchdata/shape_medium"
],
"location_paths": ["/Volumes/Seagate/benchdata/shape_medium"],
"recipe_name": "shape_medium",
"timestamp_utc": "2025-08-10T09:43:15.290493+00:00"
},
@@ -30,4 +28,4 @@
"total_gb": 375.63
}
]
}
}
@@ -30,4 +30,4 @@
"total_gb": 375.63
}
]
}
}
@@ -20,9 +20,7 @@
"memory_total_gb": 48
},
"id": "acc2002e-ac20-4e83-a772-e8e15a46abe7",
"location_paths": [
"/Volumes/Seagate/benchdata/shape_small"
],
"location_paths": ["/Volumes/Seagate/benchdata/shape_small"],
"recipe_name": "shape_small",
"timestamp_utc": "2025-08-10T09:46:40.825505+00:00"
},
@@ -30,4 +28,4 @@
"total_gb": 8.21
}
]
}
}
@@ -30,4 +30,4 @@
"total_gb": 8.21
}
]
}
}
@@ -20,9 +20,7 @@
"memory_total_gb": 48
},
"id": "597e376f-7ae5-4f08-acfd-3a5eeffb1a5c",
"location_paths": [
"/Volumes/Seagate/benchdata/shape_small"
],
"location_paths": ["/Volumes/Seagate/benchdata/shape_small"],
"recipe_name": "shape_small",
"timestamp_utc": "2025-08-10T10:41:31.601581+00:00"
},
@@ -30,4 +28,4 @@
"total_gb": 8.21
}
]
}
}
@@ -30,4 +30,4 @@
"total_gb": 8.21
}
]
}
}
@@ -20,9 +20,7 @@
"memory_total_gb": 48
},
"id": "edb6828c-3605-4dde-9147-e97d2546a9b3",
"location_paths": [
"/Volumes/Seagate/benchdata/shape_small"
],
"location_paths": ["/Volumes/Seagate/benchdata/shape_small"],
"recipe_name": "shape_small",
"timestamp_utc": "2025-08-10T09:43:23.958382+00:00"
},
@@ -30,4 +28,4 @@
"total_gb": 8.21
}
]
}
}
@@ -30,4 +30,4 @@
"total_gb": 8.21
}
]
}
}
+1 -1
View File
@@ -6,4 +6,4 @@
"hardware_model": null,
"os": "macOS",
"version": "0.1.0"
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
[toolchain]
channel = "stable"
channel = "stable"
@@ -141,4 +141,4 @@ priority = 100
[file_types.metadata]
compression = true
single_file = true
single_file = true
@@ -290,4 +290,4 @@ priority = 100
[[file_types.magic_bytes]]
pattern = "4D 54 68 64"
offset = 0
priority = 100
priority = 100
@@ -135,7 +135,11 @@ header_file = true
id = "text/javascript"
name = "JavaScript"
extensions = ["js", "mjs"]
mime_types = ["text/javascript", "application/javascript", "application/x-javascript"]
mime_types = [
"text/javascript",
"application/javascript",
"application/x-javascript",
]
uti = "com.netscape.javascript-source"
category = "code"
priority = 80
@@ -90,7 +90,9 @@ priority = 90
id = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
name = "Word Document (OOXML)"
extensions = ["docx"]
mime_types = ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
mime_types = [
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
]
uti = "org.openxmlformats.wordprocessingml.document"
category = "document"
priority = 100
@@ -104,7 +106,9 @@ priority = 90
id = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
name = "Excel Spreadsheet (OOXML)"
extensions = ["xlsx"]
mime_types = ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]
mime_types = [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
]
uti = "org.openxmlformats.spreadsheetml.sheet"
category = "document"
priority = 100
@@ -118,7 +122,9 @@ priority = 90
id = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
name = "PowerPoint Presentation (OOXML)"
extensions = ["pptx"]
mime_types = ["application/vnd.openxmlformats-officedocument.presentationml.presentation"]
mime_types = [
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
]
uti = "org.openxmlformats.presentationml.presentation"
category = "document"
priority = 100
@@ -219,7 +225,7 @@ category = "document"
priority = 100
[[file_types.magic_bytes]]
pattern = "53 44 4D 45 4D 01" # "SDMEM\x01"
pattern = "53 44 4D 45 4D 01" # "SDMEM\x01"
offset = 0
priority = 100
@@ -294,4 +294,4 @@ priority = 100
[file_types.metadata]
raw_format = true
manufacturer = "Panasonic"
manufacturer = "Panasonic"
@@ -4,7 +4,10 @@
id = "application/x-debian-package"
name = "Debian Package"
extensions = ["deb"]
mime_types = ["application/x-debian-package", "application/vnd.debian.binary-package"]
mime_types = [
"application/x-debian-package",
"application/vnd.debian.binary-package",
]
category = "package"
priority = 100
@@ -18,7 +18,9 @@ priority = 100
id = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
name = "Microsoft PowerPoint (OpenXML)"
extensions = ["pptx"]
mime_types = ["application/vnd.openxmlformats-officedocument.presentationml.presentation"]
mime_types = [
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
]
uti = "org.openxmlformats.presentationml.presentation"
category = "presentation"
priority = 100
@@ -32,7 +34,9 @@ priority = 90
id = "application/vnd.openxmlformats-officedocument.presentationml.template"
name = "PowerPoint Template (OpenXML)"
extensions = ["potx"]
mime_types = ["application/vnd.openxmlformats-officedocument.presentationml.template"]
mime_types = [
"application/vnd.openxmlformats-officedocument.presentationml.template",
]
category = "presentation"
priority = 100
@@ -58,7 +62,9 @@ priority = 90
id = "application/vnd.openxmlformats-officedocument.presentationml.slideshow"
name = "PowerPoint Slideshow"
extensions = ["ppsx"]
mime_types = ["application/vnd.openxmlformats-officedocument.presentationml.slideshow"]
mime_types = [
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
]
category = "presentation"
priority = 100
@@ -18,7 +18,9 @@ priority = 100
id = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
name = "Microsoft Excel (OpenXML)"
extensions = ["xlsx"]
mime_types = ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]
mime_types = [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
]
uti = "org.openxmlformats.spreadsheetml.sheet"
category = "spreadsheet"
priority = 100
@@ -32,7 +34,9 @@ priority = 90
id = "application/vnd.openxmlformats-officedocument.spreadsheetml.template"
name = "Excel Template (OpenXML)"
extensions = ["xltx"]
mime_types = ["application/vnd.openxmlformats-officedocument.spreadsheetml.template"]
mime_types = [
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
]
category = "spreadsheet"
priority = 100
@@ -284,4 +284,4 @@ priority = 100
[[file_types.magic_bytes]]
pattern = "66 74 79 70"
offset = 4
priority = 90
priority = 90
@@ -134,7 +134,7 @@ This enables:
```json
{
"device_id": "uuid-of-paired-device"
"device_id": "uuid-of-paired-device"
}
```
@@ -142,23 +142,23 @@ This enables:
```json
{
"device_id": "uuid",
"device_name": "Device Name",
"libraries": [
{
"id": "uuid",
"name": "My Library",
"description": "Optional description",
"created_at": "2025-01-01T00:00:00Z",
"statistics": {
"total_entries": 1000,
"total_locations": 5,
"total_size_bytes": 1000000,
"device_count": 2
}
}
],
"is_online": true
"device_id": "uuid",
"device_name": "Device Name",
"libraries": [
{
"id": "uuid",
"name": "My Library",
"description": "Optional description",
"created_at": "2025-01-01T00:00:00Z",
"statistics": {
"total_entries": 1000,
"total_locations": 5,
"total_size_bytes": 1000000,
"device_count": 2
}
}
],
"is_online": true
}
```
@@ -168,14 +168,14 @@ This enables:
```json
{
"local_device_id": "uuid",
"remote_device_id": "uuid",
"local_library_id": "uuid",
"remote_library_id": "uuid",
"action": {
"type": "RegisterOnly"
},
"leader_device_id": "uuid"
"local_device_id": "uuid",
"remote_device_id": "uuid",
"local_library_id": "uuid",
"remote_library_id": "uuid",
"action": {
"type": "RegisterOnly"
},
"leader_device_id": "uuid"
}
```
@@ -183,11 +183,11 @@ This enables:
```json
{
"success": true,
"local_library_id": "uuid",
"remote_library_id": "uuid",
"devices_registered": true,
"message": "Devices successfully registered for library access"
"success": true,
"local_library_id": "uuid",
"remote_library_id": "uuid",
"devices_registered": true,
"message": "Devices successfully registered for library access"
}
```
@@ -15,6 +15,7 @@ Shared event collector for capturing and analyzing events from the event bus dur
Collects all events emitted during test execution for analysis and verification.
**Basic Usage (Statistics Only):**
```rust
use helpers::EventCollector;
@@ -34,6 +35,7 @@ stats.print();
```
**Advanced Usage (Full Event Capture for Debugging):**
```rust
// Create collector with full event data capture
let mut collector = EventCollector::with_capture(&harness.core.events);
@@ -58,6 +60,7 @@ let indexing_events = collector.get_events_by_type("IndexingCompleted").await;
```
**Methods:**
- `new()` - Create collector for statistics only (lightweight)
- `with_capture()` - Create collector that captures full event data
- `collect_events(duration)` - Collect events for specified duration
@@ -77,6 +80,7 @@ Statistics about collected events with formatted output:
- **Entry** events (created/modified/deleted/moved)
**Use Cases:**
- Verifying watcher events during file operations
- Testing normalized cache updates
- Debugging event emission patterns
@@ -99,6 +103,7 @@ let harness = IndexingHarnessBuilder::new("my_test")
```
Automatically handles:
- Creating test directories
- Initializing tracing
- Setting up core and library
@@ -133,12 +138,14 @@ handle.reindex().await?;
#### Helper Classes
**`TestLocation`** - Builder for test locations:
- `write_file()` - Create files
- `create_dir()` - Create directories
- `create_filtered_files()` - Create files that should be filtered
- `index()` - Index the location
**`LocationHandle`** - Handle to indexed location:
- `count_files()`, `count_directories()`, `count_entries()`
- `get_all_entries()` - Get all indexed entries
- `verify_no_filtered_entries()` - Assert filtering worked
+8 -8
View File
@@ -1,15 +1,15 @@
[package]
name = "sd-actors"
name = "sd-actors"
version = "0.1.0"
edition.workspace = true
license.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
async-channel = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
async-channel = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
pin-project-lite = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
+10 -10
View File
@@ -1,26 +1,26 @@
[package]
name = "sd-archive"
name = "sd-archive"
version = "0.1.0"
edition = "2021"
[dependencies]
# Core dependencies - workspace versions
sqlx = { workspace = true, features = ["sqlite", "runtime-tokio", "derive"] }
tokio = { workspace = true, features = ["process"] }
serde = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
blake3 = { workspace = true }
toml = { workspace = true }
sqlx = { workspace = true, features = ["sqlite", "runtime-tokio", "derive"] }
tokio = { workspace = true, features = ["process"] }
serde = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
blake3 = { workspace = true }
toml = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
chrono = { workspace = true }
chrono = { workspace = true }
serde_json = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
futures = { workspace = true }
# Archive-specific dependencies - workspace versions
indexmap = { workspace = true, features = ["serde"] }
dashmap = { workspace = true }
dashmap = { workspace = true }
# Heavy dependencies for search/embedding - disabled for now to speed up integration
# These will be enabled once core archive system is integrated
+10 -2
View File
@@ -56,6 +56,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
### Integrated with Spacedrive
See `core/src/data/` for the Spacedrive integration wrapper that adds:
- Library-scoped lifecycle
- Job system integration
- Event bus integration
@@ -129,6 +130,7 @@ fields = [
```
**Field types:**
- `String` - Short text (up to 1KB)
- `Text` - Long text (unlimited)
- `Integer` - i64
@@ -139,6 +141,7 @@ fields = [
- `ForeignKey` - Reference to another type
**Field flags:**
- `indexed: true` - Create FTS5 index for full-text search
- `embedded: true` - Generate vector embeddings for semantic search
- `unique: true` - Enforce uniqueness constraint
@@ -153,7 +156,7 @@ Adapters communicate via stdin/stdout using line-delimited JSON.
Config object sent once at startup:
```json
{"email": "user@example.com", "cursor": "abc123"}
{ "email": "user@example.com", "cursor": "abc123" }
```
### Output (stdout)
@@ -167,6 +170,7 @@ Stream of operation objects:
```
**Operations:**
- `upsert` - Insert or update record
- `delete` - Delete record
- `link` - Create relationship between records
@@ -176,7 +180,7 @@ Stream of operation objects:
Adapters maintain cursor state for incremental sync:
```json
{"op": "cursor", "value": "next-page-token-xyz"}
{ "op": "cursor", "value": "next-page-token-xyz" }
```
The engine persists cursor state and provides it on next sync.
@@ -184,6 +188,7 @@ The engine persists cursor state and provides it on next sync.
## Dependencies
**Core:**
- `sqlx` - SQLite database operations
- `toml` - Schema parsing
- `serde` / `serde_json` - Serialization
@@ -192,10 +197,12 @@ The engine persists cursor state and provides it on next sync.
- `blake3` - Content hashing
**Search:**
- `lancedb` - Vector database
- `fastembed` - Embedding model
**Safety (optional):**
- `ort` - ONNX Runtime for Prompt Guard 2
- `tokenizers` - Text tokenization
- `hf-hub` - Model downloads
@@ -213,6 +220,7 @@ The engine persists cursor state and provides it on next sync.
- Embedding generation: ~100 records/sec (CPU bound)
**Memory:**
- Engine overhead: ~10MB
- Per-source overhead: ~5MB
- LanceDB cache: ~50MB
+32 -27
View File
@@ -1,53 +1,58 @@
[package]
name = "sd-crypto"
name = "sd-crypto"
version = "0.0.1"
authors = [
"Arnab Chakraborty <arnab@spacedrive.com>",
"Ericson Soares <ericson@spacedrive.com>",
"Jake Robinson <jake@spacedrive.com>"
"Arnab Chakraborty <arnab@spacedrive.com>",
"Ericson Soares <ericson@spacedrive.com>",
"Jake Robinson <jake@spacedrive.com>",
]
description = """
A cryptographic library that provides safe and high-level
encryption, hashing, and encoding interfaces.
"""
edition.workspace = true
keywords = ["crypto"]
license.workspace = true
readme = "README.md"
repository.workspace = true
edition.workspace = true
keywords = ["crypto"]
license.workspace = true
readme = "README.md"
repository.workspace = true
rust-version.workspace = true
[dependencies]
# Workspace dependencies
async-stream = { workspace = true }
base64 = { workspace = true }
blake3 = { workspace = true }
futures = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["io-util", "macros", "rt-multi-thread", "sync"] }
tracing = { workspace = true }
base64 = { workspace = true }
blake3 = { workspace = true }
futures = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = [
"io-util",
"macros",
"rt-multi-thread",
"sync",
] }
tracing = { workspace = true }
tracing-test = { workspace = true }
zeroize = { workspace = true, features = ["derive"] }
zeroize = { workspace = true, features = ["derive"] }
# External dependencies
aead = { version = "0.6.0-rc.2", default-features = false }
aead = { version = "0.6.0-rc.2", default-features = false }
chacha20poly1305 = "0.11.0-rc.1"
cmov = "0.4.4"
generic-array = { version = "=0.14.7", features = ["serde", "zeroize"] } # Update blocked by aead
hex = "0.4.3"
rand_chacha = "0.9.0-alpha.2"
rand_core = "0.9.0-alpha.2"
serdect = "0.3.0-pre.0"
typenum = "1.17"
cmov = "0.4.4"
generic-array = { version = "=0.14.7", features = ["serde", "zeroize"] } # Update blocked by aead
hex = "0.4.3"
rand_chacha = "0.9.0-alpha.2"
rand_core = "0.9.0-alpha.2"
serdect = "0.3.0-pre.0"
typenum = "1.17"
old-rand-core = { package = "rand_core", version = "0.6.4" }
[dev-dependencies]
paste = "1.0"
paste = "1.0"
tempfile = "3.10"
[[example]]
+4 -4
View File
@@ -1,10 +1,10 @@
[package]
name = "sd-fda"
name = "sd-fda"
version = "0.1.0"
authors = ["Jake Robinson <jake@spacedrive.com>"]
edition.workspace = true
license.workspace = true
authors = ["Jake Robinson <jake@spacedrive.com>"]
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
+17 -14
View File
@@ -1,13 +1,16 @@
[package]
name = "sd-ffmpeg"
name = "sd-ffmpeg"
version = "0.1.0"
authors = ["Ericson Soares <ericson@spacedrive.com>", "Vítor Vasconcellos <vitor@spacedrive.com>"]
description = "A simple library to generate video thumbnails using ffmpeg with the webp format"
edition.workspace = true
license.workspace = true
readme = "README.md"
repository.workspace = true
authors = [
"Ericson Soares <ericson@spacedrive.com>",
"Vítor Vasconcellos <vitor@spacedrive.com>",
]
description = "A simple library to generate video thumbnails using ffmpeg with the webp format"
edition.workspace = true
license.workspace = true
readme = "README.md"
repository.workspace = true
rust-version.workspace = true
[dependencies]
@@ -15,17 +18,17 @@ rust-version.workspace = true
sd-utils = { path = "../utils" }
# Workspace dependencies
chrono = { workspace = true, features = ["serde"] }
image = { workspace = true }
libc = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
image = { workspace = true }
libc = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt", "io-util"] }
tracing = { workspace = true }
webp = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt", "io-util"] }
tracing = { workspace = true }
webp = { workspace = true }
# Specific FFmpeg dependencies
ffmpeg-sys-next = "7.0"
[dev-dependencies]
tempfile = { workspace = true }
tokio = { workspace = true, features = ["fs", "macros", "rt"] }
tokio = { workspace = true, features = ["fs", "macros", "rt"] }
@@ -201,5 +201,3 @@ tokio::spawn(async move {
### Database-Backed Inode Lookup
For enhanced rename detection on macOS, the `PersistentIndexService` can maintain an inode cache. When a Remove event is received, check if the inode exists in your database to detect if it's actually a rename where the "new path" hasn't arrived yet.
+19 -13
View File
@@ -1,10 +1,13 @@
[package]
name = "sd-images"
name = "sd-images"
version = "0.1.0"
authors = ["Jake Robinson <jake@spacedrive.com>", "Vítor Vasconcellos <vitor@spacedrive.com>"]
edition.workspace = true
license.workspace = true
authors = [
"Jake Robinson <jake@spacedrive.com>",
"Vítor Vasconcellos <vitor@spacedrive.com>",
]
edition.workspace = true
license.workspace = true
repository.workspace = true
[features]
@@ -12,23 +15,26 @@ heif = ["dep:libheif-rs", "dep:libheif-sys"]
[dependencies]
# Workspace dependencies
image = { workspace = true }
serde = { workspace = true, optional = true, features = ["derive"] }
specta = { workspace = true, optional = true }
image = { workspace = true }
serde = { workspace = true, optional = true, features = ["derive"] }
specta = { workspace = true, optional = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tracing = { workspace = true }
# Specific Images dependencies
bincode = { version = "=2.0.0-rc.3", features = ["alloc", "derive"], optional = true }
# Disable defaults for libheif* to avoid bindgen and use pre-compiled headers
libheif-rs = { version = "1.0", default-features = false, optional = true }
libheif-rs = { version = "1.0", default-features = false, optional = true }
libheif-sys = { version = "2.1", default-features = false, optional = true }
# Keep the same resvg backend and raster semantics locally. The OS layer does
# not depend on the UI workspace; see svg_raster.rs for the adaptation.
resvg = { version = "0.47", default-features = false, features = ["system-fonts", "text"] }
resvg = { version = "0.47", default-features = false, features = [
"system-fonts",
"text",
] }
[dependencies.pdfium-render]
default-features = false
features = ["image", "pdfium_6666", "sync", "thread_safe"]
git = "https://github.com/spacedriveapp/pdfium-render.git"
rev = "983f8d8"
features = ["image", "pdfium_6666", "sync", "thread_safe"]
git = "https://github.com/spacedriveapp/pdfium-render.git"
rev = "983f8d8"
@@ -1,6 +1,6 @@
[package]
edition = "2021"
name = "job-derive"
name = "job-derive"
version = "0.1.0"
[lib]
@@ -8,5 +8,5 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
@@ -38,7 +38,3 @@ name = "analyze_sync_log"
required-features = ["cli"]
@@ -1,11 +1,11 @@
[package]
name = "sd-media-metadata"
name = "sd-media-metadata"
version = "0.1.0"
authors = [
"Ericson Soares <ericson@spacedrive.com>",
"Jake Robinson <jake@spacedrive.com>",
"Vítor Vasconcellos <vitor@spacedrive.com>"
"Ericson Soares <ericson@spacedrive.com>",
"Jake Robinson <jake@spacedrive.com>",
"Vítor Vasconcellos <vitor@spacedrive.com>",
]
edition = "2021"
@@ -15,16 +15,16 @@ ffmpeg = ["dep:sd-ffmpeg"]
[dependencies]
# Spacedrive Sub-crates
sd-ffmpeg = { path = "../ffmpeg", optional = true }
sd-utils = { path = "../utils" }
sd-utils = { path = "../utils" }
# Workspace dependencies
chrono = { workspace = true, features = ["serde"] }
image = { workspace = true }
serde = { workspace = true, features = ["derive"] }
chrono = { workspace = true, features = ["serde"] }
image = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
specta = { workspace = true, features = ["chrono", "derive"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
specta = { workspace = true, features = ["chrono", "derive"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
# Specific Media Metadata dependencies
kamadak-exif = "0.5.5"
@@ -1,13 +1,13 @@
[package]
description = "Proc macros for Spacedrive extension SDK"
edition = "2021"
name = "spacedrive-sdk-macros"
version = "0.1.0"
edition = "2021"
name = "spacedrive-sdk-macros"
version = "0.1.0"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["extra-traits", "full"] }
quote = "1.0"
syn = { version = "2.0", features = ["extra-traits", "full"] }
@@ -10,6 +10,7 @@
### `#[model]` Macro - Enhanced!
**Handles all field attributes:**
```rust
#[derive(Serialize, Deserialize, Clone)]
#[model(version = "1.0.0")]
@@ -47,6 +48,7 @@ struct Photo {
```
**What the macro does:**
1. Parses and strips field attributes
2. Generates `ExtensionModel` trait impl
3. Finds `id` or `uuid` field automatically
@@ -54,6 +56,7 @@ struct Photo {
5. Compiles successfully
**Recognized attributes:**
- `#[entry]` - References file/directory
- `#[sidecar]` - Extension-owned derivative data
- `#[metadata]` - Core-extracted metadata
@@ -68,16 +71,16 @@ struct Photo {
## Other Macros
| Macro | Status | Notes |
|-------|--------|-------|
| `#[extension]` | Working | Generates plugin_init(), job registration |
| `#[job]` | Working | FFI exports (proven in test-extension) |
| `#[model]` | Enhanced | Handles field attributes! |
| `#[agent]` | Pass-through | Needs impl block handling |
| `#[agent_memory]` | Working | Generates AgentMemory trait |
| `#[task]` | Pass-through | Needs implementation |
| `#[action]` | Pass-through | Needs implementation |
| `#[query]` | Pass-through | Needs implementation |
| Macro | Status | Notes |
| ----------------- | ------------ | ----------------------------------------- |
| `#[extension]` | Working | Generates plugin_init(), job registration |
| `#[job]` | Working | FFI exports (proven in test-extension) |
| `#[model]` | Enhanced | Handles field attributes! |
| `#[agent]` | Pass-through | Needs impl block handling |
| `#[agent_memory]` | Working | Generates AgentMemory trait |
| `#[task]` | Pass-through | Needs implementation |
| `#[action]` | Pass-through | Needs implementation |
| `#[query]` | Pass-through | Needs implementation |
---
@@ -106,6 +109,7 @@ struct PhotoAnalysis {
```
**The attributes:**
- Show design intent
- Will be processed when macro is enhanced
- Don't break compilation
@@ -118,6 +122,7 @@ struct PhotoAnalysis {
### For Full Compilation
Photos extension needs:
1. Convert async jobs to sync (match test-extension pattern)
2. OR: Enhance `#[job]` macro to support async
3. OR: Keep as aspirational reference
@@ -141,5 +146,4 @@ Photos extension needs:
---
**The macro system now supports the design! Field attributes work.**
**The macro system now supports the design! Field attributes work.**
+23 -20
View File
@@ -398,18 +398,18 @@ fn test_counter(ctx: &JobContext, state: &mut CounterState) -> Result<()> {
## Implementation Status
| Module | Status | Notes |
|--------|--------|-------|
| `ffi.rs` | Implemented | Low-level WASM imports |
| `job_context.rs` | Expanded | New methods added, existing preserved |
| `types.rs` | Expanded | All common types added |
| `vdfs.rs` | Stubs | Type-checks, `todo!()` for host calls |
| `ai.rs` | Stubs | Type-checks, `todo!()` for inference |
| `agent.rs` | Stubs | Type-checks, memory system defined |
| `models.rs` | Stubs | Type-checks, registration stubs |
| `actions.rs` | Stubs | Type-checks, preview/execute defined |
| `tasks.rs` | Stubs | Type-checks, task context defined |
| `query.rs` | Stubs | Type-checks, query context defined |
| Module | Status | Notes |
| ---------------- | ----------- | ------------------------------------- |
| `ffi.rs` | Implemented | Low-level WASM imports |
| `job_context.rs` | Expanded | New methods added, existing preserved |
| `types.rs` | Expanded | All common types added |
| `vdfs.rs` | Stubs | Type-checks, `todo!()` for host calls |
| `ai.rs` | Stubs | Type-checks, `todo!()` for inference |
| `agent.rs` | Stubs | Type-checks, memory system defined |
| `models.rs` | Stubs | Type-checks, registration stubs |
| `actions.rs` | Stubs | Type-checks, preview/execute defined |
| `tasks.rs` | Stubs | Type-checks, task context defined |
| `query.rs` | Stubs | Type-checks, query context defined |
**Macros:** All defined as pass-through (no codegen yet)
@@ -436,6 +436,7 @@ fn test_counter(ctx: &JobContext, state: &mut CounterState) -> Result<()> {
### Phase 1: Core Host Functions
Implement in `core/src/infra/extension/host_functions.rs`:
```rust
#[no_mangle]
pub extern "C" fn vdfs_query_entries(...) -> u32;
@@ -454,6 +455,7 @@ pub extern "C" fn ai_infer(...) -> u32;
### Phase 2: SDK Implementation
Replace `todo!()` with actual WASM host calls:
```rust
pub async fn get_model_by_content<T>(content_uuid: Uuid) -> Result<T> {
// Serialize request
@@ -476,6 +478,7 @@ pub async fn get_model_by_content<T>(content_uuid: Uuid) -> Result<T> {
### Phase 3: Macro Code Generation
Implement real macros:
```rust
#[model(scope = "content")]
struct PhotoAnalysis { ... }
@@ -526,16 +529,16 @@ cargo build --target wasm32-unknown-unknown --release
## Breaking Changes
**None!** The existing test-extension API is preserved:
- `ctx.log()`
- `ctx.checkpoint()`
- `ctx.check_interrupt()`
- `ctx.report_progress()`
- `ctx.increment_items()`
- `ctx.increment_bytes()`
- `ctx.log()`
- `ctx.checkpoint()`
- `ctx.check_interrupt()`
- `ctx.report_progress()`
- `ctx.increment_items()`
- `ctx.increment_bytes()`
New methods are additive only.
---
**The SDK is now complete for type-checking. Extensions can be written and will compile. Runtime implementation is the next phase.**
**The SDK is now complete for type-checking. Extensions can be written and will compile. Runtime implementation is the next phase.**
+14 -11
View File
@@ -1,19 +1,22 @@
[package]
authors = ["Spacedrive Technology Inc."]
authors = ["Spacedrive Technology Inc."]
description = "SDK for building Spacedrive WASM extensions"
edition = "2021"
license = "MIT OR Apache-2.0"
name = "spacedrive-sdk"
repository = "https://github.com/spacedriveapp/spacedrive"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
name = "spacedrive-sdk"
repository = "https://github.com/spacedriveapp/spacedrive"
version = "0.1.0"
[dependencies]
chrono = { version = "0.4", default-features = false, features = ["serde"] }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
chrono = { version = "0.4", default-features = false, features = ["serde"] }
serde = { version = "1.0", default-features = false, features = [
"alloc",
"derive",
] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
spacedrive-sdk-macros = { path = "../sdk-macros" }
thiserror = { version = "1.0", default-features = false }
uuid = { version = "1.0", default-features = false, features = ["serde", "v4"] }
thiserror = { version = "1.0", default-features = false }
uuid = { version = "1.0", default-features = false, features = ["serde", "v4"] }
[dev-dependencies]
tokio = { version = "1.0", features = ["macros", "rt"] }
+2 -1
View File
@@ -55,6 +55,7 @@ fn process_receipt(ctx: &ExtensionContext, pdf_data: &[u8]) -> Result<Uuid> {
## No Unsafe, No FFI, Just Clean Rust
**Before (raw C bindings):**
```rust
#[link(wasm_import_module = "spacedrive")]
extern "C" {
@@ -77,6 +78,7 @@ extern "C" {
```
**After (with SDK):**
```rust
let entry = ctx.vdfs().create_entry(CreateEntry { ... })?;
let ocr = ctx.ai().ocr(&pdf_data, OcrOptions::default())?;
@@ -252,4 +254,3 @@ See `extensions/test-extension/` for a complete working example.
---
**Beautiful API. Zero unsafe code. Just Rust. **
@@ -1,37 +1,42 @@
[package]
name = "sd-task-system"
name = "sd-task-system"
version = "0.1.0"
authors = ["Ericson Soares <ericson@spacedrive.com>"]
edition.workspace = true
license.workspace = true
repository.workspace = true
authors = ["Ericson Soares <ericson@spacedrive.com>"]
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
# Workspace deps
async-channel = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
async-channel = { workspace = true }
async-trait = { workspace = true }
futures = { workspace = true }
futures-concurrency = { workspace = true }
pin-project-lite = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["parking_lot", "rt-multi-thread", "sync", "time"] }
tokio-stream = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
pin-project-lite = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = [
"parking_lot",
"rt-multi-thread",
"sync",
"time",
] }
tokio-stream = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
# Specific Task System dependencies
downcast-rs = "1.2"
[dev-dependencies]
lending-stream = { workspace = true }
rand = { workspace = true }
rmp-serde = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs", "macros", "test-util"] }
lending-stream = { workspace = true }
rand = { workspace = true }
rmp-serde = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs", "macros", "test-util"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing-test = { workspace = true, features = ["no-env-filter"] }
uuid = { workspace = true, features = ["serde"] }
tracing-test = { workspace = true, features = ["no-env-filter"] }
uuid = { workspace = true, features = ["serde"] }
@@ -1,15 +1,15 @@
[package]
authors = ["Jamie Pine <jamie@spacedrive.com>"]
edition = "2021"
name = "task-validator"
name = "task-validator"
version = "0.1.0"
[dependencies]
chrono = "0.4"
clap = { version = "4.5", features = ["derive"] }
chrono = "0.4"
clap = { version = "4.5", features = ["derive"] }
comfy-table = "7.1"
glob = "0.3"
jsonschema = "0.17"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
glob = "0.3"
jsonschema = "0.17"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
+1 -1
View File
@@ -8,4 +8,4 @@ thiserror = { workspace = true }
tracing = { workspace = true }
[features]
default = []
default = []
+2 -2
View File
@@ -1,5 +1,5 @@
# Local crate patches (Aphrody Drive)
| Crate | Why |
|---|---|
| Crate | Why |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ffmpeg-sys-next` 7.1.3 | Its build script uses bindgen 0.70, which generates empty structs (`_address: u8`) against libclang 21+ while still emitting the real layout assertions, so the bindings fail to compile. This copy only raises the build-dependency to bindgen 0.72. Drop it once `ffmpeg-sys-next` ships a bindgen 0.72+ release for FFmpeg 7.1. |
@@ -12,10 +12,7 @@
[package]
name = "ffmpeg-sys-next"
version = "7.1.3"
authors = [
"meh. <meh@schizofreni.co>",
"Zhiming Wang <i@zhimingwang.org>",
]
authors = ["meh. <meh@schizofreni.co>", "Zhiming Wang <i@zhimingwang.org>"]
build = "build.rs"
links = "ffmpeg"
autolib = false
@@ -25,10 +22,7 @@ autotests = false
autobenches = false
description = "FFI bindings to FFmpeg"
readme = "README.md"
keywords = [
"audio",
"video",
]
keywords = ["audio", "video"]
license = "WTFPL"
repository = "https://github.com/zmwangx/rust-ffmpeg-sys"
@@ -102,12 +96,12 @@ build-videotoolbox = ["build"]
build-vulkan = ["build"]
build-zlib = ["build"]
default = [
"avcodec",
"avdevice",
"avfilter",
"avformat",
"swresample",
"swscale",
"avcodec",
"avdevice",
"avfilter",
"avformat",
"swresample",
"swscale",
]
non-exhaustive-enums = []
postproc = []
+10 -10
View File
@@ -1,18 +1,18 @@
[package]
edition = "2021"
name = "xtask"
name = "xtask"
publish = false
version = "0.1.0"
[dependencies]
anyhow = "1"
flate2 = "1.0"
mustache = "0.9"
anyhow = "1"
flate2 = "1.0"
mustache = "0.9"
owo-colors = "4"
reqwest = { version = "0.12", features = ["blocking", "json", "rustls-tls"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["blocking", "json", "rustls-tls"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tar = "0.4"
toml = "0.8"
which = "6.0"
xz2 = "0.1"
tar = "0.4"
toml = "0.8"
which = "6.0"
xz2 = "0.1"
@@ -5,18 +5,72 @@ export const AGE1000_MAP = {
width: 1200,
height: 760,
areas: [
{ id: "university", label: "Université", x: 70, y: 120, w: 245, h: 210, color: "#315568" },
{ id: "city", label: "Centre urbain", x: 280, y: 160, w: 600, h: 430, color: "#284258" },
{ id: "capsule", label: "Quartier Capsule", x: 830, y: 105, w: 300, h: 230, color: "#35536c" },
{ id: "plaza", label: "Esplanade", x: 870, y: 490, w: 260, h: 180, color: "#274b5c" },
{ id: "glacier", label: "Porte glaciaire", x: 70, y: 505, w: 245, h: 165, color: "#536a79" },
{
id: "university",
label: "Université",
x: 70,
y: 120,
w: 245,
h: 210,
color: "#315568",
},
{
id: "city",
label: "Centre urbain",
x: 280,
y: 160,
w: 600,
h: 430,
color: "#284258",
},
{
id: "capsule",
label: "Quartier Capsule",
x: 830,
y: 105,
w: 300,
h: 230,
color: "#35536c",
},
{
id: "plaza",
label: "Esplanade",
x: 870,
y: 490,
w: 260,
h: 180,
color: "#274b5c",
},
{
id: "glacier",
label: "Porte glaciaire",
x: 70,
y: 505,
w: 245,
h: 165,
color: "#536a79",
},
],
landmarks: [
{ id: "university", label: "West City University", x: 115, y: 195, w: 150, h: 86 },
{
id: "university",
label: "West City University",
x: 115,
y: 195,
w: 150,
h: 86,
},
{ id: "squad", label: "Great Saiya Squad", x: 365, y: 190, w: 165, h: 76 },
{ id: "plaza", label: "Central Plaza", x: 565, y: 320, w: 155, h: 100 },
{ id: "capsule", label: "Capsule Complex", x: 900, y: 175, w: 150, h: 90 },
{ id: "monorail", label: "Monorail Station", x: 675, y: 525, w: 135, h: 55 },
{
id: "monorail",
label: "Monorail Station",
x: 675,
y: 525,
w: 135,
h: 55,
},
{ id: "glacier", label: "Glacier Outpost", x: 115, y: 545, w: 150, h: 75 },
],
};
@@ -82,7 +136,12 @@ export function drawAge1000Map(ctx, width, height) {
ctx.stroke();
ctx.fillStyle = "#fff";
ctx.font = "600 15px system-ui";
ctx.fillText(place.label, place.x + 12, place.y + place.h / 2 + 5, place.w - 24);
ctx.fillText(
place.label,
place.x + 12,
place.y + place.h / 2 + 5,
place.w - 24,
);
}
ctx.fillStyle = "#e1eff8";
ctx.font = "800 24px system-ui";
@@ -10,7 +10,14 @@ export const DEFAULT_AVATAR = {
height: 1,
};
const races = new Set(["saiyan", "earthling", "namekian", "majin", "frieza", "android"]);
const races = new Set([
"saiyan",
"earthling",
"namekian",
"majin",
"frieza",
"android",
]);
const hairstyles = new Set(["spiky", "short", "crest"]);
const builds = new Set(["slim", "athletic", "broad"]);
const colorPattern = /^#[0-9a-f]{6}$/i;
@@ -25,8 +32,12 @@ export function normalizeAvatar(value) {
race: races.has(avatar.race) ? avatar.race : DEFAULT_AVATAR.race,
skin: colorPattern.test(avatar.skin) ? avatar.skin : DEFAULT_AVATAR.skin,
hair: colorPattern.test(avatar.hair) ? avatar.hair : DEFAULT_AVATAR.hair,
outfit: colorPattern.test(avatar.outfit) ? avatar.outfit : DEFAULT_AVATAR.outfit,
hairstyle: hairstyles.has(avatar.hairstyle) ? avatar.hairstyle : DEFAULT_AVATAR.hairstyle,
outfit: colorPattern.test(avatar.outfit)
? avatar.outfit
: DEFAULT_AVATAR.outfit,
hairstyle: hairstyles.has(avatar.hairstyle)
? avatar.hairstyle
: DEFAULT_AVATAR.hairstyle,
build: builds.has(avatar.build) ? avatar.build : DEFAULT_AVATAR.build,
height: Number.isFinite(Number(avatar.height))
? Math.max(0.85, Math.min(1.15, Number(avatar.height)))
@@ -36,7 +47,9 @@ export function normalizeAvatar(value) {
export function loadAvatar(storage) {
try {
return normalizeAvatar(JSON.parse(storage.getItem("dragon-pixel-avatar") || "null"));
return normalizeAvatar(
JSON.parse(storage.getItem("dragon-pixel-avatar") || "null"),
);
} catch {
return { ...DEFAULT_AVATAR };
}
@@ -31,7 +31,10 @@ export function step(g, input, dt) {
p.transformed = Math.max(0, p.transformed - dt);
const move = (input.right ? 1 : 0) - (input.left ? 1 : 0);
if (move) p.facing = move;
p.x = Math.max(24, Math.min(936, p.x + move * (p.style === "speed" ? 280 : 215) * dt));
p.x = Math.max(
24,
Math.min(936, p.x + move * (p.style === "speed" ? 280 : 215) * dt),
);
if (input.jump && p.y >= 320) p.vy = -420;
p.vy += 980 * dt;
p.y = Math.min(320, p.y + p.vy * dt);
@@ -46,14 +49,23 @@ export function step(g, input, dt) {
p.combo = (p.combo % 3) + 1;
p.cooldown = 0.28;
for (const e of g.enemies)
if (Math.abs(e.x - p.x) < 85 && Math.abs(e.y - p.y) < 65 && (e.x - p.x) * p.facing >= 0) {
if (
Math.abs(e.x - p.x) < 85 &&
Math.abs(e.y - p.y) < 65 &&
(e.x - p.x) * p.facing >= 0
) {
e.hp -= (10 + p.combo * 3) * power;
e.x += p.facing * 18;
}
} else if (input.blast && !p.cooldown && p.energy >= 20) {
p.energy -= 20;
p.cooldown = 0.45;
g.shots.push({ x: p.x + p.facing * 24, y: p.y - 20, vx: p.facing * 520, damage: 24 * power });
g.shots.push({
x: p.x + p.facing * 24,
y: p.y - 20,
vx: p.facing * 520,
damage: 24 * power,
});
}
if (!g.enemies.length && g.spawn <= 0) {
if (g.wave === 5) {
@@ -62,13 +74,22 @@ export function step(g, input, dt) {
}
g.wave++;
for (let i = 0; i < g.wave + 1; i++)
g.enemies.push({ x: 660 + i * 45, y: 320, hp: 35 + g.wave * 8, cooldown: 0.5 + i * 0.2 });
g.enemies.push({
x: 660 + i * 45,
y: 320,
hp: 35 + g.wave * 8,
cooldown: 0.5 + i * 0.2,
});
}
g.spawn = Math.max(0, g.spawn - dt);
for (const s of g.shots) {
s.x += s.vx * dt;
for (const e of g.enemies)
if (e.hp > 0 && Math.abs(e.x - s.x) < 28 && Math.abs(e.y - 20 - s.y) < 38) {
if (
e.hp > 0 &&
Math.abs(e.x - s.x) < 28 &&
Math.abs(e.y - 20 - s.y) < 38
) {
e.hp -= s.damage;
s.x = -1000;
break;
@@ -83,8 +104,13 @@ export function step(g, input, dt) {
}
e.cooldown -= dt;
const distance = p.x - e.x;
if (Math.abs(distance) > 42) e.x += Math.sign(distance) * (65 + g.wave * 8) * dt;
if (Math.abs(distance) < 55 && Math.abs(p.y - e.y) < 55 && e.cooldown <= 0) {
if (Math.abs(distance) > 42)
e.x += Math.sign(distance) * (65 + g.wave * 8) * dt;
if (
Math.abs(distance) < 55 &&
Math.abs(p.y - e.y) < 55 &&
e.cooldown <= 0
) {
p.hp = Math.max(0, p.hp - (p.transformed ? 3 : 6));
e.cooldown = 0.85;
}
@@ -96,7 +122,12 @@ export function step(g, input, dt) {
}
export function pointerDirection(targetX, playerX, tolerance = 12) {
if (targetX === null || !Number.isFinite(targetX) || !Number.isFinite(playerX)) return null;
if (
targetX === null ||
!Number.isFinite(targetX) ||
!Number.isFinite(playerX)
)
return null;
const difference = targetX - playerX;
if (Math.abs(difference) < tolerance) return null;
return difference < 0 ? "left" : "right";
@@ -2,8 +2,16 @@
import { test, expect } from "bun:test";
import { createGame, pointerAction, pointerDirection, step } from "./engine.js";
import { readGamepadInput } from "./gamepad.js";
import { DEFAULT_AVATAR, loadAvatar, normalizeAvatar, saveAvatar } from "./avatar.js";
import { buildLoreSearchRequest, filterLoreSearchPayload } from "./lore-search.js";
import {
DEFAULT_AVATAR,
loadAvatar,
normalizeAvatar,
saveAvatar,
} from "./avatar.js";
import {
buildLoreSearchRequest,
filterLoreSearchPayload,
} from "./lore-search.js";
import {
buildLoreCatalogRequest,
normalizeLoreCatalogPayload,
@@ -121,7 +129,9 @@ test("avatar creation validates and persists local character choices", () => {
};
expect(saveAvatar(storage, values)).toEqual({ ...values, name: "Nova" });
expect(loadAvatar(storage)).toEqual({ ...values, name: "Nova" });
expect(normalizeAvatar({ ...values, name: "", race: "alien", skin: "red" })).toEqual({
expect(
normalizeAvatar({ ...values, name: "", race: "alien", skin: "red" }),
).toEqual({
...DEFAULT_AVATAR,
hair: values.hair,
outfit: values.outfit,
@@ -130,13 +140,24 @@ test("avatar creation validates and persists local character choices", () => {
height: values.height,
});
expect(loadAvatar({ getItem: () => "{" })).toEqual(DEFAULT_AVATAR);
for (const race of ["saiyan", "earthling", "namekian", "majin", "frieza", "android"])
for (const race of [
"saiyan",
"earthling",
"namekian",
"majin",
"frieza",
"android",
])
expect(normalizeAvatar({ ...values, race }).race).toBe(race);
expect(normalizeAvatar({ ...values, build: "slim", height: 0.85 })).toMatchObject({
expect(
normalizeAvatar({ ...values, build: "slim", height: 0.85 }),
).toMatchObject({
build: "slim",
height: 0.85,
});
expect(normalizeAvatar({ ...values, build: "unknown", height: 1.5 })).toMatchObject({
expect(
normalizeAvatar({ ...values, build: "unknown", height: 1.5 }),
).toMatchObject({
build: "athletic",
height: 1.15,
});
@@ -179,7 +200,9 @@ test("manga series search scopes the query and filters mixed RAG results", () =>
});
test("manga series search rejects unsupported source identifiers", () => {
expect(buildLoreSearchRequest("Goku", "manga-dbs").error).toContain("n'est pas disponible");
expect(buildLoreSearchRequest("Goku", "manga-dbs").error).toContain(
"n'est pas disponible",
);
expect(buildLoreSearchRequest("x", "dragonball").error).toContain("2 et 120");
});
@@ -239,7 +262,9 @@ test("Super catalogue sorts chapter IDs and rejects malformed entries", () => {
completeStandardSet: false,
contiguousObservedRange: false,
});
expect(() => normalizeLoreCatalogPayload({ volumes: [] }, "invalid")).toThrow();
expect(() =>
normalizeLoreCatalogPayload({ volumes: [] }, "invalid"),
).toThrow();
});
test("RAG coverage reports global manga passage counts without inventing missing data", () => {
@@ -262,12 +287,15 @@ test("RAG coverage reports global manga passage counts without inventing missing
});
test("multiplayer uses the local port in development and same-origin TLS in production", () => {
expect(buildMultiplayerSocketUrl(new URL("http://localhost:4317/"), "test-room")).toBe(
"ws://localhost:4320/ws/test-room",
);
expect(buildMultiplayerSocketUrl(new URL("https://game.example.com/"), "test-room")).toBe(
"wss://game.example.com/ws/test-room",
);
expect(
buildMultiplayerSocketUrl(new URL("http://localhost:4317/"), "test-room"),
).toBe("ws://localhost:4320/ws/test-room");
expect(
buildMultiplayerSocketUrl(
new URL("https://game.example.com/"),
"test-room",
),
).toBe("wss://game.example.com/ws/test-room");
expect(
buildMultiplayerSocketUrl(
new URL("https://game.example.com/"),
@@ -276,7 +304,11 @@ test("multiplayer uses the local port in development and same-origin TLS in prod
),
).toBe("wss://socket.example.net/rooms/ws/test-room");
expect(
buildMultiplayerSocketUrl(new URL("https://game.example.com/"), "test-room", "/multiplayer"),
buildMultiplayerSocketUrl(
new URL("https://game.example.com/"),
"test-room",
"/multiplayer",
),
).toBe("wss://game.example.com/multiplayer/ws/test-room");
expect(() =>
buildMultiplayerSocketUrl(new URL("https://game.example.com/"), "bad room"),
+82 -37
View File
@@ -109,8 +109,10 @@ const keys = {
t: "transform",
};
for (const [key, field] of Object.entries(avatarFields))
field.value = key === "height" ? String(Math.round(avatar.height * 100)) : avatar[key];
document.querySelector("#avatar-height-value").value = `${Math.round(avatar.height * 100)} %`;
field.value =
key === "height" ? String(Math.round(avatar.height * 100)) : avatar[key];
document.querySelector("#avatar-height-value").value =
`${Math.round(avatar.height * 100)} %`;
for (const field of Object.values(avatarFields)) {
field.addEventListener("input", () => {
const values = Object.fromEntries(
@@ -120,7 +122,8 @@ for (const field of Object.values(avatarFields)) {
]),
);
avatar = normalizeAvatar(values);
document.querySelector("#avatar-height-value").value = `${Math.round(avatar.height * 100)} %`;
document.querySelector("#avatar-height-value").value =
`${Math.round(avatar.height * 100)} %`;
try {
avatar = saveAvatar(localStorage, avatar);
} catch {
@@ -152,7 +155,8 @@ document.querySelector("#pause").addEventListener("click", () => {
document.querySelector("#join-room").addEventListener("click", async () => {
const room = roomName.value.trim();
if (!/^[A-Za-z0-9_-]{1,32}$/.test(room)) {
multiplayerStatus.textContent = "Utilise 1 à 32 lettres, chiffres, tirets ou traits bas.";
multiplayerStatus.textContent =
"Utilise 1 à 32 lettres, chiffres, tirets ou traits bas.";
return;
}
const joinButton = document.querySelector("#join-room");
@@ -178,7 +182,9 @@ document.querySelector("#join-room").addEventListener("click", async () => {
} catch {
// An omitted config uses the local development port or the page's own host.
}
const socket = new WebSocket(buildMultiplayerSocketUrl(window.location, room, configuredBase));
const socket = new WebSocket(
buildMultiplayerSocketUrl(window.location, room, configuredBase),
);
multiplayerSocket = socket;
socket.addEventListener("open", () => {
if (multiplayerSocket !== socket) return;
@@ -203,7 +209,8 @@ document.querySelector("#join-room").addEventListener("click", async () => {
});
socket.addEventListener("error", () => {
if (multiplayerSocket === socket)
multiplayerStatus.textContent = "Serveur Rust indisponible; le mode solo reste actif.";
multiplayerStatus.textContent =
"Serveur Rust indisponible; le mode solo reste actif.";
});
socket.addEventListener("close", () => {
if (multiplayerSocket !== socket) return;
@@ -246,7 +253,10 @@ for (const arenaCanvas of [canvas, webGpuCanvas, bevyCanvas]) {
}
if (event.button !== 0) return;
const bounds = arenaCanvas.getBoundingClientRect();
mouseTarget = Math.max(0, Math.min(960, ((event.clientX - bounds.left) / bounds.width) * 960));
mouseTarget = Math.max(
0,
Math.min(960, ((event.clientX - bounds.left) / bounds.width) * 960),
);
});
arenaCanvas.addEventListener("pointermove", (event) => {
if (event.buttons & 1) {
@@ -257,10 +267,14 @@ for (const arenaCanvas of [canvas, webGpuCanvas, bevyCanvas]) {
);
}
});
arenaCanvas.addEventListener("pointerup", (event) => held.delete(pointerAction(event.button)));
arenaCanvas.addEventListener("pointerup", (event) =>
held.delete(pointerAction(event.button)),
);
for (const event of ["pointercancel", "lostpointercapture"])
arenaCanvas.addEventListener(event, () => held.delete("blast"));
arenaCanvas.addEventListener("contextmenu", (event) => event.preventDefault());
arenaCanvas.addEventListener("contextmenu", (event) =>
event.preventDefault(),
);
}
window.addEventListener("gamepadconnected", () => {
controllerConnected = true;
@@ -432,7 +446,8 @@ function draw() {
canvas.hidden = false;
}
}
if (!bevyApi && webGpuArena) webGpuArena.render(game, avatar, multiplayerPlayers);
if (!bevyApi && webGpuArena)
webGpuArena.render(game, avatar, multiplayerPlayers);
else if (!bevyApi) draw2d();
drawAge1000Map(mapContext, mapCanvas.width, mapCanvas.height);
}
@@ -441,15 +456,22 @@ function frame(now) {
last = now;
const gamepad = [...navigator.getGamepads()].find(Boolean);
controllerConnected = Boolean(gamepad);
const { input: gamepadInput, startPressed: gamepadStartPressed } = readGamepadInput(gamepad);
const { input: gamepadInput, startPressed: gamepadStartPressed } =
readGamepadInput(gamepad);
let networkInput = {};
if (gamepadStartPressed && !gamepadStartWasPressed && active && game.status === "playing")
if (
gamepadStartPressed &&
!gamepadStartWasPressed &&
active &&
game.status === "playing"
)
pause(!paused);
gamepadStartWasPressed = gamepadStartPressed;
if (active && !paused) {
const input = {};
for (const action of [...held, ...pointers.values()]) input[action] = true;
for (const [action, pressed] of Object.entries(gamepadInput)) input[action] ||= pressed;
for (const [action, pressed] of Object.entries(gamepadInput))
input[action] ||= pressed;
networkInput = input;
const mouseDirection = pointerDirection(mouseTarget, game.player.x);
if (mouseDirection) input[mouseDirection] = true;
@@ -475,7 +497,10 @@ function frame(now) {
const p = game.player;
const bevyReady = bevyApi?.bevy_renderer_ready() || false;
hud.textContent = `${avatar.name} · Vie ${Math.ceil(p.hp)} / 100 · Énergie ${Math.floor(p.energy)} / 100 · Vague ${game.wave} / 5 · Score ${game.score}${p.transformed ? " · Transformation active" : ""} · ${bevyReady ? "Bevy 3D WebGPU" : webGpuArena ? "3D WebGPU" : "Canvas 2D"}`;
if (multiplayerSocket?.readyState === WebSocket.OPEN && now - lastNetworkSync >= 50) {
if (
multiplayerSocket?.readyState === WebSocket.OPEN &&
now - lastNetworkSync >= 50
) {
multiplayerSocket.send(
JSON.stringify({
type: "frame",
@@ -507,7 +532,8 @@ function renderLoreResults(payload) {
const source = document.createElement("a");
title.textContent = entry.title || entry.entity || "Passage Dragon Ball";
kind.textContent = entry.kind || entry.sourceId || "archive";
passage.textContent = entry.snippet || entry.content || "Aucun extrait disponible.";
passage.textContent =
entry.snippet || entry.content || "Aucun extrait disponible.";
source.href = entry.url || "https://dragonballfr.com/";
source.textContent = "Ouvrir la source";
source.target = "_blank";
@@ -531,10 +557,12 @@ loreForm.addEventListener("submit", async (event) => {
if (sourceId) params.set("sourceId", sourceId);
const response = await fetch(`/api/lore/search?${params}`);
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "La recherche a échoué.");
if (!response.ok)
throw new Error(payload.error || "La recherche a échoué.");
renderLoreResults(payload);
} catch (error) {
loreStatus.textContent = error.message || "Les archives sont momentanément indisponibles.";
loreStatus.textContent =
error.message || "Les archives sont momentanément indisponibles.";
}
});
@@ -544,9 +572,12 @@ loreCatalogForm.addEventListener("submit", async (event) => {
loreCatalogStatus.textContent = "Chargement des métadonnées de l’inventaire…";
loreCatalogRows.replaceChildren();
try {
const response = await fetch(`/api/lore/catalog?series=${encodeURIComponent(series)}`);
const response = await fetch(
`/api/lore/catalog?series=${encodeURIComponent(series)}`,
);
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "Le catalogue n’a pas pu être chargé.");
if (!response.ok)
throw new Error(payload.error || "Le catalogue n’a pas pu être chargé.");
if (payload.series !== series || !Array.isArray(payload.records))
throw new Error("La réponse du catalogue est invalide.");
const catalog = payload;
@@ -562,9 +593,16 @@ loreCatalogForm.addEventListener("submit", async (event) => {
row.append(volume, pages, japanesePages);
loreCatalogRows.append(row);
}
const pages = catalog.records.reduce((total, record) => total + record.pages, 0);
const pagesWithJa = catalog.records.reduce((total, record) => total + record.pagesWithJa, 0);
const label = series === "DB" ? "volumes Dragon Ball" : "chapitres Dragon Ball Super";
const pages = catalog.records.reduce(
(total, record) => total + record.pages,
0,
);
const pagesWithJa = catalog.records.reduce(
(total, record) => total + record.pagesWithJa,
0,
);
const label =
series === "DB" ? "volumes Dragon Ball" : "chapitres Dragon Ball Super";
const omitted = catalog.omittedCount
? ` ${catalog.omittedCount} fiche(s) hors série ou hors numérotation standard masquée(s).`
: "";
@@ -583,18 +621,25 @@ loreCatalogForm.addEventListener("submit", async (event) => {
}
});
document.querySelector("#lore-coverage-check").addEventListener("click", async () => {
loreCoverageStatus.textContent = "Lecture des compteurs globaux du RAG…";
try {
const response = await fetch("/api/lore/coverage", { cache: "no-store" });
const coverage = await response.json();
if (!response.ok) throw new Error(coverage.error || "La couverture n’a pas pu être chargée.");
if (!Number.isSafeInteger(coverage.chunks))
throw new Error("La réponse de couverture est invalide.");
const builtAt = coverage.builtAt ? ` · index daté ${coverage.builtAt}` : "";
loreCoverageStatus.textContent = `${formatCoverageCount(coverage.chunks)} passages au total · ${formatCoverageCount(coverage.mangaPagePassages)} pages manga · ${formatCoverageCount(coverage.mangaChapterPassages)} entrées chapitre · ${formatCoverageCount(coverage.mangaVolumePassages)} entrées volume${builtAt}. Compteurs globaux, non ventilés par série et non équivalents à une vérification OCR page par page.`;
} catch (error) {
loreCoverageStatus.textContent =
error.message || "La couverture du RAG est momentanément indisponible.";
}
});
document
.querySelector("#lore-coverage-check")
.addEventListener("click", async () => {
loreCoverageStatus.textContent = "Lecture des compteurs globaux du RAG…";
try {
const response = await fetch("/api/lore/coverage", { cache: "no-store" });
const coverage = await response.json();
if (!response.ok)
throw new Error(
coverage.error || "La couverture n’a pas pu être chargée.",
);
if (!Number.isSafeInteger(coverage.chunks))
throw new Error("La réponse de couverture est invalide.");
const builtAt = coverage.builtAt
? ` · index daté ${coverage.builtAt}`
: "";
loreCoverageStatus.textContent = `${formatCoverageCount(coverage.chunks)} passages au total · ${formatCoverageCount(coverage.mangaPagePassages)} pages manga · ${formatCoverageCount(coverage.mangaChapterPassages)} entrées chapitre · ${formatCoverageCount(coverage.mangaVolumePassages)} entrées volume${builtAt}. Compteurs globaux, non ventilés par série et non équivalents à une vérification OCR page par page.`;
} catch (error) {
loreCoverageStatus.textContent =
error.message || "La couverture du RAG est momentanément indisponible.";
}
});
@@ -12,8 +12,8 @@
<p class="eyebrow">Le mini-jeu d'Omar · Prototype original</p>
<h1>Dragon <span>Pixel</span></h1>
<p>
Charge ton énergie. Transforme-toi. Repousse cinq vagues de guerriers; clique dans l’arène
pour te déplacer.
Charge ton énergie. Transforme-toi. Repousse cinq vagues de guerriers;
clique dans l’arène pour te déplacer.
</p>
</header>
<main>
@@ -47,10 +47,18 @@
<div>
<p class="eyebrow">Atelier de personnage · sauvegarde locale</p>
<h2 id="avatar-title">Créer ton avatar</h2>
<p>Personnage original : ces choix restent dans le stockage local de ce navigateur.</p>
<p>
Personnage original : ces choix restent dans le stockage local de ce
navigateur.
</p>
</div>
<label for="avatar-name">Nom</label>
<input id="avatar-name" maxlength="20" value="Patrouilleur" autocomplete="off" />
<input
id="avatar-name"
maxlength="20"
value="Patrouilleur"
autocomplete="off"
/>
<label for="avatar-race">Race</label>
<select id="avatar-race">
<option value="saiyan">Saiyan</option>
@@ -72,11 +80,15 @@
<option value="athletic">Athlétique</option>
<option value="broad">Robuste</option>
</select>
<label for="avatar-height">Taille <output id="avatar-height-value">100 %</output></label>
<label for="avatar-height"
>Taille <output id="avatar-height-value">100 %</output></label
>
<input id="avatar-height" type="range" min="85" max="115" value="100" />
<label for="avatar-skin">Peau</label><input id="avatar-skin" type="color" value="#f0c19a" />
<label for="avatar-skin">Peau</label
><input id="avatar-skin" type="color" value="#f0c19a" />
<label for="avatar-hair">Cheveux</label
><input id="avatar-hair" type="color" value="#242b46" /> <label for="color">Tenue</label
><input id="avatar-hair" type="color" value="#242b46" />
<label for="color">Tenue</label
><input id="color" type="color" value="#63e6ff" />
</section>
<p id="hud">Prêt à entrer dans l'arène.</p>
@@ -105,16 +117,17 @@
hidden
></canvas>
<p class="render-note">
Rendu 3D Bevy/WebGPU si disponible; repli WebGPU ou Canvas 2D sinon. J/K/L/T ou les boutons
à l’écran servent à combattre.
Rendu 3D Bevy/WebGPU si disponible; repli WebGPU ou Canvas 2D sinon.
J/K/L/T ou les boutons à l’écran servent à combattre.
</p>
<section class="world-map" aria-labelledby="world-map-title">
<div>
<p class="eyebrow">Carte d’expédition · AGE 1000</p>
<h2 id="world-map-title">West City et la porte glaciaire</h2>
<p>
Schéma original inspiré des lieux décrits publiquement : campus, quartiers urbains,
complexe Capsule et point de départ vers les missions.
Schéma original inspiré des lieux décrits publiquement : campus,
quartiers urbains, complexe Capsule et point de départ vers les
missions.
</p>
</div>
<canvas
@@ -125,8 +138,8 @@
aria-label="Carte schématique originale de West City en Age 1000 avec campus, quartier Capsule, centre-ville, esplanade et avant-poste glaciaire."
></canvas>
<small
>Carte de prototype dessinée pour Dragon Pixel, sans captures ni ressources
extraites.</small
>Carte de prototype dessinée pour Dragon Pixel, sans captures ni
ressources extraites.</small
>
</section>
<section class="lore" aria-labelledby="lore-title">
@@ -134,9 +147,9 @@
<p class="eyebrow">Archives Shenron · lecture en direct</p>
<h2 id="lore-title">Explorer les archives Dragon Ball</h2>
<p>
Recherche dans le corpus Dragon Ball accessible par le service Shenron. Les résultats
restent de courts extraits sourcés ; les planches et images ne sont pas copiées dans le
jeu.
Recherche dans le corpus Dragon Ball accessible par le service
Shenron. Les résultats restent de courts extraits sourcés ; les
planches et images ne sont pas copiées dans le jeu.
</p>
</div>
<form id="lore-search">
@@ -153,7 +166,9 @@
<label for="lore-source">Corpus</label>
<select id="lore-source" name="sourceId">
<option value="">Tout le RAG Dragon Ball</option>
<option value="manga">Pages OCR des mangas (Dragon Ball et Super)</option>
<option value="manga">
Pages OCR des mangas (Dragon Ball et Super)
</option>
<option value="dragonball">Dragon Ball — 42 tomes</option>
<option value="dragonballsuper">Dragon Ball Super</option>
</select>
@@ -165,10 +180,12 @@
<ol id="lore-results" aria-label="Résultats des archives"></ol>
<div class="lore-coverage">
<h3>Mesurer la couverture du RAG</h3>
<button id="lore-coverage-check" type="button">Vérifier l’index Shenron</button>
<button id="lore-coverage-check" type="button">
Vérifier l’index Shenron
</button>
<p id="lore-coverage-status" role="status" aria-live="polite">
Les compteurs globaux ne prouvent pas la transcription complète de chaque tome ou
chapitre.
Les compteurs globaux ne prouvent pas la transcription complète de
chaque tome ou chapitre.
</p>
</div>
<div class="lore-catalog">
@@ -182,8 +199,8 @@
<button type="submit">Afficher l’inventaire</button>
</form>
<p id="lore-catalog-status" role="status" aria-live="polite">
L’inventaire indique les pages déclarées par Shenron; il ne certifie pas une
transcription OCR complète de chaque page.
L’inventaire indique les pages déclarées par Shenron; il ne certifie
pas une transcription OCR complète de chaque page.
</p>
<div
class="lore-table-wrap"
@@ -209,26 +226,30 @@
</section>
<p id="result" role="status">Choisis ton style puis lance la partie.</p>
<div class="controls" aria-label="Commandes tactiles">
<button data-action="left">← Gauche</button><button data-action="right">Droite →</button
><button data-action="jump">Saut</button><button data-action="punch">Combo</button
><button data-action="blast">Projectile</button><button data-action="charge">Charger</button
<button data-action="left">← Gauche</button
><button data-action="right">Droite →</button
><button data-action="jump">Saut</button
><button data-action="punch">Combo</button
><button data-action="blast">Projectile</button
><button data-action="charge">Charger</button
><button data-action="transform">Transformer</button>
</div>
<details>
<summary>Comment jouer</summary>
<p>
Flèches gauche/droite ou Q/D : déplacement · clic gauche/glisser : bouger · clic droit :
tir · Espace : saut · J : combo · K : projectile (20 énergie) · L : charge · T :
transformation (80 énergie, 8 secondes) · P : pause. Sur manette : stick gauche ou croix
pour bouger, A sauter, X combo, B projectile, Y charger, Start pause/reprise. Sur mobile,
maintiens les boutons. Élimine cinq vagues pour gagner. Les styles changent la vitesse ou
la puissance.
Flèches gauche/droite ou Q/D : déplacement · clic gauche/glisser :
bouger · clic droit : tir · Espace : saut · J : combo · K : projectile
(20 énergie) · L : charge · T : transformation (80 énergie, 8
secondes) · P : pause. Sur manette : stick gauche ou croix pour
bouger, A sauter, X combo, B projectile, Y charger, Start
pause/reprise. Sur mobile, maintiens les boutons. Élimine cinq vagues
pour gagner. Les styles changent la vitesse ou la puissance.
</p>
</details>
</main>
<footer>
Inspiré des mécaniques de Dragon Crystal et Warriors of the Universe. Personnages et
graphismes originaux. Solo contre IA, sans compte.
Inspiré des mécaniques de Dragon Crystal et Warriors of the Universe.
Personnages et graphismes originaux. Solo contre IA, sans compte.
</footer>
<script type="module" src="game.js"></script>
</body>
@@ -4,7 +4,9 @@ const catalogSeries = new Set(["DB", "DBS"]);
export function buildLoreCatalogRequest(series) {
const normalizedSeries = typeof series === "string" ? series.trim() : "";
if (!catalogSeries.has(normalizedSeries))
return { error: "Choisis Dragon Ball ou Dragon Ball Super dans le catalogue." };
return {
error: "Choisis Dragon Ball ou Dragon Ball Super dans le catalogue.",
};
return { series: normalizedSeries };
}
@@ -46,11 +48,18 @@ export function normalizeLoreCatalogPayload(payload, series) {
? Array.from({ length: 42 }, (_, index) => index + 1)
: firstNumber === null
? []
: Array.from({ length: lastNumber - firstNumber + 1 }, (_, index) => firstNumber + index);
const missingNumbers = expectedNumbers.filter((number) => !observedNumbers.includes(number));
: Array.from(
{ length: lastNumber - firstNumber + 1 },
(_, index) => firstNumber + index,
);
const missingNumbers = expectedNumbers.filter(
(number) => !observedNumbers.includes(number),
);
return {
series,
declaredCount: Number.isSafeInteger(payload.count) ? payload.count : payload.volumes.length,
declaredCount: Number.isSafeInteger(payload.count)
? payload.count
: payload.volumes.length,
omittedCount: payload.volumes.length - records.length,
coverage: {
observedCount: observedNumbers.length,
@@ -58,14 +67,18 @@ export function normalizeLoreCatalogPayload(payload, series) {
lastNumber,
missingNumbers,
completeStandardSet: series === "DB" && missingNumbers.length === 0,
contiguousObservedRange: firstNumber !== null && missingNumbers.length === 0,
contiguousObservedRange:
firstNumber !== null && missingNumbers.length === 0,
},
records,
};
}
export function normalizeLoreCoveragePayload(payload) {
if (!Number.isSafeInteger(payload?.index?.chunks) || !Array.isArray(payload?.sources))
if (
!Number.isSafeInteger(payload?.index?.chunks) ||
!Array.isArray(payload?.sources)
)
throw new TypeError("Réponse de couverture RAG invalide.");
const sourceCount = (sourceId, kind) => {
const entry = payload.sources.find(
@@ -75,7 +88,8 @@ export function normalizeLoreCoveragePayload(payload) {
};
return {
chunks: payload.index.chunks,
builtAt: typeof payload.index.builtAt === "string" ? payload.index.builtAt : null,
builtAt:
typeof payload.index.builtAt === "string" ? payload.index.builtAt : null,
mangaPagePassages: sourceCount("manga", "manga_page"),
mangaChapterPassages: sourceCount("db_manga_chapters", "manga_chapter"),
mangaVolumePassages: sourceCount("db_manga_volumes", "manga_volume"),
@@ -2,7 +2,10 @@
const series = {
"": { queryPrefix: "", titlePrefix: null },
manga: { queryPrefix: "", titlePrefix: null },
dragonball: { queryPrefix: "Dragon Ball tome", titlePrefix: "Dragon Ball — tome " },
dragonball: {
queryPrefix: "Dragon Ball tome",
titlePrefix: "Dragon Ball — tome ",
},
dragonballsuper: {
queryPrefix: "Dragon Ball Super",
titlePrefix: "Dragon Ball Super — chapitre ",
@@ -15,7 +18,8 @@ export function buildLoreSearchRequest(query, sourceId) {
if (normalizedQuery.length < 2 || normalizedQuery.length > 120)
return { error: "La recherche doit contenir entre 2 et 120 caractères." };
const selected = series[normalizedSource];
if (!selected) return { error: "Le corpus sélectionné n'est pas disponible." };
if (!selected)
return { error: "Le corpus sélectionné n'est pas disponible." };
return {
query: [selected.queryPrefix, normalizedQuery].filter(Boolean).join(" "),
sourceId: normalizedSource ? "manga" : "",
@@ -34,6 +38,8 @@ export function filterLoreSearchPayload(payload, sourceId) {
: [];
return {
...payload,
results: items.filter((item) => item?.title?.startsWith(titlePrefix)).slice(0, 8),
results: items
.filter((item) => item?.title?.startsWith(titlePrefix))
.slice(0, 8),
};
}
@@ -6,9 +6,12 @@ export function buildMultiplayerSocketUrl(location, room, configuredBase = "") {
throw new TypeError("Le nom de salle est invalide.");
const page = new URL(location.href);
if (page.protocol !== "http:" && page.protocol !== "https:")
throw new TypeError("Le protocole de la page ne permet pas les WebSockets.");
throw new TypeError(
"Le protocole de la page ne permet pas les WebSockets.",
);
const socketProtocol = page.protocol === "https:" ? "wss:" : "ws:";
const explicitBase = typeof configuredBase === "string" ? configuredBase.trim() : "";
const explicitBase =
typeof configuredBase === "string" ? configuredBase.trim() : "";
let socket;
if (explicitBase) {
socket = new URL(explicitBase, `${socketProtocol}//${page.host}`);
+42 -18
View File
@@ -1,5 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
import { buildLoreSearchRequest, filterLoreSearchPayload } from "./lore-search.js";
import {
buildLoreSearchRequest,
filterLoreSearchPayload,
} from "./lore-search.js";
import {
buildLoreCatalogRequest,
normalizeLoreCatalogPayload,
@@ -20,7 +23,8 @@ const files = new Set([
"dragon-pixel-wasm.js",
"dragon-pixel-wasm_bg.wasm",
]);
const shenronBase = process.env.SHENRON_API_BASE || "https://bot.dragonballfr.com";
const shenronBase =
process.env.SHENRON_API_BASE || "https://bot.dragonballfr.com";
function publicMultiplayerWebSocketBase() {
const value = process.env.DRAGON_PIXEL_WS_BASE?.trim() || "";
if (!value) return "";
@@ -52,7 +56,10 @@ const server = Bun.serve({
{ multiplayerWebSocketBase: publicMultiplayerWebSocketBase() },
{ headers: { "cache-control": "no-store" } },
);
if (pathname === "/dragon-pixel-wasm.js" || pathname === "/dragon-pixel-wasm_bg.wasm") {
if (
pathname === "/dragon-pixel-wasm.js" ||
pathname === "/dragon-pixel-wasm_bg.wasm"
) {
const file = Bun.file(
new URL(
`../../../target/wasm-package-stage/aphrody-dragon-pixel-wasm/${pathname.slice(1)}`,
@@ -62,7 +69,9 @@ const server = Bun.serve({
return file.exists().then((exists) =>
exists
? new Response(file, {
headers: pathname.endsWith(".wasm") ? { "content-type": "application/wasm" } : {},
headers: pathname.endsWith(".wasm")
? { "content-type": "application/wasm" }
: {},
})
: new Response("WASM package not built", { status: 404 }),
);
@@ -71,7 +80,8 @@ const server = Bun.serve({
const params = new URL(request.url).searchParams;
const sourceId = params.get("sourceId")?.trim() || "";
const search = buildLoreSearchRequest(params.get("q"), sourceId);
if (search.error) return Response.json({ error: search.error }, { status: 400 });
if (search.error)
return Response.json({ error: search.error }, { status: 400 });
const url = new URL("/api/public/rag/search", shenronBase);
url.searchParams.set("q", search.query);
url.searchParams.set("limit", String(search.limit));
@@ -87,7 +97,8 @@ const server = Bun.serve({
status: response.status,
headers: {
"content-type":
response.headers.get("content-type") || "application/json; charset=utf-8",
response.headers.get("content-type") ||
"application/json; charset=utf-8",
"cache-control": "no-store",
},
});
@@ -100,10 +111,13 @@ const server = Bun.serve({
},
});
try {
return Response.json(filterLoreSearchPayload(JSON.parse(body), sourceId), {
status: response.status,
headers: { "cache-control": "no-store" },
});
return Response.json(
filterLoreSearchPayload(JSON.parse(body), sourceId),
{
status: response.status,
headers: { "cache-control": "no-store" },
},
);
} catch {
return Response.json(
{ error: "La réponse des archives est illisible." },
@@ -121,7 +135,8 @@ const server = Bun.serve({
if (name === "api/lore/catalog" && request.method === "GET") {
const params = new URL(request.url).searchParams;
const catalog = buildLoreCatalogRequest(params.get("series"));
if (catalog.error) return Response.json({ error: catalog.error }, { status: 400 });
if (catalog.error)
return Response.json({ error: catalog.error }, { status: 400 });
const url = new URL("/api/public/manga/tomes", shenronBase);
url.searchParams.set("series", catalog.series);
return fetch(url, {
@@ -139,9 +154,12 @@ const server = Bun.serve({
},
});
try {
return Response.json(normalizeLoreCatalogPayload(JSON.parse(body), catalog.series), {
headers: { "cache-control": "no-store" },
});
return Response.json(
normalizeLoreCatalogPayload(JSON.parse(body), catalog.series),
{
headers: { "cache-control": "no-store" },
},
);
} catch {
return Response.json(
{ error: "La réponse du catalogue est illisible." },
@@ -168,9 +186,12 @@ const server = Bun.serve({
{ status: 502, headers: { "cache-control": "no-store" } },
);
try {
return Response.json(normalizeLoreCoveragePayload(await response.json()), {
headers: { "cache-control": "no-store" },
});
return Response.json(
normalizeLoreCoveragePayload(await response.json()),
{
headers: { "cache-control": "no-store" },
},
);
} catch {
return Response.json(
{ error: "La réponse de couverture RAG est illisible." },
@@ -180,7 +201,10 @@ const server = Bun.serve({
})
.catch(() =>
Response.json(
{ error: "La couverture publique du RAG est momentanément indisponible." },
{
error:
"La couverture publique du RAG est momentanément indisponible.",
},
{ status: 502, headers: { "cache-control": "no-store" } },
),
);
@@ -24,4 +24,3 @@ chargeur d'extensions d'une application.
La qualification Linux et ses limites sont consignées dans
[le reçu](../../../docs/aphrody/operations/workspace/rust-extension-2026-10-07.md).
Les artefacts SDK et leur provenance restent dans leur store existant.
@@ -1,5 +1,5 @@
# This action syncs "studio-staging" with "main" by force pushing every time it is run.
# Adapted from:
# Adapted from:
# https://github.community/t/user-github-pages-site-auto-deploy-from-another-branch-on-push/17912/2
on:
push:
@@ -22,12 +22,11 @@ jobs:
- name: push changes
uses: ad-m/github-push-action@0fafdd62b84042d49ec0cb92d9cac7f7ce4ec79e
with:
# Note: Github does not allow the default access token to push to
# protected branches. Using a PAT with permissions can bypass this,
# Note: Github does not allow the default access token to push to
# protected branches. Using a PAT with permissions can bypass this,
# but it triggers recursive Github Actions calls on branch updates
# (which the default access token does not). Ultimately, it's better
# to leave the studio-staging branch unprotected.
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: studio-staging
force: true
+49 -37
View File
@@ -1,45 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<title>ArtCraft</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<head>
<title>ArtCraft</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Google tag (gtag.js) -->
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-Y4Y8KMLPQ7"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-Y4Y8KMLPQ7");
</script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Y4Y8KMLPQ7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-Y4Y8KMLPQ7');
</script>
<!---->
<!-- <script crossOrigin="anonymous" src="//unpkg.com/react-scan/dist/auto.global.js"></script> -->
<!---->
<!-- <script crossOrigin="anonymous" src="//unpkg.com/react-scan/dist/auto.global.js"></script> -->
<!-- Microsoft Clarity -->
<script type="text/javascript">
(function (c, l, a, r, i, t, y) {
c[a] =
c[a] ||
function () {
(c[a].q = c[a].q || []).push(arguments);
};
t = l.createElement(r);
t.async = 1;
t.src = "https://www.clarity.ms/tag/" + i;
y = l.getElementsByTagName(r)[0];
y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "sixp9uu263");
</script>
<!-- Microsoft Clarity -->
<script type="text/javascript">
(function (c, l, a, r, i, t, y) {
c[a] = c[a] || function () {(c[a].q = c[a].q || []).push(arguments)};
t = l.createElement(r); t.async = 1; t.src = "https://www.clarity.ms/tag/" + i;
y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "sixp9uu263");
</script>
<base href="/" />
<link rel="icon" type="image/svg+xml" href="/artcraft-icon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@62..125,100..900&family=Instrument+Serif:ital@0;1&family=Geist+Mono:wght@300..800&display=swap"
rel="stylesheet" />
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
</head>
<body className="overflow-hidden bg-ui-background">
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
<base href="/" />
<link rel="icon" type="image/svg+xml" href="/artcraft-icon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@62..125,100..900&family=Instrument+Serif:ital@0;1&family=Geist+Mono:wght@300..800&display=swap"
rel="stylesheet"
/>
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
</head>
<body className="overflow-hidden bg-ui-background">
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,13 +1,12 @@
/**
* This is cached whenever vite/remix/whatever launches.
* It's fed from an environment variable.
*/
export enum BuildEnvironmentType {
Dev,
DevProxy,
Staging,
Production,
Dev,
DevProxy,
Staging,
Production,
}
export class BuildEnvironment {
@@ -20,10 +19,15 @@ export class BuildEnvironment {
}
// https://v2.vitejs.dev/guide/env-and-mode.html
console.log('Reading environment variable: `VITE_ENVIRONMENT_TYPE` to determine build environment.')
console.log(`Environment variable 'VITE_ENVIRONMENT_TYPE' = ${import.meta.env.VITE_ENVIRONMENT_TYPE}`)
console.log(
"Reading environment variable: `VITE_ENVIRONMENT_TYPE` to determine build environment.",
);
console.log(
`Environment variable 'VITE_ENVIRONMENT_TYPE' = ${import.meta.env.VITE_ENVIRONMENT_TYPE}`,
);
const environmentLabel : string | undefined = import.meta.env.VITE_ENVIRONMENT_TYPE;
const environmentLabel: string | undefined = import.meta.env
.VITE_ENVIRONMENT_TYPE;
let environmentType = BuildEnvironmentType.Production;
@@ -56,6 +60,6 @@ export class BuildEnvironment {
}
}
export function GetBuildEnvironment() : BuildEnvironment {
export function GetBuildEnvironment(): BuildEnvironment {
return BuildEnvironment.getInstance();
}
@@ -3,7 +3,8 @@ import { StorytellerApiHostStore } from "@storyteller/api";
export async function get_media_url(mediaId: string) {
//This is for prod when we have the proper info on the url.
const apiSchemeAndHost = StorytellerApiHostStore.getInstance().getApiSchemeAndHost();
const apiSchemeAndHost =
StorytellerApiHostStore.getInstance().getApiSchemeAndHost();
const url = `${apiSchemeAndHost}/v1/media_files/file/${mediaId}`;
const response = await fetch(url);
@@ -190,7 +190,9 @@ export class ApiManager {
);
}
protected parseBodyValues<T extends object, B extends object>(params: NonNullableObject<T>): B {
protected parseBodyValues<T extends object, B extends object>(
params: NonNullableObject<T>,
): B {
return Object.entries(params).reduce((allParams, [key, value]) => {
if (!value) {
return allParams;
@@ -85,5 +85,4 @@ export class EngineApi extends ApiManager {
return result;
}
}
@@ -40,7 +40,7 @@ export class JobsApi extends ApiManager {
public ListRecentJobs(): Promise<ApiResponse<Job[]>> {
const endpoint = `${this.getApiSchemeAndHost()}/v1/jobs/session`;
return this.get<{
success: boolean;
jobs: Job[];
@@ -8,7 +8,11 @@ import { ApiManager, ApiResponse } from "./ApiManager";
import { authentication } from "~/signals";
import { Visibility } from "~/enums";
import { FilterEngineCategories, FilterMediaClasses, FilterMediaType } from "~/enums";
import {
FilterEngineCategories,
FilterMediaClasses,
FilterMediaType,
} from "~/enums";
interface ListMediaQuery {
sort_ascending?: boolean;
@@ -89,7 +93,9 @@ export class MediaFilesApi extends ApiManager {
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type ? query.filter_media_type.join(",") : undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
@@ -121,7 +127,9 @@ export class MediaFilesApi extends ApiManager {
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type ? query.filter_media_type.join(",") : undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
@@ -155,7 +163,9 @@ export class MediaFilesApi extends ApiManager {
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type ? query.filter_media_type.join(",") : undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
@@ -187,7 +197,9 @@ export class MediaFilesApi extends ApiManager {
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type ? query.filter_media_type.join(",") : undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
@@ -217,7 +229,9 @@ export class MediaFilesApi extends ApiManager {
filter_media_classes: query.filter_media_classes
? query.filter_media_classes.join(",")
: undefined,
filter_media_type: query.filter_media_type ? query.filter_media_type.join(",") : undefined,
filter_media_type: query.filter_media_type
? query.filter_media_type.join(",")
: undefined,
filter_engine_categories: query.filter_engine_categories
? query.filter_engine_categories.join(",")
: undefined,
@@ -5,7 +5,9 @@ import { FilterEngineCategories, Visibility } from "~/enums";
export class MediaUploadApi extends ApiManager {
private sessionToken?: string;
private async getSessionTokenForUploadStudioShot(): Promise<ApiResponse<string>> {
private async getSessionTokenForUploadStudioShot(): Promise<
ApiResponse<string>
> {
const endpoint = `${this.getApiSchemeAndHost()}/v1/session_token`;
return await this.get<{
maybe_signed_session: string;
@@ -177,7 +179,8 @@ export class MediaUploadApi extends ApiManager {
}): Promise<ApiResponse<string>> {
const endpoint = `${this.getApiSchemeAndHost()}/v1/media_files/upload/image`;
const options: Record<string, string | number | undefined> = {
is_intermediate_system_file: EIntermediateFile[is_intermediate_system_file],
is_intermediate_system_file:
EIntermediateFile[is_intermediate_system_file],
maybe_title,
maybe_visibility: maybe_visibility?.toString(),
};
@@ -1,4 +1,3 @@
import { ApiManager, ApiResponse } from "./ApiManager";
import { Visibility } from "~/enums";
@@ -85,5 +85,4 @@ export class UserBookmarksApi extends ApiManager {
return { success: false, errorMessage: err.message };
});
}
}
@@ -7,7 +7,7 @@ interface SignupRequest {
password: string;
password_confirmation: string;
signup_source?: string;
};
}
export class UsersApi extends ApiManager {
public GetSession(): Promise<
@@ -1,5 +1,8 @@
export const normalizeCanvas = (canvas: HTMLCanvasElement, width: number, height: number): OffscreenCanvas => {
export const normalizeCanvas = (
canvas: HTMLCanvasElement,
width: number,
height: number,
): OffscreenCanvas => {
const newCanvas = new OffscreenCanvas(width, height);
newCanvas.width = width;
newCanvas.height = height;
@@ -12,9 +15,13 @@ export const normalizeCanvas = (canvas: HTMLCanvasElement, width: number, height
ctx.imageSmoothingEnabled = true;
ctx.drawImage(canvas, 0, 0, width, height);
return newCanvas;
}
};
export const drawAlphaMask = (canvas: OffscreenCanvas, width: number, height: number) => {
export const drawAlphaMask = (
canvas: OffscreenCanvas,
width: number,
height: number,
) => {
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Failed to get canvas context");
@@ -26,4 +33,4 @@ export const drawAlphaMask = (canvas: OffscreenCanvas, width: number, height: nu
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
ctx.restore();
}
};
@@ -3,7 +3,7 @@
/**
* Return the CDN origin (scheme + host + optional port, but no path)
* See: https://web.dev/articles/url-parts
*
*
* TODO(bt,2025-02-10): Make this easier to configure in development
*/
class CdnHostHelper {
@@ -15,7 +15,7 @@ class CdnHostHelper {
return CdnHostHelper.instance;
}
const origin = 'https://cdn-2.fakeyou.com';
const origin = "https://cdn-2.fakeyou.com";
const instance = new CdnHostHelper(origin);
CdnHostHelper.instance = instance;
@@ -32,6 +32,6 @@ class CdnHostHelper {
}
// TODO(bt,2025-02-10): Consolidate all of this logic.
export function GetCdnOrigin() : string {
export function GetCdnOrigin(): string {
return CdnHostHelper.getInstance().getOrigin();
}
@@ -21,7 +21,7 @@ class Cache {
return instance;
}
public canCall() : boolean {
public canCall(): boolean {
if (this.lastFetchSuccess === undefined) {
return true;
}
@@ -38,7 +38,7 @@ class Cache {
* Runs any necessary user session functions if things change.
*/
export const SyncStorytellerApiConfig = async () => {
console.log("SyncStorytellerApiConfig()")
console.log("SyncStorytellerApiConfig()");
const cache = Cache.getInstance();
const oldValue = StorytellerApiHostStore.getInstance().getApiSchemeAndHost();
@@ -52,7 +52,7 @@ export const SyncStorytellerApiConfig = async () => {
const schemeAndHost = appInfo.payload.storyteller_host;
if (!schemeAndHost) {
return ;
return;
}
console.log(`Updating hostname to ${schemeAndHost}`);
@@ -61,10 +61,10 @@ export const SyncStorytellerApiConfig = async () => {
cache.setCallSuccess();
if (oldValue !== schemeAndHost) {
console.log("SyncStorytellerApiConfig() - force session refresh")
// NB: This is a hack that might prevent the login screen from being shown
console.log("SyncStorytellerApiConfig() - force session refresh");
// NB: This is a hack that might prevent the login screen from being shown
// if the user is working in development.
await forceGetUserInfoAndSubcriptions();
}
});
}
};
@@ -51,7 +51,8 @@ export function GlobalFileDropHandler() {
const setup = async () => {
try {
const { getCurrentWebviewWindow } = await import("@tauri-apps/api/webviewWindow");
const { getCurrentWebviewWindow } =
await import("@tauri-apps/api/webviewWindow");
const { convertFileSrc } = await import("@tauri-apps/api/core");
const appWindow = getCurrentWebviewWindow();
@@ -73,7 +74,9 @@ export function GlobalFileDropHandler() {
// Find first path with a recognised extension
let modalKind: ModalType = null;
for (const p of payload.paths) {
modalKind = getModalTypeForFileName(p.split(/[/\\]/).pop() ?? "");
modalKind = getModalTypeForFileName(
p.split(/[/\\]/).pop() ?? "",
);
if (modalKind) break;
}
if (!modalKind) return;
@@ -88,7 +91,7 @@ export function GlobalFileDropHandler() {
if (skippedCount > 0) {
toast(
`${skippedCount} file${skippedCount > 1 ? "s" : ""} skipped — unsupported or mixed types`,
{ icon: "⚠️" }
{ icon: "⚠️" },
);
}
@@ -98,10 +101,11 @@ export function GlobalFileDropHandler() {
const fileName = path.split(/[/\\]/).pop() ?? "file";
const assetUrl = convertFileSrc(path);
const response = await fetch(assetUrl);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
if (!response.ok)
throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
return new File([blob], fileName);
})
}),
);
await appWindow.setFocus();
setModalType(modalKind);
@@ -121,8 +125,9 @@ export function GlobalFileDropHandler() {
};
setup();
return () => { unlisteners.forEach((fn) => fn()); };
return () => {
unlisteners.forEach((fn) => fn());
};
} else {
// HTML5 path (browser dev mode)
const handleDragEnter = (e: DragEvent) => {
@@ -135,7 +140,10 @@ export function GlobalFileDropHandler() {
const handleDragLeave = (e: DragEvent) => {
if (!e.dataTransfer?.types.includes("Files")) return;
dragCounter.current--;
if (dragCounter.current <= 0) { dragCounter.current = 0; setIsDragging(false); }
if (dragCounter.current <= 0) {
dragCounter.current = 0;
setIsDragging(false);
}
};
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
@@ -161,14 +169,14 @@ export function GlobalFileDropHandler() {
// Keep only files matching the detected modal type
const matchingFiles = allFiles.filter(
(f) => getModalTypeForFileName(f.name) === modalKind
(f) => getModalTypeForFileName(f.name) === modalKind,
);
const skippedCount = allFiles.length - matchingFiles.length;
if (skippedCount > 0) {
toast(
`${skippedCount} file${skippedCount > 1 ? "s" : ""} skipped — unsupported or mixed types`,
{ icon: "⚠️" }
{ icon: "⚠️" },
);
}
@@ -194,8 +202,12 @@ export function GlobalFileDropHandler() {
<div className="pointer-events-none fixed inset-0 z-[9999] flex items-center justify-center bg-black/70">
<div className="flex flex-col items-center gap-3 border border-dashed border-white/60 bg-ui-panel px-16 py-12 text-white">
<Maximize2Icon className="text-3xl opacity-60" />
<div className="font-display text-2xl tracking-tight">Drop to Upload</div>
<div className="hud-label text-white/50">GLB, PNG, JPG, JPEG, SPZ</div>
<div className="font-display text-2xl tracking-tight">
Drop to Upload
</div>
<div className="hud-label text-white/50">
GLB, PNG, JPG, JPEG, SPZ
</div>
</div>
</div>
)}
@@ -31,7 +31,10 @@ import {
useSelectedModel,
useSelectedProviderForModel,
} from "@storyteller/ui-model-selector";
import { CostCalculatorButton, useCostBreakdownModalStore } from "@storyteller/ui-pricing-modal";
import {
CostCalculatorButton,
useCostBreakdownModalStore,
} from "@storyteller/ui-pricing-modal";
import { HelpMenuButton } from "@storyteller/ui-help-menu";
import { CreateEmptyState } from "~/components/generation-feed/CreateEmptyState";
@@ -88,7 +91,8 @@ export const ImageTo3DExperience = ({
);
const selectedObjectModel = useSelectedModel(OBJECT_MODEL_PAGE);
const selectedObjectModelId =
(selectedObjectModel?.id as EnqueueImageTo3dObjectModel | undefined) ?? DEFAULT_OBJECT_MODEL_ID;
(selectedObjectModel?.id as EnqueueImageTo3dObjectModel | undefined) ??
DEFAULT_OBJECT_MODEL_ID;
const worldCredits = useCostBreakdownModalStore(
(s) => s.estimatedCreditsByPage[WORLD_MODEL_PAGE],
);
@@ -112,7 +116,9 @@ export const ImageTo3DExperience = ({
const [selectedResultId, setSelectedResultId] = useState<string | null>(null);
const promptContentRef = useRef<HTMLDivElement>(null);
const [promptHeight, setPromptHeight] = useState<number>(400);
const [vh, setVh] = useState<number>(typeof window !== "undefined" ? window.innerHeight : 800);
const [vh, setVh] = useState<number>(
typeof window !== "undefined" ? window.innerHeight : 800,
);
const objectResults = useImageTo3DStore((s) => s.results);
const objectStartGeneration = useImageTo3DStore((s) => s.startGeneration);
@@ -123,16 +129,26 @@ export const ImageTo3DExperience = ({
const worldStartGeneration = useImageTo3DWorldStore((s) => s.startGeneration);
const worldFailGeneration = useImageTo3DWorldStore((s) => s.failGeneration);
const worldReset = useImageTo3DWorldStore((s) => s.reset);
const pendingExternalImage = useImageTo3DWorldStore((s) => s.pendingExternalImage);
const clearPendingExternalImage = useImageTo3DWorldStore((s) => s.clearPendingExternalImage);
const pendingExternalImage = useImageTo3DWorldStore(
(s) => s.pendingExternalImage,
);
const clearPendingExternalImage = useImageTo3DWorldStore(
(s) => s.clearPendingExternalImage,
);
const pendingObjectImage = useImageTo3DStore((s) => s.pendingExternalImage);
const clearPendingObjectImage = useImageTo3DStore((s) => s.clearPendingExternalImage);
const clearPendingObjectImage = useImageTo3DStore(
(s) => s.clearPendingExternalImage,
);
const results = variant === "object" ? objectResults : worldResults;
const resetResults = variant === "object" ? objectReset : worldReset;
const [uploadedMediaToken, setUploadedMediaToken] = useState<string | null>(null);
const [uploadedMediaToken, setUploadedMediaToken] = useState<string | null>(
null,
);
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>([]);
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
[],
);
useEffect(() => {
// Read from store directly to avoid Strict Mode double-execution adding the image twice
@@ -256,7 +272,9 @@ export const ImageTo3DExperience = ({
const handleWorldFiles = async (files: File[]) => {
const remaining = MAX_WORLD_IMAGES - worldImages.length;
const imageFiles = files.filter((f) => f.type.startsWith("image/")).slice(0, remaining);
const imageFiles = files
.filter((f) => f.type.startsWith("image/"))
.slice(0, remaining);
await Promise.all(
imageFiles.map(async (file) => {
@@ -380,7 +398,8 @@ export const ImageTo3DExperience = ({
.filter((img) => img.mediaToken && !img.isUploading)
.map((img) => img.mediaToken!);
const note =
worldPrompt.trim() || `${worldImages.length} image${worldImages.length !== 1 ? "s" : ""}`;
worldPrompt.trim() ||
`${worldImages.length} image${worldImages.length !== 1 ? "s" : ""}`;
const firstPreview = worldImages[0]?.preview;
worldStartGeneration("image", note, firstPreview, subscriberId);
@@ -388,7 +407,9 @@ export const ImageTo3DExperience = ({
window.__storeTaskEnqueueMeta?.({
prompt: worldPrompt.trim() || undefined,
refImageUrls: worldImages.map((img) => img.preview).filter(Boolean) as string[],
refImageUrls: worldImages
.map((img) => img.preview)
.filter(Boolean) as string[],
modelType:
(selectedWorldModel as any)?.tauriId ||
(SPLAT_MODELS[0] as any)?.tauriId ||
@@ -408,9 +429,18 @@ export const ImageTo3DExperience = ({
const snapshotPrompt = prompt.trim();
const snapshotPreview = uploadedPreview || undefined;
const snapshotName = uploadedName;
const note = activeMode === "text" ? snapshotPrompt : snapshotName || "Generated Model";
const note =
activeMode === "text"
? snapshotPrompt
: snapshotName || "Generated Model";
objectStartGeneration(activeMode, note, snapshotPreview, false, subscriberId);
objectStartGeneration(
activeMode,
note,
snapshotPreview,
false,
subscriberId,
);
setSelectedResultId(subscriberId);
window.__storeTaskEnqueueMeta?.({
@@ -423,7 +453,9 @@ export const ImageTo3DExperience = ({
await GenerateMesh({
prompt: snapshotPrompt || undefined,
reference_image_media_tokens:
activeMode === "image" && uploadedMediaToken ? [uploadedMediaToken] : undefined,
activeMode === "image" && uploadedMediaToken
? [uploadedMediaToken]
: undefined,
model: selectedObjectModel?.tauriId ?? selectedObjectModelId,
frontend_caller: "mini_app",
frontend_subscriber_id: subscriberId,
@@ -434,7 +466,10 @@ export const ImageTo3DExperience = ({
}
}
} catch (error) {
const errorMessage = commandErrorMessage(error, "An unexpected error occurred");
const errorMessage = commandErrorMessage(
error,
"An unexpected error occurred",
);
toast.error(`Failed to generate 3D model: ${errorMessage}`);
if (variant === "world") {
worldFailGeneration(subscriberId);
@@ -459,7 +494,15 @@ export const ImageTo3DExperience = ({
return prompt.trim().length > 3;
}
return true;
}, [variant, activeMode, uploadedMediaToken, prompt, isGenerating, isUploading, worldImages]);
}, [
variant,
activeMode,
uploadedMediaToken,
prompt,
isGenerating,
isUploading,
worldImages,
]);
const hasResults = results.length > 0;
const showPromptAtBottom = hasResults;
@@ -471,7 +514,9 @@ export const ImageTo3DExperience = ({
const centerTop = vh / 2 - promptHeight / 2 + 80;
const bottomTop = vh - bottomOffsetPx;
const targetTop = showPromptAtBottom ? Math.max(0, bottomTop) : Math.max(0, centerTop);
const targetTop = showPromptAtBottom
? Math.max(0, bottomTop)
: Math.max(0, centerTop);
const promptAnim = useSpring({
top: targetTop,
@@ -539,7 +584,10 @@ export const ImageTo3DExperience = ({
}}
>
<PlusIcon
className={twMerge("text-base-fg opacity-80", hasResults ? "text-2xl" : "text-4xl")}
className={twMerge(
"text-base-fg opacity-80",
hasResults ? "text-2xl" : "text-4xl",
)}
/>
{!hasResults && (
<span className="mt-3 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
@@ -663,7 +711,8 @@ export const ImageTo3DExperience = ({
onDragLeave={(e) => {
e.preventDefault();
e.stopPropagation();
if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragActive(false);
if (!e.currentTarget.contains(e.relatedTarget as Node))
setDragActive(false);
}}
onDrop={(e) => {
e.preventDefault();
@@ -671,7 +720,9 @@ export const ImageTo3DExperience = ({
setDragActive(false);
if (e.dataTransfer?.files)
handleWorldFiles(
Array.from(e.dataTransfer.files).filter((f) => f.type.startsWith("image/")),
Array.from(e.dataTransfer.files).filter((f) =>
f.type.startsWith("image/"),
),
);
}}
onClick={() => worldFileInputRef.current?.click()}
@@ -717,7 +768,10 @@ export const ImageTo3DExperience = ({
<img
src={img.preview}
alt={img.name}
className={twMerge("h-full w-full object-cover", img.isUploading && "opacity-50 blur-sm")}
className={twMerge(
"h-full w-full object-cover",
img.isUploading && "opacity-50 blur-sm",
)}
/>
{img.isUploading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
@@ -775,7 +829,10 @@ export const ImageTo3DExperience = ({
onClick={() => worldFileInputRef.current?.click()}
>
<PlusIcon
className={twMerge("text-base-fg opacity-80", hasResults ? "text-lg" : "text-2xl")}
className={twMerge(
"text-base-fg opacity-80",
hasResults ? "text-lg" : "text-2xl",
)}
/>
</Button>
</Tooltip>
@@ -849,7 +906,8 @@ export const ImageTo3DExperience = ({
return renderImageMode();
};
const activeResult = results.find((r) => r.id === selectedResultId) || results[0];
const activeResult =
results.find((r) => r.id === selectedResultId) || results[0];
return (
<div className="flex h-[calc(100vh-56px)] w-full bg-ui-background text-base-fg">
@@ -874,7 +932,11 @@ export const ImageTo3DExperience = ({
<div className="relative z-10 h-full w-full p-8">
{!hasResults && (
<div className="pointer-events-none absolute left-0 top-[calc(50%-280px)] flex w-full justify-center">
<CreateEmptyState title={title} subtitle={subtitle} className="py-8 sm:py-8" />
<CreateEmptyState
title={title}
subtitle={subtitle}
className="py-8 sm:py-8"
/>
</div>
)}
@@ -929,11 +991,14 @@ export const ImageTo3DExperience = ({
variant="action"
icon={BoxIcon}
onClick={() => {
toast.promise(downloadFileFromUrl(activeResult.modelUrl!), {
loading: "Downloading GLB...",
success: "Downloaded GLB file",
error: "Failed to download file",
});
toast.promise(
downloadFileFromUrl(activeResult.modelUrl!),
{
loading: "Downloading GLB...",
success: "Downloaded GLB file",
error: "Failed to download file",
},
);
}}
>
GLB
@@ -1000,7 +1065,9 @@ export const ImageTo3DExperience = ({
</div>
<div className="mt-1 flex items-center gap-1.5 text-xs">
{isPending ? (
<span className="text-base-fg/55">Generating…</span>
<span className="text-base-fg/55">
Generating…
</span>
) : (
<span className="font-mono tabular-nums text-base-fg/45">
{formatTime(result.timestamp)}
@@ -1021,13 +1088,21 @@ export const ImageTo3DExperience = ({
<animated.div
className={twMerge(
"fixed left-1/2 z-20 w-full -translate-x-1/2",
variant === "world" ? (hasResults ? "max-w-2xl" : "max-w-lg") : "max-w-md",
variant === "world"
? hasResults
? "max-w-2xl"
: "max-w-lg"
: "max-w-md",
)}
style={promptAnim}
>
<div ref={promptContentRef}>
<div className={twMerge("glass w-full", hasResults ? "p-3" : "p-5")}>
<div className={hasResults ? "space-y-3" : "space-y-5"}>{renderActiveMode()}</div>
<div
className={twMerge("glass w-full", hasResults ? "p-3" : "p-5")}
>
<div className={hasResults ? "space-y-3" : "space-y-5"}>
{renderActiveMode()}
</div>
<div
className={twMerge(
@@ -1042,7 +1117,9 @@ export const ImageTo3DExperience = ({
? IMAGE_TO_3D_WORLD_PAGE_MODEL_LIST
: IMAGE_TO_3D_OBJECT_PAGE_MODEL_LIST
}
page={variant === "world" ? WORLD_MODEL_PAGE : OBJECT_MODEL_PAGE}
page={
variant === "world" ? WORLD_MODEL_PAGE : OBJECT_MODEL_PAGE
}
/>
<GenerateButton
variant="primary"
@@ -1073,7 +1150,9 @@ export const ImageTo3DExperience = ({
<div className="absolute bottom-4 right-4 z-20 flex items-center gap-2">
<CostCalculatorButton
modelPage={variant === "world" ? WORLD_MODEL_PAGE : OBJECT_MODEL_PAGE}
modelPage={
variant === "world" ? WORLD_MODEL_PAGE : OBJECT_MODEL_PAGE
}
/>
<HelpMenuButton />
</div>
@@ -1109,7 +1188,9 @@ export const ImageTo3DExperience = ({
mode="select"
selectedItemIds={selectedGalleryImages}
onSelectItem={handleImageSelect}
maxSelections={variant === "world" ? MAX_WORLD_IMAGES - worldImages.length : 1}
maxSelections={
variant === "world" ? MAX_WORLD_IMAGES - worldImages.length : 1
}
onUseSelected={handleGallerySelect}
onDownloadClicked={downloadFileFromUrl}
forceFilter="image"
@@ -31,4 +31,3 @@ export const useImageTo3DStore = create<ImageTo3DState>((set) => ({
})),
reset: () => set({ results: [] }),
}));
Loaded 100 of 3426 files, more files were not shown because too many files have changed in this diff. Show more