Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

117 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Scriptum

Java Spring Boot Angular PostgreSQL Python Docker

Audio and video transcription platform with real-time progress and automatic translation. A file is uploaded, its audio is extracted, transcribed by Whisper (cloud-first with a local fallback), optionally translated, and streamed back to the browser step by step over WebSocket.


At a glance (for reviewers & recruiters)

Scriptum is a full-stack application I designed and built solo as a portfolio project, focused on a real engineering problem: turning a long-running, multi-step, potentially unreliable external-API workflow (audio extraction, cloud transcription, translation) into a responsive product experience.

What it demonstrates

  • Backend architecture: A Spring Boot 3 REST API with a clean layered design (controller → service → repository), JPA/Hibernate persistence on PostgreSQL, and centralised error handling.
  • Resilient transcription pipeline: Audio/video is extracted and chunked with ffmpeg, then transcribed through the Groq Whisper API. If Groq is unavailable or rate-limited, the pipeline automatically falls back to a self-hosted faster-whisper microservice, no manual intervention needed.
  • Correct use of async in Spring: The transcription pipeline runs in a dedicated @Async service, separated from the request-handling service, to avoid the classic self-invocation pitfall that silently disables @Async proxying.
  • Real-time UX: Job progress (extracting → transcribing → translating → done) is pushed live to the Angular frontend over WebSocket (STOMP).
  • Stateless security: JWT authentication (HMAC-SHA256), BCrypt password hashing, and per-user data isolation (a user only ever sees their own transcription jobs).
  • Scheduled housekeeping: A scheduled task automatically purges temporary media files from disk after a configurable retention period.
  • Modern frontend: An Angular 18 standalone-components SPA styled with Tailwind CSS, built around a custom design system (a "vintage manuscript / typewriter" visual identity), with drag-and-drop upload, live progress, and a searchable archive of past transcriptions.

Stack: Java 21 · Spring Boot 3.5 · Spring Security · Spring Data JPA · WebSocket (STOMP) · PostgreSQL · Angular 18 · Tailwind CSS · RxJS · Python · FastAPI · faster-whisper · Groq API · Docker


Table of contents


Architecture

scriptum/
├── scriptum-api/        Spring Boot backend (REST + WebSocket)
├── scriptum-ui/         Angular frontend (SPA)
├── scriptum-whisper/    Python microservice (FastAPI + faster-whisper)
└── docker-compose.yml   PostgreSQL, backend, frontend, whisper service
Angular client  ──HTTPS/WS──▶  Spring Boot API  ──JDBC──▶  PostgreSQL
                                │
                                ├──▶  Groq API           (primary transcription, Whisper large-v3)
                                ├──▶  scriptum-whisper    (local fallback, faster-whisper)
                                └──▶  MyMemory API        (translation)

Request flow

  1. The Angular frontend uploads the media file to the Spring Boot backend.
  2. The backend stores the file, creates a TranscriptionJob in PENDING status, and immediately returns its id, the heavy processing happens asynchronously.
  3. A dedicated async service extracts the audio track with ffmpeg and splits it into chunks if the file is too long for a single API call.
  4. Each chunk is sent to the Groq API for transcription. If Groq fails or is rate-limited after retries, the backend falls back to the local scriptum-whisper microservice.
  5. The detected source language and transcribed text are reassembled. If a target language was requested, the text is translated via MyMemory.
  6. At every step, a progress update is pushed to the frontend over a WebSocket topic dedicated to that job (/topic/job/{jobId}).

Tech stack

Backend (scriptum-api)

Technology Role
Java 21 Language
Spring Boot 3.5 Application framework
Spring Security 6 JWT authentication / authorization
Spring Data JPA (Hibernate) ORM
Spring WebSocket Real-time progress (STOMP)
Spring WebFlux (WebClient) Non-blocking calls to Groq / MyMemory / whisper fallback
PostgreSQL Database
Springdoc OpenAPI API documentation (Swagger UI)
JJWT JWT generation / validation
ffmpeg (via ProcessBuilder) Audio extraction and chunking
Lombok Boilerplate reduction

Frontend (scriptum-ui)

Technology Role
Angular 18 SPA framework, standalone components
Tailwind CSS Utility-first styling, custom design tokens
RxJS Async streams
@stomp/stompjs Native WebSocket STOMP client
Angular Router Navigation, route guards

Transcription microservice (scriptum-whisper)

Technology Role
Python Language
FastAPI HTTP API
faster-whisper Local speech-to-text model (fallback provider)

Project structure

Backend

scriptum-api/src/main/java/com/yann/scriptum/
├── config/
│   ├── SecurityConfig.java         # Spring Security, CORS, JWT filter, public routes
│   ├── WebSocketConfig.java        # STOMP broker configuration
│   ├── WebClientConfig.java        # WebClient beans (Groq, whisper fallback)
│   ├── OpenApiConfig.java          # Swagger UI + bearer auth scheme
│   └── AsyncConfig.java            # Named task executor, @EnableAsync / @EnableScheduling
├── controller/                     # REST layer (Media, Job, Auth)
├── service/                        # Business logic
│   ├── MediaUploadService.java
│   ├── FfmpegService.java
│   ├── TranscriptionService.java   # Groq call + local fallback + retry
│   ├── TranslationService.java     # MyMemory call + chunking
│   ├── JobOrchestratorService.java # Creates jobs, exposed to controllers
│   ├── JobProcessingService.java   # @Async pipeline execution (separate bean on purpose)
│   ├── NotificationService.java    # WebSocket progress publishing
│   ├── JobCleanupScheduler.java    # Scheduled temp file cleanup
│   ├── AuthService.java
│   └── JwtService.java
├── model/                          # JPA entities
├── repository/                     # Spring Data repositories
├── dto/                            # Request/response records
├── enums/                          # JobStatus, ChunkStatus, Role
└── exception/                      # GlobalExceptionHandler + domain exceptions

Frontend

scriptum-ui/src/app/
├── core/
│   ├── guards/                     # authGuard
│   ├── interceptors/               # authInterceptor (JWT injection)
│   ├── models/                     # TypeScript interfaces
│   └── services/                   # AuthService, MediaService, JobService, WebSocketService
├── features/
│   ├── home/                       # Landing page
│   ├── auth/                       # Login, Register
│   ├── scripts/                    # Upload + live transcription view (core feature)
│   │   ├── upload-panel/
│   │   ├── config-panel/
│   │   └── transcript-viewer/
│   ├── archive/                    # Searchable history with stats and filters
│   └── ledger/                     # Account usage summary
└── shared/
    └── header/                     # Navigation, auth state

Data model

Core entities

User: Account with email, hashed password and role (USER, ADMIN). Implements UserDetails directly for Spring Security integration.

MediaFile: An uploaded audio or video file. Stores the original filename, MIME type, size, and the path on disk (never exposed to the frontend).

TranscriptionJob: The central entity. Tracks the full lifecycle of one transcription request: status, progress percentage, detected source language, requested target language, raw transcribed text, translated text, and error message on failure. Linked to both a MediaFile and the owning User.

TranscriptionChunk: One audio segment of a job when the source file exceeds the maximum duration accepted by the transcription API in a single call. Stores its index, time range, transcribed text and status.

Enumerations

JobStatus: PENDING → EXTRACTING_AUDIO → TRANSCRIBING → (TRANSLATING) → DONE, or FAILED at any step.

ChunkStatus: PENDING → PROCESSING → DONE, or FAILED.

Role: USER, ADMIN.


Transcription job lifecycle

1. UPLOAD        Client uploads a file, MediaFile persisted
2. JOB CREATION  TranscriptionJob created in PENDING, id returned immediately
3. EXTRACTION    Async pipeline extracts audio via ffmpeg, EXTRACTING_AUDIO
4. CHUNKING      File split into segments if longer than the API limit
5. TRANSCRIPTION Each chunk sent to Groq (fallback: local whisper), TRANSCRIBING
6. TRANSLATION   If a target language was requested, TRANSLATING
7. COMPLETION    Final text (and translation) saved, DONE
   FAILURE       Any exception along the way, FAILED, with errorMessage recorded

Every transition publishes a ProgressMessage over the job's dedicated WebSocket topic, so the frontend never needs to poll while the job is in flight.


Transcription pipeline & fallback strategy

TranscriptionService tries the Groq Whisper API first (whisper-large-v3, requested in verbose_json format to also capture the detected source language). Calls are retried with exponential backoff on 5xx and 429 responses.

If Groq still fails after retries, the service transparently falls back to the local scriptum-whisper microservice, which exposes the exact same response shape (text, language), so the rest of the pipeline needs no special-casing:

private TranscriptionResult transcribeChunk(Path chunkFile) {
    try {
        return callGroqApi(chunkFile);
    } catch (Exception e) {
        log.warn("Groq call failed, falling back to local whisper: {}", e.getMessage());
        return callLocalFallback(chunkFile);
    }
}

Translation (TranslationService) converts the language detected by Whisper (a full name like "french") into an ISO 639-1 code before calling the MyMemory API, splits long texts into byte-sized chunks to respect the provider's per-request limit, and skips the call entirely if source and target languages are the same.


REST API reference

Local base URL: http://localhost:8080/api Local Swagger UI: http://localhost:8080/swagger-ui.html

All endpoints are JWT-protected except /api/auth/**, /swagger-ui/**, /api-docs/** and the WebSocket handshake endpoint.

Authentication

Method Endpoint Description
POST /auth/register Create an account, returns a JWT
POST /auth/login Authenticate, returns a JWT

Media

Method Endpoint Description
POST /media/upload Upload an audio or video file (multipart/form-data)

Jobs

Method Endpoint Description
POST /jobs Start a transcription job from an uploaded media file id
GET /jobs List all jobs belonging to the authenticated user
GET /jobs/{id} Get a single job (status, progress, text, translation)

WebSocket real-time progress

STOMP endpoint: /ws (native WebSocket). Broker prefix: /topic.

Once a job is created, the frontend subscribes to:

/topic/job/{jobId}

and receives a ProgressMessage (jobId, status, progress) at every pipeline transition, until DONE or FAILED, at which point it fetches the full job via GET /jobs/{id} to display the final text.


Angular frontend

Key patterns

Standalone components: No NgModule, each component declares its own imports.

Route guard: authGuard protects /scripts, /archive and /ledger, redirecting unauthenticated users to /login.

HTTP interceptor: authInterceptor automatically attaches Authorization: Bearer <token> to every outgoing request.

Design system: A custom "vintage manuscript / typewriter" visual identity (EB Garamond, Courier Prime, JetBrains Mono, parchment textures, typewriter-key buttons) implemented as Tailwind design tokens rather than default utility classes.

Main views

View Route Description
Home / Landing page
Login / Register /login, /register Authentication
Scripts /scripts Upload a file, configure translation, watch live progress, read the result
Archive /archive Searchable history of all past jobs with stats and status filters
Ledger /ledger Account summary (manuscripts count, completed/failed, characters transcribed)

JWT authentication

The JWT is signed with HMAC-SHA256 via the JJWT library. Default lifetime: 24 hours (jwt.expiration-ms).

Backend: JwtAuthenticationFilter extracts the token from the Authorization: Bearer ... header, validates it against jwt.secret, and loads the User (which implements UserDetails directly) via UserDetailsServiceImpl. Sessions are stateless (SessionCreationPolicy.STATELESS).

Frontend: The token (and the user's email) is stored in localStorage by AuthService and injected automatically by authInterceptor.

⚠️ jwt.secret should be a long, random value provided via an environment variable, never committed in application.properties.


Environment variables

Create a .env file at the project root:

Variable Required Description
GROQ_API_KEY Yes Groq API key for Whisper transcription (console.groq.com)
JWT_SECRET Yes Secret used to sign JWTs (≥ 32 random characters, never commit)

Backend (application.properties, optional)

Property Default Description
whisper.local.url http://localhost:5000 URL of the scriptum-whisper fallback service
translation.api.url https://api.mymemory.translated.net Translation provider base URL
scriptum.storage.path /tmp/scriptum Temporary storage path for uploaded/extracted media
scriptum.cleanup.max-age-hours 2 Retention period before a temp file is auto-deleted
jwt.expiration-ms 86400000 JWT lifetime in milliseconds (24h)

Running locally

Prerequisites

  • Java 21
  • Node.js and npm
  • Python 3.10+
  • Docker and Docker Compose
  • ffmpeg installed and available on your PATH
  • A Groq API key

1. Clone and configure

git clone https://github.com/Yan739/scriptum.git
cd scriptum

Create the .env file described above at the project root.

2. Start PostgreSQL

docker-compose up -d db

3. Run the backend

cd scriptum-api
./mvnw spring-boot:run

API available at http://localhost:8080.

4. Run the transcription microservice

cd scriptum-whisper
python -m venv venv
venv\Scripts\activate      # or: source venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 5000

5. Run the frontend

cd scriptum-ui
npm install
ng serve

App available at http://localhost:4200.


API documentation

Interactive OpenAPI documentation, once the backend is running:

http://localhost:8080/swagger-ui.html

Authenticate with the Authorize button using the JWT returned by /api/auth/login or /api/auth/register to test protected endpoints.


Scheduled cleanup

JobCleanupScheduler runs every hour and deletes any file in the temporary storage directory older than scriptum.cleanup.max-age-hours, preventing uploaded and extracted media from accumulating on disk after processing.


Error handling

GlobalExceptionHandler centralises all error responses:

Exception HTTP code Use
MethodArgumentNotValidException 400 Bean Validation failed
UnsupportedMediaFormatException 400 Invalid or oversized upload
ResourceNotFoundException 404 Entity not found
NoResourceFoundException 404 Unresolved static resource / bad URL
MaxUploadSizeExceededException 413 File too large
Exception (catch-all) 500 Internal error (logged server-side)

All error responses follow this format:

{
  "timestamp": "2026-07-25T22:16:49.827Z",
  "status": 400,
  "error": "Bad Request",
  "message": "Descriptive message"
}

Roadmap

  • Subtitle export (SRT/VTT)
  • Support for additional translation providers
  • Admin view for monitoring job queue and failures
  • Docker image publishing for one-command deployment

Portfolio project · designed and built by Yann

About

Audio/video to text transcription, powered by Whisper. Java 21 + Spring Boot + Angular 18.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages