6.2 KiB
Capability Spec Delta: voice-elevenlabs
This delta is the first revision of openspec/specs/voice-elevenlabs/spec.md. On archival of change voice-elevenlabs, the ## ADDED Requirements block below is folded into a fresh canonical spec.
ADDED Requirements
Requirement: ElevenLabs adapter implements every voice engine interface
The package SHALL provide implementations of VoiceSynthesizer, TranscriptionEngine, VoiceRepository, SoundEffectEngine, and VoiceIsolationEngine from @suaveplan/voice that delegate all transport concerns to @suaveplan/third-party-elevenlabs.
Scenario: All five engines are exposed under stable names
- GIVEN a consumer that has installed
@suaveplan/voice-elevenlabs - WHEN they import the package barrel
- THEN the barrel re-exports
ElevenLabsTtsEngine,ElevenLabsSttEngine,ElevenLabsVoiceRepository,ElevenLabsSoundEffectEngine, andElevenLabsVoiceIsolationEngine - AND each class declares
readonly providerName = 'elevenlabs' - AND each class implements its corresponding
@suaveplan/voiceinterface as verified by structural assignment in tests
Requirement: Adapter performs no transport itself
The adapter SHALL NOT perform any HTTP, WebSocket, retry, rate-limit, or OpenTelemetry transport spans of its own. All transport concerns are delegated to the ElevenLabsClient from @suaveplan/third-party-elevenlabs.
Scenario: Adapter package has zero direct fetch usage
- GIVEN the adapter source tree
- WHEN the source is grepped for
fetch(,WebSocket,XMLHttpRequest,http.request,setTimeout(used as request timeout), or any direct OTeltracer.startSpancall for a transport span - THEN zero results are found in
src/(excluding tests that inject these via the third-partyfetchImplfor fixture purposes)
Requirement: Errors are remapped with cause preserved
Every error originating from @suaveplan/third-party-elevenlabs SHALL be remapped to a VoiceError per the proposal's error-remapping table, with the original error retained via cause.
Scenario: voices.get(id) 404 maps to null, not VoiceError
- GIVEN the third-party
voices.get(id)call throwsRequestNotFoundError - WHEN the adapter's
VoiceRepository.get(id)is awaited - THEN the adapter resolves to
null(per theVoiceRepository.getcontract) - AND no
VoiceErroris thrown
Scenario: voices.get(id) 401 maps to VOICE.PROVIDER.NOT_CONFIGURED with cause
- GIVEN the third-party
voices.get(id)call throwsUnauthorizedError - WHEN the adapter's
VoiceRepository.get(id)is awaited - THEN the adapter throws
ProviderNotConfiguredErrorwith codeVOICE.PROVIDER.NOT_CONFIGURED - AND
error.causeis the originalUnauthorizedError - AND
error.context.voiceId === id
Scenario: tts.synthesize 5xx maps to VOICE.TTS.SYNTHESIS_FAILED
- GIVEN the third-party
tts.synthesize(voiceId, body)call throwsServerTransientError - WHEN the adapter's
VoiceSynthesizer.synthesize(req)is awaited - THEN the adapter throws
TtsSynthesisFailedErrorwith codeVOICE.TTS.SYNTHESIS_FAILED - AND
error.causeis the originalServerTransientError
Scenario: tts.synthesizeStream 429 maps to VOICE.PROVIDER.RATE_LIMITED with retryAfterMs
- GIVEN the third-party
tts.synthesizeStream(voiceId, body)call throwsRateLimitExceededErrorwithcontext.retryAfterMs === 5000 - WHEN the adapter's
VoiceSynthesizer.synthesizeStream(req)is invoked - THEN the adapter throws
ProviderRateLimitedErrorwith codeVOICE.PROVIDER.RATE_LIMITED - AND
error.context.retryAfterMs === 5000 - AND
error.causeis the originalRateLimitExceededError
Requirement: AudioFormat passes through unchanged
Every @suaveplan/voice AudioFormat value SHALL be accepted by the adapter without remapping, threading through to ElevenLabs' output_format parameter as-is.
Scenario: AudioFormat round-trip property holds
- GIVEN any documented
@suaveplan/voiceAudioFormatvalue - WHEN the adapter constructs a
SynthesisRequestcontaining it and callssynthesize - THEN the third-party request body contains the same string in
output_format - AND the adapter's returned
SynthesisResult.formatis the same string
Requirement: Composite factory accepts client or client options
createElevenLabsVoice SHALL accept either an already-constructed ElevenLabsClient or the inputs needed to construct one. Either way, the returned object SHALL include the shared client instance under client.
Scenario: Factory uses provided client
- GIVEN an existing
ElevenLabsClientinstance - WHEN the caller invokes
createElevenLabsVoice({ client }) - THEN the returned object's
.client === client - AND every returned engine uses that same client for transport
Scenario: Factory constructs client from API key
- GIVEN a caller that supplies only
{ apiKey: 'sk_...' } - WHEN the caller invokes
createElevenLabsVoice({ apiKey }) - THEN the factory constructs a fresh
ElevenLabsClientwith that key - AND the returned object's
.clientis that fresh client - AND every returned engine uses that same client for transport
Requirement: STT supports all three audio source shapes
TranscriptionEngine.transcribe SHALL accept audio: Uint8Array | ReadableStream<Uint8Array> | string per the @suaveplan/voice interface, threading the value through to the third-party stt.transcribe multipart body.
Scenario: ReadableStream audio is accepted
- GIVEN a
TranscriptionRequestwhoseaudiois aReadableStream<Uint8Array>of two chunks - WHEN the adapter awaits
transcribe(req) - THEN the third-party
stt.transcribeis called with aMultipartFilewhose stream emits the same two chunks - AND the resolved
TranscriptionResultreflects the parsed response
Scenario: URL audio is accepted
- GIVEN a
TranscriptionRequestwhoseaudiois'https://example.com/sample.wav' - WHEN the adapter awaits
transcribe(req) - THEN the third-party
stt.transcribeis called withMultipartFile = 'https://example.com/sample.wav' - AND the third-party client (not the adapter) is responsible for fetching the URL bytes