A personal experimentation project. It started at a hackathon and became the sandbox where I explore one workflow end to end: before a Roblox team publishes an update, check every changed asset against a snapshot of the last release and return an honest PASS or BLOCKED.
Try the sample workspace in your browser — it runs on authored sample data, and scanning your own files needs the desktop app.
It runs locally. A 127.0.0.1 desktop bridge pairs with a Roblox Studio plugin and reads
only normalized joint data — never raw asset files — so nothing leaves your machine.
Changed assets are compared against insert-only SQLite snapshots, provenance findings are resolved
with a required reason, and the run emits a deterministic release manifest naming exactly which
version to roll back to. With an optional Roblox Open Cloud API key, it also looks up the creator
of an animation id you enter and the target experience's owner through Roblox's own API and
surfaces whether they match — where VERIFIED means the facts were obtained, never that the team
holds the right to ship the asset, a mismatch is a review lead rather than an accusation, and
the link between your file and that animation id stays DECLARED, because no tool can read a
Roblox asset id out of a local file. Similarity and motion comparison are supporting evidence only,
never a copied/not-copied verdict.
The accurate motion engine was Java, reachable only through the localhost desktop bridge, and the website ran a separate, algorithmically different TypeScript engine. Java cannot run in a browser at all — and serving it would have meant a paid JVM tier, a lossy converter, and a network round trip purely to run code that can't run where the users are. So I reimplemented the Java algorithm in TypeScript and proved numeric parity against a Java-generated oracle before touching any of the math (rounded fields within 0.01, intermediates around 1e-6). Committing that port before changing anything is the whole reason the later numbers mean something: every improvement after it was measured against a provably identical baseline.
Swapping the old browser engine for that port is what moved the headline number:
| before | after | |
|---|---|---|
| False positives | 14/97 (14.4%) | 4/97 (4.1%) |
| Recall | 112/119 (94.1%) | 110/119 (92.4%) |
The 1.7-point recall cost was two mirror cases, and it was a deliberate trade. Three tuning changes went in on top — multiplicative-coverage composition, a position de-weight, and banded DTW — each graded separately against the same scorecard; net they bought recall back at no false-positive cost.
Those are the cutover numbers, measured on the 119-positive set of the time. The set has since widened to four rigs, and two later changes moved the operating point again — the review threshold went to 90, and mirror canonicalization now scores every pair in both orientations, because a mirrored copy moves the performance across joints and every score is computed per joint by name:
| false positives | recall | |
|---|---|---|
| at the cutover (119 positives) | 4/97 (4.1%) | 110/119 (92.4%) |
| today (133 positives) | 1/97 (1.0%) | 133/133 |
Read those two columns differently. The false-positive column is a real measurement. The recall column is not: the positives are programmatic derivations, and the mirror ones are undone by the same simplified reflection that generates them, so 133/133 means this set has been outgrown rather than that copy detection is solved. No cross-rig or real-Roblox case is in it at all — see the blind spot below.
The engine is pinned by 23 golden vectors and a parity test that fails if the port drifts from the Java reference.
Both frames are a real run of the browser workspace on the built-in sample project — the same
workspace the live demo serves — captured by
frontend/scripts/capture-readme-shots.mjs. First, the scan has finished and nothing has been
decided, so the gate is shut:
Opening a finding gives the investigation view. Both halves are the real GLB files under a single locked camera — the project derivative on the left, the upstream Khronos original on the right — so a difference on screen is a difference in the models rather than one the viewer introduced:
| Module | What it is | Stack |
|---|---|---|
core |
Verification/motion engine, project scanner, versioned manifest model, release-gate CLI | plain Java — no UI, DB or Spring deps |
desktop |
The preflight app: local loopback bridge, SQLite store, plugin pairing, project picker | JavaFX 26, SQLite |
frontend |
The release-preflight workspace UI (motion lab, snapshots, evidence, releases) | React 19 + Vite + TypeScript |
server |
Optional, self-hosted: the team provenance store — accounts, teams, join codes, fingerprint-keyed claims | Spring Boot 4.1, JPA/H2 |
core has no UI, database, or Spring dependencies, so a fingerprint means exactly the same thing
in the CLI as it does in the desktop app.
Requires JDK 24 or newer and Maven.
git clone https://github.com/Bryancruzcb/creatorflow.git
cd creatorflow
mvn install # builds everything and runs the full Java test suiteTo run the preflight desktop app:
mvn -pl desktop javafx:runPair it with the Roblox Studio plugin in roblox-plugin/, pick a project, and
export a release to see a PASS/BLOCKED record. The frozen community gallery still builds and runs
separately — see Legacy: the community gallery.
This began as an April 2026 hackathon project (SJ Hacks) whose hardest judge question was "How would you make sure something being uploaded isn't already someone else's copyrighted work?" The answer — detection layers plus a declaration-and-dispute process, with honest limits — became a community gallery. On 2026-07-17 I narrowed it to the one workflow with a real, unserved user: release preflight for small Roblox teams, where the same honesty constraint (similarity is a review lead, never a verdict) is exactly right.
Full detail in docs/STRATEGIC-REDIRECT.md and the
consolidation report that mapped the decision against the code.
Deleted (2026-08-02). This section is history, written in the present tense of the time. Everything from here down describes the pre-redirect community-gallery direction, and the code for it no longer exists: the uploads, gallery pages, version stacks, comments, disputes, Thymeleaf templates and browser session login were removed when
server/was repurposed as the team provenance store. The desktop's "Community registry" settings card is gone with them. Nothing below describes anything you can run today; it is kept because the reasoning is part of how the project got here. Whatserver/is now is inserver/README.md.
- Gallery — a dark, media-first grid ("screening room") with search, image/audio filters and a feedback wanted view; version badges and flags are labeled right on the tile
- Upload flow — pick a file, declare ownership, choose a license, and the pipeline decides: duplicate ⇒ never publishes (you're pointed at the existing asset and the dispute process), similar ⇒ publishes flagged with per-layer evidence, clear ⇒ publishes with the report recorded
- Version stacks — publish V2 from the asset page and the engine verifies the lineage: similarity inside your stack is recorded as iteration ("dHash 7/64 — fingerprint-verified iteration"), never flagged; similarity to anything outside it keeps its consequences, and a byte-identical repeat of any version is refused. The gallery shows only the latest version.
- Visual diff — compare any two image versions: a pixel-difference heatmap (changes tinted amber→rust by magnitude) plus %-changed and the fingerprint distances
- Pinned comments — click a point on the artwork and your comment carries a numbered pin, frame.io-style; owners can toggle feedback wanted to invite review
- Asset pages — near-black stage, versions rail, details, the full originality report at upload time, download, and an ownership-dispute form
- Profiles & library — every member has a public page;
/meshows your uploads, disputes in both directions, and the API key that connects the desktop app - One account, two doors — browsers used session login (BCrypt + CSRF via Spring Security);
the desktop app and API clients used per-account
X-Api-Keyheaders. Only the second door survives: the server is headless now, holds no browser session, andUserAccountno longer carries a password hash at all - Content-addressed storage — files are stored by their SHA-256, so identical bytes exist once and the hash doubles as a perfect ETag (diff heatmaps are deterministic, so a SHA pair makes a strong ETag there too)
| Version stack with a pinned review comment | Visual diff between versions |
|---|---|
![]() |
![]() |
A note on SVG: user-supplied SVG can embed script, so files are served with a no-script
Content-Security-Policy and only ever embedded through <img>, which never executes it.
Every upload (web) and import (desktop) runs the applicable layers and compares fingerprints against everything already registered:
| Layer | Catches | How |
|---|---|---|
| SHA-256 | byte-identical re-uploads of any file type | streaming content hash |
| dHash + pHash | resized, re-encoded or lightly edited image copies | 64-bit perceptual fingerprints (gradient hash + 32×32 DCT hash), compared by Hamming distance |
| Audio energy fingerprint | re-uploads of the same PCM recording, at any volume | delta-coded RMS envelope — "dHash for sound", volume-invariant by construction |
| Metadata inspection | provenance signals a human should see | EXIF/XMP/PNG-text authorship tags surfaced as findings (informational only — metadata is trivially edited) |
The verdict is the worst evidence found — any exact hash match ⇒ Duplicate, any fingerprint within Hamming distance 10/64 ⇒ Similar, otherwise Clear — and the full evidence trail is stored with the asset.
Detection can prove a conflict (this file matches that one). It can never prove originality — there is no database of all copyrighted work, because copyright exists the moment a work is created, registered or not. Real platforms (YouTube Content ID, stock marketplaces) therefore pair detection with process, which CreatorFlow implements end to end: ownership declarations and licenses recorded at upload, verdicts and evidence kept with the asset, and a dispute workflow for claims.
And no — an IP address can't tell you who owns a file. Intellectual-property checks are about content fingerprints and provenance; IP addresses only ever matter server-side as abuse signals (rate limiting, repeat-infringer heuristics per account).
Animation comparison has one limitation worth stating plainly, because it is structural rather than a tuning problem: an animation retargeted onto a different skeleton is not detectable by this engine, at all.
The engine compares animations by joint name. Two rigs with different bone names share zero
tracks, so coverage is zero and the pair is a trivial non-match — not a low score, a
by-construction miss. The test set excludes cross-rig pairs for exactly this reason
(copyDetectionCases.ts: "Cross-rig pairs are forbidden — they trivially non-match via zero
coverage and validate nothing"), which is the right testing call and does mean the scorecard's
recall figures say nothing about this case.
That matters because download → retarget onto your own rig → publish is a realistic theft
route, and it is the one route CreatorFlow's numbers do not cover. Skeleton-independent matching
(normalising to end-effector trajectories rather than named joints) is the work that would close
it; see docs/TIER3-ROADMAP.md.
A clear result means no conflict was found in the registries and rigs we checked. It has never meant more than that, and here is one concrete shape of what it excludes.
The JavaFX app manages a local library offline: projects, drag-and-drop imports, the same originality check against your own collection, SQLite persistence.
mvn -pl desktop javafx:run # add -Dcreatorflow.demo=true for sample dataConnect it to the platform under Settings → Community registry (create an account there or
paste the API key from /me). Then every local import is also checked against the community —
matches appear as REGISTRY evidence and can escalate the verdict; if the server is down, imports
still work. Desktop clients send fingerprints only (a few hundred bytes), never files.
Publishing to the gallery stores the file — that's the point of a gallery — and if you upload a
file whose fingerprints you had already registered from the desktop, the registration is upgraded
in place rather than duplicated.
roblox-plugin/ is a Studio plugin (Luau + Rojo) that brings the registry to
Roblox animation teams: select a KeyframeSequence, and the plugin canonicalizes it, fingerprints
it with pure-Luau SHA-256, and checks it against the community registry through the same
/api/v1/verify + X-Api-Key contract the desktop app uses — the animation itself never
leaves Studio. Clean versions can be registered from the panel, and Studio's per-plugin HTTP
permissions mean nothing changes in the game's own settings.
rojo build roblox-plugin --plugin CreatorFlow.rbxm # installs into Studio's plugins folderSee roblox-plugin/README.md for setup and the roadmap
(team registries, Roblox animation-ID lifecycle tracking, version stacks from Studio).
creatorflow-core now has the first working slice of the release-preflight direction:
ProjectScannerrecursively inventories supported creative files using project-relative paths.- Every file runs through the existing SHA-256, image, audio, and metadata layers.
- Relationships inside the project—exact duplicates, perceptually similar images, and related audio—are retained in the inventory.
CreativeManifestdefines the versionedcreatorflow.manifest/v0.1contract.ManifestJsonwrites deterministic JSON and re-imports it.- The JSON Schema ships inside the core JAR as
creatorflow-manifest-v0.1.schema.json. - Source/license resolution is an explicit interface; an absent record remains unresolved instead of being mistaken for a clean ownership result.
Run the current CLI bridge against a real project directory:
mvn -q -pl core org.codehaus.mojo:exec-maven-plugin:3.3.0:java \
-Dexec.mainClass=creatorflow.manifest.ManifestCli \
-Dexec.args='/path/to/project MyProject 0.1.0 /path/to/manifest.json'Append --exclude <directory-name> (repeatable) to keep fixture or vendor trees out of the scan
on top of the built-in exclusions — e.g. this repository's own dogfood scan needs
--exclude stress-fixtures, whose deliberately duplicated test textures would otherwise
hard-block the release gate.
The scanner also supports configurable exclusions, ordered progress events, cancellation with a usable partial manifest, per-file failure isolation, dependency findings, and symlink containment. The desktop module now owns a loopback-only local bridge and migrated workflow store for project selection, insert-only scan runs, source evidence, append-only decisions, releases, and workspace restoration.
To run the desktop-owned browser workspace directly from a frontend build:
npm --prefix frontend run build
mvn -pl desktop javafx:run \
-Dcreatorflow.web.root=$(pwd)/frontend/dist -Dcreatorflow.web.open=trueThe app prints the workspace URL (CreatorFlow workspace: http://127.0.0.1:<port>/launch?...)
to the console at startup, so the session is recoverable even if no browser opens.
(The old -Djavafx.options="..." form silently failed to reach the app JVM — the desktop
pom now forwards these properties itself.)
For a self-contained desktop artifact, activate the packaging profile by supplying the same build directory at package time:
mvn -pl desktop -am package \
-Dcreatorflow.web.dist=$(pwd)/frontend/distLarge demonstration GLBs are intentionally optional: serving an external dist keeps ordinary
desktop builds lean, while the packaged profile is available for an offline showcase build.
CreatorFlow now has a loopback-only Roblox Studio input for animation evidence. The Studio plugin
reads two animation IDs that the signed-in creator is permitted to access, flattens each clip into
stable joint paths and local CFrame values, and sends one bounded JSON request to the desktop
app. The Java core recanonicalizes that data, computes deterministic
SHA-256 curve fingerprints, compares pose/timing/joint coverage, and stores the result with the
selected local project. Raw joint curves are not retained in SQLite.
Studio pairing flow:
- Build the React workspace and run the desktop-owned browser workspace with the commands above.
- Open a local project, choose Animation compare, and create a temporary Studio pairing.
- Install
roblox-plugin/desktop-bridge/CreatorFlowAnimationBridge.luausing the source-first instructions in the desktop-bridge guide. - Paste the displayed loopback endpoint and token into Studio, test the connection, then compare two permitted animation IDs. The evidence inbox refreshes automatically.
Both Roblox clip types are read. A KeyframeSequence is read exactly. A CurveAnimation has no
keyframes to read, so the plugin samples its position and rotation curves 20 times a second into the
same pose shape, and every comparison carries how each side was read — KEYFRAME or
CURVE_SAMPLED — through to the workspace, where a sampled side is labeled "Sampled from a curve —
not an exact read." Sampled sides can still be pinned as drift-detection snapshots: a live-Studio
spike found the sampling bit-identical across repeat reads and a register/refetch round trip, so a
sampled fingerprint does not wobble into a false "this animation changed." Curve support reads
position/rotation on rig-joint paths only, and a curve clip with none of those is rejected with that
reason. Inaccessible/private assets, rig retargeting, and copyright conclusions stay outside v0.1.
Roblox Studio decides whether an animation can be read; CreatorFlow does not bypass asset
permissions.
Run the default release policy against a manifest with machine-readable output:
mvn -q -pl core org.codehaus.mojo:exec-maven-plugin:3.3.0:java \
-Dexec.mainClass=creatorflow.manifest.ReleaseGateCli \
-Dexec.args='/path/to/manifest.json --output /path/to/gate-report.json'The command exits 0 when the release passes, 2 when policy blocks it, and 3 for invalid input or execution failure. .github/workflows/creatorflow-release-gate.yml shows the CI integration and report upload.
| Endpoint | Auth | Does |
|---|---|---|
POST /api/v1/accounts |
— | register a username, receive your API key |
GET /api/v1/health |
— | liveness probe |
POST /api/v1/teams |
X-Api-Key |
create a team; you become its owner |
GET /api/v1/teams |
X-Api-Key |
the teams you are in |
GET /api/v1/teams/{id}/members |
X-Api-Key |
who is in a team, ordered by username |
POST /api/v1/teams/{id}/join-codes |
X-Api-Key |
owners only; returns a single-use 24h code, once |
POST /api/v1/teams/join |
X-Api-Key |
redeem a join code |
DELETE /api/v1/teams/{id}/members/{accountId} |
X-Api-Key |
owner removes anyone, or leave yourself — the last owner cannot (409) |
POST /api/v1/teams/{id}/provenance-claims |
X-Api-Key |
record that you have a curve with this fingerprint |
GET /api/v1/teams/{id}/provenance-claims?fingerprint= |
X-Api-Key |
who else in the team recorded that exact 64-hex fingerprint |
POST …/provenance-claims/{claimId}/retract |
X-Api-Key |
author or owner; removes it from future lookups |
Legacy, off by default behind creatorflow.legacy-registry.enabled=true, kept only so the
frozen Rojo plugin does not break silently: POST /api/v1/verify, POST /api/v1/assets,
GET /api/v1/assets/mine, and the per-asset mappings routes.
Server data lives in ~/.creatorflow-team (H2). Auth is per-account API keys; there are no
browser logins. Registration is open unless creatorflow.signup.token is set. A hosted deployment
would need JWT/OAuth with rotating credentials first — see
server/README.md, which also says plainly not to expose this to the
internet.
flowchart LR
subgraph server["server — optional team provenance store"]
API["REST API<br/>accounts · teams · join codes · claims"] --> TEAM["TeamService<br/>append-only claim log"]
TEAM --> H2["H2: fingerprint = who recorded it<br/>no score · no verdict · no ranking"]
end
subgraph desktop["desktop — the preflight app"]
UI["JavaFX pages"] --> SVC["AssetImporter"]
SVC --> DB["SQLite library"]
BR["LocalBridgeServer<br/>127.0.0.1 only"] --> TC["HttpTeamClient<br/>fingerprints only, never cached"]
end
subgraph core
ENG["OriginalityEngine<br/>Sha256 · ImageHashes · WavFingerprint · MetadataInspector"]
MAN["ProjectScanner → CreativeManifest v0.1<br/>deterministic JSON · relative paths"]
end
SVC --> ENG
BR --> ENG
MAN --> ENG
TC -- "HTTP, LAN only" --> API
Note the shape of that last edge: it is the only one leaving the machine, it starts in the
desktop JVM, and it carries fingerprints and typed declarations — never curves, files or paths.
The React workspace talks to LocalBridgeServer on 127.0.0.1 and to nothing else, and no team
answer is ever written to the local database, so an unreachable store renders as unknown rather
than as an out-of-date copy of somebody else's record.
core has no UI, database or Spring dependencies — the server and the desktop app share it as a
plain library, so a fingerprint means exactly the same thing on both sides. The Java test suites
across the three modules (mvn verify) cover: engine algorithms, persistence, the local bridge and
its contract fixtures, ownership verification, and the server's teams, join codes and provenance
claims — including that a retracted claim leaves every future lookup, that re-sharing after a
retract creates a new row, and that the lookup never filters by fingerprint version.
- Add project-wide source aggregation, server-side evidence search, and saved filter views
- Add incremental scan caching and resumable work after process termination
- Sign exported release artifacts and verify signatures in the CI gate
- Chromaprint spectral audio fingerprints
- CLIP-style image embeddings with an ANN index, to catch "same character, redrawn" (registry matching is currently a linear scan — fine at this scale, BK-tree/ANN is the next step)
- C2PA Content Credentials verification for provenance-signed files
- Collections/boards, tags, and following — the curation half of a gallery
- Audio waveform rendering and time-anchored comments (the pin, but for sound)
- Pluggable reverse-image-search connector (e.g. Google Vision web detection) for public-web checks
- JWT/OAuth accounts, takedown resolution workflow for disputes, hosted deployment
Regenerate the two preflight screenshots in this README:
node frontend/scripts/capture-readme-shots.mjsIt builds the frontend, serves it on vite preview, and drives the sample preflight in headless
Chromium. Prerequisites: npm install in frontend/, npx playwright install chromium, and the
comparison models — npm --prefix frontend run assets:khronos downloads the five gitignored
sources against the hashes in frontend/public/assets/ASSET-PROVENANCE.md, then
npm --prefix frontend run assets:derive rebuilds the project derivatives. Without those models
the comparison viewer falls back to a still image, and the script fails rather than photograph
the fallback.
Desktop screenshots: run creatorflow.Main with -Dcreatorflow.screenshot.dir=docs/screenshots
and a throwaway -Dcreatorflow.data.dir.
MIT — © 2026 Bryan Cruz



