From d7b4042164546c9317d9ec2e32a54b06628aee98 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 29 Mar 2026 20:03:55 +0200 Subject: [PATCH] feat(wisekeep): migrate from archive to local-first + Hono architecture - Move from apps-archived/ to apps/ - Delete NestJS backend, mobile app, legacy Python, shared-types - Create Hono/Bun server with Groq Whisper transcription via yt-dlp - Create local-first store (transcripts, playlists) with guest seed - Rewrite web app: Transcribe page, Library with search/expand, Playlists CRUD, auth via shared-auth-ui, AuthGate with guest mode - Remove broken landing page subpages (Prettier-incompatible Astro) - Add wisekeep to root CLAUDE.md and dev scripts - Fix duplicate wisekeep entries in shared-branding - 0 type errors on both server and web Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 1 + apps-archived/wisekeep/CLAUDE.md | 233 ------- apps-archived/wisekeep/README.md | 392 ----------- .../wisekeep/apps/backend/.env.example | 14 - .../wisekeep/apps/backend/nest-cli.json | 8 - .../wisekeep/apps/backend/package.json | 52 -- .../wisekeep/apps/backend/src/app.module.ts | 24 - .../backend/src/health/health.controller.ts | 30 - .../apps/backend/src/health/health.module.ts | 7 - .../wisekeep/apps/backend/src/main.ts | 31 - .../src/playlist/playlist.controller.ts | 37 - .../backend/src/playlist/playlist.module.ts | 10 - .../backend/src/playlist/playlist.service.ts | 173 ----- .../dto/transcribe-request.dto.ts | 35 - .../entities/transcription-job.entity.ts | 46 -- .../transcription/transcription.controller.ts | 33 - .../src/transcription/transcription.module.ts | 14 - .../transcription/transcription.service.ts | 254 ------- .../backend/src/websocket/progress.gateway.ts | 79 --- .../backend/src/websocket/websocket.module.ts | 9 - .../backend/src/whisper/whisper.controller.ts | 17 - .../backend/src/whisper/whisper.module.ts | 10 - .../backend/src/whisper/whisper.service.ts | 219 ------ .../backend/src/youtube/youtube.module.ts | 8 - .../backend/src/youtube/youtube.service.ts | 163 ----- .../wisekeep/apps/backend/tsconfig.json | 26 - .../src/components/admin/Dashboard.tsx | 235 ------- .../src/components/admin/PlaylistManager.tsx | 263 ------- .../landing/src/components/admin/Settings.tsx | 257 ------- .../src/components/admin/TranscriptViewer.tsx | 235 ------- .../components/speakers/QuoteCollection.astro | 109 --- .../src/components/speakers/TalkGrid.astro | 128 ---- .../landing/src/layouts/AdminLayout.astro | 66 -- .../apps/landing/src/pages/admin/index.astro | 189 ----- .../landing/src/pages/speakers/index.astro | 289 -------- .../pages/speakers/rory-sutherland/all.astro | 658 ------------------ .../speakers/rory-sutherland/analyses.astro | 650 ----------------- .../rory-sutherland/transcripts.astro | 488 ------------- .../src/pages/talks/[slug]-backup.astro | 658 ------------------ .../landing/src/pages/talks/[slug]-old.astro | 286 -------- .../src/pages/talks/[slug]-sidebar.astro | 362 ---------- .../wisekeep/apps/landing/tailwind.config.mjs | 68 -- apps-archived/wisekeep/apps/mobile/app.json | 37 - .../apps/mobile/app/(tabs)/_layout.tsx | 48 -- .../wisekeep/apps/mobile/app/(tabs)/index.tsx | 164 ----- .../apps/mobile/app/(tabs)/settings.tsx | 70 -- .../apps/mobile/app/(tabs)/transcribe.tsx | 190 ----- .../apps/mobile/app/(tabs)/transcripts.tsx | 85 --- .../wisekeep/apps/mobile/app/_layout.tsx | 23 - .../wisekeep/apps/mobile/babel.config.js | 16 - .../wisekeep/apps/mobile/package.json | 38 - .../wisekeep/apps/mobile/src/services/api.ts | 66 -- .../wisekeep/apps/mobile/src/stores/jobs.ts | 57 -- .../wisekeep/apps/mobile/tsconfig.json | 10 - .../wisekeep/apps/web/eslint.config.js | 17 - apps-archived/wisekeep/apps/web/package.json | 34 - .../wisekeep/apps/web/postcss.config.js | 6 - .../wisekeep/apps/web/src/lib/api/client.ts | 107 --- .../wisekeep/apps/web/src/lib/api/feedback.ts | 15 - .../web/src/lib/components/AppSlider.svelte | 33 - .../apps/web/src/lib/stores/auth.svelte.ts | 205 ------ .../wisekeep/apps/web/src/lib/stores/jobs.ts | 108 --- .../(auth)/forgot-password/+page.svelte | 46 -- .../web/src/routes/(auth)/login/+page.svelte | 65 -- .../src/routes/(auth)/register/+page.svelte | 60 -- .../src/routes/(protected)/+layout.server.ts | 13 - .../web/src/routes/(protected)/+layout.svelte | 182 ----- .../src/routes/(protected)/apps/+page.svelte | 17 - .../routes/(protected)/dashboard/+page.svelte | 101 --- .../routes/(protected)/feedback/+page.svelte | 11 - .../routes/(protected)/playlists/+page.svelte | 65 -- .../(protected)/transcribe/+page.svelte | 136 ---- .../(protected)/transcripts/+page.svelte | 70 -- .../apps/web/src/routes/+layout.svelte | 9 - .../wisekeep/apps/web/src/routes/+page.svelte | 27 - .../wisekeep/apps/web/tailwind.config.js | 23 - apps-archived/wisekeep/legacy/admin.html | 372 ---------- apps-archived/wisekeep/legacy/api_server.py | 372 ---------- apps-archived/wisekeep/legacy/config.json | 31 - .../wisekeep/legacy/quick_transcribe.sh | 44 -- .../wisekeep/legacy/requirements.txt | 4 - apps-archived/wisekeep/legacy/start.sh | 47 -- apps-archived/wisekeep/legacy/transcriber.py | 294 -------- .../wisekeep/legacy/transcriber_v2.py | 476 ------------- .../wisekeep/legacy/transcriber_v3.py | 603 ---------------- .../legacy/transcriber_v4_parallel.py | 559 --------------- apps-archived/wisekeep/package.json | 22 - .../packages/shared-types/package.json | 13 - .../packages/shared-types/src/index.ts | 92 --- .../packages/shared-types/tsconfig.json | 13 - {apps-archived => apps}/wisekeep/.gitignore | 0 apps/wisekeep/CLAUDE.md | 53 ++ .../wisekeep/apps/landing/astro.config.mjs | 7 +- .../wisekeep/apps/landing/package.json | 0 .../src/components/CollapsibleSection.astro | 0 .../landing/src/components/ContentCard.tsx | 0 .../src/components/ContentCardList.tsx | 0 .../apps/landing/src/components/Footer.astro | 0 .../landing/src/components/Navigation.astro | 0 .../src/components/SearchableContentList.tsx | 0 .../landing/src/components/TalksSidebar.astro | 0 .../src/components/ThemeSwitcher.astro | 0 .../src/components/speakers/SpeakerHero.astro | 0 .../components/speakers/SpeakerStats.astro | 0 .../apps/landing/src/content/config.ts | 0 ...ry-sutherland-perspective-is-everything.md | 0 .../talks/simon-sinek-leaders-eat-last.md | 0 .../talks/simon-sinek-love-your-work.md | 0 ...imon-sinek-millennials-in-the-workplace.md | 0 ...nek-why-good-leaders-make-you-feel-safe.md | 0 .../wisekeep/apps/landing/src/env.d.ts | 0 .../apps/landing/src/layouts/BaseLayout.astro | 0 .../apps/landing/src/layouts/Layout.astro | 0 .../apps/landing/src/pages/index.astro | 0 .../src/pages/speakers/rory-sutherland.astro | 0 .../src/pages/speakers/simon-sinek.astro | 0 .../apps/landing/src/pages/talks/[slug].astro | 0 .../apps/landing/src/styles/themes.css | 0 .../wisekeep/apps/landing/tailwind.config.mjs | 75 ++ apps/wisekeep/apps/server/package.json | 22 + apps/wisekeep/apps/server/src/config.ts | 30 + apps/wisekeep/apps/server/src/index.ts | 28 + apps/wisekeep/apps/server/src/lib/errors.ts | 19 + .../server/src/middleware/error-handler.ts | 11 + .../apps/server/src/middleware/jwt-auth.ts | 46 ++ .../wisekeep/apps/server/src/routes/health.ts | 10 + .../apps/server/src/routes/transcribe.ts | 25 + .../apps/server/src/services/transcribe.ts | 60 ++ .../wisekeep/apps/server}/tsconfig.json | 12 +- .../wisekeep/apps/web/.env.example | 0 apps/wisekeep/apps/web/package.json | 33 + .../wisekeep/apps/web/src/app.css | 0 .../wisekeep/apps/web/src/app.d.ts | 0 .../wisekeep/apps/web/src/app.html | 0 .../src/lib/components/WisekeepLogo.svelte | 11 + .../apps/web/src/lib/stores/auth.svelte.ts | 5 + .../apps/web/src/routes/(auth)/+layout.svelte | 0 .../web/src/routes/(auth)/login/+page.svelte | 22 + .../src/routes/(auth)/register/+page.svelte | 16 + .../web/src/routes/(protected)/+layout.svelte | 96 +++ .../routes/(protected)/playlists/+page.svelte | 97 +++ .../(protected)/transcribe/+page.svelte | 99 +++ .../(protected)/transcripts/+page.svelte | 124 ++++ .../apps/web/src/routes/+layout.svelte | 37 + .../wisekeep/apps/web/src/routes/+page.svelte | 5 + .../wisekeep/apps/web/svelte.config.js | 4 +- .../wisekeep/apps/web/tsconfig.json | 0 .../wisekeep/apps/web/vite.config.ts | 3 +- apps/wisekeep/package.json | 8 + package.json | 5 + pnpm-lock.yaml | 545 ++++++++++++++- 151 files changed, 1490 insertions(+), 11980 deletions(-) delete mode 100644 apps-archived/wisekeep/CLAUDE.md delete mode 100644 apps-archived/wisekeep/README.md delete mode 100644 apps-archived/wisekeep/apps/backend/.env.example delete mode 100644 apps-archived/wisekeep/apps/backend/nest-cli.json delete mode 100644 apps-archived/wisekeep/apps/backend/package.json delete mode 100644 apps-archived/wisekeep/apps/backend/src/app.module.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/health/health.controller.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/health/health.module.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/main.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/playlist/playlist.controller.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/playlist/playlist.module.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/playlist/playlist.service.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/transcription/dto/transcribe-request.dto.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/transcription/entities/transcription-job.entity.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/transcription/transcription.controller.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/transcription/transcription.module.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/transcription/transcription.service.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/websocket/progress.gateway.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/websocket/websocket.module.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/whisper/whisper.controller.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/whisper/whisper.module.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/whisper/whisper.service.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/youtube/youtube.module.ts delete mode 100644 apps-archived/wisekeep/apps/backend/src/youtube/youtube.service.ts delete mode 100644 apps-archived/wisekeep/apps/backend/tsconfig.json delete mode 100644 apps-archived/wisekeep/apps/landing/src/components/admin/Dashboard.tsx delete mode 100644 apps-archived/wisekeep/apps/landing/src/components/admin/PlaylistManager.tsx delete mode 100644 apps-archived/wisekeep/apps/landing/src/components/admin/Settings.tsx delete mode 100644 apps-archived/wisekeep/apps/landing/src/components/admin/TranscriptViewer.tsx delete mode 100644 apps-archived/wisekeep/apps/landing/src/components/speakers/QuoteCollection.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/components/speakers/TalkGrid.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/layouts/AdminLayout.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/pages/admin/index.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/pages/speakers/index.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/pages/speakers/rory-sutherland/all.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/pages/speakers/rory-sutherland/analyses.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/pages/speakers/rory-sutherland/transcripts.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/pages/talks/[slug]-backup.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/pages/talks/[slug]-old.astro delete mode 100644 apps-archived/wisekeep/apps/landing/src/pages/talks/[slug]-sidebar.astro delete mode 100644 apps-archived/wisekeep/apps/landing/tailwind.config.mjs delete mode 100644 apps-archived/wisekeep/apps/mobile/app.json delete mode 100644 apps-archived/wisekeep/apps/mobile/app/(tabs)/_layout.tsx delete mode 100644 apps-archived/wisekeep/apps/mobile/app/(tabs)/index.tsx delete mode 100644 apps-archived/wisekeep/apps/mobile/app/(tabs)/settings.tsx delete mode 100644 apps-archived/wisekeep/apps/mobile/app/(tabs)/transcribe.tsx delete mode 100644 apps-archived/wisekeep/apps/mobile/app/(tabs)/transcripts.tsx delete mode 100644 apps-archived/wisekeep/apps/mobile/app/_layout.tsx delete mode 100644 apps-archived/wisekeep/apps/mobile/babel.config.js delete mode 100644 apps-archived/wisekeep/apps/mobile/package.json delete mode 100644 apps-archived/wisekeep/apps/mobile/src/services/api.ts delete mode 100644 apps-archived/wisekeep/apps/mobile/src/stores/jobs.ts delete mode 100644 apps-archived/wisekeep/apps/mobile/tsconfig.json delete mode 100644 apps-archived/wisekeep/apps/web/eslint.config.js delete mode 100644 apps-archived/wisekeep/apps/web/package.json delete mode 100644 apps-archived/wisekeep/apps/web/postcss.config.js delete mode 100644 apps-archived/wisekeep/apps/web/src/lib/api/client.ts delete mode 100644 apps-archived/wisekeep/apps/web/src/lib/api/feedback.ts delete mode 100644 apps-archived/wisekeep/apps/web/src/lib/components/AppSlider.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/lib/stores/auth.svelte.ts delete mode 100644 apps-archived/wisekeep/apps/web/src/lib/stores/jobs.ts delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(auth)/forgot-password/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(auth)/login/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(auth)/register/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(protected)/+layout.server.ts delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(protected)/+layout.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(protected)/apps/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(protected)/dashboard/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(protected)/feedback/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(protected)/playlists/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(protected)/transcribe/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/(protected)/transcripts/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/+layout.svelte delete mode 100644 apps-archived/wisekeep/apps/web/src/routes/+page.svelte delete mode 100644 apps-archived/wisekeep/apps/web/tailwind.config.js delete mode 100644 apps-archived/wisekeep/legacy/admin.html delete mode 100644 apps-archived/wisekeep/legacy/api_server.py delete mode 100644 apps-archived/wisekeep/legacy/config.json delete mode 100755 apps-archived/wisekeep/legacy/quick_transcribe.sh delete mode 100644 apps-archived/wisekeep/legacy/requirements.txt delete mode 100755 apps-archived/wisekeep/legacy/start.sh delete mode 100755 apps-archived/wisekeep/legacy/transcriber.py delete mode 100755 apps-archived/wisekeep/legacy/transcriber_v2.py delete mode 100755 apps-archived/wisekeep/legacy/transcriber_v3.py delete mode 100644 apps-archived/wisekeep/legacy/transcriber_v4_parallel.py delete mode 100644 apps-archived/wisekeep/package.json delete mode 100644 apps-archived/wisekeep/packages/shared-types/package.json delete mode 100644 apps-archived/wisekeep/packages/shared-types/src/index.ts delete mode 100644 apps-archived/wisekeep/packages/shared-types/tsconfig.json rename {apps-archived => apps}/wisekeep/.gitignore (100%) create mode 100644 apps/wisekeep/CLAUDE.md rename {apps-archived => apps}/wisekeep/apps/landing/astro.config.mjs (77%) rename {apps-archived => apps}/wisekeep/apps/landing/package.json (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/CollapsibleSection.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/ContentCard.tsx (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/ContentCardList.tsx (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/Footer.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/Navigation.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/SearchableContentList.tsx (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/TalksSidebar.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/ThemeSwitcher.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/speakers/SpeakerHero.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/components/speakers/SpeakerStats.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/content/config.ts (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/content/talks/rory-sutherland-perspective-is-everything.md (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/content/talks/simon-sinek-leaders-eat-last.md (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/content/talks/simon-sinek-love-your-work.md (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/content/talks/simon-sinek-millennials-in-the-workplace.md (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/content/talks/simon-sinek-why-good-leaders-make-you-feel-safe.md (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/env.d.ts (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/layouts/BaseLayout.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/layouts/Layout.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/pages/index.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/pages/speakers/rory-sutherland.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/pages/speakers/simon-sinek.astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/pages/talks/[slug].astro (100%) rename {apps-archived => apps}/wisekeep/apps/landing/src/styles/themes.css (100%) create mode 100644 apps/wisekeep/apps/landing/tailwind.config.mjs create mode 100644 apps/wisekeep/apps/server/package.json create mode 100644 apps/wisekeep/apps/server/src/config.ts create mode 100644 apps/wisekeep/apps/server/src/index.ts create mode 100644 apps/wisekeep/apps/server/src/lib/errors.ts create mode 100644 apps/wisekeep/apps/server/src/middleware/error-handler.ts create mode 100644 apps/wisekeep/apps/server/src/middleware/jwt-auth.ts create mode 100644 apps/wisekeep/apps/server/src/routes/health.ts create mode 100644 apps/wisekeep/apps/server/src/routes/transcribe.ts create mode 100644 apps/wisekeep/apps/server/src/services/transcribe.ts rename {apps-archived/wisekeep => apps/wisekeep/apps/server}/tsconfig.json (54%) rename {apps-archived => apps}/wisekeep/apps/web/.env.example (100%) create mode 100644 apps/wisekeep/apps/web/package.json rename {apps-archived => apps}/wisekeep/apps/web/src/app.css (100%) rename {apps-archived => apps}/wisekeep/apps/web/src/app.d.ts (100%) rename {apps-archived => apps}/wisekeep/apps/web/src/app.html (100%) create mode 100644 apps/wisekeep/apps/web/src/lib/components/WisekeepLogo.svelte create mode 100644 apps/wisekeep/apps/web/src/lib/stores/auth.svelte.ts rename {apps-archived => apps}/wisekeep/apps/web/src/routes/(auth)/+layout.svelte (100%) create mode 100644 apps/wisekeep/apps/web/src/routes/(auth)/login/+page.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/(auth)/register/+page.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/(protected)/+layout.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/(protected)/playlists/+page.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/(protected)/transcribe/+page.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/(protected)/transcripts/+page.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/+layout.svelte create mode 100644 apps/wisekeep/apps/web/src/routes/+page.svelte rename {apps-archived => apps}/wisekeep/apps/web/svelte.config.js (70%) rename {apps-archived => apps}/wisekeep/apps/web/tsconfig.json (100%) rename {apps-archived => apps}/wisekeep/apps/web/vite.config.ts (58%) create mode 100644 apps/wisekeep/package.json diff --git a/CLAUDE.md b/CLAUDE.md index d81233d86..d0111e4eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/ | **taktik** | Time tracking | Web | | **uload** | URL shortener & link management | Server, Web, Landing | | **news** | AI news reader & personal library | Server, Web, Landing | +| **wisekeep** | AI transcription & wisdom library | Server, Web, Landing | | **calc** | Calculator & converter | Web | | **playground** | LLM playground | Web | diff --git a/apps-archived/wisekeep/CLAUDE.md b/apps-archived/wisekeep/CLAUDE.md deleted file mode 100644 index d2e9b330e..000000000 --- a/apps-archived/wisekeep/CLAUDE.md +++ /dev/null @@ -1,233 +0,0 @@ -# CLAUDE.md - Wisekeep - -This file provides guidance to Claude Code when working with the Wisekeep project. - -## Project Overview - -Wisekeep is an AI-powered wisdom extraction application that captures insights from video content: - -- YouTube video download via yt-dlp -- Ultra-fast audio transcription using Groq Whisper API (~300x realtime) -- Fallback to local Whisper for offline use -- Playlist management for batch processing -- Real-time progress updates via WebSocket -- Multi-platform support (Web, Mobile, Landing) - -## Architecture - -``` -apps/wisekeep/ -├── apps/ -│ ├── backend/ # NestJS API server (port 3006) -│ ├── web/ # SvelteKit web application -│ ├── landing/ # Astro landing/content site -│ └── mobile/ # Expo React Native app -├── packages/ -│ └── shared-types/ # Shared TypeScript types -├── data/ # Transcripts & playlists (gitignored) -├── legacy/ # Original Python code (reference) -├── package.json # Root orchestrator -└── CLAUDE.md # This file -``` - -## Quick Start - -### Prerequisites - -- Node.js 20+ -- pnpm 9.15.0+ -- yt-dlp installed (`brew install yt-dlp` on macOS) -- For local Whisper: Python 3 with openai-whisper package - -### Development - -```bash -# From monorepo root -pnpm install - -# Start all wisekeep apps -pnpm wisekeep:dev - -# Start individual apps -pnpm dev:wisekeep:backend # NestJS backend (port 3006) -pnpm dev:wisekeep:web # SvelteKit web (port 5173) -pnpm dev:wisekeep:landing # Astro landing (port 4321) -pnpm dev:wisekeep:mobile # Expo mobile - -# Start web + backend together -pnpm dev:wisekeep:app -``` - -### Environment Variables - -Create `apps/wisekeep/apps/backend/.env`: - -```bash -PORT=3006 -WHISPER_PROVIDER=groq # groq or local -WHISPER_MODEL=whisper-large-v3-turbo # whisper-large-v3-turbo, whisper-large-v3 (groq) | tiny, base, small, medium, large (local) -GROQ_API_KEY=gsk_... # Required for Groq provider -TEMP_AUDIO_DIR=./temp_audio -TRANSCRIPTS_DIR=./data/transcripts -PLAYLISTS_DIR=./data/playlists -``` - -## API Endpoints - -### Transcription - -| Method | Endpoint | Description | -| ------ | ---------------------- | --------------------------- | -| POST | `/transcription` | Start new transcription job | -| GET | `/transcription` | List all jobs | -| GET | `/transcription/:id` | Get job status | -| DELETE | `/transcription/:id` | Cancel job | -| GET | `/transcription/stats` | Get statistics | - -### Playlists - -| Method | Endpoint | Description | -| ------ | --------------------------- | --------------------- | -| GET | `/playlist` | List all playlists | -| GET | `/playlist/:category/:name` | Get specific playlist | -| POST | `/playlist` | Create playlist | -| DELETE | `/playlist/:category/:name` | Delete playlist | - -### Whisper - -| Method | Endpoint | Description | -| ------ | ----------------- | -------------------- | -| GET | `/whisper/models` | Get available models | - -### Health - -| Method | Endpoint | Description | -| ------ | --------------- | --------------- | -| GET | `/health` | Health check | -| GET | `/health/ready` | Readiness check | -| GET | `/health/live` | Liveness check | - -## WebSocket - -Connect to `/progress` namespace for real-time updates: - -```typescript -const socket = io('http://localhost:3006/progress'); - -socket.on('job_update', (data) => { - // { type, jobId, status, progress, videoInfo } -}); - -socket.on('job_complete', (data) => { - // { type, jobId, status, transcriptPath } -}); - -socket.on('job_error', (data) => { - // { type, jobId, error } -}); -``` - -## Whisper Configuration - -### Groq Whisper API (Recommended) - -- Ultra-fast, cloud-based (~300x realtime speed) -- Cost: ~$0.04/hour (whisper-large-v3-turbo) or ~$0.111/hour (whisper-large-v3) -- No GPU required -- Models: `whisper-large-v3-turbo` (fast) or `whisper-large-v3` (accurate) -- Set `WHISPER_PROVIDER=groq` and `GROQ_API_KEY` - -### Local Whisper - -- Free, runs locally -- Requires Python + openai-whisper -- GPU recommended for larger models -- Models: `tiny`, `base`, `small`, `medium`, `large` -- Set `WHISPER_PROVIDER=local` and `WHISPER_MODEL` - -## Technology Stack - -| Component | Technology | -| -------------- | --------------------------------- | -| Backend | NestJS 10, TypeScript | -| Web | SvelteKit 2, Svelte 5, Tailwind | -| Landing | Astro 4, Tailwind | -| Mobile | Expo 52, React Native, NativeWind | -| YouTube | yt-dlp (via child_process) | -| Transcription | Groq Whisper API / local Whisper | -| Real-time | Socket.io | -| State (Mobile) | Zustand | - -## Code Patterns - -### Backend Services - -```typescript -@Injectable() -export class TranscriptionService { - async createJob(dto: TranscribeRequestDto): Promise { - // Background processing with WebSocket updates - } -} -``` - -### Web (Svelte 5 Runes) - -```typescript -// Correct - Svelte 5 -let jobs = $state([]); -let activeJobs = $derived(jobs.filter((j) => j.status === 'active')); - -// Wrong - Old Svelte syntax -let jobs = []; -$: activeJobs = jobs.filter((j) => j.status === 'active'); -``` - -### Mobile (Zustand) - -```typescript -export const useJobStore = create((set) => ({ - jobs: [], - addJob: (job) => set((state) => ({ jobs: [...state.jobs, job] })), -})); -``` - -## Legacy Python Code - -The original Python implementation is preserved in `legacy/` for reference: - -- `transcriber_v4_parallel.py` - Main transcription logic -- `api_server.py` - FastAPI server (replaced by NestJS) -- `requirements.txt` - Python dependencies - -## Troubleshooting - -### yt-dlp not found - -```bash -# macOS -brew install yt-dlp - -# Linux -pip install yt-dlp -``` - -### Local Whisper not working - -```bash -# Install Whisper -pip install openai-whisper - -# Test -python3 -c "import whisper; print(whisper.available_models())" -``` - -### Backend can't start - -```bash -# Check port 3006 -lsof -i :3006 && kill -9 $(lsof -t -i:3006) - -# Check environment -cat apps/backend/.env -``` diff --git a/apps-archived/wisekeep/README.md b/apps-archived/wisekeep/README.md deleted file mode 100644 index b8b9eb689..000000000 --- a/apps-archived/wisekeep/README.md +++ /dev/null @@ -1,392 +0,0 @@ -# 🎥 YouTube Transcriber System - -Ein vollständiges System zur automatischen Transkription, Aufbereitung und Präsentation von YouTube-Videos mit OpenAI's Whisper, FastAPI Backend und Astro.js Frontend. - -## ✨ System-Komponenten - -### 🔧 Backend (Python) -- **OpenAI Whisper** - Lokale Speech-to-Text Transkription -- **FastAPI Server** - REST API für Web-Interface -- **Parallel Processing** - Bis zu 3.3x schnellere Verarbeitung -- **Playlist Management** - Automatische Batch-Verarbeitung - -### 🌐 Frontend (Astro.js) -- **Public Website** - Aufbereitete Vorträge als Wisdom Library -- **Admin Panel** - Transkriptions-Management (localhost only) -- **Content Collections** - Strukturierte Inhalte mit Markdown -- **Responsive Design** - Optimiert für alle Geräte - -## 🏗️ Architektur - -``` -YoutubeDL/ -├── 🐍 Python Backend -│ ├── transcriber_v4_parallel.py # Parallel-Verarbeitung -│ ├── api_server.py # FastAPI REST API -│ └── playlists/ # YouTube URL-Listen -├── 🌐 Website -│ ├── src/pages/ # Public & Admin Pages -│ ├── src/content/talks/ # Aufbereitete Vorträge -│ └── src/components/admin/ # Admin-Komponenten -└── 📂 Output - └── transcripts/ # Transkribierte Texte -``` - -## 🛠 Installation - -### Voraussetzungen - -- Python 3.10+ -- FFmpeg -- macOS (optimiert für Apple Silicon M1/M2) - -### Setup - -1. **Repository klonen:** -```bash -git clone https://github.com/yourusername/youtube-transcriber.git -cd youtube-transcriber -``` - -2. **Virtual Environment erstellen:** -```bash -python3 -m venv venv -source venv/bin/activate -``` - -3. **Dependencies installieren:** -```bash -pip install -r requirements.txt -``` - -## 🚀 Schnellstart - -### Kompletter Workflow: Von YouTube zu Website - -#### 1. Speaker Content sammeln - -Erstelle eine Playlist für einen Speaker (z.B. Simon Sinek): - -```bash -# playlists/people/simon-sinek.txt erstellen -# Simon Sinek Videos -# Popular talks and interviews from YouTube -# Created: 2025-09-09 - -# TED Talks -# How great leaders inspire action (Start with Why) - 60M+ views -https://www.youtube.com/watch?v=u4ZoJKF_VuA - -# Why good leaders make you feel safe - 18M+ views -https://www.youtube.com/watch?v=lmyZMtPVodo -``` - -#### 2. Videos transkribieren - -```bash -# Virtual Environment aktivieren -source venv/bin/activate - -# Parallel-Verarbeitung starten (3-4x schneller) -python3 transcriber_v4_parallel.py --playlist playlists/people/simon-sinek.txt --model base --language en -``` - -#### 3. Website Content erstellen - -**a) Content Schema erweitern** (wenn neue Kategorie): -```typescript -// website/src/content/config.ts -category: z.enum([ - 'behavioral-economics', - 'psychology', - 'leadership', // Neue Kategorie hinzufügen - // ... -]), -``` - -**b) Speaker Profil erstellen**: -```bash -# website/src/pages/speakers/simon-sinek.astro -``` - -**c) Talk-Seiten erstellen**: -```bash -# Für jedes erfolgreich transkribierte Video: -# website/src/content/talks/simon-sinek-[talk-slug].md -``` - -**d) SearchableContentList aktualisieren**: -```typescript -// website/src/components/SearchableContentList.tsx -// Neue Talks zur Inhaltsliste hinzufügen -``` - -#### 4. Website starten - -```bash -cd website -npm run dev -``` - -### Einzelnes Video transkribieren - -```bash -# Mit Large-Modell (beste Qualität) -python3 transcriber_v3.py process "https://www.youtube.com/watch?v=VIDEO_ID" --model large - -# Mit Tiny-Modell (schneller Test) -python3 transcriber_v3.py process "https://www.youtube.com/watch?v=VIDEO_ID" --model tiny -``` - -### Playlists verwalten - -1. **Playlist erstellen:** - - Erstelle eine `.txt` Datei im `playlists/` Ordner - - Füge YouTube-URLs ein (eine pro Zeile) - -```bash -# playlists/tech/python_tutorials.txt -https://www.youtube.com/watch?v=VIDEO_ID1 -https://www.youtube.com/watch?v=VIDEO_ID2 -``` - -2. **Alle Playlists scannen:** -```bash -python3 transcriber_v3.py scan --model large -``` - -3. **Spezifische Playlist verarbeiten:** -```bash -python3 transcriber_v3.py scan --playlist tech/python_tutorials -``` - -### Quick-Script verwenden - -```bash -./quick_transcribe.sh -``` - -Bietet ein interaktives Menü zur Modell-Auswahl. - -## 📂 Projektstruktur - -``` -YoutubeDL/ -├── playlists/ # YouTube URL-Listen nach Themen -│ ├── tech/ -│ │ └── python_tutorials.txt -│ ├── people/ -│ │ └── rory-sutherland.txt -│ └── musik/ -│ └── klassik.txt -├── transcripts/ # Transkribierte Texte (automatisch organisiert) -│ ├── tech_python_tutorials/ -│ │ └── [Kanal]/ -│ │ └── [Video]_[Timestamp].txt -│ └── people_rory-sutherland/ -│ └── TED/ -├── .cache/ # Cache für bereits verarbeitete Videos -├── temp_audio/ # Temporäre Audio-Dateien -├── venv/ # Python Virtual Environment -├── transcriber.py # v1: Basis-Funktionalität -├── transcriber_v2.py # v2: Mit Rich UI -├── transcriber_v3.py # v3: Mit Playlist-Management -└── quick_transcribe.sh # Schnellzugriff-Script -``` - -## 🎯 Whisper-Modelle - -| Modell | Größe | Geschwindigkeit | Genauigkeit | Verwendung | -|--------|-------|-----------------|-------------|------------| -| **tiny** | 39 MB | ~10x Echtzeit | 75% | Schnelle Tests | -| **base** | 74 MB | ~7x Echtzeit | 85% | Guter Kompromiss | -| **small** | 244 MB | ~4x Echtzeit | 91% | Solide Qualität | -| **medium** | 769 MB | ~2x Echtzeit | 94% | Hohe Qualität | -| **large** | 1.5 GB | ~1x Echtzeit | 96-98% | Beste Qualität | - -## 📋 Befehle - -### Hauptbefehle - -```bash -# Zeige alle Playlists -python3 transcriber_v3.py list - -# Verarbeite alle neuen Videos in allen Playlists -python3 transcriber_v3.py scan - -# Verarbeite einzelnes Video -python3 transcriber_v3.py process "URL" - -# Mit spezifischem Modell -python3 transcriber_v3.py scan --model large - -# Andere Sprache -python3 transcriber_v3.py scan --language en -``` - -### Optionen - -- `--model {tiny,base,small,medium,large}` - Whisper-Modell auswählen -- `--language LANG` - Sprache setzen (default: de) -- `--playlist NAME` - Spezifische Playlist verarbeiten -- `--output DIR` - Ausgabe-Verzeichnis (default: transcripts) -- `--force` - Cache ignorieren und neu transkribieren - -## 🔄 Automatisierung - -### Cron-Job einrichten - -Für tägliche automatische Verarbeitung: - -```bash -# Crontab öffnen -crontab -e - -# Täglich um 3 Uhr nachts alle Playlists scannen -0 3 * * * cd /path/to/YoutubeDL && source venv/bin/activate && python3 transcriber_v3.py scan --model large -``` - -## 💡 Tipps - -1. **Organisiere nach Themen**: Erstelle Unterordner in `playlists/` für verschiedene Themen -2. **Cache nutzen**: Das System merkt sich bereits transkribierte Videos automatisch -3. **Modell-Auswahl**: - - Nutze `tiny` für schnelle Tests - - Nutze `large` für wichtige Transkriptionen -4. **Batch-Verarbeitung**: Füge alle URLs zur Playlist hinzu und lasse über Nacht laufen - -## 🎨 Features im Detail - -### Rich Terminal UI (v2+) -- Farbige Ausgabe mit Emojis -- Progress Bars für Download und Transkription -- Zeitschätzungen basierend auf Video-Länge -- Video-Metadaten vor Download - -### Playlist-Management (v3) -- Automatisches Scannen von URL-Listen -- Themen-basierte Organisation -- Nur neue Videos werden verarbeitet -- Batch-Verarbeitung mehrerer Playlists - -### Cache-System -- Verhindert doppelte Verarbeitung -- Speichert Metadaten zu transkribierten Videos -- `.cache/transcribed_videos.json` enthält Historie - -## 🐛 Troubleshooting - -**FFmpeg nicht gefunden:** -```bash -# macOS -brew install ffmpeg -``` - -**Whisper-Modell lädt sehr lange:** -- Beim ersten Mal wird das Modell heruntergeladen -- Large: ~1.5GB, kann 10-30 Minuten dauern - -**"Video bereits transkribiert":** -- Nutze `--force` Flag zum Überschreiben -- Oder lösche `.cache/` Ordner für kompletten Reset - -## 📈 Performance (Apple Silicon M1) - -- **Tiny**: ~10x Echtzeit (6 Min Video → 36 Sek) -- **Base**: ~7x Echtzeit (6 Min Video → 50 Sek) -- **Small**: ~4x Echtzeit (6 Min Video → 1.5 Min) -- **Large**: ~1x Echtzeit (6 Min Video → 6 Min) - -## 🔒 Datenschutz - -- Alle Verarbeitung erfolgt **lokal** auf deinem Computer -- Keine Daten werden an externe Server gesendet -- Whisper läuft komplett offline - -## 📝 Lizenz - -MIT License - Siehe LICENSE Datei - -## 🙏 Credits - -- **OpenAI Whisper** - Speech-to-Text Engine -- **yt-dlp** - YouTube Download Tool -- **Rich** - Terminal UI Library -- **FFmpeg** - Audio/Video Verarbeitung - -## 🌐 Website Integration - -Das System generiert nicht nur Transkripte, sondern auch eine vollständige Website mit den aufbereiteten Inhalten. - -### Website-Features - -- **📚 Content Collections**: Strukturierte Talk-Seiten mit Markdown -- **🔍 Suchfunktion**: Volltextsuche über alle Talks -- **👤 Speaker Profile**: Übersichtsseiten für jeden Speaker -- **🏷️ Tag-System**: Kategorisierung nach Themen -- **📱 Responsive**: Optimiert für alle Geräte -- **🎨 Theming**: Verschiedene Farbschemata - -### Content-Struktur - -``` -website/src/ -├── content/ -│ ├── config.ts # Content Schema -│ └── talks/ # Aufbereitete Talk-Seiten -│ ├── simon-sinek-why-good-leaders-make-you-feel-safe.md -│ ├── simon-sinek-millennials-in-the-workplace.md -│ └── simon-sinek-love-your-work.md -├── pages/ -│ ├── speakers/ -│ │ ├── index.astro # Speaker-Übersicht -│ │ └── simon-sinek.astro # Speaker-Profile -│ └── talks/ -│ └── [slug].astro # Dynamische Talk-Seiten -└── components/ - ├── SearchableContentList.tsx # Hauptsuche - ├── ContentCard.tsx # Talk-Vorschau - └── speakers/ - ├── SpeakerHero.astro # Speaker-Header - ├── TalkGrid.astro # Talk-Grid - └── QuoteCollection.astro # Zitate-Sammlung -``` - -### Website entwickeln - -```bash -# Website Dependencies installieren -cd website -npm install - -# Entwicklungsserver starten -npm run dev - -# Website bauen für Produktion -npm run build -``` - -### Content-Erstellung Workflow - -1. **Transkription**: Videos mit Python-Backend transkribieren -2. **Content-Aufbereitung**: Markdown-Dateien mit Metadaten erstellen -3. **Speaker-Profile**: Übersichtsseiten für neue Speaker -4. **Integration**: Neue Inhalte in Suchfunktion einbinden -5. **Deployment**: Website bauen und deployen - -## 🚧 Roadmap - -- [x] **Parallel Processing** - 3-4x schnellere Transkription -- [x] **Website Integration** - Vollständige Content-Website -- [x] **Speaker Profiles** - Detaillierte Speaker-Übersichten -- [x] **Content Collections** - Strukturierte Talk-Aufbereitung -- [ ] **Admin Interface** - Web-UI für Transkriptions-Management -- [ ] **Speaker Diarization** - Wer spricht wann -- [ ] **Automatische Zusammenfassungen** - LLM-basierte Summaries -- [ ] **Export Formate** - SRT, VTT, JSON Export -- [ ] **YouTube Playlist Auto-Import** - Direkte Playlist-Integration - ---- - -**Entwickelt mit ❤️ für automatische Transkription** \ No newline at end of file diff --git a/apps-archived/wisekeep/apps/backend/.env.example b/apps-archived/wisekeep/apps/backend/.env.example deleted file mode 100644 index b68e539f9..000000000 --- a/apps-archived/wisekeep/apps/backend/.env.example +++ /dev/null @@ -1,14 +0,0 @@ -# Server -PORT=3006 - -# Whisper Configuration -WHISPER_PROVIDER=openai # openai or local -WHISPER_MODEL=base # tiny, base, small, medium, large (for local) - -# OpenAI API (for cloud transcription) -OPENAI_API_KEY=sk-your-openai-api-key - -# Directories -TEMP_AUDIO_DIR=./temp_audio -TRANSCRIPTS_DIR=./data/transcripts -PLAYLISTS_DIR=./data/playlists diff --git a/apps-archived/wisekeep/apps/backend/nest-cli.json b/apps-archived/wisekeep/apps/backend/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/apps-archived/wisekeep/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-archived/wisekeep/apps/backend/package.json b/apps-archived/wisekeep/apps/backend/package.json deleted file mode 100644 index cfe844f10..000000000 --- a/apps-archived/wisekeep/apps/backend/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "@wisekeep/backend", - "version": "1.0.0", - "private": true, - "description": "Wisekeep Backend - NestJS API for wisdom extraction", - "scripts": { - "dev": "nest start --watch", - "build": "nest build", - "start": "nest start", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "type-check": "tsc --noEmit", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage" - }, - "dependencies": { - "@nestjs/common": "^10.4.15", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.15", - "@nestjs/platform-express": "^10.4.15", - "@nestjs/platform-socket.io": "^10.4.15", - "@nestjs/websockets": "^10.4.15", - "@types/socket.io-client": "^3.0.0", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.1", - "openai": "^4.73.1", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "socket.io": "^4.8.1", - "uuid": "^11.0.3" - }, - "devDependencies": { - "@nestjs/cli": "^10.4.9", - "@nestjs/schematics": "^10.2.3", - "@nestjs/testing": "^10.4.15", - "@types/express": "^5.0.0", - "@types/jest": "^29.5.14", - "@types/node": "^22.10.1", - "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^8.17.0", - "@typescript-eslint/parser": "^8.17.0", - "eslint": "^9.16.0", - "jest": "^29.7.0", - "source-map-support": "^0.5.21", - "ts-jest": "^29.2.5", - "ts-loader": "^9.5.1", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.7.2" - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/app.module.ts b/apps-archived/wisekeep/apps/backend/src/app.module.ts deleted file mode 100644 index 7969c658b..000000000 --- a/apps-archived/wisekeep/apps/backend/src/app.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { TranscriptionModule } from './transcription/transcription.module'; -import { PlaylistModule } from './playlist/playlist.module'; -import { YoutubeModule } from './youtube/youtube.module'; -import { WhisperModule } from './whisper/whisper.module'; -import { WebsocketModule } from './websocket/websocket.module'; -import { HealthModule } from './health/health.module'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: '.env', - }), - TranscriptionModule, - PlaylistModule, - YoutubeModule, - WhisperModule, - WebsocketModule, - HealthModule, - ], -}) -export class AppModule {} diff --git a/apps-archived/wisekeep/apps/backend/src/health/health.controller.ts b/apps-archived/wisekeep/apps/backend/src/health/health.controller.ts deleted file mode 100644 index b8e78b4bf..000000000 --- a/apps-archived/wisekeep/apps/backend/src/health/health.controller.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -@Controller('health') -export class HealthController { - @Get() - check() { - return { - status: 'ok', - timestamp: new Date().toISOString(), - service: 'transcriber-backend', - version: '1.0.0', - }; - } - - @Get('ready') - ready() { - return { - status: 'ready', - timestamp: new Date().toISOString(), - }; - } - - @Get('live') - live() { - return { - status: 'alive', - timestamp: new Date().toISOString(), - }; - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/health/health.module.ts b/apps-archived/wisekeep/apps/backend/src/health/health.module.ts deleted file mode 100644 index a61d8b044..000000000 --- a/apps-archived/wisekeep/apps/backend/src/health/health.module.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Module } from '@nestjs/common'; -import { HealthController } from './health.controller'; - -@Module({ - controllers: [HealthController], -}) -export class HealthModule {} diff --git a/apps-archived/wisekeep/apps/backend/src/main.ts b/apps-archived/wisekeep/apps/backend/src/main.ts deleted file mode 100644 index e7bf684a6..000000000 --- a/apps-archived/wisekeep/apps/backend/src/main.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { ValidationPipe } from '@nestjs/common'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const app = await NestFactory.create(AppModule); - - app.enableCors({ - origin: [ - 'http://localhost:5173', // SvelteKit dev - 'http://localhost:4321', // Astro dev - 'http://localhost:3000', // Alternative dev - ], - credentials: true, - }); - - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - transform: true, - forbidNonWhitelisted: true, - }) - ); - - const port = process.env.PORT || 3006; - await app.listen(port); - - console.log(`[Transcriber Backend] Running on http://localhost:${port}`); -} - -bootstrap(); diff --git a/apps-archived/wisekeep/apps/backend/src/playlist/playlist.controller.ts b/apps-archived/wisekeep/apps/backend/src/playlist/playlist.controller.ts deleted file mode 100644 index fb0427753..000000000 --- a/apps-archived/wisekeep/apps/backend/src/playlist/playlist.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Controller, Get, Post, Delete, Param, Body } from '@nestjs/common'; -import { PlaylistService, CreatePlaylistDto } from './playlist.service'; - -@Controller('playlist') -export class PlaylistController { - constructor(private readonly playlistService: PlaylistService) {} - - @Get() - async getAll() { - return this.playlistService.getAll(); - } - - @Get(':category/:name') - async getOne(@Param('category') category: string, @Param('name') name: string) { - return this.playlistService.getOne(category, name); - } - - @Post() - async create(@Body() dto: CreatePlaylistDto) { - return this.playlistService.create(dto); - } - - @Delete(':category/:name') - async delete(@Param('category') category: string, @Param('name') name: string) { - await this.playlistService.delete(category, name); - return { message: 'Playlist deleted' }; - } - - @Post(':category/:name/url') - async addUrl( - @Param('category') category: string, - @Param('name') name: string, - @Body('url') url: string - ) { - return this.playlistService.addUrl(category, name, url); - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/playlist/playlist.module.ts b/apps-archived/wisekeep/apps/backend/src/playlist/playlist.module.ts deleted file mode 100644 index 17215f991..000000000 --- a/apps-archived/wisekeep/apps/backend/src/playlist/playlist.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { PlaylistController } from './playlist.controller'; -import { PlaylistService } from './playlist.service'; - -@Module({ - controllers: [PlaylistController], - providers: [PlaylistService], - exports: [PlaylistService], -}) -export class PlaylistModule {} diff --git a/apps-archived/wisekeep/apps/backend/src/playlist/playlist.service.ts b/apps-archived/wisekeep/apps/backend/src/playlist/playlist.service.ts deleted file mode 100644 index 884c726aa..000000000 --- a/apps-archived/wisekeep/apps/backend/src/playlist/playlist.service.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import * as fs from 'fs'; -import * as path from 'path'; - -export interface Playlist { - category: string; - name: string; - path: string; - urlCount: number; - urls: string[]; - description?: string; -} - -export interface CreatePlaylistDto { - name: string; - description?: string; - urls: string[]; -} - -@Injectable() -export class PlaylistService { - private readonly logger = new Logger(PlaylistService.name); - private readonly playlistsDir: string; - - constructor(private configService: ConfigService) { - this.playlistsDir = this.configService.get('PLAYLISTS_DIR') || './data/playlists'; - - // Ensure playlists directory exists - if (!fs.existsSync(this.playlistsDir)) { - fs.mkdirSync(this.playlistsDir, { recursive: true }); - } - } - - async getAll(): Promise { - const playlists: Playlist[] = []; - - if (!fs.existsSync(this.playlistsDir)) { - return playlists; - } - - const categories = fs - .readdirSync(this.playlistsDir, { withFileTypes: true }) - .filter((d) => d.isDirectory()); - - for (const category of categories) { - const categoryPath = path.join(this.playlistsDir, category.name); - const files = fs.readdirSync(categoryPath).filter((f) => f.endsWith('.txt')); - - for (const file of files) { - const filePath = path.join(categoryPath, file); - const content = fs.readFileSync(filePath, 'utf-8'); - const lines = content.split('\n'); - - let description: string | undefined; - const urls: string[] = []; - - for (const line of lines) { - const trimmed = line.trim(); - if (trimmed.startsWith('# ') && !description) { - description = trimmed.substring(2); - } else if (trimmed && !trimmed.startsWith('#')) { - urls.push(trimmed); - } - } - - playlists.push({ - category: category.name, - name: file.replace('.txt', ''), - path: filePath, - urlCount: urls.length, - urls, - description, - }); - } - } - - return playlists; - } - - async getOne(category: string, name: string): Promise { - const filePath = path.join(this.playlistsDir, category, `${name}.txt`); - - if (!fs.existsSync(filePath)) { - throw new NotFoundException(`Playlist ${category}/${name} not found`); - } - - const content = fs.readFileSync(filePath, 'utf-8'); - const lines = content.split('\n'); - - let description: string | undefined; - const urls: string[] = []; - - for (const line of lines) { - const trimmed = line.trim(); - if (trimmed.startsWith('# ') && !description) { - description = trimmed.substring(2); - } else if (trimmed && !trimmed.startsWith('#')) { - urls.push(trimmed); - } - } - - return { - category, - name, - path: filePath, - urlCount: urls.length, - urls, - description, - }; - } - - async create(dto: CreatePlaylistDto): Promise { - // Parse category/name format - const parts = dto.name.split('/'); - const category = parts.length > 1 ? parts[0] : 'general'; - const name = parts.length > 1 ? parts[1] : dto.name; - - const categoryDir = path.join(this.playlistsDir, category); - if (!fs.existsSync(categoryDir)) { - fs.mkdirSync(categoryDir, { recursive: true }); - } - - const filePath = path.join(categoryDir, `${name}.txt`); - - let content = ''; - if (dto.description) { - content += `# ${dto.description}\n`; - } - content += '# One URL per line\n\n'; - content += dto.urls.join('\n') + '\n'; - - fs.writeFileSync(filePath, content, 'utf-8'); - - this.logger.log(`Created playlist: ${category}/${name}`); - - return { - category, - name, - path: filePath, - urlCount: dto.urls.length, - urls: dto.urls, - description: dto.description, - }; - } - - async delete(category: string, name: string): Promise { - const filePath = path.join(this.playlistsDir, category, `${name}.txt`); - - if (!fs.existsSync(filePath)) { - throw new NotFoundException(`Playlist ${category}/${name} not found`); - } - - fs.unlinkSync(filePath); - this.logger.log(`Deleted playlist: ${category}/${name}`); - } - - async addUrl(category: string, name: string, url: string): Promise { - const playlist = await this.getOne(category, name); - playlist.urls.push(url); - - const content = - (playlist.description ? `# ${playlist.description}\n` : '') + - '# One URL per line\n\n' + - playlist.urls.join('\n') + - '\n'; - - fs.writeFileSync(playlist.path, content, 'utf-8'); - - playlist.urlCount = playlist.urls.length; - return playlist; - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/transcription/dto/transcribe-request.dto.ts b/apps-archived/wisekeep/apps/backend/src/transcription/dto/transcribe-request.dto.ts deleted file mode 100644 index 264e5adb5..000000000 --- a/apps-archived/wisekeep/apps/backend/src/transcription/dto/transcribe-request.dto.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IsString, IsOptional, IsUrl, IsEnum } from 'class-validator'; - -export enum WhisperProviderEnum { - GROQ = 'groq', - LOCAL = 'local', -} - -export enum WhisperModelEnum { - // Groq models (cloud) - WHISPER_LARGE_V3_TURBO = 'whisper-large-v3-turbo', - WHISPER_LARGE_V3 = 'whisper-large-v3', - // Local models - TINY = 'tiny', - BASE = 'base', - SMALL = 'small', - MEDIUM = 'medium', - LARGE = 'large', -} - -export class TranscribeRequestDto { - @IsUrl() - url: string; - - @IsString() - @IsOptional() - language?: string = 'de'; - - @IsEnum(WhisperProviderEnum) - @IsOptional() - provider?: WhisperProviderEnum; - - @IsEnum(WhisperModelEnum) - @IsOptional() - model?: WhisperModelEnum; -} diff --git a/apps-archived/wisekeep/apps/backend/src/transcription/entities/transcription-job.entity.ts b/apps-archived/wisekeep/apps/backend/src/transcription/entities/transcription-job.entity.ts deleted file mode 100644 index 4ccdf2dd8..000000000 --- a/apps-archived/wisekeep/apps/backend/src/transcription/entities/transcription-job.entity.ts +++ /dev/null @@ -1,46 +0,0 @@ -export enum JobStatus { - PENDING = 'pending', - DOWNLOADING = 'downloading', - TRANSCRIBING = 'transcribing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} - -export interface VideoInfo { - id: string; - title: string; - description: string; - duration: number; - channel: string; - channelId: string; - thumbnail: string; - uploadDate: string; -} - -export class TranscriptionJob { - id: string; - url: string; - language: string; - provider: string; - model?: string; - status: JobStatus; - progress: number; - createdAt: Date; - completedAt?: Date; - videoInfo?: VideoInfo; - transcriptPath?: string; - transcriptText?: string; - error?: string; - - constructor(id: string, url: string, language: string, provider: string, model?: string) { - this.id = id; - this.url = url; - this.language = language; - this.provider = provider; - this.model = model; - this.status = JobStatus.PENDING; - this.progress = 0; - this.createdAt = new Date(); - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/transcription/transcription.controller.ts b/apps-archived/wisekeep/apps/backend/src/transcription/transcription.controller.ts deleted file mode 100644 index 1304589f0..000000000 --- a/apps-archived/wisekeep/apps/backend/src/transcription/transcription.controller.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Controller, Get, Post, Delete, Param, Body } from '@nestjs/common'; -import { TranscriptionService } from './transcription.service'; -import { TranscribeRequestDto } from './dto/transcribe-request.dto'; - -@Controller('transcription') -export class TranscriptionController { - constructor(private readonly transcriptionService: TranscriptionService) {} - - @Post() - async createJob(@Body() dto: TranscribeRequestDto) { - return this.transcriptionService.createJob(dto); - } - - @Get() - async getAllJobs() { - return this.transcriptionService.getAllJobs(); - } - - @Get('stats') - async getStats() { - return this.transcriptionService.getStats(); - } - - @Get(':id') - async getJob(@Param('id') id: string) { - return this.transcriptionService.getJob(id); - } - - @Delete(':id') - async cancelJob(@Param('id') id: string) { - return this.transcriptionService.cancelJob(id); - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/transcription/transcription.module.ts b/apps-archived/wisekeep/apps/backend/src/transcription/transcription.module.ts deleted file mode 100644 index 45628a76d..000000000 --- a/apps-archived/wisekeep/apps/backend/src/transcription/transcription.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TranscriptionController } from './transcription.controller'; -import { TranscriptionService } from './transcription.service'; -import { YoutubeModule } from '../youtube/youtube.module'; -import { WhisperModule } from '../whisper/whisper.module'; -import { WebsocketModule } from '../websocket/websocket.module'; - -@Module({ - imports: [YoutubeModule, WhisperModule, WebsocketModule], - controllers: [TranscriptionController], - providers: [TranscriptionService], - exports: [TranscriptionService], -}) -export class TranscriptionModule {} diff --git a/apps-archived/wisekeep/apps/backend/src/transcription/transcription.service.ts b/apps-archived/wisekeep/apps/backend/src/transcription/transcription.service.ts deleted file mode 100644 index 2c5853eb4..000000000 --- a/apps-archived/wisekeep/apps/backend/src/transcription/transcription.service.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { v4 as uuidv4 } from 'uuid'; -import * as fs from 'fs'; -import * as path from 'path'; -import { YoutubeService } from '../youtube/youtube.service'; -import { WhisperService, WhisperProvider, WhisperModel } from '../whisper/whisper.service'; -import { ProgressGateway } from '../websocket/progress.gateway'; -import { TranscriptionJob, JobStatus } from './entities/transcription-job.entity'; -import { TranscribeRequestDto } from './dto/transcribe-request.dto'; - -@Injectable() -export class TranscriptionService { - private readonly logger = new Logger(TranscriptionService.name); - private readonly jobs: Map = new Map(); - private readonly transcriptsDir: string; - - constructor( - private readonly configService: ConfigService, - private readonly youtubeService: YoutubeService, - private readonly whisperService: WhisperService, - private readonly progressGateway: ProgressGateway - ) { - this.transcriptsDir = this.configService.get('TRANSCRIPTS_DIR') || './data/transcripts'; - - // Ensure transcripts directory exists - if (!fs.existsSync(this.transcriptsDir)) { - fs.mkdirSync(this.transcriptsDir, { recursive: true }); - } - } - - async createJob(dto: TranscribeRequestDto): Promise { - const jobId = uuidv4(); - const job = new TranscriptionJob( - jobId, - dto.url, - dto.language || 'de', - dto.provider || 'openai', - dto.model - ); - - this.jobs.set(jobId, job); - - // Start processing in background - this.processJob(job); - - return job; - } - - async getJob(id: string): Promise { - const job = this.jobs.get(id); - if (!job) { - throw new NotFoundException(`Job ${id} not found`); - } - return job; - } - - async getAllJobs(): Promise { - return Array.from(this.jobs.values()); - } - - async cancelJob(id: string): Promise { - const job = this.jobs.get(id); - if (!job) { - throw new NotFoundException(`Job ${id} not found`); - } - - if ( - job.status === JobStatus.PENDING || - job.status === JobStatus.DOWNLOADING || - job.status === JobStatus.TRANSCRIBING - ) { - job.status = JobStatus.CANCELLED; - job.error = 'Cancelled by user'; - - this.progressGateway.broadcastJobUpdate(job.id, { - status: job.status, - error: job.error, - }); - } - - return job; - } - - private async processJob(job: TranscriptionJob): Promise { - let audioPath: string | null = null; - const jobId = job.id; - - // Helper to check if job was cancelled (re-reads from map to get current status) - const isCancelled = (): boolean => { - const currentJob = this.jobs.get(jobId); - return currentJob?.status === JobStatus.CANCELLED; - }; - - try { - // Step 1: Get video info - this.updateJobProgress(job, JobStatus.DOWNLOADING, 5); - - const videoInfo = await this.youtubeService.getVideoInfo(job.url); - job.videoInfo = videoInfo; - this.updateJobProgress(job, JobStatus.DOWNLOADING, 10); - - this.logger.log(`Processing: ${videoInfo.title}`); - - // Check if cancelled - if (isCancelled()) return; - - // Step 2: Download audio - audioPath = await this.youtubeService.downloadAudio(job.url, (progress) => { - const overallProgress = 10 + progress.percent * 0.4; // 10-50% - this.updateJobProgress(job, JobStatus.DOWNLOADING, Math.round(overallProgress)); - }); - - this.updateJobProgress(job, JobStatus.DOWNLOADING, 50); - - // Check if cancelled - if (isCancelled()) { - if (audioPath) await this.youtubeService.cleanupFile(audioPath); - return; - } - - // Step 3: Transcribe - this.updateJobProgress(job, JobStatus.TRANSCRIBING, 55); - - const result = await this.whisperService.transcribe( - audioPath, - job.language, - job.provider as WhisperProvider, - job.model as WhisperModel - ); - - this.updateJobProgress(job, JobStatus.TRANSCRIBING, 90); - - // Check if cancelled - if (isCancelled()) { - if (audioPath) await this.youtubeService.cleanupFile(audioPath); - return; - } - - // Step 4: Save transcript - const transcriptPath = await this.saveTranscript(job, videoInfo, result.text); - - job.transcriptPath = transcriptPath; - job.transcriptText = result.text; - job.status = JobStatus.COMPLETED; - job.progress = 100; - job.completedAt = new Date(); - - this.progressGateway.broadcastJobUpdate(job.id, { - status: job.status, - progress: job.progress, - transcriptPath: job.transcriptPath, - }); - - this.logger.log(`Completed: ${videoInfo.title}`); - } catch (error) { - job.status = JobStatus.FAILED; - job.error = error instanceof Error ? error.message : 'Unknown error'; - - this.progressGateway.broadcastJobUpdate(job.id, { - status: job.status, - error: job.error, - }); - - this.logger.error(`Job failed: ${job.error}`); - } finally { - // Cleanup audio file - if (audioPath) { - await this.youtubeService.cleanupFile(audioPath); - } - } - } - - private updateJobProgress(job: TranscriptionJob, status: JobStatus, progress: number): void { - job.status = status; - job.progress = progress; - - this.progressGateway.broadcastJobUpdate(job.id, { - status: job.status, - progress: job.progress, - videoInfo: job.videoInfo, - }); - } - - private async saveTranscript( - job: TranscriptionJob, - videoInfo: { channel: string; title: string; id: string }, - text: string - ): Promise { - // Sanitize names for filesystem - const sanitize = (str: string) => str.replace(/[^a-z0-9äöüß\-_]/gi, '_').substring(0, 50); - - const channelDir = path.join(this.transcriptsDir, sanitize(videoInfo.channel)); - - if (!fs.existsSync(channelDir)) { - fs.mkdirSync(channelDir, { recursive: true }); - } - - const filename = `${sanitize(videoInfo.title)}_${videoInfo.id}.txt`; - const filePath = path.join(channelDir, filename); - - const content = `# ${videoInfo.title} -Channel: ${videoInfo.channel} -Video ID: ${videoInfo.id} -Language: ${job.language} -Transcribed: ${new Date().toISOString()} -Provider: ${job.provider} - ---- - -${text} -`; - - fs.writeFileSync(filePath, content, 'utf-8'); - - return filePath; - } - - async getStats() { - const jobs = Array.from(this.jobs.values()); - - let totalTranscripts = 0; - let totalSize = 0; - - if (fs.existsSync(this.transcriptsDir)) { - const countFiles = (dir: string) => { - const items = fs.readdirSync(dir, { withFileTypes: true }); - for (const item of items) { - const fullPath = path.join(dir, item.name); - if (item.isDirectory()) { - countFiles(fullPath); - } else if (item.name.endsWith('.txt')) { - totalTranscripts++; - totalSize += fs.statSync(fullPath).size; - } - } - }; - countFiles(this.transcriptsDir); - } - - return { - totalTranscripts, - totalSizeMB: Math.round((totalSize / 1024 / 1024) * 100) / 100, - activeJobs: jobs.filter( - (j) => - j.status === JobStatus.PENDING || - j.status === JobStatus.DOWNLOADING || - j.status === JobStatus.TRANSCRIBING - ).length, - completedJobs: jobs.filter((j) => j.status === JobStatus.COMPLETED).length, - failedJobs: jobs.filter((j) => j.status === JobStatus.FAILED).length, - }; - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/websocket/progress.gateway.ts b/apps-archived/wisekeep/apps/backend/src/websocket/progress.gateway.ts deleted file mode 100644 index f3e6a9d0c..000000000 --- a/apps-archived/wisekeep/apps/backend/src/websocket/progress.gateway.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - WebSocketGateway, - WebSocketServer, - OnGatewayConnection, - OnGatewayDisconnect, -} from '@nestjs/websockets'; -import { Logger } from '@nestjs/common'; -import { Server, Socket } from 'socket.io'; - -export interface JobUpdatePayload { - status: string; - progress?: number; - error?: string; - videoInfo?: { - id: string; - title: string; - channel: string; - thumbnail: string; - }; - transcriptPath?: string; -} - -@WebSocketGateway({ - cors: { - origin: ['http://localhost:5173', 'http://localhost:4321', 'http://localhost:3000'], - credentials: true, - }, - namespace: '/progress', -}) -export class ProgressGateway implements OnGatewayConnection, OnGatewayDisconnect { - private readonly logger = new Logger(ProgressGateway.name); - - @WebSocketServer() - server: Server; - - handleConnection(client: Socket) { - this.logger.log(`Client connected: ${client.id}`); - - // Send heartbeat every 10 seconds - const interval = setInterval(() => { - client.emit('heartbeat', { timestamp: Date.now() }); - }, 10000); - - client.on('disconnect', () => { - clearInterval(interval); - }); - } - - handleDisconnect(client: Socket) { - this.logger.log(`Client disconnected: ${client.id}`); - } - - broadcastJobUpdate(jobId: string, payload: JobUpdatePayload) { - this.server.emit('job_update', { - type: 'job_update', - jobId, - ...payload, - timestamp: Date.now(), - }); - } - - broadcastJobComplete(jobId: string, payload: JobUpdatePayload) { - this.server.emit('job_complete', { - type: 'job_complete', - jobId, - ...payload, - timestamp: Date.now(), - }); - } - - broadcastJobError(jobId: string, error: string) { - this.server.emit('job_error', { - type: 'job_error', - jobId, - error, - timestamp: Date.now(), - }); - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/websocket/websocket.module.ts b/apps-archived/wisekeep/apps/backend/src/websocket/websocket.module.ts deleted file mode 100644 index dd05f9c41..000000000 --- a/apps-archived/wisekeep/apps/backend/src/websocket/websocket.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { ProgressGateway } from './progress.gateway'; - -@Global() -@Module({ - providers: [ProgressGateway], - exports: [ProgressGateway], -}) -export class WebsocketModule {} diff --git a/apps-archived/wisekeep/apps/backend/src/whisper/whisper.controller.ts b/apps-archived/wisekeep/apps/backend/src/whisper/whisper.controller.ts deleted file mode 100644 index 66276691b..000000000 --- a/apps-archived/wisekeep/apps/backend/src/whisper/whisper.controller.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { WhisperService } from './whisper.service'; - -@Controller('whisper') -export class WhisperController { - constructor(private readonly whisperService: WhisperService) {} - - @Get('models') - getModels() { - return { - models: this.whisperService.getAvailableModels(), - defaultProvider: this.whisperService.getDefaultProvider(), - defaultModel: this.whisperService.getDefaultModel(), - groqAvailable: this.whisperService.isGroqAvailable(), - }; - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/whisper/whisper.module.ts b/apps-archived/wisekeep/apps/backend/src/whisper/whisper.module.ts deleted file mode 100644 index 479215503..000000000 --- a/apps-archived/wisekeep/apps/backend/src/whisper/whisper.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { WhisperService } from './whisper.service'; -import { WhisperController } from './whisper.controller'; - -@Module({ - controllers: [WhisperController], - providers: [WhisperService], - exports: [WhisperService], -}) -export class WhisperModule {} diff --git a/apps-archived/wisekeep/apps/backend/src/whisper/whisper.service.ts b/apps-archived/wisekeep/apps/backend/src/whisper/whisper.service.ts deleted file mode 100644 index d43014e22..000000000 --- a/apps-archived/wisekeep/apps/backend/src/whisper/whisper.service.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { spawn } from 'child_process'; -import * as fs from 'fs'; -import OpenAI from 'openai'; - -export type WhisperProvider = 'groq' | 'local'; -export type GroqWhisperModel = 'whisper-large-v3-turbo' | 'whisper-large-v3'; -export type LocalWhisperModel = 'tiny' | 'base' | 'small' | 'medium' | 'large'; -export type WhisperModel = GroqWhisperModel | LocalWhisperModel; - -export interface TranscriptionResult { - text: string; - language: string; - duration: number; - provider: WhisperProvider; -} - -export interface WhisperModelInfo { - name: string; - provider: WhisperProvider; - speed: string; - accuracy: string; - cost?: string; -} - -@Injectable() -export class WhisperService { - private readonly logger = new Logger(WhisperService.name); - private readonly groqClient: OpenAI | null; - private readonly defaultProvider: WhisperProvider; - private readonly defaultModel: WhisperModel; - - constructor(private configService: ConfigService) { - const groqApiKey = this.configService.get('GROQ_API_KEY'); - - if (groqApiKey) { - // Groq uses OpenAI-compatible API - this.groqClient = new OpenAI({ - apiKey: groqApiKey, - baseURL: 'https://api.groq.com/openai/v1', - }); - this.logger.log('Groq API configured successfully'); - } else { - this.groqClient = null; - this.logger.warn('Groq API key not configured. Only local Whisper available.'); - } - - this.defaultProvider = - (this.configService.get('WHISPER_PROVIDER') as WhisperProvider) || 'groq'; - this.defaultModel = - (this.configService.get('WHISPER_MODEL') as WhisperModel) || 'whisper-large-v3-turbo'; - } - - async transcribe( - audioPath: string, - language: string = 'de', - provider?: WhisperProvider, - model?: WhisperModel - ): Promise { - const selectedProvider = provider || this.defaultProvider; - const selectedModel = model || this.defaultModel; - - // Fallback to local if Groq not available - if (selectedProvider === 'groq' && !this.groqClient) { - this.logger.warn('Groq not configured, falling back to local Whisper'); - return this.transcribeWithLocalWhisper( - audioPath, - language, - selectedModel as LocalWhisperModel - ); - } - - if (selectedProvider === 'groq') { - return this.transcribeWithGroq(audioPath, language, selectedModel as GroqWhisperModel); - } - - return this.transcribeWithLocalWhisper(audioPath, language, selectedModel as LocalWhisperModel); - } - - private async transcribeWithGroq( - audioPath: string, - language: string, - model: GroqWhisperModel = 'whisper-large-v3-turbo' - ): Promise { - if (!this.groqClient) { - throw new Error('Groq API not configured'); - } - - this.logger.log(`Transcribing with Groq Whisper API (${model}): ${audioPath}`); - - const startTime = Date.now(); - - const transcription = await this.groqClient.audio.transcriptions.create({ - file: fs.createReadStream(audioPath), - model: model, - language, - response_format: 'verbose_json', - }); - - const duration = (Date.now() - startTime) / 1000; - - this.logger.log(`Groq transcription completed in ${duration.toFixed(2)}s`); - - return { - text: transcription.text, - language: transcription.language || language, - duration, - provider: 'groq', - }; - } - - private async transcribeWithLocalWhisper( - audioPath: string, - language: string, - model: WhisperModel - ): Promise { - this.logger.log(`Transcribing with local Whisper (model: ${model}): ${audioPath}`); - - const startTime = Date.now(); - - return new Promise((resolve, reject) => { - // Python script to run Whisper - const pythonScript = ` -import whisper -import json -import sys - -model = whisper.load_model("${model}") -result = model.transcribe("${audioPath}", language="${language}") -print(json.dumps({"text": result["text"], "language": result.get("language", "${language}")})) - `.trim(); - - const python = spawn('python3', ['-c', pythonScript]); - - let stdout = ''; - let stderr = ''; - - python.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - python.stderr.on('data', (data) => { - stderr += data.toString(); - // Whisper outputs progress to stderr, log it - this.logger.debug(data.toString()); - }); - - python.on('close', (code) => { - const duration = (Date.now() - startTime) / 1000; - - if (code !== 0) { - this.logger.error(`Local Whisper error: ${stderr}`); - reject(new Error(`Transcription failed: ${stderr}`)); - return; - } - - try { - const result = JSON.parse(stdout.trim()); - resolve({ - text: result.text, - language: result.language, - duration, - provider: 'local', - }); - } catch (e) { - reject(new Error('Failed to parse transcription result')); - } - }); - }); - } - - getAvailableModels(): WhisperModelInfo[] { - const models: WhisperModelInfo[] = []; - - // Groq models (cloud, ultra-fast) - if (this.groqClient) { - models.push( - { - name: 'whisper-large-v3-turbo', - provider: 'groq', - speed: '~300x realtime', - accuracy: '95%', - cost: '$0.04/hour', - }, - { - name: 'whisper-large-v3', - provider: 'groq', - speed: '~250x realtime', - accuracy: '97%', - cost: '$0.111/hour', - } - ); - } - - // Local models - models.push( - { name: 'tiny', provider: 'local', speed: '~10x realtime', accuracy: '75%' }, - { name: 'base', provider: 'local', speed: '~7x realtime', accuracy: '85%' }, - { name: 'small', provider: 'local', speed: '~4x realtime', accuracy: '91%' }, - { name: 'medium', provider: 'local', speed: '~2x realtime', accuracy: '94%' }, - { name: 'large', provider: 'local', speed: '~1x realtime', accuracy: '96-98%' } - ); - - return models; - } - - isGroqAvailable(): boolean { - return this.groqClient !== null; - } - - getDefaultProvider(): WhisperProvider { - return this.defaultProvider; - } - - getDefaultModel(): WhisperModel { - return this.defaultModel; - } -} diff --git a/apps-archived/wisekeep/apps/backend/src/youtube/youtube.module.ts b/apps-archived/wisekeep/apps/backend/src/youtube/youtube.module.ts deleted file mode 100644 index 7c175229a..000000000 --- a/apps-archived/wisekeep/apps/backend/src/youtube/youtube.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { YoutubeService } from './youtube.service'; - -@Module({ - providers: [YoutubeService], - exports: [YoutubeService], -}) -export class YoutubeModule {} diff --git a/apps-archived/wisekeep/apps/backend/src/youtube/youtube.service.ts b/apps-archived/wisekeep/apps/backend/src/youtube/youtube.service.ts deleted file mode 100644 index adbe06823..000000000 --- a/apps-archived/wisekeep/apps/backend/src/youtube/youtube.service.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { spawn } from 'child_process'; -import * as path from 'path'; -import * as fs from 'fs'; -import { v4 as uuidv4 } from 'uuid'; - -export interface VideoInfo { - id: string; - title: string; - description: string; - duration: number; - channel: string; - channelId: string; - thumbnail: string; - uploadDate: string; -} - -export interface DownloadProgress { - percent: number; - speed: string; - eta: string; -} - -@Injectable() -export class YoutubeService { - private readonly logger = new Logger(YoutubeService.name); - private readonly tempDir: string; - - constructor(private configService: ConfigService) { - this.tempDir = this.configService.get('TEMP_AUDIO_DIR') || './temp_audio'; - - // Ensure temp directory exists - if (!fs.existsSync(this.tempDir)) { - fs.mkdirSync(this.tempDir, { recursive: true }); - } - } - - async getVideoInfo(url: string): Promise { - return new Promise((resolve, reject) => { - const ytdlp = spawn('yt-dlp', ['--dump-json', '--no-download', url]); - - let stdout = ''; - let stderr = ''; - - ytdlp.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - ytdlp.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - ytdlp.on('close', (code) => { - if (code !== 0) { - this.logger.error(`yt-dlp info error: ${stderr}`); - reject(new Error(`Failed to get video info: ${stderr}`)); - return; - } - - try { - const info = JSON.parse(stdout); - resolve({ - id: info.id, - title: info.title, - description: info.description || '', - duration: info.duration, - channel: info.channel || info.uploader, - channelId: info.channel_id || info.uploader_id, - thumbnail: info.thumbnail, - uploadDate: info.upload_date, - }); - } catch (e) { - reject(new Error('Failed to parse video info')); - } - }); - }); - } - - async downloadAudio( - url: string, - onProgress?: (progress: DownloadProgress) => void - ): Promise { - const outputId = uuidv4(); - const outputPath = path.join(this.tempDir, `${outputId}.mp3`); - - return new Promise((resolve, reject) => { - const ytdlp = spawn('yt-dlp', [ - '-x', - '--audio-format', - 'mp3', - '--audio-quality', - '0', - '-o', - outputPath.replace('.mp3', '.%(ext)s'), - '--newline', - url, - ]); - - let stderr = ''; - - ytdlp.stdout.on('data', (data) => { - const line = data.toString(); - - // Parse download progress - const progressMatch = line.match(/(\d+\.?\d*)%.*?(\d+\.?\d*\w+\/s).*?ETA\s+(\d+:\d+)/); - if (progressMatch && onProgress) { - onProgress({ - percent: parseFloat(progressMatch[1]), - speed: progressMatch[2], - eta: progressMatch[3], - }); - } - }); - - ytdlp.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - ytdlp.on('close', (code) => { - if (code !== 0) { - this.logger.error(`yt-dlp download error: ${stderr}`); - reject(new Error(`Download failed: ${stderr}`)); - return; - } - - // Find the actual output file (might have different extension initially) - const files = fs.readdirSync(this.tempDir); - const outputFile = files.find((f) => f.startsWith(outputId)); - - if (!outputFile) { - reject(new Error('Output file not found')); - return; - } - - const actualPath = path.join(this.tempDir, outputFile); - this.logger.log(`Downloaded audio to: ${actualPath}`); - resolve(actualPath); - }); - }); - } - - async cleanupFile(filePath: string): Promise { - try { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - this.logger.log(`Cleaned up: ${filePath}`); - } - } catch (e) { - this.logger.warn(`Failed to cleanup file: ${filePath}`); - } - } - - isValidYoutubeUrl(url: string): boolean { - const patterns = [ - /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\//, - /^(https?:\/\/)?(www\.)?youtube\.com\/watch\?v=/, - /^(https?:\/\/)?youtu\.be\//, - ]; - - return patterns.some((pattern) => pattern.test(url)); - } -} diff --git a/apps-archived/wisekeep/apps/backend/tsconfig.json b/apps-archived/wisekeep/apps/backend/tsconfig.json deleted file mode 100644 index f9f2f3ddd..000000000 --- a/apps-archived/wisekeep/apps/backend/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/apps-archived/wisekeep/apps/landing/src/components/admin/Dashboard.tsx b/apps-archived/wisekeep/apps/landing/src/components/admin/Dashboard.tsx deleted file mode 100644 index 4fa83e9db..000000000 --- a/apps-archived/wisekeep/apps/landing/src/components/admin/Dashboard.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { createSignal, createEffect, onMount, For } from 'solid-js'; - -interface Job { - id: string; - url: string; - status: string; - progress: number; - created_at: string; - video_info: any; -} - -interface Stats { - total_transcripts: number; - total_size_mb: number; - active_jobs: number; - completed_jobs: number; - failed_jobs: number; -} - -const API_URL = 'http://localhost:8000'; - -export default function Dashboard() { - const [jobs, setJobs] = createSignal([]); - const [stats, setStats] = createSignal(null); - const [newUrl, setNewUrl] = createSignal(''); - const [selectedModel, setSelectedModel] = createSignal('base'); - const [isLoading, setIsLoading] = createSignal(false); - const [ws, setWs] = createSignal(null); - - onMount(() => { - fetchJobs(); - fetchStats(); - connectWebSocket(); - }); - - const connectWebSocket = () => { - const websocket = new WebSocket(`ws://localhost:8000/ws/progress`); - - websocket.onopen = () => { - console.log('WebSocket connected'); - }; - - websocket.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data.type === 'job_update' || data.type === 'job_complete') { - fetchJobs(); - fetchStats(); - } - }; - - websocket.onerror = (error) => { - console.error('WebSocket error:', error); - }; - - setWs(websocket); - }; - - const fetchJobs = async () => { - try { - const response = await fetch(`${API_URL}/api/jobs`); - const data = await response.json(); - setJobs(data); - } catch (error) { - console.error('Error fetching jobs:', error); - } - }; - - const fetchStats = async () => { - try { - const response = await fetch(`${API_URL}/api/stats`); - const data = await response.json(); - setStats(data); - } catch (error) { - console.error('Error fetching stats:', error); - } - }; - - const startTranscription = async () => { - if (!newUrl()) return; - - setIsLoading(true); - try { - const response = await fetch(`${API_URL}/api/transcribe`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - url: newUrl(), - model: selectedModel(), - language: 'de', - }), - }); - - if (response.ok) { - setNewUrl(''); - fetchJobs(); - fetchStats(); - } - } catch (error) { - console.error('Error starting transcription:', error); - } - setIsLoading(false); - }; - - const getStatusColor = (status: string) => { - switch (status) { - case 'pending': - return 'text-yellow-400'; - case 'downloading': - return 'text-blue-400'; - case 'transcribing': - return 'text-purple-400'; - case 'completed': - return 'text-green-400'; - case 'failed': - return 'text-red-400'; - default: - return 'text-gray-400'; - } - }; - - const getStatusIcon = (status: string) => { - switch (status) { - case 'pending': - return '⏳'; - case 'downloading': - return '⬇️'; - case 'transcribing': - return '🎙️'; - case 'completed': - return '✅'; - case 'failed': - return '❌'; - default: - return '❓'; - } - }; - - return ( -
- {/* Stats Cards */} -
-
-
{stats()?.total_transcripts || 0}
-
Transkripte
-
-
-
{stats()?.total_size_mb || 0} MB
-
Speicher
-
-
-
{stats()?.active_jobs || 0}
-
Aktiv
-
-
-
{stats()?.completed_jobs || 0}
-
Fertig
-
-
-
{stats()?.failed_jobs || 0}
-
Fehler
-
-
- - {/* New Transcription Form */} -
-

Neue Transkription

-
- setNewUrl(e.currentTarget.value)} - placeholder="YouTube URL eingeben..." - class="flex-1 px-4 py-2 bg-gray-700 text-white rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - - -
-
- - {/* Active Jobs */} -
-

Aktive Jobs

-
- - {(job) => ( -
-
-
- {getStatusIcon(job.status)} - - {job.status.toUpperCase()} - -
-
- {new Date(job.created_at).toLocaleString('de-DE')} -
-
-
{job.url}
- {job.status !== 'completed' && job.status !== 'failed' && ( -
-
-
- )} -
- )} - - {jobs().length === 0 && ( -
Keine aktiven Jobs
- )} -
-
-
- ); -} diff --git a/apps-archived/wisekeep/apps/landing/src/components/admin/PlaylistManager.tsx b/apps-archived/wisekeep/apps/landing/src/components/admin/PlaylistManager.tsx deleted file mode 100644 index 1cd3dbecf..000000000 --- a/apps-archived/wisekeep/apps/landing/src/components/admin/PlaylistManager.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { createSignal, createEffect, onMount, For, Show } from 'solid-js'; - -interface Playlist { - category: string; - name: string; - path: string; - url_count: number; - urls: string[]; -} - -const API_URL = 'http://localhost:8000'; - -export default function PlaylistManager() { - const [playlists, setPlaylists] = createSignal([]); - const [selectedPlaylist, setSelectedPlaylist] = createSignal(null); - const [newPlaylistName, setNewPlaylistName] = createSignal(''); - const [newPlaylistCategory, setNewPlaylistCategory] = createSignal('general'); - const [newUrls, setNewUrls] = createSignal(''); - const [isCreating, setIsCreating] = createSignal(false); - const [isProcessing, setIsProcessing] = createSignal(false); - - onMount(() => { - fetchPlaylists(); - }); - - const fetchPlaylists = async () => { - try { - const response = await fetch(`${API_URL}/api/playlists`); - const data = await response.json(); - setPlaylists(data); - } catch (error) { - console.error('Error fetching playlists:', error); - } - }; - - const createPlaylist = async () => { - if (!newPlaylistName() || !newUrls()) return; - - try { - const urls = newUrls() - .split('\n') - .filter((url) => url.trim()); - const name = - newPlaylistCategory() === 'general' - ? newPlaylistName() - : `${newPlaylistCategory()}/${newPlaylistName()}`; - - const response = await fetch(`${API_URL}/api/playlists`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: name, - urls: urls, - }), - }); - - if (response.ok) { - setNewPlaylistName(''); - setNewUrls(''); - setIsCreating(false); - fetchPlaylists(); - } - } catch (error) { - console.error('Error creating playlist:', error); - } - }; - - const processPlaylist = async (playlist: Playlist) => { - setIsProcessing(true); - try { - // Process each URL in the playlist - for (const url of playlist.urls) { - await fetch(`${API_URL}/api/transcribe`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - url: url, - model: 'large', - language: 'de', - }), - }); - } - alert(`Started processing ${playlist.url_count} videos from ${playlist.name}`); - } catch (error) { - console.error('Error processing playlist:', error); - } - setIsProcessing(false); - }; - - const getCategoryColor = (category: string) => { - const colors: { [key: string]: string } = { - tech: 'bg-blue-900', - people: 'bg-purple-900', - musik: 'bg-pink-900', - gaming: 'bg-green-900', - general: 'bg-gray-800', - }; - return colors[category] || 'bg-gray-800'; - }; - - const getCategoryIcon = (category: string) => { - const icons: { [key: string]: string } = { - tech: '💻', - people: '👥', - musik: '🎵', - gaming: '🎮', - general: '📁', - }; - return icons[category] || '📁'; - }; - - return ( -
- {/* Header with Create Button */} -
-

Playlists

- -
- - {/* Create New Playlist Form */} - -
-

Neue Playlist erstellen

-
-
- - setNewPlaylistName(e.currentTarget.value)} - placeholder="Playlist Name..." - class="flex-1 px-4 py-2 bg-gray-700 text-white rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
-