initial commit

This commit is contained in:
2026-06-08 22:49:50 -07:00
commit f6a6681e16
40 changed files with 3026 additions and 0 deletions
View File
+38
View File
@@ -0,0 +1,38 @@
import io
from pathlib import Path
from pydub import AudioSegment
from backend.models import ExportRequest, SwitchType
SAMPLE_RATE = 48000
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "assets" / "samples"
SWITCH_FILES: dict[SwitchType, str] = {
"cherry-mx-blue": "cherry-mx-blue.wav",
"cherry-mx-red": "cherry-mx-red.wav",
"cherry-mx-brown": "cherry-mx-brown.wav",
}
def _load_sample(switch: SwitchType) -> AudioSegment:
path = SAMPLES_DIR / SWITCH_FILES[switch]
if not path.exists():
raise FileNotFoundError(f"Sample not found: {path}")
sample = AudioSegment.from_file(path)
return sample.set_frame_rate(SAMPLE_RATE).set_channels(2)
def export_keyboard_audio(request: ExportRequest) -> bytes:
duration_ms = int(request.duration * 1000)
base = AudioSegment.silent(duration=duration_ms, frame_rate=SAMPLE_RATE)
click = _load_sample(request.switch)
for marker in sorted(request.markers, key=lambda m: m.time):
position_ms = int(marker.time * 1000)
if position_ms < duration_ms:
base = base.overlay(click, position=position_ms)
buffer = io.BytesIO()
base.export(buffer, format="wav")
return buffer.getvalue()
+56
View File
@@ -0,0 +1,56 @@
from pathlib import Path
import uvicorn
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response
from fastapi.staticfiles import StaticFiles
from backend.audio_export import export_keyboard_audio
from backend.models import ExportRequest
ROOT = Path(__file__).resolve().parent.parent
DIST = ROOT / "frontend" / "dist"
ASSETS = ROOT / "assets"
app = FastAPI(title="sfxkeeb")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
if ASSETS.exists():
app.mount("/assets", StaticFiles(directory=ASSETS), name="assets")
@app.post("/api/export/audio")
def export_audio(request: ExportRequest) -> Response:
try:
wav_bytes = export_keyboard_audio(request)
except FileNotFoundError as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Export failed: {exc}") from exc
return Response(
content=wav_bytes,
media_type="audio/wav",
headers={"Content-Disposition": 'attachment; filename="keyboard_track.wav"'},
)
@app.get("/api/health")
def health() -> dict[str, str]:
return {"status": "ok"}
if DIST.exists():
app.mount("/", StaticFiles(directory=DIST, html=True), name="frontend")
def run() -> None:
uvicorn.run("backend.main:app", host="127.0.0.1", port=8000, reload=True)
+23
View File
@@ -0,0 +1,23 @@
from typing import Literal
from pydantic import BaseModel, Field
SwitchType = Literal["cherry-mx-blue", "cherry-mx-red", "cherry-mx-brown"]
class Marker(BaseModel):
id: str
time: float = Field(ge=0)
key: str
class Project(BaseModel):
version: int = 1
switch: SwitchType = "cherry-mx-blue"
markers: list[Marker] = Field(default_factory=list)
class ExportRequest(BaseModel):
duration: float = Field(gt=0)
switch: SwitchType = "cherry-mx-blue"
markers: list[Marker] = Field(default_factory=list)