Skip to content

Latest commit

 

History

221 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CreatorFlow

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.

CI Java 24 JavaFX 26 React 19

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 hardest problem: making the comparison engine safe to improve

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.

What the workspace looks like

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:

The preflight workspace after a scan of the sample project: a twelve-row asset ledger with automated evidence and release state per file, an evidence panel for the selected asset, and a release bar reading "Release needs a decision · 3 blocked · 6 need review · 3 approved" with the export button disabled

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:

The match investigation for avocado_foodstudy_v02.glb: the project model and the upstream Khronos model side by side in a 3D comparison labelled 99% match confidence and "not a pixel-difference percentage", above a delta register listing one visible base-color change and two record-only differences

How it's put together

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.

Quickstart

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 suite

To run the preflight desktop app:

mvn -pl desktop javafx:run

Pair 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.

Project history

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.

Legacy: the community gallery

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. What server/ is now is in server/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; /me shows 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-Key headers. Only the second door survives: the server is headless now, holds no browser session, and UserAccount no 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
Asset page Compare view

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.

How the originality check works

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.

What it can and can't prove

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).

The retargeting blind spot

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 desktop companion

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 data

Connect 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.

The Roblox Studio plugin

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 folder

See roblox-plugin/README.md for setup and the roadmap (team registries, Roblox animation-ID lifecycle tracking, version stacks from Studio).

Release-manifest milestone

creatorflow-core now has the first working slice of the release-preflight direction:

  • ProjectScanner recursively 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.
  • CreativeManifest defines the versioned creatorflow.manifest/v0.1 contract.
  • ManifestJson writes 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=true

The 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/dist

Large 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.

Roblox animation bridge prototype

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:

  1. Build the React workspace and run the desktop-owned browser workspace with the commands above.
  2. Open a local project, choose Animation compare, and create a temporary Studio pairing.
  3. Install roblox-plugin/desktop-bridge/CreatorFlowAnimationBridge.lua using the source-first instructions in the desktop-bridge guide.
  4. 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.

API

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.

Architecture

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
Loading

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.

Roadmap

  • 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

Development

Regenerate the two preflight screenshots in this README:

node frontend/scripts/capture-readme-shots.mjs

It 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.

License

MIT — © 2026 Bryan Cruz

About

Local-first release-preflight for small Roblox teams - snapshot diffing, provenance evidence, and deterministic PASS/BLOCKED release records (Java 21, JavaFX, SQLite).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages