Fixed rest of emscripten.

pull/494/head
Sandro Hanea 1 year ago
parent ff2373a886
commit 2be35dd32b

@ -1 +1 @@
"use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",data=>onmessage({data:data}));var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(fs.readFileSync(f,"utf8")+"//# sourceURL="+f)},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;var pendingNotifiedProxyingQueues=[];function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};self.onunhandledrejection=e=>{throw e.reason??e};self.onmessage=e=>{try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=function(){postMessage({cmd:"callHandler",handler:handler,args:[...arguments]})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}whisper_factory(Module).then(function(instance){Module=instance})}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){Module["__embind_initialize_bindings"]();pendingNotifiedProxyingQueues.forEach(queue=>{Module["executeNotifiedProxyingQueue"](queue)});pendingNotifiedProxyingQueues=[];initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["__emscripten_thread_exit"](ex.status)}}else{throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processProxyingQueue"){if(initializedJS){Module["executeNotifiedProxyingQueue"](e.data.queue)}else{pendingNotifiedProxyingQueues.push(e.data.queue)}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}};
"use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",data=>onmessage({data:data}));var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(fs.readFileSync(f,"utf8")+"//# sourceURL="+f)},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;var pendingNotifiedProxyingQueues=[];function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=function(){postMessage({cmd:"callHandler",handler:handler,args:[...arguments]})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}whisper_factory(Module)}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){Module["__embind_initialize_bindings"]();pendingNotifiedProxyingQueues.forEach(queue=>{Module["executeNotifiedProxyingQueue"](queue)});pendingNotifiedProxyingQueues=[];initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["__emscripten_thread_exit"](ex.status)}}else{throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processProxyingQueue"){if(initializedJS){Module["executeNotifiedProxyingQueue"](e.data.queue)}else{pendingNotifiedProxyingQueues.push(e.data.queue)}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;

File diff suppressed because one or more lines are too long

@ -10,8 +10,8 @@
constexpr int N_THREAD = 8;
// TODO: get rid of this vector of contexts - bad idea in the first place
std::vector<struct whisper_context *> g_contexts(4, nullptr);
whisper_context * g_context;
std::vector<struct whisper_state *> g_states(4, nullptr);
std::thread g_worker;
@ -19,11 +19,11 @@ void bench_main(size_t index) {
const int n_threads = std::min(N_THREAD, (int) std::thread::hardware_concurrency());
// whisper context
auto & ctx = g_contexts[index];
whisper_state * state = g_states[index];
fprintf(stderr, "%s: running benchmark with %d threads - please wait...\n", __func__, n_threads);
if (int ret = whisper_set_mel(ctx, nullptr, 0, WHISPER_N_MEL)) {
if (int ret = whisper_set_mel(state, nullptr, 0, WHISPER_N_MEL)) {
fprintf(stderr, "error: failed to set mel: %d\n", ret);
return;
}
@ -33,12 +33,12 @@ void bench_main(size_t index) {
fprintf(stderr, "system_info: n_threads = %d / %d | %s\n", n_threads, std::thread::hardware_concurrency(), whisper_print_system_info());
}
if (int ret = whisper_encode(ctx, 0, n_threads) != 0) {
if (int ret = whisper_encode(g_context, state, 0, n_threads) != 0) {
fprintf(stderr, "error: failed to encode model: %d\n", ret);
return;
}
whisper_print_timings(ctx);
whisper_print_timings(g_context, state);
fprintf(stderr, "\n");
fprintf(stderr, "If you wish, you can submit these results here:\n");
@ -55,10 +55,14 @@ void bench_main(size_t index) {
EMSCRIPTEN_BINDINGS(bench) {
emscripten::function("init", emscripten::optional_override([](const std::string & path_model) {
for (size_t i = 0; i < g_contexts.size(); ++i) {
if (g_contexts[i] == nullptr) {
g_contexts[i] = whisper_init_from_file(path_model.c_str());
if (g_contexts[i] != nullptr) {
if(g_context == nullptr) {
g_context = whisper_init_from_file(path_model.c_str());
}
for (size_t i = 0; i < g_states.size(); ++i) {
if (g_states[i] == nullptr) {
g_states[i] = whisper_init_state(g_context);
if (g_states[i] != nullptr) {
if (g_worker.joinable()) {
g_worker.join();
}
@ -77,9 +81,11 @@ EMSCRIPTEN_BINDINGS(bench) {
}));
emscripten::function("free", emscripten::optional_override([](size_t index) {
if (index < g_contexts.size()) {
whisper_free(g_contexts[index]);
g_contexts[index] = nullptr;
if (index < g_states.size()) {
whisper_free_state(g_states[index]);
g_states[index] = nullptr;
}
whisper_free(g_context);
g_context = nullptr;
}));
}

@ -14,7 +14,8 @@
constexpr int N_THREAD = 8;
std::vector<struct whisper_context *> g_contexts(4, nullptr);
whisper_context * g_context;
std::vector<struct whisper_state *> g_states(4, nullptr);
std::mutex g_mutex;
std::thread g_worker;
@ -113,28 +114,28 @@ bool command_vad_simple(std::vector<float> & pcmf32, int sample_rate, int last_m
return true;
}
std::string command_transcribe(whisper_context * ctx, const whisper_full_params & wparams, const std::vector<float> & pcmf32, float & prob, int64_t & t_ms) {
std::string command_transcribe(whisper_context * ctx, whisper_state * state, const whisper_full_params & wparams, const std::vector<float> & pcmf32, float & prob, int64_t & t_ms) {
const auto t_start = std::chrono::high_resolution_clock::now();
prob = 0.0f;
t_ms = 0;
if (whisper_full(ctx, wparams, pcmf32.data(), pcmf32.size()) != 0) {
if (whisper_full_with_state(ctx, state, wparams, pcmf32.data(), pcmf32.size()) != 0) {
return "";
}
int prob_n = 0;
std::string result;
const int n_segments = whisper_full_n_segments(ctx);
const int n_segments = whisper_full_n_segments(state);
for (int i = 0; i < n_segments; ++i) {
const char * text = whisper_full_get_segment_text(ctx, i);
const char * text = whisper_full_get_segment_text(state, i);
result += text;
const int n_tokens = whisper_full_n_tokens(ctx, i);
const int n_tokens = whisper_full_n_tokens(state, i);
for (int j = 0; j < n_tokens; ++j) {
const auto token = whisper_full_get_token_data(ctx, i, j);
const auto token = whisper_full_get_token_data(state, i, j);
prob += token.p;
++prob_n;
@ -201,7 +202,8 @@ void command_main(size_t index) {
const std::string k_prompt = "Ok Whisper, start listening for commands.";
// whisper context
auto & ctx = g_contexts[index];
auto & ctx = g_context;
auto & state = g_states[index];
const int32_t vad_ms = 2000;
const int32_t prompt_ms = 5000;
@ -240,7 +242,7 @@ void command_main(size_t index) {
if (!have_prompt) {
command_get_audio(prompt_ms, WHISPER_SAMPLE_RATE, pcmf32_cur);
const auto txt = ::trim(::command_transcribe(ctx, wparams, pcmf32_cur, prob0, t_ms));
const auto txt = ::trim(::command_transcribe(ctx, state, wparams, pcmf32_cur, prob0, t_ms));
fprintf(stdout, "%s: Heard '%s%s%s', (t = %d ms)\n", __func__, "\033[1m", txt.c_str(), "\033[0m", (int) t_ms);
@ -271,7 +273,7 @@ void command_main(size_t index) {
// prepend the prompt audio
pcmf32_cur.insert(pcmf32_cur.begin(), pcmf32_prompt.begin(), pcmf32_prompt.end());
const auto txt = ::trim(::command_transcribe(ctx, wparams, pcmf32_cur, prob, t_ms));
const auto txt = ::trim(::command_transcribe(ctx, state, wparams, pcmf32_cur, prob, t_ms));
prob = 100.0f*(prob - prob0);
@ -314,18 +316,25 @@ void command_main(size_t index) {
}
}
if (index < g_contexts.size()) {
whisper_free(g_contexts[index]);
g_contexts[index] = nullptr;
if (index < g_states.size()) {
whisper_free_state(g_states[index]);
g_states[index] = nullptr;
}
whisper_free(g_context);
g_context = nullptr;
}
EMSCRIPTEN_BINDINGS(command) {
emscripten::function("init", emscripten::optional_override([](const std::string & path_model) {
for (size_t i = 0; i < g_contexts.size(); ++i) {
if (g_contexts[i] == nullptr) {
g_contexts[i] = whisper_init_from_file(path_model.c_str());
if (g_contexts[i] != nullptr) {
if (g_context == nullptr) {
g_context = whisper_init_from_file(path_model.c_str());
}
for (size_t i = 0; i < g_states.size(); ++i) {
if (g_states[i] == nullptr) {
g_states[i] = whisper_init_state(g_context);
if (g_states[i] != nullptr) {
g_running = true;
if (g_worker.joinable()) {
g_worker.join();
@ -353,14 +362,18 @@ EMSCRIPTEN_BINDINGS(command) {
emscripten::function("set_audio", emscripten::optional_override([](size_t index, const emscripten::val & audio) {
--index;
if (index >= g_contexts.size()) {
if (index >= g_states.size()) {
return -1;
}
if (g_contexts[index] == nullptr) {
if (g_states[index] == nullptr) {
return -2;
}
if (g_context == nullptr) {
return -3;
}
{
std::lock_guard<std::mutex> lock(g_mutex);
const int n = audio["length"].as<int>();

@ -13,7 +13,8 @@
constexpr int N_THREAD = 8;
std::vector<struct whisper_context *> g_contexts(4, nullptr);
whisper_context * g_context;
std::vector<struct whisper_state *> g_states(4, nullptr);
std::mutex g_mutex;
std::thread g_worker;
@ -59,7 +60,8 @@ void stream_main(size_t index) {
std::vector<float> pcmf32;
// whisper context
auto & ctx = g_contexts[index];
auto & ctx = g_context;
auto & state = g_states[index];
// 5 seconds interval
const int64_t window_samples = 5*WHISPER_SAMPLE_RATE;
@ -87,7 +89,7 @@ void stream_main(size_t index) {
stream_set_status("running whisper ...");
int ret = whisper_full(ctx, wparams, pcmf32.data(), pcmf32.size());
int ret = whisper_full_with_state(ctx, state, wparams, pcmf32.data(), pcmf32.size());
if (ret != 0) {
printf("whisper_full() failed: %d\n", ret);
break;
@ -102,12 +104,12 @@ void stream_main(size_t index) {
std::string text_heard;
{
const int n_segments = whisper_full_n_segments(ctx);
const int n_segments = whisper_full_n_segments(state);
for (int i = n_segments - 1; i < n_segments; ++i) {
const char * text = whisper_full_get_segment_text(ctx, i);
const char * text = whisper_full_get_segment_text(state, i);
const int64_t t0 = whisper_full_get_segment_t0(ctx, i);
const int64_t t1 = whisper_full_get_segment_t1(ctx, i);
const int64_t t0 = whisper_full_get_segment_t0(state, i);
const int64_t t1 = whisper_full_get_segment_t1(state, i);
printf("transcribed: %s\n", text);
@ -122,18 +124,25 @@ void stream_main(size_t index) {
}
}
if (index < g_contexts.size()) {
whisper_free(g_contexts[index]);
g_contexts[index] = nullptr;
if (index < g_states.size()) {
whisper_free_state(g_states[index]);
g_states[index] = nullptr;
}
whisper_free(g_context);
g_context = nullptr;
}
EMSCRIPTEN_BINDINGS(stream) {
emscripten::function("init", emscripten::optional_override([](const std::string & path_model) {
for (size_t i = 0; i < g_contexts.size(); ++i) {
if (g_contexts[i] == nullptr) {
g_contexts[i] = whisper_init_from_file(path_model.c_str());
if (g_contexts[i] != nullptr) {
if (g_context == nullptr) {
g_context = whisper_init_from_file(path_model.c_str());
}
for (size_t i = 0; i < g_states.size(); ++i) {
if (g_states[i] == nullptr) {
g_states[i] = whisper_init_state(g_context);
if (g_states[i] != nullptr) {
g_running = true;
if (g_worker.joinable()) {
g_worker.join();
@ -161,14 +170,18 @@ EMSCRIPTEN_BINDINGS(stream) {
emscripten::function("set_audio", emscripten::optional_override([](size_t index, const emscripten::val & audio) {
--index;
if (index >= g_contexts.size()) {
if (index >= g_states.size()) {
return -1;
}
if (g_contexts[index] == nullptr) {
if (g_states[index] == nullptr) {
return -2;
}
if (g_context == nullptr) {
return -3;
}
{
std::lock_guard<std::mutex> lock(g_mutex);
const int n = audio["length"].as<int>();

@ -16,7 +16,8 @@
constexpr int N_THREAD = 8;
struct gpt2_context * g_gpt2;
std::vector<struct whisper_context *> g_contexts(4, nullptr);
whisper_context * g_context;
std::vector<struct whisper_state *> g_states(4, nullptr);
std::mutex g_mutex;
std::thread g_worker;
@ -73,7 +74,8 @@ void talk_main(size_t index) {
std::vector<float> pcmf32;
// whisper context
auto & ctx = g_contexts[index];
auto & ctx = g_context;
auto & state = g_states[index];
const int64_t step_samples = 2*WHISPER_SAMPLE_RATE;
const int64_t window_samples = 9*WHISPER_SAMPLE_RATE;
@ -140,7 +142,7 @@ void talk_main(size_t index) {
if (!g_force_speak) {
const auto t_start = std::chrono::high_resolution_clock::now();
int ret = whisper_full(ctx, wparams, pcmf32.data(), pcmf32.size());
int ret = whisper_full_with_state(ctx, state, wparams, pcmf32.data(), pcmf32.size());
if (ret != 0) {
printf("whisper_full() failed: %d\n", ret);
break;
@ -155,12 +157,12 @@ void talk_main(size_t index) {
std::string text_heard;
if (!g_force_speak) {
const int n_segments = whisper_full_n_segments(ctx);
const int n_segments = whisper_full_n_segments(state);
for (int i = n_segments - 1; i < n_segments; ++i) {
const char * text = whisper_full_get_segment_text(ctx, i);
const char * text = whisper_full_get_segment_text(state, i);
const int64_t t0 = whisper_full_get_segment_t0(ctx, i);
const int64_t t1 = whisper_full_get_segment_t1(ctx, i);
const int64_t t0 = whisper_full_get_segment_t0(state, i);
const int64_t t1 = whisper_full_get_segment_t1(state, i);
printf ("[%s --> %s] %s\n", to_timestamp(t0).c_str(), to_timestamp(t1).c_str(), text);
@ -261,18 +263,25 @@ void talk_main(size_t index) {
gpt2_free(g_gpt2);
if (index < g_contexts.size()) {
whisper_free(g_contexts[index]);
g_contexts[index] = nullptr;
if (index < g_states.size()) {
whisper_free_state(g_states[index]);
g_states[index] = nullptr;
}
whisper_free(g_context);
g_context = nullptr;
}
EMSCRIPTEN_BINDINGS(talk) {
emscripten::function("init", emscripten::optional_override([](const std::string & path_model) {
for (size_t i = 0; i < g_contexts.size(); ++i) {
if (g_contexts[i] == nullptr) {
g_contexts[i] = whisper_init_from_file(path_model.c_str());
if (g_contexts[i] != nullptr) {
if(g_context == nullptr) {
g_context = whisper_init_from_file(path_model.c_str());
}
for (size_t i = 0; i < g_states.size(); ++i) {
if (g_states[i] == nullptr) {
g_states[i] = whisper_init_state(g_context);
if (g_states[i] != nullptr) {
g_running = true;
if (g_worker.joinable()) {
g_worker.join();
@ -300,14 +309,18 @@ EMSCRIPTEN_BINDINGS(talk) {
emscripten::function("set_audio", emscripten::optional_override([](size_t index, const emscripten::val & audio) {
--index;
if (index >= g_contexts.size()) {
if (index >= g_states.size()) {
return -1;
}
if (g_contexts[index] == nullptr) {
if (g_states[index] == nullptr) {
return -2;
}
if (g_context == nullptr) {
return -3;
}
{
std::lock_guard<std::mutex> lock(g_mutex);
const int n = audio["length"].as<int>();

@ -8,18 +8,24 @@
std::thread g_worker;
std::vector<struct whisper_context *> g_contexts(4, nullptr);
struct whisper_context * g_context;
std::vector<struct whisper_state *> g_states(4, nullptr);
EMSCRIPTEN_BINDINGS(whisper) {
emscripten::function("init", emscripten::optional_override([](const std::string & path_model) {
if (g_worker.joinable()) {
g_worker.join();
}
if(g_context == nullptr) {
g_context = whisper_init_from_file(path_model.c_str());
}
for (size_t i = 0; i < g_contexts.size(); ++i) {
if (g_contexts[i] == nullptr) {
g_contexts[i] = whisper_init_from_file(path_model.c_str());
if (g_contexts[i] != nullptr) {
for (size_t i = 0; i < g_states.size(); ++i) {
if (g_states[i] == nullptr) {
g_states[i] = whisper_init_state(g_context);
if (g_states[i] != nullptr) {
return i + 1;
} else {
return (size_t) 0;
@ -37,9 +43,10 @@ EMSCRIPTEN_BINDINGS(whisper) {
--index;
if (index < g_contexts.size()) {
whisper_free(g_contexts[index]);
g_contexts[index] = nullptr;
whisper_free(g_context);
if (index < g_states.size()) {
whisper_free_state(g_states[index]);
g_states[index] = nullptr;
}
}));
@ -50,14 +57,18 @@ EMSCRIPTEN_BINDINGS(whisper) {
--index;
if (index >= g_contexts.size()) {
if (index >= g_states.size()) {
return -1;
}
if (g_contexts[index] == nullptr) {
if (g_states[index] == nullptr) {
return -2;
}
if(g_context == nullptr) {
return -3;
}
struct whisper_full_params params = whisper_full_default_params(whisper_sampling_strategy::WHISPER_SAMPLING_GREEDY);
params.print_realtime = true;
@ -65,7 +76,7 @@ EMSCRIPTEN_BINDINGS(whisper) {
params.print_timestamps = true;
params.print_special = false;
params.translate = translate;
params.language = whisper_is_multilingual(g_contexts[index]) ? lang.c_str() : "en";
params.language = whisper_is_multilingual(g_context) ? lang.c_str() : "en";
params.n_threads = std::min(8, (int) std::thread::hardware_concurrency());
params.offset_ms = 0;
@ -97,9 +108,9 @@ EMSCRIPTEN_BINDINGS(whisper) {
// run the worker
{
g_worker = std::thread([index, params, pcmf32 = std::move(pcmf32)]() {
whisper_reset_timings(g_contexts[index]);
whisper_full(g_contexts[index], params, pcmf32.data(), pcmf32.size());
whisper_print_timings(g_contexts[index]);
whisper_reset_timings(g_states[index]);
whisper_full_with_state(g_context, g_states[index], params, pcmf32.data(), pcmf32.size());
whisper_print_timings(g_context, g_states[index]);
});
}

Loading…
Cancel
Save