package app import ( "context" "errors" "strings" "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/core/domain" "gitea.nik4nao.com/nik/home-services/tts-gateway/internal/core/ports/driven" ) // ErrSpeakerNotFound is returned when the requested speaker name has no // matching entry in domain.Speakers. var ErrSpeakerNotFound = errors.New("speaker not found") type TTSApp struct { normalizer driven.TextNormalizer engine driven.TTSEngine encoder driven.AudioEncoder nameToID map[string]int } // NewTTSApp constructs the synthesis application service. func NewTTSApp(normalizer driven.TextNormalizer, engine driven.TTSEngine, encoder driven.AudioEncoder) *TTSApp { nameToID := make(map[string]int, len(domain.Speakers)) for _, s := range domain.Speakers { nameToID[s.Name] = s.ID } return &TTSApp{normalizer: normalizer, engine: engine, encoder: encoder, nameToID: nameToID} } // Synthesize normalizes text, runs the TTS engine, and encodes the result. func (a *TTSApp) Synthesize(ctx context.Context, speakerName, text string, params domain.SynthesisParams) (*domain.AudioClip, error) { speakerID, ok := a.nameToID[speakerName] if !ok { return nil, ErrSpeakerNotFound } symbolIDs, err := a.normalizer.Normalize(ctx, text) if err != nil { return nil, err } pcm, sampleRate, err := a.engine.Synthesize(ctx, speakerID, symbolIDs, params) if err != nil { return nil, err } data, mimeType, err := a.encoder.Encode(ctx, pcm, sampleRate) if err != nil { return nil, err } return &domain.AudioClip{Data: data, MimeType: mimeType}, nil } // ListSpeakers returns speaker names filtered by a case-insensitive substring // match, or the full roster when search is empty - matching uma-tts-api's // /speakers endpoint. func (a *TTSApp) ListSpeakers(_ context.Context, search string) []string { search = strings.ToLower(search) names := make([]string, 0, len(domain.Speakers)) for _, s := range domain.Speakers { if search == "" || strings.Contains(strings.ToLower(s.Name), search) { names = append(names, s.Name) } } return names }