CoolFace
Apppublic

gui123/extras

sourceHugging Facemitupdated 3y agoView on Hugging Face
0likes
server.py965 linesDownload Raw Back to root
1from functools import wraps2from flask import (3    Flask,4    jsonify,5    request,6    Response,7    render_template_string,8    abort,9    send_from_directory,10    send_file,11)12from flask_cors import CORS13from flask_compress import Compress14import markdown15import argparse16from transformers import AutoTokenizer, AutoProcessor, pipeline17from transformers import AutoModelForCausalLM, AutoModelForSeq2SeqLM18from transformers import BlipForConditionalGeneration19import unicodedata20import torch21import time22import os23import gc24import sys25import secrets26from PIL import Image27import base6428from io import BytesIO29from random import randint30import webuiapi31import hashlib32from constants import *33from colorama import Fore, Style, init as colorama_init34 35colorama_init()36 37if sys.hexversion < 0x030b0000:38    print(f"{Fore.BLUE}{Style.BRIGHT}Python 3.11 or newer is recommended to run this program.{Style.RESET_ALL}")39    time.sleep(2)40 41class SplitArgs(argparse.Action):42    def __call__(self, parser, namespace, values, option_string=None):43        setattr(44            namespace, self.dest, values.replace('"', "").replace("'", "").split(",")45        )46 47#Setting Root Folders for Silero Generations so it is compatible with STSL, should not effect regular runs. - Rolyat48parent_dir = os.path.dirname(os.path.abspath(__file__))49SILERO_SAMPLES_PATH = os.path.join(parent_dir, "tts_samples")50SILERO_SAMPLE_TEXT = os.path.join(parent_dir)51 52# Create directories if they don't exist53if not os.path.exists(SILERO_SAMPLES_PATH):54    os.makedirs(SILERO_SAMPLES_PATH)55if not os.path.exists(SILERO_SAMPLE_TEXT):56    os.makedirs(SILERO_SAMPLE_TEXT)57 58# Script arguments59parser = argparse.ArgumentParser(60    prog="SillyTavern Extras", description="Web API for transformers models"61)62parser.add_argument(63    "--port", type=int, help="Specify the port on which the application is hosted"64)65parser.add_argument(66    "--listen", action="store_true", help="Host the app on the local network"67)68parser.add_argument(69    "--share", action="store_true", help="Share the app on CloudFlare tunnel"70)71parser.add_argument("--cpu", action="store_true", help="Run the models on the CPU")72parser.add_argument("--cuda", action="store_false", dest="cpu", help="Run the models on the GPU")73parser.add_argument("--cuda-device", help="Specify the CUDA device to use")74parser.add_argument("--mps", "--apple", "--m1", "--m2", action="store_false", dest="cpu", help="Run the models on Apple Silicon")75parser.set_defaults(cpu=True)76parser.add_argument("--summarization-model", help="Load a custom summarization model")77parser.add_argument(78    "--classification-model", help="Load a custom text classification model"79)80parser.add_argument("--captioning-model", help="Load a custom captioning model")81parser.add_argument("--embedding-model", help="Load a custom text embedding model")82parser.add_argument("--chroma-host", help="Host IP for a remote ChromaDB instance")83parser.add_argument("--chroma-port", help="HTTP port for a remote ChromaDB instance (defaults to 8000)")84parser.add_argument("--chroma-folder", help="Path for chromadb persistence folder", default='.chroma_db')85parser.add_argument('--chroma-persist', help="ChromaDB persistence", default=True, action=argparse.BooleanOptionalAction)86parser.add_argument(87    "--secure", action="store_true", help="Enforces the use of an API key"88)89sd_group = parser.add_mutually_exclusive_group()90 91local_sd = sd_group.add_argument_group("sd-local")92local_sd.add_argument("--sd-model", help="Load a custom SD image generation model")93local_sd.add_argument("--sd-cpu", help="Force the SD pipeline to run on the CPU", action="store_true")94 95remote_sd = sd_group.add_argument_group("sd-remote")96remote_sd.add_argument(97    "--sd-remote", action="store_true", help="Use a remote backend for SD"98)99remote_sd.add_argument(100    "--sd-remote-host", type=str, help="Specify the host of the remote SD backend"101)102remote_sd.add_argument(103    "--sd-remote-port", type=int, help="Specify the port of the remote SD backend"104)105remote_sd.add_argument(106    "--sd-remote-ssl", action="store_true", help="Use SSL for the remote SD backend"107)108remote_sd.add_argument(109    "--sd-remote-auth",110    type=str,111    help="Specify the username:password for the remote SD backend (if required)",112)113 114parser.add_argument(115    "--enable-modules",116    action=SplitArgs,117    default=[],118    help="Override a list of enabled modules",119)120 121args = parser.parse_args()122# [HF, Huggingface] Set port to 7860, set host to remote. 123port = 7860124host = "0.0.0.0"125summarization_model = (126    args.summarization_model127    if args.summarization_model128    else DEFAULT_SUMMARIZATION_MODEL129)130classification_model = (131    args.classification_model132    if args.classification_model133    else DEFAULT_CLASSIFICATION_MODEL134)135captioning_model = (136    args.captioning_model if args.captioning_model else DEFAULT_CAPTIONING_MODEL137)138embedding_model = (139    args.embedding_model if args.embedding_model else DEFAULT_EMBEDDING_MODEL140)141 142sd_use_remote = False if args.sd_model else True143sd_model = args.sd_model if args.sd_model else DEFAULT_SD_MODEL144sd_remote_host = args.sd_remote_host if args.sd_remote_host else DEFAULT_REMOTE_SD_HOST145sd_remote_port = args.sd_remote_port if args.sd_remote_port else DEFAULT_REMOTE_SD_PORT146sd_remote_ssl = args.sd_remote_ssl147sd_remote_auth = args.sd_remote_auth148 149modules = (150    args.enable_modules if args.enable_modules and len(args.enable_modules) > 0 else []151)152 153if len(modules) == 0:154    print(155        f"{Fore.RED}{Style.BRIGHT}You did not select any modules to run! Choose them by adding an --enable-modules option"156    )157    print(f"Example: --enable-modules=caption,summarize{Style.RESET_ALL}")158 159# Models init160cuda_device = DEFAULT_CUDA_DEVICE if not args.cuda_device else args.cuda_device161device_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'162device = torch.device(device_string)163torch_dtype = torch.float32 if device_string != cuda_device  else torch.float16164 165if not torch.cuda.is_available() and not args.cpu:166    print(f"{Fore.YELLOW}{Style.BRIGHT}torch-cuda is not supported on this device.{Style.RESET_ALL}")167    if not torch.backends.mps.is_available() and not args.cpu:168        print(f"{Fore.YELLOW}{Style.BRIGHT}torch-mps is not supported on this device.{Style.RESET_ALL}")169 170 171print(f"{Fore.GREEN}{Style.BRIGHT}Using torch device: {device_string}{Style.RESET_ALL}")172 173if "caption" in modules:174    print("Initializing an image captioning model...")175    captioning_processor = AutoProcessor.from_pretrained(captioning_model)176    if "blip" in captioning_model:177        captioning_transformer = BlipForConditionalGeneration.from_pretrained(178            captioning_model, torch_dtype=torch_dtype179        ).to(device)180    else:181        captioning_transformer = AutoModelForCausalLM.from_pretrained(182            captioning_model, torch_dtype=torch_dtype183        ).to(device)184 185if "summarize" in modules:186    print("Initializing a text summarization model...")187    summarization_tokenizer = AutoTokenizer.from_pretrained(summarization_model)188    summarization_transformer = AutoModelForSeq2SeqLM.from_pretrained(189        summarization_model, torch_dtype=torch_dtype190    ).to(device)191 192if "classify" in modules:193    print("Initializing a sentiment classification pipeline...")194    classification_pipe = pipeline(195        "text-classification",196        model=classification_model,197        top_k=None,198        device=device,199        torch_dtype=torch_dtype,200    )201 202if "sd" in modules and not sd_use_remote:203    from diffusers import StableDiffusionPipeline204    from diffusers import EulerAncestralDiscreteScheduler205 206    print("Initializing Stable Diffusion pipeline...")207    sd_device_string = cuda_device if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'208    sd_device = torch.device(sd_device_string)209    sd_torch_dtype = torch.float32 if sd_device_string != cuda_device else torch.float16210    sd_pipe = StableDiffusionPipeline.from_pretrained(211        sd_model, custom_pipeline="lpw_stable_diffusion", torch_dtype=sd_torch_dtype212    ).to(sd_device)213    sd_pipe.safety_checker = lambda images, clip_input: (images, False)214    sd_pipe.enable_attention_slicing()215    # pipe.scheduler = KarrasVeScheduler.from_config(pipe.scheduler.config)216    sd_pipe.scheduler = EulerAncestralDiscreteScheduler.from_config(217        sd_pipe.scheduler.config218    )219elif "sd" in modules and sd_use_remote:220    print("Initializing Stable Diffusion connection")221    try:222        sd_remote = webuiapi.WebUIApi(223            host=sd_remote_host, port=sd_remote_port, use_https=sd_remote_ssl224        )225        if sd_remote_auth:226            username, password = sd_remote_auth.split(":")227            sd_remote.set_auth(username, password)228        sd_remote.util_wait_for_ready()229    except Exception as e:230        # remote sd from modules231        print(232            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}"233        )234        modules.remove("sd")235 236if "tts" in modules:237    print("tts module is deprecated. Please use silero-tts instead.")238    modules.remove("tts")239    modules.append("silero-tts")240 241 242if "silero-tts" in modules:243    if not os.path.exists(SILERO_SAMPLES_PATH):244        os.makedirs(SILERO_SAMPLES_PATH)245    print("Initializing Silero TTS server")246    from silero_api_server import tts247 248    tts_service = tts.SileroTtsService(SILERO_SAMPLES_PATH)249    if len(os.listdir(SILERO_SAMPLES_PATH)) == 0:250        print("Generating Silero TTS samples...")251        tts_service.update_sample_text(SILERO_SAMPLE_TEXT)252        tts_service.generate_samples()253 254 255if "edge-tts" in modules:256    print("Initializing Edge TTS client")257    import tts_edge as edge258 259 260if "chromadb" in modules:261    print("Initializing ChromaDB")262    import chromadb263    import posthog264    from chromadb.config import Settings265    from sentence_transformers import SentenceTransformer266 267    # Assume that the user wants in-memory unless a host is specified268    # Also disable chromadb telemetry269    posthog.capture = lambda *args, **kwargs: None270    if args.chroma_host is None:271        if args.chroma_persist:272            chromadb_client = chromadb.PersistentClient(path=args.chroma_folder, settings=Settings(anonymized_telemetry=False))273            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.")274        else:275            chromadb_client = chromadb.EphemeralClient(Settings(anonymized_telemetry=False))276            print(f"ChromaDB is running in-memory without persistence.")277    else:278        chroma_port=(279            args.chroma_port if args.chroma_port else DEFAULT_CHROMA_PORT280        )281        chromadb_client = chromadb.HttpClient(host=args.chroma_host, port=chroma_port, settings=Settings(anonymized_telemetry=False))282        print(f"ChromaDB is remotely configured at {args.chroma_host}:{chroma_port}")283 284    chromadb_embedder = SentenceTransformer(embedding_model, device=device_string)285    chromadb_embed_fn = lambda *args, **kwargs: chromadb_embedder.encode(*args, **kwargs).tolist()286 287    # Check if the db is connected and running, otherwise tell the user288    try:289        chromadb_client.heartbeat()290        print("Successfully pinged ChromaDB! Your client is successfully connected.")291    except:292        print("Could not ping ChromaDB! If you are running remotely, please check your host and port!")293 294# Flask init295app = Flask(__name__)296CORS(app)  # allow cross-domain requests297Compress(app) # compress responses298app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 * 1024299 300 301def require_module(name):302    def wrapper(fn):303        @wraps(fn)304        def decorated_view(*args, **kwargs):305            if name not in modules:306                abort(403, "Module is disabled by config")307            return fn(*args, **kwargs)308 309        return decorated_view310 311    return wrapper312 313 314# AI stuff315def classify_text(text: str) -> list:316    output = classification_pipe(317        text,318        truncation=True,319        max_length=classification_pipe.model.config.max_position_embeddings,320    )[0]321    return sorted(output, key=lambda x: x["score"], reverse=True)322 323 324def caption_image(raw_image: Image, max_new_tokens: int = 20) -> str:325    inputs = captioning_processor(raw_image.convert("RGB"), return_tensors="pt").to(326        device, torch_dtype327    )328    outputs = captioning_transformer.generate(**inputs, max_new_tokens=max_new_tokens)329    caption = captioning_processor.decode(outputs[0], skip_special_tokens=True)330    return caption331 332 333def summarize_chunks(text: str, params: dict) -> str:334    try:335        return summarize(text, params)336    except IndexError:337        print(338            "Sequence length too large for model, cutting text in half and calling again"339        )340        new_params = params.copy()341        new_params["max_length"] = new_params["max_length"] // 2342        new_params["min_length"] = new_params["min_length"] // 2343        return summarize_chunks(344            text[: (len(text) // 2)], new_params345        ) + summarize_chunks(text[(len(text) // 2) :], new_params)346 347 348def summarize(text: str, params: dict) -> str:349    # Tokenize input350    inputs = summarization_tokenizer(text, return_tensors="pt").to(device)351    token_count = len(inputs[0])352 353    bad_words_ids = [354        summarization_tokenizer(bad_word, add_special_tokens=False).input_ids355        for bad_word in params["bad_words"]356    ]357    summary_ids = summarization_transformer.generate(358        inputs["input_ids"],359        num_beams=2,360        max_new_tokens=max(token_count, int(params["max_length"])),361        min_new_tokens=min(token_count, int(params["min_length"])),362        repetition_penalty=float(params["repetition_penalty"]),363        temperature=float(params["temperature"]),364        length_penalty=float(params["length_penalty"]),365        bad_words_ids=bad_words_ids,366    )367    summary = summarization_tokenizer.batch_decode(368        summary_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True369    )[0]370    summary = normalize_string(summary)371    return summary372 373 374def normalize_string(input: str) -> str:375    output = " ".join(unicodedata.normalize("NFKC", input).strip().split())376    return output377 378 379def generate_image(data: dict) -> Image:380    prompt = normalize_string(f'{data["prompt_prefix"]} {data["prompt"]}')381 382    if sd_use_remote:383        image = sd_remote.txt2img(384            prompt=prompt,385            negative_prompt=data["negative_prompt"],386            sampler_name=data["sampler"],387            steps=data["steps"],388            cfg_scale=data["scale"],389            width=data["width"],390            height=data["height"],391            restore_faces=data["restore_faces"],392            enable_hr=data["enable_hr"],393            save_images=True,394            send_images=True,395            do_not_save_grid=False,396            do_not_save_samples=False,397        ).image398    else:399        image = sd_pipe(400            prompt=prompt,401            negative_prompt=data["negative_prompt"],402            num_inference_steps=data["steps"],403            guidance_scale=data["scale"],404            width=data["width"],405            height=data["height"],406        ).images[0]407 408    image.save("./debug.png")409    return image410 411 412def image_to_base64(image: Image, quality: int = 75) -> str:413    buffer = BytesIO()414    image.convert("RGB")415    image.save(buffer, format="JPEG", quality=quality)416    img_str = base64.b64encode(buffer.getvalue()).decode("utf-8")417    return img_str418 419 420ignore_auth = []    421# [HF, Huggingface] Get password instead of text file.422api_key = os.environ.get("password")423 424def is_authorize_ignored(request):425    view_func = app.view_functions.get(request.endpoint)426 427    if view_func is not None:428        if view_func in ignore_auth:429            return True430    return False431 432@app.before_request433def before_request():434    # Request time measuring435    request.start_time = time.time()436 437    # Checks if an API key is present and valid, otherwise return unauthorized438    # The options check is required so CORS doesn't get angry439    try:440        if request.method != 'OPTIONS' and is_authorize_ignored(request) == False and getattr(request.authorization, 'token', '') != api_key:441            print(f"WARNING: Unauthorized API key access from {request.remote_addr}")442            if request.method == 'POST':443                print(f"Incoming POST request with {request.headers.get('Authorization')}")444            response = jsonify({ 'error': '401: Invalid API key' })445            response.status_code = 401446            return "https://(hf_name)-(space_name).hf.space/"447    except Exception as e:448        print(f"API key check error: {e}")449        return "https://(hf_name)-(space_name).hf.space/"450 451 452@app.after_request453def after_request(response):454    duration = time.time() - request.start_time455    response.headers["X-Request-Duration"] = str(duration)456    return response457 458 459@app.route("/", methods=["GET"])460def index():461    with open("./README.md", "r", encoding="utf8") as f:462        content = f.read()463    return render_template_string(markdown.markdown(content, extensions=["tables"]))464 465 466@app.route("/api/extensions", methods=["GET"])467def get_extensions():468    extensions = dict(469        {470            "extensions": [471                {472                    "name": "not-supported",473                    "metadata": {474                        "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>""",475                        "requires": [],476                        "assets": [],477                    },478                }479            ]480        }481    )482    return jsonify(extensions)483 484 485@app.route("/api/caption", methods=["POST"])486@require_module("caption")487def api_caption():488    data = request.get_json()489 490    if "image" not in data or not isinstance(data["image"], str):491        abort(400, '"image" is required')492 493    image = Image.open(BytesIO(base64.b64decode(data["image"])))494    image = image.convert("RGB")495    image.thumbnail((512, 512))496    caption = caption_image(image)497    thumbnail = image_to_base64(image)498    print("Caption:", caption, sep="\n")499    gc.collect()500    return jsonify({"caption": caption, "thumbnail": thumbnail})501 502 503@app.route("/api/summarize", methods=["POST"])504@require_module("summarize")505def api_summarize():506    data = request.get_json()507 508    if "text" not in data or not isinstance(data["text"], str):509        abort(400, '"text" is required')510 511    params = DEFAULT_SUMMARIZE_PARAMS.copy()512 513    if "params" in data and isinstance(data["params"], dict):514        params.update(data["params"])515 516    print("Summary input:", data["text"], sep="\n")517    summary = summarize_chunks(data["text"], params)518    print("Summary output:", summary, sep="\n")519    gc.collect()520    return jsonify({"summary": summary})521 522 523@app.route("/api/classify", methods=["POST"])524@require_module("classify")525def api_classify():526    data = request.get_json()527 528    if "text" not in data or not isinstance(data["text"], str):529        abort(400, '"text" is required')530 531    print("Classification input:", data["text"], sep="\n")532    classification = classify_text(data["text"])533    print("Classification output:", classification, sep="\n")534    gc.collect()535    return jsonify({"classification": classification})536 537 538@app.route("/api/classify/labels", methods=["GET"])539@require_module("classify")540def api_classify_labels():541    classification = classify_text("")542    labels = [x["label"] for x in classification]543    return jsonify({"labels": labels})544 545 546@app.route("/api/image", methods=["POST"])547@require_module("sd")548def api_image():549    required_fields = {550        "prompt": str,551    }552 553    optional_fields = {554        "steps": 30,555        "scale": 6,556        "sampler": "DDIM",557        "width": 512,558        "height": 512,559        "restore_faces": False,560        "enable_hr": False,561        "prompt_prefix": PROMPT_PREFIX,562        "negative_prompt": NEGATIVE_PROMPT,563    }564 565    data = request.get_json()566 567    # Check required fields568    for field, field_type in required_fields.items():569        if field not in data or not isinstance(data[field], field_type):570            abort(400, f'"{field}" is required')571 572    # Set optional fields to default values if not provided573    for field, default_value in optional_fields.items():574        type_match = (575            (int, float)576            if isinstance(default_value, (int, float))577            else type(default_value)578        )579        if field not in data or not isinstance(data[field], type_match):580            data[field] = default_value581 582    try:583        print("SD inputs:", data, sep="\n")584        image = generate_image(data)585        base64image = image_to_base64(image, quality=90)586        return jsonify({"image": base64image})587    except RuntimeError as e:588        abort(400, str(e))589 590 591@app.route("/api/image/model", methods=["POST"])592@require_module("sd")593def api_image_model_set():594    data = request.get_json()595 596    if not sd_use_remote:597        abort(400, "Changing model for local sd is not supported.")598    if "model" not in data or not isinstance(data["model"], str):599        abort(400, '"model" is required')600 601    old_model = sd_remote.util_get_current_model()602    sd_remote.util_set_model(data["model"], find_closest=False)603    # sd_remote.util_set_model(data['model'])604    sd_remote.util_wait_for_ready()605    new_model = sd_remote.util_get_current_model()606 607    return jsonify({"previous_model": old_model, "current_model": new_model})608 609 610@app.route("/api/image/model", methods=["GET"])611@require_module("sd")612def api_image_model_get():613    model = sd_model614 615    if sd_use_remote:616        model = sd_remote.util_get_current_model()617 618    return jsonify({"model": model})619 620 621@app.route("/api/image/models", methods=["GET"])622@require_module("sd")623def api_image_models():624    models = [sd_model]625 626    if sd_use_remote:627        models = sd_remote.util_get_model_names()628 629    return jsonify({"models": models})630 631 632@app.route("/api/image/samplers", methods=["GET"])633@require_module("sd")634def api_image_samplers():635    samplers = ["Euler a"]636 637    if sd_use_remote:638        samplers = [sampler["name"] for sampler in sd_remote.get_samplers()]639 640    return jsonify({"samplers": samplers})641 642 643@app.route("/api/modules", methods=["GET"])644def get_modules():645    return jsonify({"modules": modules})646 647 648@app.route("/api/tts/speakers", methods=["GET"])649@require_module("silero-tts")650def tts_speakers():651    voices = [652        {653            "name": speaker,654            "voice_id": speaker,655            "preview_url": f"{str(request.url_root)}api/tts/sample/{speaker}",656        }657        for speaker in tts_service.get_speakers()658    ]659    return jsonify(voices)660 661# Added fix for Silero not working as new files were unable to be created if one already existed. - Rolyat 7/7/23662@app.route("/api/tts/generate", methods=["POST"])663@require_module("silero-tts")664def tts_generate():665    voice = request.get_json()666    if "text" not in voice or not isinstance(voice["text"], str):667        abort(400, '"text" is required')668    if "speaker" not in voice or not isinstance(voice["speaker"], str):669        abort(400, '"speaker" is required')670    # Remove asterisks671    voice["text"] = voice["text"].replace("*", "")672    try:673        # Remove the destination file if it already exists674        if os.path.exists('test.wav'):675            os.remove('test.wav')676 677        audio = tts_service.generate(voice["speaker"], voice["text"])678        audio_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.basename(audio))679 680        os.rename(audio, audio_file_path)681        return send_file(audio_file_path, mimetype="audio/x-wav")682    except Exception as e:683        print(e)684        abort(500, voice["speaker"])685 686 687@app.route("/api/tts/sample/<speaker>", methods=["GET"])688@require_module("silero-tts")689def tts_play_sample(speaker: str):690    return send_from_directory(SILERO_SAMPLES_PATH, f"{speaker}.wav")691 692 693@app.route("/api/edge-tts/list", methods=["GET"])694@require_module("edge-tts")695def edge_tts_list():696    voices = edge.get_voices()697    return jsonify(voices)698 699 700@app.route("/api/edge-tts/generate", methods=["POST"])701@require_module("edge-tts")702def edge_tts_generate():703    data = request.get_json()704    if "text" not in data or not isinstance(data["text"], str):705        abort(400, '"text" is required')706    if "voice" not in data or not isinstance(data["voice"], str):707        abort(400, '"voice" is required')708    if "rate" in data and isinstance(data['rate'], int):709        rate = data['rate']710    else:711        rate = 0712    # Remove asterisks713    data["text"] = data["text"].replace("*", "")714    try:715        audio = edge.generate_audio(text=data["text"], voice=data["voice"], rate=rate)716        return Response(audio, mimetype="audio/mpeg")717    except Exception as e:718        print(e)719        abort(500, data["voice"])720 721 722@app.route("/api/chromadb", methods=["POST"])723@require_module("chromadb")724def chromadb_add_messages():725    data = request.get_json()726    if "chat_id" not in data or not isinstance(data["chat_id"], str):727        abort(400, '"chat_id" is required')728    if "messages" not in data or not isinstance(data["messages"], list):729        abort(400, '"messages" is required')730 731    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()732    collection = chromadb_client.get_or_create_collection(733        name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn734    )735 736    documents = [m["content"] for m in data["messages"]]737    ids = [m["id"] for m in data["messages"]]738    metadatas = [739        {"role": m["role"], "date": m["date"], "meta": m.get("meta", "")}740        for m in data["messages"]741    ]742 743    collection.upsert(744        ids=ids,745        documents=documents,746        metadatas=metadatas,747    )748 749    return jsonify({"count": len(ids)})750 751 752@app.route("/api/chromadb/purge", methods=["POST"])753@require_module("chromadb")754def chromadb_purge():755    data = request.get_json()756    if "chat_id" not in data or not isinstance(data["chat_id"], str):757        abort(400, '"chat_id" is required')758 759    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()760    collection = chromadb_client.get_or_create_collection(761        name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn762    )763 764    count = collection.count()765    collection.delete()766    print("ChromaDB embeddings deleted", count)767    return 'Ok', 200768 769 770@app.route("/api/chromadb/query", methods=["POST"])771@require_module("chromadb")772def chromadb_query():773    data = request.get_json()774    if "chat_id" not in data or not isinstance(data["chat_id"], str):775        abort(400, '"chat_id" is required')776    if "query" not in data or not isinstance(data["query"], str):777        abort(400, '"query" is required')778 779    if "n_results" not in data or not isinstance(data["n_results"], int):780        n_results = 1781    else:782        n_results = data["n_results"]783 784    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()785    collection = chromadb_client.get_or_create_collection(786        name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn787    )788 789    if collection.count() == 0:790        print(f"Queried empty/missing collection for {repr(data['chat_id'])}.")791        return jsonify([])792 793 794    n_results = min(collection.count(), n_results)795    query_result = collection.query(796        query_texts=[data["query"]],797        n_results=n_results,798    )799 800    documents = query_result["documents"][0]801    ids = query_result["ids"][0]802    metadatas = query_result["metadatas"][0]803    distances = query_result["distances"][0]804 805    messages = [806        {807            "id": ids[i],808            "date": metadatas[i]["date"],809            "role": metadatas[i]["role"],810            "meta": metadatas[i]["meta"],811            "content": documents[i],812            "distance": distances[i],813        }814        for i in range(len(ids))815    ]816 817    return jsonify(messages)818 819@app.route("/api/chromadb/multiquery", methods=["POST"])820@require_module("chromadb")821def chromadb_multiquery():822    data = request.get_json()823    if "chat_list" not in data or not isinstance(data["chat_list"], list):824        abort(400, '"chat_list" is required and should be a list')825    if "query" not in data or not isinstance(data["query"], str):826        abort(400, '"query" is required')827 828    if "n_results" not in data or not isinstance(data["n_results"], int):829        n_results = 1830    else:831        n_results = data["n_results"]832 833    messages = []834 835    for chat_id in data["chat_list"]:836        if not isinstance(chat_id, str):837            continue838 839        try:840            chat_id_md5 = hashlib.md5(chat_id.encode()).hexdigest()841            collection = chromadb_client.get_collection(842                name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn843            )844 845            # Skip this chat if the collection is empty846            if collection.count() == 0:847                continue848 849            n_results_per_chat = min(collection.count(), n_results)850            query_result = collection.query(851                query_texts=[data["query"]],852                n_results=n_results_per_chat,853            )854            documents = query_result["documents"][0]855            ids = query_result["ids"][0]856            metadatas = query_result["metadatas"][0]857            distances = query_result["distances"][0]858 859            chat_messages = [860                {861                    "id": ids[i],862                    "date": metadatas[i]["date"],863                    "role": metadatas[i]["role"],864                    "meta": metadatas[i]["meta"],865                    "content": documents[i],866                    "distance": distances[i],867                }868                for i in range(len(ids))869            ]870 871            messages.extend(chat_messages)872        except Exception as e:873            print(e)874 875    #remove duplicate msgs, filter down to the right number876    seen = set()877    messages = [d for d in messages if not (d['content'] in seen or seen.add(d['content']))]878    messages = sorted(messages, key=lambda x: x['distance'])[0:n_results]879 880    return jsonify(messages)881 882 883@app.route("/api/chromadb/export", methods=["POST"])884@require_module("chromadb")885def chromadb_export():886    data = request.get_json()887    if "chat_id" not in data or not isinstance(data["chat_id"], str):888        abort(400, '"chat_id" is required')889 890    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()891    try:892        collection = chromadb_client.get_collection(893            name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn894        )895    except Exception as e:896        print(e)897        abort(400, "Chat collection not found in chromadb")898 899    collection_content = collection.get()900    documents = collection_content.get('documents', [])901    ids = collection_content.get('ids', [])902    metadatas = collection_content.get('metadatas', [])903 904    unsorted_content = [905        {906            "id": ids[i],907            "metadata": metadatas[i],908            "document": documents[i],909        }910        for i in range(len(ids))911    ]912 913    sorted_content = sorted(unsorted_content, key=lambda x: x['metadata']['date'])914 915    export = {916        "chat_id": data["chat_id"],917        "content": sorted_content918    }919 920    return jsonify(export)921 922@app.route("/api/chromadb/import", methods=["POST"])923@require_module("chromadb")924def chromadb_import():925    data = request.get_json()926    content = data['content']927    if "chat_id" not in data or not isinstance(data["chat_id"], str):928        abort(400, '"chat_id" is required')929 930    chat_id_md5 = hashlib.md5(data["chat_id"].encode()).hexdigest()931    collection = chromadb_client.get_or_create_collection(932        name=f"chat-{chat_id_md5}", embedding_function=chromadb_embed_fn933    )934 935    documents = [item['document'] for item in content]936    metadatas = [item['metadata'] for item in content]937    ids = [item['id'] for item in content]938 939 940    collection.upsert(documents=documents, metadatas=metadatas, ids=ids)941    print(f"Imported {len(ids)} (total {collection.count()}) content entries into {repr(data['chat_id'])}")942 943    return jsonify({"count": len(ids)})944 945 946if args.share:947    from flask_cloudflared import _run_cloudflared948    import inspect949 950    sig = inspect.signature(_run_cloudflared)951    sum = sum(952        1953        for param in sig.parameters.values()954        if param.kind == param.POSITIONAL_OR_KEYWORD955    )956    if sum > 1:957        metrics_port = randint(8100, 9000)958        cloudflare = _run_cloudflared(port, metrics_port)959    else:960        cloudflare = _run_cloudflared(port)961    print("Running on", cloudflare)962 963ignore_auth.append(tts_play_sample)964app.run(host=host, port=port)965