diff --git a/apps/lightwrite/CLAUDE.md b/apps/lightwrite/CLAUDE.md deleted file mode 100644 index 56e98c66d..000000000 --- a/apps/lightwrite/CLAUDE.md +++ /dev/null @@ -1,165 +0,0 @@ -# LightWrite - Beat & Lyrics Editor - -LightWrite is a web application for creating and editing beats with synchronized lyrics. It provides waveform visualization, BPM detection, timestamp markers, and exports to multiple formats. - -## Architecture - -``` -apps/lightwrite/ -├── apps/ -│ ├── backend/ # NestJS API (port 3010) -│ ├── web/ # SvelteKit app (port 5180) -│ └── landing/ # Astro marketing page -├── packages/ -│ └── shared/ # Shared types (@lightwrite/shared) -└── package.json -``` - -## Quick Start - -```bash -# Start with full database setup -pnpm dev:lightwrite:full - -# Or start components individually -pnpm docker:up # Start PostgreSQL, Redis, MinIO -pnpm --filter @lightwrite/backend dev # Backend on port 3010 -pnpm --filter @lightwrite/web dev # Web on port 5180 -pnpm --filter @lightwrite/landing dev # Landing page -``` - -## Backend API Endpoints - -### Projects -- `GET /projects` - List user's projects -- `GET /projects/:id` - Get project with beat and lyrics -- `POST /projects` - Create project -- `PUT /projects/:id` - Update project -- `DELETE /projects/:id` - Delete project - -### Beats -- `GET /beats/project/:projectId` - Get beat for project -- `GET /beats/:id` - Get beat with markers -- `GET /beats/:id/download-url` - Get presigned download URL -- `POST /beats/upload` - Create beat and get upload URL -- `PUT /beats/:id/metadata` - Update BPM, duration, waveform data -- `DELETE /beats/:id` - Delete beat - -### Markers -- `GET /markers/beat/:beatId` - Get markers for beat -- `POST /markers` - Create marker -- `POST /markers/bulk` - Bulk create markers -- `PUT /markers/:id` - Update marker -- `PUT /markers/bulk` - Bulk update markers -- `DELETE /markers/:id` - Delete marker -- `DELETE /markers/beat/:beatId` - Delete all markers for beat - -### Lyrics -- `GET /lyrics/project/:projectId` - Get lyrics with synced lines -- `POST /lyrics/project/:projectId` - Create/update lyrics content -- `POST /lyrics/:id/sync` - Sync line timestamps -- `PUT /lyrics/line/:lineId/timestamp` - Update single line timestamp - -### Export -- `GET /export/:projectId?format=lrc|srt|json` - Export project - -## Database Schema - -```typescript -// projects - User projects -{ id, userId, title, description, createdAt, updatedAt } - -// beats - Audio files -{ id, projectId, storagePath, filename, duration, bpm, bpmConfidence, waveformData } - -// markers - Part/section markers -{ id, beatId, type, label, startTime, endTime, color, sortOrder } - -// lyrics - Full lyrics text -{ id, projectId, content } - -// lyric_lines - Individual synced lines -{ id, lyricsId, lineNumber, text, startTime, endTime } -``` - -## Marker Types - -- `intro` - Introduction -- `verse` - Verse section -- `hook` - Hook/Chorus -- `bridge` - Bridge section -- `drop` - Drop -- `breakdown` - Breakdown -- `outro` - Outro -- `custom` - Custom marker - -## Key Technologies - -| Component | Technology | -|-----------|------------| -| Frontend | SvelteKit 2, Svelte 5, Tailwind CSS 4 | -| Waveform | wavesurfer.js 7.x | -| BPM Detection | Web Audio API (peak detection) | -| Backend | NestJS 10, Drizzle ORM | -| Database | PostgreSQL | -| Storage | MinIO (dev) / Hetzner S3 (prod) | -| Auth | mana-core-auth | - -## Environment Variables - -### Backend (.env) -``` -DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/lightwrite -MANA_CORE_AUTH_URL=http://localhost:3001 -S3_ENDPOINT=http://localhost:9000 -S3_REGION=us-east-1 -S3_ACCESS_KEY=minioadmin -S3_SECRET_KEY=minioadmin -S3_BUCKET=lightwrite-storage -``` - -### Web (.env) -``` -PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 -PUBLIC_BACKEND_URL=http://localhost:3010 -``` - -## Export Formats - -| Format | Use Case | -|--------|----------| -| LRC | Standard lyrics format for music players | -| SRT | Subtitles for video players | -| JSON | API/integration, full project data | - -## Development Commands - -```bash -# Database -pnpm --filter @lightwrite/backend db:push # Push schema -pnpm --filter @lightwrite/backend db:studio # Open Drizzle Studio - -# Type checking -pnpm --filter @lightwrite/backend type-check -pnpm --filter @lightwrite/web type-check - -# Build -pnpm --filter @lightwrite/backend build -pnpm --filter @lightwrite/web build -``` - -## Feature Implementation Status - -- [x] Project CRUD -- [x] Beat upload with S3 storage -- [x] Waveform visualization (wavesurfer.js) -- [x] BPM detection (Web Audio API) -- [x] Part markers with regions -- [x] Lyrics editor with line sync -- [x] Karaoke preview -- [x] LRC export -- [x] SRT export -- [x] JSON export -- [ ] Video export (client-side Canvas → WebM) -- [ ] Word-by-word sync -- [ ] essentia.js WASM for better BPM detection diff --git a/apps/lightwrite/apps/backend/.env.example b/apps/lightwrite/apps/backend/.env.example deleted file mode 100644 index 18b9e74d1..000000000 --- a/apps/lightwrite/apps/backend/.env.example +++ /dev/null @@ -1,19 +0,0 @@ -# Database -DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/lightwrite - -# Auth -MANA_CORE_AUTH_URL=http://localhost:3001 -NODE_ENV=development -DEV_BYPASS_AUTH=true -DEV_USER_ID=dev-user-id - -# Storage (S3/MinIO) -S3_ENDPOINT=http://localhost:9000 -S3_REGION=us-east-1 -S3_ACCESS_KEY=minioadmin -S3_SECRET_KEY=minioadmin -S3_BUCKET=lightwrite-storage - -# STT (Speech-to-Text) -MANA_STT_URL=http://localhost:3020 -# MANA_STT_API_KEY= # Optional, only if mana-stt requires auth diff --git a/apps/lightwrite/apps/backend/Dockerfile b/apps/lightwrite/apps/backend/Dockerfile deleted file mode 100644 index baec505e6..000000000 --- a/apps/lightwrite/apps/backend/Dockerfile +++ /dev/null @@ -1,82 +0,0 @@ -# Build stage -FROM node:20-alpine AS builder - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@9.15.0 --activate - -WORKDIR /app - -# Copy root workspace files -COPY pnpm-workspace.yaml ./ -COPY package.json ./ -COPY pnpm-lock.yaml ./ - -# Copy shared packages -COPY packages/shared-drizzle-config ./packages/shared-drizzle-config -COPY packages/shared-errors ./packages/shared-errors -COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth -COPY packages/shared-nestjs-health ./packages/shared-nestjs-health -COPY packages/shared-nestjs-setup ./packages/shared-nestjs-setup -COPY packages/shared-storage ./packages/shared-storage -COPY packages/shared-tsconfig ./packages/shared-tsconfig - -# Copy lightwrite packages -COPY apps/lightwrite/packages ./apps/lightwrite/packages -COPY apps/lightwrite/apps/backend ./apps/lightwrite/apps/backend - -# Install dependencies -RUN pnpm install --frozen-lockfile --ignore-scripts - -# Build shared packages -WORKDIR /app/packages/shared-errors -RUN pnpm build - -WORKDIR /app/packages/shared-nestjs-auth -RUN pnpm build - -WORKDIR /app/packages/shared-nestjs-health -RUN pnpm build - -WORKDIR /app/packages/shared-storage -RUN pnpm build - -WORKDIR /app/packages/shared-nestjs-setup -RUN pnpm build - -# Build the backend -WORKDIR /app/apps/lightwrite/apps/backend -RUN pnpm build - -# Production stage -FROM node:20-alpine AS production - -# Install pnpm and postgresql-client -RUN corepack enable && corepack prepare pnpm@9.15.0 --activate \ - && apk add --no-cache postgresql-client - -WORKDIR /app - -# Copy everything from builder -COPY --from=builder /app/pnpm-workspace.yaml ./ -COPY --from=builder /app/package.json ./ -COPY --from=builder /app/pnpm-lock.yaml ./ -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/packages ./packages -COPY --from=builder /app/apps/lightwrite ./apps/lightwrite - -# Copy entrypoint script -COPY apps/lightwrite/apps/backend/docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -WORKDIR /app/apps/lightwrite/apps/backend - -# Expose port -EXPOSE 3010 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3010/health || exit 1 - -# Run entrypoint script -ENTRYPOINT ["docker-entrypoint.sh"] -CMD ["node", "dist/main.js"] diff --git a/apps/lightwrite/apps/backend/docker-entrypoint.sh b/apps/lightwrite/apps/backend/docker-entrypoint.sh deleted file mode 100644 index 2b2b5306c..000000000 --- a/apps/lightwrite/apps/backend/docker-entrypoint.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh -set -e - -echo "==========================================" -echo " LightWrite Backend Startup" -echo "==========================================" -echo "Environment: ${NODE_ENV:-development}" -echo "Port: ${PORT:-3010}" - -# Wait for database to be ready -if [ -n "$DATABASE_URL" ]; then - echo "Waiting for database..." - - # Extract host and port from DATABASE_URL - DB_HOST=$(echo $DATABASE_URL | sed -n 's/.*@\([^:]*\):.*/\1/p') - DB_PORT=$(echo $DATABASE_URL | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') - - # Default port if not found - DB_PORT=${DB_PORT:-5432} - - # Wait for database to accept connections - max_attempts=30 - attempt=1 - while [ $attempt -le $max_attempts ]; do - if pg_isready -h "$DB_HOST" -p "$DB_PORT" > /dev/null 2>&1; then - echo "Database is ready!" - break - fi - echo "Waiting for database... (attempt $attempt/$max_attempts)" - sleep 2 - attempt=$((attempt + 1)) - done - - if [ $attempt -gt $max_attempts ]; then - echo "Warning: Could not connect to database after $max_attempts attempts" - fi -fi - -# Push database schema (safe for production - only adds missing tables/columns) -if [ "$RUN_DB_PUSH" = "true" ]; then - echo "Pushing database schema..." - npx drizzle-kit push --force || echo "Warning: db:push failed, continuing anyway..." -fi - -echo "Starting application..." -exec "$@" diff --git a/apps/lightwrite/apps/backend/drizzle.config.ts b/apps/lightwrite/apps/backend/drizzle.config.ts deleted file mode 100644 index 9c94ba17b..000000000 --- a/apps/lightwrite/apps/backend/drizzle.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createDrizzleConfig } from '@manacore/shared-drizzle-config'; - -export default createDrizzleConfig({ - dbName: 'lightwrite', - additionalEnvVars: ['LIGHTWRITE_DATABASE_URL'], -}); diff --git a/apps/lightwrite/apps/backend/nest-cli.json b/apps/lightwrite/apps/backend/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/apps/lightwrite/apps/backend/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/apps/lightwrite/apps/backend/package.json b/apps/lightwrite/apps/backend/package.json deleted file mode 100644 index 354f61ed6..000000000 --- a/apps/lightwrite/apps/backend/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@lightwrite/backend", - "version": "1.0.0", - "private": true, - "scripts": { - "build": "nest build", - "start": "nest start", - "dev": "nest start --watch", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "type-check": "tsc --noEmit", - "migration:generate": "drizzle-kit generate", - "migration:run": "tsx src/db/migrate.ts", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio", - "db:seed": "tsx src/db/seed.ts" - }, - "dependencies": { - "@lightwrite/shared": "workspace:*", - "@manacore/shared-nestjs-auth": "workspace:*", - "@manacore/shared-nestjs-health": "workspace:*", - "@manacore/shared-nestjs-setup": "workspace:*", - "@manacore/shared-storage": "workspace:*", - "@nestjs/common": "^10.4.15", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.15", - "@nestjs/platform-express": "^10.4.15", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.1", - "dotenv": "^16.4.7", - "drizzle-kit": "^0.30.2", - "drizzle-orm": "^0.38.3", - "postgres": "^3.4.5", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" - }, - "devDependencies": { - "@nestjs/cli": "^10.4.9", - "@nestjs/schematics": "^10.2.3", - "@types/express": "^5.0.0", - "@types/node": "^22.10.2", - "@typescript-eslint/eslint-plugin": "^8.18.1", - "@typescript-eslint/parser": "^8.18.1", - "eslint": "^9.17.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.2.1", - "prettier": "^3.4.2", - "source-map-support": "^0.5.21", - "ts-loader": "^9.5.1", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", - "tsx": "^4.19.2", - "typescript": "^5.7.2" - } -} diff --git a/apps/lightwrite/apps/backend/src/app.module.ts b/apps/lightwrite/apps/backend/src/app.module.ts deleted file mode 100644 index 5349e3c82..000000000 --- a/apps/lightwrite/apps/backend/src/app.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { DatabaseModule } from './db/database.module'; -import { ProjectModule } from './project/project.module'; -import { BeatModule } from './beat/beat.module'; -import { MarkerModule } from './marker/marker.module'; -import { LyricsModule } from './lyrics/lyrics.module'; -import { ExportModule } from './export/export.module'; -import { SttModule } from './stt/stt.module'; -import { HealthModule } from '@manacore/shared-nestjs-health'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: '.env', - }), - DatabaseModule, - ProjectModule, - BeatModule, - MarkerModule, - LyricsModule, - ExportModule, - SttModule, - HealthModule.forRoot({ serviceName: 'lightwrite-backend' }), - ], -}) -export class AppModule {} diff --git a/apps/lightwrite/apps/backend/src/beat/beat.controller.ts b/apps/lightwrite/apps/backend/src/beat/beat.controller.ts deleted file mode 100644 index 7656dee61..000000000 --- a/apps/lightwrite/apps/backend/src/beat/beat.controller.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - UseGuards, - ParseUUIDPipe, -} from '@nestjs/common'; -import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; -import { BeatService } from './beat.service'; -import { CreateBeatUploadDto, UpdateBeatMetadataDto, UseLibraryBeatDto } from './dto/beat.dto'; - -@Controller('beats') -export class BeatController { - constructor(private readonly beatService: BeatService) {} - - // ==================== Library Beats (Public) ==================== - - @Get('library') - async getLibraryBeats() { - const beats = await this.beatService.getLibraryBeats(); - return { beats }; - } - - @Get('library/:id') - async getLibraryBeat(@Param('id', ParseUUIDPipe) id: string) { - const beat = await this.beatService.getLibraryBeatById(id); - if (!beat) { - return { beat: null }; - } - return { beat }; - } - - @Get('library/:id/download-url') - async getLibraryBeatDownloadUrl(@Param('id', ParseUUIDPipe) id: string) { - const url = await this.beatService.getLibraryBeatDownloadUrl(id); - return { url }; - } - - @Post('library/:id/use') - @UseGuards(JwtAuthGuard) - async useLibraryBeat( - @CurrentUser() user: CurrentUserData, - @Param('id', ParseUUIDPipe) id: string, - @Body() dto: UseLibraryBeatDto - ) { - const beat = await this.beatService.useLibraryBeat(id, dto.projectId, user.userId); - return { beat }; - } - - // ==================== STT Transcription ==================== - - @Get('stt/available') - async getSttAvailability() { - const available = await this.beatService.isSttAvailable(); - return { available }; - } - - // ==================== User Beats (Protected) ==================== - - @Get('project/:projectId') - @UseGuards(JwtAuthGuard) - async findByProject( - @CurrentUser() user: CurrentUserData, - @Param('projectId', ParseUUIDPipe) projectId: string - ) { - await this.beatService.verifyProjectOwnership(projectId, user.userId); - const beat = await this.beatService.findByProjectId(projectId); - return { beat }; - } - - @Get(':id') - @UseGuards(JwtAuthGuard) - async findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { - const beat = await this.beatService.findByIdOrThrow(id); - await this.beatService.verifyProjectOwnership(beat.projectId, user.userId); - const beatMarkers = await this.beatService.getMarkersForBeat(id); - return { beat, markers: beatMarkers }; - } - - @Get(':id/download-url') - @UseGuards(JwtAuthGuard) - async getDownloadUrl( - @CurrentUser() user: CurrentUserData, - @Param('id', ParseUUIDPipe) id: string - ) { - const url = await this.beatService.getDownloadUrl(id, user.userId); - return { url }; - } - - @Post('upload') - @UseGuards(JwtAuthGuard) - async createUploadUrl(@CurrentUser() user: CurrentUserData, @Body() dto: CreateBeatUploadDto) { - const result = await this.beatService.createUploadUrl(dto.projectId, user.userId, dto.filename); - return result; - } - - @Put(':id/metadata') - @UseGuards(JwtAuthGuard) - async updateMetadata( - @CurrentUser() user: CurrentUserData, - @Param('id', ParseUUIDPipe) id: string, - @Body() dto: UpdateBeatMetadataDto - ) { - const beat = await this.beatService.updateBeatMetadata(id, user.userId, dto); - return { beat }; - } - - @Delete(':id') - @UseGuards(JwtAuthGuard) - async delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { - await this.beatService.delete(id, user.userId); - return { success: true }; - } - - @Post(':id/transcribe') - @UseGuards(JwtAuthGuard) - async transcribeBeat( - @CurrentUser() user: CurrentUserData, - @Param('id', ParseUUIDPipe) id: string - ) { - const result = await this.beatService.transcribeBeat(id, user.userId); - return result; - } -} diff --git a/apps/lightwrite/apps/backend/src/beat/beat.module.ts b/apps/lightwrite/apps/backend/src/beat/beat.module.ts deleted file mode 100644 index 026443f44..000000000 --- a/apps/lightwrite/apps/backend/src/beat/beat.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module, forwardRef } from '@nestjs/common'; -import { BeatController } from './beat.controller'; -import { BeatService } from './beat.service'; -import { SttModule } from '../stt/stt.module'; -import { LyricsModule } from '../lyrics/lyrics.module'; - -@Module({ - imports: [SttModule, forwardRef(() => LyricsModule)], - controllers: [BeatController], - providers: [BeatService], - exports: [BeatService], -}) -export class BeatModule {} diff --git a/apps/lightwrite/apps/backend/src/beat/beat.service.ts b/apps/lightwrite/apps/backend/src/beat/beat.service.ts deleted file mode 100644 index 0b4936695..000000000 --- a/apps/lightwrite/apps/backend/src/beat/beat.service.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { Injectable, Inject, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; -import { eq, and } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../db/database.module'; -import { Database } from '../db/connection'; -import { beats, projects, markers, libraryBeats } from '../db/schema'; -import type { Beat, Marker, LibraryBeat } from '../db/schema'; -import { - createLightWriteStorage, - generateUserFileKey, - getContentType, - type StorageClient, -} from '@manacore/shared-storage'; -import { SttService } from '../stt/stt.service'; -import { LyricsService } from '../lyrics/lyrics.service'; - -@Injectable() -export class BeatService { - private readonly logger = new Logger(BeatService.name); - private storage: StorageClient; - - constructor( - @Inject(DATABASE_CONNECTION) private db: Database, - private sttService: SttService, - private lyricsService: LyricsService - ) { - this.storage = createLightWriteStorage(); - } - - async findByProjectId(projectId: string): Promise { - const [beat] = await this.db.select().from(beats).where(eq(beats.projectId, projectId)); - return beat || null; - } - - async findById(id: string): Promise { - const [beat] = await this.db.select().from(beats).where(eq(beats.id, id)); - return beat || null; - } - - async findByIdOrThrow(id: string): Promise { - const beat = await this.findById(id); - if (!beat) { - throw new NotFoundException('Beat not found'); - } - return beat; - } - - async verifyProjectOwnership(projectId: string, userId: string): Promise { - const [project] = await this.db - .select() - .from(projects) - .where(and(eq(projects.id, projectId), eq(projects.userId, userId))); - if (!project) { - throw new NotFoundException('Project not found'); - } - } - - async createUploadUrl( - projectId: string, - userId: string, - filename: string - ): Promise<{ beat: Beat; uploadUrl: string }> { - await this.verifyProjectOwnership(projectId, userId); - - // Check if beat already exists for this project - const existingBeat = await this.findByProjectId(projectId); - if (existingBeat) { - throw new BadRequestException('Beat already exists for this project. Delete it first.'); - } - - const key = generateUserFileKey(userId, filename); - const contentType = getContentType(filename); - - if (!contentType.startsWith('audio/') && !['application/octet-stream'].includes(contentType)) { - throw new BadRequestException('Invalid file type. Only audio files are allowed.'); - } - - // Create beat record - const [beat] = await this.db - .insert(beats) - .values({ - projectId, - storagePath: key, - filename, - }) - .returning(); - - // Generate presigned upload URL - const uploadUrl = await this.storage.getUploadUrl(key, { - expiresIn: 3600, - }); - - return { beat, uploadUrl }; - } - - async updateBeatMetadata( - id: string, - userId: string, - data: { - duration?: number; - bpm?: number; - bpmConfidence?: number; - waveformData?: unknown; - } - ): Promise { - const beat = await this.findByIdOrThrow(id); - await this.verifyProjectOwnership(beat.projectId, userId); - - const [updatedBeat] = await this.db.update(beats).set(data).where(eq(beats.id, id)).returning(); - return updatedBeat; - } - - async getDownloadUrl(id: string, userId: string): Promise { - const beat = await this.findByIdOrThrow(id); - await this.verifyProjectOwnership(beat.projectId, userId); - - return this.storage.getDownloadUrl(beat.storagePath, { expiresIn: 3600 }); - } - - async delete(id: string, userId: string): Promise { - const beat = await this.findByIdOrThrow(id); - await this.verifyProjectOwnership(beat.projectId, userId); - - // Delete from storage - try { - await this.storage.delete(beat.storagePath); - } catch { - // Ignore storage errors, continue with DB deletion - } - - // Delete from database (markers will be cascade deleted) - await this.db.delete(beats).where(eq(beats.id, id)); - } - - async getMarkersForBeat(beatId: string): Promise { - return this.db.select().from(markers).where(eq(markers.beatId, beatId)); - } - - // ==================== Library Beats ==================== - - async getLibraryBeats(): Promise { - return this.db - .select() - .from(libraryBeats) - .where(eq(libraryBeats.isActive, true)) - .orderBy(libraryBeats.title); - } - - async getLibraryBeatById(id: string): Promise { - const [beat] = await this.db.select().from(libraryBeats).where(eq(libraryBeats.id, id)); - return beat || null; - } - - async getLibraryBeatDownloadUrl(id: string): Promise { - const beat = await this.getLibraryBeatById(id); - if (!beat) { - throw new NotFoundException('Library beat not found'); - } - return this.storage.getDownloadUrl(beat.storagePath, { expiresIn: 3600 }); - } - - async useLibraryBeat(libraryBeatId: string, projectId: string, userId: string): Promise { - await this.verifyProjectOwnership(projectId, userId); - - // Check if beat already exists for this project - const existingBeat = await this.findByProjectId(projectId); - if (existingBeat) { - throw new BadRequestException('Beat already exists for this project. Delete it first.'); - } - - const libraryBeat = await this.getLibraryBeatById(libraryBeatId); - if (!libraryBeat) { - throw new NotFoundException('Library beat not found'); - } - - // Create beat record referencing the same storage path - const [beat] = await this.db - .insert(beats) - .values({ - projectId, - storagePath: libraryBeat.storagePath, - filename: `${libraryBeat.title}${libraryBeat.artist ? ` - ${libraryBeat.artist}` : ''}.mp3`, - duration: libraryBeat.duration, - bpm: libraryBeat.bpm, - }) - .returning(); - - return beat; - } - - // ==================== STT Transcription ==================== - - /** - * Check if STT service is available - */ - async isSttAvailable(): Promise { - return this.sttService.isAvailable(); - } - - /** - * Transcribe beat audio and save lyrics to the project - */ - async transcribeBeat( - beatId: string, - userId: string - ): Promise<{ beat: Beat; lyrics: string | null }> { - const beat = await this.findByIdOrThrow(beatId); - await this.verifyProjectOwnership(beat.projectId, userId); - - // Set status to pending - await this.db - .update(beats) - .set({ - transcriptionStatus: 'pending', - transcriptionError: null, - }) - .where(eq(beats.id, beatId)); - - try { - this.logger.log(`Starting transcription for beat ${beatId}`); - - // Download audio from storage - const audioBuffer = await this.storage.download(beat.storagePath); - - // Call STT service - const result = await this.sttService.transcribe(audioBuffer, beat.filename || 'audio.mp3'); - - // Save transcribed text as lyrics - const lyricsRecord = await this.lyricsService.createOrUpdate( - beat.projectId, - userId, - result.text - ); - - // Update beat status to completed - const [updatedBeat] = await this.db - .update(beats) - .set({ - transcriptionStatus: 'completed', - transcribedAt: new Date(), - transcriptionError: null, - }) - .where(eq(beats.id, beatId)) - .returning(); - - this.logger.log(`Transcription completed for beat ${beatId}: ${result.text.length} chars`); - - return { - beat: updatedBeat, - lyrics: lyricsRecord.content, - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - this.logger.error(`Transcription failed for beat ${beatId}: ${errorMessage}`); - - // Update beat status to failed - await this.db - .update(beats) - .set({ - transcriptionStatus: 'failed', - transcriptionError: errorMessage, - }) - .where(eq(beats.id, beatId)); - - throw error; - } - } -} diff --git a/apps/lightwrite/apps/backend/src/beat/dto/beat.dto.ts b/apps/lightwrite/apps/backend/src/beat/dto/beat.dto.ts deleted file mode 100644 index c32925b5b..000000000 --- a/apps/lightwrite/apps/backend/src/beat/dto/beat.dto.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { IsString, IsNotEmpty, IsUUID, IsNumber, IsOptional, IsObject } from 'class-validator'; - -export class CreateBeatUploadDto { - @IsUUID() - @IsNotEmpty() - projectId!: string; - - @IsString() - @IsNotEmpty() - filename!: string; -} - -export class UseLibraryBeatDto { - @IsUUID() - @IsNotEmpty() - projectId!: string; -} - -export class UpdateBeatMetadataDto { - @IsNumber() - @IsOptional() - duration?: number; - - @IsNumber() - @IsOptional() - bpm?: number; - - @IsNumber() - @IsOptional() - bpmConfidence?: number; - - @IsObject() - @IsOptional() - waveformData?: { - peaks: number[]; - sampleRate: number; - duration: number; - }; -} diff --git a/apps/lightwrite/apps/backend/src/db/connection.ts b/apps/lightwrite/apps/backend/src/db/connection.ts deleted file mode 100644 index fccc63f4a..000000000 --- a/apps/lightwrite/apps/backend/src/db/connection.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { drizzle } from 'drizzle-orm/postgres-js'; -import * as schema from './schema'; - -// Use require for postgres to avoid ESM/CommonJS interop issues -// eslint-disable-next-line @typescript-eslint/no-var-requires -const postgres = require('postgres'); - -let connection: ReturnType | null = null; -let db: ReturnType | null = null; - -export function getConnection(databaseUrl: string) { - if (!connection) { - connection = postgres(databaseUrl, { - max: 10, - idle_timeout: 20, - connect_timeout: 10, - }); - } - return connection; -} - -export function getDb(databaseUrl: string) { - if (!db) { - const conn = getConnection(databaseUrl); - db = drizzle(conn, { schema }); - } - return db; -} - -export async function closeConnection() { - if (connection) { - await connection.end(); - connection = null; - db = null; - } -} - -export type Database = ReturnType; diff --git a/apps/lightwrite/apps/backend/src/db/database.module.ts b/apps/lightwrite/apps/backend/src/db/database.module.ts deleted file mode 100644 index 5a0a033b3..000000000 --- a/apps/lightwrite/apps/backend/src/db/database.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Module, Global, OnModuleDestroy } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { getDb, closeConnection } from './connection'; -import type { Database } from './connection'; - -export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; - -@Global() -@Module({ - providers: [ - { - provide: DATABASE_CONNECTION, - useFactory: (configService: ConfigService): Database => { - const databaseUrl = configService.get('DATABASE_URL'); - if (!databaseUrl) { - throw new Error('DATABASE_URL environment variable is not set'); - } - return getDb(databaseUrl); - }, - inject: [ConfigService], - }, - ], - exports: [DATABASE_CONNECTION], -}) -export class DatabaseModule implements OnModuleDestroy { - async onModuleDestroy() { - await closeConnection(); - } -} diff --git a/apps/lightwrite/apps/backend/src/db/migrate.ts b/apps/lightwrite/apps/backend/src/db/migrate.ts deleted file mode 100644 index 902f9f6a8..000000000 --- a/apps/lightwrite/apps/backend/src/db/migrate.ts +++ /dev/null @@ -1,26 +0,0 @@ -import 'dotenv/config'; -import { drizzle } from 'drizzle-orm/postgres-js'; -import { migrate } from 'drizzle-orm/postgres-js/migrator'; -import postgres from 'postgres'; - -async function main() { - const databaseUrl = process.env.DATABASE_URL; - if (!databaseUrl) { - throw new Error('DATABASE_URL environment variable is not set'); - } - - const connection = postgres(databaseUrl, { max: 1 }); - const db = drizzle(connection); - - console.log('Running migrations...'); - await migrate(db, { migrationsFolder: './drizzle' }); - console.log('Migrations complete!'); - - await connection.end(); - process.exit(0); -} - -main().catch((err) => { - console.error('Migration failed:', err); - process.exit(1); -}); diff --git a/apps/lightwrite/apps/backend/src/db/schema/beats.schema.ts b/apps/lightwrite/apps/backend/src/db/schema/beats.schema.ts deleted file mode 100644 index dd29cca1b..000000000 --- a/apps/lightwrite/apps/backend/src/db/schema/beats.schema.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { pgTable, uuid, text, timestamp, varchar, real, jsonb } from 'drizzle-orm/pg-core'; -import { projects } from './projects.schema'; - -export const beats = pgTable('beats', { - id: uuid('id').primaryKey().defaultRandom(), - projectId: uuid('project_id') - .references(() => projects.id, { onDelete: 'cascade' }) - .notNull(), - storagePath: text('storage_path').notNull(), - filename: varchar('filename', { length: 255 }), - duration: real('duration'), - bpm: real('bpm'), - bpmConfidence: real('bpm_confidence'), - waveformData: jsonb('waveform_data'), - // STT Transcription fields - transcriptionStatus: varchar('transcription_status', { length: 50 }).default('none'), // 'none' | 'pending' | 'completed' | 'failed' - transcriptionError: text('transcription_error'), - transcribedAt: timestamp('transcribed_at', { withTimezone: true }), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), -}); - -export type Beat = typeof beats.$inferSelect; -export type NewBeat = typeof beats.$inferInsert; diff --git a/apps/lightwrite/apps/backend/src/db/schema/index.ts b/apps/lightwrite/apps/backend/src/db/schema/index.ts deleted file mode 100644 index 078101c6f..000000000 --- a/apps/lightwrite/apps/backend/src/db/schema/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './projects.schema'; -export * from './beats.schema'; -export * from './markers.schema'; -export * from './lyrics.schema'; -export * from './library-beats.schema'; diff --git a/apps/lightwrite/apps/backend/src/db/schema/library-beats.schema.ts b/apps/lightwrite/apps/backend/src/db/schema/library-beats.schema.ts deleted file mode 100644 index e500a85e0..000000000 --- a/apps/lightwrite/apps/backend/src/db/schema/library-beats.schema.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { pgTable, uuid, text, timestamp, varchar, real, boolean } from 'drizzle-orm/pg-core'; - -/** - * Library beats are free beats available to all users. - * They are pre-uploaded by admins and can be used in any project. - */ -export const libraryBeats = pgTable('library_beats', { - id: uuid('id').primaryKey().defaultRandom(), - title: varchar('title', { length: 255 }).notNull(), - artist: varchar('artist', { length: 255 }), - genre: varchar('genre', { length: 100 }), - bpm: real('bpm'), - duration: real('duration'), - storagePath: text('storage_path').notNull(), - previewUrl: text('preview_url'), - license: varchar('license', { length: 100 }).default('free'), - isActive: boolean('is_active').default(true), - tags: text('tags').array(), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), -}); - -export type LibraryBeat = typeof libraryBeats.$inferSelect; -export type NewLibraryBeat = typeof libraryBeats.$inferInsert; diff --git a/apps/lightwrite/apps/backend/src/db/schema/lyrics.schema.ts b/apps/lightwrite/apps/backend/src/db/schema/lyrics.schema.ts deleted file mode 100644 index 1a4195f43..000000000 --- a/apps/lightwrite/apps/backend/src/db/schema/lyrics.schema.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { pgTable, uuid, text, real, integer } from 'drizzle-orm/pg-core'; -import { projects } from './projects.schema'; - -export const lyrics = pgTable('lyrics', { - id: uuid('id').primaryKey().defaultRandom(), - projectId: uuid('project_id') - .references(() => projects.id, { onDelete: 'cascade' }) - .notNull() - .unique(), - content: text('content'), -}); - -export const lyricLines = pgTable('lyric_lines', { - id: uuid('id').primaryKey().defaultRandom(), - lyricsId: uuid('lyrics_id') - .references(() => lyrics.id, { onDelete: 'cascade' }) - .notNull(), - lineNumber: integer('line_number').notNull(), - text: text('text').notNull(), - startTime: real('start_time'), - endTime: real('end_time'), -}); - -export type Lyrics = typeof lyrics.$inferSelect; -export type NewLyrics = typeof lyrics.$inferInsert; -export type LyricLine = typeof lyricLines.$inferSelect; -export type NewLyricLine = typeof lyricLines.$inferInsert; diff --git a/apps/lightwrite/apps/backend/src/db/schema/markers.schema.ts b/apps/lightwrite/apps/backend/src/db/schema/markers.schema.ts deleted file mode 100644 index d39cef761..000000000 --- a/apps/lightwrite/apps/backend/src/db/schema/markers.schema.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { pgTable, uuid, varchar, real, integer } from 'drizzle-orm/pg-core'; -import { beats } from './beats.schema'; - -export const markers = pgTable('markers', { - id: uuid('id').primaryKey().defaultRandom(), - beatId: uuid('beat_id') - .references(() => beats.id, { onDelete: 'cascade' }) - .notNull(), - type: varchar('type', { length: 50 }).notNull(), - label: varchar('label', { length: 100 }), - startTime: real('start_time').notNull(), - endTime: real('end_time'), - color: varchar('color', { length: 7 }), - sortOrder: integer('sort_order'), -}); - -export type Marker = typeof markers.$inferSelect; -export type NewMarker = typeof markers.$inferInsert; diff --git a/apps/lightwrite/apps/backend/src/db/schema/projects.schema.ts b/apps/lightwrite/apps/backend/src/db/schema/projects.schema.ts deleted file mode 100644 index 9336137f1..000000000 --- a/apps/lightwrite/apps/backend/src/db/schema/projects.schema.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { pgTable, uuid, text, timestamp, varchar } from 'drizzle-orm/pg-core'; - -export const projects = pgTable('projects', { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id').notNull(), - title: varchar('title', { length: 255 }).notNull(), - description: text('description'), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), -}); - -export type Project = typeof projects.$inferSelect; -export type NewProject = typeof projects.$inferInsert; diff --git a/apps/lightwrite/apps/backend/src/db/seed.ts b/apps/lightwrite/apps/backend/src/db/seed.ts deleted file mode 100644 index 17e70faaa..000000000 --- a/apps/lightwrite/apps/backend/src/db/seed.ts +++ /dev/null @@ -1,34 +0,0 @@ -import 'dotenv/config'; -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import * as schema from './schema'; - -async function main() { - const databaseUrl = process.env.DATABASE_URL; - if (!databaseUrl) { - throw new Error('DATABASE_URL environment variable is not set'); - } - - const connection = postgres(databaseUrl, { max: 1 }); - const db = drizzle(connection, { schema }); - - console.log('Seeding database...'); - - // Add seed data here if needed - // Example: - // await db.insert(schema.projects).values({ - // userId: 'test-user', - // title: 'Demo Project', - // description: 'A demo project for testing', - // }); - - console.log('Seeding complete!'); - - await connection.end(); - process.exit(0); -} - -main().catch((err) => { - console.error('Seeding failed:', err); - process.exit(1); -}); diff --git a/apps/lightwrite/apps/backend/src/export/export.controller.ts b/apps/lightwrite/apps/backend/src/export/export.controller.ts deleted file mode 100644 index 242425dfa..000000000 --- a/apps/lightwrite/apps/backend/src/export/export.controller.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Controller, Get, Param, Query, UseGuards, ParseUUIDPipe, Res } from '@nestjs/common'; -import { Response } from 'express'; -import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; -import { ExportService } from './export.service'; -import type { ExportFormat } from '@lightwrite/shared'; - -@Controller('export') -@UseGuards(JwtAuthGuard) -export class ExportController { - constructor(private readonly exportService: ExportService) {} - - @Get(':projectId') - async exportProject( - @CurrentUser() user: CurrentUserData, - @Param('projectId', ParseUUIDPipe) projectId: string, - @Query('format') format: ExportFormat = 'json', - @Res() res: Response - ) { - const result = await this.exportService.exportProject(projectId, user.userId, format); - - res.setHeader('Content-Type', result.contentType); - res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`); - res.send(result.content); - } -} diff --git a/apps/lightwrite/apps/backend/src/export/export.module.ts b/apps/lightwrite/apps/backend/src/export/export.module.ts deleted file mode 100644 index f760abd25..000000000 --- a/apps/lightwrite/apps/backend/src/export/export.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ExportController } from './export.controller'; -import { ExportService } from './export.service'; -import { ProjectModule } from '../project/project.module'; -import { BeatModule } from '../beat/beat.module'; -import { MarkerModule } from '../marker/marker.module'; -import { LyricsModule } from '../lyrics/lyrics.module'; - -@Module({ - imports: [ProjectModule, BeatModule, MarkerModule, LyricsModule], - controllers: [ExportController], - providers: [ExportService], - exports: [ExportService], -}) -export class ExportModule {} diff --git a/apps/lightwrite/apps/backend/src/export/export.service.ts b/apps/lightwrite/apps/backend/src/export/export.service.ts deleted file mode 100644 index d935fc693..000000000 --- a/apps/lightwrite/apps/backend/src/export/export.service.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; -import { ProjectService } from '../project/project.service'; -import { BeatService } from '../beat/beat.service'; -import { MarkerService } from '../marker/marker.service'; -import { LyricsService } from '../lyrics/lyrics.service'; -import type { ExportFormat, JsonExportData } from '@lightwrite/shared'; - -@Injectable() -export class ExportService { - constructor( - private projectService: ProjectService, - private beatService: BeatService, - private markerService: MarkerService, - private lyricsService: LyricsService - ) {} - - async exportProject( - projectId: string, - userId: string, - format: ExportFormat - ): Promise<{ content: string; filename: string; contentType: string }> { - const project = await this.projectService.findByIdOrThrow(projectId, userId); - const beat = await this.beatService.findByProjectId(projectId); - const lyricsData = await this.lyricsService.getWithLines(projectId, userId); - const markerList = beat ? await this.markerService.findByBeatId(beat.id) : []; - - const lines = lyricsData?.lines || []; - const safeTitle = project.title.replace(/[^a-z0-9]/gi, '_').toLowerCase(); - - switch (format) { - case 'lrc': - return { - content: this.generateLrc(lines, beat?.bpm), - filename: `${safeTitle}.lrc`, - contentType: 'text/plain', - }; - case 'srt': - return { - content: this.generateSrt(lines), - filename: `${safeTitle}.srt`, - contentType: 'text/plain', - }; - case 'json': - return { - content: this.generateJson(project, beat, markerList, lines), - filename: `${safeTitle}.json`, - contentType: 'application/json', - }; - case 'video': - throw new BadRequestException( - 'Video export is not yet supported. Use client-side video generation.' - ); - default: - throw new BadRequestException(`Unknown export format: ${format}`); - } - } - - private formatTime(seconds: number, format: 'lrc' | 'srt'): string { - const minutes = Math.floor(seconds / 60); - const secs = seconds % 60; - - if (format === 'lrc') { - // LRC format: [mm:ss.xx] - const hundredths = Math.round((secs % 1) * 100); - const wholeSecs = Math.floor(secs); - return `[${minutes.toString().padStart(2, '0')}:${wholeSecs.toString().padStart(2, '0')}.${hundredths.toString().padStart(2, '0')}]`; - } else { - // SRT format: hh:mm:ss,mmm - const hours = Math.floor(minutes / 60); - const mins = minutes % 60; - const millis = Math.round((secs % 1) * 1000); - const wholeSecs = Math.floor(secs); - return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${wholeSecs.toString().padStart(2, '0')},${millis.toString().padStart(3, '0')}`; - } - } - - private generateLrc( - lines: Array<{ text: string; startTime?: number | null; lineNumber: number }>, - bpm?: number | null - ): string { - const output: string[] = []; - - // Add metadata - output.push('[ti:LightWrite Export]'); - output.push('[ar:Unknown Artist]'); - if (bpm) { - output.push(`[bpm:${bpm}]`); - } - output.push(''); - - // Add synced lines - for (const line of lines) { - if (line.startTime !== null && line.startTime !== undefined) { - const timestamp = this.formatTime(line.startTime, 'lrc'); - output.push(`${timestamp}${line.text}`); - } else { - output.push(line.text); - } - } - - return output.join('\n'); - } - - private generateSrt( - lines: Array<{ - text: string; - startTime?: number | null; - endTime?: number | null; - lineNumber: number; - }> - ): string { - const output: string[] = []; - let index = 1; - - for (const line of lines) { - if (line.startTime !== null && line.startTime !== undefined) { - const start = this.formatTime(line.startTime, 'srt'); - const end = this.formatTime(line.endTime ?? line.startTime + 3, 'srt'); - - output.push(index.toString()); - output.push(`${start} --> ${end}`); - output.push(line.text); - output.push(''); - index++; - } - } - - return output.join('\n'); - } - - private generateJson( - project: { id: string; title: string; description?: string | null }, - beat: { bpm?: number | null; duration?: number | null } | null, - markers: Array<{ - type: string; - label?: string | null; - startTime: number; - endTime?: number | null; - }>, - lines: Array<{ - lineNumber: number; - text: string; - startTime?: number | null; - endTime?: number | null; - }> - ): string { - const data: JsonExportData = { - project: { - id: project.id, - title: project.title, - description: project.description || undefined, - }, - beat: { - bpm: beat?.bpm || undefined, - duration: beat?.duration || undefined, - }, - markers: markers.map((m) => ({ - type: m.type, - label: m.label || undefined, - startTime: m.startTime, - endTime: m.endTime || undefined, - })), - lyrics: lines.map((l) => ({ - lineNumber: l.lineNumber, - text: l.text, - startTime: l.startTime || undefined, - endTime: l.endTime || undefined, - })), - }; - - return JSON.stringify(data, null, 2); - } -} diff --git a/apps/lightwrite/apps/backend/src/lyrics/dto/lyrics.dto.ts b/apps/lightwrite/apps/backend/src/lyrics/dto/lyrics.dto.ts deleted file mode 100644 index 019252484..000000000 --- a/apps/lightwrite/apps/backend/src/lyrics/dto/lyrics.dto.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - IsString, - IsNumber, - IsOptional, - IsArray, - ValidateNested, - IsInt, - Min, -} from 'class-validator'; -import { Type } from 'class-transformer'; - -export class CreateOrUpdateLyricsDto { - @IsString() - content!: string; -} - -class LyricLineDto { - @IsInt() - @Min(0) - lineNumber!: number; - - @IsString() - text!: string; - - @IsNumber() - @IsOptional() - @Min(0) - startTime?: number; - - @IsNumber() - @IsOptional() - @Min(0) - endTime?: number; -} - -export class SyncLinesDto { - @IsArray() - @ValidateNested({ each: true }) - @Type(() => LyricLineDto) - lines!: LyricLineDto[]; -} - -export class UpdateLineTimestampDto { - @IsNumber() - @IsOptional() - @Min(0) - startTime?: number; - - @IsNumber() - @IsOptional() - @Min(0) - endTime?: number; -} diff --git a/apps/lightwrite/apps/backend/src/lyrics/lyrics.controller.ts b/apps/lightwrite/apps/backend/src/lyrics/lyrics.controller.ts deleted file mode 100644 index fd3525c0e..000000000 --- a/apps/lightwrite/apps/backend/src/lyrics/lyrics.controller.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Controller, Get, Post, Put, Body, Param, UseGuards, ParseUUIDPipe } from '@nestjs/common'; -import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; -import { LyricsService } from './lyrics.service'; -import { CreateOrUpdateLyricsDto, SyncLinesDto, UpdateLineTimestampDto } from './dto/lyrics.dto'; - -@Controller('lyrics') -@UseGuards(JwtAuthGuard) -export class LyricsController { - constructor(private readonly lyricsService: LyricsService) {} - - @Get('project/:projectId') - async findByProject( - @CurrentUser() user: CurrentUserData, - @Param('projectId', ParseUUIDPipe) projectId: string - ) { - const result = await this.lyricsService.getWithLines(projectId, user.userId); - return { lyrics: result }; - } - - @Post('project/:projectId') - async createOrUpdate( - @CurrentUser() user: CurrentUserData, - @Param('projectId', ParseUUIDPipe) projectId: string, - @Body() dto: CreateOrUpdateLyricsDto - ) { - const lyricsRecord = await this.lyricsService.createOrUpdate( - projectId, - user.userId, - dto.content - ); - return { lyrics: lyricsRecord }; - } - - @Post(':id/sync') - async syncLines( - @CurrentUser() user: CurrentUserData, - @Param('id', ParseUUIDPipe) id: string, - @Body() dto: SyncLinesDto - ) { - const lines = await this.lyricsService.syncLines(id, user.userId, dto.lines); - return { lines }; - } - - @Put('line/:lineId/timestamp') - async updateLineTimestamp( - @CurrentUser() user: CurrentUserData, - @Param('lineId', ParseUUIDPipe) lineId: string, - @Body() dto: UpdateLineTimestampDto - ) { - const line = await this.lyricsService.updateLineTimestamp(lineId, user.userId, dto); - return { line }; - } -} diff --git a/apps/lightwrite/apps/backend/src/lyrics/lyrics.module.ts b/apps/lightwrite/apps/backend/src/lyrics/lyrics.module.ts deleted file mode 100644 index 203ca8185..000000000 --- a/apps/lightwrite/apps/backend/src/lyrics/lyrics.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { LyricsController } from './lyrics.controller'; -import { LyricsService } from './lyrics.service'; - -@Module({ - controllers: [LyricsController], - providers: [LyricsService], - exports: [LyricsService], -}) -export class LyricsModule {} diff --git a/apps/lightwrite/apps/backend/src/lyrics/lyrics.service.ts b/apps/lightwrite/apps/backend/src/lyrics/lyrics.service.ts deleted file mode 100644 index 2125b3081..000000000 --- a/apps/lightwrite/apps/backend/src/lyrics/lyrics.service.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Injectable, Inject, NotFoundException } from '@nestjs/common'; -import { eq, and, asc } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../db/database.module'; -import { Database } from '../db/connection'; -import { lyrics, lyricLines, projects } from '../db/schema'; -import type { Lyrics, NewLyrics, LyricLine, NewLyricLine } from '../db/schema'; - -@Injectable() -export class LyricsService { - constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} - - async verifyProjectOwnership(projectId: string, userId: string): Promise { - const [project] = await this.db - .select() - .from(projects) - .where(and(eq(projects.id, projectId), eq(projects.userId, userId))); - if (!project) { - throw new NotFoundException('Project not found'); - } - } - - async findByProjectId(projectId: string): Promise { - const [lyricsRecord] = await this.db - .select() - .from(lyrics) - .where(eq(lyrics.projectId, projectId)); - return lyricsRecord || null; - } - - async findById(id: string): Promise { - const [lyricsRecord] = await this.db.select().from(lyrics).where(eq(lyrics.id, id)); - return lyricsRecord || null; - } - - async findByIdOrThrow(id: string): Promise { - const lyricsRecord = await this.findById(id); - if (!lyricsRecord) { - throw new NotFoundException('Lyrics not found'); - } - return lyricsRecord; - } - - async createOrUpdate(projectId: string, userId: string, content: string): Promise { - await this.verifyProjectOwnership(projectId, userId); - - const existing = await this.findByProjectId(projectId); - if (existing) { - const [updated] = await this.db - .update(lyrics) - .set({ content }) - .where(eq(lyrics.id, existing.id)) - .returning(); - return updated; - } - - const [created] = await this.db.insert(lyrics).values({ projectId, content }).returning(); - return created; - } - - async getLinesForLyrics(lyricsId: string): Promise { - return this.db - .select() - .from(lyricLines) - .where(eq(lyricLines.lyricsId, lyricsId)) - .orderBy(asc(lyricLines.lineNumber)); - } - - async syncLines( - lyricsId: string, - userId: string, - lines: Array<{ - lineNumber: number; - text: string; - startTime?: number; - endTime?: number; - }> - ): Promise { - const lyricsRecord = await this.findByIdOrThrow(lyricsId); - await this.verifyProjectOwnership(lyricsRecord.projectId, userId); - - // Delete existing lines - await this.db.delete(lyricLines).where(eq(lyricLines.lyricsId, lyricsId)); - - if (lines.length === 0) return []; - - // Insert new lines - const values: NewLyricLine[] = lines.map((line) => ({ - lyricsId, - lineNumber: line.lineNumber, - text: line.text, - startTime: line.startTime, - endTime: line.endTime, - })); - - return this.db.insert(lyricLines).values(values).returning(); - } - - async updateLineTimestamp( - lineId: string, - userId: string, - data: { startTime?: number; endTime?: number } - ): Promise { - const [line] = await this.db.select().from(lyricLines).where(eq(lyricLines.id, lineId)); - if (!line) { - throw new NotFoundException('Lyric line not found'); - } - - const lyricsRecord = await this.findByIdOrThrow(line.lyricsId); - await this.verifyProjectOwnership(lyricsRecord.projectId, userId); - - const [updated] = await this.db - .update(lyricLines) - .set(data) - .where(eq(lyricLines.id, lineId)) - .returning(); - return updated; - } - - async getWithLines(projectId: string, userId: string) { - await this.verifyProjectOwnership(projectId, userId); - - const lyricsRecord = await this.findByProjectId(projectId); - if (!lyricsRecord) { - return null; - } - - const lines = await this.getLinesForLyrics(lyricsRecord.id); - return { - ...lyricsRecord, - lines, - }; - } -} diff --git a/apps/lightwrite/apps/backend/src/main.ts b/apps/lightwrite/apps/backend/src/main.ts deleted file mode 100644 index 29f7746d4..000000000 --- a/apps/lightwrite/apps/backend/src/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { bootstrapApp } from '@manacore/shared-nestjs-setup'; -import { AppModule } from './app.module'; - -bootstrapApp(AppModule, { - defaultPort: 3010, - serviceName: 'LightWrite', - additionalCorsOrigins: ['http://localhost:5180'], -}); diff --git a/apps/lightwrite/apps/backend/src/marker/dto/marker.dto.ts b/apps/lightwrite/apps/backend/src/marker/dto/marker.dto.ts deleted file mode 100644 index 36f00162d..000000000 --- a/apps/lightwrite/apps/backend/src/marker/dto/marker.dto.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { - IsString, - IsNotEmpty, - IsUUID, - IsNumber, - IsOptional, - IsIn, - IsArray, - ValidateNested, - MaxLength, - Min, -} from 'class-validator'; -import { Type } from 'class-transformer'; - -const MARKER_TYPES = [ - 'verse', - 'hook', - 'bridge', - 'intro', - 'outro', - 'drop', - 'breakdown', - 'custom', -] as const; - -export class CreateMarkerDto { - @IsUUID() - @IsNotEmpty() - beatId!: string; - - @IsString() - @IsIn(MARKER_TYPES) - type!: string; - - @IsString() - @IsOptional() - @MaxLength(100) - label?: string; - - @IsNumber() - @Min(0) - startTime!: number; - - @IsNumber() - @IsOptional() - @Min(0) - endTime?: number; - - @IsString() - @IsOptional() - @MaxLength(7) - color?: string; -} - -export class UpdateMarkerDto { - @IsString() - @IsIn(MARKER_TYPES) - @IsOptional() - type?: string; - - @IsString() - @IsOptional() - @MaxLength(100) - label?: string; - - @IsNumber() - @IsOptional() - @Min(0) - startTime?: number; - - @IsNumber() - @IsOptional() - @Min(0) - endTime?: number; - - @IsString() - @IsOptional() - @MaxLength(7) - color?: string; - - @IsNumber() - @IsOptional() - sortOrder?: number; -} - -class MarkerItemDto { - @IsString() - @IsIn(MARKER_TYPES) - type!: string; - - @IsString() - @IsOptional() - @MaxLength(100) - label?: string; - - @IsNumber() - @Min(0) - startTime!: number; - - @IsNumber() - @IsOptional() - @Min(0) - endTime?: number; - - @IsString() - @IsOptional() - @MaxLength(7) - color?: string; -} - -export class BulkCreateMarkersDto { - @IsUUID() - @IsNotEmpty() - beatId!: string; - - @IsArray() - @ValidateNested({ each: true }) - @Type(() => MarkerItemDto) - markers!: MarkerItemDto[]; -} - -class MarkerUpdateItemDto { - @IsUUID() - @IsNotEmpty() - id!: string; - - @ValidateNested() - @Type(() => UpdateMarkerDto) - data!: UpdateMarkerDto; -} - -export class BulkUpdateMarkersDto { - @IsArray() - @ValidateNested({ each: true }) - @Type(() => MarkerUpdateItemDto) - updates!: MarkerUpdateItemDto[]; -} diff --git a/apps/lightwrite/apps/backend/src/marker/marker.controller.ts b/apps/lightwrite/apps/backend/src/marker/marker.controller.ts deleted file mode 100644 index c02d79607..000000000 --- a/apps/lightwrite/apps/backend/src/marker/marker.controller.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - UseGuards, - ParseUUIDPipe, -} from '@nestjs/common'; -import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; -import { MarkerService } from './marker.service'; -import { - CreateMarkerDto, - UpdateMarkerDto, - BulkCreateMarkersDto, - BulkUpdateMarkersDto, -} from './dto/marker.dto'; - -@Controller('markers') -@UseGuards(JwtAuthGuard) -export class MarkerController { - constructor(private readonly markerService: MarkerService) {} - - @Get('beat/:beatId') - async findByBeat( - @CurrentUser() user: CurrentUserData, - @Param('beatId', ParseUUIDPipe) beatId: string - ) { - await this.markerService.verifyBeatOwnership(beatId, user.userId); - const markerList = await this.markerService.findByBeatId(beatId); - return { markers: markerList }; - } - - @Post() - async create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateMarkerDto) { - await this.markerService.verifyBeatOwnership(dto.beatId, user.userId); - const marker = await this.markerService.create(dto); - return { marker }; - } - - @Post('bulk') - async bulkCreate(@CurrentUser() user: CurrentUserData, @Body() dto: BulkCreateMarkersDto) { - const markerList = await this.markerService.bulkCreate(dto.beatId, user.userId, dto.markers); - return { markers: markerList }; - } - - @Put('bulk') - async bulkUpdate(@CurrentUser() user: CurrentUserData, @Body() dto: BulkUpdateMarkersDto) { - const markerList = await this.markerService.bulkUpdate(user.userId, dto.updates); - return { markers: markerList }; - } - - @Put(':id') - async update( - @CurrentUser() user: CurrentUserData, - @Param('id', ParseUUIDPipe) id: string, - @Body() dto: UpdateMarkerDto - ) { - const marker = await this.markerService.update(id, user.userId, dto); - return { marker }; - } - - @Delete(':id') - async delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { - await this.markerService.delete(id, user.userId); - return { success: true }; - } - - @Delete('beat/:beatId') - async deleteAllForBeat( - @CurrentUser() user: CurrentUserData, - @Param('beatId', ParseUUIDPipe) beatId: string - ) { - await this.markerService.deleteAllForBeat(beatId, user.userId); - return { success: true }; - } -} diff --git a/apps/lightwrite/apps/backend/src/marker/marker.module.ts b/apps/lightwrite/apps/backend/src/marker/marker.module.ts deleted file mode 100644 index f44725e04..000000000 --- a/apps/lightwrite/apps/backend/src/marker/marker.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MarkerController } from './marker.controller'; -import { MarkerService } from './marker.service'; - -@Module({ - controllers: [MarkerController], - providers: [MarkerService], - exports: [MarkerService], -}) -export class MarkerModule {} diff --git a/apps/lightwrite/apps/backend/src/marker/marker.service.ts b/apps/lightwrite/apps/backend/src/marker/marker.service.ts deleted file mode 100644 index 4ecabfb62..000000000 --- a/apps/lightwrite/apps/backend/src/marker/marker.service.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Injectable, Inject, NotFoundException } from '@nestjs/common'; -import { eq, and, asc } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../db/database.module'; -import { Database } from '../db/connection'; -import { markers, beats, projects } from '../db/schema'; -import type { Marker, NewMarker } from '../db/schema'; - -@Injectable() -export class MarkerService { - constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} - - async verifyBeatOwnership(beatId: string, userId: string): Promise { - const [beat] = await this.db.select().from(beats).where(eq(beats.id, beatId)); - if (!beat) { - throw new NotFoundException('Beat not found'); - } - const [project] = await this.db - .select() - .from(projects) - .where(and(eq(projects.id, beat.projectId), eq(projects.userId, userId))); - if (!project) { - throw new NotFoundException('Project not found'); - } - } - - async findByBeatId(beatId: string): Promise { - return this.db - .select() - .from(markers) - .where(eq(markers.beatId, beatId)) - .orderBy(asc(markers.startTime)); - } - - async findById(id: string): Promise { - const [marker] = await this.db.select().from(markers).where(eq(markers.id, id)); - return marker || null; - } - - async findByIdOrThrow(id: string): Promise { - const marker = await this.findById(id); - if (!marker) { - throw new NotFoundException('Marker not found'); - } - return marker; - } - - async create(data: NewMarker): Promise { - const [marker] = await this.db.insert(markers).values(data).returning(); - return marker; - } - - async update( - id: string, - userId: string, - data: Partial> - ): Promise { - const marker = await this.findByIdOrThrow(id); - await this.verifyBeatOwnership(marker.beatId, userId); - - const [updatedMarker] = await this.db - .update(markers) - .set(data) - .where(eq(markers.id, id)) - .returning(); - return updatedMarker; - } - - async delete(id: string, userId: string): Promise { - const marker = await this.findByIdOrThrow(id); - await this.verifyBeatOwnership(marker.beatId, userId); - await this.db.delete(markers).where(eq(markers.id, id)); - } - - async deleteAllForBeat(beatId: string, userId: string): Promise { - await this.verifyBeatOwnership(beatId, userId); - await this.db.delete(markers).where(eq(markers.beatId, beatId)); - } - - async bulkCreate( - beatId: string, - userId: string, - items: Omit[] - ): Promise { - await this.verifyBeatOwnership(beatId, userId); - - if (items.length === 0) return []; - - const values = items.map((item) => ({ - ...item, - beatId, - })); - - return this.db.insert(markers).values(values).returning(); - } - - async bulkUpdate( - userId: string, - updates: Array<{ - id: string; - data: Partial>; - }> - ): Promise { - const results: Marker[] = []; - for (const update of updates) { - const marker = await this.update(update.id, userId, update.data); - results.push(marker); - } - return results; - } -} diff --git a/apps/lightwrite/apps/backend/src/project/dto/project.dto.ts b/apps/lightwrite/apps/backend/src/project/dto/project.dto.ts deleted file mode 100644 index 9bd0098fa..000000000 --- a/apps/lightwrite/apps/backend/src/project/dto/project.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IsString, IsNotEmpty, IsOptional, MaxLength } from 'class-validator'; - -export class CreateProjectDto { - @IsString() - @IsNotEmpty() - @MaxLength(255) - title!: string; - - @IsString() - @IsOptional() - description?: string; -} - -export class UpdateProjectDto { - @IsString() - @IsOptional() - @MaxLength(255) - title?: string; - - @IsString() - @IsOptional() - description?: string; -} diff --git a/apps/lightwrite/apps/backend/src/project/project.controller.ts b/apps/lightwrite/apps/backend/src/project/project.controller.ts deleted file mode 100644 index 0b36318ab..000000000 --- a/apps/lightwrite/apps/backend/src/project/project.controller.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - UseGuards, - ParseUUIDPipe, -} from '@nestjs/common'; -import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; -import { ProjectService } from './project.service'; -import { CreateProjectDto, UpdateProjectDto } from './dto/project.dto'; - -@Controller('projects') -@UseGuards(JwtAuthGuard) -export class ProjectController { - constructor(private readonly projectService: ProjectService) {} - - @Get() - async findAll(@CurrentUser() user: CurrentUserData) { - const projectsList = await this.projectService.findByUserId(user.userId); - return { projects: projectsList }; - } - - @Get(':id') - async findOne(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { - const project = await this.projectService.getProjectWithRelations(id, user.userId); - return { project }; - } - - @Post() - async create(@CurrentUser() user: CurrentUserData, @Body() dto: CreateProjectDto) { - const project = await this.projectService.create({ - userId: user.userId, - title: dto.title, - description: dto.description, - }); - return { project }; - } - - @Put(':id') - async update( - @CurrentUser() user: CurrentUserData, - @Param('id', ParseUUIDPipe) id: string, - @Body() dto: UpdateProjectDto - ) { - const project = await this.projectService.update(id, user.userId, dto); - return { project }; - } - - @Delete(':id') - async delete(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) { - await this.projectService.delete(id, user.userId); - return { success: true }; - } -} diff --git a/apps/lightwrite/apps/backend/src/project/project.module.ts b/apps/lightwrite/apps/backend/src/project/project.module.ts deleted file mode 100644 index 19ddb7bbb..000000000 --- a/apps/lightwrite/apps/backend/src/project/project.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ProjectController } from './project.controller'; -import { ProjectService } from './project.service'; - -@Module({ - controllers: [ProjectController], - providers: [ProjectService], - exports: [ProjectService], -}) -export class ProjectModule {} diff --git a/apps/lightwrite/apps/backend/src/project/project.service.ts b/apps/lightwrite/apps/backend/src/project/project.service.ts deleted file mode 100644 index 6734e3c13..000000000 --- a/apps/lightwrite/apps/backend/src/project/project.service.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Injectable, Inject, NotFoundException } from '@nestjs/common'; -import { eq, and, desc } from 'drizzle-orm'; -import { DATABASE_CONNECTION } from '../db/database.module'; -import { Database } from '../db/connection'; -import { projects, beats, lyrics } from '../db/schema'; -import type { Project, NewProject } from '../db/schema'; - -@Injectable() -export class ProjectService { - constructor(@Inject(DATABASE_CONNECTION) private db: Database) {} - - async findByUserId(userId: string): Promise { - return this.db - .select() - .from(projects) - .where(eq(projects.userId, userId)) - .orderBy(desc(projects.updatedAt)); - } - - async findById(id: string, userId: string): Promise { - const [project] = await this.db - .select() - .from(projects) - .where(and(eq(projects.id, id), eq(projects.userId, userId))); - return project || null; - } - - async findByIdOrThrow(id: string, userId: string): Promise { - const project = await this.findById(id, userId); - if (!project) { - throw new NotFoundException('Project not found'); - } - return project; - } - - async create(data: NewProject): Promise { - const [project] = await this.db.insert(projects).values(data).returning(); - return project; - } - - async update( - id: string, - userId: string, - data: Partial> - ): Promise { - await this.findByIdOrThrow(id, userId); - const [project] = await this.db - .update(projects) - .set({ ...data, updatedAt: new Date() }) - .where(and(eq(projects.id, id), eq(projects.userId, userId))) - .returning(); - return project; - } - - async delete(id: string, userId: string): Promise { - await this.findByIdOrThrow(id, userId); - await this.db.delete(projects).where(and(eq(projects.id, id), eq(projects.userId, userId))); - } - - async getProjectWithRelations(id: string, userId: string) { - const project = await this.findByIdOrThrow(id, userId); - - const [projectBeat] = await this.db.select().from(beats).where(eq(beats.projectId, id)); - - const [projectLyrics] = await this.db.select().from(lyrics).where(eq(lyrics.projectId, id)); - - return { - ...project, - beat: projectBeat || null, - lyrics: projectLyrics || null, - }; - } -} diff --git a/apps/lightwrite/apps/backend/src/stt/stt.module.ts b/apps/lightwrite/apps/backend/src/stt/stt.module.ts deleted file mode 100644 index acc7f6132..000000000 --- a/apps/lightwrite/apps/backend/src/stt/stt.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SttService } from './stt.service'; - -@Module({ - providers: [SttService], - exports: [SttService], -}) -export class SttModule {} diff --git a/apps/lightwrite/apps/backend/src/stt/stt.service.ts b/apps/lightwrite/apps/backend/src/stt/stt.service.ts deleted file mode 100644 index 2500e466f..000000000 --- a/apps/lightwrite/apps/backend/src/stt/stt.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -export interface TranscriptionResult { - text: string; - language: string | null; - model: string; - latencyMs: number | null; - durationSeconds: number | null; -} - -@Injectable() -export class SttService { - private readonly logger = new Logger(SttService.name); - private readonly sttUrl: string; - private readonly apiKey: string | undefined; - - constructor(private configService: ConfigService) { - this.sttUrl = this.configService.get('MANA_STT_URL') || 'http://localhost:3020'; - this.apiKey = this.configService.get('MANA_STT_API_KEY'); - } - - /** - * Check if mana-stt service is available - */ - async isAvailable(): Promise { - try { - const response = await fetch(`${this.sttUrl}/health`, { - method: 'GET', - signal: AbortSignal.timeout(5000), - }); - return response.ok; - } catch (error) { - this.logger.warn(`STT service not available: ${error}`); - return false; - } - } - - /** - * Transcribe audio buffer using Whisper via mana-stt - */ - async transcribe( - audioBuffer: Buffer, - filename: string, - language?: string - ): Promise { - this.logger.log(`Starting transcription for ${filename} (${audioBuffer.length} bytes)`); - - const formData = new FormData(); - // Convert Buffer to Uint8Array for Blob compatibility - const uint8Array = new Uint8Array(audioBuffer); - formData.append('file', new Blob([uint8Array]), filename); - - if (language) { - formData.append('language', language); - } - - const headers: Record = {}; - if (this.apiKey) { - headers['X-API-Key'] = this.apiKey; - } - - const response = await fetch(`${this.sttUrl}/transcribe`, { - method: 'POST', - body: formData, - headers, - signal: AbortSignal.timeout(120000), // 2 minute timeout - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`STT transcription failed: ${response.status} - ${error}`); - } - - const result = await response.json(); - - this.logger.log( - `Transcription complete: ${result.text?.length || 0} chars, language: ${result.language}, model: ${result.model}` - ); - - return { - text: result.text, - language: result.language || null, - model: result.model, - latencyMs: result.latency_ms || null, - durationSeconds: result.duration_seconds || null, - }; - } -} diff --git a/apps/lightwrite/apps/backend/tsconfig.json b/apps/lightwrite/apps/backend/tsconfig.json deleted file mode 100644 index 27971033a..000000000 --- a/apps/lightwrite/apps/backend/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2021", - "module": "commonjs", - "moduleResolution": "node", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "outDir": "./dist", - "baseUrl": "./", - "rootDir": "./src", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "sourceMap": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/apps/lightwrite/apps/landing/astro.config.mjs b/apps/lightwrite/apps/landing/astro.config.mjs deleted file mode 100644 index 0e44a7e46..000000000 --- a/apps/lightwrite/apps/landing/astro.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'astro/config'; -import sitemap from '@astrojs/sitemap'; - -export default defineConfig({ - site: 'https://lightwrite.app', - integrations: [sitemap()], -}); diff --git a/apps/lightwrite/apps/landing/package.json b/apps/lightwrite/apps/landing/package.json deleted file mode 100644 index 0002a49da..000000000 --- a/apps/lightwrite/apps/landing/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@lightwrite/landing", - "type": "module", - "version": "1.0.0", - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro check && astro build", - "preview": "astro preview", - "astro": "astro", - "type-check": "astro check" - }, - "dependencies": { - "@astrojs/check": "^0.9.4", - "@astrojs/sitemap": "^3.3.0", - "@manacore/shared-landing-ui": "workspace:*", - "astro": "^5.1.1", - "typescript": "^5.7.2" - } -} diff --git a/apps/lightwrite/apps/landing/src/layouts/Layout.astro b/apps/lightwrite/apps/landing/src/layouts/Layout.astro deleted file mode 100644 index 5d0a9bb04..000000000 --- a/apps/lightwrite/apps/landing/src/layouts/Layout.astro +++ /dev/null @@ -1,48 +0,0 @@ ---- -interface Props { - title: string; -} - -const { title } = Astro.props; ---- - - - - - - - - - - {title} - - - - - - diff --git a/apps/lightwrite/apps/landing/src/pages/index.astro b/apps/lightwrite/apps/landing/src/pages/index.astro deleted file mode 100644 index 6e8e7cce9..000000000 --- a/apps/lightwrite/apps/landing/src/pages/index.astro +++ /dev/null @@ -1,212 +0,0 @@ ---- -import Layout from '../layouts/Layout.astro'; ---- - - -
- -
-
-
-
-

- LightWrite -

-

- Create synchronized lyrics for your beats with precision timing and beautiful karaoke - exports. -

- -
-
-
- - -
-
-

- Everything You Need for Lyric Sync -

- -
-
-
- - - -
-

Waveform Editor

-

- Visualize your audio with an interactive waveform. Zoom, scroll, and navigate with - precision. -

-
- -
-
- - - -
-

BPM Detection

-

- Automatic tempo detection helps you sync lyrics to the beat with snap-to-beat - functionality. -

-
- -
-
- - - -
-

Part Markers

-

- Mark verses, hooks, bridges, and more. Organize your song structure visually. -

-
- -
-
- - - -
-

Live Sync Recording

-

- Record timestamps in real-time as the song plays. Just tap to sync each line. -

-
- -
-
- - - - -
-

Karaoke Preview

-

- Preview your synced lyrics in real-time with smooth karaoke-style highlighting. -

-
- -
-
- - - -
-

Multiple Exports

-

- Export to LRC, SRT, JSON, or generate karaoke videos for social media. -

-
-
-
-
- - -
-
-

Ready to Create?

-

- Start syncing your lyrics today. Free to use, no credit card required. -

- - Start Creating - -
-
- - -
-
-

© {new Date().getFullYear()} LightWrite. Part of the ManaCore ecosystem.

-
-
-
-
diff --git a/apps/lightwrite/apps/landing/tsconfig.json b/apps/lightwrite/apps/landing/tsconfig.json deleted file mode 100644 index adb44640f..000000000 --- a/apps/lightwrite/apps/landing/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "astro/tsconfigs/strict", - "compilerOptions": { - "strictNullChecks": true - } -} diff --git a/apps/lightwrite/apps/web/.env.example b/apps/lightwrite/apps/web/.env.example deleted file mode 100644 index f3a566531..000000000 --- a/apps/lightwrite/apps/web/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -# Auth -PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 -PUBLIC_MANA_CORE_AUTH_URL_CLIENT=http://localhost:3001 - -# Backend -PUBLIC_BACKEND_URL=http://localhost:3010 -PUBLIC_BACKEND_URL_CLIENT=http://localhost:3010 diff --git a/apps/lightwrite/apps/web/Dockerfile b/apps/lightwrite/apps/web/Dockerfile deleted file mode 100644 index 65b81455e..000000000 --- a/apps/lightwrite/apps/web/Dockerfile +++ /dev/null @@ -1,95 +0,0 @@ -# Build stage -FROM node:20-alpine AS builder - -# Build arguments for SvelteKit static env vars -ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001 -ARG PUBLIC_BACKEND_URL=http://lightwrite-backend:3010 - -# Set as environment variables for build -ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL -ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@9.15.0 --activate - -WORKDIR /app - -# Copy root workspace files -COPY pnpm-workspace.yaml ./ -COPY package.json ./ -COPY pnpm-lock.yaml ./ - -# Copy shared packages needed by lightwrite web -COPY packages/shared-api-client ./packages/shared-api-client -COPY packages/shared-auth ./packages/shared-auth -COPY packages/shared-auth-ui ./packages/shared-auth-ui -COPY packages/shared-branding ./packages/shared-branding -COPY packages/shared-config ./packages/shared-config -COPY packages/shared-i18n ./packages/shared-i18n -COPY packages/shared-icons ./packages/shared-icons -COPY packages/shared-pwa ./packages/shared-pwa -COPY packages/shared-stores ./packages/shared-stores -COPY packages/shared-tailwind ./packages/shared-tailwind -COPY packages/shared-theme ./packages/shared-theme -COPY packages/shared-theme-ui ./packages/shared-theme-ui -COPY packages/shared-types ./packages/shared-types -COPY packages/shared-ui ./packages/shared-ui -COPY packages/shared-utils ./packages/shared-utils -COPY packages/shared-vite-config ./packages/shared-vite-config - -# Copy lightwrite shared package -COPY apps/lightwrite/packages ./apps/lightwrite/packages - -# Copy lightwrite web -COPY apps/lightwrite/apps/web ./apps/lightwrite/apps/web - -# Install dependencies -RUN pnpm install --frozen-lockfile - -# Build shared packages that need building -WORKDIR /app/packages/shared-vite-config -RUN pnpm build - -WORKDIR /app/packages/shared-auth -RUN pnpm build || true - -# Build the web app -WORKDIR /app/apps/lightwrite/apps/web -RUN pnpm exec svelte-kit sync -RUN pnpm build - -# Production stage -FROM node:20-alpine AS production - -# Keep same directory structure as builder so pnpm symlinks resolve correctly -WORKDIR /app/apps/lightwrite/apps/web - -# Copy the pnpm store that symlinks point to -COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm - -# Copy the app's node_modules -COPY --from=builder /app/apps/lightwrite/apps/web/node_modules ./node_modules - -# Copy built application -COPY --from=builder /app/apps/lightwrite/apps/web/build ./build -COPY --from=builder /app/apps/lightwrite/apps/web/package.json ./ - -# Copy entrypoint script -COPY apps/lightwrite/apps/web/docker-entrypoint.sh /docker-entrypoint.sh -RUN chmod +x /docker-entrypoint.sh - -# Expose port -EXPOSE 5180 - -# Set environment variables -ENV NODE_ENV=production -ENV PORT=5180 -ENV HOST=0.0.0.0 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:5180/health || exit 1 - -# Run the app -ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["node", "build"] diff --git a/apps/lightwrite/apps/web/docker-entrypoint.sh b/apps/lightwrite/apps/web/docker-entrypoint.sh deleted file mode 100644 index 9e483bd5e..000000000 --- a/apps/lightwrite/apps/web/docker-entrypoint.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -set -e - -# This script injects runtime environment variables into the SvelteKit build -# SvelteKit builds env vars at build time, but we need to inject them at runtime -# for Docker deployments where the container runs in different environments - -echo "Starting LightWrite Web with runtime configuration..." -echo "PUBLIC_MANA_CORE_AUTH_URL_CLIENT: ${PUBLIC_MANA_CORE_AUTH_URL_CLIENT:-not set}" -echo "PUBLIC_BACKEND_URL_CLIENT: ${PUBLIC_BACKEND_URL_CLIENT:-not set}" - -# Execute the main command -exec "$@" diff --git a/apps/lightwrite/apps/web/package.json b/apps/lightwrite/apps/web/package.json deleted file mode 100644 index 08b336fe6..000000000 --- a/apps/lightwrite/apps/web/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "@lightwrite/web", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "eslint .", - "format": "prettier --write .", - "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" - }, - "devDependencies": { - "@manacore/shared-pwa": "workspace:*", - "@manacore/shared-vite-config": "workspace:*", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.47.1", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@tailwindcss/vite": "^4.1.7", - "@types/node": "^20.0.0", - "@vite-pwa/sveltekit": "^1.1.0", - "prettier": "^3.1.1", - "prettier-plugin-svelte": "^3.1.2", - "svelte": "^5.41.0", - "svelte-check": "^4.3.3", - "tailwindcss": "^4.1.7", - "tslib": "^2.4.1", - "typescript": "^5.9.3", - "vite": "^6.0.0" - }, - "dependencies": { - "@lightwrite/shared": "workspace:*", - "@manacore/shared-api-client": "workspace:*", - "@manacore/shared-auth": "workspace:*", - "@manacore/shared-auth-ui": "workspace:*", - "@manacore/shared-branding": "workspace:*", - "@manacore/shared-i18n": "workspace:*", - "@manacore/shared-icons": "workspace:*", - "@manacore/shared-stores": "workspace:*", - "@manacore/shared-tailwind": "workspace:*", - "@manacore/shared-theme": "workspace:*", - "@manacore/shared-theme-ui": "workspace:*", - "@manacore/shared-ui": "workspace:*", - "wavesurfer.js": "^7.8.0" - }, - "type": "module" -} diff --git a/apps/lightwrite/apps/web/src/app.css b/apps/lightwrite/apps/web/src/app.css deleted file mode 100644 index 9755be955..000000000 --- a/apps/lightwrite/apps/web/src/app.css +++ /dev/null @@ -1,168 +0,0 @@ -@import "tailwindcss"; -@import "@manacore/shared-tailwind/themes.css"; - -/* Scan shared packages for Tailwind classes */ -@source "../../../../packages/shared-ui/src"; -@source "../../../../packages/shared-auth-ui/src"; -@source "../../../../packages/shared-branding/src"; -@source "../../../../packages/shared-theme-ui/src"; -@source "../../../../packages/shared-theme-ui/src/components"; -@source "../../../../packages/shared-theme-ui/src/pages"; -@source "../../../../packages/shared-stores/src"; - -/* Waveform styles */ -.waveform-container { - position: relative; - width: 100%; - height: 128px; - background: var(--color-surface); - border-radius: 8px; - overflow: hidden; -} - -/* Marker colors */ -.marker-verse { background-color: #3B82F6; } -.marker-hook { background-color: #EF4444; } -.marker-bridge { background-color: #8B5CF6; } -.marker-intro { background-color: #22C55E; } -.marker-outro { background-color: #F97316; } -.marker-drop { background-color: #EC4899; } -.marker-breakdown { background-color: #14B8A6; } -.marker-custom { background-color: #6B7280; } - -/* Lyrics editor styles */ -.lyrics-editor { - font-family: 'JetBrains Mono', 'Fira Code', monospace; - line-height: 1.8; -} - -.lyrics-line { - padding: 4px 8px; - border-radius: 4px; - transition: background-color 0.2s; -} - -.lyrics-line:hover { - background-color: var(--color-surface); -} - -.lyrics-line.active { - background-color: var(--color-primary); - color: white; -} - -.lyrics-line.synced { - border-left: 3px solid var(--color-primary); -} - -/* Karaoke animation */ -@keyframes karaoke-highlight { - 0% { background-position: 0% 50%; } - 100% { background-position: 100% 50%; } -} - -.karaoke-word { - transition: color 0.1s, transform 0.1s; -} - -.karaoke-word.active { - color: var(--color-primary); - transform: scale(1.05); -} - -.karaoke-word.past { - color: var(--color-foreground); -} - -.karaoke-word.future { - color: var(--color-foreground-secondary); -} - -/* Timeline styles */ -.timeline-ruler { - height: 24px; - background: var(--color-surface); - position: relative; -} - -.timeline-marker { - position: absolute; - top: 0; - height: 100%; - cursor: pointer; - opacity: 0.8; - transition: opacity 0.2s; -} - -.timeline-marker:hover { - opacity: 1; -} - -/* Playhead */ -.playhead { - position: absolute; - top: 0; - bottom: 0; - width: 2px; - background: var(--color-primary); - z-index: 10; - pointer-events: none; -} - -.playhead::after { - content: ''; - position: absolute; - top: 0; - left: -4px; - width: 10px; - height: 10px; - background: var(--color-primary); - border-radius: 50%; -} - -/* Mobile responsive waveform */ -@media (max-width: 767px) { - .waveform-container { - height: 80px; - } - - .timeline-ruler { - height: 20px; - } - - .timeline-marker span { - font-size: 8px; - } -} - -/* Touch-friendly range inputs */ -input[type="range"] { - -webkit-appearance: none; - appearance: none; - background: transparent; - cursor: pointer; -} - -input[type="range"]::-webkit-slider-runnable-track { - background: hsl(var(--color-surface-hover)); - border-radius: 4px; - height: 8px; -} - -input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - background: hsl(var(--color-primary)); - height: 16px; - width: 16px; - border-radius: 50%; - margin-top: -4px; -} - -@media (max-width: 767px) { - input[type="range"]::-webkit-slider-thumb { - height: 20px; - width: 20px; - margin-top: -6px; - } -} diff --git a/apps/lightwrite/apps/web/src/app.html b/apps/lightwrite/apps/web/src/app.html deleted file mode 100644 index 650e7aa11..000000000 --- a/apps/lightwrite/apps/web/src/app.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - LightWrite - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/apps/lightwrite/apps/web/src/hooks.server.ts b/apps/lightwrite/apps/web/src/hooks.server.ts deleted file mode 100644 index 6d7a5089d..000000000 --- a/apps/lightwrite/apps/web/src/hooks.server.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Server Hooks for SvelteKit - * - Injects runtime environment variables for client-side use - * - Auth is handled client-side via Mana Core Auth - */ - -import type { Handle } from '@sveltejs/kit'; - -// Get client-side URLs from environment (Docker runtime) -const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = - process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; -const PUBLIC_BACKEND_URL_CLIENT = - process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; - -export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { - transformPageChunk: ({ html }) => { - // Inject runtime environment variables into the HTML - // These will be available on window.__PUBLIC_*__ for client-side code - const envScript = ``; - return html.replace('', `${envScript}`); - }, - }); -}; diff --git a/apps/lightwrite/apps/web/src/lib/components/BeatLibrary.svelte b/apps/lightwrite/apps/web/src/lib/components/BeatLibrary.svelte deleted file mode 100644 index 733b4e40d..000000000 --- a/apps/lightwrite/apps/web/src/lib/components/BeatLibrary.svelte +++ /dev/null @@ -1,225 +0,0 @@ - - -
- {#if isLoading} -
-
-
- {:else if error} -
-

{error}

- -
- {:else if beats.length === 0} -
-
- - - -
-

No beats available in the library yet.

-

Upload your own beat instead.

-
- {:else} -
- {#each beats as beat} -
- - - - -
-

{beat.title}

-
- {#if beat.artist} - {beat.artist} - {/if} - {#if beat.genre} - - {beat.genre} - - {/if} - {#if beat.bpm} - {beat.bpm} BPM - {/if} - {formatDuration(beat.duration)} -
- {#if beat.tags && beat.tags.length > 0} -
- {#each beat.tags.slice(0, 3) as tag} - - {tag} - - {/each} - {#if beat.tags.length > 3} - - +{beat.tags.length - 3} more - - {/if} -
- {/if} -
- - - -
- {/each} -
- {/if} -
diff --git a/apps/lightwrite/apps/web/src/lib/components/BeatUploader.svelte b/apps/lightwrite/apps/web/src/lib/components/BeatUploader.svelte deleted file mode 100644 index 5d81a3161..000000000 --- a/apps/lightwrite/apps/web/src/lib/components/BeatUploader.svelte +++ /dev/null @@ -1,298 +0,0 @@ - - -
- -
- - -
- - - {#if activeTab === 'upload'} -
- - - {#if isUploading} -
-
- {#if isDetectingBpm} - - - - {:else} -
- {/if} -
-

- {isDetectingBpm ? 'Detecting BPM...' : 'Uploading...'} -

-
-
-
-
- {:else} - - {/if} - - {#if errorMessage} -

{errorMessage}

- {/if} -
- - - {#if isTranscribing} -
-
-
-

Transcribing lyrics...

-

- Analyzing audio to extract lyrics automatically -

-
-
- {:else if transcriptionError} -
- - - -
-

Transcription failed

-

{transcriptionError}

-
- -
- {/if} - {:else} - - {/if} -
diff --git a/apps/lightwrite/apps/web/src/lib/components/KaraokePreview.svelte b/apps/lightwrite/apps/web/src/lib/components/KaraokePreview.svelte deleted file mode 100644 index 92622df2c..000000000 --- a/apps/lightwrite/apps/web/src/lib/components/KaraokePreview.svelte +++ /dev/null @@ -1,109 +0,0 @@ - - -
- {#each visibleLines as line} -
- {#if line.relativeIndex === 0} - -
- - {line.text} - - - - {line.text} - -
- {:else if line.relativeIndex < 0} - - {line.text} - {:else} - - {line.text} - {/if} -
- {/each} - - {#if projectStore.currentLines.length === 0} -

No synced lyrics to preview.

- {/if} -
diff --git a/apps/lightwrite/apps/web/src/lib/components/LyricsEditor.svelte b/apps/lightwrite/apps/web/src/lib/components/LyricsEditor.svelte deleted file mode 100644 index aa768a7d8..000000000 --- a/apps/lightwrite/apps/web/src/lib/components/LyricsEditor.svelte +++ /dev/null @@ -1,214 +0,0 @@ - - -
- -
-

Lyrics

-
- - -
-
- - -
- - -
- - -
- {#if editorStore.mode === 'edit'} - - - {:else} - -
- {#each projectStore.currentLines as line, index} -
handleLineClick(index)} - onkeydown={(e) => e.key === 'Enter' && handleLineClick(index)} - class="lyrics-line w-full text-left flex items-center gap-3 cursor-pointer {activeLineIndex === - index - ? 'active' - : ''} {line.startTime !== null ? 'synced' : ''} {editorStore.selectedLineIndex === - index - ? 'ring-2 ring-primary' - : ''}" - > - - - {line.startTime !== null && line.startTime !== undefined - ? formatTimeWithMs(line.startTime) - : '--:--'} - - - - {line.text} - - - {#if editorStore.isRecordingTimestamps} - - {:else if line.startTime === null || line.startTime === undefined} - - {/if} -
- {/each} - - {#if projectStore.currentLines.length === 0} -

- No lyrics yet. Switch to Edit mode to add lyrics. -

- {/if} -
- {/if} -
-
diff --git a/apps/lightwrite/apps/web/src/lib/components/MarkerTimeline.svelte b/apps/lightwrite/apps/web/src/lib/components/MarkerTimeline.svelte deleted file mode 100644 index 797cbceb2..000000000 --- a/apps/lightwrite/apps/web/src/lib/components/MarkerTimeline.svelte +++ /dev/null @@ -1,204 +0,0 @@ - - -
- -
-
- Markers - - - - - -
- - -
- {#each markerTypes.slice(0, 5) as type} -
- - {type} -
- {/each} -
-
- - -
- - {#each projectStore.currentMarkers as marker} - - {/each} - - - {#if audioStore.duration > 0} -
- {/if} -
- - - {#if editorStore.selectedMarkerId} - {@const selectedMarker = projectStore.currentMarkers.find( - (m) => m.id === editorStore.selectedMarkerId - )} - {#if selectedMarker} -
-
- - {selectedMarker.type} - {#if selectedMarker.label} - - {selectedMarker.label} - {/if} -
-
- - {selectedMarker.startTime.toFixed(2)}s - {( - selectedMarker.endTime || selectedMarker.startTime - ).toFixed(2)}s - - - -
-
- {/if} - {/if} -
diff --git a/apps/lightwrite/apps/web/src/lib/components/PlaybackControls.svelte b/apps/lightwrite/apps/web/src/lib/components/PlaybackControls.svelte deleted file mode 100644 index 17f87cd21..000000000 --- a/apps/lightwrite/apps/web/src/lib/components/PlaybackControls.svelte +++ /dev/null @@ -1,204 +0,0 @@ - - -{#if compact} - -
- - - - -
- {formatTime(audioStore.currentTime)} -
- - -
- -
- - -
- {formatTime(audioStore.duration)} -
- - - {#if audioStore.bpm} -
- {audioStore.bpm} -
- {/if} -
-{:else} - -
- -
- {formatTime(audioStore.currentTime)} / {formatTime(audioStore.duration)} -
- - -
- - - - - -
- - -
- -
- - -
- - - {Math.round(editorStore.zoom * 100)}% - - -
- - - {#if audioStore.bpm} -
- {audioStore.bpm} BPM -
- {/if} -
-{/if} diff --git a/apps/lightwrite/apps/web/src/lib/components/WaveformEditor.svelte b/apps/lightwrite/apps/web/src/lib/components/WaveformEditor.svelte deleted file mode 100644 index 317919b21..000000000 --- a/apps/lightwrite/apps/web/src/lib/components/WaveformEditor.svelte +++ /dev/null @@ -1,235 +0,0 @@ - - -
- {#if !audioStore.isLoaded && audioUrl} -
-
-
- {/if} -
diff --git a/apps/lightwrite/apps/web/src/lib/stores/audio.svelte.ts b/apps/lightwrite/apps/web/src/lib/stores/audio.svelte.ts deleted file mode 100644 index cd830bbf9..000000000 --- a/apps/lightwrite/apps/web/src/lib/stores/audio.svelte.ts +++ /dev/null @@ -1,81 +0,0 @@ -interface AudioState { - isPlaying: boolean; - currentTime: number; - duration: number; - isLoaded: boolean; - bpm: number | null; - audioUrl: string | null; -} - -function createAudioStore() { - let state = $state({ - isPlaying: false, - currentTime: 0, - duration: 0, - isLoaded: false, - bpm: null, - audioUrl: null, - }); - - return { - get isPlaying() { - return state.isPlaying; - }, - get currentTime() { - return state.currentTime; - }, - get duration() { - return state.duration; - }, - get isLoaded() { - return state.isLoaded; - }, - get bpm() { - return state.bpm; - }, - get audioUrl() { - return state.audioUrl; - }, - - setPlaying(playing: boolean) { - state.isPlaying = playing; - }, - - setCurrentTime(time: number) { - state.currentTime = time; - }, - - setDuration(duration: number) { - state.duration = duration; - }, - - setLoaded(loaded: boolean) { - state.isLoaded = loaded; - }, - - setBpm(bpm: number | null) { - state.bpm = bpm; - }, - - setAudioUrl(url: string | null) { - state.audioUrl = url; - if (!url) { - state.isLoaded = false; - state.duration = 0; - state.currentTime = 0; - state.isPlaying = false; - } - }, - - reset() { - state.isPlaying = false; - state.currentTime = 0; - state.duration = 0; - state.isLoaded = false; - state.bpm = null; - state.audioUrl = null; - }, - }; -} - -export const audioStore = createAudioStore(); diff --git a/apps/lightwrite/apps/web/src/lib/stores/auth.svelte.ts b/apps/lightwrite/apps/web/src/lib/stores/auth.svelte.ts deleted file mode 100644 index cdbd557c6..000000000 --- a/apps/lightwrite/apps/web/src/lib/stores/auth.svelte.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Auth Store - Manages authentication state using Svelte 5 runes - * Uses Mana Core Auth - */ - -import { browser } from '$app/environment'; -import { initializeWebAuth } from '@manacore/shared-auth'; -import type { UserData } from '@manacore/shared-auth'; - -// Get auth URL dynamically at runtime - fallback for SSR and client -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - return injectedUrl || 'http://localhost:3001'; - } - return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; -} - -// Get backend URL dynamically at runtime -function getBackendUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) - .__PUBLIC_BACKEND_URL__; - return injectedUrl || 'http://localhost:3010'; - } - return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3010'; -} - -// Lazy initialization to avoid SSR issues with localStorage -let _authService: ReturnType['authService'] | null = null; -let _tokenManager: ReturnType['tokenManager'] | null = null; - -function getAuthService() { - if (!browser) return null; - if (!_authService) { - const auth = initializeWebAuth({ - baseUrl: getAuthUrl(), - backendUrl: getBackendUrl(), - }); - _authService = auth.authService; - _tokenManager = auth.tokenManager; - } - return _authService; -} - -function getTokenManager() { - if (!browser) return null; - getAuthService(); - return _tokenManager; -} - -// State -let user = $state(null); -let loading = $state(true); -let initialized = $state(false); - -export const authStore = { - get user() { - return user; - }, - get isLoading() { - return loading; - }, - get isAuthenticated() { - return !!user; - }, - get initialized() { - return initialized; - }, - - async initialize() { - if (initialized) return; - - const authService = getAuthService(); - if (!authService) { - initialized = true; - loading = false; - return; - } - - loading = true; - try { - let authenticated = await authService.isAuthenticated(); - - if (!authenticated) { - const ssoResult = await authService.trySSO(); - if (ssoResult.success) { - authenticated = true; - } - } - - if (authenticated) { - const userData = await authService.getUserFromToken(); - user = userData; - } - initialized = true; - } catch (error) { - console.error('Failed to initialize auth:', error); - user = null; - } finally { - loading = false; - } - }, - - async signIn(email: string, password: string) { - const authService = getAuthService(); - if (!authService) { - return { success: false, error: 'Auth not available on server' }; - } - - try { - const result = await authService.signIn(email, password); - - if (!result.success) { - return { success: false, error: result.error || 'Login failed' }; - } - - const userData = await authService.getUserFromToken(); - user = userData; - - return { success: true }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { success: false, error: errorMessage }; - } - }, - - async signUp(email: string, password: string) { - const authService = getAuthService(); - if (!authService) { - return { success: false, error: 'Auth not available on server', needsVerification: false }; - } - - try { - const sourceAppUrl = browser ? window.location.origin : undefined; - const result = await authService.signUp(email, password, sourceAppUrl); - - if (!result.success) { - return { success: false, error: result.error || 'Signup failed', needsVerification: false }; - } - - if (result.needsVerification) { - return { success: true, needsVerification: true }; - } - - const signInResult = await this.signIn(email, password); - return { ...signInResult, needsVerification: false }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { success: false, error: errorMessage, needsVerification: false }; - } - }, - - async signOut() { - const authService = getAuthService(); - if (!authService) { - user = null; - return; - } - - try { - await authService.signOut(); - user = null; - } catch (error) { - console.error('Sign out error:', error); - user = null; - } - }, - - async resendVerificationEmail(email: string) { - const authService = getAuthService(); - if (!authService) { - return { success: false, error: 'Auth not available on server' }; - } - - try { - const sourceAppUrl = browser ? window.location.origin : undefined; - const result = await authService.resendVerificationEmail(email, sourceAppUrl); - - if (!result.success) { - return { success: false, error: result.error || 'Failed to resend verification email' }; - } - - return { success: true }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return { success: false, error: errorMessage }; - } - }, - - async getValidToken(): Promise { - const tokenManager = getTokenManager(); - if (!tokenManager) { - return null; - } - return await tokenManager.getValidToken(); - }, - - getAuthHeaders(): Record { - const authService = getAuthService(); - if (!authService) return {}; - - // Get token synchronously from storage if available - const token = - typeof localStorage !== 'undefined' ? localStorage.getItem('manacore_access_token') : null; - if (token) { - return { Authorization: `Bearer ${token}` }; - } - return {}; - }, -}; diff --git a/apps/lightwrite/apps/web/src/lib/stores/editor.svelte.ts b/apps/lightwrite/apps/web/src/lib/stores/editor.svelte.ts deleted file mode 100644 index cffad38f5..000000000 --- a/apps/lightwrite/apps/web/src/lib/stores/editor.svelte.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { MarkerType } from '@lightwrite/shared'; - -type EditorMode = 'edit' | 'preview'; -type SyncMode = 'line' | 'word'; - -interface EditorState { - mode: EditorMode; - syncMode: SyncMode; - selectedMarkerId: string | null; - selectedLineIndex: number | null; - isRecordingTimestamps: boolean; - zoom: number; - scrollPosition: number; - markerTypeToCreate: MarkerType; - snapToBeat: boolean; - showWaveform: boolean; - showMarkers: boolean; - showLyrics: boolean; - loopRegionId: string | null; - isLooping: boolean; -} - -function createEditorStore() { - let state = $state({ - mode: 'edit', - syncMode: 'line', - selectedMarkerId: null, - selectedLineIndex: null, - isRecordingTimestamps: false, - zoom: 1, - scrollPosition: 0, - markerTypeToCreate: 'verse', - snapToBeat: true, - showWaveform: true, - showMarkers: true, - showLyrics: true, - loopRegionId: null, - isLooping: false, - }); - - return { - get mode() { - return state.mode; - }, - get syncMode() { - return state.syncMode; - }, - get selectedMarkerId() { - return state.selectedMarkerId; - }, - get selectedLineIndex() { - return state.selectedLineIndex; - }, - get isRecordingTimestamps() { - return state.isRecordingTimestamps; - }, - get zoom() { - return state.zoom; - }, - get scrollPosition() { - return state.scrollPosition; - }, - get markerTypeToCreate() { - return state.markerTypeToCreate; - }, - get snapToBeat() { - return state.snapToBeat; - }, - get showWaveform() { - return state.showWaveform; - }, - get showMarkers() { - return state.showMarkers; - }, - get showLyrics() { - return state.showLyrics; - }, - get loopRegionId() { - return state.loopRegionId; - }, - get isLooping() { - return state.isLooping; - }, - - setMode(mode: EditorMode) { - state.mode = mode; - }, - - setSyncMode(syncMode: SyncMode) { - state.syncMode = syncMode; - }, - - selectMarker(markerId: string | null) { - state.selectedMarkerId = markerId; - }, - - selectLine(lineIndex: number | null) { - state.selectedLineIndex = lineIndex; - }, - - setRecordingTimestamps(recording: boolean) { - state.isRecordingTimestamps = recording; - }, - - setZoom(zoom: number) { - state.zoom = Math.max(0.5, Math.min(10, zoom)); - }, - - zoomIn() { - state.zoom = Math.min(10, state.zoom * 1.25); - }, - - zoomOut() { - state.zoom = Math.max(0.5, state.zoom / 1.25); - }, - - setScrollPosition(position: number) { - state.scrollPosition = position; - }, - - setMarkerTypeToCreate(type: MarkerType) { - state.markerTypeToCreate = type; - }, - - toggleSnapToBeat() { - state.snapToBeat = !state.snapToBeat; - }, - - toggleWaveform() { - state.showWaveform = !state.showWaveform; - }, - - toggleMarkers() { - state.showMarkers = !state.showMarkers; - }, - - toggleLyrics() { - state.showLyrics = !state.showLyrics; - }, - - setLoopRegion(markerId: string | null) { - state.loopRegionId = markerId; - state.isLooping = markerId !== null; - }, - - reset() { - state.mode = 'edit'; - state.syncMode = 'line'; - state.selectedMarkerId = null; - state.selectedLineIndex = null; - state.isRecordingTimestamps = false; - state.zoom = 1; - state.scrollPosition = 0; - state.loopRegionId = null; - state.isLooping = false; - }, - }; -} - -export const editorStore = createEditorStore(); diff --git a/apps/lightwrite/apps/web/src/lib/stores/project.svelte.ts b/apps/lightwrite/apps/web/src/lib/stores/project.svelte.ts deleted file mode 100644 index 1fa82d8c0..000000000 --- a/apps/lightwrite/apps/web/src/lib/stores/project.svelte.ts +++ /dev/null @@ -1,286 +0,0 @@ -import type { Project, Beat, Lyrics, LyricLine, Marker } from '@lightwrite/shared'; -import { authStore } from './auth.svelte'; - -interface ProjectState { - projects: Project[]; - currentProject: Project | null; - currentBeat: Beat | null; - currentLyrics: Lyrics | null; - currentLines: LyricLine[]; - currentMarkers: Marker[]; - isLoading: boolean; - error: string | null; -} - -function getBackendUrl(): string { - let baseUrl = 'http://localhost:3010'; - if (typeof window !== 'undefined') { - baseUrl = - (window as unknown as { __PUBLIC_BACKEND_URL__: string }).__PUBLIC_BACKEND_URL__ || - 'http://localhost:3010'; - } - // Ensure API prefix is included - return baseUrl.endsWith('/api/v1') ? baseUrl : `${baseUrl}/api/v1`; -} - -function createProjectStore() { - let state = $state({ - projects: [], - currentProject: null, - currentBeat: null, - currentLyrics: null, - currentLines: [], - currentMarkers: [], - isLoading: false, - error: null, - }); - - async function fetchApi(path: string, options: RequestInit = {}): Promise { - const response = await fetch(`${getBackendUrl()}${path}`, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), - ...options.headers, - }, - }); - - if (!response.ok) { - const error = await response.json().catch(() => ({ message: 'Request failed' })); - throw new Error(error.message || 'Request failed'); - } - - return response.json(); - } - - return { - get projects() { - return state.projects; - }, - get currentProject() { - return state.currentProject; - }, - get currentBeat() { - return state.currentBeat; - }, - get currentLyrics() { - return state.currentLyrics; - }, - get currentLines() { - return state.currentLines; - }, - get currentMarkers() { - return state.currentMarkers; - }, - get isLoading() { - return state.isLoading; - }, - get error() { - return state.error; - }, - - async loadProjects() { - state.isLoading = true; - state.error = null; - try { - const data = await fetchApi<{ projects: Project[] }>('/projects'); - state.projects = data.projects; - } catch (e) { - state.error = e instanceof Error ? e.message : 'Failed to load projects'; - } - state.isLoading = false; - }, - - async loadProject(id: string) { - state.isLoading = true; - state.error = null; - try { - const data = await fetchApi<{ - project: Project & { beat: Beat | null; lyrics: Lyrics | null }; - }>(`/projects/${id}`); - state.currentProject = data.project; - state.currentBeat = data.project.beat; - state.currentLyrics = data.project.lyrics; - - // Load markers if beat exists - if (data.project.beat) { - const markersData = await fetchApi<{ markers: Marker[] }>( - `/markers/beat/${data.project.beat.id}` - ); - state.currentMarkers = markersData.markers; - } - - // Load lyrics lines if lyrics exists - if (data.project.lyrics) { - const lyricsData = await fetchApi<{ lyrics: { lines: LyricLine[] } | null }>( - `/lyrics/project/${id}` - ); - state.currentLines = lyricsData.lyrics?.lines || []; - } - } catch (e) { - state.error = e instanceof Error ? e.message : 'Failed to load project'; - } - state.isLoading = false; - }, - - async createProject(title: string, description?: string) { - const data = await fetchApi<{ project: Project }>('/projects', { - method: 'POST', - body: JSON.stringify({ title, description }), - }); - state.projects = [data.project, ...state.projects]; - return data.project; - }, - - async updateProject(id: string, updates: { title?: string; description?: string }) { - const data = await fetchApi<{ project: Project }>(`/projects/${id}`, { - method: 'PUT', - body: JSON.stringify(updates), - }); - state.projects = state.projects.map((p) => (p.id === id ? data.project : p)); - if (state.currentProject?.id === id) { - state.currentProject = data.project; - } - return data.project; - }, - - async deleteProject(id: string) { - await fetchApi(`/projects/${id}`, { method: 'DELETE' }); - state.projects = state.projects.filter((p) => p.id !== id); - if (state.currentProject?.id === id) { - state.currentProject = null; - state.currentBeat = null; - state.currentLyrics = null; - state.currentLines = []; - state.currentMarkers = []; - } - }, - - async uploadBeat(projectId: string, file: File) { - // Get upload URL - const uploadData = await fetchApi<{ beat: Beat; uploadUrl: string }>('/beats/upload', { - method: 'POST', - body: JSON.stringify({ projectId, filename: file.name }), - }); - - // Upload file to S3 - await fetch(uploadData.uploadUrl, { - method: 'PUT', - body: file, - headers: { 'Content-Type': file.type }, - }); - - state.currentBeat = uploadData.beat; - return uploadData.beat; - }, - - async updateBeatMetadata( - beatId: string, - metadata: { duration?: number; bpm?: number; bpmConfidence?: number; waveformData?: unknown } - ) { - const data = await fetchApi<{ beat: Beat }>(`/beats/${beatId}/metadata`, { - method: 'PUT', - body: JSON.stringify(metadata), - }); - state.currentBeat = data.beat; - return data.beat; - }, - - async getBeatDownloadUrl(beatId: string): Promise { - const data = await fetchApi<{ url: string }>(`/beats/${beatId}/download-url`); - return data.url; - }, - - async deleteBeat(beatId: string) { - await fetchApi(`/beats/${beatId}`, { method: 'DELETE' }); - state.currentBeat = null; - state.currentMarkers = []; - }, - - async checkSttAvailable(): Promise { - try { - const data = await fetchApi<{ available: boolean }>('/beats/stt/available'); - return data.available; - } catch { - return false; - } - }, - - async transcribeBeat(beatId: string): Promise<{ beat: Beat; lyrics: string | null }> { - const data = await fetchApi<{ beat: Beat; lyrics: string | null }>( - `/beats/${beatId}/transcribe`, - { method: 'POST' } - ); - state.currentBeat = data.beat; - if (data.lyrics) { - state.currentLyrics = { ...state.currentLyrics!, content: data.lyrics }; - } - return data; - }, - - async updateLyrics(projectId: string, content: string) { - const data = await fetchApi<{ lyrics: Lyrics }>(`/lyrics/project/${projectId}`, { - method: 'POST', - body: JSON.stringify({ content }), - }); - state.currentLyrics = data.lyrics; - return data.lyrics; - }, - - async syncLines( - lyricsId: string, - lines: Array<{ lineNumber: number; text: string; startTime?: number; endTime?: number }> - ) { - const data = await fetchApi<{ lines: LyricLine[] }>(`/lyrics/${lyricsId}/sync`, { - method: 'POST', - body: JSON.stringify({ lines }), - }); - state.currentLines = data.lines; - return data.lines; - }, - - async updateLineTimestamp(lineId: string, startTime?: number, endTime?: number) { - const data = await fetchApi<{ line: LyricLine }>(`/lyrics/line/${lineId}/timestamp`, { - method: 'PUT', - body: JSON.stringify({ startTime, endTime }), - }); - state.currentLines = state.currentLines.map((l) => (l.id === lineId ? data.line : l)); - return data.line; - }, - - async createMarker(beatId: string, marker: Omit) { - const data = await fetchApi<{ marker: Marker }>('/markers', { - method: 'POST', - body: JSON.stringify({ beatId, ...marker }), - }); - state.currentMarkers = [...state.currentMarkers, data.marker].sort( - (a, b) => a.startTime - b.startTime - ); - return data.marker; - }, - - async updateMarker(markerId: string, updates: Partial) { - const data = await fetchApi<{ marker: Marker }>(`/markers/${markerId}`, { - method: 'PUT', - body: JSON.stringify(updates), - }); - state.currentMarkers = state.currentMarkers.map((m) => (m.id === markerId ? data.marker : m)); - return data.marker; - }, - - async deleteMarker(markerId: string) { - await fetchApi(`/markers/${markerId}`, { method: 'DELETE' }); - state.currentMarkers = state.currentMarkers.filter((m) => m.id !== markerId); - }, - - clearCurrent() { - state.currentProject = null; - state.currentBeat = null; - state.currentLyrics = null; - state.currentLines = []; - state.currentMarkers = []; - }, - }; -} - -export const projectStore = createProjectStore(); diff --git a/apps/lightwrite/apps/web/src/lib/stores/theme.svelte.ts b/apps/lightwrite/apps/web/src/lib/stores/theme.svelte.ts deleted file mode 100644 index 67b4ac098..000000000 --- a/apps/lightwrite/apps/web/src/lib/stores/theme.svelte.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createThemeStore, type HSLValue, type ThemeVariant } from '@manacore/shared-theme'; - -/** - * LightWrite theme store - * - * Uses blue primary color matching the waveform progress color - */ -export const theme = createThemeStore({ - appId: 'lightwrite', - defaultVariant: 'ocean' as ThemeVariant, - primaryColor: { - light: '217 91% 60%' as HSLValue, // Blue #3b82f6 - dark: '217 91% 60%' as HSLValue, - }, -}); diff --git a/apps/lightwrite/apps/web/src/lib/utils/bpm-detector.ts b/apps/lightwrite/apps/web/src/lib/utils/bpm-detector.ts deleted file mode 100644 index 791dd448b..000000000 --- a/apps/lightwrite/apps/web/src/lib/utils/bpm-detector.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * BPM Detection using Web Audio API - * Uses peak detection algorithm for BPM estimation - * - * Note: For more accurate results, consider using essentia.js WASM module - * This implementation provides a lightweight fallback - */ - -interface BpmResult { - bpm: number; - confidence: number; -} - -/** - * Detect BPM from an audio buffer - */ -export async function detectBpm(audioBuffer: AudioBuffer): Promise { - // Get audio data from the first channel - const channelData = audioBuffer.getChannelData(0); - const sampleRate = audioBuffer.sampleRate; - - // Downsample for efficiency - const downsampleFactor = 4; - const downsampled = downsample(channelData, downsampleFactor); - const effectiveSampleRate = sampleRate / downsampleFactor; - - // Apply low-pass filter to focus on bass frequencies (kick drum) - const filtered = lowPassFilter(downsampled, effectiveSampleRate, 150); - - // Detect peaks - const peaks = detectPeaks(filtered, effectiveSampleRate); - - // Calculate intervals between peaks - const intervals = calculateIntervals(peaks, effectiveSampleRate); - - // Estimate BPM from intervals - const result = estimateBpm(intervals); - - return result; -} - -/** - * Detect BPM from a File object - */ -export async function detectBpmFromFile(file: File): Promise { - const arrayBuffer = await file.arrayBuffer(); - const audioContext = new AudioContext(); - const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); - const result = await detectBpm(audioBuffer); - await audioContext.close(); - return result; -} - -/** - * Detect BPM from a URL - */ -export async function detectBpmFromUrl(url: string): Promise { - const response = await fetch(url); - const arrayBuffer = await response.arrayBuffer(); - const audioContext = new AudioContext(); - const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); - const result = await detectBpm(audioBuffer); - await audioContext.close(); - return result; -} - -function downsample(data: Float32Array, factor: number): Float32Array { - const length = Math.floor(data.length / factor); - const result = new Float32Array(length); - for (let i = 0; i < length; i++) { - result[i] = data[i * factor]; - } - return result; -} - -function lowPassFilter(data: Float32Array, sampleRate: number, cutoff: number): Float32Array { - const rc = 1.0 / (cutoff * 2 * Math.PI); - const dt = 1.0 / sampleRate; - const alpha = dt / (rc + dt); - - const result = new Float32Array(data.length); - result[0] = data[0]; - - for (let i = 1; i < data.length; i++) { - result[i] = result[i - 1] + alpha * (data[i] - result[i - 1]); - } - - return result; -} - -function detectPeaks(data: Float32Array, sampleRate: number): number[] { - const peaks: number[] = []; - const minPeakDistance = Math.floor(sampleRate * 0.2); // Min 200ms between peaks (300 BPM max) - - // Calculate threshold as percentage of max amplitude - let maxAmplitude = 0; - for (let i = 0; i < data.length; i++) { - const abs = Math.abs(data[i]); - if (abs > maxAmplitude) maxAmplitude = abs; - } - const threshold = maxAmplitude * 0.5; - - let lastPeak = -minPeakDistance; - - for (let i = 1; i < data.length - 1; i++) { - if (i - lastPeak < minPeakDistance) continue; - - const current = Math.abs(data[i]); - const prev = Math.abs(data[i - 1]); - const next = Math.abs(data[i + 1]); - - if (current > threshold && current > prev && current > next) { - peaks.push(i); - lastPeak = i; - } - } - - return peaks; -} - -function calculateIntervals(peaks: number[], sampleRate: number): number[] { - const intervals: number[] = []; - - for (let i = 1; i < peaks.length; i++) { - const interval = (peaks[i] - peaks[i - 1]) / sampleRate; - // Filter to reasonable BPM range (60-200 BPM = 0.3-1.0 seconds) - if (interval >= 0.3 && interval <= 1.0) { - intervals.push(interval); - } - } - - return intervals; -} - -function estimateBpm(intervals: number[]): BpmResult { - if (intervals.length === 0) { - return { bpm: 120, confidence: 0 }; - } - - // Group intervals into buckets and find the most common - const bucketSize = 0.02; // 20ms buckets - const buckets: Map = new Map(); - - for (const interval of intervals) { - const bucket = Math.round(interval / bucketSize) * bucketSize; - if (!buckets.has(bucket)) { - buckets.set(bucket, []); - } - buckets.get(bucket)!.push(interval); - } - - // Find the bucket with most intervals - let maxCount = 0; - let bestBucket = 0.5; - let bestIntervals: number[] = []; - - for (const [bucket, bucketIntervals] of buckets) { - if (bucketIntervals.length > maxCount) { - maxCount = bucketIntervals.length; - bestBucket = bucket; - bestIntervals = bucketIntervals; - } - } - - // Calculate average interval from best bucket - const avgInterval = bestIntervals.reduce((a, b) => a + b, 0) / bestIntervals.length; - const bpm = Math.round(60 / avgInterval); - - // Calculate confidence based on how many intervals fell into the best bucket - const confidence = Math.min(1, (maxCount / intervals.length) * 2); - - // Ensure BPM is in reasonable range - let finalBpm = bpm; - if (finalBpm < 60) finalBpm *= 2; - if (finalBpm > 200) finalBpm /= 2; - - return { - bpm: Math.round(finalBpm), - confidence: Math.round(confidence * 100) / 100, - }; -} - -/** - * Snap a time value to the nearest beat based on BPM - */ -export function snapToBeat(time: number, bpm: number, offset: number = 0): number { - const beatDuration = 60 / bpm; - const adjustedTime = time - offset; - const nearestBeat = Math.round(adjustedTime / beatDuration) * beatDuration; - return nearestBeat + offset; -} - -/** - * Get beat times within a range - */ -export function getBeatTimes( - startTime: number, - endTime: number, - bpm: number, - offset: number = 0 -): number[] { - const beatDuration = 60 / bpm; - const beats: number[] = []; - - const firstBeat = Math.ceil((startTime - offset) / beatDuration) * beatDuration + offset; - - for (let beat = firstBeat; beat <= endTime; beat += beatDuration) { - beats.push(beat); - } - - return beats; -} - -/** - * Get bar (measure) times within a range (assuming 4/4 time) - */ -export function getBarTimes( - startTime: number, - endTime: number, - bpm: number, - offset: number = 0, - beatsPerBar: number = 4 -): number[] { - const barDuration = (60 / bpm) * beatsPerBar; - const bars: number[] = []; - - const firstBar = Math.ceil((startTime - offset) / barDuration) * barDuration + offset; - - for (let bar = firstBar; bar <= endTime; bar += barDuration) { - bars.push(bar); - } - - return bars; -} diff --git a/apps/lightwrite/apps/web/src/lib/utils/time-format.ts b/apps/lightwrite/apps/web/src/lib/utils/time-format.ts deleted file mode 100644 index 284f84f93..000000000 --- a/apps/lightwrite/apps/web/src/lib/utils/time-format.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Format time in seconds to MM:SS format - */ -export function formatTime(seconds: number): string { - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; -} - -/** - * Format time in seconds to MM:SS.ms format - */ -export function formatTimeWithMs(seconds: number): string { - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - const ms = Math.floor((seconds % 1) * 100); - return `${mins}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`; -} - -/** - * Parse MM:SS or MM:SS.ms format to seconds - */ -export function parseTime(timeString: string): number | null { - const match = timeString.match(/^(\d+):(\d{2})(?:\.(\d{2}))?$/); - if (!match) return null; - - const mins = parseInt(match[1], 10); - const secs = parseInt(match[2], 10); - const ms = match[3] ? parseInt(match[3], 10) / 100 : 0; - - return mins * 60 + secs + ms; -} - -/** - * Format duration for display (e.g., "3:45") - */ -export function formatDuration(seconds: number): string { - if (seconds < 60) { - return `0:${Math.floor(seconds).toString().padStart(2, '0')}`; - } - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; -} diff --git a/apps/lightwrite/apps/web/src/routes/(auth)/+layout.svelte b/apps/lightwrite/apps/web/src/routes/(auth)/+layout.svelte deleted file mode 100644 index a54cfdcb7..000000000 --- a/apps/lightwrite/apps/web/src/routes/(auth)/+layout.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - -{@render children()} diff --git a/apps/lightwrite/apps/web/src/routes/(auth)/login/+page.svelte b/apps/lightwrite/apps/web/src/routes/(auth)/login/+page.svelte deleted file mode 100644 index 5325881c4..000000000 --- a/apps/lightwrite/apps/web/src/routes/(auth)/login/+page.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - - - Login - LightWrite - - - diff --git a/apps/lightwrite/apps/web/src/routes/(auth)/register/+page.svelte b/apps/lightwrite/apps/web/src/routes/(auth)/register/+page.svelte deleted file mode 100644 index 0c120c750..000000000 --- a/apps/lightwrite/apps/web/src/routes/(auth)/register/+page.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - - - Register - LightWrite - - - diff --git a/apps/lightwrite/apps/web/src/routes/+layout.svelte b/apps/lightwrite/apps/web/src/routes/+layout.svelte deleted file mode 100644 index 5dddb98cc..000000000 --- a/apps/lightwrite/apps/web/src/routes/+layout.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - -{#if loading} -
-
-
-

LightWrite

-
-
-{:else} -
- {@render children()} -
-{/if} diff --git a/apps/lightwrite/apps/web/src/routes/+page.svelte b/apps/lightwrite/apps/web/src/routes/+page.svelte deleted file mode 100644 index 4fc4c25df..000000000 --- a/apps/lightwrite/apps/web/src/routes/+page.svelte +++ /dev/null @@ -1,335 +0,0 @@ - - - - LightWrite - Beat & Lyrics Editor - - -
- -
-
-

- LightWrite -

- -
- {#if authStore.isAuthenticated} - - {authStore.user?.email} - - - {:else} - - Login - - {/if} -
-
-
- - -
- {#if !authStore.isAuthenticated} - -
-

Create Synced Lyrics for Your Beats

-

- Upload your beats, add lyrics, sync timestamps, and export to LRC, SRT, or video formats. -

- - - -
-
-
- - - -
-

Waveform Editor

-

- Visual waveform display with zoom, markers, and precise navigation. -

-
- -
-
- - - -
-

BPM Detection

-

- Automatic tempo detection with snap-to-beat functionality. -

-
- -
-
- - - -
-

Multiple Exports

-

- Export to LRC, SRT, JSON, or generate karaoke videos. -

-
-
-
- {:else} - -
-

Your Projects

- -
- - {#if projectStore.isLoading} -
-
-
- {:else if projectStore.projects.length === 0} -
-
- - - -
-

No projects yet

-

Create your first project to get started

- -
- {:else} - - {/if} - {/if} -
-
- - -{#if showCreateModal} -
(showCreateModal = false)} - role="dialog" - > -
e.stopPropagation()} - role="document" - > -

Create New Project

-
{ - e.preventDefault(); - handleCreateProject(); - }} - > -
-
- - -
-
- - -
-
-
- - -
-
-
-
-{/if} diff --git a/apps/lightwrite/apps/web/src/routes/editor/[id]/+page.svelte b/apps/lightwrite/apps/web/src/routes/editor/[id]/+page.svelte deleted file mode 100644 index 90626cbbf..000000000 --- a/apps/lightwrite/apps/web/src/routes/editor/[id]/+page.svelte +++ /dev/null @@ -1,491 +0,0 @@ - - - - {projectStore.currentProject?.title || 'Editor'} - LightWrite - - - - -
- -
-
-
- - - - - -
-

- {projectStore.currentProject?.title || 'Loading...'} -

- {#if projectStore.currentProject?.description && !isMobile} -

- {projectStore.currentProject.description} -

- {/if} -
-
- -
- - - - -
- - - {#if showExportMenu} -
- - - -
- {/if} -
-
-
-
- - {#if projectStore.isLoading} -
-
-
- {:else if projectStore.error} -
-
-

{projectStore.error}

- Go back -
-
- {:else} - -
- -
- {#if projectStore.currentBeat} -
-
-
- - - - {projectStore.currentBeat.filename} -
- -
- - - - {#if !isMobile} - waveformEditor?.toggleLoop(markerId)} - /> - {/if} - - -
- {:else} - - {/if} -
- - - {#if isMobile} - -
- -
- - -
- - -
- {#if mobileTab === 'lyrics'} - - {:else} - - {/if} -
-
- {:else} - -
- -
- -
- - -
- {#if editorStore.mode === 'preview'} - - {:else} -
-
-

Switch to Preview mode to see karaoke animation

- -
-
- {/if} -
-
- {/if} -
- {/if} -
- - -{#if showExportMenu} - -{/if} diff --git a/apps/lightwrite/apps/web/src/routes/health/+server.ts b/apps/lightwrite/apps/web/src/routes/health/+server.ts deleted file mode 100644 index 367b3dba7..000000000 --- a/apps/lightwrite/apps/web/src/routes/health/+server.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { json } from '@sveltejs/kit'; - -export function GET() { - return json({ status: 'ok', service: 'lightwrite-web' }); -} diff --git a/apps/lightwrite/apps/web/src/routes/offline/+page.svelte b/apps/lightwrite/apps/web/src/routes/offline/+page.svelte deleted file mode 100644 index b5a19108d..000000000 --- a/apps/lightwrite/apps/web/src/routes/offline/+page.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - - - Offline - LightWrite - - -
-
-
- - - - -
- -

- {isOnline ? 'Verbindung wiederhergestellt!' : 'Du bist offline'} -

- -

- {#if isOnline} - Du wirst gleich weitergeleitet... - {:else} - LightWrite benötigt eine Internetverbindung für Audio. - {/if} -

- - {#if !isOnline} -
- - - - - Zur Startseite - - - -
- {:else} -
- - - - - Weiterleitung... -
- {/if} -
-
diff --git a/apps/lightwrite/apps/web/svelte.config.js b/apps/lightwrite/apps/web/svelte.config.js deleted file mode 100644 index a7a917e4c..000000000 --- a/apps/lightwrite/apps/web/svelte.config.js +++ /dev/null @@ -1,14 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter({ - out: 'build', - }), - }, -}; - -export default config; diff --git a/apps/lightwrite/apps/web/tsconfig.json b/apps/lightwrite/apps/web/tsconfig.json deleted file mode 100644 index a8f10c8e3..000000000 --- a/apps/lightwrite/apps/web/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } -} diff --git a/apps/lightwrite/apps/web/vite.config.ts b/apps/lightwrite/apps/web/vite.config.ts deleted file mode 100644 index 2d2edc3b1..000000000 --- a/apps/lightwrite/apps/web/vite.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; -import tailwindcss from '@tailwindcss/vite'; -import { SvelteKitPWA } from '@vite-pwa/sveltekit'; -import { createPWAConfig } from '@manacore/shared-pwa'; -import { MANACORE_SHARED_PACKAGES } from '@manacore/shared-vite-config'; - -export default defineConfig({ - plugins: [ - tailwindcss(), - sveltekit(), - SvelteKitPWA( - createPWAConfig({ - name: 'LightWrite - Audio Editor', - shortName: 'LightWrite', - description: 'Beat und Lyrics Editor', - themeColor: '#f97316', - }) - ), - ], - server: { - port: 5180, - strictPort: true, - }, - ssr: { - noExternal: [...MANACORE_SHARED_PACKAGES, '@lightwrite/shared'], - }, - optimizeDeps: { - exclude: [...MANACORE_SHARED_PACKAGES, '@lightwrite/shared'], - }, -}); diff --git a/apps/lightwrite/package.json b/apps/lightwrite/package.json deleted file mode 100644 index 7ae43041b..000000000 --- a/apps/lightwrite/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "lightwrite", - "private": true, - "scripts": { - "dev": "pnpm run --filter=@lightwrite/* --parallel dev" - } -} diff --git a/apps/lightwrite/packages/shared/package.json b/apps/lightwrite/packages/shared/package.json deleted file mode 100644 index 90936b203..000000000 --- a/apps/lightwrite/packages/shared/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@lightwrite/shared", - "version": "1.0.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "exports": { - ".": "./src/index.ts", - "./types": "./src/types/index.ts" - }, - "scripts": { - "type-check": "tsc --noEmit" - }, - "devDependencies": { - "typescript": "^5.7.3" - } -} diff --git a/apps/lightwrite/packages/shared/src/index.ts b/apps/lightwrite/packages/shared/src/index.ts deleted file mode 100644 index fcb073fef..000000000 --- a/apps/lightwrite/packages/shared/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './types'; diff --git a/apps/lightwrite/packages/shared/src/types/beat.ts b/apps/lightwrite/packages/shared/src/types/beat.ts deleted file mode 100644 index 153f95185..000000000 --- a/apps/lightwrite/packages/shared/src/types/beat.ts +++ /dev/null @@ -1,40 +0,0 @@ -export type TranscriptionStatus = 'none' | 'pending' | 'completed' | 'failed'; - -export interface Beat { - id: string; - projectId: string; - storagePath: string; - filename?: string | null; - duration?: number | null; - bpm?: number | null; - bpmConfidence?: number | null; - waveformData?: WaveformData | null; - // STT Transcription fields - transcriptionStatus?: TranscriptionStatus | null; - transcriptionError?: string | null; - transcribedAt?: Date | null; - createdAt: Date; -} - -export interface WaveformData { - peaks: number[]; - sampleRate: number; - duration: number; -} - -export interface CreateBeatDto { - projectId: string; - filename: string; -} - -export interface UpdateBeatDto { - bpm?: number; - bpmConfidence?: number; - duration?: number; - waveformData?: WaveformData; -} - -export interface BeatUploadResponse { - beat: Beat; - uploadUrl: string; -} diff --git a/apps/lightwrite/packages/shared/src/types/export.ts b/apps/lightwrite/packages/shared/src/types/export.ts deleted file mode 100644 index c01226899..000000000 --- a/apps/lightwrite/packages/shared/src/types/export.ts +++ /dev/null @@ -1,57 +0,0 @@ -export type ExportFormat = 'lrc' | 'srt' | 'json' | 'video'; - -export interface ExportOptions { - format: ExportFormat; - includeMarkers?: boolean; - videoOptions?: VideoExportOptions; -} - -export interface VideoExportOptions { - width: number; - height: number; - fps: number; - backgroundColor: string; - textColor: string; - highlightColor: string; - fontFamily: string; - fontSize: number; -} - -export interface LrcExportResult { - content: string; - filename: string; -} - -export interface SrtExportResult { - content: string; - filename: string; -} - -export interface JsonExportResult { - data: JsonExportData; - filename: string; -} - -export interface JsonExportData { - project: { - id: string; - title: string; - description?: string; - }; - beat: { - bpm?: number; - duration?: number; - }; - markers: Array<{ - type: string; - label?: string; - startTime: number; - endTime?: number; - }>; - lyrics: Array<{ - lineNumber: number; - text: string; - startTime?: number; - endTime?: number; - }>; -} diff --git a/apps/lightwrite/packages/shared/src/types/index.ts b/apps/lightwrite/packages/shared/src/types/index.ts deleted file mode 100644 index ea26cf36b..000000000 --- a/apps/lightwrite/packages/shared/src/types/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './project'; -export * from './beat'; -export * from './marker'; -export * from './lyrics'; -export * from './export'; diff --git a/apps/lightwrite/packages/shared/src/types/lyrics.ts b/apps/lightwrite/packages/shared/src/types/lyrics.ts deleted file mode 100644 index 9bb1ee1a2..000000000 --- a/apps/lightwrite/packages/shared/src/types/lyrics.ts +++ /dev/null @@ -1,55 +0,0 @@ -export interface Lyrics { - id: string; - projectId: string; - content?: string | null; -} - -export interface LyricLine { - id: string; - lyricsId: string; - lineNumber: number; - text: string; - startTime?: number | null; - endTime?: number | null; -} - -export interface CreateLyricsDto { - projectId: string; - content?: string; -} - -export interface UpdateLyricsDto { - content?: string; -} - -export interface CreateLyricLineDto { - lyricsId: string; - lineNumber: number; - text: string; - startTime?: number; - endTime?: number; -} - -export interface UpdateLyricLineDto { - text?: string; - startTime?: number; - endTime?: number; -} - -export interface SyncedLyrics { - lines: SyncedLine[]; -} - -export interface SyncedLine { - lineNumber: number; - text: string; - startTime: number; - endTime?: number; - words?: SyncedWord[]; -} - -export interface SyncedWord { - word: string; - startTime: number; - endTime: number; -} diff --git a/apps/lightwrite/packages/shared/src/types/marker.ts b/apps/lightwrite/packages/shared/src/types/marker.ts deleted file mode 100644 index 54e3bb17c..000000000 --- a/apps/lightwrite/packages/shared/src/types/marker.ts +++ /dev/null @@ -1,49 +0,0 @@ -export type MarkerType = - | 'verse' - | 'hook' - | 'bridge' - | 'intro' - | 'outro' - | 'drop' - | 'breakdown' - | 'custom'; - -export interface Marker { - id: string; - beatId: string; - type: MarkerType; - label?: string | null; - startTime: number; - endTime?: number | null; - color?: string | null; - sortOrder?: number | null; -} - -export interface CreateMarkerDto { - beatId: string; - type: MarkerType; - label?: string; - startTime: number; - endTime?: number; - color?: string; -} - -export interface UpdateMarkerDto { - type?: MarkerType; - label?: string; - startTime?: number; - endTime?: number; - color?: string; - sortOrder?: number; -} - -export const MARKER_COLORS: Record = { - verse: '#3B82F6', // blue - hook: '#EF4444', // red - bridge: '#8B5CF6', // purple - intro: '#22C55E', // green - outro: '#F97316', // orange - drop: '#EC4899', // pink - breakdown: '#14B8A6', // teal - custom: '#6B7280', // gray -}; diff --git a/apps/lightwrite/packages/shared/src/types/project.ts b/apps/lightwrite/packages/shared/src/types/project.ts deleted file mode 100644 index aaac4e85b..000000000 --- a/apps/lightwrite/packages/shared/src/types/project.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface Project { - id: string; - userId: string; - title: string; - description?: string | null; - createdAt: Date; - updatedAt: Date; -} - -export interface CreateProjectDto { - title: string; - description?: string; -} - -export interface UpdateProjectDto { - title?: string; - description?: string; -} diff --git a/apps/lightwrite/packages/shared/tsconfig.json b/apps/lightwrite/packages/shared/tsconfig.json deleted file mode 100644 index 9976a4fcb..000000000 --- a/apps/lightwrite/packages/shared/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "declaration": true, - "declarationMap": true, - "noEmit": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules"] -}