| class InferenceApp {
|
| constructor() {
|
|
|
| this.fileInput = document.getElementById('fileInput');
|
| this.depthfileInput = document.getElementById('depthfileInput');
|
| this.inputImage = document.getElementById('inputImage');
|
| this.DepthInputImage = document.getElementById('DepthInputImage');
|
| this.runBtn = document.getElementById('runBtn');
|
| this.adjustDepthBtn = document.getElementById('adjustDepthBtn');
|
|
|
|
|
| this.resultCanvas = document.getElementById('resultCanvas');
|
| this.resultCtx = this.resultCanvas.getContext('2d');
|
|
|
|
|
| this.resultCanvas_Adjust = document.getElementById('resultCanvas_Adjust');
|
| this.resultCtx_Adjust = this.resultCanvas_Adjust.getContext('2d');
|
|
|
|
|
| this.session = null;
|
|
|
|
|
| this.fileInput.addEventListener('change', (e) => this.onFileChange(e));
|
| this.depthfileInput.addEventListener('change', (e) => this.onDepthFileChange(e));
|
| this.runBtn.addEventListener('click', () => this.runInference());
|
| this.adjustDepthBtn.addEventListener('click', () => this.runDepthAdjustment());
|
|
|
|
|
| this.maskData = null;
|
| this.depthData = null;
|
| this.depthWidth = 0;
|
| this.depthHeight = 0;
|
| }
|
|
|
| |
| |
|
|
| async init() {
|
| console.log('初始化模型...');
|
| try {
|
| console.log('嘗試使用 WebGPU...');
|
| this.session = await ort.InferenceSession.create('isnet_infer.onnx', {
|
| executionProviders: ['webgpu'],
|
| });
|
| console.log('成功使用 WebGPU');
|
| } catch (error) {
|
| console.warn('使用 WebGPU 失敗,改用 WASM:', error);
|
| console.log('使用 WASM (CPU)...');
|
| this.session = await ort.InferenceSession.create('isnet_infer.onnx', {
|
| executionProviders: ['wasm'],
|
| });
|
| }
|
| console.log('模型初始化完成!');
|
| }
|
|
|
| |
| |
|
|
| onFileChange(e) {
|
| const file = e.target.files[0];
|
| if (!file) return;
|
| const url = URL.createObjectURL(file);
|
|
|
| this.inputImage.src = url;
|
| }
|
|
|
| |
| |
|
|
| onDepthFileChange(e) {
|
| const file = e.target.files[0];
|
| if (!file) return;
|
|
|
| const img = new Image();
|
| img.onload = () => {
|
|
|
| const resizeWidth = 1024;
|
| const resizeHeight = 1024;
|
|
|
|
|
| const tmpCanvas = document.createElement('canvas');
|
| tmpCanvas.width = resizeWidth;
|
| tmpCanvas.height = resizeHeight;
|
| const tmpCtx = tmpCanvas.getContext('2d');
|
|
|
| tmpCtx.drawImage(img, 0, 0, resizeWidth, resizeHeight);
|
|
|
|
|
| const imageData = tmpCtx.getImageData(0, 0, resizeWidth, resizeHeight);
|
|
|
|
|
| this.depthData = new Uint8ClampedArray(resizeWidth * resizeHeight);
|
| for (let i = 0; i < imageData.data.length; i += 4) {
|
|
|
| this.depthData[i / 4] = imageData.data[i];
|
| }
|
|
|
| this.depthWidth = resizeWidth;
|
| this.depthHeight = resizeHeight;
|
|
|
|
|
| this.DepthInputImage.src = URL.createObjectURL(file);
|
| console.log(`已將深度圖縮放為: ${resizeWidth}x${resizeHeight}`);
|
| };
|
| img.src = URL.createObjectURL(file);
|
| }
|
|
|
| |
| |
| |
|
|
| async preprocessImage() {
|
| const desiredWidth = 1024;
|
| const desiredHeight = 1024;
|
|
|
|
|
| const tmpCanvas = document.createElement('canvas');
|
| tmpCanvas.width = desiredWidth;
|
| tmpCanvas.height = desiredHeight;
|
| const tmpCtx = tmpCanvas.getContext('2d');
|
|
|
|
|
| tmpCtx.drawImage(this.inputImage, 0, 0, desiredWidth, desiredHeight);
|
|
|
|
|
| const imageData = tmpCtx.getImageData(0, 0, desiredWidth, desiredHeight);
|
| const data = imageData.data;
|
|
|
|
|
| const floatData = new Float32Array(3 * desiredHeight * desiredWidth);
|
|
|
|
|
| let idx = 0;
|
| for (let i = 0; i < data.length; i += 4) {
|
| const r = data[i];
|
| const g = data[i + 1];
|
| const b = data[i + 2];
|
|
|
| floatData[idx + 0 * desiredWidth * desiredHeight] = r / 255.0 - 0.5;
|
| floatData[idx + 1 * desiredWidth * desiredHeight] = g / 255.0 - 0.5;
|
| floatData[idx + 2 * desiredWidth * desiredHeight] = b / 255.0 - 0.5;
|
| idx++;
|
| }
|
|
|
|
|
| const inputTensor = new ort.Tensor('float32', floatData, [
|
| 1,
|
| 3,
|
| desiredHeight,
|
| desiredWidth,
|
| ]);
|
| return inputTensor;
|
| }
|
|
|
| |
| |
|
|
| async runInference() {
|
| if (!this.session) {
|
| alert('模型尚未初始化完成,請稍後再試。');
|
| return;
|
| }
|
| if (!this.inputImage.src) {
|
| alert('請先上傳 RGB 圖片 (做 mask 推論)。');
|
| return;
|
| }
|
|
|
| try {
|
| console.log('開始前處理圖片 (for mask 推論)...');
|
| const inputTensor = await this.preprocessImage();
|
|
|
| console.log('開始推論...');
|
| const startTime = performance.now();
|
| const feeds = { input: inputTensor };
|
| const results = await this.session.run(feeds);
|
|
|
|
|
| const outputTensor = results['output'];
|
| const outputData = outputTensor.data;
|
| const endTime = performance.now();
|
| console.log(`推論完成, 耗時: ${endTime - startTime} ms`);
|
|
|
|
|
| let minVal = +Infinity;
|
| let maxVal = -Infinity;
|
| for (let i = 0; i < outputData.length; i++) {
|
| const v = outputData[i];
|
| if (v < minVal) minVal = v;
|
| if (v > maxVal) maxVal = v;
|
| }
|
| const range = maxVal - minVal;
|
| const normalized = new Float32Array(outputData.length);
|
| for (let i = 0; i < outputData.length; i++) {
|
| normalized[i] = (outputData[i] - minVal) / range;
|
| }
|
|
|
|
|
| const width = 1024, height = 1024;
|
| const resultImageData = this.resultCtx.createImageData(width, height);
|
| for (let i = 0; i < width * height; i++) {
|
| const v = Math.floor(normalized[i] * 255);
|
| resultImageData.data[i * 4 + 0] = v;
|
| resultImageData.data[i * 4 + 1] = v;
|
| resultImageData.data[i * 4 + 2] = v;
|
| resultImageData.data[i * 4 + 3] = 255;
|
| }
|
| this.resultCtx.putImageData(resultImageData, 0, 0);
|
|
|
|
|
|
|
| this.maskData = normalized;
|
|
|
| const tmpCanvas = document.createElement('canvas');
|
| tmpCanvas.width = width;
|
| tmpCanvas.height = height;
|
| const tmpCtx = tmpCanvas.getContext('2d');
|
|
|
|
|
| tmpCtx.putImageData(resultImageData, 0, 0);
|
|
|
|
|
| this.resultCtx.clearRect(0, 0, this.resultCanvas.width, this.resultCanvas.height);
|
|
|
| this.resultCtx.drawImage(tmpCanvas, 0, 0, width, height, 0, 0, 400, 400);
|
|
|
| } catch (e) {
|
| console.error('推論時發生錯誤:', e);
|
| }
|
| }
|
|
|
| |
| |
|
|
| async runDepthAdjustment() {
|
| if (!this.depthData) {
|
| alert('尚未上傳深度圖');
|
| return;
|
| }
|
| if (!this.maskData) {
|
| alert('尚未進行 mask 推論');
|
| return;
|
| }
|
|
|
|
|
| const width = 1024, height = 1024;
|
| if (width * height !== this.depthWidth * this.depthHeight) {
|
| alert('目前示範假設深度圖與 mask 同尺寸 1024×1024,請自行在程式裡做對應處理。');
|
| return;
|
| }
|
|
|
| console.log('開始對深度圖做 CLHE_new + MainObjectTo128_new...');
|
| const startTime = performance.now();
|
|
|
|
|
| const depthFloat = new Float32Array(width * height);
|
| for (let i = 0; i < width * height; i++) {
|
| depthFloat[i] = this.depthData[i] / 255.0;
|
| }
|
|
|
|
|
| const maskBin = new Uint8Array(width * height);
|
| for (let i = 0; i < width * height; i++) {
|
| maskBin[i] = this.maskData[i] > 0.5 ? 1 : 0;
|
| }
|
|
|
|
|
| const { depthCLHE, cdfArray } = this.CLHE_new(depthFloat, maskBin, width, height);
|
|
|
|
|
|
|
|
|
| const depthMaskVals = [];
|
| for (let i = 0; i < width * height; i++) {
|
| if (maskBin[i] === 1) {
|
| depthMaskVals.push(depthCLHE[i]);
|
| }
|
| }
|
| const yCurve = this.MainObjectTo128_new(depthMaskVals);
|
|
|
|
|
|
|
| const combineCurve = new Float32Array(1024);
|
| for (let i = 0; i < 1024; i++) {
|
|
|
| const cdfVal = cdfArray[i];
|
| combineCurve[i] = yCurve[cdfVal];
|
| }
|
|
|
|
|
| const finalDepth = new Float32Array(width * height);
|
| for (let i = 0; i < width * height; i++) {
|
| const idx = Math.floor(depthFloat[i] * 1023);
|
| finalDepth[i] = combineCurve[idx];
|
| }
|
| const endTime = performance.now();
|
| console.log(`調整完成, 耗時: ${endTime - startTime} ms`);
|
|
|
|
|
| const resultImageData = this.resultCtx_Adjust.createImageData(width, height);
|
| for (let i = 0; i < width * height; i++) {
|
| const v = Math.floor(finalDepth[i] / 1023 * 255);
|
| const i4 = i * 4;
|
| resultImageData.data[i4 + 0] = v;
|
| resultImageData.data[i4 + 1] = v;
|
| resultImageData.data[i4 + 2] = v;
|
| resultImageData.data[i4 + 3] = 255;
|
| }
|
|
|
| const tmpCanvas = document.createElement('canvas');
|
| tmpCanvas.width = width;
|
| tmpCanvas.height = height;
|
| const tmpCtx = tmpCanvas.getContext('2d');
|
|
|
|
|
| tmpCtx.putImageData(resultImageData, 0, 0);
|
|
|
|
|
| this.resultCtx_Adjust.clearRect(0, 0, this.resultCanvas_Adjust.width, this.resultCanvas_Adjust.height);
|
| this.resultCtx_Adjust.drawImage(tmpCanvas, 0, 0, width, height, 0, 0, 400, 400);
|
|
|
| console.log('深度圖調整完成!已顯示於右側畫布。');
|
|
|
| }
|
|
|
|
|
| CLHE_new(depthArr, maskArr, width, height) {
|
| const maskedVals = [];
|
| for (let i = 0; i < width * height; i++) {
|
| if (maskArr[i] === 1) {
|
| maskedVals.push(depthArr[i]);
|
| }
|
| }
|
| let median = 0.5, th25 = 0.5, th75 = 0.5;
|
| if (maskedVals.length > 50) {
|
| maskedVals.sort((a, b) => a - b);
|
| const n = maskedVals.length;
|
| median = maskedVals[Math.floor(n / 2)];
|
| th25 = maskedVals[Math.floor(n * 0.15)];
|
| th75 = maskedVals[Math.floor(n * 0.85)];
|
| }
|
| console.log(`th25: ${th25}, median: ${median}, th75: ${th75}`);
|
|
|
| const bins = 1024;
|
|
|
| const hist = new Float32Array(bins);
|
| for (let i = 0; i < depthArr.length; i++) {
|
| const idx = Math.floor(depthArr[i] * (bins - 1));
|
| hist[idx]++;
|
| }
|
|
|
|
|
|
|
| const totalnum = width * height;
|
| const mean_val = hist.reduce((a, b) => a + b, 0) / bins;
|
| const Limit = 2.5;
|
| let excessSum = 0.0;
|
| for (let i = 0; i < bins; i++) {
|
| const limitVal = mean_val * Limit;
|
| if (hist[i] > limitVal) {
|
| excessSum += hist[i] - limitVal;
|
| hist[i] = limitVal;
|
| }
|
| }
|
|
|
| const avgExcess = excessSum / bins;
|
| for (let i = 0; i < bins; i++) {
|
| hist[i] += avgExcess;
|
| }
|
|
|
|
|
| const pdf = new Float32Array(bins);
|
| for (let i = 0; i < bins; i++) {
|
| pdf[i] = hist[i] / totalnum;
|
| }
|
|
|
| const weight = 1.5;
|
| const lower_bound = th25 * (bins - 1);
|
| const upper_bound = th75 * (bins - 1);
|
| const pdfWeighted = new Float32Array(bins);
|
| for (let i = 0; i < bins; i++) {
|
| const inRange = (i >= lower_bound) && (i <= upper_bound);
|
| pdfWeighted[i] = inRange ? pdf[i] * weight : pdf[i];
|
| }
|
|
|
|
|
| const cdfArray = new Int32Array(bins);
|
| let cumulative = 0;
|
| let sumAll = pdfWeighted.reduce((a, b) => a + b, 0);
|
| for (let i = 0; i < bins; i++) {
|
| cumulative += pdfWeighted[i];
|
|
|
| cdfArray[i] = Math.round((cumulative / sumAll) * (bins - 1));
|
| }
|
|
|
|
|
|
|
| const cdfFloat = new Float32Array(cdfArray.length);
|
| for (let i = 0; i < cdfArray.length; i++) cdfFloat[i] = cdfArray[i];
|
| const cdfSmooth = this.meanFilter1D(cdfFloat, 101);
|
| const oldMin = Math.min(...cdfFloat);
|
| const oldMax = Math.max(...cdfFloat);
|
| const newMin = Math.min(...cdfSmooth);
|
| const newMax = Math.max(...cdfSmooth);
|
| const ratio = (oldMax - oldMin) / (newMax - newMin);
|
| for (let i = 0; i < bins; i++) {
|
| cdfFloat[i] = (cdfSmooth[i] - newMin) * ratio + oldMin;
|
| }
|
| for (let i = 0; i < bins; i++) {
|
| cdfArray[i] = Math.round(cdfFloat[i]);
|
| }
|
|
|
|
|
|
|
| const depthCLHE = new Float32Array(depthArr.length);
|
| for (let i = 0; i < depthArr.length; i++) {
|
| const idx = Math.floor(depthArr[i] * (bins - 1));
|
| depthCLHE[i] = cdfArray[idx] / (bins - 1);
|
| }
|
|
|
| return { depthCLHE, cdfArray };
|
| }
|
|
|
| MainObjectTo128_new(depthMaskVals) {
|
| const bins = 1024;
|
| if (depthMaskVals.length < 50) {
|
| console.log('MainObjectTo128_new: mask 值太少,直接回傳 identity');
|
| const y_ = new Float32Array(bins);
|
| for (let i = 0; i < bins; i++) {
|
| y_[i] = i / (bins - 1);
|
| }
|
| return y_;
|
| }
|
|
|
| depthMaskVals.sort((a, b) => a - b);
|
| const n = depthMaskVals.length;
|
| const median = depthMaskVals[Math.floor(n / 2)];
|
| const th25 = depthMaskVals[Math.floor(n * 0.25)];
|
| const th75 = depthMaskVals[Math.floor(n * 0.75)];
|
| console.log(`MainObjectTo128_new:: th25: ${th25}, median: ${median}, th75: ${th75}`);
|
|
|
| if (median === 0 || median === 1 || (th75 - th25) === 0) {
|
| console.log('[MainObjectTo128_new_js] Condition not met, return identity.');
|
| const y_ = new Float32Array(bins);
|
| for (let i = 0; i < bins; i++) {
|
| y_[i] = i / (bins - 1);
|
| }
|
| return y_;
|
| }
|
|
|
| if (th25 === 0) th25 = 0.0000001;
|
| if (th75 === 1) th75 = 0.9999999;
|
|
|
| const a = 0.44535377;
|
| const b = 0.6315172;
|
| const AfterRange = Math.pow(a * (th75 - th25), b);
|
| let scale = AfterRange / (th75 - th25);
|
|
|
| let medianScaleShift = median * scale - 0.5;
|
| let th25_ = th25 * scale - medianScaleShift;
|
| let th75_ = th75 * scale - medianScaleShift;
|
|
|
|
|
| let low_break = false;
|
| let high_break = false;
|
| if (th25_ < 0) {
|
| th25_ = 0;
|
| low_break = true;
|
| }
|
| if (th75_ > 1) {
|
| th75_ = 1;
|
| high_break = true;
|
| }
|
|
|
| if (low_break) {
|
| scale = (th75_ - th25_) / (th75 - th25);
|
| medianScaleShift = th25 * scale;
|
| th25 = (th25_ + medianScaleShift) / scale;
|
| th75 = (th75_ + medianScaleShift) / scale;
|
| }
|
| if (high_break) {
|
| scale = (th75_ - th25_) / (th75 - th25);
|
| medianScaleShift = th75 * scale - 1;
|
| th25 = (th25_ + medianScaleShift) / scale;
|
| th75 = (th75_ + medianScaleShift) / scale;
|
| }
|
|
|
| const ext = 0.01;
|
| const th25_2 = (th25 + ext) * scale - medianScaleShift;
|
| const th75_2 = (th75 - ext) * scale - medianScaleShift;
|
|
|
|
|
| let b1 = 0.0, a1 = 0.0;
|
| let b2 = 0.0, a2 = 0.0;
|
|
|
|
|
| if (!low_break) {
|
| const logNumer = Math.log(th25_2) - Math.log(th25_);
|
| const logDenom = Math.log(th25 + ext) - Math.log(th25);
|
| b1 = logNumer / logDenom;
|
| if (b1 > 1.2) b1 = 1.2;
|
| a1 = th25_ / Math.pow(th25, b1);
|
| }
|
|
|
| if (!high_break) {
|
| const logNumer = Math.log(1 - th75_2) - Math.log(th75_ - th75_2);
|
| const logDenom = Math.log(1 - th75 + ext) - Math.log(ext);
|
| b2 = logNumer / logDenom;
|
| a2 = (th75_ - th75_2) / Math.pow(ext, b2);
|
| }
|
| console.log(`a1: ${a1}, b1: ${b1}, a2: ${a2}, b2: ${b2}`);
|
|
|
|
|
|
|
| const x = new Float32Array(bins);
|
| for (let i = 0; i < bins; i++) {
|
| x[i] = i / (bins - 1);
|
| }
|
|
|
| const y = new Float32Array(bins).fill(0);
|
|
|
|
|
| for (let i = 0; i < bins; i++) {
|
| if (x[i] >= th25 && x[i] <= th75) {
|
| y[i] = x[i] * scale - medianScaleShift;
|
| }
|
| }
|
|
|
|
|
| if (low_break) {
|
| for (let i = 0; i < bins; i++) {
|
| if (x[i] < th25) {
|
| y[i] = 0;
|
| }
|
| }
|
| }
|
| else {
|
| for (let i = 0; i < bins; i++) {
|
| if (x[i] < th25) {
|
| if (b1 < 0.3) {
|
| y[i] = (x[i] * (scale * 2)) - ((scale * 2) * th25 - th25_);
|
| } else {
|
| y[i] = a1 * Math.pow(x[i], b1);
|
| }
|
| }
|
| }
|
| }
|
|
|
|
|
| if (high_break) {
|
| for (let i = 0; i < bins; i++) {
|
| if (x[i] > th75) {
|
| y[i] = 1;
|
| }
|
| }
|
| } else {
|
| for (let i = 0; i < bins; i++) {
|
| if (x[i] > th75) {
|
| if (b2 > 1) {
|
| y[i] = (x[i] * scale) - (scale * th75 - th75_);
|
| } else {
|
| y[i] = a2 * Math.pow((x[i] - th75 + ext), b2) + th75_2;
|
| }
|
| }
|
| }
|
| }
|
|
|
|
|
| const ksize = 301;
|
| const y_ = this.MeanFilter2_fast_js(y, ksize);
|
|
|
|
|
| const yMin = Math.min(...y);
|
| const yMax = Math.max(...y);
|
| const y_FMin = Math.min(...y_);
|
| const y_FMax = Math.max(...y_);
|
| const ratio = (yMax - yMin) / (y_FMax - y_FMin);
|
|
|
| for (let i = 0; i < bins; i++) {
|
| y_[i] = (y_[i] - y_FMin) * ratio + yMin;
|
| }
|
|
|
|
|
|
|
| const max_th = 0.85;
|
| let curveMAX = Math.max(...y_);
|
| if (curveMAX < max_th) {
|
| const yMin2 = Math.min(...y_);
|
| const max_new = curveMAX * (0.625 / 0.5);
|
| let ratio2 = 1.0;
|
| if (max_new > max_th) {
|
| ratio2 = (max_th - yMin2) / (curveMAX - yMin2);
|
| } else {
|
| ratio2 = (max_new - yMin2) / (curveMAX - yMin2);
|
| }
|
| for (let i = 0; i < bins; i++) {
|
| y_[i] = (y_[i] - yMin2) * ratio2 + yMin2;
|
| }
|
| }
|
|
|
| const min_th = 0.15;
|
| let curveMIN = Math.min(...y_);
|
| if (curveMIN > min_th) {
|
| const yMax2 = Math.max(...y_);
|
| const diff = (1 - curveMIN) * (0.625 / 0.5);
|
| const min_new = 1 - diff;
|
|
|
| let ratio3 = 1.0, newmin = 0.0;
|
| if (min_new > min_th) {
|
| ratio3 = (yMax2 - min_th) / (yMax2 - curveMIN);
|
| newmin = min_th;
|
| } else if (min_new < 0) {
|
| ratio3 = (yMax2 - 0) / (yMax2 - curveMIN);
|
| newmin = 0;
|
| } else {
|
| ratio3 = (yMax2 - min_new) / (yMax2 - curveMIN);
|
| newmin = min_new;
|
| }
|
| for (let i = 0; i < bins; i++) {
|
| y_[i] = (y_[i] - curveMIN) * ratio3 + newmin;
|
| }
|
| }
|
|
|
|
|
| for (let i = 0; i < bins; i++) {
|
| y_[i] *= (bins - 1);
|
| }
|
|
|
|
|
| return y_;
|
| }
|
|
|
| meanFilter1D(arr, k) {
|
| const half = Math.floor(k / 2);
|
| const n = arr.length;
|
| const out = new Float32Array(n);
|
|
|
|
|
| const ext = new Float32Array(n + 2 * half);
|
| for (let i = 0; i < half; i++) {
|
| ext[i] = arr[0];
|
| }
|
| for (let i = 0; i < n; i++) {
|
| ext[i + half] = arr[i];
|
| }
|
| for (let i = 0; i < half; i++) {
|
| ext[n + half + i] = arr[n - 1];
|
| }
|
|
|
|
|
| const oneOverK = 1.0 / k;
|
| for (let i = 0; i < n; i++) {
|
| let sum = 0.0;
|
| for (let j = 0; j < k; j++) {
|
| sum += ext[i + j];
|
| }
|
| out[i] = sum * oneOverK;
|
| }
|
| return out;
|
| }
|
|
|
| MeanFilter2_fast_js(x, k) {
|
| const n = x.length;
|
| const k2 = Math.floor((k - 1) / 2);
|
|
|
| const step = x[n - 1] - x[n - 2];
|
| const leftPad = new Float32Array(k2).fill(x[0]);
|
| const rightPad = new Float32Array(k2);
|
| for (let i = 0; i < k2; i++) {
|
| rightPad[i] = x[n - 1] + (i + 1) * step;
|
| }
|
| const x_ext = new Float32Array(n + 2 * k2);
|
| x_ext.set(leftPad, 0);
|
| x_ext.set(x, k2);
|
| x_ext.set(rightPad, k2 + n);
|
|
|
| const kernel = new Float32Array(k).fill(1.0 / k);
|
| const out = new Float32Array(n);
|
| for (let i = 0; i < n; i++) {
|
| let sum = 0;
|
| for (let j = 0; j < k; j++) {
|
| sum += x_ext[i + j] * kernel[j];
|
| }
|
| out[i] = sum;
|
| }
|
| return out;
|
| }
|
| }
|
|
|
|
|
| document.addEventListener('DOMContentLoaded', async () => {
|
| const app = new InferenceApp();
|
| await app.init();
|
| });
|
|
|