diff --git a/Dockerfile b/Dockerfile index 180df1e3..af5aac4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,9 @@ FROM node:20-alpine AS builder ARG VITE_PROXY_SERVER ENV VITE_PROXY_SERVER=${VITE_PROXY_SERVER} +ARG VITE_READ_ALOUD_TTS_URL +ENV VITE_READ_ALOUD_TTS_URL=${VITE_READ_ALOUD_TTS_URL} + WORKDIR /app # Copy package files first diff --git a/PROXY_SETUP.md b/PROXY_SETUP.md index 550ad8e3..39110f61 100644 --- a/PROXY_SETUP.md +++ b/PROXY_SETUP.md @@ -97,6 +97,28 @@ docker-compose up -d - What URL is being used to fetch metadata - Any errors (CORS, network, etc.) +## Read-aloud / Piper TTS (same-origin `/api/piper-tts`) + +The client uses **`POST /api/piper-tts`** on the **same host** as the app (default build: `VITE_READ_ALOUD_TTS_URL=/api/piper-tts`) so the browser does not need cross-origin CORS to aitherboard. + +Add these **before** the catch-all `ProxyPass /` to the Jumble static container (same ordering as `/sites/`): + +```apache +ProxyPass /api/piper-tts http://127.0.0.1:9876/api/piper-tts +ProxyPassReverse /api/piper-tts http://127.0.0.1:9876/api/piper-tts +``` + +Use the port where **aitherboard** listens (example: `9876`). Reload Apache, then test: + +```bash +curl -sS -o /tmp/t.wav -w "%{http_code}\n" -H "Content-Type: application/json" \ + -d '{"text":"test","speed":1}' "https://jumble.imwald.eu/api/piper-tts" +``` + +Expect **200** and a WAV file. **Local dev:** `npm run dev` proxies `/api/piper-tts` → `http://127.0.0.1:9876` in `vite.config.ts`. + +Rebuild the Jumble image after changing `VITE_READ_ALOUD_TTS_URL`; `Dockerfile` passes `ARG`/`ENV` `VITE_READ_ALOUD_TTS_URL` into `npm run build`. + ## Update Proxy Server's ALLOW_ORIGIN Since users access via `https://jumble.imwald.eu`, you need to update the proxy server's `ALLOW_ORIGIN`: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 2556314b..dae10a22 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,10 +1,12 @@ # Minimal compose for running the published image (e.g. on remote server). # Usage: docker compose -f docker-compose.prod.yml pull && docker compose -f docker-compose.prod.yml up -d # -# Apache (unchanged on your host) should keep: +# Apache (unchanged on your host) should keep (order: specific paths before catch-all /): # ProxyPass /sites/ http://127.0.0.1:8090/sites/ +# ProxyPass /api/piper-tts http://127.0.0.1:9876/api/piper-tts +# ProxyPassReverse /api/piper-tts http://127.0.0.1:9876/api/piper-tts # ProxyPass / http://127.0.0.1:8089/ -# so the browser hits https:///sites/?url=… on Apache → OG proxy (8090); static SPA is 8089 only. +# so the browser hits same-origin /api/piper-tts → aitherboard; /sites/ → OG proxy; else static SPA on 8089. # VITE_PROXY_SERVER / VITE_READ_ALOUD_TTS_URL are baked at image build — see scripts/build-and-push-prod.sh # # NIP-66 monitor: set NIP66_MONITOR_NSEC (and optionally NIP66_MONITOR_NPUB) in the host env or .env. diff --git a/scripts/build-and-push-prod.sh b/scripts/build-and-push-prod.sh index 7af5fe77..5c2ed9cc 100755 --- a/scripts/build-and-push-prod.sh +++ b/scripts/build-and-push-prod.sh @@ -6,7 +6,8 @@ # Optional env: # JUMBLE_PROXY_SERVER_URL — build-arg VITE_PROXY_SERVER (default https://jumble.imwald.eu). # Must match the public origin where Apache serves the app; Apache proxies /sites/ → :8090, not this container. -# READ_ALOUD_TTS_URL — build-arg VITE_READ_ALOUD_TTS_URL (default https://aitherboard.imwald.eu/api/piper-tts). +# READ_ALOUD_TTS_URL — build-arg VITE_READ_ALOUD_TTS_URL (default /api/piper-tts). +# Same-origin: Apache proxies /api/piper-tts → aitherboard (e.g. :9876). Override only if you use CORS on another host. set -e REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -21,7 +22,7 @@ IMAGE_MONITOR="silberengel/imwald-jumble-nip66-monitor" # Use public origin only (no /proxy path): web.service builds /sites/?url=… # Override: JUMBLE_PROXY_SERVER_URL=https://other.example ./scripts/build-and-push-prod.sh JUMBLE_PROXY_SERVER_URL="${JUMBLE_PROXY_SERVER_URL:-https://jumble.imwald.eu}" -READ_ALOUD_TTS_URL="${READ_ALOUD_TTS_URL:-https://aitherboard.imwald.eu/api/piper-tts}" +READ_ALOUD_TTS_URL="${READ_ALOUD_TTS_URL:-/api/piper-tts}" echo "Building main app (version: $VERSION, VITE_PROXY_SERVER=$JUMBLE_PROXY_SERVER_URL, VITE_READ_ALOUD_TTS_URL=$READ_ALOUD_TTS_URL)" docker build \ diff --git a/src/constants.ts b/src/constants.ts index ddd0ec9b..d92c6a37 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,8 +12,9 @@ export const GITREPUBLIC_WEB_BASE_URL = ( .replace(/\/$/, '') /** - * Piper TTS proxy (same contract as aitherboard `POST /api/piper-tts`: JSON `{ text, voice?, speed? }`, body `audio/wav`). - * Set `VITE_READ_ALOUD_TTS_URL` to your deployed aitherboard URL, e.g. `https://aitherboard.example.com/api/piper-tts`. + * Piper TTS (same contract as aitherboard `POST /api/piper-tts`: JSON `{ text, voice?, speed? }`, body `audio/wav`). + * Default production: `/api/piper-tts` (same origin; reverse-proxy to aitherboard — see PROXY_SETUP.md). + * For cross-origin aitherboard instead, set full URL and configure CORS on that host. * If empty, read-aloud uses the Web Speech API only. */ export const READ_ALOUD_TTS_URL = diff --git a/src/lib/read-aloud.ts b/src/lib/read-aloud.ts index df8be9b0..c9e8abe2 100644 --- a/src/lib/read-aloud.ts +++ b/src/lib/read-aloud.ts @@ -1,7 +1,19 @@ import { ExtendedKind, READ_ALOUD_TTS_URL } from '@/constants' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' +import logger from '@/lib/logger' import { Event, kinds } from 'nostr-tools' +function readAloudEndpointForLog(): string { + const u = READ_ALOUD_TTS_URL + if (!u) return '' + try { + const parsed = new URL(u) + return `${parsed.origin}${parsed.pathname}` + } catch { + return u.length > 96 ? `${u.slice(0, 96)}…` : u + } +} + export type ReadAloudResult = 'ok' | 'unsupported' | 'empty' | 'error' const KINDS_WITH_METADATA_TITLE = new Set([ @@ -77,11 +89,16 @@ async function speakViaPiperTts(text: string): Promise { }) if (!response.ok) { + logger.warn('[ReadAloud] Piper HTTP error', { + status: response.status, + endpoint: readAloudEndpointForLog() + }) return 'error' } const blob = await response.blob() if (!blob.size) { + logger.warn('[ReadAloud] Piper returned empty body', { endpoint: readAloudEndpointForLog() }) return 'error' } @@ -109,7 +126,11 @@ async function speakViaPiperTts(text: string): Promise { try { await audio.play() return 'ok' - } catch { + } catch (playErr) { + logger.warn('[ReadAloud] Piper audio.play() blocked or failed', { + endpoint: readAloudEndpointForLog(), + error: playErr instanceof Error ? playErr.message : String(playErr) + }) cleanupBlob() if (readAloudAudio === audio) { readAloudAudio = null @@ -123,6 +144,10 @@ async function speakViaPiperTts(text: string): Promise { if (isAbort) { return 'ok' } + logger.warn('[ReadAloud] Piper fetch failed (check CORS on the TTS host or use same-origin /api/piper-tts)', { + endpoint: readAloudEndpointForLog(), + error: e instanceof Error ? e.message : String(e) + }) return 'error' } } @@ -147,7 +172,10 @@ export async function speakNoteReadAloud(event: Event): Promise if (piperResult === 'ok') { return 'ok' } - // Server failed or unreachable: fall back to Web Speech when available + logger.warn( + '[ReadAloud] Using Web Speech fallback — Piper did not play. See previous [ReadAloud] log for cause.', + { endpoint: readAloudEndpointForLog() } + ) } if (!window.speechSynthesis) { diff --git a/vite.config.ts b/vite.config.ts index e3a7c144..d01a68b3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -102,6 +102,11 @@ export default defineConfig(({ mode }) => { // OG/link preview uses `/sites/?url=…`. Without this, Vite serves `index.html` and WebService parses the app shell. // Run the scraper on 8090 per PROXY_SETUP.md, or rely on allorigins fallback in dev (web.service.ts). proxy: { + // Read-aloud Piper: same path as production Apache → aitherboard (avoid cross-origin CORS in dev). + '/api/piper-tts': { + target: 'http://127.0.0.1:9876', + changeOrigin: true + }, '/sites': { target: 'http://127.0.0.1:8090', changeOrigin: true