CoolFace
Apppublic

piealamodewhitebread/SillyTavern-Extras1

sourceHugging Facemitupdated 3y agoView on Hugging Face
0likes
server.py1108 linesDownload Raw Back to root
1import argparse2import base643import gc4import hashlib5import os6import secrets7import sys8import time9import unicodedata10from functools import wraps11from io import BytesIO12from random import randint13 14import markdown15import torch16import webuiapi17from colorama import Fore, Style18from colorama import init as colorama_init19from flask import (Flask, Response, abort, jsonify, render_template_string,20                   request, send_file, send_from_directory)21from flask_compress import Compress22from flask_cors import CORS23from PIL import Image24from transformers import (AutoModelForCausalLM, AutoModelForSeq2SeqLM,25                          AutoProcessor, AutoTokenizer,26                          BlipForConditionalGeneration, pipeline)27 28from constants import *29 30colorama_init()31 32if sys.hexversion < 0x030b0000:33    print(f"{Fore.BLUE}{Style.BRIGHT}Python 3.11 or newer is recommended to run this program.{Style.RESET_ALL}")34    time.sleep(2)35 36class SplitArgs(argparse.Action):37    def __call__(self, parser, namespace, values, option_string=None):38        setattr(39            namespace, self.dest, values.replace('"', "").replace("'", "").split(",")40        )41 42#Setting Root Folders for Silero Generations so it is compatible with STSL, should not effect regular runs. - Rolyat43parent_dir = os.path.dirname(os.path.abspath(__file__))44SILERO_SAMPLES_PATH = os.path.join(parent_dir, "tts_samples")45SILERO_SAMPLE_TEXT = os.path.join(parent_dir)46 47# Create directories if they don't exist48if not os.path.exists(SILERO_SAMPLES_PATH):49    os.makedirs(SILERO_SAMPLES_PATH)50if not os.path.exists(SILERO_SAMPLE_TEXT):51    os.makedirs(SILERO_SAMPLE_TEXT)52 53# Script arguments54parser = argparse.ArgumentParser(55    prog="SillyTavern Extras", description="Web API for transformers models"56)57parser.add_argument(58    "--port", type=int, help="Specify the port on which the application is hosted"59)60parser.add_argument(61    "--listen", action="store_true", help="Host the app on the local network"62)63parser.add_argument(64    "--share", action="store_true", help="Share the app on CloudFlare tunnel"65)66parser.add_argument("--cpu", action="store_true", help="Run the models on the CPU")67parser.add_argument("--cuda", action="store_false", dest="cpu", help="Run the models on the GPU")68parser.add_argument("--cuda-device", help="Specify the CUDA device to use")69parser.add_argument("--mps", "--apple", "--m1", "--m2", action="store_false", dest="cpu", help="Run the models on Apple Silicon")70parser.set_defaults(cpu=True)71parser.add_argument("--summarization-model", help="Load a custom summarization model")72parser.add_argument(73    "--classification-model", help="Load a custom text classification model"74)75parser.add_argument("--captioning-model", help="Load a custom captioning model")76parser.add_argument("--embedding-model", help="Load a custom text embedding model")77parser.add_argument("--chroma-host", help="Host IP for a remote ChromaDB instance")78parser.add_argument("--chroma-port", help="HTTP port for a remote ChromaDB instance (defaults to 8000)")79parser.add_argument("--chroma-folder", help="Path for chromadb persistence folder", default='.chroma_db')80parser.add_argument('--chroma-persist', help="ChromaDB persistence", default=True, action=argparse.BooleanOptionalAction)81parser.add_argument(82    "--secure", action="store_true", help="Enforces the use of an API key"83)84parser.add_argument("--talkinghead-gpu", action="store_true", help="Run the talkinghead animation on the GPU (CPU is default)")85 86parser.add_argument("--coqui-gpu", action="store_true", help="Run the voice models on the GPU (CPU is default)")87parser.add_argument("--coqui-models", help="Install given Coqui-api TTS model at launch (comma separated list, last one will be loaded at start)")88 89parser.add_argument("--max-content-length", help="Set the max")90parser.add_argument("--rvc-save-file", action="store_true", help="Save the last rvc input/output audio file into data/tmp/ folder (for research)")91 92parser.add_argument("--stt-vosk-model-path", help="Load a custom vosk speech-to-text model")93parser.add_argument("--stt-whisper-model-path", help="Load a custom vosk speech-to-text model")94sd_group = parser.add_mutually_exclusive_group()95 96local_sd = parser.add_argument_group("sd-local")97local_sd.add_argument("--sd-model", help="Load a custom SD image generation model")98local_sd.add_argument("--sd-cpu", help="Force the SD pipeline to run on the CPU", action="store_true")99 100remote_sd = parser.add_argument_group("sd-remote")101remote_sd.add_argument(102    "--sd-remote", action="store_true", help="Use a remote backend for SD"103)104remote_sd.add_argument(105    "--sd-remote-host", type=str, help="Specify the host of the remote SD backend"106)107remote_sd.add_argument(108    "--sd-remote-port", type=int, help="Specify the port of the remote SD backend"109)110remote_sd.add_argument(111    "--sd-remote-ssl", action="store_true", help="Use SSL for the remote SD backend"112)113remote_sd.add_argument(114    "--sd-remote-auth",115    type=str,116    help="Specify the username:password for the remote SD backend (if required)",117)118 119parser.add_argument(120    "--enable-modules",121    action=SplitArgs,122    default=[],123    help="Override a list of enabled modules",124)125 126args = parser.parse_args()127# [HF, Huggingface] Set port to 7860, set host to remote.128port = 7860129host = "0.0.0.0"130summarization_model = (131    args.summarization_model132    if args.summarization_model133    else DEFAULT_SUMMARIZATION_MODEL134)135classification_model = (136    args.classification_model137    if args.classification_model138    else DEFAULT_CLASSIFICATION_MODEL139)140captioning_model = (141    args.captioning_model if args.captioning_model else DEFAULT_CAPTIONING_MODEL142)143embedding_model = (144    args.embedding_model if args.embedding_model else DEFAULT_EMBEDDING_MODEL145)146 147sd_use_remote = False if args.sd_model else True148sd_model = args.sd_model if args.sd_model else DEFAULT_SD_MODEL149sd_remote_host = args.sd_remote_host if args.sd_remote_host else DEFAULT_REMOTE_SD_HOST150sd_remote_port = args.sd_remote_port if args.sd_remote_port else DEFAULT_REMOTE_SD_PORT151sd_remote_ssl = args.sd_remote_ssl152sd_remote_auth = args.sd_remote_auth153 154modules = (155    args.enable_modules if args.enable_modules and len(args.enable_modules) > 0 else []156)157 158if len(modules) == 0:159    print(160        f"{Fore.RED}{Style.BRIGHT}You did not select any modules to run! Choose them by adding an --enable-modules option"161    )162    print(f"Example: --enable-modules=caption,summarize{Style.RESET_ALL}")163 164# Models init165cuda_device = DEFAULT_CUDA_DEVICE if not args.cuda_device else args.cuda_device166device_string = cuda_device if torch.cuda.is_available() and not args.cpu else 'mps' if torch.backends.mps.is_available() and not args.cpu else 'cpu'167device = torch.device(device_string)168torch_dtype = torch.float32 if device_string != cuda_device  else torch.float16169 170if not torch.cuda.is_available() and not args.cpu:171    print(f"{Fore.YELLOW}{Style.BRIGHT}torch-cuda is not supported on this device.{Style.RESET_ALL}")172    if not torch.backends.mps.is_available() and not args.cpu:173        print(f"{Fore.YELLOW}{Style.BRIGHT}torch-mps is not supported on this device.{Style.RESET_ALL}")174 175 176print(f"{Fore.GREEN}{Style.BRIGHT}Using torch device: {device_string}{Style.RESET_ALL}")177 178if "talkinghead" in modules:179    import sys180    import threading181    mode = "cuda" if args.talkinghead_gpu else "cpu"182    print("Initializing talkinghead pipeline in " + mode + " mode....")183    talkinghead_path = os.path.abspath(os.path.join(os.getcwd(), "talkinghead"))184    sys.path.append(talkinghead_path) # Add the path to the 'tha3' module to the sys.path list185 186    try:187        import talkinghead.tha3.app.app as talkinghead188        from talkinghead import *189        def launch_talkinghead_gui():190            talkinghead.launch_gui(mode, "separable_float")191        #choices=['standard_float', 'separable_float', 'standard_half', 'separable_half'],192        #choices='The device to use for PyTorch ("cuda" for GPU, "cpu" for CPU).'193        talkinghead_thread = threading.Thread(target=launch_talkinghead_gui)194        talkinghead_thread.daemon = True  # Set the thread as a daemon thread195        talkinghead_thread.start()196 197    except ModuleNotFoundError:198        print("Error: Could not import the 'talkinghead' module.")199 200if "caption" in modules:201    print("Initializing an image captioning model...")202    captioning_processor = AutoProcessor.from_pretrained(captioning_model)203    if "blip" in captioning_model:204        captioning_transformer = BlipForConditionalGeneration.from_pretrained(205            captioning_model, torch_dtype=torch_dtype206        ).to(device)207    else:208        captioning_transformer = AutoModelForCausalLM.from_pretrained(209            captioning_model, torch_dtype=torch_dtype210        ).to(device)211 212if "summarize" in modules:213    print("Initializing a text summarization model...")214    summarization_tokenizer = AutoTokenizer.from_pretrained(summarization_model)215    summarization_transformer = AutoModelForSeq2SeqLM.from_pretrained(216        summarization_model, torch_dtype=torch_dtype217    ).to(device)218 219if "sd" in modules and not sd_use_remote:220    from diffusers import (EulerAncestralDiscreteScheduler,221                           StableDiffusionPipeline)222 223    print("Initializing Stable Diffusion pipeline...")224    sd_device_string = cuda_device if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'225    sd_device = torch.device(sd_device_string)226    sd_torch_dtype = torch.float32 if sd_device_string != cuda_device else torch.float16227    sd_pipe = StableDiffusionPipeline.from_pretrained(228        sd_model, custom_pipeline="lpw_stable_diffusion", torch_dtype=sd_torch_dtype229    ).to(sd_device)230    sd_pipe.safety_checker = lambda images, clip_input: (images, False)231    sd_pipe.enable_attention_slicing()232    # pipe.scheduler = KarrasVeScheduler.from_config(pipe.scheduler.config)233    sd_pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(234        sd_pipe.scheduler.config235    )236elif "sd" in modules and sd_use_remote:237    print("Initializing Stable Diffusion connection")238    try:239        sd_remote = webuiapi.WebUIApi(240            host=sd_remote_host, port=sd_remote_port, use_https=sd_remote_ssl241        )242        if sd_remote_auth:243            username, password = sd_remote_auth.split(":")244            sd_remote.set_auth(username, password)245        sd_remote.util_wait_for_ready()246    except Exception as e:247        # remote sd from modules248        print(249            f"{Fore.RED}{Style.BRIGHT}Could not connect to remote SD backend at http{'s' if sd_remote_ssl else ''}://{sd_remote_host}:{sd_remote_port}! Disabling SD module...{Style.RESET_ALL}"250        )251        modules.remove("sd")252 253if "tts" in modules:254    print("tts module is deprecated. Please use silero-tts instead.")255    modules.remove("tts")256    modules.append("silero-tts")257 258 259if "silero-tts" in modules:260    if not os.path.exists(SILERO_SAMPLES_PATH):261        os.makedirs(SILERO_SAMPLES_PATH)262    print("Initializing Silero TTS server")263    from silero_api_server import tts264 265    tts_service = tts.SileroTtsService(SILERO_SAMPLES_PATH)266    if len(os.listdir(SILERO_SAMPLES_PATH)) == 0:267        print("Generating Silero TTS samples...")268        tts_service.update_sample_text(SILERO_SAMPLE_TEXT)269        tts_service.generate_samples()270 271if "edge-tts" in modules:272    print("Initializing Edge TTS client")273    import tts_edge as edge274 275 276if "chromadb" in modules:277    print("Initializing ChromaDB")278    import chromadb279    import posthog280    from chromadb.config import Settings281    from sentence_transformers import SentenceTransformer282 283    # Assume that the user wants in-memory unless a host is specified284    # Also disable chromadb telemetry285    posthog.capture = lambda *args, **kwargs: None286    if args.chroma_host is None:287        if args.chroma_persist:288            chromadb_client = chromadb.PersistentClient(path=args.chroma_folder, settings=Settings(anonymized_telemetry=False))289            print(f"ChromaDB is running in-memory with persistence. Persistence is stored in {args.chroma_folder}. Can be cleared by deleting the folder or purging db.")290        else:291            chromadb_client = chromadb.EphemeralClient(Settings(anonymized_telemetry=False))292            print(f"ChromaDB is running in-memory without persistence.")293    else:294        chroma_port=(295            args.chroma_port if args.chroma_port else DEFAULT_CHROMA_PORT296        )297        chromadb_client = chromadb.HttpClient(host=args.chroma_host, port=chroma_port, settings=Settings(anonymized_telemetry=False))298        print(f"ChromaDB is remotely configured at {args.chroma_host}:{chroma_port}")299 300    chromadb_embedder = SentenceTransformer(embedding_model, device=device_string)301    chromadb_embed_fn = lambda *args, **kwargs: chromadb_embedder.encode(*args, **kwargs).tolist()302 303    # Check if the db is connected and running, otherwise tell the user304    try:305        chromadb_client.heartbeat()306        print("Successfully pinged ChromaDB! Your client is successfully connected.")307    except:308        print("Could not ping ChromaDB! If you are running remotely, please check your host and port!")309 310# Flask init311app = Flask(__name__)312CORS(app)  # allow cross-domain requests313Compress(app) # compress responses314app.config["MAX_CONTENT_LENGTH"] = 500 * 1024 * 1024315 316max_content_length = (317    args.max_content_length318    if args.max_content_length319    else None)320 321if max_content_length is not None:322    print("Setting MAX_CONTENT_LENGTH to",max_content_length,"Mb")323    app.config["MAX_CONTENT_LENGTH"] = int(max_content_length) * 1024 * 1024324 325if "classify" in modules:326    import modules.classify.classify_module as classify_module327    classify_module.init_text_emotion_classifier(classification_model, device, torch_dtype)328 329if "vosk-stt" in modules:330    print("Initializing Vosk speech-recognition (from ST request file)")331    vosk_model_path = (332    args.stt_vosk_model_path333    if args.stt_vosk_model_path334    else None)335 336    import modules.speech_recognition.vosk_module as vosk_module337 338    vosk_module.model = vosk_module.load_model(file_path=vosk_model_path)339    app.add_url_rule("/api/speech-recognition/vosk/process-audio", view_func=vosk_module.process_audio, methods=["POST"])340 341if "whisper-stt" in modules:342    print("Initializing Whisper speech-recognition (from ST request file)")343    whisper_model_path = (344    args.stt_whisper_model_path345    if args.stt_whisper_model_path346    else None)347 348    import modules.speech_recognition.whisper_module as whisper_module349 350    whisper_module.model = whisper_module.load_model(file_path=whisper_model_path)351    app.add_url_rule("/api/speech-recognition/whisper/process-audio", view_func=whisper_module.process_audio, methods=["POST"])352 353if "streaming-stt" in modules:354    print("Initializing vosk/whisper speech-recognition (from extras server microphone)")355    whisper_model_path = (356    args.stt_whisper_model_path357    if args.stt_whisper_model_path358    else None)359 360    import modules.speech_recognition.streaming_module as streaming_module361 362    streaming_module.whisper_model, streaming_module.vosk_model = streaming_module.load_model(file_path=whisper_model_path)363    app.add_url_rule("/api/speech-recognition/streaming/record-and-transcript", view_func=streaming_module.record_and_transcript, methods=["POST"])364 365if "rvc" in modules:366    print("Initializing RVC voice conversion (from ST request file)")367    print("Increasing server upload limit")368    rvc_save_file = (369    args.rvc_save_file370    if args.rvc_save_file371    else False)372 373    if rvc_save_file:374        print("RVC saving file option detected, input/output audio will be savec into data/tmp/ folder")375 376    import sys377    sys.path.insert(0,'modules/voice_conversion')378 379    import modules.voice_conversion.rvc_module as rvc_module380    rvc_module.save_file = rvc_save_file381 382    if "classify" in modules:383        rvc_module.classification_mode = True384 385    rvc_module.fix_model_install()386    app.add_url_rule("/api/voice-conversion/rvc/get-models-list", view_func=rvc_module.rvc_get_models_list, methods=["POST"])387    app.add_url_rule("/api/voice-conversion/rvc/upload-models", view_func=rvc_module.rvc_upload_models, methods=["POST"])388    app.add_url_rule("/api/voice-conversion/rvc/process-audio", view_func=rvc_module.rvc_process_audio, methods=["POST"])389 390 391if "coqui-tts" in modules:392    mode = "GPU" if args.coqui_gpu else "CPU"393    print("Initializing Coqui TTS client in " + mode + " mode")394    import modules.text_to_speech.coqui.coqui_module as coqui_module395 396    if mode == "GPU":397        coqui_module.gpu_mode = True398 399    coqui_models = (400    args.coqui_models401    if args.coqui_models402    else None403    )404 405    if coqui_models is not None:406        coqui_models = coqui_models.split(",")407        for i in coqui_models:408            if not coqui_module.install_model(i):409                raise ValueError("Coqui model loading failed, most likely a wrong model name in --coqui-models argument, check log above to see which one")410 411    # Coqui-api models412    app.add_url_rule("/api/text-to-speech/coqui/coqui-api/check-model-state", view_func=coqui_module.coqui_check_model_state, methods=["POST"])413    app.add_url_rule("/api/text-to-speech/coqui/coqui-api/install-model", view_func=coqui_module.coqui_install_model, methods=["POST"])414 415    # Users models416    app.add_url_rule("/api/text-to-speech/coqui/local/get-models", view_func=coqui_module.coqui_get_local_models, methods=["POST"])417 418    # Handle both coqui-api/users models419    app.add_url_rule("/api/text-to-speech/coqui/generate-tts", view_func=coqui_module.coqui_generate_tts, methods=["POST"])420 421def require_module(name):422    def wrapper(fn):423        @wraps(fn)424        def decorated_view(*args, **kwargs):425            if name not in modules:426                abort(403, "Module is disabled by config")427            return fn(*args, **kwargs)428 429        return decorated_view430 431    return wrapper432 433 434# AI stuff435def classify_text(text: str) -> list:436    return classify_module.classify_text_emotion(text)437 438 439def caption_image(raw_image: Image, max_new_tokens: int = 20) -> str:440    inputs = captioning_processor(raw_image.convert("RGB"), return_tensors="pt").to(441        device, torch_dtype442    )443    outputs = captioning_transformer.generate(**inputs, max_new_tokens=max_new_tokens)444    caption = captioning_processor.decode(outputs[0], skip_special_tokens=True)445    return caption446 447 448def summarize_chunks(text: str, params: dict) -> str:449    try:450        return summarize(text, params)451    except IndexError:452        print(453            "Sequence length too large for model, cutting text in half and calling again"454        )455        new_params = params.copy()456        new_params["max_length"] = new_params["max_length"] // 2457        new_params["min_length"] = new_params["min_length"] // 2458        return summarize_chunks(459            text[: (len(text) // 2)], new_params460        ) + summarize_chunks(text[(len(text) // 2) :], new_params)461 462 463def summarize(text: str, params: dict) -> str:464    # Tokenize input465    inputs = summarization_tokenizer(text, return_tensors="pt").to(device)466    token_count = len(inputs[0])467 468    bad_words_ids = [469        summarization_tokenizer(bad_word, add_special_tokens=False).input_ids470        for bad_word in params["bad_words"]471    ]472    summary_ids = summarization_transformer.generate(473        inputs["input_ids"],474        num_beams=2,475        max_new_tokens=max(token_count, int(params["max_length"])),476        min_new_tokens=min(token_count, int(params["min_length"])),477        repetition_penalty=float(params["repetition_penalty"]),478        temperature=float(params["temperature"]),479        length_penalty=float(params["length_penalty"]),480        bad_words_ids=bad_words_ids,481    )482    summary = summarization_tokenizer.batch_decode(483        summary_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True484    )[0]485    summary = normalize_string(summary)486    return summary487 488 489def normalize_string(input: str) -> str:490    output = " ".join(unicodedata.normalize("NFKC", input).strip().split())491    return output492 493 494def generate_image(data: dict) -> Image:495    prompt = normalize_string(f'{data["prompt_prefix"]} {data["prompt"]}')496 497    if sd_use_remote:498        image = sd_remote.txt2img(499            prompt=prompt,500            negative_prompt=data["negative_prompt"],501            sampler_name=data["sampler"],502            steps=data["steps"],503            cfg_scale=data["scale"],504            width=data["width"],505            height=data["height"],506            restore_faces=data["restore_faces"],507            enable_hr=data["enable_hr"],508            save_images=True,509            send_images=True,510            do_not_save_grid=False,511            do_not_save_samples=False,512        ).image513    else:514        image = sd_pipe(515            prompt=prompt,516            negative_prompt=data["negative_prompt"],517            num_inference_steps=data["steps"],518            guidance_scale=data["scale"],519            width=data["width"],520            height=data["height"],521        ).images[0]522 523    image.save("./debug.png")524    return image525 526 527def image_to_base64(image: Image, quality: int = 75) -> str:528    buffer = BytesIO()529    image.convert("RGB")530    image.save(buffer, format="JPEG", quality=quality)531    img_str = base64.b64encode(buffer.getvalue()).decode("utf-8")532    return img_str533 534 535ignore_auth = []536# [HF, Huggingface] Get password instead of text file.537api_key = os.environ.get("password")538 539def is_authorize_ignored(request):540    view_func = app.view_functions.get(request.endpoint)541 542    if view_func is not None:543        if view_func in ignore_auth:544            return True545    return False546 547 548@app.before_request549def before_request():550    # Request time measuring551    request.start_time = time.time()552 553    # Checks if an API key is present and valid, otherwise return unauthorized554    # The options check is required so CORS doesn't get angry555    try:556        if request.method != 'OPTIONS' and is_authorize_ignored(request) == False and getattr(request.authorization, 'token', '') != api_key:557            print(f"WARNING: Unauthorized API key access from {request.remote_addr}")558            if request.method == 'POST':559                print(f"Incoming POST request with {request.headers.get('Authorization')}")560            response = jsonify({ 'error': '401: Invalid API key' })561            response.status_code = 401562            return "https://(hf_name)-(space_name).hf.space/"563    except Exception as e:564        print(f"API key check error: {e}")565        return "https://(hf_name)-(space_name).hf.space/"566 567 568@app.after_request569def after_request(response):570    duration = time.time() - request.start_time571    response.headers["X-Request-Duration"] = str(duration)572    return response573 574 575@app.route("/", methods=["GET"])576def index():577    with open("./README.md", "r", encoding="utf8") as f:578        content = f.read()579    return render_template_string(markdown.markdown(content, extensions=["tables"]))580 581 582@app.route("/api/extensions", methods=["GET"])583def get_extensions():584    extensions = dict(585        {586            "extensions": [587                {588                    "name": "not-supported",589                    "metadata": {590                        "display_name": """<span style="white-space:break-spaces;">Extensions serving using Extensions API is no longer supported. Please update the mod from: <a href="https://github.com/Cohee1207/SillyTavern">https://github.com/Cohee1207/SillyTavern</a></span>""",591                        "requires": [],592                        "assets": [],593                    },594                }595            ]596        }597    )598    return jsonify(extensions)599 600 601@app.route("/api/caption", methods=["POST"])602@require_module("caption")603def api_caption():604    data = request.get_json()605 606    if "image" not in data or not isinstance(data["image"], str):607        abort(400, '"image" is required')608 609    image = Image.open(BytesIO(base64.b64decode(data["image"])))610    image = image.convert("RGB")611    image.thumbnail((512, 512))612    caption = caption_image(image)613    thumbnail = image_to_base64(image)614    print("Caption:", caption, sep="\n")615    gc.collect()616    return jsonify({"caption": caption, "thumbnail": thumbnail})617 618 619@app.route("/api/summarize", methods=["POST"])620@require_module("summarize")621def api_summarize():622    data = request.get_json()623 624    if "text" not in data or not isinstance(data["text"], str):625        abort(400, '"text" is required')626 627    params = DEFAULT_SUMMARIZE_PARAMS.copy()628 629    if "params" in data and isinstance(data["params"], dict):630        params.update(data["params"])631 632    print("Summary input:", data["text"], sep="\n")633    summary = summarize_chunks(data["text"], params)634    print("Summary output:", summary, sep="\n")635    gc.collect()636    return jsonify({"summary": summary})637 638 639@app.route("/api/classify", methods=["POST"])640@require_module("classify")641def api_classify():642    data = request.get_json()643 644    if "text" not in data or not isinstance(data["text"], str):645        abort(400, '"text" is required')646 647    print("Classification input:", data["text"], sep="\n")648    classification = classify_text(data["text"])649    print("Classification output:", classification, sep="\n")650    gc.collect()651    if "talkinghead" in modules: #send emotion to talkinghead652        talkinghead.setEmotion(classification)653    return jsonify({"classification": classification})654 655 656@app.route("/api/classify/labels", methods=["GET"])657@require_module("classify")658def api_classify_labels():659    classification = classify_text("")660    labels = [x["label"] for x in classification]661    if "talkinghead" in modules:662        labels.append('talkinghead')  # Add 'talkinghead' to the labels list663    return jsonify({"labels": labels})664 665@app.route("/api/talkinghead/load", methods=["POST"])666def live_load():667    file = request.files['file']668    # convert stream to bytes and pass to talkinghead_load669    return talkinghead.talkinghead_load_file(file.stream)670 671@app.route('/api/talkinghead/unload')672def live_unload():673    return talkinghead.unload()674 675@app.route('/api/talkinghead/start_talking')676def start_talking():677    return talkinghead.start_talking()678 679@app.route('/api/talkinghead/stop_talking')680def stop_talking():681    return talkinghead.stop_talking()682 683@app.route('/api/talkinghead/result_feed')684def result_feed():685    return talkinghead.result_feed()686 687@app.route("/api/image", methods=["POST"])688@require_module("sd")689def api_image():690    required_fields = {691        "prompt": str,692    }693 694    optional_fields = {695        "steps": 30,696        "scale": 6,697        "sampler": "DDIM",698        "width": 512,699        "height": 512,700        "restore_faces": False,701        "enable_hr": False,702        "prompt_prefix": PROMPT_PREFIX,703        "negative_prompt": NEGATIVE_PROMPT,704    }705 706    data = request.get_json()707 708    # Check required fields709    for field, field_type in required_fields.items():710        if field not in data or not isinstance(data[field], field_type):711            abort(400, f'"{field}" is required')712 713    # Set optional fields to default values if not provided714    for field, default_value in optional_fields.items():715        type_match = (716            (int, float)717            if isinstance(default_value, (int, float))718            else type(default_value)719        )720        if field not in data or not isinstance(data[field], type_match):721            data[field] = default_value722 723    try:724        print("SD inputs:", data, sep="\n")725        image = generate_image(data)726        base64image = image_to_base64(image, quality=90)727        return jsonify({"image": base64image})728    except RuntimeError as e:729        abort(400, str(e))730 731 732@app.route("/api/image/model", methods=["POST"])733@require_module("sd")734def api_image_model_set():735    data = request.get_json()736 737    if not sd_use_remote:738        abort(400, "Changing model for local sd is not supported.")739    if "model" not in data or not isinstance(data["model"], str):740        abort(400, '"model" is required')741 742    old_model = sd_remote.util_get_current_model()743    sd_remote.util_set_model(data["model"], find_closest=False)744    # sd_remote.util_set_model(data['model'])745    sd_remote.util_wait_for_ready()746    new_model = sd_remote.util_get_current_model()747 748    return jsonify({"previous_model": old_model, "current_model": new_model})749 750 751@app.route("/api/image/model", methods=["GET"])752@require_module("sd")753def api_image_model_get():754    model = sd_model755 756    if sd_use_remote:757        model = sd_remote.util_get_current_model()758 759    return jsonify({"model": model})760 761 762@app.route("/api/image/models", methods=["GET"])763@require_module("sd")764def api_image_models():765    models = [sd_model]766 767    if sd_use_remote:768        models = sd_remote.util_get_model_names()769 770    return jsonify({"models": models})771 772 773@app.route("/api/image/samplers", methods=["GET"])774@require_module("sd")775def api_image_samplers():776    samplers = ["Euler a"]777 778    if sd_use_remote:779        samplers = [sampler["name"] for sampler in sd_remote.get_samplers()]780 781    return jsonify({"samplers": samplers})782 783 784@app.route("/api/modules", methods=["GET"])785def get_modules():786    return jsonify({"modules": modules})787 788 789@app.route("/api/tts/speakers", methods=["GET"])790@require_module("silero-tts")791def tts_speakers():792    voices = [793        {794            "name": speaker,795            "voice_id": speaker,796            "preview_url": f"{str(request.url_root)}api/tts/sample/{speaker}",797        }798        for speaker in tts_service.get_speakers()799    ]800    return jsonify(voices)801 802# Added fix for Silero not working as new files were unable to be created if one already existed. - Rolyat 7/7/23803@app.route("/api/tts/generate", methods=["POST"])804@require_module("silero-tts")805def tts_generate():806    voice = request.get_json()807    if "text" not in voice or not isinstance(voice["text"], str):808        abort(400, '"text" is required')809    if "speaker" not in voice or not isinstance(voice["speaker"], str):810        abort(400, '"speaker" is required')811    # Remove asterisks812    voice["text"] = voice["text"].replace("*", "")813    try:814        # Remove the destination file if it already exists815        if os.path.exists('test.wav'):816            os.remove('test.wav')817 818        audio = tts_service.generate(voice["speaker"], voice["text"])819        audio_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.basename(audio))820 821        os.rename(audio, audio_file_path)822        return send_file(audio_file_path, mimetype="audio/x-wav")823    except Exception as e:824        print(e)825        abort(500, voice["speaker"])826 827 828@app.route("/api/tts/sample/<speaker>", methods=["GET"])829@require_module("silero-tts")830def tts_play_sample(speaker: str):831    return send_from_directory(SILERO_SAMPLES_PATH, f"{speaker}.wav")832 833 834@app.route("/api/edge-tts/list", methods=["GET"])835@require_module("edge-tts")836def edge_tts_list():837    voices = edge.get_voices()838    return jsonify(voices)839 840 841@app.route("/api/edge-tts/generate", methods=["POST"])842@require_module("edge-tts")843def edge_tts_generate():844    data = request.get_json()845    if "text" not in data or not isinstance(data["text"], str):846        abort(400, '"text" is required')847    if "voice" not in data or not isinstance(data["voice"], str):848        abort(400, '"voice" is required')849    if "rate" in data and isinstance(data['rate'], int):850        rate = data['rate']851    else:852        rate = 0853    # Remove asterisks854    data["text"] = data["text"].replace("*", "")855    try:856        audio = edge.generate_audio(text=data["text"], voice=data["voice"], rate=rate)857        return Response(audio, mimetype="audio/mpeg")858    except Exception as e:859        print(e)860        abort(500, data["voice"])861 862 863@app.route("/api/chromadb", methods=["POST"])864@require_module("chromadb")865def chromadb_add_messages():866    data = request.get_json()867    if "chat_id" not in data or not isinstance(data["chat_id"], str):868        abort(400, '"chat_id" is required')869    if "messages" not in data or not isinstance(data["messages"], list):870        abort(400, '"messages" is required')871 872    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()873    collection = chromadb_client.get_or_create_collection(874        name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn875    )876 877    documents = [m["content"] for m in data["messages"]]878    ids = [m["id"] for m in data["messages"]]879    metadatas = [880        {"role": m["role"], "date": m["date"], "meta": m.get("meta", "")}881        for m in data["messages"]882    ]883 884    collection.upsert(885        ids=ids,886        documents=documents,887        metadatas=metadatas,888    )889 890    return jsonify({"count": len(ids)})891 892 893@app.route("/api/chromadb/purge", methods=["POST"])894@require_module("chromadb")895def chromadb_purge():896    data = request.get_json()897    if "chat_id" not in data or not isinstance(data["chat_id"], str):898        abort(400, '"chat_id" is required')899 900    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()901    collection = chromadb_client.get_or_create_collection(902        name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn903    )904 905    count = collection.count()906    collection.delete()907    print("ChromaDB embeddings deleted", count)908    return 'Ok', 200909 910 911@app.route("/api/chromadb/query", methods=["POST"])912@require_module("chromadb")913def chromadb_query():914    data = request.get_json()915    if "chat_id" not in data or not isinstance(data["chat_id"], str):916        abort(400, '"chat_id" is required')917    if "query" not in data or not isinstance(data["query"], str):918        abort(400, '"query" is required')919 920    if "n_results" not in data or not isinstance(data["n_results"], int):921        n_results = 1922    else:923        n_results = data["n_results"]924 925    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()926    collection = chromadb_client.get_or_create_collection(927        name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn928    )929 930    if collection.count() == 0:931        print(f"Queried empty/missing collection for {repr(data['chat_id'])}.")932        return jsonify([])933 934 935    n_results = min(collection.count(), n_results)936    query_result = collection.query(937        query_texts=[data["query"]],938        n_results=n_results,939    )940 941    documents = query_result["documents"][0]942    ids = query_result["ids"][0]943    metadatas = query_result["metadatas"][0]944    distances = query_result["distances"][0]945 946    messages = [947        {948            "id": ids[i],949            "date": metadatas[i]["date"],950            "role": metadatas[i]["role"],951            "meta": metadatas[i]["meta"],952            "content": documents[i],953            "distance": distances[i],954        }955        for i in range(len(ids))956    ]957 958    return jsonify(messages)959 960@app.route("/api/chromadb/multiquery", methods=["POST"])961@require_module("chromadb")962def chromadb_multiquery():963    data = request.get_json()964    if "chat_list" not in data or not isinstance(data["chat_list"], list):965        abort(400, '"chat_list" is required and should be a list')966    if "query" not in data or not isinstance(data["query"], str):967        abort(400, '"query" is required')968 969    if "n_results" not in data or not isinstance(data["n_results"], int):970        n_results = 1971    else:972        n_results = data["n_results"]973 974    messages = []975 976    for chat_id in data["chat_list"]:977        if not isinstance(chat_id, str):978            continue979 980        try:981            chat_id_md5 = hashlib.md5(chat_id.encode()).hexdigest()982            collection = chromadb_client.get_collection(983                name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn984            )985 986            # Skip this chat if the collection is empty987            if collection.count() == 0:988                continue989 990            n_results_per_chat = min(collection.count(), n_results)991            query_result = collection.query(992                query_texts=[data["query"]],993                n_results=n_results_per_chat,994            )995            documents = query_result["documents"][0]996            ids = query_result["ids"][0]997            metadatas = query_result["metadatas"][0]998            distances = query_result["distances"][0]999 1000            chat_messages = [1001                {1002                    "id": ids[i],1003                    "date": metadatas[i]["date"],1004                    "role": metadatas[i]["role"],1005                    "meta": metadatas[i]["meta"],1006                    "content": documents[i],1007                    "distance": distances[i],1008                }1009                for i in range(len(ids))1010            ]1011 1012            messages.extend(chat_messages)1013        except Exception as e:1014            print(e)1015 1016    #remove duplicate msgs, filter down to the right number1017    seen = set()1018    messages = [d for d in messages if not (d['content'] in seen or seen.add(d['content']))]1019    messages = sorted(messages, key=lambda x: x['distance'])[0:n_results]1020 1021    return jsonify(messages)1022 1023 1024@app.route("/api/chromadb/export", methods=["POST"])1025@require_module("chromadb")1026def chromadb_export():1027    data = request.get_json()1028    if "chat_id" not in data or not isinstance(data["chat_id"], str):1029        abort(400, '"chat_id" is required')1030 1031    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()1032    try:1033        collection = chromadb_client.get_collection(1034            name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn1035        )1036    except Exception as e:1037        print(e)1038        abort(400, "Chat collection not found in chromadb")1039 1040    collection_content = collection.get()1041    documents = collection_content.get('documents', [])1042    ids = collection_content.get('ids', [])1043    metadatas = collection_content.get('metadatas', [])1044 1045    unsorted_content = [1046        {1047            "id": ids[i],1048            "metadata": metadatas[i],1049            "document": documents[i],1050        }1051        for i in range(len(ids))1052    ]1053 1054    sorted_content = sorted(unsorted_content, key=lambda x: x['metadata']['date'])1055 1056    export = {1057        "chat_id": data["chat_id"],1058        "content": sorted_content1059    }1060 1061    return jsonify(export)1062 1063@app.route("/api/chromadb/import", methods=["POST"])1064@require_module("chromadb")1065def chromadb_import():1066    data = request.get_json()1067    content = data['content']1068    if "chat_id" not in data or not isinstance(data["chat_id"], str):1069        abort(400, '"chat_id" is required')1070 1071    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()1072    collection = chromadb_client.get_or_create_collection(1073        name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn1074    )1075 1076    documents = [item['document'] for item in content]1077    metadatas = [item['metadata'] for item in content]1078    ids = [item['id'] for item in content]1079 1080 1081    collection.upsert(documents=documents, metadatas=metadatas, ids=ids)1082    print(f"Imported {len(ids)} (total {collection.count()}) content entries into {repr(data['chat_id'])}")1083 1084    return jsonify({"count": len(ids)})1085 1086 1087if args.share:1088    import inspect1089 1090    from flask_cloudflared import _run_cloudflared1091 1092    sig = inspect.signature(_run_cloudflared)1093    sum = sum(1094        11095        for param in sig.parameters.values()1096        if param.kind == param.POSITIONAL_OR_KEYWORD1097    )1098    if sum > 1:1099        metrics_port = randint(8100, 9000)1100        cloudflare = _run_cloudflared(port, metrics_port)1101    else:1102        cloudflare = _run_cloudflared(port)1103    print(f"{Fore.GREEN}{Style.NORMAL}Running on: {cloudflare}{Style.RESET_ALL}")1104 1105ignore_auth.append(tts_play_sample)1106ignore_auth.append(result_feed)1107app.run(host=host, port=port)1108