2023-08-09 03:30:26 +02:00
/ *
TODO :
2023-08-15 21:15:31 +02:00
- load RVC models list from extras
- Settings per characters
2023-08-09 03:30:26 +02:00
* /
import { saveSettingsDebounced } from "../../../script.js" ;
2023-08-16 17:54:52 +02:00
import { getContext , getApiUrl , extension _settings , doExtrasFetch , ModuleWorkerWrapper , modules } from "../../extensions.js" ;
2023-08-17 11:05:17 +02:00
export { MODULE _NAME , rvcVoiceConversion } ;
2023-08-09 03:30:26 +02:00
const MODULE _NAME = 'RVC' ;
const DEBUG _PREFIX = "<RVC module> "
2023-08-15 21:15:31 +02:00
const UPDATE _INTERVAL = 1000
2023-08-09 03:30:26 +02:00
2023-08-15 21:15:31 +02:00
let charactersList = [ ] // Updated with module worker
let rvcModelsList = [ ] // Initialized only once
2023-08-17 01:16:57 +02:00
let rvcModelsReceived = false ;
2023-08-09 03:30:26 +02:00
2023-08-17 11:05:17 +02:00
function updateVoiceMapText ( ) {
2023-08-15 21:15:31 +02:00
let voiceMapText = ""
2023-08-17 11:05:17 +02:00
for ( let i in extension _settings . rvc . voiceMap ) {
2023-08-15 21:15:31 +02:00
const voice _settings = extension _settings . rvc . voiceMap [ i ] ;
voiceMapText += i + ":"
2023-08-17 11:05:17 +02:00
+ voice _settings [ "modelName" ] + "("
+ voice _settings [ "pitchExtraction" ] + ","
+ voice _settings [ "pitchOffset" ] + ","
+ voice _settings [ "indexRate" ] + ","
+ voice _settings [ "filterRadius" ] + ","
+ voice _settings [ "rmsMixRate" ] + ","
+ voice _settings [ "protect" ]
+ "),\n"
2023-08-09 03:30:26 +02:00
}
2023-08-15 21:15:31 +02:00
extension _settings . rvc . voiceMapText = voiceMapText ;
$ ( '#rvc_voice_map' ) . val ( voiceMapText ) ;
2023-08-09 03:30:26 +02:00
2023-08-17 11:05:17 +02:00
console . debug ( DEBUG _PREFIX , "Updated voice map debug text to\n" , voiceMapText )
2023-08-09 03:30:26 +02:00
}
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
enabled : false ,
2023-08-17 11:05:17 +02:00
model : "" ,
pitchOffset : 0 ,
pitchExtraction : "dio" ,
indexRate : 0.88 ,
filterRadius : 3 ,
rmsMixRate : 1 ,
protect : 0.33 ,
2023-08-09 03:30:26 +02:00
voicMapText : "" ,
voiceMap : { }
}
function loadSettings ( ) {
2023-08-20 20:46:53 +02:00
if ( extension _settings . rvc === undefined )
extension _settings . rvc = { } ;
2023-08-09 03:30:26 +02:00
if ( Object . keys ( extension _settings . rvc ) . length === 0 ) {
Object . assign ( extension _settings . rvc , defaultSettings )
}
2023-08-17 11:05:17 +02:00
$ ( '#rvc_enabled' ) . prop ( 'checked' , extension _settings . rvc . enabled ) ;
2023-08-09 03:30:26 +02:00
$ ( '#rvc_model' ) . val ( extension _settings . rvc . model ) ;
$ ( '#rvc_pitch_extraction' ) . val ( extension _settings . rvc . pitchExtraction ) ;
$ ( '#rvc_pitch_extractiont_value' ) . text ( extension _settings . rvc . pitchExtraction ) ;
$ ( '#rvc_index_rate' ) . val ( extension _settings . rvc . indexRate ) ;
$ ( '#rvc_index_rate_value' ) . text ( extension _settings . rvc . indexRate ) ;
$ ( '#rvc_filter_radius' ) . val ( extension _settings . rvc . filterRadius ) ;
$ ( "#rvc_filter_radius_value" ) . text ( extension _settings . rvc . filterRadius ) ;
2023-08-16 17:54:52 +02:00
2023-08-15 21:15:31 +02:00
$ ( '#rvc_pitch_offset' ) . val ( extension _settings . rvc . pitchOffset ) ;
$ ( '#rvc_pitch_offset_value' ) . text ( extension _settings . rvc . pitchOffset ) ;
$ ( '#rvc_rms_mix_rate' ) . val ( extension _settings . rvc . rmsMixRate ) ;
$ ( "#rvc_rms_mix_rate_value" ) . text ( extension _settings . rvc . rmsMixRate ) ;
2023-08-09 03:30:26 +02:00
$ ( '#rvc_protect' ) . val ( extension _settings . rvc . protect ) ;
$ ( "#rvc_protect_value" ) . text ( extension _settings . rvc . protect ) ;
$ ( '#rvc_voice_map' ) . val ( extension _settings . rvc . voiceMapText ) ;
}
async function onEnabledClick ( ) {
extension _settings . rvc . enabled = $ ( '#rvc_enabled' ) . is ( ':checked' ) ;
saveSettingsDebounced ( )
}
async function onPitchExtractionChange ( ) {
extension _settings . rvc . pitchExtraction = $ ( '#rvc_pitch_extraction' ) . val ( ) ;
saveSettingsDebounced ( )
}
async function onIndexRateChange ( ) {
extension _settings . rvc . indexRate = Number ( $ ( '#rvc_index_rate' ) . val ( ) ) ;
$ ( "#rvc_index_rate_value" ) . text ( extension _settings . rvc . indexRate )
saveSettingsDebounced ( )
}
async function onFilterRadiusChange ( ) {
extension _settings . rvc . filterRadius = Number ( $ ( '#rvc_filter_radius' ) . val ( ) ) ;
$ ( "#rvc_filter_radius_value" ) . text ( extension _settings . rvc . filterRadius )
saveSettingsDebounced ( )
}
async function onPitchOffsetChange ( ) {
extension _settings . rvc . pitchOffset = Number ( $ ( '#rvc_pitch_offset' ) . val ( ) ) ;
$ ( "#rvc_pitch_offset_value" ) . text ( extension _settings . rvc . pitchOffset )
saveSettingsDebounced ( )
}
2023-08-15 21:15:31 +02:00
async function onRmsMixRateChange ( ) {
extension _settings . rvc . rmsMixRate = Number ( $ ( '#rvc_rms_mix_rate' ) . val ( ) ) ;
$ ( "#rvc_rms_mix_rate_value" ) . text ( extension _settings . rvc . rmsMixRate )
saveSettingsDebounced ( )
}
2023-08-09 03:30:26 +02:00
async function onProtectChange ( ) {
extension _settings . rvc . protect = Number ( $ ( '#rvc_protect' ) . val ( ) ) ;
$ ( "#rvc_protect_value" ) . text ( extension _settings . rvc . protect )
saveSettingsDebounced ( )
}
2023-08-15 21:15:31 +02:00
async function onApplyClick ( ) {
let error = false ;
const character = $ ( "#rvc_character_select" ) . val ( ) ;
const model _name = $ ( "#rvc_model_select" ) . val ( ) ;
const pitchExtraction = $ ( "#rvc_pitch_extraction" ) . val ( ) ;
const indexRate = $ ( "#rvc_index_rate" ) . val ( ) ;
const filterRadius = $ ( "#rvc_filter_radius" ) . val ( ) ;
const pitchOffset = $ ( "#rvc_pitch_offset" ) . val ( ) ;
const rmsMixRate = $ ( "#rvc_rms_mix_rate" ) . val ( ) ;
const protect = $ ( "#rvc_protect" ) . val ( ) ;
if ( character === "none" ) {
2023-08-17 11:05:17 +02:00
toastr . error ( "Character not selected." , DEBUG _PREFIX + " voice mapping apply" , { timeOut : 10000 , extendedTimeOut : 20000 , preventDuplicates : true } ) ;
2023-08-15 21:15:31 +02:00
return ;
}
if ( model _name == "none" ) {
2023-08-17 11:05:17 +02:00
toastr . error ( "Model not selected." , DEBUG _PREFIX + " voice mapping apply" , { timeOut : 10000 , extendedTimeOut : 20000 , preventDuplicates : true } ) ;
2023-08-15 21:15:31 +02:00
return ;
}
extension _settings . rvc . voiceMap [ character ] = {
"modelName" : model _name ,
"pitchExtraction" : pitchExtraction ,
"indexRate" : indexRate ,
"filterRadius" : filterRadius ,
"pitchOffset" : pitchOffset ,
"rmsMixRate" : rmsMixRate ,
"protect" : protect
}
updateVoiceMapText ( ) ;
2023-08-17 11:05:17 +02:00
console . debug ( DEBUG _PREFIX , "Updated settings of " , character , ":" , extension _settings . rvc . voiceMap [ character ] )
2023-08-15 21:15:31 +02:00
saveSettingsDebounced ( ) ;
}
async function onDeleteClick ( ) {
const character = $ ( "#rvc_character_select" ) . val ( ) ;
if ( character === "none" ) {
2023-08-17 11:05:17 +02:00
toastr . error ( "Character not selected." , DEBUG _PREFIX + " voice mapping delete" , { timeOut : 10000 , extendedTimeOut : 20000 , preventDuplicates : true } ) ;
2023-08-15 21:15:31 +02:00
return ;
}
delete extension _settings . rvc . voiceMap [ character ] ;
2023-08-17 11:05:17 +02:00
console . debug ( DEBUG _PREFIX , "Deleted settings of " , character ) ;
2023-08-15 21:15:31 +02:00
updateVoiceMapText ( ) ;
saveSettingsDebounced ( ) ;
}
2023-08-19 04:34:39 +02:00
async function onChangeUploadFiles ( ) {
2023-08-17 04:47:41 +02:00
const url = new URL ( getApiUrl ( ) ) ;
2023-08-19 04:34:39 +02:00
const inputFiles = $ ( "#rvc_model_upload_files" ) . get ( 0 ) . files ;
2023-08-17 04:47:41 +02:00
let formData = new FormData ( ) ;
2023-08-17 11:05:17 +02:00
for ( const file of inputFiles )
2023-08-17 04:47:41 +02:00
formData . append ( file . name , file ) ;
2023-08-17 11:05:17 +02:00
console . debug ( DEBUG _PREFIX , "Sending files:" , formData ) ;
2023-08-17 04:47:41 +02:00
url . pathname = '/api/voice-conversion/rvc/upload-models' ;
const apiResult = await doExtrasFetch ( url , {
method : 'POST' ,
body : formData
} ) ;
if ( ! apiResult . ok ) {
2023-08-17 11:05:17 +02:00
toastr . error ( apiResult . statusText , DEBUG _PREFIX + ' Check extras console for errors log' ) ;
2023-08-17 04:47:41 +02:00
throw new Error ( ` HTTP ${ apiResult . status } : ${ await apiResult . text ( ) } ` ) ;
}
2023-08-19 04:34:39 +02:00
alert ( 'The files have been uploaded successfully.' ) ;
2023-08-17 04:47:41 +02:00
}
2023-08-09 03:30:26 +02:00
$ ( document ) . ready ( function ( ) {
function addExtensionControls ( ) {
const settingsHtml = `
< div id = "rvc_settings" >
< div class = "inline-drawer" >
< div class = "inline-drawer-toggle inline-drawer-header" >
< b > RVC < / b >
< div class = "inline-drawer-icon fa-solid fa-circle-chevron-down down" > < / d i v >
< / d i v >
< div class = "inline-drawer-content" >
2023-08-19 04:34:39 +02:00
< h4 class = "center" > Characters Voice Mapping < / h 4 >
2023-08-09 03:30:26 +02:00
< div >
< label class = "checkbox_label" for = "rvc_enabled" >
< input type = "checkbox" id = "rvc_enabled" name = "rvc_enabled" >
< small > Enabled < / s m a l l >
< / l a b e l >
2023-08-15 21:15:31 +02:00
< label > Voice Map ( debug infos ) < / l a b e l >
< textarea id = "rvc_voice_map" type = "text" class = "text_pole textarea_compact" rows = "4"
placeholder = "Voice map will appear here for debug purpose" > < / t e x t a r e a >
2023-08-09 03:30:26 +02:00
< / d i v >
< div >
2023-08-19 04:34:39 +02:00
< div class = "background_controls" >
< label for = "rvc_character_select" > Character : < / l a b e l >
< select id = "rvc_character_select" >
<!-- Populated by JS -- >
< / s e l e c t >
< div id = "rvc_delete" class = "menu_button" >
< i class = "fa-solid fa-times" > < / i >
Remove
< / d i v >
< / d i v >
< div class = "background_controls" >
< label for = "rvc_model_select" > Voice : < / l a b e l >
< select id = "rvc_model_select" >
<!-- Populated by JS -- >
< / s e l e c t >
< div id = "rvc_model_refresh_button" class = "menu_button" >
< i class = "fa-solid fa-refresh" > < / i >
<!-- Refresh -- >
< / d i v >
< div id = "rvc_model_upload_select_button" class = "menu_button" >
< i class = "fa-solid fa-upload" > < / i >
Upload
< / d i v >
< input
type = "file"
id = "rvc_model_upload_files"
accept = ".zip,.rar,.7zip,.7z" multiple / >
< / d i v >
< / d i v >
2023-08-17 04:47:41 +02:00
< div >
2023-08-19 04:34:39 +02:00
< small >
Upload one archive per model . With . pth and . index ( optional ) inside . < br / >
Supported format : . zip . rar . 7 zip . 7 z
< / s m a l l >
< / d i v >
< div >
< h4 > Model Settings < / h 4 >
< / d i v >
< div >
< label for = "rvc_pitch_extraction" >
Pitch Extraction
< / l a b e l >
< select id = "rvc_pitch_extraction" >
< option value = "dio" > dio < / o p t i o n >
< option value = "pm" > pm < / o p t i o n >
< option value = "harvest" > harvest < / o p t i o n >
< option value = "torchcrepe" > torchcrepe < / o p t i o n >
< option value = "rmvpe" > rmvpe < / o p t i o n >
< option value = "" > None < / o p t i o n >
< / s e l e c t >
< small >
Tips : dio and pm faster , harvest slower but good . < br / >
Torchcrepe and rmvpe are good but uses GPU .
< / s m a l l >
< / d i v >
< div >
< label for = "rvc_index_rate" >
Search feature ratio ( < span id = "rvc_index_rate_value" > < / s p a n > )
< / l a b e l >
< input id = "rvc_index_rate" type = "range" min = "0" max = "1" step = "0.01" value = "0.5" / >
< small >
Controls accent strength , too high may produce artifact .
< / s m a l l >
< / d i v >
< div >
< label for = "rvc_filter_radius" > Filter radius ( < span id = "rvc_filter_radius_value" > < / s p a n > ) < / l a b e l >
< input id = "rvc_filter_radius" type = "range" min = "0" max = "7" step = "1" value = "3" / >
< small >
Higher can reduce breathiness but may increase run time .
< / s m a l l >
< / d i v >
< div >
< label for = "rvc_pitch_offset" > Pitch offset ( < span id = "rvc_pitch_offset_value" > < / s p a n > ) < / l a b e l >
< input id = "rvc_pitch_offset" type = "range" min = "-20" max = "20" step = "1" value = "0" / >
< small >
Recommended + 12 key for male to female conversion and - 12 key for female to male conversion .
< / s m a l l >
< / d i v >
< div >
< label for = "rvc_rms_mix_rate" > Mix rate ( < span id = "rvc_rms_mix_rate_value" > < / s p a n > ) < / l a b e l >
< input id = "rvc_rms_mix_rate" type = "range" min = "0" max = "1" step = "0.01" value = "1" / >
< small >
Closer to 0 is closer to TTS and 1 is closer to trained voice .
Can help mask noise and sound more natural when set relatively low .
< / s m a l l >
< / d i v >
< div >
< label for = "rvc_protect" > Protect amount ( < span id = "rvc_protect_value" > < / s p a n > ) < / l a b e l >
< input id = "rvc_protect" type = "range" min = "0" max = "1" step = "0.01" value = "0.33" / >
< small >
Avoid non voice sounds . Lower is more being ignored .
< / s m a l l >
2023-08-17 04:47:41 +02:00
< / d i v >
2023-08-09 03:30:26 +02:00
< div id = "rvc_status" >
< / d i v >
< div class = "rvc_buttons" >
< input id = "rvc_apply" class = "menu_button" type = "submit" value = "Apply" / >
< / d i v >
< / d i v >
< / d i v >
< / d i v >
< / d i v >
` ;
$ ( '#extensions_settings' ) . append ( settingsHtml ) ;
$ ( "#rvc_enabled" ) . on ( "click" , onEnabledClick ) ;
2023-08-17 11:05:17 +02:00
$ ( "#rvc_voice_map" ) . attr ( "disabled" , "disabled" ) ; ;
2023-08-09 03:30:26 +02:00
$ ( '#rvc_pitch_extraction' ) . on ( 'change' , onPitchExtractionChange ) ;
$ ( '#rvc_index_rate' ) . on ( 'input' , onIndexRateChange ) ;
$ ( '#rvc_filter_radius' ) . on ( 'input' , onFilterRadiusChange ) ;
$ ( '#rvc_pitch_offset' ) . on ( 'input' , onPitchOffsetChange ) ;
2023-08-15 21:15:31 +02:00
$ ( '#rvc_rms_mix_rate' ) . on ( 'input' , onRmsMixRateChange ) ;
2023-08-09 03:30:26 +02:00
$ ( '#rvc_protect' ) . on ( 'input' , onProtectChange ) ;
$ ( "#rvc_apply" ) . on ( "click" , onApplyClick ) ;
2023-08-15 21:15:31 +02:00
$ ( "#rvc_delete" ) . on ( "click" , onDeleteClick ) ;
2023-08-16 17:54:52 +02:00
2023-08-19 04:34:39 +02:00
$ ( "#rvc_model_upload_files" ) . hide ( ) ;
$ ( "#rvc_model_upload_select_button" ) . on ( "click" , function ( ) { $ ( "#rvc_model_upload_files" ) . click ( ) } ) ;
$ ( "#rvc_model_upload_files" ) . on ( "change" , onChangeUploadFiles ) ;
//$("#rvc_model_upload_button").on("click", onClickUpload);
2023-08-17 04:47:41 +02:00
$ ( "#rvc_model_refresh_button" ) . on ( "click" , refreshVoiceList ) ;
2023-08-09 03:30:26 +02:00
}
addExtensionControls ( ) ; // No init dependencies
loadSettings ( ) ; // Depends on Extension Controls
2023-08-15 21:15:31 +02:00
const wrapper = new ModuleWorkerWrapper ( moduleWorker ) ;
setInterval ( wrapper . update . bind ( wrapper ) , UPDATE _INTERVAL ) ;
moduleWorker ( ) ;
2023-08-09 03:30:26 +02:00
} )
2023-08-15 21:15:31 +02:00
//#############################//
// API Calls //
//#############################//
/ *
Check model installation state , return one of [ "installed" , "corrupted" , "absent" ]
* /
async function get _models _list ( model _id ) {
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/voice-conversion/rvc/get-models-list' ;
const apiResult = await doExtrasFetch ( url , {
method : 'POST'
} ) ;
if ( ! apiResult . ok ) {
2023-08-17 11:05:17 +02:00
toastr . error ( apiResult . statusText , DEBUG _PREFIX + ' Check model state request failed' ) ;
2023-08-15 21:15:31 +02:00
throw new Error ( ` HTTP ${ apiResult . status } : ${ await apiResult . text ( ) } ` ) ;
}
return apiResult
}
/ *
Send an audio file to RVC to convert voice
* /
2023-08-18 01:12:00 +02:00
async function rvcVoiceConversion ( response , character , text ) {
2023-08-15 21:15:31 +02:00
let apiResult
// Check voice map
if ( extension _settings . rvc . voiceMap [ character ] === undefined ) {
2023-08-15 21:35:07 +02:00
//toastr.error("No model is assigned to character '"+character+"', check RVC voice map in the extension menu.", DEBUG_PREFIX+'RVC Voice map error', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
2023-08-17 11:05:17 +02:00
console . info ( DEBUG _PREFIX , "No RVC model assign in voice map for current character " + character ) ;
2023-08-15 21:15:31 +02:00
return response ;
}
const audioData = await response . blob ( )
if ( ! audioData . type in [ 'audio/mpeg' , 'audio/wav' , 'audio/x-wav' , 'audio/wave' , 'audio/webm' ] ) {
throw ` TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${ audioData . type } `
}
2023-08-17 11:05:17 +02:00
console . log ( "Audio type received:" , audioData . type )
2023-08-15 21:15:31 +02:00
const voice _settings = extension _settings . rvc . voiceMap [ character ] ;
var requestData = new FormData ( ) ;
requestData . append ( 'AudioFile' , audioData , 'record' ) ;
requestData . append ( "json" , JSON . stringify ( {
"modelName" : voice _settings [ "modelName" ] ,
"pitchExtraction" : voice _settings [ "pitchExtraction" ] ,
"pitchOffset" : voice _settings [ "pitchOffset" ] ,
"indexRate" : voice _settings [ "indexRate" ] ,
"filterRadius" : voice _settings [ "filterRadius" ] ,
"rmsMixRate" : voice _settings [ "rmsMixRate" ] ,
2023-08-18 01:12:00 +02:00
"protect" : voice _settings [ "protect" ] ,
"text" : text
2023-08-15 21:15:31 +02:00
} ) ) ;
2023-08-16 17:54:52 +02:00
2023-08-18 01:12:00 +02:00
console . log ( "Sending tts audio data to RVC on extras server" , requestData )
2023-08-15 21:15:31 +02:00
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/voice-conversion/rvc/process-audio' ;
apiResult = await doExtrasFetch ( url , {
method : 'POST' ,
body : requestData ,
} ) ;
if ( ! apiResult . ok ) {
2023-08-17 11:05:17 +02:00
toastr . error ( apiResult . statusText , DEBUG _PREFIX + ' RVC Voice Conversion Failed' , { timeOut : 10000 , extendedTimeOut : 20000 , preventDuplicates : true } ) ;
2023-08-15 21:15:31 +02:00
throw new Error ( ` HTTP ${ apiResult . status } : ${ await apiResult . text ( ) } ` ) ;
}
return apiResult ;
}
//#############################//
// Module Worker //
//#############################//
2023-08-17 04:47:41 +02:00
async function refreshVoiceList ( ) {
let result = await get _models _list ( ) ;
2023-08-17 11:05:17 +02:00
result = await result . json ( ) ;
rvcModelsList = result [ "models_list" ]
$ ( '#rvc_model_select' )
. find ( 'option' )
. remove ( )
. end ( )
. append ( '<option value="none">Select Voice</option>' )
. val ( 'none' )
for ( const modelName of rvcModelsList ) {
$ ( "#rvc_model_select" ) . append ( new Option ( modelName , modelName ) ) ;
}
2023-08-15 21:15:31 +02:00
2023-08-17 11:05:17 +02:00
rvcModelsReceived = true
console . debug ( DEBUG _PREFIX , "Updated model list to:" , rvcModelsList ) ;
2023-08-17 04:47:41 +02:00
}
async function moduleWorker ( ) {
updateCharactersList ( ) ;
if ( modules . includes ( 'rvc' ) && ! rvcModelsReceived ) {
refreshVoiceList ( ) ;
2023-08-15 21:15:31 +02:00
}
}
function updateCharactersList ( ) {
let currentcharacters = new Set ( ) ;
2023-08-20 20:46:53 +02:00
const context = getContext ( ) ;
for ( const i of context . characters ) {
2023-08-15 21:15:31 +02:00
currentcharacters . add ( i . name ) ;
}
2023-08-20 20:46:53 +02:00
currentcharacters = Array . from ( currentcharacters ) ;
currentcharacters . unshift ( context . name1 ) ;
2023-08-16 17:54:52 +02:00
2023-08-15 21:15:31 +02:00
if ( JSON . stringify ( charactersList ) !== JSON . stringify ( currentcharacters ) ) {
charactersList = currentcharacters
$ ( '#rvc_character_select' )
. find ( 'option' )
. remove ( )
. end ( )
. append ( '<option value="none">Select Character</option>' )
. val ( 'none' )
2023-08-16 17:54:52 +02:00
2023-08-17 11:05:17 +02:00
for ( const charName of charactersList ) {
$ ( "#rvc_character_select" ) . append ( new Option ( charName , charName ) ) ;
2023-08-15 21:15:31 +02:00
}
2023-08-17 11:05:17 +02:00
console . debug ( DEBUG _PREFIX , "Updated character list to:" , charactersList ) ;
2023-08-15 21:15:31 +02:00
}
2023-08-16 17:54:52 +02:00
}