scaffold: OpenScribe open-source self-hosted AI voice recorder
Bootstrap of the project (M0). Sets up the monorepo, design docs, hardware BOM, the open API contract, component skeletons, licensing and CI, following the Default Workflow SOP. What changed: - CLAUDE.md + docs/: copied the Default Workflow so sessions load the SOP. - state/: PROJECT, ARCHITECTURE, DECISIONS, TODO, NOTES filled in for OpenScribe. ARCHITECTURE captures the four-part design (firmware, server, app, case) and the three sync paths; DECISIONS records the hardware, AI-stack, storage, app and licensing choices; TODO lays out milestones M1-M9. - hardware/BOM.md: two build options (compact XIAO ESP32-S3 Sense; dev ESP32-S3 + I2S mic + SD), wiring/pinout, indicative cost. - api/openapi.yaml: the completely open API (device + server surfaces), including recording list/download/delete and exports (wav/ogg/txt/srt/vtt/md/json). - firmware/: PlatformIO ESP32-S3 project, two board profiles, pin map, boot scaffold with module seams for M1-M4. - server/: FastAPI skeleton mirroring the OpenAPI, config for self-hosted MinIO, faster-whisper and Ollama; stub routes browsable at /docs. - app/, case/: Flutter app plan; parametric OpenSCAD enclosure. - Licensing: GPL-3.0 (code), CERN-OHL-S-2.0 (hardware), CC-BY-SA-4.0 (case/docs), REUSE-style LICENSES/ with SPDX headers; LICENSING.md explains the split. - CI: Forgejo Actions workflow builds firmware (both profiles) and lints/imports server. Why: - Everything self-hosted and openly licensed per the user's requirements: an open API, three sync paths (BLE control, WiFi transfer, independent WiFi upload on charge to generic cloud storage), and a full self-hosted transcription+summary stack. Notes: - No custom PCB in v1; off-the-shelf modules. Physical verification waits on parts. - Component code is stubs at M0; features land milestone by milestone, each as its own branch/PR per the workflow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
031074c9a9
36 changed files with 2922 additions and 0 deletions
68
server/app/main.py
Normal file
68
server/app/main.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
"""OpenScribe server - FastAPI app.
|
||||
|
||||
M0 scaffold: wires the routes from api/openapi.yaml with in-memory stubs so the API shape
|
||||
is real and browsable at /docs. The AI pipeline (faster-whisper transcription in M5,
|
||||
Ollama summaries in M6) and real storage/DB replace the stubs in later milestones.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
from .config import settings
|
||||
from .models import Recording, RecordingPage, Summary, Transcript
|
||||
|
||||
app = FastAPI(
|
||||
title="OpenScribe API",
|
||||
version="0.1.0",
|
||||
description="Self-hosted AI voice recorder server. See api/openapi.yaml.",
|
||||
)
|
||||
|
||||
# In-memory store stands in for the DB + object storage until M5.
|
||||
_recordings: dict[str, Recording] = {}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health() -> dict:
|
||||
return {
|
||||
"status": "ok",
|
||||
"storage_backend": settings.storage_backend,
|
||||
"whisper_model": settings.whisper_model,
|
||||
"ollama_model": settings.ollama_model,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/v1/recordings", response_model=RecordingPage, tags=["recordings"])
|
||||
def list_recordings(limit: int = 50) -> RecordingPage:
|
||||
return RecordingPage(items=list(_recordings.values())[:limit], next_cursor=None)
|
||||
|
||||
|
||||
@app.get("/api/v1/recordings/{rec_id}", response_model=Recording, tags=["recordings"])
|
||||
def get_recording(rec_id: str) -> Recording:
|
||||
rec = _recordings.get(rec_id)
|
||||
if rec is None:
|
||||
raise HTTPException(status_code=404, detail="No such recording")
|
||||
return rec
|
||||
|
||||
|
||||
@app.get("/api/v1/recordings/{rec_id}/transcript", response_model=Transcript, tags=["server"])
|
||||
def get_transcript(rec_id: str) -> Transcript:
|
||||
# Implemented in M5 (faster-whisper). Until then, signal "not transcribed yet".
|
||||
if rec_id not in _recordings:
|
||||
raise HTTPException(status_code=404, detail="No such recording")
|
||||
raise HTTPException(status_code=409, detail="Not transcribed yet (M5)")
|
||||
|
||||
|
||||
@app.get("/api/v1/recordings/{rec_id}/summary", response_model=Summary, tags=["server"])
|
||||
def get_summary(rec_id: str) -> Summary:
|
||||
# Implemented in M6 (Ollama). Until then, signal "not summarised yet".
|
||||
if rec_id not in _recordings:
|
||||
raise HTTPException(status_code=404, detail="No such recording")
|
||||
raise HTTPException(status_code=409, detail="Not summarised yet (M6)")
|
||||
|
||||
|
||||
@app.post("/api/v1/ingest", response_model=Recording, status_code=202, tags=["server"])
|
||||
def ingest(recording: Recording) -> Recording:
|
||||
# M5 will store audio to the object store and queue transcription + summary.
|
||||
_recordings[recording.id] = recording
|
||||
return recording
|
||||
Loading…
Add table
Add a link
Reference in a new issue