Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions .github/workflows/build-desktop-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
name: Build Desktop App

on:
push:
tags: ["desktop-v*"]
workflow_dispatch:

permissions:
contents: write # required for tauri-action to create/update the draft release

jobs:
test:
name: Pre-build sanity checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
architecture: "x64"
cache: "pip"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Install FFmpeg
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg
- name: black --check
run: black --check .
- name: pytest
run: pytest skelly_synchronize/tests

build:
name: Build (${{ matrix.target }})
needs: test
strategy:
fail-fast: false
matrix:
include:
- platform: windows-latest
target: x86_64-pc-windows-msvc
- platform: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- platform: macos-14
target: aarch64-apple-darwin
- platform: macos-13
target: x86_64-apple-darwin
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4

- name: Install Linux system dependencies
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential curl wget file \
libwebkit2gtk-4.1-dev libxdo-dev libssl-dev \
libayatana-appindicator3-dev librsvg2-dev

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
architecture: "x64"
cache: "pip"

- name: Install Python build dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[api,dev,packaging]"

- name: Freeze API sidecar with PyInstaller
shell: bash
run: poe freeze-api

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
cache-dependency-path: frontend/package-lock.json

- name: Install frontend dependencies
run: npm ci --prefix frontend

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- name: Cache Rust build
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri -> target

# ── Code signing prerequisites (release builds only) ──
# Reuses the freemocap org's Azure Trusted Signing account ("freemocap"
# account + "freemocap" certificate profile, see src-tauri/tauri.release.conf.json)
# and the freemocap org's Apple Developer ID, per the same pattern as
# freemocap-ui's build workflow. Dry-run (workflow_dispatch) builds stay
# unsigned and don't require any of these secrets.
- name: Validate Windows code signing prerequisites
if: matrix.platform == 'windows-latest' && startsWith(github.ref, 'refs/tags/desktop-v')
shell: pwsh
run: |
$missingSecrets = @()
if (-not "${{ secrets.AZURE_TENANT_ID }}") { $missingSecrets += "AZURE_TENANT_ID" }
if (-not "${{ secrets.AZURE_CLIENT_ID }}") { $missingSecrets += "AZURE_CLIENT_ID" }
if (-not "${{ secrets.AZURE_CLIENT_SECRET }}") { $missingSecrets += "AZURE_CLIENT_SECRET" }
if ($missingSecrets.Count -gt 0) {
Write-Error "Missing required secrets for Windows code signing: $($missingSecrets -join ', ')"
exit 1
}
Write-Output "All required Windows code signing secrets are available"

- name: Install trusted-signing-cli
if: matrix.platform == 'windows-latest' && startsWith(github.ref, 'refs/tags/desktop-v')
run: cargo install trusted-signing-cli --locked

- name: Validate macOS code signing prerequisites
if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/desktop-v')
shell: bash
run: |
MISSING=()
[ -z "${{ secrets.MAC_CERTIFICATE_P12 }}" ] && MISSING+=("MAC_CERTIFICATE_P12")
[ -z "${{ secrets.MAC_CERTIFICATE_PASSWORD }}" ] && MISSING+=("MAC_CERTIFICATE_PASSWORD")
[ -z "${{ secrets.APPLE_SIGNING_IDENTITY }}" ] && MISSING+=("APPLE_SIGNING_IDENTITY")
[ -z "${{ secrets.APPLE_ID }}" ] && MISSING+=("APPLE_ID")
[ -z "${{ secrets.APPLE_ID_PASSWORD }}" ] && MISSING+=("APPLE_ID_PASSWORD")
[ -z "${{ secrets.APPLE_TEAM_ID }}" ] && MISSING+=("APPLE_TEAM_ID")
if [ ${#MISSING[@]} -gt 0 ]; then
echo "::error::Missing required secrets for macOS code signing: ${MISSING[*]}"
exit 1
fi
echo "All required macOS code signing secrets are available"

# Tag push: build, sign/notarize, and attach installers to a draft GitHub Release.
#
# macOS signing+notarization is handled natively by tauri-action/the Tauri
# bundler from the APPLE_* env vars below (it imports the cert into a
# temporary keychain and notarizes itself -- no manual `security`/`codesign`
# steps needed, unlike freemocap-ui's Electron-builder flow).
#
# Windows signing goes through tauri.release.conf.json's bundle.windows.signCommand,
# which shells out to trusted-signing-cli (installed above) using Azure Trusted
# Signing -- the same service and account freemocap-ui signs with via
# azure/trusted-signing-action, just invoked during the Tauri bundle step instead
# of as a separate post-build signing pass.
- name: Build and publish desktop bundle
if: startsWith(github.ref, 'refs/tags/desktop-v')
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.MAC_CERTIFICATE_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
with:
projectPath: .
tagName: ${{ github.ref_name }}
releaseName: "Skelly Synchronize Desktop ${{ github.ref_name }}"
releaseDraft: true
prerelease: false
args: --target ${{ matrix.target }} --config src-tauri/tauri.release.conf.json

# Manual dispatch: build only, upload as a workflow artifact for inspection.
- name: Build (dry run, no release)
if: github.event_name == 'workflow_dispatch'
run: npx --prefix frontend tauri build -- --target ${{ matrix.target }}

- name: Upload build artifacts (dry run)
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: skelly-synchronize-${{ matrix.target }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/**
retention-days: 7
14 changes: 13 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ MANIFEST
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
!packaging/pyinstaller/*.spec

# Tauri desktop shell
src-tauri/target/
src-tauri/resources/

# Installer logs
pip-log.txt
Expand Down Expand Up @@ -153,4 +158,11 @@ cython_debug/

.vscode/

.DS_Store
.DS_Store
CLAUDE.md

# Node (frontend/)
node_modules/
dist/
dist-ssr/
*.local
123 changes: 123 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Contributing

This covers setting up a dev environment and running/building the project's pieces:
the `core` library, the `api` server, the `frontend`, and the Tauri desktop app. For
architecture background, see [`docs/architecture/`](docs/architecture/README.md).

## Setup

Requires [uv](https://docs.astral.sh/uv/), Node.js/npm, and (for the desktop app) the
Rust toolchain (`rustup`). FFmpeg must also be installed and on `PATH` — see the
[README](README.md).

```bash
uv venv
uv pip install -e ".[api,dev,packaging]"

cd frontend && npm install && cd ..
```

`packaging` pulls in PyInstaller, needed only for building the desktop app's sidecar
binary (see below) — omit it if you're just working on `core`/`api`/`cli`.

## Running the pieces

Run Python commands via `uv run <command>` (or activate the venv with
`source .venv/bin/activate` first, if you'd rather not prefix every command).

```bash
# Tests
uv run poe test # fast unit + integration tests
uv run poe test-slow # opt-in slow tier (real dataset, network + ffmpeg/deffcode)

# Formatting (CI runs black --check on PRs)
uv run poe format

# CLI
uv run skelly-synchronize <raw_video_folder_path> [-o OUTPUT] [-m audio|brightness]

# API server (http://127.0.0.1:8000)
uv run skelly-sync-api
```

`test`, `test-slow`, and `format` are [poe](https://poethepoet.natn.io/) tasks defined
in `pyproject.toml`'s `[tool.poe.tasks]` — run `uv run poe --help` to list all of them.

For the frontend dev server (talks to the API server above — run both):

```bash
cd frontend && npm run dev
```

## Desktop app (Tauri)

The desktop app wraps `frontend` in a Tauri shell that spawns `api` as a managed
sidecar process. Design details: [`docs/architecture/06-tauri-desktop.md`](docs/architecture/06-tauri-desktop.md).
Windows, Linux, and macOS (Apple Silicon + Intel) are all supported; CI builds and
releases installers for all four via
[`.github/workflows/build-desktop-app.yml`](.github/workflows/build-desktop-app.yml).

### Dev mode

The venv must be **activated** (not just `uv run`) so the Rust shell can find
`skelly-sync-api` on `PATH` when it spawns it, and the command must run from the
**repo root**, not `frontend/` — Tauri's CLI only finds `src-tauri/` by searching
subdirectories of the current directory, and `src-tauri/` is a sibling of `frontend/`,
not nested inside it.

```bash
source .venv/bin/activate
npx --prefix frontend tauri dev
```

This runs the Vite dev server and `cargo run` together, spawns `skelly-sync-api` from
the activated venv's `PATH`, and opens the app window. The first run compiles the Rust
dependency graph from scratch (a few minutes); subsequent runs are fast.

### Building the release app

The release build uses a PyInstaller-frozen `skelly-sync-api` build as its sidecar.
Freezing isn't wired into `tauri build` itself yet, so it's a separate step first:

```bash
uv run poe freeze-api
```

This freezes `skelly-sync-api` with PyInstaller in **onedir** mode (not onefile — see
the comment atop `packaging/pyinstaller/skelly-sync-api.spec` for why: with onefile,
every `ProcessPoolExecutor` trim worker re-paid the full self-extraction cost, which
made real (multi-video) sync jobs pathologically slow) and copies the result to
`src-tauri/resources/skelly-sync-api/`. It's bundled as a plain Tauri resource (not a
`externalBin` sidecar, since onedir's directory-plus-payload output doesn't fit that
convention) and spawned directly from a resolved resource path in
`src-tauri/src/lib.rs`. Rerun it any time the Python side changes; if you're only
touching Rust or frontend code, the previously-frozen build is reused.

Then build the bundle:

```bash
npx --prefix frontend tauri build
```

Output:
- `src-tauri/target/release/bundle/macos/Skelly Synchronize.app`
- `src-tauri/target/release/bundle/dmg/Skelly Synchronize_0.1.0_aarch64.dmg`

### Testing the built app

```bash
open "src-tauri/target/release/bundle/macos/Skelly Synchronize.app"

curl http://127.0.0.1:8000/health # expect {"status":"ok"}
```

From there, use the app normally: pick a raw video folder (or paste a path), submit a
job, watch it complete. Quit the app normally (Cmd+Q or the menu) rather than `kill`ing
the process directly — a raw `kill` bypasses Tauri's exit-cleanup hook and isn't
representative of real usage.

To confirm the sidecar didn't leak a process after quitting:

```bash
ps -ef | grep skelly-sync-api | grep -v grep # should print nothing
```
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,48 @@ Skelly Synchronize is a package for synchronizing videos post-recording, without

## Install and Run

Skelly_synchronize can be installed through pip by running `pip install skelly_synchronize` in your terminal. Once it has installed, it can be run with the command `python -m skelly_synchronize`.
Skelly Synchronize is a Python library (`core`), a FastAPI server (`api`), and a React web UI (`frontend`), plus a CLI. To use the web UI, run the API server and the frontend dev server as two processes:

While running, the GUI window may appear frozen, but the terminal should show the progress. Large videos may take a significant amount of time.
```
pip install -e ".[api]"
skelly-sync-api
```

This starts the API on `http://127.0.0.1:8000`. Then, in a second terminal:

```
cd frontend
npm install
npm run dev
```

Open the URL Vite prints (typically `http://localhost:5173`) in your browser. See [`frontend/README.md`](frontend/README.md) for details.

A standalone macOS desktop app (packaged with Tauri, bundling the API so you don't
need to run it separately) is also in progress — see [CONTRIBUTING.md](CONTRIBUTING.md#desktop-app-tauri).

Skelly_synchronize currently depends on FFmpeg, a command line tool that handles the video files. If you do not have FFmpeg downloaded, you will need to install it separately. You can download FFmpeg here: https://ffmpeg.org/download.html

<img width="598" alt="Screen Shot 2023-10-10 at 9 51 11 AM" src="https://github.com/freemocap/skelly_synchronize/assets/24758117/2c34a076-90d9-4d8f-bd3a-5b4649586d8c">
## Contributing

For dev environment setup (via [uv](https://docs.astral.sh/uv/)), running tests, and
building/testing the desktop app, see [CONTRIBUTING.md](CONTRIBUTING.md).

## Using Skelly Synchronize

Once you have the GUI open, choose a folder of raw videos that you would like to synchronize. The videos must overlap in time to be able to be synchronized. The software currently works with `mp4`, `mkv`, `avi`, `mpeg`, and `mov` files. Once the folder of videos has been selected, you can press the button for the synchronization method you would like to run. The synchronized videos will be placed in a folder called "synchronized_videos" that will be in the same directory as the folder of raw videos.
Once you have the web UI open, choose a folder of raw videos that you would like to synchronize. The videos must overlap in time to be able to be synchronized. The software currently works with `mp4`, `mkv`, `avi`, `mpeg`, and `mov` files. Once the folder of videos has been selected, you can choose the synchronization method you would like to run. The synchronized videos will be placed in a folder called "synchronized_videos" that will be in the same directory as the folder of raw videos (or in a custom output folder, if one was specified).

### Synchronization Methods

**Audio Cross Correlation** synchronizes by aligning the audio files of each video as closely as possible. Cross correlation is a mathematical technique used to find the amount of offset between different signals. In this case, Skelly Synchronize is using cross correlation to find the time difference between the audio tracks of the video files.

**Brightness Contrast Detection** synchronizes by looking for a quick flash near the beginning of each video. This flash can be from a camera flash, turning on a light, or even opening curtains to a bright window. Skelly Synchronize looks for the first time in each video that the change in brightness (contrast) between subsequent frames passes a certain threshold, and then aligns the brightness change of each video. The brightness contrast threshold used can be set as a parameter in the GUI, and higher threshold values will require a more abrupt and brighter flash in the video. Synchronization will be best if all cameras see the flash at the same time, so methods like turning on a light will yield better synchronization than methods like opening curtains.
**Brightness Contrast Detection** synchronizes by looking for a quick flash near the beginning of each video. This flash can be from a camera flash, turning on a light, or even opening curtains to a bright window. Skelly Synchronize looks for the first time in each video that the change in brightness (contrast) between subsequent frames passes a certain threshold, and then aligns the brightness change of each video. The brightness contrast threshold used can be set as a parameter in the web UI, and higher threshold values will require a more abrupt and brighter flash in the video. Synchronization will be best if all cameras see the flash at the same time, so methods like turning on a light will yield better synchronization than methods like opening curtains.

### Video Requirements

For **audio synchronization**, all videos must have audio tracks. Synchronization will work better if there are short, distinct sounds audible from each camera, for example a loud clap.

For **brightness synchronization**, there must be a quick increase in brightness across all of the video files. This method requires a significant brightness change visible to all cameras, for example turning on a bright light or firing a flash visible to all cameras. The synchronization will be based off of the first brightness change in each video that crosses a threshold. You can set the brightness ratio threshold in the gui before synchronizing. The threshold takes into account both the brightness contrast compared to the preceding frame, and the rate of change of brightness contrast. It may take multiple tries with different brightness ratio thresholds to get proper synchronization, although the default should work in most cases.
For **brightness synchronization**, there must be a quick increase in brightness across all of the video files. This method requires a significant brightness change visible to all cameras, for example turning on a bright light or firing a flash visible to all cameras. The synchronization will be based off of the first brightness change in each video that crosses a threshold. You can set the brightness ratio threshold in the web UI before synchronizing. The threshold takes into account both the brightness contrast compared to the preceding frame, and the rate of change of brightness contrast. It may take multiple tries with different brightness ratio thresholds to get proper synchronization, although the default should work in most cases.

### Additional Files

Expand Down
Loading
Loading