From 89e61a508e397084b551a845fa29bd127dc4edf2 Mon Sep 17 00:00:00 2001 From: Vasyl Vdovychenko Date: Sun, 4 Oct 2026 15:27:44 -0400 Subject: [PATCH] chore: delete podcast feature Worker + script builder + ffmpeg audio assembly, API/admin endpoints, podcast.script route/eval/seed, web player, admin Generate button. DropPodcast migration drops podcast_generation_jobs. ffmpeg and TTS registration removed from Worker. Stored mp3s untouched. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + apps/admin/src/api/client.ts | 26 - apps/admin/src/pages/EditEditionPage.tsx | 65 +- apps/web/src/App.tsx | 1 - apps/web/src/api/client.ts | 4 - apps/web/src/components/BookPodcastPlayer.tsx | 167 - apps/web/src/pages/BookDetailPage.tsx | 22 +- apps/web/src/styles/podcast-player.css | 233 - apps/web/src/types/api.ts | 7 - backend/Docker/Worker.Dockerfile | 1 - .../Datasets/podcast.json | 32 - .../TextStack.Ai.EvalSuite/EvalDefinitions.cs | 19 +- .../TextStack.Ai.EvalSuite/PodcastGolden.cs | 6 - backend/src/Ai/TextStack.Ai.Llm/RouteAlarm.cs | 2 +- backend/src/Api/Endpoints/PodcastEndpoints.cs | 125 - ...ServiceCollectionExtensions.Persistence.cs | 4 +- backend/src/Api/Program.cs | 1 - backend/src/Api/appsettings.json | 5 +- backend/src/Application/Ai/PodcastPrompt.cs | 41 - .../Common/Interfaces/IAppDbContext.cs | 1 - .../src/Application/DependencyInjection.cs | 2 +- .../src/Application/LLM/LlmServiceFactory.cs | 1 - .../Domain/Entities/PodcastGenerationJob.cs | 33 - backend/src/Domain/Enums/PodcastJobStatus.cs | 10 - .../20261004192449_DropPodcast.Designer.cs | 5663 +++++++++++++++++ .../Migrations/20261004192449_DropPodcast.cs | 60 + .../Migrations/AppDbContextModelSnapshot.cs | 77 - .../Persistence/AppDbContext.Podcasts.cs | 25 - .../Persistence/AppDbContext.cs | 2 - .../Persistence/ModelRegistrySeeder.cs | 1 - backend/src/Worker/Program.cs | 7 - backend/src/Worker/Services/AudioAssembler.cs | 122 - .../Worker/Services/PodcastScriptBuilder.cs | 106 - backend/src/Worker/Services/PodcastWorker.cs | 117 - backend/src/Worker/Worker.csproj | 1 - backend/src/Worker/appsettings.json | 5 +- docker-compose.yml | 5 +- docs/01-architecture/README.md | 8 +- docs/01-architecture/data-model.md | 1 - docs/02-system/database.md | 4 +- docs/05-features/assistant-handoff.md | 2 +- tests/TextStack.AiEvals/CapturingDb.cs | 1 - tests/TextStack.UnitTests/AiRouteMapTests.cs | 1 - .../AudioAssemblerTests.cs | 28 - .../Fakes/FakeAppDbContext.cs | 1 - .../ModelPromotionServiceTests.cs | 2 +- .../PodcastScriptParseTests.cs | 58 - 47 files changed, 5745 insertions(+), 1361 deletions(-) delete mode 100644 apps/web/src/components/BookPodcastPlayer.tsx delete mode 100644 apps/web/src/styles/podcast-player.css delete mode 100644 backend/src/Ai/TextStack.Ai.EvalSuite/Datasets/podcast.json delete mode 100644 backend/src/Ai/TextStack.Ai.EvalSuite/PodcastGolden.cs delete mode 100644 backend/src/Api/Endpoints/PodcastEndpoints.cs delete mode 100644 backend/src/Application/Ai/PodcastPrompt.cs delete mode 100644 backend/src/Domain/Entities/PodcastGenerationJob.cs delete mode 100644 backend/src/Domain/Enums/PodcastJobStatus.cs create mode 100644 backend/src/Infrastructure/Migrations/20261004192449_DropPodcast.Designer.cs create mode 100644 backend/src/Infrastructure/Migrations/20261004192449_DropPodcast.cs delete mode 100644 backend/src/Infrastructure/Persistence/AppDbContext.Podcasts.cs delete mode 100644 backend/src/Worker/Services/AudioAssembler.cs delete mode 100644 backend/src/Worker/Services/PodcastScriptBuilder.cs delete mode 100644 backend/src/Worker/Services/PodcastWorker.cs delete mode 100644 tests/TextStack.UnitTests/AudioAssemblerTests.cs delete mode 100644 tests/TextStack.UnitTests/PodcastScriptParseTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e8d47b42..1dbe270af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ the archive; if it broke production, it belongs in `docs/incidents/`. See - **Ops** — deploys no longer run a full SSG rebuild (~25 min of CPU, held the only runner): it runs nightly after the backup; `rebuild_ssg` input on a manual deploy for releases that change SEO rendering; health check alarms at 36h stale (was 72h) — infra - **Security** — hardening from the architecture review: stored files are served with a sandbox CSP + nosniff (nginx and the API), `/internal/*` is refused at nginx and its network check is one tested helper, two copyrighted test PDFs removed (image-only test now uses a generated PDF) — backend, infra - **Security** — connect keys get the same account-route scope as OAuth tokens; per-IP rate limits group IPv6 by /64; nginx rate-limit zones key on the client address — api, infra +- **Cleanup** — podcast feature deleted: worker, script builder, ffmpeg audio assembly, `/books/{slug}/podcast` + `/admin/podcasts` endpoints, `podcast.script` route + eval, web book-page player, admin Generate button; `podcast_generation_jobs` dropped (`DropPodcast`), ffmpeg removed from the Worker image; stored mp3s left on disk — backend, admin, web - **Discover** — the "Learn a language by reading real books" card no longer fills a small screen: it scrolls with the page under the search box, and a × hides it for good (`onboarding.startReadingCard.dismissed`) — closed-test report, Unihertz Titan 2 — mobile - **Docs** — docs checked against the code before the architecture review: STATUS, CLAUDE.md, architecture + system docs, ADR status lines, feature/ops/dev docs; `[Unreleased]` cut into deploy-date headings; stale plans marked historical — docs - **Ops** — health check hits `/api/health` and asserts `"healthy"` (`/health` was the SPA's 200); `pg_dump | gzip` under pipefail + dump-trailer check (backup, pre-deploy); daily disk alarm at 85% (`/`, docker root); deploy polls the API instead of `sleep 30` and fails on unhealthy services; host Claude CLI pollers run from a temp dir with `--tools ""`; deploy restarts `quality-poller` — infra diff --git a/apps/admin/src/api/client.ts b/apps/admin/src/api/client.ts index 014f86534..4075a7903 100644 --- a/apps/admin/src/api/client.ts +++ b/apps/admin/src/api/client.ts @@ -1,8 +1,5 @@ export const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080' -/// Origin that serves /storage (strip a trailing /api so podcast mp3 urls resolve). -export const mediaBase = API_BASE.replace(/\/api\/?$/, '') - export interface IngestionJob { id: string editionId: string @@ -443,12 +440,6 @@ export interface EvalStatus { startedAt: string | null lastError: string | null } -export interface PodcastStatusDto { - jobId: string - status: 'Queued' | 'Running' | 'Succeeded' | 'Failed' - audioUrl: string | null - durationSeconds: number | null -} export interface AiQualitySummary { from: string to: string @@ -1402,23 +1393,6 @@ export const adminApi = { }) }, - // Podcasts - generatePodcast: async (editionId: string, lang?: string, force?: boolean): Promise => { - return fetchJson('/admin/podcasts', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ editionId, lang, force }), - }) - }, - - getPodcastStatus: async (editionId: string): Promise => { - try { - return await fetchJson(`/admin/podcasts/${editionId}`) - } catch { - return null // 404 = not generated yet - } - }, - // ── SEO Backfill ── getSeoCoverage: async (): Promise => { return fetchJson('/admin/seo/coverage') diff --git a/apps/admin/src/pages/EditEditionPage.tsx b/apps/admin/src/pages/EditEditionPage.tsx index 90e310825..8b34a5777 100644 --- a/apps/admin/src/pages/EditEditionPage.tsx +++ b/apps/admin/src/pages/EditEditionPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, FormEvent } from 'react' import { useParams, useNavigate, Link } from 'react-router-dom' -import { adminApi, EditionDetail, PodcastStatusDto, mediaBase } from '../api/client' +import { adminApi, EditionDetail } from '../api/client' import { AuthorAutocomplete } from '../components/AuthorAutocomplete' import { AuthorList, AuthorItem } from '../components/AuthorList' import { CreateAuthorModal } from '../components/CreateAuthorModal' @@ -24,8 +24,6 @@ export function EditEditionPage() { const [uploadingCover, setUploadingCover] = useState(false) const [deletingCover, setDeletingCover] = useState(false) const [error, setError] = useState(null) - const [podcast, setPodcast] = useState(null) - const [podcastBusy, setPodcastBusy] = useState(false) const [title, setTitle] = useState('') const [authors, setAuthors] = useState([]) @@ -126,35 +124,6 @@ export function EditEditionPage() { setNewAuthorName('') } - // Podcast: load latest status on mount, poll while generating. - useEffect(() => { - if (!id) return - let active = true - adminApi.getPodcastStatus(id).then((p) => { if (active) setPodcast(p) }) - return () => { active = false } - }, [id]) - - useEffect(() => { - if (!id || (podcast?.status !== 'Queued' && podcast?.status !== 'Running')) return - const timer = setInterval(async () => { - const p = await adminApi.getPodcastStatus(id) - setPodcast(p) - }, 3000) - return () => clearInterval(timer) - }, [id, podcast?.status]) - - const handleGeneratePodcast = async (force = false) => { - if (!id) return - setPodcastBusy(true) - try { - setPodcast(await adminApi.generatePodcast(id, undefined, force)) - } catch (err) { - alert(err instanceof Error ? err.message : 'Failed to start podcast') - } finally { - setPodcastBusy(false) - } - } - const handlePublish = async () => { if (!id) return try { @@ -476,38 +445,6 @@ export function EditEditionPage() { -
-

Podcast

-
- - {(podcast?.status === 'Succeeded' || podcast?.status === 'Failed') && ( - - )} -
- {podcast && ( -

- Status: {podcast.status} - {podcast.durationSeconds ? ` · ${podcast.durationSeconds}s` : ''} -

- )} - {podcast?.status === 'Succeeded' && podcast.audioUrl && ( -
- {showCreateModal && ( (`${langPrefix}/books/${slug}`) }, - getPodcast: (slug: string) => { - return fetchJson(`${langPrefix}/books/${slug}/podcast`) - }, - getChapter: (bookSlug: string, chapterSlug: string) => { return fetchJson( `${langPrefix}/books/${bookSlug}/chapters/${chapterSlug}` diff --git a/apps/web/src/components/BookPodcastPlayer.tsx b/apps/web/src/components/BookPodcastPlayer.tsx deleted file mode 100644 index 3108cb819..000000000 --- a/apps/web/src/components/BookPodcastPlayer.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react' - -interface BookPodcastPlayerProps { - src: string - title: string - coverUrl?: string | null - /** Server-reported length; used as a fallback until the media metadata loads. */ - durationSeconds?: number | null -} - -const SPEEDS = [1, 1.25, 1.5, 1.75, 2] as const - -function fmt(sec: number): string { - if (!Number.isFinite(sec) || sec < 0) return '0:00' - const m = Math.floor(sec / 60) - const s = Math.floor(sec % 60) - return `${m}:${s.toString().padStart(2, '0')}` -} - -/** - * Custom, on-brand audio player for a book's AI-narrated intro — replaces the raw - * browser