Imported from Jellypod-Inc/speech-sdk (
AGENTS.md). Install upstream withnpx skills add Jellypod-Inc/speech-sdk. Copyright stays with the author.
Speech SDK
This file provides guidance when working with code in this repository.
Commands
pnpm install # install dependencies
pnpm build # compile TypeScript (tsc)
pnpm test # run unit tests
pnpm test -- -t "test name" # run a single test by name
pnpm run test:e2e # run e2e tests (requires OPENAI_API_KEY / ELEVENLABS_API_KEY)
pnpm run typecheck # type-check without emitting
pnpm fix # format/lint via ultracite (biome)
pnpm check # check for lint issues
E2E tests hit real provider APIs and require keys in .env or exported in shell. Unit tests are in src/__tests__/*.test.ts, e2e tests in src/__tests__/e2e/*.e2e.test.ts.
Architecture
This is @speech-sdk/core — a universal TTS SDK (Node, Edge, Browser) with a single public function generateSpeech() and a provider abstraction for multi-provider support.
Core flow: generateSpeech() → resolveModel() → provider.generate() → SpeechResult
src/generate-speech.ts— the public API entry point; handles retry logic viap-retrysrc/resolve-provider.ts—"provider/model"strings resolve to that provider's own factory viaPROVIDER_FACTORIES;ResolvedModelinstances pass through unchangedsrc/speech-provider.ts—SpeechProviderinterface all providers implementsrc/speech-result.ts—DefaultGeneratedAudioFilewith lazy base64 conversionsrc/provider-utils.ts— sharedresolveApiKey()andhandleErrorResponse()src/providers/openai/andsrc/providers/elevenlabs/— provider implementations
Two ways to name a model (both hit the provider's own API directly):
- String (
"openai/tts-1") →resolveModel()looks the prefix up inPROVIDER_FACTORIESand builds that provider with default config; reads the per-provider env var (OPENAI_API_KEY) unlessapiKeyis passed to the call. A bare provider id ("openai") uses the provider'sdefaultModel. - Factory (
createOpenAI()("tts-1")) → same provider, but lets you setbaseURL,fetch,fallbackSTT, and other per-provider config.
Adding a new provider:
- Create
src/providers/<name>/index.tswith a<Name>SpeechProviderclass implementingSpeechProviderand acreate<Name>()factory. - Add subpath export in
package.jsonunderexports. - Register the provider id and factory in
PROVIDER_FACTORIESinsrc/resolve-provider.tsso"<id>/<model>"strings resolve to it. - Implement
resolveOutputFormat(modelId, output)so the SDK can request the user's chosen output format natively from the API. Return{ providerOptions, expectedMediaType }for each format the provider supports; for formats the provider can't produce natively, return options that yield a decodable wav/pcm so the SDK can convert via mediabunny. Returnundefinedfor unknown model ids. The SDK never decodes compressed audio — providers must produce wav/pcm for any format the user requests that isn't natively available. - Implement
getStitchOptions(modelId)so the conversation stitch path can request decodable wav/pcm regardless of user format preference (the stitch pipeline always operates on raw samples).
Key Conventions
- ESM-only (
"type": "module"in package.json); use.jsextensions in imports - TypeScript strict mode, target ES2022
providerOptionsare passed through to provider APIs untransformed- Tests use vitest with globals enabled
- Run
pnpm fixbefore committing to ensure formatting compliance
Versioning & Releases
Follow semver. Prereleases use the canonical 0.N.M-alpha.K form so they collapse cleanly into the corresponding stable 0.N.M.
- Stable:
0.8.0,0.8.1,0.9.0. Published to npmlatest. - Prerelease:
0.9.0-alpha.0,0.9.0-alpha.1, … all pre-patches of the same target0.9.0. Published to npmnext. - When the next stable is cut, drop the suffix:
0.9.0-alpha.3→0.9.0. Do not keep incrementing the minor/patch on the alpha track (e.g.0.9.0-alpha,0.9.1-alpha,0.9.2-alpha) — that creates phantom stable versions that never shipped and confuses npm's version ordering. - Bump the alpha counter (
-alpha.K), not the minor/patch, between prereleases of the same target. - Breaking changes bump the minor while pre-1.0 (
0.8.0→0.9.0); features alone can ride a patch on a stable line if no API changes.
Code Standards
Formatting and linting enforced by Biome via ultracite. Husky pre-commit hook runs tests and lint automatically.
TypeScript
- Prefer
unknownoverany - Use const assertions (
as const) for immutable values - Leverage type narrowing instead of type assertions
- Use
constby default,letonly when needed, nevervar - Use
async/awaitover promise chains - Prefer
for...ofover.forEach()
Comments
- Default to no comments. Add one only when the WHY is non-obvious — a hidden constraint, a subtle invariant, a workaround, or a spec/RFC reference
- Single-line only. Never write multi-line
//blocks or block comments outside of JSDoc on exported APIs - Don't explain WHAT the code does — well-named identifiers already do that
- Don't reference the current task, PR, fix, or callers ("added for X", "used by Y") — that rots; put it in the PR description
Error Handling
- Throw
Errorobjects with descriptive messages, not strings - Prefer early returns over nested conditionals
- Don't catch errors just to rethrow them
Testing
- Write assertions inside
it()ortest()blocks - Use async/await, not done callbacks
- Don't commit
.onlyor.skip
Ultracite Code Standards
This project uses Ultracite, a zero-config preset that enforces strict code quality standards through automated formatting and linting.
Quick Reference
- Format code:
pnpm dlx ultracite fix - Check for issues:
pnpm dlx ultracite check - Diagnose setup:
pnpm dlx ultracite doctor
Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable.
Core Principles
Write code that is accessible, performant, type-safe, and maintainable. Focus on clarity and explicit intent over brevity.
Type Safety & Explicitness
- Use explicit types for function parameters and return values when they enhance clarity
- Prefer
unknownoveranywhen the type is genuinely unknown - Use const assertions (
as const) for immutable values and literal types - Leverage TypeScript's type narrowing instead of type assertions
- Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Modern JavaScript/TypeScript
- Use arrow functions for callbacks and short functions
- Prefer
for...ofloops over.forEach()and indexedforloops - Use optional chaining (
?.) and nullish coalescing (??) for safer property access - Prefer template literals over string concatenation
- Use destructuring for object and array assignments
- Use
constby default,letonly when reassignment is needed, nevervar
Async & Promises
- Always
awaitpromises in async functions - don't forget to use the return value - Use
async/awaitsyntax instead of promise chains for better readability - Handle errors appropriately in async code with try-catch blocks
- Don't use async functions as Promise executors
React & JSX
- Use function components over class components
- Call hooks at the top level only, never conditionally
- Specify all dependencies in hook dependency arrays correctly
- Use the
keyprop for elements in iterables (prefer unique IDs over array indices) - Nest children between opening and closing tags instead of passing as props
- Don't define components inside other components
- Use semantic HTML and ARIA attributes for accessibility:
- Provide meaningful alt text for images
- Use proper heading hierarchy
- Add labels for form inputs
- Include keyboard event handlers alongside mouse events
- Use semantic elements (
<button>,<nav>, etc.) instead of divs with roles
Error Handling & Debugging
- Remove
console.log,debugger, andalertstatements from production code - Throw
Errorobjects with descriptive messages, not strings or other values - Use
try-catchblocks meaningfully - don't catch errors just to rethrow them - Prefer early returns over nested conditionals for error cases
Code Organization
- Keep functions focused and under reasonable cognitive complexity limits
- Extract complex conditions into well-named boolean variables
- Use early returns to reduce nesting
- Prefer simple conditionals over nested ternary operators
- Group related code together and separate concerns
Security
- Add
rel="noopener"when usingtarget="_blank"on links - Avoid
dangerouslySetInnerHTMLunless absolutely necessary - Don't use
eval()or assign directly todocument.cookie - Validate and sanitize user input
Performance
- Avoid spread syntax in accumulators within loops
- Use top-level regex literals instead of creating them in loops
- Prefer specific imports over namespace imports
- Avoid barrel files (index files that re-export everything)
- Use proper image components (e.g., Next.js
<Image>) over<img>tags
Framework-Specific Guidance
Next.js:
- Use Next.js
<Image>component for images - Use
next/heador App Router metadata API for head elements - Use Server Components for async data fetching instead of async Client Components
React 19+:
- Use ref as a prop instead of
React.forwardRef
Solid/Svelte/Vue/Qwik:
- Use
classandforattributes (notclassNameorhtmlFor)
Testing
- Write assertions inside
it()ortest()blocks - Avoid done callbacks in async tests - use async/await instead
- Don't use
.onlyor.skipin committed code - Keep test suites reasonably flat - avoid excessive
describenesting
When Biome Can't Help
Biome's linter will catch most issues automatically. Focus your attention on:
- Business logic correctness - Biome can't validate your algorithms
- Meaningful naming - Use descriptive names for functions, variables, and types
- Architecture decisions - Component structure, data flow, and API design
- Edge cases - Handle boundary conditions and error states
- User experience - Accessibility, performance, and usability considerations
- Documentation - Add comments for complex logic, but prefer self-documenting code
Most formatting and common issues are automatically fixed by Biome. Run pnpm dlx ultracite fix before committing to ensure compliance.