Merge branch 'SillyTavern:staging' into staging

f1720be0179b34d3b19b5a5944332948bf0c4f4f

Yokayo <52032299+Yokayo@users.noreply.github.com>

Signed
4 files changed, +671 -1Ignore whitespace
public/scripts/extensions/tts/chatterbox.js+649 -0
@@ -0,0 +1,649 @@
1+import { saveTtsProviderSettings } from './index.js';
2+
3+export { ChatterboxTtsProvider };
4+
5+class ChatterboxTtsProvider {
6+ //########//
7+ // Config //
8+ //########//
9+
10+ settings = {};
11+ constructor() {
12+ // Initialize with default settings
13+ this.settings = {
14+ provider_endpoint: this.settings.provider_endpoint || 'http://localhost:8004',
15+ voice_mode: this.settings.voice_mode || 'predefined',
16+ predefined_voice: this.settings.predefined_voice || 'S1',
17+ reference_voice: this.settings.reference_voice || '',
18+ temperature: this.settings.temperature || 0.8,
19+ exaggeration: this.settings.exaggeration || 0.5,
20+ cfg_weight: this.settings.cfg_weight || 0.5,
21+ seed: this.settings.seed || -1,
22+ speed_factor: this.settings.speed_factor || 1.0,
23+ language: this.settings.language || 'en',
24+ split_text: this.settings.split_text || true,
25+ chunk_size: this.settings.chunk_size || 120,
26+ output_format: this.settings.output_format || 'wav',
27+ voiceMap: this.settings.voiceMap || {},
28+ };
29+ }
30+
31+ ready = false;
32+ voices = [];
33+ separator = '. ';
34+ audioElement = document.createElement('audio');
35+
36+ languageLabels = {
37+ 'English': 'en',
38+ 'Spanish': 'es',
39+ 'French': 'fr',
40+ 'German': 'de',
41+ 'Italian': 'it',
42+ 'Portuguese': 'pt',
43+ 'Polish': 'pl',
44+ 'Turkish': 'tr',
45+ 'Russian': 'ru',
46+ 'Dutch': 'nl',
47+ 'Czech': 'cs',
48+ 'Arabic': 'ar',
49+ 'Chinese': 'zh-cn',
50+ 'Japanese': 'ja',
51+ 'Korean': 'ko',
52+ 'Hindi': 'hi',
53+ };
54+
55+ get settingsHtml() {
56+ let html = `<div class="chatterbox-settings-container">
57+ <div class="chatterbox-settings-header">
58+ <h3>Chatterbox TTS Settings</h3>
59+ <div class="status-indicator">
60+ Status: <span id="chatterbox-status" class="offline">Offline</span>
61+ </div>
62+ </div>`;
63+
64+ // Server endpoint
65+ html += `<div class="chatterbox-setting-row">
66+ <label for="chatterbox-endpoint">Server Endpoint:</label>
67+ <input id="chatterbox-endpoint" type="text" class="text_pole" value="${this.settings.provider_endpoint}" />
68+ </div>`;
69+
70+ // Language selection
71+ html += `<div class="chatterbox-setting-row">
72+ <label for="chatterbox-language">Language:</label>
73+ <select id="chatterbox-language">`;
74+ for (let language in this.languageLabels) {
75+ html += `<option value="${this.languageLabels[language]}" ${this.languageLabels[language] === this.settings.language ? 'selected' : ''}>${language}</option>`;
76+ }
77+ html += `</select>
78+ </div>`;
79+
80+ // Generation parameters
81+ html += `<div class="chatterbox-params-section">
82+ <h4>Generation Parameters</h4>`;
83+
84+ // Temperature
85+ html += `<div class="chatterbox-setting-row">
86+ <label for="chatterbox-temperature">Temperature: <span id="chatterbox-temperature-value">${this.settings.temperature}</span></label>
87+ <input id="chatterbox-temperature" type="range" min="0" max="1" step="0.1" value="${this.settings.temperature}" />
88+ </div>`;
89+
90+ // Exaggeration
91+ html += `<div class="chatterbox-setting-row">
92+ <label for="chatterbox-exaggeration">Exaggeration: <span id="chatterbox-exaggeration-value">${this.settings.exaggeration}</span></label>
93+ <input id="chatterbox-exaggeration" type="range" min="0" max="2" step="0.1" value="${this.settings.exaggeration}" />
94+ </div>`;
95+
96+ // CFG Weight
97+ html += `<div class="chatterbox-setting-row">
98+ <label for="chatterbox-cfg-weight">CFG Weight: <span id="chatterbox-cfg-weight-value">${this.settings.cfg_weight}</span></label>
99+ <input id="chatterbox-cfg-weight" type="range" min="0" max="1" step="0.1" value="${this.settings.cfg_weight}" />
100+ </div>`;
101+
102+ // Speed Factor
103+ html += `<div class="chatterbox-setting-row">
104+ <label for="chatterbox-speed">Speed Factor: <span id="chatterbox-speed-value">${this.settings.speed_factor}</span></label>
105+ <input id="chatterbox-speed" type="range" min="0.5" max="2" step="0.1" value="${this.settings.speed_factor}" />
106+ </div>`;
107+
108+ // Seed
109+ html += `<div class="chatterbox-setting-row">
110+ <label for="chatterbox-seed">Seed (-1 for random):</label>
111+ <input id="chatterbox-seed" class="text_pole" type="number" min="-1" value="${this.settings.seed}" />
112+ </div>`;
113+
114+ // Text chunking
115+ html += `<div class="chatterbox-setting-row">
116+ <label class="checkbox_label">
117+ <input type="checkbox" id="chatterbox-split-text" ${this.settings.split_text ? 'checked' : ''} />
118+ Split long texts into chunks
119+ </label>
120+ </div>`;
121+
122+ // Chunk size
123+ html += `<div class="chatterbox-setting-row" id="chunk-size-row" ${!this.settings.split_text ? 'style="display: none;"' : ''}>
124+ <label for="chatterbox-chunk-size">Chunk Size:</label>
125+ <input id="chatterbox-chunk-size" class="text_pole" type="number" min="50" max="500" value="${this.settings.chunk_size}" />
126+ </div>`;
127+
128+ // Output format
129+ html += `<div class="chatterbox-setting-row">
130+ <label for="chatterbox-format">Output Format:</label>
131+ <select id="chatterbox-format">
132+ <option value="wav" ${this.settings.output_format === 'wav' ? 'selected' : ''}>WAV</option>
133+ <option value="opus" ${this.settings.output_format === 'opus' ? 'selected' : ''}>Opus</option>
134+ </select>
135+ </div>`;
136+
137+ html += '</div>'; // End params section
138+
139+ // Footer with links
140+ html += `<div class="chatterbox-footer">
141+ <a href="${this.settings.provider_endpoint}" target="_blank">Chatterbox Web UI</a> |
142+ <a href="https://github.com/devnen/Chatterbox-TTS-Server" target="_blank">Documentation</a>
143+ </div>`;
144+
145+ html += '</div>'; // End container
146+
147+ // Add CSS styles
148+ html += `<style>
149+ .chatterbox-settings-container {
150+ padding: 10px;
151+ }
152+ .chatterbox-settings-header {
153+ display: flex;
154+ justify-content: space-between;
155+ align-items: center;
156+ margin-bottom: 15px;
157+ }
158+ .chatterbox-settings-header h3 {
159+ margin: 0;
160+ }
161+ .chatterbox-settings-container .status-indicator {
162+ font-weight: bold;
163+ }
164+ #chatterbox-status.ready { color: #4CAF50; }
165+ #chatterbox-status.offline { color: #f44336; }
166+ #chatterbox-status.processing { color: #2196F3; }
167+ .chatterbox-setting-row {
168+ margin-bottom: 10px;
169+ display: flex;
170+ align-items: center;
171+ gap: 10px;
172+ }
173+ .chatterbox-setting-row label {
174+ flex: 0 0 150px;
175+ }
176+ .chatterbox-setting-row label.checkbox_label {
177+ flex-basis: auto;
178+ }
179+ .chatterbox-setting-row input[type="text"],
180+ .chatterbox-setting-row input[type="number"],
181+ .chatterbox-setting-row select {
182+ flex: 1;
183+ }
184+ .chatterbox-setting-row input[type="range"] {
185+ flex: 1;
186+ }
187+ .chatterbox-params-section {
188+ margin-top: 15px;
189+ padding-top: 15px;
190+ border-top: 1px solid #ccc;
191+ }
192+ .chatterbox-params-section h4 {
193+ margin-top: 0;
194+ margin-bottom: 10px;
195+ }
196+ .chatterbox-footer {
197+ margin-top: 15px;
198+ padding-top: 15px;
199+ border-top: 1px solid #ccc;
200+ text-align: center;
201+ font-size: 0.9em;
202+ }
203+ </style>`;
204+
205+ return html;
206+ }
207+
208+ //######################//
209+ // Startup & Initialize //
210+ //######################//
211+
212+ async loadSettings(settings) {
213+ this.updateStatus('Offline');
214+
215+ if (Object.keys(settings).length === 0) {
216+ console.info('Using default Chatterbox TTS Provider settings');
217+ } else {
218+ // Populate settings with provided values
219+ for (const key in settings) {
220+ if (key in this.settings) {
221+ this.settings[key] = settings[key];
222+ }
223+ }
224+ }
225+
226+ // Update UI elements
227+ this.updateUIFromSettings();
228+
229+ console.debug('ChatterboxTTS: Settings loaded');
230+
231+ try {
232+ // Check if TTS provider is ready
233+ await this.checkReady();
234+
235+ if (this.ready) {
236+ // Fetch all voice types for the voice map
237+ await this.fetchTtsVoiceObjects();
238+ this.updateStatus('Ready');
239+ }
240+
241+ this.setupEventListeners();
242+
243+ } catch (error) {
244+ console.error('Error loading Chatterbox settings:', error);
245+ this.updateStatus('Offline');
246+ }
247+ }
248+
249+ updateUIFromSettings() {
250+ $('#chatterbox-endpoint').val(this.settings.provider_endpoint);
251+ $('#chatterbox-language').val(this.settings.language);
252+ $('#chatterbox-temperature').val(this.settings.temperature);
253+ $('#chatterbox-temperature-value').text(this.settings.temperature);
254+ $('#chatterbox-exaggeration').val(this.settings.exaggeration);
255+ $('#chatterbox-exaggeration-value').text(this.settings.exaggeration);
256+ $('#chatterbox-cfg-weight').val(this.settings.cfg_weight);
257+ $('#chatterbox-cfg-weight-value').text(this.settings.cfg_weight);
258+ $('#chatterbox-speed').val(this.settings.speed_factor);
259+ $('#chatterbox-speed-value').text(this.settings.speed_factor);
260+ $('#chatterbox-seed').val(this.settings.seed);
261+ $('#chatterbox-split-text').prop('checked', this.settings.split_text);
262+ $('#chatterbox-chunk-size').val(this.settings.chunk_size);
263+ $('#chatterbox-format').val(this.settings.output_format);
264+
265+ // Show/hide chunk size based on split text
266+ if (this.settings.split_text) {
267+ $('#chunk-size-row').show();
268+ } else {
269+ $('#chunk-size-row').hide();
270+ }
271+ }
272+
273+ //##############################//
274+ // Check Server is Available //
275+ //##############################//
276+
277+ async checkReady() {
278+ try {
279+ const response = await fetch(`${this.settings.provider_endpoint}/api/ui/initial-data`);
280+
281+ if (!response.ok) {
282+ throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
283+ }
284+
285+ const data = await response.json();
286+
287+ // Check if we got valid data
288+ if (data) {
289+ this.ready = true;
290+ console.log('Chatterbox TTS service is ready.');
291+ } else {
292+ this.ready = false;
293+ console.log('Chatterbox TTS service returned invalid data.');
294+ }
295+ } catch (error) {
296+ console.error('Error checking Chatterbox TTS service readiness:', error);
297+ this.ready = false;
298+ }
299+ }
300+
301+ //######################//
302+ // Get Available Voices //
303+ //######################//
304+
305+ async fetchTtsVoiceObjects() {
306+ try {
307+ // Always fetch predefined voices
308+ const predefinedResponse = await fetch(`${this.settings.provider_endpoint}/get_predefined_voices`);
309+ if (!predefinedResponse.ok) {
310+ throw new Error(`HTTP ${predefinedResponse.status}: ${predefinedResponse.statusText}`);
311+ }
312+
313+ const predefinedData = await predefinedResponse.json();
314+
315+ // Transform predefined voices
316+ const predefinedVoices = predefinedData.map(voice => ({
317+ name: voice.display_name,
318+ voice_id: voice.voice_id || voice.filename,
319+ preview_url: null,
320+ lang: voice.language || 'en',
321+ }));
322+
323+ // Always try to fetch reference voices
324+ let referenceVoices = [];
325+ try {
326+ const refResponse = await fetch(`${this.settings.provider_endpoint}/get_reference_files`);
327+ if (refResponse.ok) {
328+ const refData = await refResponse.json();
329+ referenceVoices = refData.map(filename => ({
330+ name: `[Clone] ${filename}`,
331+ voice_id: `ref_${filename}`,
332+ preview_url: null,
333+ lang: 'en',
334+ }));
335+ }
336+ } catch (error) {
337+ console.warn('Failed to fetch reference voices:', error);
338+ }
339+
340+ // Combine all voices
341+ this.voices = [...predefinedVoices, ...referenceVoices];
342+
343+ console.log(`Loaded ${this.voices.length} voices (${predefinedVoices.length} predefined, ${referenceVoices.length} reference)`);
344+ return this.voices;
345+ } catch (error) {
346+ console.error('Error fetching Chatterbox voices:', error);
347+ this.voices = [];
348+ return [];
349+ }
350+ }
351+
352+ // Alias for internal use
353+ async fetchVoices() {
354+ return this.fetchTtsVoiceObjects();
355+ }
356+
357+ //###########################//
358+ // Setup Event Listeners //
359+ //###########################//
360+
361+ setupEventListeners() {
362+ // Server endpoint change
363+ $('#chatterbox-endpoint').on('input', () => {
364+ this.settings.provider_endpoint = $('#chatterbox-endpoint').val();
365+ this.onSettingsChange();
366+ });
367+
368+ // Language
369+ $('#chatterbox-language').on('change', (e) => {
370+ this.settings.language = e.target.value;
371+ this.onSettingsChange();
372+ });
373+
374+ // Parameter sliders
375+ $('#chatterbox-temperature').on('input', (e) => {
376+ this.settings.temperature = parseFloat(e.target.value);
377+ $('#chatterbox-temperature-value').text(this.settings.temperature);
378+ this.onSettingsChange();
379+ });
380+
381+ $('#chatterbox-exaggeration').on('input', (e) => {
382+ this.settings.exaggeration = parseFloat(e.target.value);
383+ $('#chatterbox-exaggeration-value').text(this.settings.exaggeration);
384+ this.onSettingsChange();
385+ });
386+
387+ $('#chatterbox-cfg-weight').on('input', (e) => {
388+ this.settings.cfg_weight = parseFloat(e.target.value);
389+ $('#chatterbox-cfg-weight-value').text(this.settings.cfg_weight);
390+ this.onSettingsChange();
391+ });
392+
393+ $('#chatterbox-speed').on('input', (e) => {
394+ this.settings.speed_factor = parseFloat(e.target.value);
395+ $('#chatterbox-speed-value').text(this.settings.speed_factor);
396+ this.onSettingsChange();
397+ });
398+
399+ // Seed
400+ $('#chatterbox-seed').on('change', (e) => {
401+ this.settings.seed = parseInt(e.target.value);
402+ this.onSettingsChange();
403+ });
404+
405+ // Text splitting
406+ $('#chatterbox-split-text').on('change', (e) => {
407+ this.settings.split_text = e.target.checked;
408+ if (e.target.checked) {
409+ $('#chunk-size-row').show();
410+ } else {
411+ $('#chunk-size-row').hide();
412+ }
413+ this.onSettingsChange();
414+ });
415+
416+ $('#chatterbox-chunk-size').on('change', (e) => {
417+ this.settings.chunk_size = parseInt(e.target.value);
418+ this.onSettingsChange();
419+ });
420+
421+ // Output format
422+ $('#chatterbox-format').on('change', (e) => {
423+ this.settings.output_format = e.target.value;
424+ this.onSettingsChange();
425+ });
426+ }
427+
428+ //#############################//
429+ // Store ST interface settings //
430+ //#############################//
431+
432+ onSettingsChange() {
433+ // Save the updated settings
434+ saveTtsProviderSettings();
435+ }
436+
437+ //#########################//
438+ // Handle Reload button //
439+ //#########################//
440+
441+ async onRefreshClick() {
442+ try {
443+ this.updateStatus('Processing');
444+ await this.checkReady();
445+
446+ if (this.ready) {
447+ await this.fetchTtsVoiceObjects();
448+ this.updateStatus('Ready');
449+ } else {
450+ this.updateStatus('Offline');
451+ }
452+ } catch (error) {
453+ console.error('Error during refresh:', error);
454+ this.updateStatus('Offline');
455+ }
456+ }
457+
458+ //##################//
459+ // Preview Voice //
460+ //##################//
461+
462+ async previewTtsVoice(voiceId) {
463+ try {
464+ this.updateStatus('Processing');
465+
466+ const previewText = 'Hello! This is a preview of the selected voice.';
467+
468+ // Determine if this is a reference voice
469+ let isReferenceVoice = false;
470+ let actualVoiceId = voiceId;
471+
472+ if (voiceId && voiceId.startsWith('ref_')) {
473+ isReferenceVoice = true;
474+ actualVoiceId = voiceId.substring(4); // Remove 'ref_' prefix
475+ }
476+
477+ // Generate preview using the main TTS endpoint
478+ const requestBody = {
479+ text: previewText,
480+ voice_mode: isReferenceVoice ? 'clone' : 'predefined',
481+ temperature: this.settings.temperature,
482+ exaggeration: this.settings.exaggeration,
483+ cfg_weight: this.settings.cfg_weight,
484+ seed: this.settings.seed >= 0 ? this.settings.seed : Math.floor(Math.random() * 2147483648), // Use random seed if -1
485+ speed_factor: this.settings.speed_factor,
486+ language: this.settings.language,
487+ split_text: false, // Don't split for preview
488+ output_format: this.settings.output_format,
489+ };
490+
491+ // Add voice-specific parameters
492+ if (isReferenceVoice) {
493+ requestBody.reference_audio_filename = actualVoiceId;
494+ } else {
495+ requestBody.predefined_voice_id = actualVoiceId;
496+ }
497+
498+ const response = await fetch(`${this.settings.provider_endpoint}/tts`, {
499+ method: 'POST',
500+ headers: {
501+ 'Content-Type': 'application/json',
502+ },
503+ body: JSON.stringify(requestBody),
504+ });
505+
506+ if (!response.ok) {
507+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
508+ }
509+
510+ // Get the audio blob and play it
511+ const audioBlob = await response.blob();
512+ const audioUrl = URL.createObjectURL(audioBlob);
513+
514+ const audio = new Audio(audioUrl);
515+ audio.addEventListener('ended', () => {
516+ URL.revokeObjectURL(audioUrl);
517+ this.updateStatus('Ready');
518+ });
519+
520+ await audio.play();
521+
522+ } catch (error) {
523+ console.error('Error previewing voice:', error);
524+ this.updateStatus('Ready');
525+ throw error;
526+ }
527+ }
528+
529+ //#####################//
530+ // Get Voice Object //
531+ //#####################//
532+
533+ async getVoice(voiceName) {
534+ // Ensure voices are loaded
535+ if (this.voices.length === 0) {
536+ await this.fetchTtsVoiceObjects();
537+ }
538+
539+ // Find the voice object by name or voice_id
540+ let match = this.voices.find(voice =>
541+ voice.name === voiceName ||
542+ voice.voice_id === voiceName ||
543+ voice.display_name === voiceName,
544+ );
545+
546+ if (!match) {
547+ console.warn(`Voice not found: ${voiceName}`);
548+ // Check if it's a reference voice that wasn't in the list
549+ if (voiceName && voiceName.startsWith('ref_')) {
550+ const filename = voiceName.substring(4);
551+ return {
552+ name: `[Clone] ${filename}`,
553+ voice_id: voiceName,
554+ preview_url: null,
555+ lang: 'en',
556+ };
557+ }
558+ // Return a default voice object
559+ return {
560+ name: voiceName || 'Default',
561+ voice_id: voiceName || this.settings.predefined_voice || 'S1',
562+ preview_url: null,
563+ lang: 'en',
564+ };
565+ }
566+
567+ return match;
568+ }
569+
570+ //##################//
571+ // Generate TTS //
572+ //##################//
573+
574+ async generateTts(inputText, voiceId) {
575+ try {
576+ this.updateStatus('Processing');
577+
578+ // Determine if this is a reference voice
579+ let isReferenceVoice = false;
580+ let actualVoiceId = voiceId;
581+
582+ if (voiceId && voiceId.startsWith('ref_')) {
583+ isReferenceVoice = true;
584+ actualVoiceId = voiceId.substring(4); // Remove 'ref_' prefix
585+ }
586+
587+ // Prepare the request body
588+ const requestBody = {
589+ text: inputText,
590+ voice_mode: isReferenceVoice ? 'clone' : 'predefined',
591+ temperature: this.settings.temperature,
592+ exaggeration: this.settings.exaggeration,
593+ cfg_weight: this.settings.cfg_weight,
594+ seed: this.settings.seed >= 0 ? this.settings.seed : Math.floor(Math.random() * 2147483648), // Use random seed if -1
595+ speed_factor: this.settings.speed_factor,
596+ language: this.settings.language,
597+ split_text: this.settings.split_text,
598+ chunk_size: this.settings.chunk_size,
599+ output_format: this.settings.output_format,
600+ };
601+
602+ // Add voice-specific parameters
603+ if (isReferenceVoice) {
604+ requestBody.reference_audio_filename = actualVoiceId;
605+ } else {
606+ requestBody.predefined_voice_id = actualVoiceId || this.settings.predefined_voice;
607+ }
608+
609+ console.log('Generating TTS with params:', requestBody);
610+
611+ const response = await fetch(`${this.settings.provider_endpoint}/tts`, {
612+ method: 'POST',
613+ headers: {
614+ 'Content-Type': 'application/json',
615+ 'Cache-Control': 'no-cache',
616+ },
617+ body: JSON.stringify(requestBody),
618+ });
619+
620+ if (!response.ok) {
621+ const errorText = await response.text();
622+ console.error('TTS generation error:', errorText);
623+ throw new Error(`HTTP ${response.status}: ${errorText}`);
624+ }
625+
626+ this.updateStatus('Ready');
627+
628+ // Return the response directly - SillyTavern expects a Response object
629+ return response;
630+
631+ } catch (error) {
632+ console.error('Error in generateTts:', error);
633+ this.updateStatus('Ready');
634+ throw error;
635+ }
636+ }
637+
638+ //######################//
639+ // Update Status //
640+ //######################//
641+
642+ updateStatus(status) {
643+ const statusElement = document.getElementById('chatterbox-status');
644+ if (statusElement) {
645+ statusElement.textContent = status;
646+ statusElement.className = status.toLowerCase();
647+ }
648+ }
649+}
public/scripts/extensions/tts/index.js+2 -0
@@ -27,6 +27,7 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
2727import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
2828import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
2929import { GoogleTranslateTtsProvider } from './google-translate.js';
30+import { ChatterboxTtsProvider } from './chatterbox.js';
3031import { KokoroTtsProvider } from './kokoro.js';
3132import { TtsWebuiProvider } from './tts-webui.js';
3233
@@ -89,6 +90,7 @@ export function getPreviewString(lang) {
8990const ttsProviders = {
9091 AllTalk: AllTalkTtsProvider,
9192 Azure: AzureTtsProvider,
93+ Chatterbox: ChatterboxTtsProvider,
9294 Coqui: CoquiTtsProvider,
9395 'CosyVoice (Unofficial)': CosyVoiceProvider,
9496 Edge: EdgeTtsProvider,
public/scripts/slash-commands.js+16 -1
@@ -54,7 +54,7 @@ import { getMessageTimeStamp, isMobile } from './RossAscends-mods.js';
5454import { hideChatMessageRange } from './chats.js';
5555import { getContext, saveMetadataDebounced } from './extensions.js';
5656import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
5757import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js';
5858import { chat_completion_sources, oai_settings, promptManager } from './openai.js';
5959import { user_avatar } from './personas.js';
6060import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
@@ -944,6 +944,12 @@ export function initDefaultSlashCommands() {
944944 `,
945945 }));
946946 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
947+ name: 'member-count',
948+ callback: countGroupMemberCallback,
949+ aliases: ['countmember', 'membercount'],
950+ helpString: 'Returns the total number of group members in the group chat list.',
951+ }));
952+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
947953 name: 'delswipe',
948954 callback: deleteSwipeCallback,
949955 returns: 'the new, currently selected swipe id',
@@ -3546,6 +3552,15 @@ async function peekCallback(_, arg) {
35463552 return '';
35473553}
35483554
3555+async function countGroupMemberCallback() {
3556+ if (!selected_group) {
3557+ toastr.warning('Cannot run /member-count command outside of a group chat.');
3558+ return '';
3559+ }
3560+
3561+ return getGroupMembers(selected_group).length;
3562+}
3563+
35493564async function removeGroupMemberCallback(_, arg) {
35503565 if (!selected_group) {
35513566 toastr.warning('Cannot run /member-remove command outside of a group chat.');
public/style.css+4 -0
@@ -715,6 +715,10 @@ hr {
715715 opacity: 0.2;
716716}
717717
718+#chat hr {
719+ opacity: 0.4;
720+}
721+
718722#bg1,
719723#bg_custom {
720724 background-repeat: no-repeat;