Initial Commit
This commit is contained in:
270
openspec/archive/2026-05-02-voice/proposal.md
Normal file
270
openspec/archive/2026-05-02-voice/proposal.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# OpenSpec Change: voice
|
||||
|
||||
- **Change ID**: voice
|
||||
- **Package**: `@suaveplan/voice`
|
||||
- **Category**: universal
|
||||
- **Target path**: `universal/media/voice`
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: none — new capability
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/voice` on the genesis branch from scratch at enterprise quality. The package is the provider-agnostic voice core: abstract interfaces, shared types, Zod schemas, and a structured error hierarchy for speech synthesis (TTS), speech recognition (STT), voice management, sound-effect generation, and voice isolation. No provider-specific code, no HTTP client, no API keys. Concrete provider adapters (ElevenLabs, OpenAI, Google Cloud, Azure, Amazon Polly) live in sibling `voice-*` packages.
|
||||
|
||||
This package enables any downstream package — API plugins, React components, browser audio engines, or game narrative systems — to depend on voice contracts without pulling in provider SDKs or networking libraries.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | `VoiceError`; `VOICE.*` error codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `Clock`, `LoggerLike`, `CancellableOptions` |
|
||||
| `@suaveplan/streams` (`workspace:^`) | `ReadableStream` / `WritableStream` helpers for audio streaming |
|
||||
| `zod` (`catalog:forms`) | Schema validation |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check` (all `catalog:testing`).
|
||||
|
||||
---
|
||||
|
||||
## Public API surface
|
||||
|
||||
### Error hierarchy
|
||||
|
||||
`VoiceError extends SuaveplanError` with codes:
|
||||
|
||||
- `VOICE.TTS.SYNTHESIS_FAILED`
|
||||
- `VOICE.TTS.STREAM_INTERRUPTED`
|
||||
- `VOICE.STT.TRANSCRIPTION_FAILED`
|
||||
- `VOICE.STT.UNSUPPORTED_FORMAT`
|
||||
- `VOICE.VOICE.NOT_FOUND`
|
||||
- `VOICE.VOICE.CLONE_REJECTED`
|
||||
- `VOICE.EFFECTS.GENERATION_FAILED`
|
||||
- `VOICE.ISOLATION.FAILED`
|
||||
- `VOICE.PROVIDER.NOT_CONFIGURED`
|
||||
- `VOICE.PROVIDER.RATE_LIMITED`
|
||||
- `VOICE.VALIDATION.INVALID_REQUEST`
|
||||
- `VOICE.AUDIO.FORMAT_UNSUPPORTED`
|
||||
|
||||
### Shared types (schemas + TypeScript)
|
||||
|
||||
```ts
|
||||
// types/common
|
||||
AudioFormat: 'mp3_22050_32' | 'mp3_44100_128' | 'mp3_44100_192' | 'pcm_16000' | 'pcm_22050' | 'pcm_44100' | 'ulaw_8000' | 'wav_44100' | 'opus_48000' | 'aac_44100' | 'flac_44100'
|
||||
LatencyMode: 0 | 1 | 2 | 3 | 4
|
||||
VoiceCategory: 'premade' | 'cloned' | 'generated' | 'professional'
|
||||
VoiceGender: 'male' | 'female' | 'neutral'
|
||||
VoiceAge: 'young' | 'middle' | 'old'
|
||||
|
||||
VoiceSettings {
|
||||
stability?: number // 0–1
|
||||
similarityBoost?: number // 0–1
|
||||
style?: number // 0–1
|
||||
speakerBoost?: boolean
|
||||
}
|
||||
|
||||
Voice {
|
||||
id: string
|
||||
name: string
|
||||
category: VoiceCategory
|
||||
gender?: VoiceGender
|
||||
accent?: string
|
||||
age?: VoiceAge
|
||||
languageCodes?: string[] // ISO 639-1
|
||||
description?: string
|
||||
previewUrl?: string
|
||||
settings?: VoiceSettings
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
ListVoicesOptions {
|
||||
category?: VoiceCategory
|
||||
languageCode?: string
|
||||
search?: string
|
||||
pageToken?: string
|
||||
pageSize?: number // 1–100
|
||||
}
|
||||
|
||||
VoiceList {
|
||||
voices: Voice[]
|
||||
nextPageToken?: string
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
AudioSource = Uint8Array | ReadableStream<Uint8Array> | string // URL or file path (provider decides)
|
||||
```
|
||||
|
||||
### TTS engine interface
|
||||
|
||||
```ts
|
||||
interface VoiceSynthesizer {
|
||||
readonly providerName: string
|
||||
synthesize(options: SynthesisRequest): Promise<SynthesisResult>
|
||||
synthesizeStream(options: SynthesisRequest): ReadableStream<Uint8Array>
|
||||
}
|
||||
|
||||
SynthesisRequest {
|
||||
text: string
|
||||
voiceId: string
|
||||
modelId?: string
|
||||
languageCode?: string
|
||||
voiceSettings?: VoiceSettings
|
||||
outputFormat?: AudioFormat
|
||||
latencyMode?: LatencyMode
|
||||
previousText?: string
|
||||
nextText?: string
|
||||
seed?: number
|
||||
pronunciationDictionaryIds?: string[]
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
SynthesisResult {
|
||||
audio: Uint8Array
|
||||
format: AudioFormat
|
||||
durationMs?: number
|
||||
charactersUsed?: number
|
||||
requestId?: string
|
||||
}
|
||||
```
|
||||
|
||||
### STT engine interface
|
||||
|
||||
```ts
|
||||
interface TranscriptionEngine {
|
||||
readonly providerName: string
|
||||
transcribe(options: TranscriptionRequest): Promise<TranscriptionResult>
|
||||
}
|
||||
|
||||
TranscriptionRequest {
|
||||
audio: AudioSource
|
||||
modelId?: string
|
||||
languageCode?: string
|
||||
tagAudioEvents?: boolean
|
||||
numSpeakers?: number // 1–32
|
||||
timestampsGranularity?: 'word' | 'character'
|
||||
diarize?: boolean
|
||||
diarizationThreshold?: number
|
||||
useMultiChannel?: boolean
|
||||
entityDetection?: string[] | 'all'
|
||||
entityRedaction?: string[] | 'all'
|
||||
keyterms?: string[]
|
||||
temperature?: number // 0–2
|
||||
seed?: number
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
WordTimestamp {
|
||||
word: string
|
||||
startMs: number
|
||||
endMs: number
|
||||
speakerId?: string
|
||||
channelIndex?: number
|
||||
confidence?: number
|
||||
}
|
||||
|
||||
SpeakerSegment {
|
||||
speakerId: string
|
||||
startMs: number
|
||||
endMs: number
|
||||
}
|
||||
|
||||
DetectedEntity {
|
||||
text: string
|
||||
type: string
|
||||
startChar: number
|
||||
endChar: number
|
||||
}
|
||||
|
||||
TranscriptionResult {
|
||||
text: string
|
||||
words?: WordTimestamp[]
|
||||
speakers?: SpeakerSegment[]
|
||||
entities?: DetectedEntity[]
|
||||
language?: string
|
||||
durationMs?: number
|
||||
requestId?: string
|
||||
}
|
||||
```
|
||||
|
||||
### Voice repository interface
|
||||
|
||||
```ts
|
||||
interface VoiceRepository {
|
||||
readonly providerName: string
|
||||
list(options?: ListVoicesOptions): Promise<VoiceList>
|
||||
get(voiceId: string): Promise<Voice | null>
|
||||
// Cloning is intentionally NOT in the base interface;
|
||||
// provider-specific clone APIs vary too widely.
|
||||
}
|
||||
```
|
||||
|
||||
### Sound-effect engine interface
|
||||
|
||||
```ts
|
||||
interface SoundEffectEngine {
|
||||
readonly providerName: string
|
||||
generate(options: SoundEffectRequest): Promise<SoundEffectResult>
|
||||
}
|
||||
|
||||
SoundEffectRequest {
|
||||
text: string
|
||||
durationSeconds?: number // 0.5–30
|
||||
loop?: boolean
|
||||
promptInfluence?: number // 0–1
|
||||
modelId?: string
|
||||
outputFormat?: AudioFormat
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
SoundEffectResult {
|
||||
audio: Uint8Array
|
||||
format: AudioFormat
|
||||
durationMs?: number
|
||||
requestId?: string
|
||||
}
|
||||
```
|
||||
|
||||
### Voice-isolation engine interface
|
||||
|
||||
```ts
|
||||
interface VoiceIsolationEngine {
|
||||
readonly providerName: string
|
||||
isolate(options: IsolationRequest): Promise<IsolationResult>
|
||||
}
|
||||
|
||||
IsolationRequest {
|
||||
audio: AudioSource
|
||||
outputFormat?: AudioFormat
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
IsolationResult {
|
||||
audio: Uint8Array
|
||||
format: AudioFormat
|
||||
requestId?: string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design decisions
|
||||
|
||||
1. **No provider-specific types leak into the core.** `AudioSource` is intentionally loose (`Uint8Array | ReadableStream | string`) so providers can accept URLs, file paths, or raw bytes. The provider validates and narrows at runtime.
|
||||
2. **Streaming is TTS-only in the base interface.** STT streaming, real-time conversational AI, and duplex WebSocket audio are provider-specific extensions that will ship in per-provider `/streaming` subpaths, not in the core contract.
|
||||
3. **Voice cloning is excluded from `VoiceRepository`.** Clone APIs (sample upload, verification, fine-tuning status) vary radically across providers. A `VoiceCloner` interface may be added later in a provider-specific subpath or a future core revision.
|
||||
4. **Voice changing is excluded from the core.** It is an ElevenLabs-specific feature with no clear generic equivalent in other providers.
|
||||
5. **Conversational AI agents are excluded from the core.** They are a product-level abstraction above raw TTS/STT and will ship as a separate package if needed.
|
||||
6. **`@suaveplan/types` stays pure.** Voice types live in `@suaveplan/voice/types/common` so API plugins can depend on the voice contract without circular issues, while `@suaveplan/types` remains a zero-runtime foundation.
|
||||
7. **All async entrypoints accept `AbortSignal`.** Every engine method propagates cancellation to provider adapters.
|
||||
8. **All engines expose `readonly providerName`.** This enables observability tags and debugging without `instanceof` checks.
|
||||
52
openspec/archive/2026-05-02-voice/tasks.md
Normal file
52
openspec/archive/2026-05-02-voice/tasks.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Tasks: voice
|
||||
|
||||
**Change ID**: voice
|
||||
**Package**: `@suaveplan/voice`
|
||||
**Target path**: `universal/media/voice`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/streams`
|
||||
|
||||
---
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] Create `universal/media/voice/` with `src/`, `tests/`, and all feature subdirectories per the layout in `proposal.md`.
|
||||
- [x] Write `package.json` at version `0.1.0` with `"type": "module"`, `"sideEffects": false`, correct `exports` map, `dependencies`, and `devDependencies`.
|
||||
- [x] Write `tsconfig.lib.json` extending `../../../tsconfig.base.json`.
|
||||
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config`.
|
||||
- [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check` scripts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Errors + types
|
||||
|
||||
- [x] `src/errors/voice-error/voice-error.ts` — `VoiceError extends SuaveplanError`; `defineCodes('VOICE', [...])` with all 12 codes; `voice-error.md`.
|
||||
- [x] `src/types/common.ts` — `AudioFormat`, `LatencyMode`, `VoiceCategory`, `VoiceGender`, `VoiceAge`, `VoiceSettings`, `Voice`, `ListVoicesOptions`, `VoiceList`, `AudioSource`, `WordTimestamp`, `SpeakerSegment`, `DetectedEntity`; shared Zod schemas in `common.schemas.ts`; `common.md`.
|
||||
- [x] `src/types/common.test.ts` — schema round-trips for `AudioFormat`, `Voice`, `VoiceSettings`; invalid values rejected.
|
||||
|
||||
---
|
||||
|
||||
## 3. Engine interfaces + schemas
|
||||
|
||||
- [x] `src/tts/tts-engine/tts-engine.ts` + `tts-engine.types.ts` — `VoiceSynthesizer` interface, `SynthesisRequest`, `SynthesisResult`; `tts-engine.schemas.ts` — Zod schemas; `tts-engine.md`; `tts-engine.test.ts` — schema validation only (no impl to test).
|
||||
- [x] `src/stt/stt-engine/stt-engine.ts` + `stt-engine.types.ts` — `TranscriptionEngine` interface, `TranscriptionRequest`, `TranscriptionResult`; `stt-engine.schemas.ts`; `stt-engine.md`; `stt-engine.test.ts`.
|
||||
- [x] `src/voices/voice-repository/voice-repository.ts` + `voice-repository.types.ts` — `VoiceRepository` interface; `voice-repository.schemas.ts`; `voice-repository.md`; `voice-repository.test.ts`.
|
||||
- [x] `src/effects/sound-effect-engine/sound-effect-engine.ts` + `sound-effect-engine.types.ts` — `SoundEffectEngine` interface; `sound-effect-engine.schemas.ts`; `sound-effect-engine.md`; `sound-effect-engine.test.ts`.
|
||||
- [x] `src/isolation/voice-isolation-engine/voice-isolation-engine.ts` + `voice-isolation-engine.types.ts` — `VoiceIsolationEngine` interface; `voice-isolation-engine.schemas.ts`; `voice-isolation-engine.md`; `voice-isolation-engine.test.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Property-based tests (`tests/property.test.ts`)
|
||||
|
||||
- [x] `AudioFormat` round-trip: every valid format string parses and serialises to itself.
|
||||
- [x] `VoiceSettings` bounds: `stability`, `similarityBoost`, `style` ∈ [0, 1] when parsed via Zod.
|
||||
- [x] `WordTimestamp` ordering: for any generated array of words, `startMs < endMs` and sequences are monotonic.
|
||||
|
||||
---
|
||||
|
||||
## 5. Docs + verify
|
||||
|
||||
- [x] Root `README.md` with install, quick-start, modules table linking every co-located `.md`.
|
||||
- [x] `bun run docs:check --filter=@suaveplan/voice` ≥ 90%.
|
||||
- [x] `bunx turbo lint typecheck test:coverage --filter=@suaveplan/voice` green with 100% coverage (statement + branch + function + line).
|
||||
- [x] No `TODO`, `FIXME`, `@deprecated`, `any`, `ts-ignore`, or `.skip` in `src/`.
|
||||
- [x] Publish to Verdaccio, update `CLAUDE.md`, archive OpenSpec change.
|
||||
Reference in New Issue
Block a user