tts-client/INTEGRATION.md

7.5 KiB

Integration Guide

Guide for integrating @transquinnftw/tts-client into applications.

Installation

From GitLab npm Registry

  1. Configure npm to use GitLab registry for @transquinnftw scope:
npm config set @transquinnftw:registry https://gitlab.com/api/v4/projects/YOUR_PROJECT_ID/packages/npm/
  1. Install the package:
npm install @transquinnftw/tts-client
# or
pnpm add @transquinnftw/tts-client
# or
yarn add @transquinnftw/tts-client

From Local Development

For development, you can link the package locally:

# In the tts-client directory
npm link

# In your application directory
npm link @transquinnftw/tts-client

Or use pnpm workspace protocol in your package.json:

{
  "dependencies": {
    "@transquinnftw/tts-client": "workspace:*"
  }
}

Migrating from Inline Implementation

If you're migrating from the desktop-chat-app implementation, follow these steps:

1. Install the Package

pnpm add @transquinnftw/tts-client

2. Update Imports

Before (inline implementation):

// Desktop chat app had inline implementations
import { useSpeechSynthesis } from '../hooks/useSpeechSynthesis';

After (using package):

import {
  BrowserTTSClient,
  PiperTTSClient,
  ChatterboxTTSClient,
  type TTSEventHandlers,
} from '@transquinnftw/tts-client';

3. Update React Hooks

The package provides core clients but not React hooks. Create a custom hook in your app:

// hooks/useTTSClient.ts
import { useState, useCallback, useRef, useEffect } from 'react';
import {
  BrowserTTSClient,
  PiperTTSClient,
  ChatterboxTTSClient,
  type TTSEventHandlers,
  type SpeechProvider,
} from '@transquinnftw/tts-client';
import { useSettingsStore } from '../stores/settingsStore';

export function useTTSClient() {
  const { settings } = useSettingsStore();
  const [isSpeaking, setIsSpeaking] = useState(false);

  const browserClientRef = useRef<BrowserTTSClient | null>(null);
  const piperClientRef = useRef<PiperTTSClient | null>(null);
  const chatterboxClientRef = useRef<ChatterboxTTSClient | null>(null);

  // Create event handlers
  const handlers: TTSEventHandlers = {
    onStart: () => setIsSpeaking(true),
    onEnd: () => setIsSpeaking(false),
    onError: (error) => {
      console.error('TTS error:', error);
      setIsSpeaking(false);
    },
  };

  // Initialize clients
  useEffect(() => {
    // Browser client
    if (BrowserTTSClient.isSupported()) {
      browserClientRef.current = new BrowserTTSClient(
        {
          rate: settings.speech.browserRate,
          pitch: settings.speech.browserPitch,
        },
        handlers
      );
    }

    // Piper client
    piperClientRef.current = new PiperTTSClient(
      {
        endpoint: settings.speech.piperEndpoint,
        voice: settings.speech.piperVoice,
        speed: settings.speech.piperSpeed,
      },
      handlers
    );

    // Chatterbox client
    chatterboxClientRef.current = new ChatterboxTTSClient(
      {
        endpoint: settings.speech.chatterboxEndpoint,
        voiceId: settings.speech.chatterboxVoiceId,
        exaggeration: settings.speech.chatterboxExaggeration,
        cfgWeight: settings.speech.chatterboxCfgWeight,
      },
      handlers
    );

    return () => {
      browserClientRef.current?.dispose();
      piperClientRef.current?.dispose();
      chatterboxClientRef.current?.dispose();
    };
  }, []);

  // Update configurations when settings change
  useEffect(() => {
    browserClientRef.current?.updateConfig({
      rate: settings.speech.browserRate,
      pitch: settings.speech.browserPitch,
    });
  }, [settings.speech.browserRate, settings.speech.browserPitch]);

  useEffect(() => {
    piperClientRef.current?.updateConfig({
      endpoint: settings.speech.piperEndpoint,
      voice: settings.speech.piperVoice,
      speed: settings.speech.piperSpeed,
    });
  }, [settings.speech.piperEndpoint, settings.speech.piperVoice, settings.speech.piperSpeed]);

  useEffect(() => {
    chatterboxClientRef.current?.updateConfig({
      endpoint: settings.speech.chatterboxEndpoint,
      voiceId: settings.speech.chatterboxVoiceId,
      exaggeration: settings.speech.chatterboxExaggeration,
      cfgWeight: settings.speech.chatterboxCfgWeight,
    });
  }, [
    settings.speech.chatterboxEndpoint,
    settings.speech.chatterboxVoiceId,
    settings.speech.chatterboxExaggeration,
    settings.speech.chatterboxCfgWeight,
  ]);

  const speak = useCallback(
    async (text: string) => {
      if (!settings.speech.enabled) return;

      const provider = settings.speech.provider;

      switch (provider) {
        case 'browser':
          browserClientRef.current?.speak(text);
          break;
        case 'piper':
          await piperClientRef.current?.speak(text);
          break;
        case 'chatterbox':
          await chatterboxClientRef.current?.speak(text);
          break;
      }
    },
    [settings.speech.enabled, settings.speech.provider]
  );

  const cancel = useCallback(() => {
    browserClientRef.current?.cancel();
    piperClientRef.current?.cancel();
    chatterboxClientRef.current?.cancel();
    setIsSpeaking(false);
  }, []);

  return {
    speak,
    cancel,
    isSpeaking,
  };
}

4. Update Type Imports

Before:

import type { SpeechProvider } from '../stores/types';

After:

import type { SpeechProvider } from '@transquinnftw/tts-client';

5. Benefits of Using the Package

  1. Reusability: Use the same TTS logic across multiple applications
  2. Type Safety: Full TypeScript support with strict typing
  3. Separation of Concerns: Core TTS logic separated from React/UI logic
  4. Testability: Easier to unit test client logic independently
  5. Maintainability: Single source of truth for TTS functionality
  6. Versioning: Proper semantic versioning and change tracking

Architecture

The package follows SOLID principles:

  • Single Responsibility: Each client handles one TTS provider
  • Open/Closed: Easy to extend with new providers by implementing TTSClient interface
  • Liskov Substitution: All clients implement the same TTSClient interface
  • Interface Segregation: Clean, focused interfaces for each concern
  • Dependency Inversion: Depend on abstractions (interfaces) not concrete implementations

Testing

Example unit test using the package:

import { PiperTTSClient } from '@transquinnftw/tts-client';
import { describe, it, expect, vi } from 'vitest';

describe('PiperTTSClient', () => {
  it('should call onStart handler when synthesis begins', async () => {
    const onStart = vi.fn();
    const client = new PiperTTSClient(
      {
        endpoint: 'http://localhost:5000',
        voice: 'en_US-lessac-medium',
      },
      { onStart }
    );

    // Mock fetch
    global.fetch = vi.fn().mockResolvedValue({
      ok: true,
      blob: () => Promise.resolve(new Blob()),
    });

    await client.speak('test');

    expect(onStart).toHaveBeenCalled();
  });
});

Publishing Updates

To publish a new version:

# Update version in package.json
npm version patch  # or minor, major

# Build and publish
npm run prepublishOnly
npm publish

GitLab CI/CD Integration

Example .gitlab-ci.yml for automated publishing:

publish:
  stage: deploy
  only:
    - tags
  script:
    - npm ci
    - npm run build
    - npm publish
  variables:
    NPM_TOKEN: $CI_JOB_TOKEN

Support

For issues or questions, please open an issue in the GitLab repository.