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
64 changes: 64 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: CI

on:
push:
branches: [master, "desktop/**", "feature/**"]
pull_request:
branches: [master]

jobs:
test:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.13"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install dependencies (no heavy ML)
run: |
pip install fastapi pydantic httpx pytest pytest-asyncio sqlalchemy aiofiles

- name: Lint (ruff)
run: |
pip install ruff
ruff check backend/ tests/ --ignore E501

- name: Run unit tests
run: pytest tests/unit/ -v

- name: Run integration tests (mock pipeline)
run: pytest tests/integration/ -v

build-windows:
runs-on: windows-latest
needs: test
if: startsWith(github.ref, 'refs/tags/v')

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip

- name: Install build deps
run: pip install pyinstaller PySide6

- name: Build executable
run: python scripts/build_windows.py

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: AegisScoreStudio-Windows
path: dist/AegisScoreStudio.exe
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ build/
venv/
*.log
test_sf.wav
aegis.db

.env.local
.setup_complete
models/
4 changes: 4 additions & 0 deletions AegisScore.vbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Set objShell = CreateObject("WScript.Shell")
Dim dir : dir = Left(WScript.ScriptFullName, InStrRev(WScript.ScriptFullName, "\") - 1)
objShell.CurrentDirectory = dir
objShell.Run "cmd /c npm start", 0, False
39 changes: 39 additions & 0 deletions Launch Aegis.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@echo off
setlocal enabledelayedexpansion
cd /d "%~dp0"

:: Auto-run setup on first launch
if not exist ".setup_complete" (
echo First launch detected -- running setup...
echo.
call setup.bat
if errorlevel 1 (
echo Setup failed. Please fix errors above and try again.
pause & exit /b 1
)
)

:: Load omnizart path from .env.local if present
if exist ".env.local" (
for /f "tokens=1,* delims==" %%a in (.env.local) do (
if "%%a"=="OMNIZART_PYTHON" set OMNIZART_PYTHON=%%b
)
)

:: Validate omnizart path still exists (venv may have been deleted)
if defined OMNIZART_PYTHON (
if not exist "!OMNIZART_PYTHON!" (
echo WARNING: OMNIZART_PYTHON path not found: !OMNIZART_PYTHON!
echo omnizart disabled. Re-run setup.bat to reinstall.
set OMNIZART_PYTHON=
)
)

if defined OMNIZART_PYTHON (
echo omnizart: enabled ^(!OMNIZART_PYTHON!^)
) else (
echo omnizart: disabled ^(install Python 3.9 + run setup.bat to enable^)
)

echo.
npm start
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
download evrything as a zip.
use ```pip install -r requirements.txt```
also make sure u have python 3.9 installed
have fun
this barely works
good luck
pls fix ts
Empty file added assets/fonts/.gitkeep
Empty file.
Empty file added assets/icons/.gitkeep
Empty file.
24 changes: 24 additions & 0 deletions assets/splash.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
background:#0a0a0c;
display:flex; flex-direction:column;
align-items:center; justify-content:center;
height:100vh; font-family:system-ui; color:#fff;
user-select:none; overflow:hidden;
}
img { width:120px; height:auto; margin-bottom:16px; }
.title { font-size:18px; font-weight:700; letter-spacing:.05em; }
.sub { font-size:11px; color:#555; margin-top:6px; }
</style>
</head>
<body>
<img src="logo.svg" alt="" draggable="false"/>
<div class="title">AEGISSCORE</div>
<div class="sub">Starting local engine…</div>
</body>
</html>
Empty file added backend/__init__.py
Empty file.
Empty file added backend/api/__init__.py
Empty file.
35 changes: 35 additions & 0 deletions backend/api/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""FastAPI app factory — same pipeline logic as web prototype, now IPC-only."""
from __future__ import annotations

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pathlib import Path

from backend.api.routers import analyze, upload, status, midi_notes, history
from backend.db.database import init_db


def create_app() -> FastAPI:
app = FastAPI(title="AegisScore-Desktop", version="0.1.0")

app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:7471", "app://aegis"],
allow_methods=["*"],
allow_headers=["*"],
)

init_db()

app.include_router(analyze.router, prefix="/api")
app.include_router(upload.router, prefix="/api")
app.include_router(status.router, prefix="/api")
app.include_router(midi_notes.router, prefix="/api")
app.include_router(history.router, prefix="/api")

work_dir = Path("work")
work_dir.mkdir(exist_ok=True)
app.mount("/work", StaticFiles(directory=str(work_dir)), name="work")

return app
Empty file added backend/api/routers/.gitkeep
Empty file.
Empty file added backend/api/routers/__init__.py
Empty file.
20 changes: 20 additions & 0 deletions backend/api/routers/analyze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

import uuid
from fastapi import APIRouter, BackgroundTasks
from backend.models.schemas import AnalyzeRequest, JobState, JobStatus
from backend.services.pipeline import JOBS, URL_CACHE, pipeline_url

router = APIRouter()


@router.post("/analyze")
async def analyze(req: AnalyzeRequest, bg: BackgroundTasks):
url = req.url.strip()
cached = URL_CACHE.get(url)
if cached and JOBS.get(cached, {}).get("status") == JobStatus.DONE:
return {"session_id": cached, "cached": True}
sid = str(uuid.uuid4())
JOBS[sid] = JobState().model_dump()
bg.add_task(pipeline_url, sid, url)
return {"session_id": sid}
28 changes: 28 additions & 0 deletions backend/api/routers/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Session history — CRUD over SQLite."""
from __future__ import annotations

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from backend.db.database import get_db
from backend.db.models import Session as DbSession

router = APIRouter()


@router.get("/history")
def list_history(limit: int = 50, db: Session = Depends(get_db)):
rows = db.query(DbSession).order_by(DbSession.created_at.desc()).limit(limit).all()
return [
{"id": r.id, "title": r.title, "artist": r.artist, "source": r.source,
"duration": r.duration, "created_at": r.created_at.isoformat(),
"stem_count": len(r.stems)}
for r in rows
]


@router.delete("/history/{sid}")
def delete_session(sid: str, db: Session = Depends(get_db)):
row = db.query(DbSession).filter(DbSession.id == sid).first()
if row:
db.delete(row); db.commit()
return {"ok": True}
28 changes: 28 additions & 0 deletions backend/api/routers/midi_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations

from fastapi import APIRouter, HTTPException
from backend.models.schemas import MidiResponse
from backend.services.pipeline import WORK_DIR

router = APIRouter()


@router.get("/midi/{sid}/{stem_name}", response_model=MidiResponse)
async def get_midi_notes(sid: str, stem_name: str):
midi_path = WORK_DIR / sid / "midi" / f"{stem_name}.mid"
if not midi_path.exists():
raise HTTPException(404, "MIDI not found")
import pretty_midi
pm = pretty_midi.PrettyMIDI(str(midi_path))
notes = []
for inst in pm.instruments:
for note in inst.notes:
notes.append({
"pitch": note.pitch,
"start": round(float(note.start), 4),
"end": round(float(note.end), 4),
"velocity": note.velocity,
"drum": bool(inst.is_drum),
})
notes.sort(key=lambda n: n["start"])
return {"notes": notes, "end_time": round(float(pm.get_end_time()), 2)}
15 changes: 15 additions & 0 deletions backend/api/routers/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from backend.services.pipeline import JOBS

router = APIRouter()


@router.get("/status/{sid}")
async def status(sid: str):
job = JOBS.get(sid)
if not job:
raise HTTPException(404, "session not found")
return JSONResponse(job)
22 changes: 22 additions & 0 deletions backend/api/routers/upload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

import uuid
from pathlib import Path
from fastapi import APIRouter, BackgroundTasks, UploadFile, File
from backend.models.schemas import JobState
from backend.services.pipeline import JOBS, WORK_DIR, pipeline_file

router = APIRouter()


@router.post("/upload")
async def upload(bg: BackgroundTasks, file: UploadFile = File(...)):
sid = str(uuid.uuid4())
job_dir = WORK_DIR / sid
job_dir.mkdir(parents=True, exist_ok=True)
suffix = Path(file.filename or "audio.wav").suffix or ".wav"
dest = job_dir / f"audio{suffix}"
dest.write_bytes(await file.read())
JOBS[sid] = JobState().model_dump()
bg.add_task(pipeline_file, sid, dest, file.filename or "audio")
return {"session_id": sid}
Loading
Loading