Browse Source

Merge remote-tracking branch 'upstream/dev' into playwright

# Conflicts:
#	backend/requirements.txt
Rory 2 months ago
parent
commit
2c711d8365
38 changed files with 919 additions and 570 deletions
  1. 1 1
      README.md
  2. 8 1
      backend/open_webui/config.py
  3. 1 0
      backend/open_webui/env.py
  4. 3 0
      backend/open_webui/main.py
  5. 1 1
      backend/open_webui/models/chats.py
  6. 34 0
      backend/open_webui/retrieval/vector/dbs/opensearch.py
  7. 16 4
      backend/open_webui/retrieval/web/jina_search.py
  8. 64 0
      backend/open_webui/routers/audio.py
  9. 76 47
      backend/open_webui/routers/ollama.py
  10. 6 6
      backend/open_webui/routers/openai.py
  11. 6 0
      backend/open_webui/routers/retrieval.py
  12. 20 8
      backend/open_webui/storage/provider.py
  13. 35 109
      backend/open_webui/utils/chat.py
  14. 99 0
      backend/open_webui/utils/filter.py
  15. 1 1
      backend/open_webui/utils/images/comfyui.py
  16. 42 99
      backend/open_webui/utils/middleware.py
  17. 5 4
      backend/open_webui/utils/misc.py
  18. 37 11
      backend/open_webui/utils/oauth.py
  19. 4 2
      backend/requirements.txt
  20. 186 120
      package-lock.json
  21. 4 2
      package.json
  22. 4 1
      pyproject.toml
  23. 3 1
      src/lib/components/admin/Evaluations/Leaderboard.svelte
  24. 36 1
      src/lib/components/admin/Settings/Audio.svelte
  25. 16 4
      src/lib/components/admin/Settings/Connections.svelte
  26. 28 0
      src/lib/components/admin/Settings/WebSearch.svelte
  27. 1 1
      src/lib/components/chat/MessageInput.svelte
  28. 19 11
      src/lib/components/chat/Messages/Citations.svelte
  29. 5 0
      src/lib/components/chat/Settings/General.svelte
  30. 9 3
      src/lib/components/common/Collapsible.svelte
  31. 1 0
      src/lib/components/common/SensitiveInput.svelte
  32. 101 101
      src/lib/i18n/locales/de-DE/translation.json
  33. 1 0
      src/lib/i18n/locales/en-US/translation.json
  34. 11 10
      src/lib/i18n/locales/zh-CN/translation.json
  35. 19 19
      src/lib/i18n/locales/zh-TW/translation.json
  36. 1 0
      src/lib/stores/index.ts
  37. 1 1
      src/routes/(app)/admin/+layout.svelte
  38. 14 1
      vite.config.ts

+ 1 - 1
README.md

@@ -174,7 +174,7 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa
 
 In the last part of the command, replace `open-webui` with your container name if it is different.
 
-Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/tutorials/migration/).
+Check our Updating Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/updating).
 
 ### Using the Dev Branch 🌙
 

+ 8 - 1
backend/open_webui/config.py

@@ -660,6 +660,7 @@ S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None)
 S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None)
 S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None)
 S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
+S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None)
 S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
 
 GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None)
@@ -1645,7 +1646,7 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
 # This ensures the highest level of safety and reliability of the information sources.
 RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
     "RAG_WEB_SEARCH_DOMAIN_FILTER_LIST",
-    "rag.rag.web.search.domain.filter_list",
+    "rag.web.search.domain.filter_list",
     [
         # "wikipedia.com",
         # "wikimedia.org",
@@ -2023,6 +2024,12 @@ WHISPER_MODEL_AUTO_UPDATE = (
     and os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
 )
 
+# Add Deepgram configuration
+DEEPGRAM_API_KEY = PersistentConfig(
+    "DEEPGRAM_API_KEY",
+    "audio.stt.deepgram.api_key",
+    os.getenv("DEEPGRAM_API_KEY", ""),
+)
 
 AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
     "AUDIO_STT_OPENAI_API_BASE_URL",

+ 1 - 0
backend/open_webui/env.py

@@ -92,6 +92,7 @@ log_sources = [
     "RAG",
     "WEBHOOK",
     "SOCKET",
+    "OAUTH",
 ]
 
 SRC_LOG_LEVELS = {}

+ 3 - 0
backend/open_webui/main.py

@@ -132,6 +132,7 @@ from open_webui.config import (
     PLAYWRIGHT_WS_URI,
     RAG_WEB_LOADER,
     WHISPER_MODEL,
+    DEEPGRAM_API_KEY,
     WHISPER_MODEL_AUTO_UPDATE,
     WHISPER_MODEL_DIR,
     # Retrieval
@@ -615,6 +616,7 @@ app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
 app.state.config.STT_MODEL = AUDIO_STT_MODEL
 
 app.state.config.WHISPER_MODEL = WHISPER_MODEL
+app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY
 
 app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
 app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
@@ -1021,6 +1023,7 @@ async def get_app_config(request: Request):
                     "enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
                     "enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING,
                     "enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING,
+                    "enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
                     "enable_admin_export": ENABLE_ADMIN_EXPORT,
                     "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
                 }

+ 1 - 1
backend/open_webui/models/chats.py

@@ -470,7 +470,7 @@ class ChatTable:
         try:
             with get_db() as db:
                 # it is possible that the shared link was deleted. hence,
-                # we check if the chat is still shared by checkng if a chat with the share_id exists
+                # we check if the chat is still shared by checking if a chat with the share_id exists
                 chat = db.query(Chat).filter_by(share_id=id).first()
 
                 if chat:

+ 34 - 0
backend/open_webui/retrieval/vector/dbs/opensearch.py

@@ -113,6 +113,40 @@ class OpenSearchClient:
 
         return self._result_to_search_result(result)
 
+    def query(
+        self, collection_name: str, filter: dict, limit: Optional[int] = None
+    ) -> Optional[GetResult]:
+        if not self.has_collection(collection_name):
+            return None
+
+        query_body = {
+            "query": {
+                "bool": {
+                    "filter": []
+                }
+            },
+            "_source": ["text", "metadata"],
+        }
+
+        for field, value in filter.items():
+            query_body["query"]["bool"]["filter"].append({
+                "term": {field: value}
+            })
+
+        size = limit if limit else 10
+
+        try:
+            result = self.client.search(
+                index=f"{self.index_prefix}_{collection_name}",
+                body=query_body,
+                size=size
+            )
+
+            return self._result_to_get_result(result)
+
+        except Exception as e:
+            return None
+
     def get_or_create_index(self, index_name: str, dimension: int):
         if not self.has_index(index_name):
             self._create_index(index_name, dimension)

+ 16 - 4
backend/open_webui/retrieval/web/jina_search.py

@@ -20,14 +20,26 @@ def search_jina(api_key: str, query: str, count: int) -> list[SearchResult]:
         list[SearchResult]: A list of search results
     """
     jina_search_endpoint = "https://s.jina.ai/"
-    headers = {"Accept": "application/json", "Authorization": f"Bearer {api_key}"}
-    url = str(URL(jina_search_endpoint + query))
-    response = requests.get(url, headers=headers)
+
+    headers = {
+        "Accept": "application/json",
+        "Content-Type": "application/json",
+        "Authorization": api_key,
+        "X-Retain-Images": "none"
+    }
+
+    payload = {
+        "q": query,
+        "count": count if count <= 10 else 10
+    }
+
+    url = str(URL(jina_search_endpoint))
+    response = requests.post(url, headers=headers, json=payload)
     response.raise_for_status()
     data = response.json()
 
     results = []
-    for result in data["data"][:count]:
+    for result in data["data"]:
         results.append(
             SearchResult(
                 link=result["url"],

+ 64 - 0
backend/open_webui/routers/audio.py

@@ -11,6 +11,7 @@ from pydub.silence import split_on_silence
 import aiohttp
 import aiofiles
 import requests
+import mimetypes
 
 from fastapi import (
     Depends,
@@ -138,6 +139,7 @@ class STTConfigForm(BaseModel):
     ENGINE: str
     MODEL: str
     WHISPER_MODEL: str
+    DEEPGRAM_API_KEY: str
 
 
 class AudioConfigUpdateForm(BaseModel):
@@ -165,6 +167,7 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)):
             "ENGINE": request.app.state.config.STT_ENGINE,
             "MODEL": request.app.state.config.STT_MODEL,
             "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
+            "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY,
         },
     }
 
@@ -190,6 +193,7 @@ async def update_audio_config(
     request.app.state.config.STT_ENGINE = form_data.stt.ENGINE
     request.app.state.config.STT_MODEL = form_data.stt.MODEL
     request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL
+    request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY
 
     if request.app.state.config.STT_ENGINE == "":
         request.app.state.faster_whisper_model = set_faster_whisper_model(
@@ -214,6 +218,7 @@ async def update_audio_config(
             "ENGINE": request.app.state.config.STT_ENGINE,
             "MODEL": request.app.state.config.STT_MODEL,
             "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL,
+            "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY,
         },
     }
 
@@ -521,6 +526,65 @@ def transcribe(request: Request, file_path):
 
             raise Exception(detail if detail else "Open WebUI: Server Connection Error")
 
+    elif request.app.state.config.STT_ENGINE == "deepgram":
+        try:
+            # Determine the MIME type of the file
+            mime, _ = mimetypes.guess_type(file_path)
+            if not mime:
+                mime = "audio/wav"  # fallback to wav if undetectable
+
+            # Read the audio file
+            with open(file_path, "rb") as f:
+                file_data = f.read()
+
+            # Build headers and parameters
+            headers = {
+                "Authorization": f"Token {request.app.state.config.DEEPGRAM_API_KEY}",
+                "Content-Type": mime,
+            }
+
+            # Add model if specified
+            params = {}
+            if request.app.state.config.STT_MODEL:
+                params["model"] = request.app.state.config.STT_MODEL
+
+            # Make request to Deepgram API
+            r = requests.post(
+                "https://api.deepgram.com/v1/listen",
+                headers=headers,
+                params=params,
+                data=file_data,
+            )
+            r.raise_for_status()
+            response_data = r.json()
+
+            # Extract transcript from Deepgram response
+            try:
+                transcript = response_data["results"]["channels"][0]["alternatives"][0].get("transcript", "")
+            except (KeyError, IndexError) as e:
+                log.error(f"Malformed response from Deepgram: {str(e)}")
+                raise Exception("Failed to parse Deepgram response - unexpected response format")
+            data = {"text": transcript.strip()}
+
+            # Save transcript
+            transcript_file = f"{file_dir}/{id}.json"
+            with open(transcript_file, "w") as f:
+                json.dump(data, f)
+
+            return data
+
+        except Exception as e:
+            log.exception(e)
+            detail = None
+            if r is not None:
+                try:
+                    res = r.json()
+                    if "error" in res:
+                        detail = f"External: {res['error'].get('message', '')}"
+                except Exception:
+                    detail = f"External: {e}"
+            raise Exception(detail if detail else "Open WebUI: Server Connection Error")
+
 
 def compress_audio(file_path):
     if os.path.getsize(file_path) > MAX_FILE_SIZE:

+ 76 - 47
backend/open_webui/routers/ollama.py

@@ -11,10 +11,8 @@ import re
 import time
 from typing import Optional, Union
 from urllib.parse import urlparse
-
 import aiohttp
 from aiocache import cached
-
 import requests
 
 from fastapi import (
@@ -990,6 +988,8 @@ async def generate_chat_completion(
         )
 
     payload = {**form_data.model_dump(exclude_none=True)}
+    if "metadata" in payload:
+        del payload["metadata"]
 
     model_id = payload["model"]
     model_info = Models.get_model_by_id(model_id)
@@ -1408,9 +1408,10 @@ async def download_model(
         return None
 
 
+# TODO: Progress bar does not reflect size & duration of upload.
 @router.post("/models/upload")
 @router.post("/models/upload/{url_idx}")
-def upload_model(
+async def upload_model(
     request: Request,
     file: UploadFile = File(...),
     url_idx: Optional[int] = None,
@@ -1419,62 +1420,90 @@ def upload_model(
     if url_idx is None:
         url_idx = 0
     ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
-
-    file_path = f"{UPLOAD_DIR}/{file.filename}"
-
-    # Save file in chunks
-    with open(file_path, "wb+") as f:
-        for chunk in file.file:
-            f.write(chunk)
-
-    def file_process_stream():
+    file_path = os.path.join(UPLOAD_DIR, file.filename)
+    os.makedirs(UPLOAD_DIR, exist_ok=True)
+
+    # --- P1: save file locally ---
+    chunk_size = 1024 * 1024 * 2 # 2 MB chunks
+    with open(file_path, "wb") as out_f:
+        while True:
+            chunk = file.file.read(chunk_size)
+            #log.info(f"Chunk: {str(chunk)}") # DEBUG
+            if not chunk:
+                break
+            out_f.write(chunk)
+
+    async def file_process_stream():
         nonlocal ollama_url
         total_size = os.path.getsize(file_path)
-        chunk_size = 1024 * 1024
+        log.info(f"Total Model Size: {str(total_size)}") # DEBUG
+
+        # --- P2: SSE progress + calculate sha256 hash ---
+        file_hash = calculate_sha256(file_path, chunk_size)
+        log.info(f"Model Hash: {str(file_hash)}") # DEBUG
         try:
             with open(file_path, "rb") as f:
-                total = 0
-                done = False
-
-                while not done:
-                    chunk = f.read(chunk_size)
-                    if not chunk:
-                        done = True
-                        continue
-
-                    total += len(chunk)
-                    progress = round((total / total_size) * 100, 2)
-
-                    res = {
+                bytes_read = 0
+                while chunk := f.read(chunk_size): 
+                    bytes_read += len(chunk)
+                    progress = round(bytes_read / total_size * 100, 2)
+                    data_msg = {
                         "progress": progress,
                         "total": total_size,
-                        "completed": total,
+                        "completed": bytes_read,
                     }
-                    yield f"data: {json.dumps(res)}\n\n"
+                    yield f"data: {json.dumps(data_msg)}\n\n"
 
-                if done:
-                    f.seek(0)
-                    hashed = calculate_sha256(f)
-                    f.seek(0)
+            # --- P3: Upload to ollama /api/blobs ---
+            with open(file_path, "rb") as f:
+                url = f"{ollama_url}/api/blobs/sha256:{file_hash}"
+                response = requests.post(url, data=f)
+
+            if response.ok:
+                log.info(f"Uploaded to /api/blobs") # DEBUG
+                # Remove local file
+                os.remove(file_path)
+
+                # Create model in ollama
+                model_name, ext = os.path.splitext(file.filename)
+                log.info(f"Created Model: {model_name}") # DEBUG
+
+                create_payload = {
+                    "model": model_name,
+                    # Reference the file by its original name => the uploaded blob's digest
+                    "files": {
+                        file.filename: f"sha256:{file_hash}"
+                    },
+                }
+                log.info(f"Model Payload: {create_payload}") # DEBUG
+
+                # Call ollama /api/create
+                #https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model
+                create_resp = requests.post(
+                    url=f"{ollama_url}/api/create",
+                    headers={"Content-Type": "application/json"},
+                    data=json.dumps(create_payload),
+                )
 
-                    url = f"{ollama_url}/api/blobs/sha256:{hashed}"
-                    response = requests.post(url, data=f)
+                if create_resp.ok:
+                    log.info(f"API SUCCESS!") # DEBUG
+                    done_msg = {
+                        "done": True,
+                        "blob": f"sha256:{file_hash}",
+                        "name": file.filename,
+                        "model_created": model_name,
+                    }
+                    yield f"data: {json.dumps(done_msg)}\n\n"
+                else:
+                    raise Exception(
+                        f"Failed to create model in Ollama. {create_resp.text}"
+                    )
 
-                    if response.ok:
-                        res = {
-                            "done": done,
-                            "blob": f"sha256:{hashed}",
-                            "name": file.filename,
-                        }
-                        os.remove(file_path)
-                        yield f"data: {json.dumps(res)}\n\n"
-                    else:
-                        raise Exception(
-                            "Ollama: Could not create blob, Please try again."
-                        )
+            else:
+                raise Exception("Ollama: Could not create blob, Please try again.")
 
         except Exception as e:
             res = {"error": str(e)}
             yield f"data: {json.dumps(res)}\n\n"
 
-    return StreamingResponse(file_process_stream(), media_type="text/event-stream")
+    return StreamingResponse(file_process_stream(), media_type="text/event-stream")

+ 6 - 6
backend/open_webui/routers/openai.py

@@ -75,9 +75,9 @@ async def cleanup_response(
         await session.close()
 
 
-def openai_o1_handler(payload):
+def openai_o1_o3_handler(payload):
     """
-    Handle O1 specific parameters
+    Handle o1, o3 specific parameters
     """
     if "max_tokens" in payload:
         # Remove "max_tokens" from the payload
@@ -621,10 +621,10 @@ async def generate_chat_completion(
     url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
     key = request.app.state.config.OPENAI_API_KEYS[idx]
 
-    # Fix: O1 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens"
-    is_o1 = payload["model"].lower().startswith("o1-")
-    if is_o1:
-        payload = openai_o1_handler(payload)
+    # Fix: o1,o3 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens"
+    is_o1_o3 = payload["model"].lower().startswith(("o1", "o3-"))
+    if is_o1_o3:
+        payload = openai_o1_o3_handler(payload)
     elif "api.openai.com" not in url:
         # Remove "max_completion_tokens" from the payload for backward compatibility
         if "max_completion_tokens" in payload:

+ 6 - 0
backend/open_webui/routers/retrieval.py

@@ -392,6 +392,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
                 "exa_api_key": request.app.state.config.EXA_API_KEY,
                 "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
                 "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
+                "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
             },
         },
     }
@@ -441,6 +442,7 @@ class WebSearchConfig(BaseModel):
     exa_api_key: Optional[str] = None
     result_count: Optional[int] = None
     concurrent_requests: Optional[int] = None
+    domain_filter_list: Optional[List[str]] = []
 
 
 class WebConfig(BaseModel):
@@ -553,6 +555,9 @@ async def update_rag_config(
         request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
             form_data.web.search.concurrent_requests
         )
+        request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = (
+            form_data.web.search.domain_filter_list
+        )
 
     return {
         "status": True,
@@ -599,6 +604,7 @@ async def update_rag_config(
                 "exa_api_key": request.app.state.config.EXA_API_KEY,
                 "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
                 "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
+                "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
             },
         },
     }

+ 20 - 8
backend/open_webui/storage/provider.py

@@ -10,6 +10,7 @@ from open_webui.config import (
     S3_ACCESS_KEY_ID,
     S3_BUCKET_NAME,
     S3_ENDPOINT_URL,
+    S3_KEY_PREFIX,
     S3_REGION_NAME,
     S3_SECRET_ACCESS_KEY,
     GCS_BUCKET_NAME,
@@ -93,34 +94,36 @@ class S3StorageProvider(StorageProvider):
             aws_secret_access_key=S3_SECRET_ACCESS_KEY,
         )
         self.bucket_name = S3_BUCKET_NAME
+        self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else "" 
 
     def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]:
         """Handles uploading of the file to S3 storage."""
         _, file_path = LocalStorageProvider.upload_file(file, filename)
         try:
-            self.s3_client.upload_file(file_path, self.bucket_name, filename)
+            s3_key = os.path.join(self.key_prefix, filename)
+            self.s3_client.upload_file(file_path, self.bucket_name, s3_key)
             return (
                 open(file_path, "rb").read(),
-                "s3://" + self.bucket_name + "/" + filename,
+                "s3://" + self.bucket_name + "/" + s3_key,
             )
         except ClientError as e:
             raise RuntimeError(f"Error uploading file to S3: {e}")
-
+    
     def get_file(self, file_path: str) -> str:
         """Handles downloading of the file from S3 storage."""
         try:
-            bucket_name, key = file_path.split("//")[1].split("/")
-            local_file_path = f"{UPLOAD_DIR}/{key}"
-            self.s3_client.download_file(bucket_name, key, local_file_path)
+            s3_key = self._extract_s3_key(file_path)
+            local_file_path = self._get_local_file_path(s3_key)
+            self.s3_client.download_file(self.bucket_name, s3_key, local_file_path)
             return local_file_path
         except ClientError as e:
             raise RuntimeError(f"Error downloading file from S3: {e}")
 
     def delete_file(self, file_path: str) -> None:
         """Handles deletion of the file from S3 storage."""
-        filename = file_path.split("/")[-1]
         try:
-            self.s3_client.delete_object(Bucket=self.bucket_name, Key=filename)
+            s3_key = self._extract_s3_key(file_path)
+            self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key)
         except ClientError as e:
             raise RuntimeError(f"Error deleting file from S3: {e}")
 
@@ -133,6 +136,9 @@ class S3StorageProvider(StorageProvider):
             response = self.s3_client.list_objects_v2(Bucket=self.bucket_name)
             if "Contents" in response:
                 for content in response["Contents"]:
+                    # Skip objects that were not uploaded from open-webui in the first place
+                    if not content["Key"].startswith(self.key_prefix): continue
+
                     self.s3_client.delete_object(
                         Bucket=self.bucket_name, Key=content["Key"]
                     )
@@ -142,6 +148,12 @@ class S3StorageProvider(StorageProvider):
         # Always delete from local storage
         LocalStorageProvider.delete_all_files()
 
+    # The s3 key is the name assigned to an object. It excludes the bucket name, but includes the internal path and the file name.
+    def _extract_s3_key(self, full_file_path: str) -> str:
+        return '/'.join(full_file_path.split("//")[1].split("/")[1:])
+    
+    def _get_local_file_path(self, s3_key: str) -> str:
+        return f"{UPLOAD_DIR}/{s3_key.split('/')[-1]}"
 
 class GCSStorageProvider(StorageProvider):
     def __init__(self):

+ 35 - 109
backend/open_webui/utils/chat.py

@@ -44,6 +44,10 @@ from open_webui.utils.response import (
     convert_response_ollama_to_openai,
     convert_streaming_response_ollama_to_openai,
 )
+from open_webui.utils.filter import (
+    get_sorted_filter_ids,
+    process_filter_functions,
+)
 
 from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL
 
@@ -177,116 +181,38 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
     except Exception as e:
         return Exception(f"Error: {e}")
 
-    __event_emitter__ = get_event_emitter(
-        {
-            "chat_id": data["chat_id"],
-            "message_id": data["id"],
-            "session_id": data["session_id"],
-            "user_id": user.id,
-        }
-    )
-
-    __event_call__ = get_event_call(
-        {
-            "chat_id": data["chat_id"],
-            "message_id": data["id"],
-            "session_id": data["session_id"],
-            "user_id": user.id,
-        }
-    )
-
-    def get_priority(function_id):
-        function = Functions.get_function_by_id(function_id)
-        if function is not None and hasattr(function, "valves"):
-            # TODO: Fix FunctionModel to include vavles
-            return (function.valves if function.valves else {}).get("priority", 0)
-        return 0
-
-    filter_ids = [function.id for function in Functions.get_global_filter_functions()]
-    if "info" in model and "meta" in model["info"]:
-        filter_ids.extend(model["info"]["meta"].get("filterIds", []))
-        filter_ids = list(set(filter_ids))
-
-    enabled_filter_ids = [
-        function.id
-        for function in Functions.get_functions_by_type("filter", active_only=True)
-    ]
-    filter_ids = [
-        filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids
-    ]
-
-    # Sort filter_ids by priority, using the get_priority function
-    filter_ids.sort(key=get_priority)
-
-    for filter_id in filter_ids:
-        filter = Functions.get_function_by_id(filter_id)
-        if not filter:
-            continue
-
-        if filter_id in request.app.state.FUNCTIONS:
-            function_module = request.app.state.FUNCTIONS[filter_id]
-        else:
-            function_module, _, _ = load_function_module_by_id(filter_id)
-            request.app.state.FUNCTIONS[filter_id] = function_module
-
-        if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
-            valves = Functions.get_function_valves_by_id(filter_id)
-            function_module.valves = function_module.Valves(
-                **(valves if valves else {})
-            )
-
-        if not hasattr(function_module, "outlet"):
-            continue
-        try:
-            outlet = function_module.outlet
-
-            # Get the signature of the function
-            sig = inspect.signature(outlet)
-            params = {"body": data}
-
-            # Extra parameters to be passed to the function
-            extra_params = {
-                "__model__": model,
-                "__id__": filter_id,
-                "__event_emitter__": __event_emitter__,
-                "__event_call__": __event_call__,
-                "__request__": request,
-            }
+    metadata = {
+        "chat_id": data["chat_id"],
+        "message_id": data["id"],
+        "session_id": data["session_id"],
+        "user_id": user.id,
+    }
+
+    extra_params = {
+        "__event_emitter__": get_event_emitter(metadata),
+        "__event_call__": get_event_call(metadata),
+        "__user__": {
+            "id": user.id,
+            "email": user.email,
+            "name": user.name,
+            "role": user.role,
+        },
+        "__metadata__": metadata,
+        "__request__": request,
+        "__model__": model,
+    }
 
-            # Add extra params in contained in function signature
-            for key, value in extra_params.items():
-                if key in sig.parameters:
-                    params[key] = value
-
-            if "__user__" in sig.parameters:
-                __user__ = {
-                    "id": user.id,
-                    "email": user.email,
-                    "name": user.name,
-                    "role": user.role,
-                }
-
-                try:
-                    if hasattr(function_module, "UserValves"):
-                        __user__["valves"] = function_module.UserValves(
-                            **Functions.get_user_valves_by_id_and_user_id(
-                                filter_id, user.id
-                            )
-                        )
-                except Exception as e:
-                    print(e)
-
-                params = {**params, "__user__": __user__}
-
-            if inspect.iscoroutinefunction(outlet):
-                data = await outlet(**params)
-            else:
-                data = outlet(**params)
-
-        except Exception as e:
-            return Exception(f"Error: {e}")
-
-    return data
+    try:
+        result, _ = await process_filter_functions(
+            request=request,
+            filter_ids=get_sorted_filter_ids(model),
+            filter_type="outlet",
+            form_data=data,
+            extra_params=extra_params,
+        )
+        return result
+    except Exception as e:
+        return Exception(f"Error: {e}")
 
 
 async def chat_action(request: Request, action_id: str, form_data: dict, user: Any):

+ 99 - 0
backend/open_webui/utils/filter.py

@@ -0,0 +1,99 @@
+import inspect
+from open_webui.utils.plugin import load_function_module_by_id
+from open_webui.models.functions import Functions
+
+
+def get_sorted_filter_ids(model):
+    def get_priority(function_id):
+        function = Functions.get_function_by_id(function_id)
+        if function is not None and hasattr(function, "valves"):
+            # TODO: Fix FunctionModel to include vavles
+            return (function.valves if function.valves else {}).get("priority", 0)
+        return 0
+
+    filter_ids = [function.id for function in Functions.get_global_filter_functions()]
+    if "info" in model and "meta" in model["info"]:
+        filter_ids.extend(model["info"]["meta"].get("filterIds", []))
+        filter_ids = list(set(filter_ids))
+
+    enabled_filter_ids = [
+        function.id
+        for function in Functions.get_functions_by_type("filter", active_only=True)
+    ]
+
+    filter_ids = [fid for fid in filter_ids if fid in enabled_filter_ids]
+    filter_ids.sort(key=get_priority)
+    return filter_ids
+
+
+async def process_filter_functions(
+    request, filter_ids, filter_type, form_data, extra_params
+):
+    skip_files = None
+
+    for filter_id in filter_ids:
+        filter = Functions.get_function_by_id(filter_id)
+        if not filter:
+            continue
+
+        if filter_id in request.app.state.FUNCTIONS:
+            function_module = request.app.state.FUNCTIONS[filter_id]
+        else:
+            function_module, _, _ = load_function_module_by_id(filter_id)
+            request.app.state.FUNCTIONS[filter_id] = function_module
+
+        # Check if the function has a file_handler variable
+        if filter_type == "inlet" and hasattr(function_module, "file_handler"):
+            skip_files = function_module.file_handler
+
+        # Apply valves to the function
+        if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
+            valves = Functions.get_function_valves_by_id(filter_id)
+            function_module.valves = function_module.Valves(
+                **(valves if valves else {})
+            )
+
+        # Prepare handler function
+        handler = getattr(function_module, filter_type, None)
+        if not handler:
+            continue
+
+        try:
+            # Prepare parameters
+            sig = inspect.signature(handler)
+            params = {"body": form_data} | {
+                k: v
+                for k, v in {
+                    **extra_params,
+                    "__id__": filter_id,
+                }.items()
+                if k in sig.parameters
+            }
+
+            # Handle user parameters
+            if "__user__" in sig.parameters:
+                if hasattr(function_module, "UserValves"):
+                    try:
+                        params["__user__"]["valves"] = function_module.UserValves(
+                            **Functions.get_user_valves_by_id_and_user_id(
+                                filter_id, params["__user__"]["id"]
+                            )
+                        )
+                    except Exception as e:
+                        print(e)
+
+            # Execute handler
+            if inspect.iscoroutinefunction(handler):
+                form_data = await handler(**params)
+            else:
+                form_data = handler(**params)
+
+        except Exception as e:
+            print(f"Error in {filter_type} handler {filter_id}: {e}")
+            raise e
+
+    # Handle file cleanup for inlet
+    if skip_files and "files" in form_data.get("metadata", {}):
+        del form_data["metadata"]["files"]
+
+    return form_data, {}

+ 1 - 1
backend/open_webui/utils/images/comfyui.py

@@ -161,7 +161,7 @@ async def comfyui_generate_image(
                 seed = (
                     payload.seed
                     if payload.seed
-                    else random.randint(0, 18446744073709551614)
+                    else random.randint(0, 1125899906842624)
                 )
                 for node_id in node.node_ids:
                     workflow[node_id]["inputs"][node.key] = seed

+ 42 - 99
backend/open_webui/utils/middleware.py

@@ -68,6 +68,10 @@ from open_webui.utils.misc import (
 )
 from open_webui.utils.tools import get_tools
 from open_webui.utils.plugin import load_function_module_by_id
+from open_webui.utils.filter import (
+    get_sorted_filter_ids,
+    process_filter_functions,
+)
 
 
 from open_webui.tasks import create_task
@@ -91,99 +95,6 @@ log = logging.getLogger(__name__)
 log.setLevel(SRC_LOG_LEVELS["MAIN"])
 
 
-async def chat_completion_filter_functions_handler(request, body, model, extra_params):
-    skip_files = None
-
-    def get_filter_function_ids(model):
-        def get_priority(function_id):
-            function = Functions.get_function_by_id(function_id)
-            if function is not None and hasattr(function, "valves"):
-                # TODO: Fix FunctionModel
-                return (function.valves if function.valves else {}).get("priority", 0)
-            return 0
-
-        filter_ids = [
-            function.id for function in Functions.get_global_filter_functions()
-        ]
-        if "info" in model and "meta" in model["info"]:
-            filter_ids.extend(model["info"]["meta"].get("filterIds", []))
-            filter_ids = list(set(filter_ids))
-
-        enabled_filter_ids = [
-            function.id
-            for function in Functions.get_functions_by_type("filter", active_only=True)
-        ]
-
-        filter_ids = [
-            filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids
-        ]
-
-        filter_ids.sort(key=get_priority)
-        return filter_ids
-
-    filter_ids = get_filter_function_ids(model)
-    for filter_id in filter_ids:
-        filter = Functions.get_function_by_id(filter_id)
-        if not filter:
-            continue
-
-        if filter_id in request.app.state.FUNCTIONS:
-            function_module = request.app.state.FUNCTIONS[filter_id]
-        else:
-            function_module, _, _ = load_function_module_by_id(filter_id)
-            request.app.state.FUNCTIONS[filter_id] = function_module
-
-        # Check if the function has a file_handler variable
-        if hasattr(function_module, "file_handler"):
-            skip_files = function_module.file_handler
-
-        # Apply valves to the function
-        if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
-            valves = Functions.get_function_valves_by_id(filter_id)
-            function_module.valves = function_module.Valves(
-                **(valves if valves else {})
-            )
-
-        if hasattr(function_module, "inlet"):
-            try:
-                inlet = function_module.inlet
-
-                # Create a dictionary of parameters to be passed to the function
-                params = {"body": body} | {
-                    k: v
-                    for k, v in {
-                        **extra_params,
-                        "__model__": model,
-                        "__id__": filter_id,
-                    }.items()
-                    if k in inspect.signature(inlet).parameters
-                }
-
-                if "__user__" in params and hasattr(function_module, "UserValves"):
-                    try:
-                        params["__user__"]["valves"] = function_module.UserValves(
-                            **Functions.get_user_valves_by_id_and_user_id(
-                                filter_id, params["__user__"]["id"]
-                            )
-                        )
-                    except Exception as e:
-                        print(e)
-
-                if inspect.iscoroutinefunction(inlet):
-                    body = await inlet(**params)
-                else:
-                    body = inlet(**params)
-
-            except Exception as e:
-                print(f"Error: {e}")
-                raise e
-
-    if skip_files and "files" in body.get("metadata", {}):
-        del body["metadata"]["files"]
-
-    return body, {}
-
-
 async def chat_completion_tools_handler(
     request: Request, body: dict, user: UserModel, models, tools
 ) -> tuple[dict, dict]:
@@ -566,13 +477,13 @@ async def chat_image_generation_handler(
             {
                 "type": "status",
                 "data": {
-                    "description": f"An error occured while generating an image",
+                    "description": f"An error occurred while generating an image",
                     "done": True,
                 },
             }
         )
 
-        system_message_content = "<context>Unable to generate an image, tell the user that an error occured</context>"
+        system_message_content = "<context>Unable to generate an image, tell the user that an error occurred</context>"
 
     if system_message_content:
         form_data["messages"] = add_or_update_system_message(
@@ -700,6 +611,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
         },
         "__metadata__": metadata,
         "__request__": request,
+        "__model__": model,
     }
 
     # Initialize events to store additional event to be sent to the client
@@ -776,8 +688,12 @@ async def process_chat_payload(request, form_data, metadata, user, model):
             )
 
     try:
-        form_data, flags = await chat_completion_filter_functions_handler(
-            request, form_data, model, extra_params
+        form_data, flags = await process_filter_functions(
+            request=request,
+            filter_ids=get_sorted_filter_ids(model),
+            filter_type="inlet",
+            form_data=form_data,
+            extra_params=extra_params,
         )
     except Exception as e:
         raise Exception(f"Error: {e}")
@@ -1116,6 +1032,20 @@ async def process_chat_response(
             },
         )
 
+        def split_content_and_whitespace(content):
+            content_stripped = content.rstrip()
+            original_whitespace = (
+                content[len(content_stripped) :]
+                if len(content) > len(content_stripped)
+                else ""
+            )
+            return content_stripped, original_whitespace
+
+        def is_opening_code_block(content):
+            backtick_segments = content.split("```")
+            # Even number of segments means the last backticks are opening a new block
+            return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0
+
         # Handle as a background task
         async def post_response_handler(response, events):
             def serialize_content_blocks(content_blocks, raw=False):
@@ -1182,6 +1112,19 @@ async def process_chat_response(
                         output = block.get("output", None)
                         lang = attributes.get("lang", "")
 
+                        content_stripped, original_whitespace = (
+                            split_content_and_whitespace(content)
+                        )
+                        if is_opening_code_block(content_stripped):
+                            # Remove trailing backticks that would open a new block
+                            content = (
+                                content_stripped.rstrip("`").rstrip()
+                                + original_whitespace
+                            )
+                        else:
+                            # Keep content as is - either closing backticks or no backticks
+                            content = content_stripped + original_whitespace
+
                         if output:
                             output = html.escape(json.dumps(output))
 
@@ -1236,10 +1179,10 @@ async def process_chat_response(
                                 match.end() :
                             ]  # Content after opening tag
 
-                            # Remove the start tag from the currently handling text block
+                            # Remove the start tag and after from the currently handling text block
                             content_blocks[-1]["content"] = content_blocks[-1][
                                 "content"
-                            ].replace(match.group(0), "")
+                            ].replace(match.group(0) + after_tag, "")
 
                             if before_tag:
                                 content_blocks[-1]["content"] = before_tag

+ 5 - 4
backend/open_webui/utils/misc.py

@@ -244,11 +244,12 @@ def get_gravatar_url(email):
     return f"https://www.gravatar.com/avatar/{hash_hex}?d=mp"
 
 
-def calculate_sha256(file):
+def calculate_sha256(file_path, chunk_size):
+    #Compute SHA-256 hash of a file efficiently in chunks
     sha256 = hashlib.sha256()
-    # Read the file in chunks to efficiently handle large files
-    for chunk in iter(lambda: file.read(8192), b""):
-        sha256.update(chunk)
+    with open(file_path, "rb") as f:
+        while chunk := f.read(chunk_size):
+            sha256.update(chunk)
     return sha256.hexdigest()
 
 

+ 37 - 11
backend/open_webui/utils/oauth.py

@@ -1,6 +1,7 @@
 import base64
 import logging
 import mimetypes
+import sys
 import uuid
 
 import aiohttp
@@ -40,7 +41,11 @@ from open_webui.utils.misc import parse_duration
 from open_webui.utils.auth import get_password_hash, create_token
 from open_webui.utils.webhook import post_webhook
 
+from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
+
+logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
 log = logging.getLogger(__name__)
+log.setLevel(SRC_LOG_LEVELS["OAUTH"])
 
 auth_manager_config = AppConfig()
 auth_manager_config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
@@ -72,12 +77,15 @@ class OAuthManager:
     def get_user_role(self, user, user_data):
         if user and Users.get_num_users() == 1:
             # If the user is the only user, assign the role "admin" - actually repairs role for single user on login
+            log.debug("Assigning the only user the admin role")
             return "admin"
         if not user and Users.get_num_users() == 0:
             # If there are no users, assign the role "admin", as the first user will be an admin
+            log.debug("Assigning the first user the admin role")
             return "admin"
 
         if auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT:
+            log.debug("Running OAUTH Role management")
             oauth_claim = auth_manager_config.OAUTH_ROLES_CLAIM
             oauth_allowed_roles = auth_manager_config.OAUTH_ALLOWED_ROLES
             oauth_admin_roles = auth_manager_config.OAUTH_ADMIN_ROLES
@@ -93,17 +101,24 @@ class OAuthManager:
                     claim_data = claim_data.get(nested_claim, {})
                 oauth_roles = claim_data if isinstance(claim_data, list) else None
 
+            log.debug(f"Oauth Roles claim: {oauth_claim}")
+            log.debug(f"User roles from oauth: {oauth_roles}")
+            log.debug(f"Accepted user roles: {oauth_allowed_roles}")
+            log.debug(f"Accepted admin roles: {oauth_admin_roles}")
+
             # If any roles are found, check if they match the allowed or admin roles
             if oauth_roles:
                 # If role management is enabled, and matching roles are provided, use the roles
                 for allowed_role in oauth_allowed_roles:
                     # If the user has any of the allowed roles, assign the role "user"
                     if allowed_role in oauth_roles:
+                        log.debug("Assigned user the user role")
                         role = "user"
                         break
                 for admin_role in oauth_admin_roles:
                     # If the user has any of the admin roles, assign the role "admin"
                     if admin_role in oauth_roles:
+                        log.debug("Assigned user the admin role")
                         role = "admin"
                         break
         else:
@@ -117,16 +132,23 @@ class OAuthManager:
         return role
 
     def update_user_groups(self, user, user_data, default_permissions):
+        log.debug("Running OAUTH Group management")
         oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM
 
         user_oauth_groups: list[str] = user_data.get(oauth_claim, list())
         user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
         all_available_groups: list[GroupModel] = Groups.get_groups()
 
+        log.debug(f"Oauth Groups claim: {oauth_claim}")
+        log.debug(f"User oauth groups: {user_oauth_groups}")
+        log.debug(f"User's current groups: {[g.name for g in user_current_groups]}")
+        log.debug(f"All groups available in OpenWebUI: {[g.name for g in all_available_groups]}")
+
         # Remove groups that user is no longer a part of
         for group_model in user_current_groups:
             if group_model.name not in user_oauth_groups:
                 # Remove group from user
+                log.debug(f"Removing user from group {group_model.name} as it is no longer in their oauth groups")
 
                 user_ids = group_model.user_ids
                 user_ids = [i for i in user_ids if i != user.id]
@@ -152,6 +174,7 @@ class OAuthManager:
                 gm.name == group_model.name for gm in user_current_groups
             ):
                 # Add user to group
+                log.debug(f"Adding user to group {group_model.name} as it was found in their oauth groups")
 
                 user_ids = group_model.user_ids
                 user_ids.append(user.id)
@@ -193,7 +216,7 @@ class OAuthManager:
             log.warning(f"OAuth callback error: {e}")
             raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
         user_data: UserInfo = token.get("userinfo")
-        if not user_data:
+        if not user_data or "email" not in user_data:
             user_data: UserInfo = await client.userinfo(token=token)
         if not user_data:
             log.warning(f"OAuth callback failed, user data is missing: {token}")
@@ -261,15 +284,18 @@ class OAuthManager:
                             }
                         async with aiohttp.ClientSession() as session:
                             async with session.get(picture_url, **get_kwargs) as resp:
-                                picture = await resp.read()
-                                base64_encoded_picture = base64.b64encode(
-                                    picture
-                                ).decode("utf-8")
-                                guessed_mime_type = mimetypes.guess_type(picture_url)[0]
-                                if guessed_mime_type is None:
-                                    # assume JPG, browsers are tolerant enough of image formats
-                                    guessed_mime_type = "image/jpeg"
-                                picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}"
+                                if resp.ok:
+                                    picture = await resp.read()
+                                    base64_encoded_picture = base64.b64encode(
+                                        picture
+                                    ).decode("utf-8")
+                                    guessed_mime_type = mimetypes.guess_type(picture_url)[0]
+                                    if guessed_mime_type is None:
+                                        # assume JPG, browsers are tolerant enough of image formats
+                                        guessed_mime_type = "image/jpeg"
+                                    picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}"
+                                else:
+                                    picture_url = "/user.png"
                     except Exception as e:
                         log.error(
                             f"Error downloading profile image '{picture_url}': {e}"
@@ -281,7 +307,7 @@ class OAuthManager:
                 username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
 
                 name = user_data.get(username_claim)
-                if not isinstance(user, str):
+                if not isinstance(name, str):
                     name = email
 
                 role = self.get_user_role(None, user_data)

+ 4 - 2
backend/requirements.txt

@@ -32,6 +32,8 @@ boto3==1.35.53
 argon2-cffi==23.1.0
 APScheduler==3.10.4
 
+RestrictedPython==8.0
+
 # AI libraries
 openai
 anthropic
@@ -45,7 +47,7 @@ fake-useragent==1.5.1
 chromadb==0.6.2
 pymilvus==2.5.0
 qdrant-client~=1.12.0
-opensearch-py==2.7.1
+opensearch-py==2.8.0
 playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
 
 transformers
@@ -77,7 +79,7 @@ opencv-python-headless==4.11.0.86
 rapidocr-onnxruntime==1.3.24
 rank-bm25==0.2.2
 
-faster-whisper==1.0.3
+faster-whisper==1.1.1
 
 PyJWT[crypto]==2.10.1
 authlib==1.4.1

+ 186 - 120
package-lock.json

@@ -42,6 +42,7 @@
 				"idb": "^7.1.1",
 				"js-sha256": "^0.10.1",
 				"katex": "^0.16.21",
+				"kokoro-js": "^1.1.1",
 				"marked": "^9.1.0",
 				"mermaid": "^10.9.3",
 				"paneforge": "^0.0.6",
@@ -62,7 +63,8 @@
 				"svelte-sonner": "^0.3.19",
 				"tippy.js": "^6.3.7",
 				"turndown": "^7.2.0",
-				"uuid": "^9.0.1"
+				"uuid": "^9.0.1",
+				"vite-plugin-static-copy": "^2.2.0"
 			},
 			"devDependencies": {
 				"@sveltejs/adapter-auto": "3.2.2",
@@ -91,7 +93,7 @@
 				"tslib": "^2.4.1",
 				"typescript": "^5.5.4",
 				"vite": "^5.4.14",
-				"vitest": "^1.6.0"
+				"vitest": "^1.6.1"
 			},
 			"engines": {
 				"node": ">=18.13.0 <=22.x.x",
@@ -1078,21 +1080,23 @@
 			}
 		},
 		"node_modules/@huggingface/jinja": {
-			"version": "0.3.1",
-			"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.1.tgz",
-			"integrity": "sha512-SbcBWUKDQ76lzlVYOloscUk0SJjuL1LcbZsfQv/Bxxc7dwJMYuS+DAQ+HhVw6ZkTFXArejaX5HQRuCuleYwYdA==",
+			"version": "0.3.3",
+			"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.3.tgz",
+			"integrity": "sha512-vQQr2JyWvVFba3Lj9es4q9vCl1sAc74fdgnEMoX8qHrXtswap9ge9uO3ONDzQB0cQ0PUyaKY2N6HaVbTBvSXvw==",
+			"license": "MIT",
 			"engines": {
 				"node": ">=18"
 			}
 		},
 		"node_modules/@huggingface/transformers": {
-			"version": "3.0.0",
-			"resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.0.0.tgz",
-			"integrity": "sha512-OWIPnTijAw4DQ+IFHBOrej2SDdYyykYlTtpTLCEt5MZq/e9Cb65RS2YVhdGcgbaW/6JAL3i8ZA5UhDeWGm4iRQ==",
+			"version": "3.3.3",
+			"resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.3.3.tgz",
+			"integrity": "sha512-OcMubhBjW6u1xnp0zSt5SvCxdGHuhP2k+w2Vlm3i0vNcTJhJTZWxxYQmPBfcb7PX+Q6c43lGSzWD6tsJFwka4Q==",
+			"license": "Apache-2.0",
 			"dependencies": {
-				"@huggingface/jinja": "^0.3.0",
-				"onnxruntime-node": "1.19.2",
-				"onnxruntime-web": "1.20.0-dev.20241016-2b8fc5529b",
+				"@huggingface/jinja": "^0.3.3",
+				"onnxruntime-node": "1.20.1",
+				"onnxruntime-web": "1.21.0-dev.20250206-d981b153d3",
 				"sharp": "^0.33.5"
 			}
 		},
@@ -1546,6 +1550,7 @@
 			"version": "4.0.1",
 			"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
 			"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+			"license": "ISC",
 			"dependencies": {
 				"minipass": "^7.0.4"
 			},
@@ -1558,6 +1563,7 @@
 			"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
 			"integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"@sinclair/typebox": "^0.27.8"
 			},
@@ -1798,7 +1804,6 @@
 			"version": "2.1.5",
 			"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 			"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-			"dev": true,
 			"dependencies": {
 				"@nodelib/fs.stat": "2.0.5",
 				"run-parallel": "^1.1.9"
@@ -1811,7 +1816,6 @@
 			"version": "2.0.5",
 			"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
 			"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
-			"dev": true,
 			"engines": {
 				"node": ">= 8"
 			}
@@ -1820,7 +1824,6 @@
 			"version": "1.2.8",
 			"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
 			"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-			"dev": true,
 			"dependencies": {
 				"@nodelib/fs.scandir": "2.1.5",
 				"fastq": "^1.6.0"
@@ -1856,27 +1859,32 @@
 		"node_modules/@protobufjs/aspromise": {
 			"version": "1.1.2",
 			"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
-			"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+			"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+			"license": "BSD-3-Clause"
 		},
 		"node_modules/@protobufjs/base64": {
 			"version": "1.1.2",
 			"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
-			"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+			"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+			"license": "BSD-3-Clause"
 		},
 		"node_modules/@protobufjs/codegen": {
 			"version": "2.0.4",
 			"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
-			"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+			"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+			"license": "BSD-3-Clause"
 		},
 		"node_modules/@protobufjs/eventemitter": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
-			"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+			"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+			"license": "BSD-3-Clause"
 		},
 		"node_modules/@protobufjs/fetch": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
 			"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+			"license": "BSD-3-Clause",
 			"dependencies": {
 				"@protobufjs/aspromise": "^1.1.1",
 				"@protobufjs/inquire": "^1.1.0"
@@ -1885,27 +1893,32 @@
 		"node_modules/@protobufjs/float": {
 			"version": "1.0.2",
 			"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
-			"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+			"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+			"license": "BSD-3-Clause"
 		},
 		"node_modules/@protobufjs/inquire": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
-			"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
+			"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+			"license": "BSD-3-Clause"
 		},
 		"node_modules/@protobufjs/path": {
 			"version": "1.1.2",
 			"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
-			"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+			"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+			"license": "BSD-3-Clause"
 		},
 		"node_modules/@protobufjs/pool": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
-			"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+			"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+			"license": "BSD-3-Clause"
 		},
 		"node_modules/@protobufjs/utf8": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
-			"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
+			"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+			"license": "BSD-3-Clause"
 		},
 		"node_modules/@pyscript/core": {
 			"version": "0.4.32",
@@ -2210,7 +2223,8 @@
 			"version": "0.27.8",
 			"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
 			"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
-			"dev": true
+			"dev": true,
+			"license": "MIT"
 		},
 		"node_modules/@socket.io/component-emitter": {
 			"version": "3.1.2",
@@ -3146,13 +3160,14 @@
 			"integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw=="
 		},
 		"node_modules/@vitest/expect": {
-			"version": "1.6.0",
-			"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
-			"integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
+			"version": "1.6.1",
+			"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
+			"integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
-				"@vitest/spy": "1.6.0",
-				"@vitest/utils": "1.6.0",
+				"@vitest/spy": "1.6.1",
+				"@vitest/utils": "1.6.1",
 				"chai": "^4.3.10"
 			},
 			"funding": {
@@ -3160,12 +3175,13 @@
 			}
 		},
 		"node_modules/@vitest/runner": {
-			"version": "1.6.0",
-			"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz",
-			"integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
+			"version": "1.6.1",
+			"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz",
+			"integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
-				"@vitest/utils": "1.6.0",
+				"@vitest/utils": "1.6.1",
 				"p-limit": "^5.0.0",
 				"pathe": "^1.1.1"
 			},
@@ -3178,6 +3194,7 @@
 			"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
 			"integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"yocto-queue": "^1.0.0"
 			},
@@ -3189,10 +3206,11 @@
 			}
 		},
 		"node_modules/@vitest/runner/node_modules/yocto-queue": {
-			"version": "1.0.0",
-			"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
-			"integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==",
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz",
+			"integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": ">=12.20"
 			},
@@ -3201,10 +3219,11 @@
 			}
 		},
 		"node_modules/@vitest/snapshot": {
-			"version": "1.6.0",
-			"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz",
-			"integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
+			"version": "1.6.1",
+			"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz",
+			"integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"magic-string": "^0.30.5",
 				"pathe": "^1.1.1",
@@ -3215,10 +3234,11 @@
 			}
 		},
 		"node_modules/@vitest/spy": {
-			"version": "1.6.0",
-			"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz",
-			"integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
+			"version": "1.6.1",
+			"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz",
+			"integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"tinyspy": "^2.2.0"
 			},
@@ -3227,10 +3247,11 @@
 			}
 		},
 		"node_modules/@vitest/utils": {
-			"version": "1.6.0",
-			"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz",
-			"integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
+			"version": "1.6.1",
+			"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz",
+			"integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"diff-sequences": "^29.6.3",
 				"estree-walker": "^3.0.3",
@@ -3246,6 +3267,7 @@
 			"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
 			"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"@types/estree": "^1.0.0"
 			}
@@ -3416,7 +3438,6 @@
 			"version": "3.1.3",
 			"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
 			"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
-			"dev": true,
 			"dependencies": {
 				"normalize-path": "^3.0.0",
 				"picomatch": "^2.0.4"
@@ -3496,6 +3517,7 @@
 			"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
 			"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": "*"
 			}
@@ -3644,7 +3666,6 @@
 			"version": "2.3.0",
 			"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
 			"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
-			"dev": true,
 			"engines": {
 				"node": ">=8"
 			},
@@ -3720,7 +3741,6 @@
 			"version": "3.0.3",
 			"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
 			"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
-			"dev": true,
 			"dependencies": {
 				"fill-range": "^7.1.1"
 			},
@@ -3895,6 +3915,7 @@
 			"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
 			"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": ">=8"
 			}
@@ -3972,10 +3993,11 @@
 			"dev": true
 		},
 		"node_modules/chai": {
-			"version": "4.4.1",
-			"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
-			"integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==",
+			"version": "4.5.0",
+			"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
+			"integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"assertion-error": "^1.1.0",
 				"check-error": "^1.0.3",
@@ -3983,7 +4005,7 @@
 				"get-func-name": "^2.0.2",
 				"loupe": "^2.3.6",
 				"pathval": "^1.1.1",
-				"type-detect": "^4.0.8"
+				"type-detect": "^4.1.0"
 			},
 			"engines": {
 				"node": ">=4"
@@ -4019,6 +4041,7 @@
 			"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
 			"integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"get-func-name": "^2.0.2"
 			},
@@ -4077,7 +4100,6 @@
 			"version": "3.6.0",
 			"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
 			"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
-			"dev": true,
 			"dependencies": {
 				"anymatch": "~3.1.2",
 				"braces": "~3.0.2",
@@ -4101,7 +4123,6 @@
 			"version": "5.1.2",
 			"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
 			"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-			"dev": true,
 			"dependencies": {
 				"is-glob": "^4.0.1"
 			},
@@ -4113,6 +4134,7 @@
 			"version": "3.0.0",
 			"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
 			"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+			"license": "BlueOak-1.0.0",
 			"engines": {
 				"node": ">=18"
 			}
@@ -5135,10 +5157,11 @@
 			}
 		},
 		"node_modules/deep-eql": {
-			"version": "4.1.3",
-			"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
-			"integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
+			"version": "4.1.4",
+			"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
+			"integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"type-detect": "^4.0.0"
 			},
@@ -5257,6 +5280,7 @@
 			"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
 			"integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
 			}
@@ -5899,7 +5923,6 @@
 			"version": "3.3.2",
 			"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
 			"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
-			"dev": true,
 			"dependencies": {
 				"@nodelib/fs.stat": "^2.0.2",
 				"@nodelib/fs.walk": "^1.2.3",
@@ -5915,7 +5938,6 @@
 			"version": "5.1.2",
 			"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
 			"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-			"dev": true,
 			"dependencies": {
 				"is-glob": "^4.0.1"
 			},
@@ -5939,7 +5961,6 @@
 			"version": "1.17.1",
 			"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
 			"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
-			"dev": true,
 			"dependencies": {
 				"reusify": "^1.0.4"
 			}
@@ -5998,7 +6019,6 @@
 			"version": "7.1.1",
 			"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
 			"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
-			"dev": true,
 			"dependencies": {
 				"to-regex-range": "^5.0.1"
 			},
@@ -6037,9 +6057,10 @@
 			}
 		},
 		"node_modules/flatbuffers": {
-			"version": "1.12.0",
-			"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz",
-			"integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ=="
+			"version": "25.1.24",
+			"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.1.24.tgz",
+			"integrity": "sha512-Ni+KCqYquU30UEgGkrrwpbYtUcUmNuLFcQ5Xdy9DK7WUaji+AAov+Bf12FEYmu0eI15y31oD38utnBexe0cAYA==",
+			"license": "Apache-2.0"
 		},
 		"node_modules/flatted": {
 			"version": "3.3.1",
@@ -6110,7 +6131,6 @@
 			"version": "11.2.0",
 			"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
 			"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
-			"dev": true,
 			"dependencies": {
 				"graceful-fs": "^4.2.0",
 				"jsonfile": "^6.0.1",
@@ -6238,6 +6258,7 @@
 			"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
 			"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": "*"
 			}
@@ -6429,8 +6450,7 @@
 		"node_modules/graceful-fs": {
 			"version": "4.2.11",
 			"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
-			"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
-			"dev": true
+			"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
 		},
 		"node_modules/graphemer": {
 			"version": "1.4.0",
@@ -6441,7 +6461,8 @@
 		"node_modules/guid-typescript": {
 			"version": "1.0.9",
 			"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
-			"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ=="
+			"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
+			"license": "ISC"
 		},
 		"node_modules/gulp-sort": {
 			"version": "2.0.0",
@@ -6809,7 +6830,6 @@
 			"version": "2.1.0",
 			"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
 			"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-			"dev": true,
 			"dependencies": {
 				"binary-extensions": "^2.0.0"
 			},
@@ -6858,7 +6878,6 @@
 			"version": "2.1.1",
 			"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
 			"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-			"dev": true,
 			"engines": {
 				"node": ">=0.10.0"
 			}
@@ -6875,7 +6894,6 @@
 			"version": "4.0.3",
 			"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
 			"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-			"dev": true,
 			"dependencies": {
 				"is-extglob": "^2.1.1"
 			},
@@ -6917,7 +6935,6 @@
 			"version": "7.0.0",
 			"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
 			"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-			"dev": true,
 			"engines": {
 				"node": ">=0.12.0"
 			}
@@ -7097,7 +7114,6 @@
 			"version": "6.1.0",
 			"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
 			"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-			"dev": true,
 			"dependencies": {
 				"universalify": "^2.0.0"
 			},
@@ -7172,6 +7188,16 @@
 			"integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==",
 			"dev": true
 		},
+		"node_modules/kokoro-js": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/kokoro-js/-/kokoro-js-1.1.1.tgz",
+			"integrity": "sha512-cyLO34iI8nBJXPnd3fI4fGeQGS+a6Uatg7eXNL6QS8TLSxaa30WD6Fj7/XoIZYaHg8q6d+TCrui/f74MTY2g1g==",
+			"license": "Apache-2.0",
+			"dependencies": {
+				"@huggingface/transformers": "^3.3.3",
+				"phonemizer": "^1.2.1"
+			}
+		},
 		"node_modules/layout-base": {
 			"version": "1.0.2",
 			"resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
@@ -7455,15 +7481,17 @@
 			}
 		},
 		"node_modules/long": {
-			"version": "5.2.3",
-			"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
-			"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+			"version": "5.2.4",
+			"resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz",
+			"integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==",
+			"license": "Apache-2.0"
 		},
 		"node_modules/loupe": {
 			"version": "2.3.7",
 			"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
 			"integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"get-func-name": "^2.0.1"
 			}
@@ -7609,7 +7637,6 @@
 			"version": "1.4.1",
 			"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
 			"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
-			"dev": true,
 			"engines": {
 				"node": ">= 8"
 			}
@@ -8066,7 +8093,6 @@
 			"version": "4.0.8",
 			"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
 			"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
-			"dev": true,
 			"dependencies": {
 				"braces": "^3.0.3",
 				"picomatch": "^2.3.1"
@@ -8150,6 +8176,7 @@
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz",
 			"integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==",
+			"license": "MIT",
 			"dependencies": {
 				"minipass": "^7.0.4",
 				"rimraf": "^5.0.5"
@@ -8162,6 +8189,7 @@
 			"version": "10.4.5",
 			"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
 			"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+			"license": "ISC",
 			"dependencies": {
 				"foreground-child": "^3.1.0",
 				"jackspeak": "^3.1.2",
@@ -8181,6 +8209,7 @@
 			"version": "3.4.3",
 			"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
 			"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+			"license": "BlueOak-1.0.0",
 			"dependencies": {
 				"@isaacs/cliui": "^8.0.2"
 			},
@@ -8195,6 +8224,7 @@
 			"version": "9.0.5",
 			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
 			"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+			"license": "ISC",
 			"dependencies": {
 				"brace-expansion": "^2.0.1"
 			},
@@ -8209,6 +8239,7 @@
 			"version": "5.0.10",
 			"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
 			"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
+			"license": "ISC",
 			"dependencies": {
 				"glob": "^10.3.7"
 			},
@@ -8329,7 +8360,6 @@
 			"version": "3.0.0",
 			"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
 			"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-			"dev": true,
 			"engines": {
 				"node": ">=0.10.0"
 			}
@@ -8433,42 +8463,46 @@
 			}
 		},
 		"node_modules/onnxruntime-common": {
-			"version": "1.19.2",
-			"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.19.2.tgz",
-			"integrity": "sha512-a4R7wYEVFbZBlp0BfhpbFWqe4opCor3KM+5Wm22Az3NGDcQMiU2hfG/0MfnBs+1ZrlSGmlgWeMcXQkDk1UFb8Q=="
+			"version": "1.20.1",
+			"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.1.tgz",
+			"integrity": "sha512-YiU0s0IzYYC+gWvqD1HzLc46Du1sXpSiwzKb63PACIJr6LfL27VsXSXQvt68EzD3V0D5Bc0vyJTjmMxp0ylQiw==",
+			"license": "MIT"
 		},
 		"node_modules/onnxruntime-node": {
-			"version": "1.19.2",
-			"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.19.2.tgz",
-			"integrity": "sha512-9eHMP/HKbbeUcqte1JYzaaRC8JPn7ojWeCeoyShO86TOR97OCyIyAIOGX3V95ErjslVhJRXY8Em/caIUc0hm1Q==",
+			"version": "1.20.1",
+			"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.20.1.tgz",
+			"integrity": "sha512-di/I4HDXRw+FLgq+TyHmQEDd3cEp9iFFZm0r4uJ1Wd7b/WE1VXtKWo8yemex347c6GNF/3Pv86ZfPhIWxORr0w==",
 			"hasInstallScript": true,
+			"license": "MIT",
 			"os": [
 				"win32",
 				"darwin",
 				"linux"
 			],
 			"dependencies": {
-				"onnxruntime-common": "1.19.2",
+				"onnxruntime-common": "1.20.1",
 				"tar": "^7.0.1"
 			}
 		},
 		"node_modules/onnxruntime-web": {
-			"version": "1.20.0-dev.20241016-2b8fc5529b",
-			"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.20.0-dev.20241016-2b8fc5529b.tgz",
-			"integrity": "sha512-1XovqtgqeEFtupuyzdDQo7Tqj4GRyNHzOoXjapCEo4rfH3JrXok5VtqucWfRXHPsOI5qoNxMQ9VE+drDIp6woQ==",
+			"version": "1.21.0-dev.20250206-d981b153d3",
+			"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.21.0-dev.20250206-d981b153d3.tgz",
+			"integrity": "sha512-esDVQdRic6J44VBMFLumYvcGfioMh80ceLmzF1yheJyuLKq/Th8VT2aj42XWQst+2bcWnAhw4IKmRQaqzU8ugg==",
+			"license": "MIT",
 			"dependencies": {
-				"flatbuffers": "^1.12.0",
+				"flatbuffers": "^25.1.24",
 				"guid-typescript": "^1.0.9",
 				"long": "^5.2.3",
-				"onnxruntime-common": "1.20.0-dev.20241016-2b8fc5529b",
+				"onnxruntime-common": "1.21.0-dev.20250206-d981b153d3",
 				"platform": "^1.3.6",
 				"protobufjs": "^7.2.4"
 			}
 		},
 		"node_modules/onnxruntime-web/node_modules/onnxruntime-common": {
-			"version": "1.20.0-dev.20241016-2b8fc5529b",
-			"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.0-dev.20241016-2b8fc5529b.tgz",
-			"integrity": "sha512-KZK8b6zCYGZFjd4ANze0pqBnqnFTS3GIVeclQpa2qseDpXrCQJfkWBixRcrZShNhm3LpFOZ8qJYFC5/qsJK9WQ=="
+			"version": "1.21.0-dev.20250206-d981b153d3",
+			"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0-dev.20250206-d981b153d3.tgz",
+			"integrity": "sha512-TwaE51xV9q2y8pM61q73rbywJnusw9ivTEHAJ39GVWNZqxCoDBpe/tQkh/w9S+o/g+zS7YeeL0I/2mEWd+dgyA==",
+			"license": "MIT"
 		},
 		"node_modules/optionator": {
 			"version": "0.9.3",
@@ -8546,7 +8580,8 @@
 		"node_modules/package-json-from-dist": {
 			"version": "1.0.1",
 			"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
-			"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
+			"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+			"license": "BlueOak-1.0.0"
 		},
 		"node_modules/paneforge": {
 			"version": "0.0.6",
@@ -8686,6 +8721,7 @@
 			"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
 			"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": "*"
 			}
@@ -8728,6 +8764,12 @@
 				"@types/estree": "*"
 			}
 		},
+		"node_modules/phonemizer": {
+			"version": "1.2.1",
+			"resolved": "https://registry.npmjs.org/phonemizer/-/phonemizer-1.2.1.tgz",
+			"integrity": "sha512-v0KJ4mi2T4Q7eJQ0W15Xd4G9k4kICSXE8bpDeJ8jisL4RyJhNWsweKTOi88QXFc4r4LZlz5jVL5lCHhkpdT71A==",
+			"license": "Apache-2.0"
+		},
 		"node_modules/picocolors": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
@@ -8781,7 +8823,8 @@
 		"node_modules/platform": {
 			"version": "1.3.6",
 			"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
-			"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
+			"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
+			"license": "MIT"
 		},
 		"node_modules/polyscript": {
 			"version": "0.12.8",
@@ -9064,6 +9107,7 @@
 			"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
 			"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"@jest/schemas": "^29.6.3",
 				"ansi-styles": "^5.0.0",
@@ -9078,6 +9122,7 @@
 			"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
 			"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": ">=10"
 			},
@@ -9314,6 +9359,7 @@
 			"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
 			"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
 			"hasInstallScript": true,
+			"license": "BSD-3-Clause",
 			"dependencies": {
 				"@protobufjs/aspromise": "^1.1.2",
 				"@protobufjs/base64": "^1.1.2",
@@ -9413,7 +9459,6 @@
 			"version": "1.2.3",
 			"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
 			"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-			"dev": true,
 			"funding": [
 				{
 					"type": "github",
@@ -9504,7 +9549,8 @@
 			"version": "18.3.1",
 			"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
 			"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
-			"dev": true
+			"dev": true,
+			"license": "MIT"
 		},
 		"node_modules/read-cache": {
 			"version": "1.0.0",
@@ -9534,7 +9580,6 @@
 			"version": "3.6.0",
 			"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
 			"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-			"dev": true,
 			"dependencies": {
 				"picomatch": "^2.2.1"
 			},
@@ -9637,7 +9682,6 @@
 			"version": "1.0.4",
 			"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
 			"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
-			"dev": true,
 			"engines": {
 				"iojs": ">=1.0.0",
 				"node": ">=0.10.0"
@@ -9763,7 +9807,6 @@
 			"version": "1.2.0",
 			"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
 			"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-			"dev": true,
 			"funding": [
 				{
 					"type": "github",
@@ -11131,6 +11174,7 @@
 			"version": "7.4.3",
 			"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
 			"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+			"license": "ISC",
 			"dependencies": {
 				"@isaacs/fs-minipass": "^4.0.0",
 				"chownr": "^3.0.0",
@@ -11147,6 +11191,7 @@
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
 			"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+			"license": "MIT",
 			"bin": {
 				"mkdirp": "dist/cjs/src/bin.js"
 			},
@@ -11247,6 +11292,7 @@
 			"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
 			"integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": ">=14.0.0"
 			}
@@ -11277,7 +11323,6 @@
 			"version": "5.0.1",
 			"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
 			"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-			"dev": true,
 			"dependencies": {
 				"is-number": "^7.0.0"
 			},
@@ -11406,10 +11451,11 @@
 			"integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A=="
 		},
 		"node_modules/type-detect": {
-			"version": "4.0.8",
-			"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
-			"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+			"version": "4.1.0",
+			"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+			"integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
 			"dev": true,
+			"license": "MIT",
 			"engines": {
 				"node": ">=4"
 			}
@@ -11484,7 +11530,6 @@
 			"version": "2.0.1",
 			"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
 			"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
-			"dev": true,
 			"engines": {
 				"node": ">= 10.0.0"
 			}
@@ -11749,10 +11794,11 @@
 			}
 		},
 		"node_modules/vite-node": {
-			"version": "1.6.0",
-			"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz",
-			"integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
+			"version": "1.6.1",
+			"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz",
+			"integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
 				"cac": "^6.7.14",
 				"debug": "^4.3.4",
@@ -11770,6 +11816,24 @@
 				"url": "https://opencollective.com/vitest"
 			}
 		},
+		"node_modules/vite-plugin-static-copy": {
+			"version": "2.2.0",
+			"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.2.0.tgz",
+			"integrity": "sha512-ytMrKdR9iWEYHbUxs6x53m+MRl4SJsOSoMu1U1+Pfg0DjPeMlsRVx3RR5jvoonineDquIue83Oq69JvNsFSU5w==",
+			"license": "MIT",
+			"dependencies": {
+				"chokidar": "^3.5.3",
+				"fast-glob": "^3.2.11",
+				"fs-extra": "^11.1.0",
+				"picocolors": "^1.0.0"
+			},
+			"engines": {
+				"node": "^18.0.0 || >=20.0.0"
+			},
+			"peerDependencies": {
+				"vite": "^5.0.0 || ^6.0.0"
+			}
+		},
 		"node_modules/vite/node_modules/@esbuild/aix-ppc64": {
 			"version": "0.21.5",
 			"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -12166,16 +12230,17 @@
 			}
 		},
 		"node_modules/vitest": {
-			"version": "1.6.0",
-			"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
-			"integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
+			"version": "1.6.1",
+			"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz",
+			"integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
 			"dev": true,
+			"license": "MIT",
 			"dependencies": {
-				"@vitest/expect": "1.6.0",
-				"@vitest/runner": "1.6.0",
-				"@vitest/snapshot": "1.6.0",
-				"@vitest/spy": "1.6.0",
-				"@vitest/utils": "1.6.0",
+				"@vitest/expect": "1.6.1",
+				"@vitest/runner": "1.6.1",
+				"@vitest/snapshot": "1.6.1",
+				"@vitest/spy": "1.6.1",
+				"@vitest/utils": "1.6.1",
 				"acorn-walk": "^8.3.2",
 				"chai": "^4.3.10",
 				"debug": "^4.3.4",
@@ -12189,7 +12254,7 @@
 				"tinybench": "^2.5.1",
 				"tinypool": "^0.8.3",
 				"vite": "^5.0.0",
-				"vite-node": "1.6.0",
+				"vite-node": "1.6.1",
 				"why-is-node-running": "^2.2.2"
 			},
 			"bin": {
@@ -12204,8 +12269,8 @@
 			"peerDependencies": {
 				"@edge-runtime/vm": "*",
 				"@types/node": "^18.0.0 || >=20.0.0",
-				"@vitest/browser": "1.6.0",
-				"@vitest/ui": "1.6.0",
+				"@vitest/browser": "1.6.1",
+				"@vitest/ui": "1.6.1",
 				"happy-dom": "*",
 				"jsdom": "*"
 			},
@@ -12567,6 +12632,7 @@
 			"version": "5.0.0",
 			"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
 			"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+			"license": "BlueOak-1.0.0",
 			"engines": {
 				"node": ">=18"
 			}

+ 4 - 2
package.json

@@ -47,7 +47,7 @@
 		"tslib": "^2.4.1",
 		"typescript": "^5.5.4",
 		"vite": "^5.4.14",
-		"vitest": "^1.6.0"
+		"vitest": "^1.6.1"
 	},
 	"type": "module",
 	"dependencies": {
@@ -85,6 +85,7 @@
 		"idb": "^7.1.1",
 		"js-sha256": "^0.10.1",
 		"katex": "^0.16.21",
+		"kokoro-js": "^1.1.1",
 		"marked": "^9.1.0",
 		"mermaid": "^10.9.3",
 		"paneforge": "^0.0.6",
@@ -105,7 +106,8 @@
 		"svelte-sonner": "^0.3.19",
 		"tippy.js": "^6.3.7",
 		"turndown": "^7.2.0",
-		"uuid": "^9.0.1"
+		"uuid": "^9.0.1",
+		"vite-plugin-static-copy": "^2.2.0"
 	},
 	"engines": {
 		"node": ">=18.13.0 <=22.x.x",

+ 4 - 1
pyproject.toml

@@ -40,6 +40,9 @@ dependencies = [
     "argon2-cffi==23.1.0",
     "APScheduler==3.10.4",
 
+
+    "RestrictedPython==8.0",
+
     "openai",
     "anthropic",
     "google-generativeai==0.7.2",
@@ -83,7 +86,7 @@ dependencies = [
     "rapidocr-onnxruntime==1.3.24",
     "rank-bm25==0.2.2",
 
-    "faster-whisper==1.0.3",
+    "faster-whisper==1.1.1",
 
     "PyJWT[crypto]==2.10.1",
     "authlib==1.4.1",

+ 3 - 1
src/lib/components/admin/Evaluations/Leaderboard.svelte

@@ -1,6 +1,8 @@
 <script lang="ts">
 	import * as ort from 'onnxruntime-web';
-	import { AutoModel, AutoTokenizer } from '@huggingface/transformers';
+	import { env, AutoModel, AutoTokenizer } from '@huggingface/transformers';
+
+	env.backends.onnx.wasm.wasmPaths = '/wasm/';
 
 	import { onMount, getContext } from 'svelte';
 	import { models } from '$lib/stores';

+ 36 - 1
src/lib/components/admin/Settings/Audio.svelte

@@ -39,6 +39,7 @@
 	let STT_ENGINE = '';
 	let STT_MODEL = '';
 	let STT_WHISPER_MODEL = '';
+	let STT_DEEPGRAM_API_KEY = '';
 
 	let STT_WHISPER_MODEL_LOADING = false;
 
@@ -103,7 +104,8 @@
 				OPENAI_API_KEY: STT_OPENAI_API_KEY,
 				ENGINE: STT_ENGINE,
 				MODEL: STT_MODEL,
-				WHISPER_MODEL: STT_WHISPER_MODEL
+				WHISPER_MODEL: STT_WHISPER_MODEL,
+				DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY
 			}
 		});
 
@@ -143,6 +145,7 @@
 			STT_ENGINE = res.stt.ENGINE;
 			STT_MODEL = res.stt.MODEL;
 			STT_WHISPER_MODEL = res.stt.WHISPER_MODEL;
+			STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY;
 		}
 
 		await getVoices();
@@ -173,6 +176,7 @@
 							<option value="">{$i18n.t('Whisper (Local)')}</option>
 							<option value="openai">OpenAI</option>
 							<option value="web">{$i18n.t('Web API')}</option>
+							<option value="deepgram">Deepgram</option>
 						</select>
 					</div>
 				</div>
@@ -210,6 +214,37 @@
 							</div>
 						</div>
 					</div>
+				{:else if STT_ENGINE === 'deepgram'}
+					<div>
+						<div class="mt-1 flex gap-2 mb-1">
+							<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_DEEPGRAM_API_KEY} />
+						</div>
+					</div>
+
+					<hr class=" dark:border-gray-850 my-2" />
+
+					<div>
+						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
+						<div class="flex w-full">
+							<div class="flex-1">
+								<input
+									class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+									bind:value={STT_MODEL}
+									placeholder="Select a model (optional)"
+								/>
+							</div>
+						</div>
+						<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
+							{$i18n.t('Leave model field empty to use the default model.')}
+							<a
+								class=" hover:underline dark:text-gray-200 text-gray-800"
+								href="https://developers.deepgram.com/docs/models"
+								target="_blank"
+							>
+								{$i18n.t('Click here to see available models.')}
+							</a>
+						</div>
+					</div>
 				{:else if STT_ENGINE === ''}
 					<div>
 						<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>

+ 16 - 4
src/lib/components/admin/Settings/Connections.svelte

@@ -101,14 +101,17 @@
 	const addOpenAIConnectionHandler = async (connection) => {
 		OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url];
 		OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key];
-		OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length] = connection.config;
+		OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config;
 
 		await updateOpenAIHandler();
 	};
 
 	const addOllamaConnectionHandler = async (connection) => {
 		OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url];
-		OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length] = connection.config;
+		OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = {
+			...connection.config,
+			key: connection.key
+		};
 
 		await updateOllamaHandler();
 	};
@@ -244,7 +247,11 @@
 											);
 											OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
 
-											delete OPENAI_API_CONFIGS[idx];
+											let newConfig = {};
+											OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
+												newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
+											});
+											OPENAI_API_CONFIGS = newConfig;
 										}}
 									/>
 								{/each}
@@ -302,7 +309,12 @@
 										}}
 										onDelete={() => {
 											OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
-											delete OLLAMA_API_CONFIGS[idx];
+
+											let newConfig = {};
+											OLLAMA_BASE_URLS.forEach((url, newIdx) => {
+												newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
+											});
+											OLLAMA_API_CONFIGS = newConfig;
 										}}
 									/>
 								{/each}

+ 28 - 0
src/lib/components/admin/Settings/WebSearch.svelte

@@ -34,6 +34,16 @@
 	let youtubeProxyUrl = '';
 
 	const submitHandler = async () => {
+		// Convert domain filter string to array before sending
+		if (webConfig.search.domain_filter_list) {
+			webConfig.search.domain_filter_list = webConfig.search.domain_filter_list
+				.split(',')
+				.map((domain) => domain.trim())
+				.filter((domain) => domain.length > 0);
+		} else {
+			webConfig.search.domain_filter_list = [];
+		}
+
 		const res = await updateRAGConfig(localStorage.token, {
 			web: webConfig,
 			youtube: {
@@ -49,6 +59,10 @@
 
 		if (res) {
 			webConfig = res.web;
+			// Convert array back to comma-separated string for display
+			if (webConfig?.search?.domain_filter_list) {
+				webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', ');
+			}
 
 			youtubeLanguage = res.youtube.language.join(',');
 			youtubeTranslation = res.youtube.translation;
@@ -334,6 +348,20 @@
 							/>
 						</div>
 					</div>
+
+					<div class="mt-2">
+						<div class=" self-center text-xs font-medium mb-1">
+							{$i18n.t('Domain Filter List')}
+						</div>
+
+						<input
+							class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
+							placeholder={$i18n.t(
+								'Enter domains separated by commas (e.g., example.com,site.org)'
+							)}
+							bind:value={webConfig.search.domain_filter_list}
+						/>
+					</div>
 				{/if}
 			</div>
 

+ 1 - 1
src/lib/components/chat/MessageInput.svelte

@@ -695,7 +695,7 @@
 													)}
 												placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
 												largeTextAsFile={$settings?.largeTextAsFile ?? false}
-												autocomplete={true}
+												autocomplete={$config?.features.enable_autocomplete_generation}
 												generateAutoCompletion={async (text) => {
 													if (selectedModelIds.length === 0 || !selectedModelIds.at(0)) {
 														toast.error($i18n.t('Please select a model first.'));

+ 19 - 11
src/lib/components/chat/Messages/Citations.svelte

@@ -97,7 +97,7 @@
 {#if citations.length > 0}
 	<div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
 		{#if citations.length <= 3}
-			<div class="flex text-xs font-medium">
+			<div class="flex text-xs font-medium flex-wrap">
 				{#each citations as citation, idx}
 					<button
 						id={`source-${citation.source.name}`}
@@ -113,7 +113,7 @@
 							</div>
 						{/if}
 						<div
-							class="flex-1 mx-1 line-clamp-1 text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
+							class="flex-1 mx-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
 						>
 							{citation.source.name}
 						</div>
@@ -121,13 +121,21 @@
 				{/each}
 			</div>
 		{:else}
-			<Collapsible bind:open={isCollapsibleOpen} className="w-full">
+			<Collapsible
+				bind:open={isCollapsibleOpen}
+				className="w-full max-w-full "
+				buttonClassName="w-fit max-w-full"
+			>
 				<div
-					class="flex items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
+					class="flex w-full overflow-auto items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
 				>
-					<div class="flex-grow flex items-center gap-1 overflow-hidden">
-						<span class="whitespace-nowrap hidden sm:inline">{$i18n.t('References from')}</span>
-						<div class="flex items-center">
+					<div
+						class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full"
+					>
+						<span class="whitespace-nowrap hidden sm:inline flex-shrink-0"
+							>{$i18n.t('References from')}</span
+						>
+						<div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1">
 							<div class="flex text-xs font-medium items-center">
 								{#each citations.slice(0, 2) as citation, idx}
 									<button
@@ -145,14 +153,14 @@
 												{idx + 1}
 											</div>
 										{/if}
-										<div class="flex-1 mx-1 line-clamp-1 truncate">
+										<div class="flex-1 mx-1 truncate">
 											{citation.source.name}
 										</div>
 									</button>
 								{/each}
 							</div>
 						</div>
-						<div class="flex items-center gap-1 whitespace-nowrap">
+						<div class="flex items-center gap-1 whitespace-nowrap flex-shrink-0">
 							<span class="hidden sm:inline">{$i18n.t('and')}</span>
 							{citations.length - 2}
 							<span>{$i18n.t('more')}</span>
@@ -167,7 +175,7 @@
 					</div>
 				</div>
 				<div slot="content">
-					<div class="flex text-xs font-medium">
+					<div class="flex text-xs font-medium flex-wrap">
 						{#each citations as citation, idx}
 							<button
 								class="no-toggle outline-none flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
@@ -181,7 +189,7 @@
 										{idx + 1}
 									</div>
 								{/if}
-								<div class="flex-1 mx-1 line-clamp-1 truncate">
+								<div class="flex-1 mx-1 truncate">
 									{citation.source.name}
 								</div>
 							</button>

+ 5 - 0
src/lib/components/chat/Settings/General.svelte

@@ -49,6 +49,7 @@
 		function_calling: null,
 		seed: null,
 		temperature: null,
+		reasoning_effort: null,
 		frequency_penalty: null,
 		repeat_last_n: null,
 		mirostat: null,
@@ -333,9 +334,13 @@
 					system: system !== '' ? system : undefined,
 					params: {
 						stream_response: params.stream_response !== null ? params.stream_response : undefined,
+						function_calling:
+							params.function_calling !== null ? params.function_calling : undefined,
 						seed: (params.seed !== null ? params.seed : undefined) ?? undefined,
 						stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
 						temperature: params.temperature !== null ? params.temperature : undefined,
+						reasoning_effort:
+							params.reasoning_effort !== null ? params.reasoning_effort : undefined,
 						frequency_penalty:
 							params.frequency_penalty !== null ? params.frequency_penalty : undefined,
 						repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined,

+ 9 - 3
src/lib/components/common/Collapsible.svelte

@@ -74,9 +74,15 @@
 				<div class="">
 					{#if attributes?.type === 'reasoning'}
 						{#if attributes?.done === 'true' && attributes?.duration}
-							{$i18n.t('Thought for {{DURATION}}', {
-								DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
-							})}
+							{#if attributes.duration < 60}
+								{$i18n.t('Thought for {{DURATION}} seconds', {
+									DURATION: attributes.duration
+								})}
+							{:else}
+								{$i18n.t('Thought for {{DURATION}}', {
+									DURATION: dayjs.duration(attributes.duration, 'seconds').humanize()
+								})}
+							{/if}
 						{:else}
 							{$i18n.t('Thinking...')}
 						{/if}

+ 1 - 0
src/lib/components/common/SensitiveInput.svelte

@@ -23,6 +23,7 @@
 	/>
 	<button
 		class={showButtonClassName}
+		type="button"
 		on:click={(e) => {
 			e.preventDefault();
 			show = !show;

+ 101 - 101
src/lib/i18n/locales/de-DE/translation.json

@@ -1,11 +1,11 @@
 {
-	"-1 for no limit, or a positive integer for a specific limit": "",
+	"-1 for no limit, or a positive integer for a specific limit": "-1 für kein Limit oder eine positive Zahl für ein spezifisches Limit",
 	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' oder '-1' für keine Ablaufzeit.",
 	"(e.g. `sh webui.sh --api --api-auth username_password`)": "(z. B. `sh webui.sh --api --api-auth username_password`)",
 	"(e.g. `sh webui.sh --api`)": "(z. B. `sh webui.sh --api`)",
 	"(latest)": "(neueste)",
 	"{{ models }}": "{{ Modelle }}",
-	"{{COUNT}} Replies": "",
+	"{{COUNT}} Replies": "{{COUNT}} Antworten",
 	"{{user}}'s Chats": "{{user}}s Unterhaltungen",
 	"{{webUIName}} Backend Required": "{{webUIName}}-Backend erforderlich",
 	"*Prompt node ID(s) are required for image generation": "*Prompt-Node-ID(s) sind für die Bildgenerierung erforderlich",
@@ -35,7 +35,7 @@
 	"Add Group": "Gruppe hinzufügen",
 	"Add Memory": "Erinnerung hinzufügen",
 	"Add Model": "Modell hinzufügen",
-	"Add Reaction": "",
+	"Add Reaction": "Reaktion hinzufügen",
 	"Add Tag": "Tag hinzufügen",
 	"Add Tags": "Tags hinzufügen",
 	"Add text content": "Textinhalt hinzufügen",
@@ -51,7 +51,7 @@
 	"Advanced Params": "Erweiterte Parameter",
 	"All Documents": "Alle Dokumente",
 	"All models deleted successfully": "Alle Modelle erfolgreich gelöscht",
-	"Allow Chat Controls": "",
+	"Allow Chat Controls": "Chat-Steuerung erlauben",
 	"Allow Chat Delete": "Löschen von Unterhaltungen erlauben",
 	"Allow Chat Deletion": "Löschen von Unterhaltungen erlauben",
 	"Allow Chat Edit": "Bearbeiten von Unterhaltungen erlauben",
@@ -60,21 +60,21 @@
 	"Allow Temporary Chat": "Temporäre Unterhaltungen erlauben",
 	"Allow User Location": "Standort freigeben",
 	"Allow Voice Interruption in Call": "Unterbrechung durch Stimme im Anruf zulassen",
-	"Allowed Endpoints": "",
+	"Allowed Endpoints": "Erlaubte Endpunkte",
 	"Already have an account?": "Haben Sie bereits einen Account?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "Alternative zu top_p und zielt darauf ab, ein Gleichgewicht zwischen Qualität und Vielfalt zu gewährleisten. Der Parameter p repräsentiert die Mindestwahrscheinlichkeit für ein Token, um berücksichtigt zu werden, relativ zur Wahrscheinlichkeit des wahrscheinlichsten Tokens. Zum Beispiel, bei p=0.05 und das wahrscheinlichste Token hat eine Wahrscheinlichkeit von 0.9, werden Logits mit einem Wert von weniger als 0.045 herausgefiltert. (Standard: 0.0)",
-	"Always": "",
+	"Always": "Immer",
 	"Amazing": "Fantastisch",
 	"an assistant": "ein Assistent",
-	"Analyzed": "",
-	"Analyzing...": "",
+	"Analyzed": "Analysiert",
+	"Analyzing...": "Analysiere...",
 	"and": "und",
 	"and {{COUNT}} more": "und {{COUNT}} mehr",
 	"and create a new shared link.": "und erstellen Sie einen neuen freigegebenen Link.",
 	"API Base URL": "API-Basis-URL",
 	"API Key": "API-Schlüssel",
 	"API Key created.": "API-Schlüssel erstellt.",
-	"API Key Endpoint Restrictions": "",
+	"API Key Endpoint Restrictions": "API-Schlüssel Endpunkteinschränkungen",
 	"API keys": "API-Schlüssel",
 	"Application DN": "Anwendungs-DN",
 	"Application DN Password": "Anwendungs-DN-Passwort",
@@ -84,8 +84,8 @@
 	"Archive All Chats": "Alle Unterhaltungen archivieren",
 	"Archived Chats": "Archivierte Unterhaltungen",
 	"archived-chat-export": "archivierter-chat-export",
-	"Are you sure you want to delete this channel?": "",
-	"Are you sure you want to delete this message?": "",
+	"Are you sure you want to delete this channel?": "Sind Sie sicher, dass Sie diesen Kanal löschen möchten?",
+	"Are you sure you want to delete this message?": "Sind Sie sicher, dass Sie diese Nachricht löschen möchten?",
 	"Are you sure you want to unarchive all archived chats?": "Sind Sie sicher, dass Sie alle archivierten Unterhaltungen wiederherstellen möchten?",
 	"Are you sure?": "Sind Sie sicher?",
 	"Arena Models": "Arena-Modelle",
@@ -94,15 +94,15 @@
 	"Assistant": "Assistent",
 	"Attach file": "Datei anhängen",
 	"Attention to detail": "Aufmerksamkeit für Details",
-	"Attribute for Mail": "",
+	"Attribute for Mail": "Attribut für E-Mail",
 	"Attribute for Username": "Attribut für Benutzername",
 	"Audio": "Audio",
 	"August": "August",
 	"Authenticate": "Authentifizieren",
 	"Auto-Copy Response to Clipboard": "Antwort automatisch in die Zwischenablage kopieren",
 	"Auto-playback response": "Antwort automatisch abspielen",
-	"Autocomplete Generation": "",
-	"Autocomplete Generation Input Max Length": "",
+	"Autocomplete Generation": "Automatische Vervollständigung",
+	"Autocomplete Generation Input Max Length": "Maximale Eingabenlände für automatische Vervollständigung",
 	"Automatic1111": "Automatic1111",
 	"AUTOMATIC1111 Api Auth String": "AUTOMATIC1111-API-Authentifizierungszeichenfolge",
 	"AUTOMATIC1111 Base URL": "AUTOMATIC1111-Basis-URL",
@@ -119,7 +119,7 @@
 	"Batch Size (num_batch)": "Stapelgröße (num_batch)",
 	"before": "bereits geteilt",
 	"Being lazy": "Faulheit",
-	"Beta": "",
+	"Beta": "Beta",
 	"Bing Search V7 Endpoint": "Bing Search V7-Endpunkt",
 	"Bing Search V7 Subscription Key": "Bing Search V7-Abonnement-Schlüssel",
 	"Brave Search API Key": "Brave Search API-Schlüssel",
@@ -130,13 +130,13 @@
 	"Camera": "Kamera",
 	"Cancel": "Abbrechen",
 	"Capabilities": "Fähigkeiten",
-	"Capture": "",
+	"Capture": "Aufnehmen",
 	"Certificate Path": "Zertifikatpfad",
 	"Change Password": "Passwort ändern",
-	"Channel Name": "",
-	"Channels": "",
+	"Channel Name": "Kanalname",
+	"Channels": "Kanäle",
 	"Character": "Zeichen",
-	"Character limit for autocomplete generation input": "",
+	"Character limit for autocomplete generation input": "Zeichenlimit für die Eingabe der automatischen Vervollständigung",
 	"Chart new frontiers": "Neue Wege beschreiten",
 	"Chat": "Gespräch",
 	"Chat Background Image": "Hintergrundbild des Unterhaltungsfensters",
@@ -171,16 +171,16 @@
 	"Click on the user role button to change a user's role.": "Klicken Sie auf die Benutzerrolle, um sie zu ändern.",
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Schreibberechtigung für die Zwischenablage verweigert. Bitte überprüfen Sie Ihre Browsereinstellungen, um den erforderlichen Zugriff zu erlauben.",
 	"Clone": "Klonen",
-	"Clone Chat": "",
-	"Clone of {{TITLE}}": "",
+	"Clone Chat": "Konversation klonen",
+	"Clone of {{TITLE}}": "Klon von {{TITLE}}",
 	"Close": "Schließen",
 	"Code execution": "Codeausführung",
 	"Code formatted successfully": "Code erfolgreich formatiert",
-	"Code Interpreter": "",
+	"Code Interpreter": "Code-Interpreter",
 	"Collection": "Kollektion",
 	"Color": "Farbe",
 	"ComfyUI": "ComfyUI",
-	"ComfyUI API Key": "",
+	"ComfyUI API Key": "ComfyUI-API-Schlüssel",
 	"ComfyUI Base URL": "ComfyUI-Basis-URL",
 	"ComfyUI Base URL is required.": "ComfyUI-Basis-URL wird benötigt.",
 	"ComfyUI Workflow": "ComfyUI-Workflow",
@@ -192,9 +192,9 @@
 	"Confirm": "Bestätigen",
 	"Confirm Password": "Passwort bestätigen",
 	"Confirm your action": "Bestätigen Sie Ihre Aktion.",
-	"Confirm your new password": "",
+	"Confirm your new password": "Neues Passwort bestätigen",
 	"Connections": "Verbindungen",
-	"Constrains effort on reasoning for reasoning models. Only applicable to reasoning models from specific providers that support reasoning effort. (Default: medium)": "",
+	"Constrains effort on reasoning for reasoning models. Only applicable to reasoning models from specific providers that support reasoning effort. (Default: medium)": "Beschränkt den Aufwand für das Schlussfolgern bei Schlussfolgerungsmodellen. Nur anwendbar auf Schlussfolgerungsmodelle von spezifischen Anbietern, die den Schlussfolgerungsaufwand unterstützen. (Standard: medium)",
 	"Contact Admin for WebUI Access": "Kontaktieren Sie den Administrator für den Zugriff auf die Weboberfläche",
 	"Content": "Info",
 	"Content Extraction": "Inhaltsextraktion",
@@ -220,7 +220,7 @@
 	"Create a model": "Modell erstellen",
 	"Create Account": "Konto erstellen",
 	"Create Admin Account": "Administrator-Account erstellen",
-	"Create Channel": "",
+	"Create Channel": "Kanal erstellen",
 	"Create Group": "Gruppe erstellen",
 	"Create Knowledge": "Wissen erstellen",
 	"Create new key": "Neuen Schlüssel erstellen",
@@ -238,10 +238,10 @@
 	"Default": "Standard",
 	"Default (Open AI)": "Standard (Open AI)",
 	"Default (SentenceTransformers)": "Standard (SentenceTransformers)",
-	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "Der Standardmodus funktioniert mit einer breiteren Auswahl von Modellen, indem er Werkzeuge einmal vor der Ausführung aufruft. Der native Modus nutzt die integrierten Tool-Aufrufmöglichkeiten des Modells, erfordert jedoch, dass das Modell diese Funktion von Natur aus unterstützt.",
 	"Default Model": "Standardmodell",
 	"Default model updated": "Standardmodell aktualisiert",
-	"Default Models": "",
+	"Default Models": "Standardmodelle",
 	"Default permissions": "Standardberechtigungen",
 	"Default permissions updated successfully": "Standardberechtigungen erfolgreich aktualisiert",
 	"Default Prompt Suggestions": "Prompt-Vorschläge",
@@ -257,7 +257,7 @@
 	"Delete chat?": "Unterhaltung löschen?",
 	"Delete folder?": "Ordner löschen?",
 	"Delete function?": "Funktion löschen?",
-	"Delete Message": "",
+	"Delete Message": "Nachricht löschen",
 	"Delete prompt?": "Prompt löschen?",
 	"delete this link": "diesen Link löschen",
 	"Delete tool?": "Werkzeug löschen?",
@@ -310,7 +310,7 @@
 	"e.g. Tools for performing various operations": "z. B. Werkzeuge für verschiedene Operationen",
 	"Edit": "Bearbeiten",
 	"Edit Arena Model": "Arena-Modell bearbeiten",
-	"Edit Channel": "",
+	"Edit Channel": "Kanal bearbeiten",
 	"Edit Connection": "Verbindung bearbeiten",
 	"Edit Default Permissions": "Standardberechtigungen bearbeiten",
 	"Edit Memory": "Erinnerungen bearbeiten",
@@ -323,10 +323,10 @@
 	"Embedding Model": "Embedding-Modell",
 	"Embedding Model Engine": "Embedding-Modell-Engine",
 	"Embedding model set to \"{{embedding_model}}\"": "Embedding-Modell auf \"{{embedding_model}}\" gesetzt",
-	"Enable API Key": "",
-	"Enable autocomplete generation for chat messages": "",
+	"Enable API Key": "API-Schlüssel aktivieren",
+	"Enable autocomplete generation for chat messages": "Automatische Vervollständigung für Chat-Nachrichten aktivieren",
 	"Enable Community Sharing": "Community-Freigabe aktivieren",
-	"Enable Google Drive": "",
+	"Enable Google Drive": "Google Drive aktivieren",
 	"Enable Memory Locking (mlock) to prevent model data from being swapped out of RAM. This option locks the model's working set of pages into RAM, ensuring that they will not be swapped out to disk. This can help maintain performance by avoiding page faults and ensuring fast data access.": "Aktiviere Memory Locking (mlock), um zu verhindern, dass Modelldaten aus dem RAM ausgelagert werden. Diese Option sperrt die Arbeitsseiten des Modells im RAM, um sicherzustellen, dass sie nicht auf die Festplatte ausgelagert werden. Dies kann die Leistung verbessern, indem Page Faults vermieden und ein schneller Datenzugriff sichergestellt werden.",
 	"Enable Memory Mapping (mmap) to load model data. This option allows the system to use disk storage as an extension of RAM by treating disk files as if they were in RAM. This can improve model performance by allowing for faster data access. However, it may not work correctly with all systems and can consume a significant amount of disk space.": "Aktiviere Memory Mapping (mmap), um Modelldaten zu laden. Diese Option ermöglicht es dem System, den Festplattenspeicher als Erweiterung des RAM zu verwenden, indem Festplattendateien so behandelt werden, als ob sie im RAM wären. Dies kann die Modellleistung verbessern, indem ein schnellerer Datenzugriff ermöglicht wird. Es kann jedoch nicht auf allen Systemen korrekt funktionieren und einen erheblichen Teil des Festplattenspeichers beanspruchen.",
 	"Enable Message Rating": "Nachrichtenbewertung aktivieren",
@@ -349,20 +349,20 @@
 	"Enter Chunk Overlap": "Geben Sie die Blocküberlappung ein",
 	"Enter Chunk Size": "Geben Sie die Blockgröße ein",
 	"Enter description": "Geben Sie eine Beschreibung ein",
-	"Enter Exa API Key": "",
+	"Enter Exa API Key": "Geben Sie den Exa-API-Schlüssel ein",
 	"Enter Github Raw URL": "Geben Sie die Github Raw-URL ein",
 	"Enter Google PSE API Key": "Geben Sie den Google PSE-API-Schlüssel ein",
 	"Enter Google PSE Engine Id": "Geben Sie die Google PSE-Engine-ID ein",
 	"Enter Image Size (e.g. 512x512)": "Geben Sie die Bildgröße ein (z. B. 512x512)",
 	"Enter Jina API Key": "Geben Sie den Jina-API-Schlüssel ein",
-	"Enter Kagi Search API Key": "",
+	"Enter Kagi Search API Key": "Geben sie den Kagi Search API-Schlüssel ein",
 	"Enter language codes": "Geben Sie die Sprachcodes ein",
 	"Enter Model ID": "Geben Sie die Modell-ID ein",
 	"Enter model tag (e.g. {{modelTag}})": "Geben Sie den Model-Tag ein",
 	"Enter Mojeek Search API Key": "Geben Sie den Mojeek Search API-Schlüssel ein",
 	"Enter Number of Steps (e.g. 50)": "Geben Sie die Anzahl an Schritten ein (z. B. 50)",
-	"Enter proxy URL (e.g. https://user:password@host:port)": "",
-	"Enter reasoning effort": "",
+	"Enter proxy URL (e.g. https://user:password@host:port)": "Geben sie die Proxy-URL ein (z. B. https://user:password@host:port)",
+	"Enter reasoning effort": "Geben Sie den Schlussfolgerungsaufwand ein",
 	"Enter Sampler (e.g. Euler a)": "Geben Sie den Sampler ein (z. B. Euler a)",
 	"Enter Scheduler (e.g. Karras)": "Geben Sie den Scheduler ein (z. B. Karras)",
 	"Enter Score": "Punktzahl eingeben",
@@ -379,33 +379,33 @@
 	"Enter stop sequence": "Stop-Sequenz eingeben",
 	"Enter system prompt": "Systemprompt eingeben",
 	"Enter Tavily API Key": "Geben Sie den Tavily-API-Schlüssel ein",
-	"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "",
+	"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "Geben sie die öffentliche URL Ihrer WebUI ein. Diese URL wird verwendet, um Links in den Benachrichtigungen zu generieren.",
 	"Enter Tika Server URL": "Geben Sie die Tika-Server-URL ein",
 	"Enter Top K": "Geben Sie Top K ein",
 	"Enter URL (e.g. http://127.0.0.1:7860/)": "Geben Sie die URL ein (z. B. http://127.0.0.1:7860/)",
 	"Enter URL (e.g. http://localhost:11434)": "Geben Sie die URL ein (z. B. http://localhost:11434)",
-	"Enter your current password": "",
+	"Enter your current password": "Geben Sie Ihr aktuelles Passwort ein",
 	"Enter Your Email": "Geben Sie Ihre E-Mail-Adresse ein",
 	"Enter Your Full Name": "Geben Sie Ihren vollständigen Namen ein",
 	"Enter your message": "Geben Sie Ihre Nachricht ein",
-	"Enter your new password": "",
+	"Enter your new password": "Geben Sie Ihr neues Passwort ein",
 	"Enter Your Password": "Geben Sie Ihr Passwort ein",
 	"Enter Your Role": "Geben Sie Ihre Rolle ein",
 	"Enter Your Username": "Geben Sie Ihren Benutzernamen ein",
-	"Enter your webhook URL": "",
+	"Enter your webhook URL": "Geben Sie Ihre Webhook-URL ein",
 	"Error": "Fehler",
 	"ERROR": "FEHLER",
-	"Error accessing Google Drive: {{error}}": "",
-	"Error uploading file: {{error}}": "",
+	"Error accessing Google Drive: {{error}}": "Fehler beim Zugriff auf Google Drive: {{error}}",
+	"Error uploading file: {{error}}": "Fehler beim Hochladen der Datei: {{error}}",
 	"Evaluations": "Evaluationen",
-	"Exa API Key": "",
+	"Exa API Key": "Exa-API-Schlüssel",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "Beispiel: (&(objectClass=inetOrgPerson)(uid=%s))",
 	"Example: ALL": "Beispiel: ALL",
-	"Example: mail": "",
+	"Example: mail": "Beispiel: mail",
 	"Example: ou=users,dc=foo,dc=example": "Beispiel: ou=users,dc=foo,dc=example",
 	"Example: sAMAccountName or uid or userPrincipalName": "Beispiel: sAMAccountName or uid or userPrincipalName",
 	"Exclude": "Ausschließen",
-	"Execute code for analysis": "",
+	"Execute code for analysis": "Code für Analyse ausführen",
 	"Experimental": "Experimentell",
 	"Explore the cosmos": "Erforschen Sie das Universum",
 	"Export": "Exportieren",
@@ -423,12 +423,12 @@
 	"External Models": "Externe Modelle",
 	"Failed to add file.": "Fehler beim Hinzufügen der Datei.",
 	"Failed to create API Key.": "Fehler beim Erstellen des API-Schlüssels.",
-	"Failed to fetch models": "",
+	"Failed to fetch models": "Fehler beim Abrufen der Modelle",
 	"Failed to read clipboard contents": "Fehler beim Abruf der Zwischenablage",
-	"Failed to save models configuration": "",
+	"Failed to save models configuration": "Fehler beim Speichern der Modellkonfiguration",
 	"Failed to update settings": "Fehler beim Aktualisieren der Einstellungen",
 	"Failed to upload file.": "Fehler beim Hochladen der Datei.",
-	"Features Permissions": "",
+	"Features Permissions": "Funktionen-Berechtigungen",
 	"February": "Februar",
 	"Feedback History": "Feedback-Verlauf",
 	"Feedbacks": "Feedbacks",
@@ -440,7 +440,7 @@
 	"File not found.": "Datei nicht gefunden.",
 	"File removed successfully.": "Datei erfolgreich entfernt.",
 	"File size should not exceed {{maxSize}} MB.": "Datei darf nicht größer als {{maxSize}} MB sein.",
-	"File uploaded successfully": "",
+	"File uploaded successfully": "Datei erfolgreich hochgeladen",
 	"Files": "Dateien",
 	"Filter is now globally disabled": "Filter ist jetzt global deaktiviert",
 	"Filter is now globally enabled": "Filter ist jetzt global aktiviert",
@@ -458,7 +458,7 @@
 	"Format your variables using brackets like this:": "Formatieren Sie Ihre Variablen mit Klammern, wie hier:",
 	"Frequency Penalty": "Frequenzstrafe",
 	"Function": "Funktion",
-	"Function Calling": "",
+	"Function Calling": "Funktionsaufruf",
 	"Function created successfully": "Funktion erfolgreich erstellt",
 	"Function deleted successfully": "Funktion erfolgreich gelöscht",
 	"Function Description": "Funktionsbeschreibung",
@@ -473,14 +473,14 @@
 	"Functions imported successfully": "Funktionen erfolgreich importiert",
 	"General": "Allgemein",
 	"General Settings": "Allgemeine Einstellungen",
-	"Generate an image": "",
+	"Generate an image": "Bild erzeugen",
 	"Generate Image": "Bild erzeugen",
 	"Generating search query": "Suchanfrage wird erstellt",
 	"Get started": "Loslegen",
 	"Get started with {{WEBUI_NAME}}": "Loslegen mit {{WEBUI_NAME}}",
 	"Global": "Global",
 	"Good Response": "Gute Antwort",
-	"Google Drive": "",
+	"Google Drive": "Google Drive",
 	"Google PSE API Key": "Google PSE-API-Schlüssel",
 	"Google PSE Engine Id": "Google PSE-Engine-ID",
 	"Group created successfully": "Gruppe erfolgreich erstellt",
@@ -504,14 +504,14 @@
 	"I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Ich bestätige, dass ich gelesen habe und die Auswirkungen meiner Aktion verstehe. Mir sind die Risiken bewusst, die mit der Ausführung beliebigen Codes verbunden sind, und ich habe die Vertrauenswürdigkeit der Quelle überprüft.",
 	"ID": "ID",
 	"Ignite curiosity": "Neugier entfachen",
-	"Image": "",
-	"Image Compression": "",
-	"Image Generation": "",
+	"Image": "Bild",
+	"Image Compression": "Bildkomprimierung",
+	"Image Generation": "Bildgenerierung",
 	"Image Generation (Experimental)": "Bildgenerierung (experimentell)",
 	"Image Generation Engine": "Bildgenerierungs-Engine",
-	"Image Max Compression Size": "",
-	"Image Prompt Generation": "",
-	"Image Prompt Generation Prompt": "",
+	"Image Max Compression Size": "Maximale Bildkomprimierungsgröße",
+	"Image Prompt Generation": "Bild-Prompt-Generierung",
+	"Image Prompt Generation Prompt": "Prompt für die Bild-Prompt-Generierung",
 	"Image Settings": "Bildeinstellungen",
 	"Images": "Bilder",
 	"Import Chats": "Unterhaltungen importieren",
@@ -532,7 +532,7 @@
 	"Interface": "Benutzeroberfläche",
 	"Invalid file format.": "Ungültiges Dateiformat.",
 	"Invalid Tag": "Ungültiger Tag",
-	"is typing...": "",
+	"is typing...": "schreibt ...",
 	"January": "Januar",
 	"Jina API Key": "Jina-API-Schlüssel",
 	"join our Discord for help.": "Treten Sie unserem Discord bei, um Hilfe zu erhalten.",
@@ -542,7 +542,7 @@
 	"June": "Juni",
 	"JWT Expiration": "JWT-Ablauf",
 	"JWT Token": "JWT-Token",
-	"Kagi Search API Key": "",
+	"Kagi Search API Key": "Kagi Search API-Schlüssel",
 	"Keep Alive": "Verbindung aufrechterhalten",
 	"Key": "Schlüssel",
 	"Keyboard shortcuts": "Tastenkombinationen",
@@ -557,7 +557,7 @@
 	"Language": "Sprache",
 	"Last Active": "Zuletzt aktiv",
 	"Last Modified": "Zuletzt bearbeitet",
-	"Last reply": "",
+	"Last reply": "Letzte Antwort",
 	"LDAP": "LDAP",
 	"LDAP server updated": "LDAP-Server aktualisiert",
 	"Leaderboard": "Bestenliste",
@@ -568,7 +568,7 @@
 	"Leave empty to use the default prompt, or enter a custom prompt": "Leer lassen, um den Standardprompt zu verwenden, oder geben Sie einen benutzerdefinierten Prompt ein",
 	"Light": "Hell",
 	"Listening...": "Höre zu...",
-	"Llama.cpp": "",
+	"Llama.cpp": "Llama.cpp",
 	"LLMs can make mistakes. Verify important information.": "LLMs können Fehler machen. Überprüfe wichtige Informationen.",
 	"Local": "Lokal",
 	"Local Models": "Lokale Modelle",
@@ -579,7 +579,7 @@
 	"Make sure to export a workflow.json file as API format from ComfyUI.": "Stellen Sie sicher, dass sie eine workflow.json-Datei im API-Format von ComfyUI exportieren.",
 	"Manage": "Verwalten",
 	"Manage Arena Models": "Arena-Modelle verwalten",
-	"Manage Models": "",
+	"Manage Models": "Modelle verwalten",
 	"Manage Ollama": "Ollama verwalten",
 	"Manage Ollama API Connections": "Ollama-API-Verbindungen verwalten",
 	"Manage OpenAI API Connections": "OpenAI-API-Verbindungen verwalten",
@@ -624,17 +624,17 @@
 	"Modelfile Content": "Modelfile-Inhalt",
 	"Models": "Modelle",
 	"Models Access": "Modell-Zugriff",
-	"Models configuration saved successfully": "",
+	"Models configuration saved successfully": "Modellkonfiguration erfolgreich gespeichert",
 	"Mojeek Search API Key": "Mojeek Search API-Schlüssel",
 	"more": "mehr",
 	"More": "Mehr",
 	"Name": "Name",
 	"Name your knowledge base": "Benennen Sie Ihren Wissensspeicher",
-	"Native": "",
+	"Native": "Nativ",
 	"New Chat": "Neue Unterhaltung",
-	"New Folder": "",
+	"New Folder": "Neuer Ordner",
 	"New Password": "Neues Passwort",
-	"new-channel": "",
+	"new-channel": "neuer-kanal",
 	"No content found": "Kein Inhalt gefunden",
 	"No content to speak": "Kein Inhalt zum Vorlesen",
 	"No distance available": "Keine Distanz verfügbar",
@@ -643,11 +643,11 @@
 	"No files found.": "Keine Dateien gefunden.",
 	"No groups with access, add a group to grant access": "Keine Gruppen mit Zugriff, fügen Sie eine Gruppe hinzu, um Zugriff zu gewähren",
 	"No HTML, CSS, or JavaScript content found.": "Keine HTML-, CSS- oder JavaScript-Inhalte gefunden.",
-	"No inference engine with management support found": "",
+	"No inference engine with management support found": "Keine Inferenz-Engine mit Management-Unterstützung gefunden",
 	"No knowledge found": "Kein Wissen gefunden",
 	"No model IDs": "Keine Modell-IDs",
 	"No models found": "Keine Modelle gefunden",
-	"No models selected": "",
+	"No models selected": "Keine Modelle ausgewählt",
 	"No results found": "Keine Ergebnisse gefunden",
 	"No search query generated": "Keine Suchanfrage generiert",
 	"No source available": "Keine Quelle verfügbar",
@@ -658,8 +658,8 @@
 	"Not helpful": "Nich hilfreich",
 	"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Hinweis: Wenn Sie eine Mindestpunktzahl festlegen, werden in der Suche nur Dokumente mit einer Punktzahl größer oder gleich der Mindestpunktzahl zurückgegeben.",
 	"Notes": "Notizen",
-	"Notification Sound": "",
-	"Notification Webhook": "",
+	"Notification Sound": "Benachrichtigungston",
+	"Notification Webhook": "Benachrichtigungs-Webhook",
 	"Notifications": "Benachrichtigungen",
 	"November": "November",
 	"num_gpu (Ollama)": "num_gpu (Ollama)",
@@ -724,8 +724,8 @@
 	"Please carefully review the following warnings:": "Bitte überprüfen Sie die folgenden Warnungen sorgfältig:",
 	"Please enter a prompt": "Bitte geben Sie einen Prompt ein",
 	"Please fill in all fields.": "Bitte füllen Sie alle Felder aus.",
-	"Please select a model first.": "",
-	"Please select a model.": "",
+	"Please select a model first.": "Bitte wählen Sie zuerst ein Modell aus.",
+	"Please select a model.": "Bitte wählen Sie ein Modell aus.",
 	"Please select a reason": "Bitte wählen Sie einen Grund aus",
 	"Port": "Port",
 	"Positive attitude": "Positive Einstellung",
@@ -734,7 +734,7 @@
 	"Previous 30 days": "Vorherige 30 Tage",
 	"Previous 7 days": "Vorherige 7 Tage",
 	"Profile Image": "Profilbild",
-	"Prompt": "",
+	"Prompt": "Prompt",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (z. B. \"Erzähle mir eine interessante Tatsache über das Römische Reich\")",
 	"Prompt Content": "Prompt-Inhalt",
 	"Prompt created successfully": "Prompt erfolgreich erstellt",
@@ -742,7 +742,7 @@
 	"Prompt updated successfully": "Prompt erfolgreich aktualisiert",
 	"Prompts": "Prompts",
 	"Prompts Access": "Prompt-Zugriff",
-	"Proxy URL": "",
+	"Proxy URL": "Proxy-URL",
 	"Pull \"{{searchValue}}\" from Ollama.com": "\"{{searchValue}}\" von Ollama.com beziehen",
 	"Pull a model from Ollama.com": "Modell von Ollama.com beziehen",
 	"Query Generation Prompt": "Abfragegenerierungsprompt",
@@ -750,9 +750,9 @@
 	"RAG Template": "RAG-Vorlage",
 	"Rating": "Bewertung",
 	"Re-rank models by topic similarity": "Modelle nach thematischer Ähnlichkeit neu ordnen",
-	"Read": "",
+	"Read": "Lesen",
 	"Read Aloud": "Vorlesen",
-	"Reasoning Effort": "",
+	"Reasoning Effort": "Schlussfolgerungsaufwand",
 	"Record voice": "Stimme aufnehmen",
 	"Redirecting you to OpenWebUI Community": "Sie werden zur OpenWebUI-Community weitergeleitet",
 	"Reduces the probability of generating nonsense. A higher value (e.g. 100) will give more diverse answers, while a lower value (e.g. 10) will be more conservative. (Default: 40)": "Reduziert die Wahrscheinlichkeit, Unsinn zu generieren. Ein höherer Wert (z.B. 100) liefert vielfältigere Antworten, während ein niedrigerer Wert (z.B. 10) konservativer ist. (Standard: 40)",
@@ -765,22 +765,22 @@
 	"Remove": "Entfernen",
 	"Remove Model": "Modell entfernen",
 	"Rename": "Umbenennen",
-	"Reorder Models": "",
+	"Reorder Models": "Modelle neu anordnen",
 	"Repeat Last N": "Wiederhole die letzten N",
-	"Reply in Thread": "",
+	"Reply in Thread": "Im Thread antworten",
 	"Request Mode": "Anforderungsmodus",
 	"Reranking Model": "Reranking-Modell",
 	"Reranking model disabled": "Reranking-Modell deaktiviert",
 	"Reranking model set to \"{{reranking_model}}\"": "Reranking-Modell \"{{reranking_model}}\" fesgelegt",
 	"Reset": "Zurücksetzen",
-	"Reset All Models": "",
+	"Reset All Models": "Alle Modelle zurücksetzen",
 	"Reset Upload Directory": "Upload-Verzeichnis zurücksetzen",
 	"Reset Vector Storage/Knowledge": "Vektorspeicher/Wissen zurücksetzen",
-	"Reset view": "",
+	"Reset view": "Ansicht zurücksetzen",
 	"Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Benachrichtigungen können nicht aktiviert werden, da die Website-Berechtigungen abgelehnt wurden. Bitte besuchen Sie Ihre Browser-Einstellungen, um den erforderlichen Zugriff zu gewähren.",
 	"Response splitting": "Antwortaufteilung",
 	"Result": "Ergebnis",
-	"Retrieval Query Generation": "",
+	"Retrieval Query Generation": "Abfragegenerierung",
 	"Rich Text Input for Chat": "Rich-Text-Eingabe für Unterhaltungen",
 	"RK": "RK",
 	"Role": "Rolle",
@@ -810,11 +810,11 @@
 	"Search options": "Suchoptionen",
 	"Search Prompts": "Prompts durchsuchen...",
 	"Search Result Count": "Anzahl der Suchergebnisse",
-	"Search the internet": "",
+	"Search the internet": "Im Internet suchen",
 	"Search Tools": "Werkzeuge durchsuchen...",
 	"SearchApi API Key": "SearchApi-API-Schlüssel",
 	"SearchApi Engine": "SearchApi-Engine",
-	"Searched {{count}} sites": "",
+	"Searched {{count}} sites": "{{count}} Websites durchsucht",
 	"Searching \"{{searchQuery}}\"": "Suche nach \"{{searchQuery}}\"",
 	"Searching Knowledge for \"{{searchQuery}}\"": "Suche im Wissen nach \"{{searchQuery}}\"",
 	"Searxng Query URL": "Searxng-Abfrage-URL",
@@ -829,7 +829,7 @@
 	"Select a pipeline": "Wählen Sie eine Pipeline",
 	"Select a pipeline url": "Wählen Sie eine Pipeline-URL",
 	"Select a tool": "Wählen Sie ein Werkzeug",
-	"Select an Ollama instance": "",
+	"Select an Ollama instance": "Wählen Sie eine Ollama-Instanz",
 	"Select Engine": "Engine auswählen",
 	"Select Knowledge": "Wissensdatenbank auswählen",
 	"Select model": "Modell auswählen",
@@ -856,7 +856,7 @@
 	"Set Scheduler": "Scheduler festlegen",
 	"Set Steps": "Schrittgröße festlegen",
 	"Set Task Model": "Aufgabenmodell festlegen",
-	"Set the number of layers, which will be off-loaded to GPU. Increasing this value can significantly improve performance for models that are optimized for GPU acceleration but may also consume more power and GPU resources.": "",
+	"Set the number of layers, which will be off-loaded to GPU. Increasing this value can significantly improve performance for models that are optimized for GPU acceleration but may also consume more power and GPU resources.": "Legen Sie die Anzahl der Schichten fest, die auf die GPU ausgelagert werden. Eine Erhöhung dieses Wertes kann die Leistung für Modelle, die für die GPU-Beschleunigung optimiert sind, erheblich verbessern, kann jedoch auch mehr Strom und GPU-Ressourcen verbrauchen.",
 	"Set the number of worker threads used for computation. This option controls how many threads are used to process incoming requests concurrently. Increasing this value can improve performance under high concurrency workloads but may also consume more CPU resources.": "Legt die Anzahl der für die Berechnung verwendeten GPU-Geräte fest. Diese Option steuert, wie viele GPU-Geräte (falls verfügbar) zur Verarbeitung eingehender Anfragen verwendet werden. Eine Erhöhung dieses Wertes kann die Leistung für Modelle, die für GPU-Beschleunigung optimiert sind, erheblich verbessern, kann jedoch auch mehr Strom und GPU-Ressourcen verbrauchen.",
 	"Set Voice": "Stimme festlegen",
 	"Set whisper model": "Whisper-Modell festlegen",
@@ -883,7 +883,7 @@
 	"Sign up": "Registrieren",
 	"Sign up to {{WEBUI_NAME}}": "Bei {{WEBUI_NAME}} registrieren",
 	"Signing in to {{WEBUI_NAME}}": "Wird bei {{WEBUI_NAME}} angemeldet",
-	"sk-1234": "",
+	"sk-1234": "sk-1234",
 	"Source": "Quelle",
 	"Speech Playback Speed": "Sprachwiedergabegeschwindigkeit",
 	"Speech recognition error: {{error}}": "Spracherkennungsfehler: {{error}}",
@@ -903,7 +903,7 @@
 	"System": "System",
 	"System Instructions": "Systemanweisungen",
 	"System Prompt": "System-Prompt",
-	"Tags Generation": "",
+	"Tags Generation": "Tag-Generierung",
 	"Tags Generation Prompt": "Prompt für Tag-Generierung",
 	"Tail free sampling is used to reduce the impact of less probable tokens from the output. A higher value (e.g., 2.0) will reduce the impact more, while a value of 1.0 disables this setting. (default: 1)": "Tail-Free Sampling wird verwendet, um den Einfluss weniger wahrscheinlicher Tokens auf die Ausgabe zu reduzieren. Ein höherer Wert (z.B. 2.0) reduziert den Einfluss stärker, während ein Wert von 1.0 diese Einstellung deaktiviert. (Standard: 1)",
 	"Tap to interrupt": "Zum Unterbrechen tippen",
@@ -921,7 +921,7 @@
 	"The batch size determines how many text requests are processed together at once. A higher batch size can increase the performance and speed of the model, but it also requires more memory.  (Default: 512)": "Die Batch-Größe bestimmt, wie viele Textanfragen gleichzeitig verarbeitet werden. Eine größere Batch-Größe kann die Leistung und Geschwindigkeit des Modells erhöhen, erfordert jedoch auch mehr Speicher. (Standard: 512)",
 	"The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "Die Entwickler hinter diesem Plugin sind leidenschaftliche Freiwillige aus der Community. Wenn Sie dieses Plugin hilfreich finden, erwägen Sie bitte, zu seiner Entwicklung beizutragen.",
 	"The evaluation leaderboard is based on the Elo rating system and is updated in real-time.": "Die Bewertungs-Bestenliste basiert auf dem Elo-Bewertungssystem und wird in Echtzeit aktualisiert.",
-	"The LDAP attribute that maps to the mail that users use to sign in.": "",
+	"The LDAP attribute that maps to the mail that users use to sign in.": "Das LDAP-Attribut, das der Mail zugeordnet ist, die Benutzer zum Anmelden verwenden.",
 	"The LDAP attribute that maps to the username that users use to sign in.": "Das LDAP-Attribut, das dem Benutzernamen zugeordnet ist, den Benutzer zum Anmelden verwenden.",
 	"The leaderboard is currently in beta, and we may adjust the rating calculations as we refine the algorithm.": "Die Bestenliste befindet sich derzeit in der Beta-Phase, und es ist möglich, dass wir die Bewertungsberechnungen anpassen, während wir den Algorithmus verfeinern.",
 	"The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.": "Die maximale Dateigröße in MB. Wenn die Dateigröße dieses Limit überschreitet, wird die Datei nicht hochgeladen.",
@@ -943,7 +943,7 @@
 	"This will delete all models including custom models and cannot be undone.": "Dies wird alle Modelle einschließlich benutzerdefinierter Modelle löschen und kann nicht rückgängig gemacht werden.",
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "Dadurch wird die Wissensdatenbank zurückgesetzt und alle Dateien synchronisiert. Möchten Sie fortfahren?",
 	"Thorough explanation": "Ausführliche Erklärung",
-	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}}": "Nachgedacht für {{DURATION}}",
 	"Tika": "Tika",
 	"Tika Server URL required.": "Tika-Server-URL erforderlich.",
 	"Tiktoken": "Tiktoken",
@@ -958,7 +958,7 @@
 	"To access the GGUF models available for downloading,": "Um auf die verfügbaren GGUF-Modelle zuzugreifen,",
 	"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Um auf das WebUI zugreifen zu können, wenden Sie sich bitte an einen Administrator. Administratoren können den Benutzerstatus über das Admin-Panel verwalten.",
 	"To attach knowledge base here, add them to the \"Knowledge\" workspace first.": "Um Wissensdatenbanken hier anzuhängen, fügen Sie sie zunächst dem Arbeitsbereich \"Wissen\" hinzu.",
-	"To learn more about available endpoints, visit our documentation.": "",
+	"To learn more about available endpoints, visit our documentation.": "Um mehr über verfügbare Endpunkte zu erfahren, besuchen Sie unsere Dokumentation.",
 	"To protect your privacy, only ratings, model IDs, tags, and metadata are shared from your feedback—your chat logs remain private and are not included.": "Um Ihre Privatsphäre zu schützen, werden nur Bewertungen, Modell-IDs, Tags und Metadaten aus Ihrem Feedback geteilt – Ihre Unterhaltungen bleiben privat und werden nicht einbezogen.",
 	"To select actions here, add them to the \"Functions\" workspace first.": "Um Aktionen auszuwählen, fügen Sie diese zunächst dem Arbeitsbereich „Funktionen“ hinzu.",
 	"To select filters here, add them to the \"Functions\" workspace first.": "Um Filter auszuwählen, fügen Sie diese zunächst dem Arbeitsbereich „Funktionen“ hinzu.",
@@ -980,7 +980,7 @@
 	"Tools": "Werkzeuge",
 	"Tools Access": "Werkzeugzugriff",
 	"Tools are a function calling system with arbitrary code execution": "Wekzeuge sind ein Funktionssystem mit beliebiger Codeausführung",
-	"Tools Function Calling Prompt": "",
+	"Tools Function Calling Prompt": "Prompt für Funktionssystemaufrufe",
 	"Tools have a function calling system that allows arbitrary code execution": "Werkezuge verfügen über ein Funktionssystem, das die Ausführung beliebigen Codes ermöglicht",
 	"Tools have a function calling system that allows arbitrary code execution.": "Werkzeuge verfügen über ein Funktionssystem, das die Ausführung beliebigen Codes ermöglicht.",
 	"Top K": "Top K",
@@ -992,7 +992,7 @@
 	"TTS Voice": "TTS-Stimme",
 	"Type": "Art",
 	"Type Hugging Face Resolve (Download) URL": "Geben Sie die Hugging Face Resolve-URL ein",
-	"Uh-oh! There was an issue with the response.": "",
+	"Uh-oh! There was an issue with the response.": "Oh nein! Es gab ein Problem mit der Antwort.",
 	"UI": "Oberfläche",
 	"Unarchive All": "Alle wiederherstellen",
 	"Unarchive All Archived Chats": "Alle archivierten Unterhaltungen wiederherstellen",
@@ -1038,7 +1038,7 @@
 	"variable to have them replaced with clipboard content.": "Variable, um den Inhalt der Zwischenablage beim Nutzen des Prompts zu ersetzen.",
 	"Version": "Version",
 	"Version {{selectedVersion}} of {{totalVersions}}": "Version {{selectedVersion}} von {{totalVersions}}",
-	"View Replies": "",
+	"View Replies": "Antworten anzeigen",
 	"Visibility": "Sichtbarkeit",
 	"Voice": "Stimme",
 	"Voice Input": "Spracheingabe",
@@ -1051,11 +1051,11 @@
 	"Web Loader Settings": "Web Loader Einstellungen",
 	"Web Search": "Websuche",
 	"Web Search Engine": "Suchmaschine",
-	"Web Search in Chat": "",
-	"Web Search Query Generation": "",
+	"Web Search in Chat": "Websuche im Chat",
+	"Web Search Query Generation": "Abfragegenerierung für Websuche",
 	"Webhook URL": "Webhook URL",
 	"WebUI Settings": "WebUI-Einstellungen",
-	"WebUI URL": "",
+	"WebUI URL": "WebUI-URL",
 	"WebUI will make requests to \"{{url}}/api/chat\"": "WebUI wird Anfragen an \"{{url}}/api/chat\" senden",
 	"WebUI will make requests to \"{{url}}/chat/completions\"": "WebUI wird Anfragen an \"{{url}}/chat/completions\" senden",
 	"What are you trying to achieve?": "Was versuchen Sie zu erreichen?",
@@ -1070,7 +1070,7 @@
 	"Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text. (Default: 0.9)": "Funktioniert zusammen mit top-k. Ein höherer Wert (z.B. 0,95) führt zu vielfältigerem Text, während ein niedrigerer Wert (z.B. 0,5) fokussierteren und konservativeren Text erzeugt. (Standard: 0,9)",
 	"Workspace": "Arbeitsbereich",
 	"Workspace Permissions": "Arbeitsbereichsberechtigungen",
-	"Write": "",
+	"Write": "Schreiben",
 	"Write a prompt suggestion (e.g. Who are you?)": "Schreiben Sie einen Promptvorschlag (z. B. Wer sind Sie?)",
 	"Write a summary in 50 words that summarizes [topic or keyword].": "Schreibe eine kurze Zusammenfassung in 50 Wörtern, die [Thema oder Schlüsselwort] zusammenfasst.",
 	"Write something...": "Schreiben Sie etwas...",
@@ -1080,8 +1080,8 @@
 	"You can only chat with a maximum of {{maxCount}} file(s) at a time.": "Sie können nur mit maximal {{maxCount}} Datei(en) gleichzeitig chatten.",
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Personalisieren Sie Interaktionen mit LLMs, indem Sie über die Schaltfläche \"Verwalten\" Erinnerungen hinzufügen.",
 	"You cannot upload an empty file.": "Sie können keine leere Datei hochladen.",
-	"You do not have permission to access this feature.": "",
-	"You do not have permission to upload files": "",
+	"You do not have permission to access this feature.": "Sie haben keine Berechtigung, auf diese Funktion zuzugreifen.",
+	"You do not have permission to upload files": "Sie haben keine Berechtigung, Dateien hochzuladen",
 	"You do not have permission to upload files.": "Sie haben keine Berechtigung zum Hochladen von Dateien.",
 	"You have no archived conversations.": "Du hast keine archivierten Unterhaltungen.",
 	"You have shared this chat": "Sie haben diese Unterhaltung geteilt",

+ 1 - 0
src/lib/i18n/locales/en-US/translation.json

@@ -944,6 +944,7 @@
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "",
 	"Thorough explanation": "",
 	"Thought for {{DURATION}}": "",
+	"Thought for {{DURATION}} seconds": "",
 	"Tika": "",
 	"Tika Server URL required.": "",
 	"Tiktoken": "",

+ 11 - 10
src/lib/i18n/locales/zh-CN/translation.json

@@ -63,9 +63,9 @@
 	"Allowed Endpoints": "允许的端点",
 	"Already have an account?": "已经拥有账号了?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "top_p的替代方法,目标是在质量和多样性之间取得平衡。参数p表示一个token相对于最有可能的token所需的最低概率。比如,当p=0.05且最有可能的token概率为0.9时,概率低于0.045的logits会被排除。(默认值:0.0)",
-	"Always": "",
+	"Always": "保持",
 	"Amazing": "很棒",
-	"an assistant": "AI模型",
+	"an assistant": "一个助手",
 	"Analyzed": "已分析",
 	"Analyzing...": "正在分析...",
 	"and": "和",
@@ -183,8 +183,8 @@
 	"ComfyUI API Key": "ComfyUI API 密钥",
 	"ComfyUI Base URL": "ComfyUI 基础地址",
 	"ComfyUI Base URL is required.": "ComfyUI 基础地址为必需填写。",
-	"ComfyUI Workflow": "ComfyUI Workflow",
-	"ComfyUI Workflow Nodes": "ComfyUI Workflow 节点",
+	"ComfyUI Workflow": "ComfyUI 工作流",
+	"ComfyUI Workflow Nodes": "ComfyUI 工作流节点",
 	"Command": "命令",
 	"Completions": "续写",
 	"Concurrent Requests": "并发请求",
@@ -522,8 +522,8 @@
 	"Import Prompts": "导入提示词",
 	"Import Tools": "导入工具",
 	"Include": "包括",
-	"Include `--api-auth` flag when running stable-diffusion-webui": "运行 stable-diffusion-webui 时包含 `--api-auth` 标志",
-	"Include `--api` flag when running stable-diffusion-webui": "运行 stable-diffusion-webui 时包含 `--api` 标志",
+	"Include `--api-auth` flag when running stable-diffusion-webui": "运行 stable-diffusion-webui 时包含 `--api-auth` 参数",
+	"Include `--api` flag when running stable-diffusion-webui": "运行 stable-diffusion-webui 时包含 `--api` 参数",
 	"Influences how quickly the algorithm responds to feedback from the generated text. A lower learning rate will result in slower adjustments, while a higher learning rate will make the algorithm more responsive. (Default: 0.1)": "影响算法对生成文本反馈的响应速度。较低的学习率会导致调整速度较慢,而较高的学习率会使算法更具响应性。(默认值:0.1)",
 	"Info": "信息",
 	"Input commands": "输入命令",
@@ -750,7 +750,7 @@
 	"RAG Template": "RAG 提示词模板",
 	"Rating": "评价",
 	"Re-rank models by topic similarity": "根据主题相似性对模型重新排序",
-	"Read": "读",
+	"Read": "读",
 	"Read Aloud": "朗读",
 	"Reasoning Effort": "推理努力",
 	"Record voice": "录音",
@@ -799,10 +799,10 @@
 	"Scroll to bottom when switching between branches": "在分支间切换时滚动到底部",
 	"Search": "搜索",
 	"Search a model": "搜索模型",
-	"Search Base": "Search Base",
+	"Search Base": "搜索库",
 	"Search Chats": "搜索对话",
 	"Search Collection": "搜索内容",
-	"Search Filters": "Search Filters",
+	"Search Filters": "搜索过滤器",
 	"search for tags": "搜索标签",
 	"Search Functions": "搜索函数",
 	"Search Knowledge": "搜索知识",
@@ -943,7 +943,8 @@
 	"This will delete all models including custom models and cannot be undone.": "这将删除所有模型,包括自定义模型,且无法撤销。",
 	"This will reset the knowledge base and sync all files. Do you wish to continue?": "这将重置知识库并替换所有文件为目录下文件。确认继续?",
 	"Thorough explanation": "解释较为详细",
-	"Thought for {{DURATION}}": "思考时间 {{DURATION}}",
+	"Thought for {{DURATION}}": "已推理 持续 {{DURATION}}",
+	"Thought for {{DURATION}} seconds": "已推理 持续 {{DURATION}} 秒",
 	"Tika": "Tika",
 	"Tika Server URL required.": "请输入 Tika 服务器地址。",
 	"Tiktoken": "Tiktoken",

+ 19 - 19
src/lib/i18n/locales/zh-TW/translation.json

@@ -63,11 +63,11 @@
 	"Allowed Endpoints": "允許的端點",
 	"Already have an account?": "已經有帳號了嗎?",
 	"Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "作為 top_p 的替代方案,旨在確保質量和多樣性的平衡。相對於最可能的 token 機率而言,參數 p 代表一個 token 被考慮在内的最低機率。例如,當 p=0.05 且最可能的 token 機率為 0.9 時,數值低於 0.045 的對數機率會被過濾掉。(預設值:0.0)",
-	"Always": "",
+	"Always": "總是",
 	"Amazing": "很棒",
 	"an assistant": "一位助手",
-	"Analyzed": "",
-	"Analyzing...": "",
+	"Analyzed": "分析完畢",
+	"Analyzing...": "分析中……",
 	"and": "和",
 	"and {{COUNT}} more": "和另外 {{COUNT}} 個",
 	"and create a new shared link.": "並建立新的共用連結。",
@@ -172,11 +172,11 @@
 	"Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "剪貼簿寫入權限遭拒。請檢查您的瀏覽器設定,授予必要的存取權限。",
 	"Clone": "複製",
 	"Clone Chat": "複製對話",
-	"Clone of {{TITLE}}": "",
+	"Clone of {{TITLE}}": "{{TITLE}} 的副本",
 	"Close": "關閉",
 	"Code execution": "程式碼執行",
 	"Code formatted successfully": "程式碼格式化成功",
-	"Code Interpreter": "",
+	"Code Interpreter": "程式碼解釋器",
 	"Collection": "收藏",
 	"Color": "顏色",
 	"ComfyUI": "ComfyUI",
@@ -238,7 +238,7 @@
 	"Default": "預設",
 	"Default (Open AI)": "預設 (OpenAI)",
 	"Default (SentenceTransformers)": "預設 (SentenceTransformers)",
-	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "",
+	"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "預設模式透過在執行前呼叫工具一次,來與更廣泛的模型相容。原生模式則利用模型內建的工具呼叫能力,但需要模型本身就支援此功能。",
 	"Default Model": "預設模型",
 	"Default model updated": "預設模型已更新",
 	"Default Models": "預設模型",
@@ -349,7 +349,7 @@
 	"Enter Chunk Overlap": "輸入區塊重疊",
 	"Enter Chunk Size": "輸入區塊大小",
 	"Enter description": "輸入描述",
-	"Enter Exa API Key": "",
+	"Enter Exa API Key": "輸入 Exa API 金鑰",
 	"Enter Github Raw URL": "輸入 GitHub Raw URL",
 	"Enter Google PSE API Key": "輸入 Google PSE API 金鑰",
 	"Enter Google PSE Engine Id": "輸入 Google PSE 引擎 ID",
@@ -398,14 +398,14 @@
 	"Error accessing Google Drive: {{error}}": "存取 Google Drive 時發生錯誤:{{error}}",
 	"Error uploading file: {{error}}": "上傳檔案時發生錯誤:{{error}}",
 	"Evaluations": "評估",
-	"Exa API Key": "",
+	"Exa API Key": "Exa API 金鑰",
 	"Example: (&(objectClass=inetOrgPerson)(uid=%s))": "範例:(&(objectClass=inetOrgPerson)(uid=%s))",
 	"Example: ALL": "範例:ALL",
 	"Example: mail": "範例:mail",
 	"Example: ou=users,dc=foo,dc=example": "範例:ou=users,dc=foo,dc=example",
 	"Example: sAMAccountName or uid or userPrincipalName": "範例:sAMAccountName 或 uid 或 userPrincipalName",
 	"Exclude": "排除",
-	"Execute code for analysis": "",
+	"Execute code for analysis": "執行程式碼以進行分析",
 	"Experimental": "實驗性功能",
 	"Explore the cosmos": "探索宇宙",
 	"Export": "匯出",
@@ -458,7 +458,7 @@
 	"Format your variables using brackets like this:": "使用方括號格式化您的變數,如下所示:",
 	"Frequency Penalty": "頻率懲罰",
 	"Function": "函式",
-	"Function Calling": "",
+	"Function Calling": "函式呼叫",
 	"Function created successfully": "成功建立函式",
 	"Function deleted successfully": "成功刪除函式",
 	"Function Description": "函式描述",
@@ -473,7 +473,7 @@
 	"Functions imported successfully": "成功匯入函式",
 	"General": "一般",
 	"General Settings": "一般設定",
-	"Generate an image": "",
+	"Generate an image": "產生圖片",
 	"Generate Image": "產生圖片",
 	"Generating search query": "正在產生搜尋查詢",
 	"Get started": "開始使用",
@@ -630,7 +630,7 @@
 	"More": "更多",
 	"Name": "名稱",
 	"Name your knowledge base": "命名您的知識庫",
-	"Native": "",
+	"Native": "原生",
 	"New Chat": "新增對話",
 	"New Folder": "新增資料夾",
 	"New Password": "新密碼",
@@ -725,7 +725,7 @@
 	"Please enter a prompt": "請輸入提示詞",
 	"Please fill in all fields.": "請填寫所有欄位。",
 	"Please select a model first.": "請先選擇型號。",
-	"Please select a model.": "",
+	"Please select a model.": "請選擇一個模型。",
 	"Please select a reason": "請選擇原因",
 	"Port": "連接埠",
 	"Positive attitude": "積極的態度",
@@ -734,7 +734,7 @@
 	"Previous 30 days": "過去 30 天",
 	"Previous 7 days": "過去 7 天",
 	"Profile Image": "個人檔案圖片",
-	"Prompt": "",
+	"Prompt": "提示詞",
 	"Prompt (e.g. Tell me a fun fact about the Roman Empire)": "提示詞(例如:告訴我關於羅馬帝國的一些趣事)",
 	"Prompt Content": "提示詞內容",
 	"Prompt created successfully": "提示詞建立成功",
@@ -810,7 +810,7 @@
 	"Search options": "搜尋選項",
 	"Search Prompts": "搜尋提示詞",
 	"Search Result Count": "搜尋結果數量",
-	"Search the internet": "",
+	"Search the internet": "搜尋網際網路",
 	"Search Tools": "搜尋工具",
 	"SearchApi API Key": "SearchApi API 金鑰",
 	"SearchApi Engine": "SearchApi 引擎",
@@ -980,7 +980,7 @@
 	"Tools": "工具",
 	"Tools Access": "工具存取",
 	"Tools are a function calling system with arbitrary code execution": "工具是一個具有任意程式碼執行功能的函式呼叫系統",
-	"Tools Function Calling Prompt": "",
+	"Tools Function Calling Prompt": "工具函式呼叫提示詞",
 	"Tools have a function calling system that allows arbitrary code execution": "工具具有允許執行任意程式碼的函式呼叫系統",
 	"Tools have a function calling system that allows arbitrary code execution.": "工具具有允許執行任意程式碼的函式呼叫系統。",
 	"Top K": "Top K",
@@ -1051,7 +1051,7 @@
 	"Web Loader Settings": "網頁載入器設定",
 	"Web Search": "網頁搜尋",
 	"Web Search Engine": "網頁搜尋引擎",
-	"Web Search in Chat": "",
+	"Web Search in Chat": "在對話中進行網路搜尋",
 	"Web Search Query Generation": "網頁搜尋查詢生成",
 	"Webhook URL": "Webhook URL",
 	"WebUI Settings": "WebUI 設定",
@@ -1081,8 +1081,8 @@
 	"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "您可以透過下方的「管理」按鈕新增記憶,將您與大型語言模型的互動個人化,讓它們更有幫助並更符合您的需求。",
 	"You cannot upload an empty file.": "您無法上傳空檔案",
 	"You do not have permission to access this feature.": "您沒有權限訪問此功能",
-	"You do not have permission to upload files": "",
-	"You do not have permission to upload files.": "您沒有權限上傳檔案",
+	"You do not have permission to upload files": "您沒有權限上傳檔案",
+	"You do not have permission to upload files.": "您沒有權限上傳檔案",
 	"You have no archived conversations.": "您沒有已封存的對話。",
 	"You have shared this chat": "您已分享此對話",
 	"You're a helpful assistant.": "您是一位樂於助人的助手。",

+ 1 - 0
src/lib/stores/index.ts

@@ -206,6 +206,7 @@ type Config = {
 		enable_admin_export: boolean;
 		enable_admin_chat_access: boolean;
 		enable_community_sharing: boolean;
+		enable_autocomplete_generation: boolean;
 	};
 	oauth: {
 		providers: {

+ 1 - 1
src/routes/(app)/admin/+layout.svelte

@@ -26,7 +26,7 @@
 
 {#if loaded}
 	<div
-		class=" flex flex-col w-full min-h-screen max-h-screen transition-width duration-200 ease-in-out {$showSidebar
+		class=" flex flex-col w-full h-screen max-h-[100dvh] transition-width duration-200 ease-in-out {$showSidebar
 			? 'md:max-w-[calc(100%-260px)]'
 			: ''} max-w-full"
 	>

+ 14 - 1
vite.config.ts

@@ -1,6 +1,8 @@
 import { sveltekit } from '@sveltejs/kit/vite';
 import { defineConfig } from 'vite';
 
+import { viteStaticCopy } from 'vite-plugin-static-copy';
+
 // /** @type {import('vite').Plugin} */
 // const viteServerConfig = {
 // 	name: 'log-request-middleware',
@@ -16,7 +18,18 @@ import { defineConfig } from 'vite';
 // };
 
 export default defineConfig({
-	plugins: [sveltekit()],
+	plugins: [
+		sveltekit(),
+		viteStaticCopy({
+			targets: [
+				{
+					src: 'node_modules/onnxruntime-web/dist/*.jsep.*',
+
+					dest: 'wasm'
+				}
+			]
+		})
+	],
 	define: {
 		APP_VERSION: JSON.stringify(process.env.npm_package_version),
 		APP_BUILD_HASH: JSON.stringify(process.env.APP_BUILD_HASH || 'dev-build')