diff --git a/CHANGELOG.json b/CHANGELOG.json index 82e2eb0..ce55e07 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -6,6 +6,61 @@ "commitConvention": "conventional", "maintainers": ["grokify", "johncwang@gmail.com"], "releases": [ + { + "version": "v0.6.0", + "date": "2026-11-02", + "highlights": [ + { + "description": "`@plexusone/speech` 0.1.0: read text aloud and take dictation with the browser's own speech, with word highlighting, React hooks and a standalone build for pages without a build step", + "commit": "7cdc6a3" + } + ], + "added": [ + { + "description": "`speak`: text or segments with pauses, split into short utterances, with pause and resume, a cancel before new speech and one retry when the browser accepts speech but never starts it (`start-timeout`); speech cancelled by another script ends as `stopped`", + "commit": "7cdc6a3" + }, + { + "description": "`listen`: speech recognition with final and interim text, restarted across the browser's own stops until `stop()`, a fatal error or `maxSilences` silences in a row", + "commit": "7cdc6a3" + }, + { + "description": "`getVoices`, `pickVoice`, `langMatches`, `splitText`, `isSynthesisSupported`, `isRecognitionSupported`, `stopSpeaking` and `SpeechError`", + "commit": "7cdc6a3" + }, + { + "description": "`useSpeech`, `useListen` and `useVoices` in `@plexusone/speech/react`, and `@plexusone/speech/browser`, one file defining `window.PlexusSpeech`", + "commit": "7cdc6a3" + }, + { + "description": "`onWord` on `speak`, with each word's segment and its offsets in the text passed, without surrounding punctuation; pieces with no letter or digit, such as a dash, are not reported", + "commit": "c160f17" + }, + { + "description": "`trackWords` on `useSpeech`, which keeps the word being spoken in `word`", + "commit": "c160f17" + } + ], + "tests": [ + { + "description": "Cover speaking, pausing, retrying, listening, text splitting and the hooks with fake browser speech", + "commit": "8d3028d" + }, + { + "description": "Cover word offsets across chunks, words without a length, punctuation, dashes and `trackWords`", + "commit": "170ba5c" + } + ], + "documentation": [ + { + "description": "`@plexusone/speech` package page, with highlighting words and how voices differ", + "commit": "cd7f540" + }, + { + "description": "Adding read-aloud and voice input guide, and v0.6.0 release notes" + } + ] + }, { "version": "v0.5.1", "date": "2026-11-02", diff --git a/CHANGELOG.md b/CHANGELOG.md index d939519..70ffd26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,31 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro ## [Unreleased] +## [v0.6.0] - 2026-11-02 + +### Highlights + +- `@plexusone/speech` 0.1.0: read text aloud and take dictation with the browser's own speech, with word highlighting, React hooks and a standalone build for pages without a build step + +### Added + +- `speak`: text or segments with pauses, split into short utterances, with pause and resume, a cancel before new speech and one retry when the browser accepts speech but never starts it (`start-timeout`); speech cancelled by another script ends as `stopped` ([`7cdc6a3`](https://github.com/plexusone/systemforge-web/commit/7cdc6a3)) +- `listen`: speech recognition with final and interim text, restarted across the browser's own stops until `stop()`, a fatal error or `maxSilences` silences in a row ([`7cdc6a3`](https://github.com/plexusone/systemforge-web/commit/7cdc6a3)) +- `getVoices`, `pickVoice`, `langMatches`, `splitText`, `isSynthesisSupported`, `isRecognitionSupported`, `stopSpeaking` and `SpeechError` ([`7cdc6a3`](https://github.com/plexusone/systemforge-web/commit/7cdc6a3)) +- `useSpeech`, `useListen` and `useVoices` in `@plexusone/speech/react`, and `@plexusone/speech/browser`, one file defining `window.PlexusSpeech` ([`7cdc6a3`](https://github.com/plexusone/systemforge-web/commit/7cdc6a3)) +- `onWord` on `speak`, with each word's segment and its offsets in the text passed, without surrounding punctuation; pieces with no letter or digit, such as a dash, are not reported ([`c160f17`](https://github.com/plexusone/systemforge-web/commit/c160f17)) +- `trackWords` on `useSpeech`, which keeps the word being spoken in `word` ([`c160f17`](https://github.com/plexusone/systemforge-web/commit/c160f17)) + +### Documentation + +- `@plexusone/speech` package page, with highlighting words and how voices differ ([`cd7f540`](https://github.com/plexusone/systemforge-web/commit/cd7f540)) +- Adding read-aloud and voice input guide, and v0.6.0 release notes + +### Tests + +- Cover speaking, pausing, retrying, listening, text splitting and the hooks with fake browser speech ([`8d3028d`](https://github.com/plexusone/systemforge-web/commit/8d3028d)) +- Cover word offsets across chunks, words without a length, punctuation, dashes and `trackWords` ([`170ba5c`](https://github.com/plexusone/systemforge-web/commit/170ba5c)) + ## [v0.5.1] - 2026-11-02 ### Highlights @@ -277,7 +302,8 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro - Shared pnpm workflows from grokify/.github ([`bb07c1d`](https://github.com/plexusone/systemforge-web/commit/bb07c1d)) -[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.5.1...HEAD +[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.6.0...HEAD +[v0.6.0]: https://github.com/plexusone/systemforge-web/compare/v0.5.1...v0.6.0 [v0.5.1]: https://github.com/plexusone/systemforge-web/compare/v0.5.0...v0.5.1 [v0.5.0]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...v0.5.0 [v0.4.0]: https://github.com/plexusone/systemforge-web/compare/v0.3.1...v0.4.0 diff --git a/README.md b/README.md index 391b673..6d34054 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Alphabetical; see [Packages](docs/packages/index.md) for how they depend on each | `@plexusone/invites` | Invite-gated sign-up pages | [![npm](https://img.shields.io/npm/v/@plexusone/invites)](https://www.npmjs.com/package/@plexusone/invites) | | `@plexusone/pages` | Pre-built pages | [![npm](https://img.shields.io/npm/v/@plexusone/pages)](https://www.npmjs.com/package/@plexusone/pages) | | `@plexusone/shell` | Application shell | [![npm](https://img.shields.io/npm/v/@plexusone/shell)](https://www.npmjs.com/package/@plexusone/shell) | +| `@plexusone/speech` | Browser speech in and out | [![npm](https://img.shields.io/npm/v/@plexusone/speech)](https://www.npmjs.com/package/@plexusone/speech) | | `@plexusone/telemetry` | Event instrumentation | [![npm](https://img.shields.io/npm/v/@plexusone/telemetry)](https://www.npmjs.com/package/@plexusone/telemetry) | | `@plexusone/tenant` | Multi-tenant context | [![npm](https://img.shields.io/npm/v/@plexusone/tenant)](https://www.npmjs.com/package/@plexusone/tenant) | | `@plexusone/ui` | UI components | [![npm](https://img.shields.io/npm/v/@plexusone/ui)](https://www.npmjs.com/package/@plexusone/ui) | diff --git a/docs/changelog.md b/docs/changelog.md index d939519..70ffd26 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,31 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro ## [Unreleased] +## [v0.6.0] - 2026-11-02 + +### Highlights + +- `@plexusone/speech` 0.1.0: read text aloud and take dictation with the browser's own speech, with word highlighting, React hooks and a standalone build for pages without a build step + +### Added + +- `speak`: text or segments with pauses, split into short utterances, with pause and resume, a cancel before new speech and one retry when the browser accepts speech but never starts it (`start-timeout`); speech cancelled by another script ends as `stopped` ([`7cdc6a3`](https://github.com/plexusone/systemforge-web/commit/7cdc6a3)) +- `listen`: speech recognition with final and interim text, restarted across the browser's own stops until `stop()`, a fatal error or `maxSilences` silences in a row ([`7cdc6a3`](https://github.com/plexusone/systemforge-web/commit/7cdc6a3)) +- `getVoices`, `pickVoice`, `langMatches`, `splitText`, `isSynthesisSupported`, `isRecognitionSupported`, `stopSpeaking` and `SpeechError` ([`7cdc6a3`](https://github.com/plexusone/systemforge-web/commit/7cdc6a3)) +- `useSpeech`, `useListen` and `useVoices` in `@plexusone/speech/react`, and `@plexusone/speech/browser`, one file defining `window.PlexusSpeech` ([`7cdc6a3`](https://github.com/plexusone/systemforge-web/commit/7cdc6a3)) +- `onWord` on `speak`, with each word's segment and its offsets in the text passed, without surrounding punctuation; pieces with no letter or digit, such as a dash, are not reported ([`c160f17`](https://github.com/plexusone/systemforge-web/commit/c160f17)) +- `trackWords` on `useSpeech`, which keeps the word being spoken in `word` ([`c160f17`](https://github.com/plexusone/systemforge-web/commit/c160f17)) + +### Documentation + +- `@plexusone/speech` package page, with highlighting words and how voices differ ([`cd7f540`](https://github.com/plexusone/systemforge-web/commit/cd7f540)) +- Adding read-aloud and voice input guide, and v0.6.0 release notes + +### Tests + +- Cover speaking, pausing, retrying, listening, text splitting and the hooks with fake browser speech ([`8d3028d`](https://github.com/plexusone/systemforge-web/commit/8d3028d)) +- Cover word offsets across chunks, words without a length, punctuation, dashes and `trackWords` ([`170ba5c`](https://github.com/plexusone/systemforge-web/commit/170ba5c)) + ## [v0.5.1] - 2026-11-02 ### Highlights @@ -277,7 +302,8 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro - Shared pnpm workflows from grokify/.github ([`bb07c1d`](https://github.com/plexusone/systemforge-web/commit/bb07c1d)) -[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.5.1...HEAD +[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.6.0...HEAD +[v0.6.0]: https://github.com/plexusone/systemforge-web/compare/v0.5.1...v0.6.0 [v0.5.1]: https://github.com/plexusone/systemforge-web/compare/v0.5.0...v0.5.1 [v0.5.0]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...v0.5.0 [v0.4.0]: https://github.com/plexusone/systemforge-web/compare/v0.3.1...v0.4.0 diff --git a/docs/guides/read-aloud.md b/docs/guides/read-aloud.md new file mode 100644 index 0000000..47348f1 --- /dev/null +++ b/docs/guides/read-aloud.md @@ -0,0 +1,203 @@ +# Adding read-aloud and voice input + +`@plexusone/speech` lets an application read text aloud and take dictation +with the browser's own speech: nothing to run and no key to manage. This guide +adds a read-aloud control with word highlighting, a voice picker and voice +input to an application. The [package page](../packages/speech.md) is the +reference for every option. + +## 1. Choose an entry point + +| You have | Use | +| ------------------------------------------ | ---------------------------------------------------------------- | +| A React application | `@plexusone/speech/react`: `useSpeech`, `useListen`, `useVoices` | +| Code without React | `@plexusone/speech`: `speak`, `listen`, `getVoices` | +| A page with no build step, or a Go program | `@plexusone/speech/browser`: one file defining `PlexusSpeech` | + +```bash +pnpm add @plexusone/speech +``` + +The main entry point has no dependencies. React is needed only for the +hooks. + +## 2. Read text aloud + +Pass the text as segments when it has natural parts, such as the sentences +of a lesson or the paragraphs of an article. Each segment can have a pause +after it, and `segment` says which one is being read: + +```tsx +import { useSpeech } from '@plexusone/speech/react'; + +function ReadAloud({ paragraphs }: { paragraphs: string[] }) { + const { speak, stop, pause, resume, state, segment, error, supported } = useSpeech({ + lang: 'en-US', + }); + if (!supported) return null; + + const segments = paragraphs.map((text) => ({ text, pauseMs: 400 })); + return ( + <> + {state === 'speaking' ? ( + + ) : state === 'paused' ? ( + + ) : ( + + )} + {(state === 'speaking' || state === 'paused') && } + {error?.code === 'start-timeout' && ( +

Your browser's voice isn't responding. Here is the text.

+ )} + {paragraphs.map((p, i) => ( +

+ {p} +

+ ))} + + ); +} +``` + +Start speech from a click or a key press. Browsers may refuse speech that a +page starts on its own, and report `not-allowed`. + +Long text is split into short utterances for you, because some voices stop +partway through a long one. Pausing repeats at most the sentence that was +interrupted when speech resumes. + +## 3. Highlight the word being read + +Set `trackWords` and mark `word` in the segment being read. `start` and `end` +are offsets into that segment's text exactly as you passed it: + +```tsx +import type { SpeakWord } from '@plexusone/speech'; + +// In ReadAloud above: useSpeech({ lang: 'en-US', trackWords: true }), and +// render each paragraph with renderParagraph(p, i, segment, word). +function renderParagraph(text: string, index: number, segment: number, word?: SpeakWord) { + if (index !== segment) return

{text}

; + if (!word) + return ( +

+ {text} +

+ ); + return ( +

+ {text.slice(0, word.start)} + {text.slice(word.start, word.end)} + {text.slice(word.end)} +

+ ); +} +``` + +Pick the colours together: a light background on the segment being read +and a stronger one on the word reads well. Two things keep the segment's +mark visible: + +- **No transition on the segment's background.** A short sentence is + spoken in under half a second, so a fade can still be running when the + next one starts. +- **Mark the word with more than a shade,** such as an underline as well as + a background. In dark mode two backgrounds of one hue are hard to tell + apart. + +A one-word segment is covered by the word's mark, so only the word shows. + +Words are marked without the punctuation around them. Not every voice +reports words: many of Chrome's online voices report none, and the word +mark never appears while the segment's still does. Some voices report +pieces of a word or join two short words, so treat the mark as a guide to +where the voice is, not an exact transcript. + +`trackWords` re-renders the component on every word. To highlight without +re-rendering, for example by setting a class on a DOM node, pass `onWord` +instead and update the DOM yourself. + +## 4. When speech does not start + +`speak` rejects with a `SpeechError`. Show the text, or offer a recording, +rather than a silent button: + +| `error.code` | What happened | What to show | +| --------------- | ------------------------------------------------ | ---------------------------------------------- | +| `unsupported` | The browser cannot speak (`supported` is false) | Hide the control | +| `not-allowed` | Speech was not started from a click or key press | Start it from a button | +| `start-timeout` | The browser accepted speech but never started it | The text to read, or a recording | +| anything else | The voice failed | The text, and let the person try another voice | + +`ReadAloud` above shows the `start-timeout` message from the same hook that +started the speech. + +## 5. Let people choose a voice + +`useVoices` lists the voices for a language once the browser has loaded +them. Store the chosen voice's `voiceURI`; `voice` accepts it as a string: + +```tsx +import { useVoices } from '@plexusone/speech/react'; + +function VoicePicker({ value, onChange }: { value: string; onChange: (uri: string) => void }) { + const voices = useVoices('en'); + return ( + + ); +} + +// useSpeech({ lang: 'en-US', voice: savedVoiceURI || undefined }) +``` + +Voices differ by browser and device, so a saved voice may be missing on +another computer. Without it, `speak` picks a voice for `lang`: the +browser's default for that language, then a voice on the device. + +## 6. Add voice input to a text field + +`useListen` reports text as it is recognized, final and interim together: + +```tsx +import { useListen } from '@plexusone/speech/react'; + +function Dictation({ onText }: { onText: (text: string) => void }) { + const { start, stop, listening, text, error, supported } = useListen({ lang: 'en-US' }); + if (!supported) return null; + return ( + <> +