npm package: @superapp_men/text-to-speech
Description: Text-to-speech with multi-language support for SuperApp Partner Apps
Author: Your Organization
Latest version: 1.0.1

# @superapp_men/text-to-speech

Multi-language text-to-speech for SuperApp Partner Apps. Convert text to speech with language support, playback control, and real-time events.

[![npm version](https://img.shields.io/npm/v/@superapp_men/text-to-speech.svg)](https://www.npmjs.com/package/@superapp_men/text-to-speech)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- **Multi-language Support** - English, French, Spanish, Arabic and more
- **Playback Control** - Speak, stop, pause, and resume
- **Event-driven Architecture** - React to speech events in real-time
- **Configurable** - Control rate, pitch, and volume
- **Voice Discovery** - Query available voices per language
- **TypeScript** - Full type safety and IntelliSense
- **Cross-platform** - Works on Web, iOS, and Android
- **Zero Dependencies** - Lightweight and efficient

## Installation

```bash
npm install @superapp_men/text-to-speech
```

or

```bash
yarn add @superapp_men/text-to-speech
```

## Quick Start

```typescript
import { TextToSpeech, Language } from "@superapp_men/text-to-speech";

// Initialize
const tts = new TextToSpeech({
  timeout: 5000,
  debug: true,
});

// Check availability
const available = await tts.isAvailable();
if (!available) {
  console.log("Text-to-speech not available");
  return;
}

// Listen for events
tts.on("speakFinished", ({ duration }) => {
  console.log("Finished speaking, duration:", duration, "ms");
});

tts.on("error", ({ message }) => {
  console.error("Error:", message);
});

// Speak text in any supported language
await tts.speak({ text: "Hello world", language: Language.EN_US });
await tts.speak({ text: "Bonjour le monde", language: Language.FR_FR });
await tts.speak({ text: "Hola mundo", language: Language.ES_ES });
await tts.speak({ text: "مرحبا بالعالم", language: Language.AR_SA });
```

## Table of Contents

- [Basic Usage](#basic-usage)
- [Configuration](#configuration)
- [API Reference](#api-reference)
- [Events](#events)
- [React Integration](#react-integration)
- [Vue Integration](#vue-integration)
- [Supported Languages](#supported-languages)
- [Error Handling](#error-handling)
- [Best Practices](#best-practices)

## Basic Usage

### Simple Text-to-Speech

```typescript
import { TextToSpeech, Language } from "@superapp_men/text-to-speech";

async function speakText() {
  const tts = new TextToSpeech({ debug: true });

  // Check if available
  const available = await tts.isAvailable();
  if (!available) {
    console.log("Text-to-speech not available");
    return;
  }

  // Listen for completion
  tts.on("speakFinished", ({ duration }) => {
    console.log("Done speaking! Duration:", duration, "ms");
  });

  // Speak with language
  await tts.speak({
    text: "Bonjour, comment allez-vous ?",
    language: Language.FR_FR,
  });
}
```

### With Playback Control

```typescript
import { TextToSpeech } from "@superapp_men/text-to-speech";

const tts = new TextToSpeech();

// Start speaking
await tts.speak({
  text: "This is a long text that the user might want to pause.",
  language: "en-US",
  rate: 0.9,   // Slightly slower
  pitch: 1.0,
  volume: 1.0,
});

// Pause speech
await tts.pause();

// Resume speech
await tts.resume();

// Stop speech entirely
await tts.stop();
```

### With Event Listeners (React Example)

```typescript
import { useState, useEffect } from "react";
import { TextToSpeech, SpeechState, Language } from "@superapp_men/text-to-speech";

function TextToSpeechComponent() {
  const [tts] = useState(() => new TextToSpeech({ debug: true }));
  const [state, setState] = useState<SpeechState>(SpeechState.IDLE);
  const [isSpeaking, setIsSpeaking] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [text, setText] = useState("");
  const [language, setLanguage] = useState<string>(Language.EN_US);

  useEffect(() => {
    const unsubState = tts.on("stateChange", ({ state }) => {
      setState(state);
      setIsSpeaking(
        state === SpeechState.SPEAKING || state === SpeechState.PAUSED
      );
    });

    const unsubFinished = tts.on("speakFinished", ({ duration }) => {
      console.log("Finished speaking, duration:", duration, "ms");
    });

    const unsubError = tts.on("error", ({ message }) => {
      setError(message);
    });

    return () => {
      unsubState();
      unsubFinished();
      unsubError();
      tts.destroy();
    };
  }, [tts]);

  const handleSpeak = async () => {
    try {
      setError(null);
      await tts.speak({ text, language });
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to speak");
    }
  };

  const handleStop = async () => {
    await tts.stop();
  };

  return (
    <div>
      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Enter text to speak..."
      />

      <select value={language} onChange={(e) => setLanguage(e.target.value)}>
        <option value="en-US">English (US)</option>
        <option value="fr-FR">French</option>
        <option value="es-ES">Spanish</option>
        <option value="ar-SA">Arabic (Standard)</option>
        <option value="ar-MA">Arabic (Morocco)</option>
      </select>

      <button onClick={handleSpeak} disabled={isSpeaking || !text}>
        {isSpeaking ? "Speaking..." : "Speak"}
      </button>

      <button onClick={handleStop} disabled={!isSpeaking}>
        Stop
      </button>

      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}
```

## Configuration

### TextToSpeech Configuration

```typescript
interface TextToSpeechConfig {
  timeout?: number;  // Request timeout in ms (default: 5000)
  debug?: boolean;   // Enable debug logging (default: false)
}

const tts = new TextToSpeech({
  timeout: 10000,
  debug: true,
});
```

### Speak Configuration

```typescript
interface SpeakConfig {
  text: string;          // The text to speak (required)
  language?: string;     // Language/locale code (default: 'en-US')
  rate?: number;         // Speech rate 0.1 - 10 (default: 1.0)
  pitch?: number;        // Speech pitch 0 - 2 (default: 1.0)
  volume?: number;       // Speech volume 0 - 1 (default: 1.0)
}

await tts.speak({
  text: "Hello world",
  language: Language.EN_US,
  rate: 1.0,
  pitch: 1.0,
  volume: 1.0,
});
```

**Notes:**

- **`language`**: Determines the voice and pronunciation. Use the `Language` enum or any valid BCP-47 language tag (e.g. `"fr-FR"`, `"ar-MA"`).
- **`rate`**: `1.0` is normal speed, `0.5` is half speed, `2.0` is double speed.
- **`pitch`**: `1.0` is normal pitch, `0` is lowest, `2` is highest.
- **`volume`**: `1.0` is full volume, `0` is silent.

## API Reference

### TextToSpeech Class

#### Constructor

```typescript
new TextToSpeech(config?: TextToSpeechConfig)
```

#### Methods

##### `isAvailable(): Promise<boolean>`

Check if text-to-speech is available on the device.

```typescript
const available = await tts.isAvailable();
if (!available) {
  console.log("Text-to-speech not supported");
}
```

##### `getSupportedLanguages(): Promise<string[]>`

Get list of supported language codes.

```typescript
const languages = await tts.getSupportedLanguages();
console.log("Supported:", languages);
// ['en-US', 'es-ES', 'fr-FR', 'ar-SA', ...]
```

##### `getVoices(): Promise<VoiceInfo[]>`

Get available voices with their language and name.

```typescript
const voices = await tts.getVoices();
voices.forEach((voice) => {
  console.log(`${voice.name} (${voice.language})`);
});
```

**Returns:**

```typescript
interface VoiceInfo {
  voiceId: string;   // Voice identifier
  name: string;      // Human-readable name
  language: string;  // Language code (e.g. "en-US")
}
```

##### `speak(config: SpeakConfig): Promise<void>`

Speak the given text. If already speaking, the current speech is stopped before starting the new one.

```typescript
await tts.speak({
  text: "Bonjour le monde",
  language: Language.FR_FR,
  rate: 1.0,
});
```

##### `stop(): Promise<void>`

Stop current speech.

```typescript
await tts.stop();
```

##### `pause(): Promise<void>`

Pause current speech.

```typescript
await tts.pause();
```

##### `resume(): Promise<void>`

Resume paused speech.

```typescript
await tts.resume();
```

##### `getStatus(): Promise<StatusResponsePayload>`

Get current speech status from the SuperApp.

```typescript
const status = await tts.getStatus();
console.log("Is speaking:", status.isSpeaking);
console.log("State:", status.state);
```

##### `getState(): SpeechState`

Get current speech state.

**Returns:** `'idle' | 'speaking' | 'paused' | 'error'`

```typescript
const state = tts.getState();
```

##### `isSpeaking(): boolean`

Check if currently speaking or paused.

```typescript
if (tts.isSpeaking()) {
  await tts.stop();
}
```

##### `on<T>(event: TTSEventType, callback: EventListener<T>): () => void`

Add event listener. Returns unsubscribe function.

```typescript
const unsubscribe = tts.on("speakFinished", ({ duration }) => {
  console.log("Duration:", duration);
});

// Later, unsubscribe
unsubscribe();
```

##### `off<T>(event: TTSEventType, callback: EventListener<T>): void`

Remove event listener.

```typescript
tts.off("speakFinished", myCallback);
```

##### `removeAllListeners(event?: TTSEventType): void`

Remove all listeners for an event, or all events if none specified.

```typescript
tts.removeAllListeners("speakFinished");
tts.removeAllListeners();
```

##### `destroy(): void`

Cleanup and destroy the instance. Stops any current speech.

```typescript
tts.destroy();
```

## Events

### Event Types

```typescript
type TTSEventType =
  | "stateChange"     // Speech state changed
  | "speakStarted"    // Started speaking
  | "speakFinished"   // Finished speaking
  | "error";          // Error occurred
```

### stateChange

Fired when speech state changes.

```typescript
tts.on("stateChange", ({ state, previousState }) => {
  console.log(`${previousState} -> ${state}`);

  switch (state) {
    case "speaking":
      button.textContent = "Stop";
      break;
    case "paused":
      button.textContent = "Resume";
      break;
    case "idle":
      button.textContent = "Speak";
      break;
  }
});
```

### speakStarted

Fired when speech starts.

```typescript
tts.on("speakStarted", ({ sessionId, config }) => {
  console.log("Session started:", sessionId);
  console.log("Language:", config.language);
  console.log("Text:", config.text);
});
```

### speakFinished

Fired when speech finishes (naturally or via stop).

```typescript
tts.on("speakFinished", ({ sessionId, duration }) => {
  console.log("Session ended:", sessionId);
  console.log("Duration:", duration, "ms");
});
```

### error

Fired when an error occurs.

```typescript
tts.on("error", ({ code, message, details }) => {
  console.error(`Error [${code}]: ${message}`);

  switch (code) {
    case "NOT_SUPPORTED":
      alert("Text-to-speech is not supported on this device");
      break;
    case "INVALID_TEXT":
      alert("Please enter some text");
      break;
    case "SPEAK_FAILED":
      alert("Speech synthesis failed");
      break;
  }
});
```

## React Integration

### Custom Hook

```typescript
import { useState, useEffect, useCallback } from "react";
import { TextToSpeech, SpeechState } from "@superapp_men/text-to-speech";

function useTextToSpeech() {
  const [tts] = useState(() => new TextToSpeech());
  const [isSpeaking, setIsSpeaking] = useState(false);
  const [state, setState] = useState<SpeechState>(SpeechState.IDLE);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const unsubscribers = [
      tts.on("stateChange", ({ state }) => {
        setState(state);
        setIsSpeaking(
          state === SpeechState.SPEAKING || state === SpeechState.PAUSED
        );
      }),

      tts.on("error", ({ message }) => {
        setError(message);
      }),
    ];

    return () => {
      unsubscribers.forEach((unsub) => unsub());
      tts.destroy();
    };
  }, [tts]);

  const speak = useCallback(
    async (text: string, language: string = "en-US") => {
      setError(null);
      try {
        await tts.speak({ text, language });
      } catch (err: any) {
        setError(err.message);
      }
    },
    [tts]
  );

  const stop = useCallback(async () => {
    await tts.stop();
  }, [tts]);

  const pause = useCallback(async () => {
    await tts.pause();
  }, [tts]);

  const resume = useCallback(async () => {
    await tts.resume();
  }, [tts]);

  return { speak, stop, pause, resume, isSpeaking, state, error, tts };
}
```

### Usage in Component

```typescript
function MyComponent() {
  const { speak, stop, isSpeaking, error } = useTextToSpeech();

  return (
    <div>
      <button onClick={() => speak("Bonjour!", "fr-FR")} disabled={isSpeaking}>
        Speak French
      </button>
      <button onClick={() => speak("Hello!", "en-US")} disabled={isSpeaking}>
        Speak English
      </button>
      <button onClick={() => speak("مرحبا!", "ar-SA")} disabled={isSpeaking}>
        Speak Arabic
      </button>
      <button onClick={stop} disabled={!isSpeaking}>
        Stop
      </button>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}
```

## Vue Integration

### Composable

```typescript
import { ref, onUnmounted } from "vue";
import { TextToSpeech, SpeechState } from "@superapp_men/text-to-speech";

export function useTextToSpeech() {
  const tts = new TextToSpeech();
  const isSpeaking = ref(false);
  const state = ref<SpeechState>(SpeechState.IDLE);
  const error = ref<string | null>(null);

  tts.on("stateChange", ({ state: newState }) => {
    state.value = newState;
    isSpeaking.value =
      newState === SpeechState.SPEAKING || newState === SpeechState.PAUSED;
  });

  tts.on("error", ({ message }) => {
    error.value = message;
  });

  onUnmounted(() => {
    tts.destroy();
  });

  const speak = async (text: string, language: string = "en-US") => {
    error.value = null;
    try {
      await tts.speak({ text, language });
    } catch (err: any) {
      error.value = err.message;
    }
  };

  const stop = async () => {
    await tts.stop();
  };

  const pause = async () => {
    await tts.pause();
  };

  const resume = async () => {
    await tts.resume();
  };

  return { speak, stop, pause, resume, isSpeaking, state, error, tts };
}
```

### Component

```vue
<template>
  <div>
    <input v-model="text" placeholder="Enter text to speak..." />

    <select v-model="language">
      <option value="en-US">English (US)</option>
      <option value="fr-FR">French</option>
      <option value="es-ES">Spanish</option>
      <option value="ar-SA">Arabic (Standard)</option>
      <option value="ar-MA">Arabic (Morocco)</option>
    </select>

    <button @click="speak(text, language)" :disabled="isSpeaking || !text">
      {{ isSpeaking ? "Speaking..." : "Speak" }}
    </button>

    <button @click="stop()" :disabled="!isSpeaking">Stop</button>

    <p v-if="error" class="error">{{ error }}</p>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useTextToSpeech } from "./useTextToSpeech";

const text = ref("");
const language = ref("en-US");

const { speak, stop, isSpeaking, error } = useTextToSpeech();
</script>
```

## Supported Languages

### Language Enum

```typescript
enum Language {
  EN_US = "en-US",  // English (US)
  ES_ES = "es-ES",  // Spanish (Spain)
  FR_FR = "fr-FR",  // French
  AR_SA = "ar-SA",  // Arabic (Standard)
  AR_MA = "ar-MA",  // Arabic (Morocco)
}
```

### Using Custom Language Codes

You can pass any valid BCP-47 language tag:

```typescript
await tts.speak({ text: "Guten Tag", language: "de-DE" });
await tts.speak({ text: "Ciao mondo", language: "it-IT" });
await tts.speak({ text: "Olá mundo", language: "pt-BR" });
```

**Note:** Actual language/voice availability depends on the device. Use `getSupportedLanguages()` and `getVoices()` to check what's available at runtime.

## Error Handling

### Error Codes

```typescript
enum TextToSpeechError {
  NOT_SUPPORTED = "NOT_SUPPORTED",
  SPEAK_FAILED = "SPEAK_FAILED",
  NOT_SPEAKING = "NOT_SPEAKING",
  ALREADY_SPEAKING = "ALREADY_SPEAKING",
  LANGUAGE_NOT_SUPPORTED = "LANGUAGE_NOT_SUPPORTED",
  INVALID_TEXT = "INVALID_TEXT",
  CANCELLED = "CANCELLED",
  SUPERAPP_NOT_AVAILABLE = "SUPERAPP_NOT_AVAILABLE",
  TIMEOUT = "TIMEOUT",
}
```

### Handling Errors

```typescript
import { TextToSpeech, TextToSpeechError } from "@superapp_men/text-to-speech";

const tts = new TextToSpeech();

// Try-catch for methods
try {
  await tts.speak({ text: "Hello", language: "en-US" });
} catch (error: any) {
  switch (error.code) {
    case TextToSpeechError.NOT_SUPPORTED:
      alert("Text-to-speech not supported on this device");
      break;
    case TextToSpeechError.INVALID_TEXT:
      alert("Please provide text to speak");
      break;
    default:
      console.error("Error:", error.message);
  }
}

// Event listener for errors
tts.on("error", ({ code, message }) => {
  console.error(`Error [${code}]: ${message}`);
});
```

## Best Practices

### 1. Always Check Availability

```typescript
const available = await tts.isAvailable();
if (!available) {
  // Show alternative (e.g. no audio feedback)
  return;
}
```

### 2. Stop Before Speaking New Text

The `speak()` method automatically stops any current speech before starting new speech, so you don't need to call `stop()` manually:

```typescript
// This is safe - previous speech is auto-stopped
await tts.speak({ text: "First sentence", language: "en-US" });
await tts.speak({ text: "Second sentence", language: "fr-FR" });
```

### 3. Query Available Languages at Runtime

```typescript
const languages = await tts.getSupportedLanguages();
const voices = await tts.getVoices();

// Filter voices for a specific language
const frenchVoices = voices.filter((v) => v.language.startsWith("fr"));
```

### 4. Use Appropriate Speech Rate for Education

```typescript
// Slower rate for language learning
await tts.speak({
  text: "Bonjour, comment allez-vous ?",
  language: "fr-FR",
  rate: 0.8, // Slightly slower for clarity
});

// Normal rate for feedback
await tts.speak({
  text: "Correct! Well done!",
  language: "en-US",
  rate: 1.0,
});
```

### 5. Clean Up Resources

```typescript
// In React
useEffect(() => {
  const tts = new TextToSpeech();
  // ... use tts
  return () => {
    tts.destroy();
  };
}, []);

// In Vue
onUnmounted(() => {
  tts.destroy();
});

// Vanilla JS
window.addEventListener("beforeunload", () => {
  tts.destroy();
});
```

### 6. Handle Background/Foreground

```typescript
document.addEventListener("visibilitychange", async () => {
  if (document.hidden && tts.isSpeaking()) {
    await tts.stop();
  }
});
```

## Browser Support

- Chrome/Edge 33+
- Firefox 49+
- Safari 7+
- iOS Safari 7+ (via Capacitor)
- Android WebView (via Capacitor)

**Note:** Voice availability varies by platform and device. Mobile devices via Capacitor typically provide the widest range of voices and languages.

## Troubleshooting

### "Text-to-speech not available"

**Cause:** Device or browser doesn't support the SpeechSynthesis API
**Solution:** Check with `isAvailable()` and provide a fallback

### "No voice found for language"

**Cause:** The requested language voice is not installed on the device
**Solution:** Use `getSupportedLanguages()` to check available languages, or let the system use a fallback voice

### Speech sounds robotic

**Solution:**
- Try different voices with `getVoices()`
- Adjust `rate` and `pitch` for more natural speech
- Some platforms have higher quality voices than others

### Speech cuts off on mobile

**Cause:** Some mobile browsers pause speech synthesis when the app goes to background
**Solution:** Handle the `visibilitychange` event and inform the user

## License

MIT

## Support

- Email: h.afifi@alexsys.solutions

---

**Ready to add text-to-speech to your partner app? Install the package and start speaking!**