@crvouga/mockingbird-service-aws-speech 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog — @crvouga/mockingbird-service-aws-speech
2
+
3
+ ## 0.1.0 (2026-09-22)
4
+
5
+ Initial release.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # @crvouga/mockingbird-service-aws-speech
2
+
3
+ Stateful mock of **Amazon Polly** and **Amazon Transcribe** for test suites: Polly
4
+ `SynthesizeSpeech` and `StartSpeechSynthesisStream` (HTTP/2 duplex event stream), Transcribe
5
+ Streaming `StartStreamTranscription` (HTTP/2 duplex) and Transcribe batch
6
+ `StartTranscriptionJob` / `GetTranscriptionJob`. Chat voice turns run end to end with no audio
7
+ leaving the machine: Transcribe "hears" what a test scripts, Polly answers with deterministic
8
+ synthetic audio whose length follows the text.
9
+
10
+ - Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/aws-speech/SUPPORT.md)
11
+ - Proven with the official clients our consumer pins: `@aws-sdk/client-polly`,
12
+ `@aws-sdk/client-transcribe-streaming`, `@aws-sdk/client-transcribe` (3.1132.0), and the MP3
13
+ decoder our backend uses (`mpg123-decoder@1.0.3`).
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install -D @crvouga/mockingbird-service-aws-speech
19
+ ```
20
+
21
+ ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
22
+ `npx mockingbird-aws-speech serve` (h2c + HTTP/1.1 on one port), `createServer` from
23
+ `./server`, or `createRuntime` with any Fetch server (HTTP/1.1 only).
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ npx mockingbird-aws-speech serve --port 8797
29
+ export AWS_ENDPOINT_URL_POLLY=http://127.0.0.1:8797
30
+ export AWS_ENDPOINT_URL_TRANSCRIBE_STREAMING=http://127.0.0.1:8797
31
+ export AWS_ENDPOINT_URL_TRANSCRIBE=http://127.0.0.1:8797
32
+ ```
33
+
34
+ ```ts
35
+ import { createServer } from "@crvouga/mockingbird-service-aws-speech/server"
36
+
37
+ const speech = await createServer({ port: 8797 })
38
+ // What the next voice session "hears": partials while audio streams in, then the final.
39
+ await fetch(`${speech.url}/__admin/transcripts`, {
40
+ method: "PUT",
41
+ headers: { "content-type": "application/json" },
42
+ body: JSON.stringify({ match: { any: true }, partials: ["I have", "I have a headache"], final: "I have a headache." }),
43
+ })
44
+ // …the backend's /v2/chatbot/voice turn transcribes, replies, and speaks through Polly…
45
+ await speech.close()
46
+ ```
47
+
48
+ ### Protocols
49
+
50
+ Polly and Transcribe Streaming clients default to `NodeHttp2Handler`: against an `http://`
51
+ endpoint they speak **h2c** (cleartext HTTP/2, prior knowledge), and their bidirectional
52
+ operations need HTTP/2 duplex. Transcribe batch uses HTTP/1.1. `mockingbird-aws-speech serve`
53
+ and `createServer` sniff each connection and serve both on one port (`serve --config` from
54
+ another service's CLI, and `createRuntime` behind a plain Fetch server, speak HTTP/1.1 only).
55
+
56
+ ### Routes
57
+
58
+ | Route | Behaviour |
59
+ | --- | --- |
60
+ | `POST /v1/speech` | Polly `SynthesizeSpeech`. `OutputFormat: pcm` → raw PCM s16le mono 440 Hz tone (`audio/pcm`, even byte count, `SampleRate` 8000/16000, default 16000); `mp3` → valid MPEG-2 Layer III frames (`audio/mpeg`, 8000/16000/22050/24000). 60 ms of audio per character, byte-identical on every run; `x-amzn-RequestCharacters`. Errors: `ValidationException` (unknown voice or engine), `InvalidSampleRateException`, `TextLengthExceededException` (> 3000 characters). |
61
+ | `POST /v1/synthesisStream` | Polly `StartSpeechSynthesisStream` (HTTP/2 duplex, parameters in `x-amzn-Engine` / `x-amzn-VoiceId` / `x-amzn-OutputFormat` / `x-amzn-SampleRate`; generative engine only). Each `TextEvent` answers `AudioEvent`s (24 kHz MP3 frames mpg123 decodes); `CloseStreamEvent` (or the end of input) answers `StreamClosedEvent {RequestCharacters}`. |
62
+ | `POST /stream-transcription` | Transcribe `StartStreamTranscription` (HTTP/2 duplex). Validates the `x-amzn-transcribe-*` parameters (400 `BadRequestException`), echoes them in the response headers with `x-amzn-transcribe-session-id`. Sends one scripted partial (`IsPartial: true`) per `AudioEvent` received, the rest when the audio ends, then the final result, then closes. Audio bytes are counted, never kept. |
63
+ | `POST /` | Transcribe batch (AWS JSON 1.1, `X-Amz-Target`). `StartTranscriptionJob` → `IN_PROGRESS` (`ConflictException` for a repeated name); `GetTranscriptionJob` → `COMPLETED` once `jobDurationMs` (2 s) have passed on the mock clock, with `Transcript.TranscriptFileUri` = `https://s3.<region>.amazonaws.com/<OutputBucketName>/<OutputKey>`; `BadRequestException` for an unknown job. |
64
+
65
+ ### Transcripts (`PUT /__admin/transcripts`)
66
+
67
+ `{match?, partials?, final, times?}` (or `{"transcripts": [...]}`). `match` is
68
+ `{sessionIndex}` (0-based count of streaming sessions in the namespace), `{jobName}` (a batch
69
+ job), or `{any: true}`; an exact match wins over `any`. Unscripted sessions and jobs hear
70
+ `defaultTranscript` (`"Hello."`, `PUT /__admin/settings`) and count as `unscripted`
71
+ (`GET /__admin/transcripts` → `stats`).
72
+
73
+ ### Admin (beyond the standard contract)
74
+
75
+ | Route | Effect |
76
+ | --- | --- |
77
+ | `PUT` / `POST` / `GET` / `DELETE /__admin/transcripts` | Replace, add, list (with `stats`), remove (`?id=`). |
78
+ | `GET /__admin/jobs` | Batch jobs (advanced on the mock clock). |
79
+ | `POST /__admin/jobs/:name/complete` | Complete now, optionally with `{transcript}`. |
80
+ | `POST /__admin/jobs/:name/fail` | Fail with `{reason}` (`FailureReason`). |
81
+ | `GET /__admin/jobs/:name/transcript` | The transcript JSON Transcribe would write to S3. |
82
+ | `GET /__admin/speech` | Metadata of every synthesis / transcription (voice, engine, format, characters, bytes, script id — never text). |
83
+ | `GET/PUT /__admin/settings` | `defaultTranscript`, `jobDurationMs`. |
84
+
85
+ With `--s3-endpoint <url>` (`transcriptStore` in code) a completed job's transcript JSON is also
86
+ written to that S3 (the stack's s3rver) at `OutputBucketName/OutputKey`, so the app's own
87
+ `GetObject` on `TranscriptFileUri` finds it.
88
+
89
+ Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`):
90
+ `polly_throttling` (429 `ThrottlingException`), `polly_service_failure` (500),
91
+ `polly_stream_throttling` / `polly_stream_validation` / `polly_stream_quota` (an exception
92
+ event before any audio: our adapter falls back to `SynthesizeSpeech`), `polly_stream_failure`
93
+ (`ServiceFailureException` after audio), `polly_stream_no_close` (no `StreamClosedEvent`),
94
+ `transcribe_bad_request` (400), `transcribe_limit_exceeded` (429), `transcribe_service_unavailable`
95
+ (503), `transcribe_mid_stream_failure` (`InternalFailureException` after the first partial),
96
+ `transcribe_job_limit` (`LimitExceededException`), `transcribe_job_failed` (next job `FAILED`).
97
+
98
+ ### Namespaces
99
+
100
+ `x-mockingbird-namespace`, a `/ns/<name>` prefix on the endpoint URL, or the SigV4 access key id:
101
+ `PUT /__admin/credentials {"credentials": {"<AWS_ACCESS_KEY_ID>": "<namespace>"}}`. The journal
102
+ records voice, engine, format, character counts and script ids — never text.
103
+
104
+ ### Deliberately not modelled
105
+
106
+ - Real speech: Polly audio is a tone (PCM) or silent-but-valid MP3 frames (a tone needs a real
107
+ MP3 encoder); Transcribe never listens — words come only from scripts.
108
+ - `ogg_vorbis`, `ogg_opus`, `mulaw`, `alaw` and speech-mark (`json`) output, SSML semantics (tags
109
+ are stripped for length), lexicons, per-voice engine availability.
110
+ - Transcribe language identification, speaker labels, custom vocabularies (accepted and echoed,
111
+ not applied), Call Analytics and Medical variants, batch jobs reading their media from S3.
112
+ - SigV4 signatures and event signatures are not verified; the access key id only selects a
113
+ namespace.
114
+
115
+ ## API
116
+
117
+ | Export | Kind | Description |
118
+ | --- | --- | --- |
119
+ | `SpeechAPI` | class | The in-process mock: `fetch`, `reset`, `jobs()`, `complete(name, transcript?)`, `fail(name, reason)`, `transcriptDocument(job)`, `speechLog()`, `stats()`. Options: `sqlite`, `now`, `namespace`, `settings`, `transcripts`, `transcriptStore`. |
120
+ | `createRuntime` | function | The mock with the full service contract. Options: `settings`, `transcripts`, `transcriptStore`, `clock`, `seed`, `adminKey`, `onLog`, `sqlite`. |
121
+ | `SPEECH_PRESETS` | object | Every named fault preset. |
122
+ | `SPEECH_NAMESPACE` | string | The service name, `"aws-speech"`. |
123
+ | `POLLY_VOICES` | array | Every Polly voice id the mock accepts. |
124
+ | `speechError` | function | A restJson1 error response (`x-amzn-ErrorType` + `{message}`). |
125
+ | `accessKeyCredential` | function | The SigV4 access key id of a request (how credentials map to namespaces). |
126
+ | `pcmTone`, `mp3Audio`, `mp3Frame`, `durationFor`, `MS_PER_CHARACTER`, `MP3_SAMPLE_RATES`, `MP3_SAMPLES_PER_FRAME` | audio | The deterministic synthetic audio. |
127
+ | `DEFAULT_SETTINGS` | value | Default settings. |
128
+ | `encodeMessage`, `decodeMessage`, `FrameReader`, `readFrames`, `eventFrame`, `exceptionFrame`, `unwrapSigned`, `crc32`, `EventStreamError` | codec | The event-stream codec (exact prelude, headers and CRC32s), in both directions. |
129
+ | `document`, `operationIds`, `supportedOperationIds` | values | The vendored OpenAPI contract and its operation ids. |
130
+ | `createServer`, `serveTarget`, `DEFAULT_PORT`, `listenH2c` (`./server`) | Node | Serve h2c + HTTP/1.1 on one port; the `serve` CLI target; port 8797; the dual-protocol listener. |
131
+
132
+ Part of [mockingbird](https://github.com/crvouga/mockingbird).