Blame Raw
· · · 151 lines (4.6 KB)
0 contributors
1import { getRequestHeaders } from '../../../script.js';
2import { splitRecursive } from '../../utils.js';
3import { getPreviewString, saveTtsProviderSettings } from './index.js';
4
5export class PollinationsTtsProvider {
6 settings;
7 voices = [];
8 separator = ' . ';
9 audioElement = document.createElement('audio');
10
11 defaultSettings = {
12 // TODO: Make this configurable
13 model: 'openai-audio',
14 voiceMap: {},
15 };
16
17 get settingsHtml() {
18 return '';
19 }
20
21 onSettingsChange() {
22 this.voices = [];
23 saveTtsProviderSettings();
24 }
25
26 async loadSettings(settings) {
27 // Populate Provider UI given input settings
28 if (Object.keys(settings).length == 0) {
29 console.info('Using default TTS Provider settings');
30 }
31
32 // Only accept keys defined in defaultSettings
33 this.settings = this.defaultSettings;
34
35 for (const key in settings) {
36 if (key in this.settings) {
37 this.settings[key] = settings[key];
38 } else {
39 throw `Invalid setting passed to TTS Provider: ${key}`;
40 }
41 }
42
43 try {
44 await this.checkReady();
45 console.debug('Pollinations TTS: Settings loaded');
46 } catch {
47 console.debug('Pollinations TTS: Settings loaded, but not ready');
48 }
49 }
50
51 // Perform a simple readiness check by trying to fetch voiceIds
52 async checkReady() {
53 await this.fetchTtsVoiceObjects();
54 }
55
56 async onRefreshClick() {
57 await this.checkReady();
58 }
59
60 //#################//
61 // TTS Interfaces //
62 //#################//
63
64 async getVoice(voiceName) {
65 if (this.voices.length == 0) {
66 this.voices = await this.fetchTtsVoiceObjects();
67 }
68 const match = this.voices.filter(
69 voice => voice.name == voiceName || voice.voice_id == voiceName,
70 )[0];
71 if (!match) {
72 throw `TTS Voice name ${voiceName} not found`;
73 }
74 return match;
75 }
76
77 /**
78 * Generate TTS audio for the given text using the specified voice.
79 * @param {string} text Text to generate
80 * @param {string} voiceId Voice ID
81 * @returns {AsyncGenerator<Response>} Audio response generator
82 */
83 generateTts(text, voiceId) {
84 return this.fetchTtsGeneration(text, voiceId);
85 }
86
87 //###########//
88 // API CALLS //
89 //###########//
90 async fetchTtsVoiceObjects() {
91 const response = await fetch('/api/speech/pollinations/voices', {
92 method: 'POST',
93 headers: getRequestHeaders(),
94 body: JSON.stringify({ model: this.settings.model }),
95 });
96
97 if (!response.ok) {
98 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
99 }
100 const responseJson = await response.json();
101 return responseJson
102 .sort()
103 .map(x => ({ name: x, voice_id: x, preview_url: false, lang: 'en-US' }));
104 }
105
106 /**
107 * Preview TTS for a given voice ID.
108 * @param {string} id Voice ID
109 */
110 async previewTtsVoice(id) {
111 this.audioElement.pause();
112 this.audioElement.currentTime = 0;
113 const voice = await this.getVoice(id);
114 const text = getPreviewString(voice.lang);
115 for await (const response of this.generateTts(text, id)) {
116 const audio = await response.blob();
117 const url = URL.createObjectURL(audio);
118 await new Promise(resolve => {
119 const audioElement = new Audio();
120 audioElement.src = url;
121 audioElement.play();
122 audioElement.onended = () => resolve();
123 });
124 URL.revokeObjectURL(url);
125 }
126 }
127
128 async* fetchTtsGeneration(text, voiceId) {
129 const MAX_LENGTH = 1000;
130 console.info(`Generating new TTS for voice_id ${voiceId}`);
131 const chunks = splitRecursive(text, MAX_LENGTH);
132 for (const chunk of chunks) {
133 const response = await fetch('/api/speech/pollinations/generate', {
134 method: 'POST',
135 headers: getRequestHeaders(),
136 body: JSON.stringify({
137 model: this.settings.model,
138 text: 'Say exactly this and nothing else:' + '\n' + chunk,
139 voice: voiceId,
140 }),
141 });
142
143 if (!response.ok) {
144 toastr.error(response.statusText, 'TTS Generation Failed');
145 throw new Error(`HTTP ${response.status}: ${await response.text()}`);
146 }
147
148 yield response;
149 }
150 }
151}