Browse Source

add read-alouds

imwald
Silberengel 1 month ago
parent
commit
24ccafdaac
  1. 8
      docker-compose.prod.yml
  2. 10
      scripts/build-and-push-prod.sh
  3. 26
      src/components/NoteOptions/useMenuActions.tsx
  4. 20
      src/constants.ts
  5. 5
      src/i18n/locales/ar.ts
  6. 5
      src/i18n/locales/de.ts
  7. 5
      src/i18n/locales/en.ts
  8. 5
      src/i18n/locales/es.ts
  9. 5
      src/i18n/locales/fa.ts
  10. 5
      src/i18n/locales/fr.ts
  11. 5
      src/i18n/locales/hi.ts
  12. 5
      src/i18n/locales/it.ts
  13. 5
      src/i18n/locales/ja.ts
  14. 5
      src/i18n/locales/ko.ts
  15. 5
      src/i18n/locales/pl.ts
  16. 5
      src/i18n/locales/pt-BR.ts
  17. 5
      src/i18n/locales/pt-PT.ts
  18. 5
      src/i18n/locales/ru.ts
  19. 5
      src/i18n/locales/th.ts
  20. 5
      src/i18n/locales/zh.ts
  21. 159
      src/lib/read-aloud.ts

8
docker-compose.prod.yml

@ -1,5 +1,11 @@
# Minimal compose for running the published image (e.g. on remote server). # Minimal compose for running the published image (e.g. on remote server).
# Usage: docker compose -f docker-compose.prod.yml up -d # 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:
# ProxyPass /sites/ http://127.0.0.1:8090/sites/
# ProxyPass / http://127.0.0.1:8089/
# so the browser hits https://<host>/sites/?url=… on Apache → OG proxy (8090); static SPA is 8089 only.
# 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. # NIP-66 monitor: set NIP66_MONITOR_NSEC (and optionally NIP66_MONITOR_NPUB) in the host env or .env.
# - Cron (jumble-nip66-monitor) uses NIP66_MONITOR_NSEC to publish 30166/10166; nsec never goes to the client. # - Cron (jumble-nip66-monitor) uses NIP66_MONITOR_NSEC to publish 30166/10166; nsec never goes to the client.

10
scripts/build-and-push-prod.sh

@ -3,8 +3,10 @@
# Then create git tag v<version> and push it. # Then create git tag v<version> and push it.
# Run from repo root. Requires: docker, docker login, git. # Run from repo root. Requires: docker, docker login, git.
# #
# Optional env: JUMBLE_PROXY_SERVER_URL — passed as Docker build-arg VITE_PROXY_SERVER (default site origin). # Optional env:
# Must match Apache: ProxyPass /sites/ → OG backend; the app requests https://<origin>/sites/?url=… # 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).
set -e set -e
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@ -19,10 +21,12 @@ IMAGE_MONITOR="silberengel/imwald-jumble-nip66-monitor"
# Use public origin only (no /proxy path): web.service builds <origin>/sites/?url=… # Use public origin only (no /proxy path): web.service builds <origin>/sites/?url=…
# Override: JUMBLE_PROXY_SERVER_URL=https://other.example ./scripts/build-and-push-prod.sh # 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}" 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}"
echo "Building main app (version: $VERSION, VITE_PROXY_SERVER=$JUMBLE_PROXY_SERVER_URL)" echo "Building main app (version: $VERSION, VITE_PROXY_SERVER=$JUMBLE_PROXY_SERVER_URL, VITE_READ_ALOUD_TTS_URL=$READ_ALOUD_TTS_URL)"
docker build \ docker build \
--build-arg "VITE_PROXY_SERVER=$JUMBLE_PROXY_SERVER_URL" \ --build-arg "VITE_PROXY_SERVER=$JUMBLE_PROXY_SERVER_URL" \
--build-arg "VITE_READ_ALOUD_TTS_URL=$READ_ALOUD_TTS_URL" \
-t "$IMAGE_APP:latest" -t "$IMAGE_APP:$VERSION" . -t "$IMAGE_APP:latest" -t "$IMAGE_APP:$VERSION" .
echo "Building NIP-66 monitor (version: $VERSION)" echo "Building NIP-66 monitor (version: $VERSION)"

26
src/components/NoteOptions/useMenuActions.tsx

@ -1,4 +1,4 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind, READ_ALOUD_KINDS } from '@/constants'
import { getNoteBech32Id, isProtectedEvent, getRootEventHexId } from '@/lib/event' import { getNoteBech32Id, isProtectedEvent, getRootEventHexId } from '@/lib/event'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { buildHiveTalkJoinUrl } from '@/lib/hivetalk' import { buildHiveTalkJoinUrl } from '@/lib/hivetalk'
@ -6,6 +6,7 @@ import { toAlexandria } from '@/lib/link'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { normalizeUrl, simplifyUrl } from '@/lib/url' import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { speakNoteReadAloud } from '@/lib/read-aloud'
import { buildPinListTagsAfterToggle, fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest' import { buildPinListTagsAfterToggle, fetchLatestReplaceableListEvent } from '@/lib/replaceable-list-latest'
import { generateBech32IdFromATag } from '@/lib/tag' import { generateBech32IdFromATag } from '@/lib/tag'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
@ -33,7 +34,8 @@ import {
Send, Send,
Trash2, Trash2,
TriangleAlert, TriangleAlert,
Video Video,
Volume2
} from 'lucide-react' } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
@ -595,6 +597,26 @@ export function useMenuActions({
closeDrawer() closeDrawer()
} }
}, },
...(READ_ALOUD_KINDS.includes(event.kind)
? [
{
icon: Volume2,
label: t('Read this note aloud'),
onClick: () => {
closeDrawer()
void speakNoteReadAloud(event).then((result) => {
if (result === 'unsupported') {
toast.error(t('Read-aloud is not supported in this browser'))
} else if (result === 'empty') {
toast.error(t('Nothing to read aloud'))
} else if (result === 'error') {
toast.error(t('Read-aloud failed'))
}
})
}
} as MenuAction
]
: []),
...(pubkey && event.pubkey !== pubkey && onOpenPublicMessage ...(pubkey && event.pubkey !== pubkey && onOpenPublicMessage
? [ ? [
{ {

20
src/constants.ts

@ -4,6 +4,14 @@ import { kinds } from 'nostr-tools'
export const JUMBLE_API_BASE_URL = export const JUMBLE_API_BASE_URL =
(import.meta.env.VITE_JUMBLE_API_BASE_URL as string | undefined) ?? 'https://api.jumble.imwald.eu' (import.meta.env.VITE_JUMBLE_API_BASE_URL as string | undefined) ?? 'https://api.jumble.imwald.eu'
/**
* 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`.
* If empty, read-aloud uses the Web Speech API only.
*/
export const READ_ALOUD_TTS_URL =
(import.meta.env.VITE_READ_ALOUD_TTS_URL as string | undefined)?.trim() || ''
/** HiveTalk (WebRTC video call) base URL; override with VITE_HIVETALK_BASE_URL for self-hosted instances. */ /** HiveTalk (WebRTC video call) base URL; override with VITE_HIVETALK_BASE_URL for self-hosted instances. */
export const HIVETALK_BASE_URL = export const HIVETALK_BASE_URL =
(import.meta.env.VITE_HIVETALK_BASE_URL as string | undefined) ?? 'https://vanilla.hivetalk.org' (import.meta.env.VITE_HIVETALK_BASE_URL as string | undefined) ?? 'https://vanilla.hivetalk.org'
@ -306,6 +314,18 @@ export const ExtendedKind = {
BADGE_DEFINITION: 30009 BADGE_DEFINITION: 30009
} }
/** Event kinds that show “Read this note aloud” in note options (Web Speech API). */
export const READ_ALOUD_KINDS: readonly number[] = [
kinds.ShortTextNote,
ExtendedKind.DISCUSSION,
ExtendedKind.COMMENT,
kinds.LongFormArticle,
ExtendedKind.PUBLICATION,
ExtendedKind.PUBLICATION_CONTENT,
ExtendedKind.WIKI_ARTICLE_MARKDOWN,
ExtendedKind.WIKI_ARTICLE
]
/** NIP-52 calendar event kinds (addressable by d-tag); use in isReplaceableEvent. */ /** NIP-52 calendar event kinds (addressable by d-tag); use in isReplaceableEvent. */
export const CALENDAR_EVENT_KINDS = [ export const CALENDAR_EVENT_KINDS = [
ExtendedKind.CALENDAR_EVENT_DATE, ExtendedKind.CALENDAR_EVENT_DATE,

5
src/i18n/locales/ar.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/de.ts

@ -176,6 +176,11 @@ export default {
'Copy call invite link': 'Anruf-Einladungslink kopieren', 'Copy call invite link': 'Anruf-Einladungslink kopieren',
'Start call about this': 'Anruf zu diesem Beitrag starten', 'Start call about this': 'Anruf zu diesem Beitrag starten',
'Send call invite': 'Anruf-Einladung senden', 'Send call invite': 'Anruf-Einladung senden',
'Read this note aloud': 'Diese Notiz vorlesen',
'Read-aloud is not supported in this browser':
'Vorlesen wird in diesem Browser nicht unterstützt',
'Nothing to read aloud': 'Kein Text zum Vorlesen',
'Read-aloud failed': 'Vorlesen fehlgeschlagen',
'Join the video call': 'Am Videoanruf teilnehmen', 'Join the video call': 'Am Videoanruf teilnehmen',
'Schedule video call': 'Videoanruf planen', 'Schedule video call': 'Videoanruf planen',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.":

5
src/i18n/locales/en.ts

@ -173,6 +173,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/es.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/fa.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/fr.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/hi.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/it.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/ja.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/ko.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/pl.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/pt-BR.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/pt-PT.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/ru.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/th.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

5
src/i18n/locales/zh.ts

@ -159,6 +159,11 @@ export default {
'Copy call invite link': 'Copy call invite link', 'Copy call invite link': 'Copy call invite link',
'Start call about this': 'Start call about this', 'Start call about this': 'Start call about this',
'Send call invite': 'Send call invite', 'Send call invite': 'Send call invite',
'Read this note aloud': 'Read this note aloud',
'Read-aloud is not supported in this browser':
'Read-aloud is not supported in this browser',
'Nothing to read aloud': 'Nothing to read aloud',
'Read-aloud failed': 'Read-aloud failed',
'Join the video call': 'Join the video call', 'Join the video call': 'Join the video call',
'Schedule video call': 'Schedule video call', 'Schedule video call': 'Schedule video call',
"You're invited to a scheduled video call.": "You're invited to a scheduled video call.", "You're invited to a scheduled video call.": "You're invited to a scheduled video call.",

159
src/lib/read-aloud.ts

@ -0,0 +1,159 @@
import { ExtendedKind, READ_ALOUD_TTS_URL } from '@/constants'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { Event, kinds } from 'nostr-tools'
export type ReadAloudResult = 'ok' | 'unsupported' | 'empty' | 'error'
const KINDS_WITH_METADATA_TITLE = new Set<number>([
kinds.LongFormArticle,
ExtendedKind.PUBLICATION,
ExtendedKind.PUBLICATION_CONTENT,
ExtendedKind.WIKI_ARTICLE_MARKDOWN,
ExtendedKind.WIKI_ARTICLE
])
let readAloudAbort: AbortController | null = null
let readAloudAudio: HTMLAudioElement | null = null
function stopReadAloudPlayback(): void {
readAloudAbort?.abort()
readAloudAbort = null
if (readAloudAudio) {
const url = readAloudAudio.src
readAloudAudio.onended = null
readAloudAudio.onerror = null
readAloudAudio.pause()
readAloudAudio.removeAttribute('src')
readAloudAudio.load()
if (url.startsWith('blob:')) {
URL.revokeObjectURL(url)
}
}
readAloudAudio = null
window.speechSynthesis?.cancel()
}
/** Strip common Markdown / AsciiDoc / code so TTS reads plain text (same idea as NotePage preview). */
function stripMarkupForReadAloud(content: string): string {
let text = content
text = text.replace(/^#{1,6}\s+/gm, '')
text = text.replace(/\*\*([^*]+)\*\*/g, '$1')
text = text.replace(/\*([^*]+)\*/g, '$1')
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
text = text.replace(/^=+\s+/gm, '')
text = text.replace(/_([^_]+)_/g, '$1')
text = text.replace(/```[\s\S]*?```/g, '')
text = text.replace(/`([^`]+)`/g, '$1')
text = text.replace(/<[^>]+>/g, '')
text = text.replace(/\n{3,}/g, '\n\n')
return text.trim()
}
function buildReadAloudPlainText(event: Event): string {
let raw = event.content?.trim() ?? ''
if (KINDS_WITH_METADATA_TITLE.has(event.kind)) {
const meta = getLongFormArticleMetadataFromEvent(event)
const title = meta.title?.trim()
if (title) {
raw = `${title}. ${raw}`
}
}
return stripMarkupForReadAloud(raw)
}
/**
* Piper / Wyoming proxy (aitherboard-compatible): POST JSON, receive WAV.
*/
async function speakViaPiperTts(text: string): Promise<ReadAloudResult> {
stopReadAloudPlayback()
readAloudAbort = new AbortController()
try {
const response = await fetch(READ_ALOUD_TTS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, speed: 1 }),
signal: readAloudAbort.signal
})
if (!response.ok) {
return 'error'
}
const blob = await response.blob()
if (!blob.size) {
return 'error'
}
const audioUrl = URL.createObjectURL(blob)
const audio = new Audio()
readAloudAudio = audio
audio.src = audioUrl
const cleanupBlob = () => {
if (audio.src.startsWith('blob:')) {
URL.revokeObjectURL(audioUrl)
}
}
audio.addEventListener('ended', () => {
cleanupBlob()
if (readAloudAudio === audio) {
readAloudAudio = null
}
})
audio.addEventListener('error', () => {
cleanupBlob()
})
try {
await audio.play()
return 'ok'
} catch {
cleanupBlob()
if (readAloudAudio === audio) {
readAloudAudio = null
}
return 'error'
}
} catch (e) {
const isAbort =
(e instanceof DOMException && e.name === 'AbortError') ||
(e instanceof Error && e.name === 'AbortError')
if (isAbort) {
return 'ok'
}
return 'error'
}
}
function speakViaWebSpeech(text: string): void {
stopReadAloudPlayback()
window.speechSynthesis.speak(new SpeechSynthesisUtterance(text))
}
export async function speakNoteReadAloud(event: Event): Promise<ReadAloudResult> {
if (typeof window === 'undefined') {
return 'unsupported'
}
const text = buildReadAloudPlainText(event)
if (!text) {
return 'empty'
}
if (READ_ALOUD_TTS_URL) {
const piperResult = await speakViaPiperTts(text)
if (piperResult === 'ok') {
return 'ok'
}
// Server failed or unreachable: fall back to Web Speech when available
}
if (!window.speechSynthesis) {
return READ_ALOUD_TTS_URL ? 'error' : 'unsupported'
}
speakViaWebSpeech(text)
return 'ok'
}
Loading…
Cancel
Save