Saat memasukkan key ke ngrok ada pas enter ada error berikut.
Error starting ngrok: SyntaxError: Unexpected token 'I', "Internal S"... is not valid JSON
startNgrok @ main.js:39
Perbaiki!
main.js
class NgrokController {
    constructor() {
        this.floatingButton = document.querySelector(".floating-button");
        this.modal = new bootstrap.Modal(document.getElementById("settingsModal"));
        this.ngrokForm = document.getElementById("ngrok-form");
        this.apiKeyInput = document.getElementById("api-key");
        this.ngrokStatus = document.getElementById("ngrok-status");
        this.startNgrokButton = document.getElementById("start-ngrok");
        this.stopNgrokButton = document.getElementById("stop-ngrok");

        this.initializeEvents();
    }

    initializeEvents() {
        this.floatingButton.addEventListener("click", () => this.modal.show());
        this.startNgrokButton.addEventListener("click", () => this.startNgrok());
        this.stopNgrokButton.addEventListener("click", () => this.stopNgrok());
    }

    async startNgrok() {
        const apiKey = this.apiKeyInput.value;
        try {
            const response = await fetch("/api/start_ngrok", {
                method: "POST",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                body: new URLSearchParams({ api_key: apiKey }),
            });

            const data = await response.json();
            if (response.ok) {
                this.updateNgrokStatus('success', `Ngrok is running. Public URL: <a href="${data.public_url}" target="_blank">${data.public_url}</a>`);
                this.toggleNgrokButtons();
            } else {
                this.updateNgrokStatus('danger', data.message);
            }
        } catch (error) {
            console.error("Error starting ngrok:", error);
            this.updateNgrokStatus('danger', "Error starting ngrok.");
        }
    }

    async stopNgrok() {
        try {
            const response = await fetch("/api/stop_ngrok", { method: "POST" });
            const data = await response.json();
            this.updateNgrokStatus(response.ok ? 'success' : 'danger', data.message);
            if (response.ok) this.toggleNgrokButtons();
        } catch (error) {
            console.error("Error stopping ngrok:", error);
            this.updateNgrokStatus('danger', "Error stopping ngrok.");
        }
    }

    updateNgrokStatus(status, message) {
        this.ngrokStatus.innerHTML = `<div class="alert alert-${status} mt-3" role="alert">${message}</div>`;
    }

    toggleNgrokButtons() {
        this.startNgrokButton.style.display = this.startNgrokButton.style.display === "none" ? "block" : "none";
        this.stopNgrokButton.style.display = this.stopNgrokButton.style.display === "none" ? "block" : "none";
    }
}

document.addEventListener("DOMContentLoaded", () => new NgrokController());
main.py
import logging
import threading
import urllib.request
import webbrowser
from fastapi import FastAPI, File, UploadFile, Form, Cookie
from fastapi.responses import HTMLResponse, JSONResponse
from starlette.middleware.cors import CORSMiddleware
from starlette.staticfiles import StaticFiles
from Chatbot import Chatbot
import cv2
import numpy as np
import uvicorn

logging.disable(logging.CRITICAL)


class ServerApp:
    def __init__(self):
        self.app = FastAPI()
        self.chatbot = Chatbot()
        self.ngrok_process = None
        self.public_url = None
        self.setup_routes_and_middlewares()

    def setup_routes_and_middlewares(self):
        self.app.add_middleware(
            CORSMiddleware,
            allow_origins=["*"],
            allow_credentials=True,
            allow_methods=["*"],
            allow_headers=["*"]
        )

        directories = {
            "/app": "app",
            "/assets": "app/assets",
            "/js": "app/js",
            "/temp": "temp",
            "/model/live2d": "model/live2d",
            "/CSS": "app/CSS"
        }
        for mount_point, directory in directories.items():
            self.app.mount(mount_point, StaticFiles(directory=directory), name=mount_point.strip("/"))

        # Define routes
        self.app.get("/")(self.index)
        self.app.get("/api/audio_status")(self.get_audio_status)
        self.app.post("/api/reset_audio_status")(self.reset_audio_status)
        self.app.post("/api/upload_frame")(self.upload_frame)
        self.app.post("/api/upload_audio")(self.upload_audio)
        self.app.post("/api/start_ngrok")(self.start_ngrok)
        self.app.post("/api/stop_ngrok")(self.stop_ngrok)

    @staticmethod
    async def index(ngrok_api_key: str = Cookie(default=None)):
        with open('app/index.html', 'r') as f:
            html_content = f.read()
        headers = {"ngrok_api_key": ngrok_api_key} if ngrok_api_key else {}
        return HTMLResponse(content=html_content, headers=headers)

    async def get_audio_status(self):
        return {"audio_ready": self.chatbot.audio_ready}

    async def reset_audio_status(self):
        self.chatbot.audio_ready = False
        return {"audio_ready": self.chatbot.audio_ready}

    async def upload_frame(self, image: UploadFile = File(...)):
        try:
            image_bytes = await image.read()
            frame = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_COLOR)
            self.chatbot.process_frame(frame)
            return JSONResponse(content={"message": "Frame processed successfully"}, status_code=200)
        except Exception as e:
            logging.error(f"Error processing frame: {e}")
            return JSONResponse(content={"message": "Failed to process frame"}, status_code=500)

    async def upload_audio(self, audio: UploadFile = File(...)):
        try:
            audio_bytes = await audio.read()
            self.chatbot.process_audio(audio_bytes)
            return JSONResponse(content={"message": "Audio processed successfully"}, status_code=200)
        except Exception as e:
            logging.error(f"Error processing audio: {e}")
            return JSONResponse(content={"message": "Failed to process audio"}, status_code=500)

    async def start_ngrok(self, api_key: str = Form(...)):
        if not self.check_internet_connection():
            return JSONResponse(content={"message": "Tidak ada koneksi internet."}, status_code=500)

        if self.ngrok_process:
            return JSONResponse(content={"message": "Ngrok sudah berjalan.", "public_url": self.public_url},
                                status_code=200)

        import ngrok
        ngrok.set_auth_token(api_key)
        self.public_url = ngrok.connect(8000).public_url
        self.ngrok_process = ngrok.get_ngrok_process()
        threading.Thread(target=self.ngrok_process.proc.wait).start()

        return JSONResponse(content={"message": "Ngrok berhasil dimulai.", "public_url": self.public_url},
                            status_code=200)

    async def stop_ngrok(self):
        if self.ngrok_process:
            import ngrok
            ngrok.kill()
            self.ngrok_process = None
            self.public_url = None
            return JSONResponse(content={"message": "Ngrok berhasil dihentikan."}, status_code=200)

        return JSONResponse(content={"message": "Ngrok tidak berjalan."}, status_code=400)

    @staticmethod
    def check_internet_connection():
        try:
            urllib.request.urlopen('https://google.com', timeout=5)
            return True
        except Exception:
            return False

    def run(self):
        threading.Thread(target=self.open_browser).start()
        uvicorn.run(self.app, host="localhost", port=8000)

    @staticmethod
    def open_browser():
        webbrowser.open_new("http://localhost:8000")


if __name__ == "__main__":
    server_app = ServerApp()
    server_app.run()