|
|
from __future__ import annotations |
|
|
|
|
|
|
|
|
|
|
|
import json |
|
|
import os |
|
|
import sys |
|
|
import threading |
|
|
import time |
|
|
from datetime import datetime, timedelta |
|
|
from typing import Any |
|
|
|
|
|
import gradio as gr |
|
|
|
|
|
|
|
|
class RateLimiter: |
|
|
"""Best-effort in-process rate limiter for HTTP-heavy tools.""" |
|
|
|
|
|
def __init__(self, requests_per_minute: int = 30) -> None: |
|
|
self.requests_per_minute = requests_per_minute |
|
|
self._requests: list[datetime] = [] |
|
|
self._lock = threading.Lock() |
|
|
|
|
|
def acquire(self) -> None: |
|
|
now = datetime.now() |
|
|
with self._lock: |
|
|
self._requests = [req for req in self._requests if now - req < timedelta(minutes=1)] |
|
|
if len(self._requests) >= self.requests_per_minute: |
|
|
wait_time = 60 - (now - self._requests[0]).total_seconds() |
|
|
if wait_time > 0: |
|
|
time.sleep(max(1, wait_time)) |
|
|
self._requests.append(now) |
|
|
|
|
|
|
|
|
_search_rate_limiter = RateLimiter(requests_per_minute=20) |
|
|
_fetch_rate_limiter = RateLimiter(requests_per_minute=25) |
|
|
|
|
|
|
|
|
def _truncate_for_log(value: Any, limit: int = 500) -> str: |
|
|
if not isinstance(value, str): |
|
|
value = str(value) |
|
|
if len(value) <= limit: |
|
|
return value |
|
|
return value[: limit - 1] + "…" |
|
|
|
|
|
|
|
|
def _serialize_input(val: Any) -> Any: |
|
|
try: |
|
|
if isinstance(val, (str, int, float, bool)) or val is None: |
|
|
return val |
|
|
if isinstance(val, (list, tuple)): |
|
|
return [_serialize_input(v) for v in list(val)[:10]] + (["…"] if len(val) > 10 else []) |
|
|
if isinstance(val, dict): |
|
|
out: dict[str, Any] = {} |
|
|
for i, (k, v) in enumerate(val.items()): |
|
|
if i >= 12: |
|
|
out["…"] = "…" |
|
|
break |
|
|
out[str(k)] = _serialize_input(v) |
|
|
return out |
|
|
return repr(val)[:120] |
|
|
except Exception: |
|
|
return "<unserializable>" |
|
|
|
|
|
|
|
|
def _log_call_start(func_name: str, **kwargs: Any) -> None: |
|
|
try: |
|
|
compact = {k: _serialize_input(v) for k, v in kwargs.items()} |
|
|
print(f"[TOOL CALL] {func_name} inputs: {json.dumps(compact, ensure_ascii=False)[:800]}", flush=True) |
|
|
except Exception as exc: |
|
|
print(f"[TOOL CALL] {func_name} (failed to log inputs: {exc})", flush=True) |
|
|
|
|
|
|
|
|
def _log_call_end(func_name: str, output_desc: str) -> None: |
|
|
try: |
|
|
print(f"[TOOL RESULT] {func_name} output: {output_desc}", flush=True) |
|
|
except Exception as exc: |
|
|
print(f"[TOOL RESULT] {func_name} (failed to log output: {exc})", flush=True) |
|
|
|
|
|
|
|
|
|
|
|
sys.modules.setdefault("app", sys.modules[__name__]) |
|
|
|
|
|
|
|
|
from Modules.Web_Fetch import build_interface as build_fetch_interface |
|
|
from Modules.Web_Search import build_interface as build_search_interface |
|
|
from Modules.Agent_Terminal import build_interface as build_agent_terminal_interface |
|
|
from Modules.Code_Interpreter import build_interface as build_code_interface |
|
|
from Modules.Memory_Manager import build_interface as build_memory_interface |
|
|
from Modules.Generate_Speech import build_interface as build_speech_interface |
|
|
from Modules.Generate_Image import build_interface as build_image_interface |
|
|
from Modules.Generate_Video import build_interface as build_video_interface |
|
|
from Modules.Deep_Research import build_interface as build_research_interface |
|
|
from Modules.File_System import build_interface as build_fs_interface |
|
|
from Modules.Obsidian_Vault import build_interface as build_obsidian_interface |
|
|
from Modules.Shell_Command import build_interface as build_shell_interface |
|
|
|
|
|
|
|
|
HF_IMAGE_TOKEN = bool(os.getenv("HF_READ_TOKEN")) |
|
|
HF_VIDEO_TOKEN = bool(os.getenv("HF_READ_TOKEN") or os.getenv("HF_TOKEN")) |
|
|
HF_TEXTGEN_TOKEN = bool(os.getenv("HF_READ_TOKEN") or os.getenv("HF_TOKEN")) |
|
|
|
|
|
|
|
|
CSS_STYLES = """ |
|
|
/* Style only the top-level app title to avoid affecting headings elsewhere */ |
|
|
.app-title { |
|
|
text-align: center; |
|
|
/* Ensure main title appears first, then our two subtitle lines */ |
|
|
display: grid; |
|
|
justify-items: center; |
|
|
} |
|
|
.app-title::after { |
|
|
grid-row: 2; |
|
|
content: "General purpose tools useful for any agent."; |
|
|
display: block; |
|
|
font-size: 1rem; |
|
|
font-weight: 400; |
|
|
opacity: 0.9; |
|
|
margin-top: 2px; |
|
|
white-space: pre-wrap; |
|
|
} |
|
|
|
|
|
/* Sidebar Container */ |
|
|
.app-sidebar { |
|
|
background-color: rgba(255, 255, 255, 0.02) !important; |
|
|
border-right: 1px solid rgba(255, 255, 255, 0.08) !important; |
|
|
} |
|
|
@media (prefers-color-scheme: light) { |
|
|
.app-sidebar { |
|
|
background-color: rgba(0, 0, 0, 0.02) !important; |
|
|
border-right: 1px solid rgba(0, 0, 0, 0.08) !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Historical safeguard: if any h1 appears inside tabs, don't attach pseudo content */ |
|
|
.gradio-container [role=\"tabpanel\"] h1::before, |
|
|
.gradio-container [role=\"tabpanel\"] h1::after { |
|
|
content: none !important; |
|
|
} |
|
|
|
|
|
/* Information accordion - modern info cards */ |
|
|
.info-accordion { |
|
|
margin: 8px 0 2px; |
|
|
} |
|
|
.info-grid { |
|
|
display: grid; |
|
|
gap: 12px; |
|
|
/* Force a 2x2 layout on medium+ screens */ |
|
|
grid-template-columns: repeat(2, minmax(0, 1fr)); |
|
|
align-items: stretch; |
|
|
} |
|
|
/* On narrow screens, stack into a single column */ |
|
|
@media (max-width: 800px) { |
|
|
.info-grid { |
|
|
grid-template-columns: 1fr; |
|
|
} |
|
|
} |
|
|
.info-card { |
|
|
display: flex; |
|
|
gap: 14px; |
|
|
padding: 14px 16px; |
|
|
border: 1px solid rgba(255, 255, 255, 0.08); |
|
|
background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)); |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
backdrop-filter: blur(2px); |
|
|
} |
|
|
.info-card::before { |
|
|
content: ""; |
|
|
position: absolute; |
|
|
inset: 0; |
|
|
border-radius: 12px; |
|
|
pointer-events: none; |
|
|
background: linear-gradient(90deg, rgba(99,102,241,0.06), rgba(59,130,246,0.05)); |
|
|
} |
|
|
.info-card__icon { |
|
|
font-size: 24px; |
|
|
flex: 0 0 28px; |
|
|
line-height: 1; |
|
|
filter: saturate(1.1); |
|
|
} |
|
|
.info-card__body { |
|
|
min-width: 0; |
|
|
} |
|
|
.info-card__body h3 { |
|
|
margin: 0 0 6px; |
|
|
font-size: 1.05rem; |
|
|
} |
|
|
.info-card__body p { |
|
|
margin: 6px 0; |
|
|
opacity: 0.95; |
|
|
} |
|
|
/* Readable code blocks inside info cards */ |
|
|
.info-card pre { |
|
|
margin: 8px 0; |
|
|
padding: 10px 12px; |
|
|
background: rgba(20, 20, 30, 0.55); |
|
|
border: 1px solid rgba(255, 255, 255, 0.08); |
|
|
border-radius: 10px; |
|
|
overflow-x: auto; |
|
|
white-space: pre; |
|
|
} |
|
|
.info-card code { |
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; |
|
|
font-size: 0.95em; |
|
|
} |
|
|
.info-card pre code { |
|
|
display: block; |
|
|
} |
|
|
.info-card p { |
|
|
word-wrap: break-word; |
|
|
overflow-wrap: break-word; |
|
|
} |
|
|
.info-card p code { |
|
|
word-break: break-all; |
|
|
} |
|
|
.info-list { |
|
|
margin: 6px 0 0 18px; |
|
|
padding: 0; |
|
|
} |
|
|
.info-hint { |
|
|
margin-top: 8px; |
|
|
font-size: 0.9em; |
|
|
opacity: 0.9; |
|
|
} |
|
|
|
|
|
/* Light theme adjustments */ |
|
|
@media (prefers-color-scheme: light) { |
|
|
.info-card { |
|
|
border-color: rgba(0, 0, 0, 0.08); |
|
|
background: linear-gradient(180deg, rgba(255,255,255,0.95), rgba(255,255,255,0.9)); |
|
|
} |
|
|
.info-card::before { |
|
|
background: linear-gradient(90deg, rgba(99,102,241,0.08), rgba(59,130,246,0.06)); |
|
|
} |
|
|
.info-card pre { |
|
|
background: rgba(245, 246, 250, 0.95); |
|
|
border-color: rgba(0, 0, 0, 0.08); |
|
|
} |
|
|
} |
|
|
|
|
|
/* Sidebar Navigation - styled like the previous tabs */ |
|
|
.sidebar-nav { |
|
|
background: transparent !important; |
|
|
border: none !important; |
|
|
padding: 0 !important; |
|
|
} |
|
|
.sidebar-nav .form { |
|
|
gap: 8px !important; |
|
|
display: flex !important; |
|
|
flex-direction: column !important; |
|
|
border: none !important; |
|
|
background: transparent !important; |
|
|
} |
|
|
.sidebar-nav label { |
|
|
display: flex !important; |
|
|
align-items: center !important; |
|
|
padding: 10px 12px !important; |
|
|
border-radius: 10px !important; |
|
|
border: 1px solid rgba(255, 255, 255, 0.08) !important; |
|
|
background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)) !important; |
|
|
transition: background .2s ease, border-color .2s ease, box-shadow .2s ease, transform .06s ease !important; |
|
|
cursor: pointer !important; |
|
|
margin-bottom: 0 !important; |
|
|
width: 100% !important; |
|
|
justify-content: flex-start !important; |
|
|
text-align: left !important; |
|
|
} |
|
|
.sidebar-nav label:hover { |
|
|
border-color: rgba(99,102,241,0.28) !important; |
|
|
background: linear-gradient(180deg, rgba(99,102,241,0.10), rgba(59,130,246,0.08)) !important; |
|
|
} |
|
|
/* Selected state - Gradio adds 'selected' class to the label in some versions, or we check input:checked */ |
|
|
.sidebar-nav label.selected { |
|
|
border-color: rgba(99,102,241,0.35) !important; |
|
|
box-shadow: inset 0 0 0 1px rgba(99,102,241,0.25), 0 1px 2px rgba(0,0,0,0.25) !important; |
|
|
background: linear-gradient(180deg, rgba(99,102,241,0.18), rgba(59,130,246,0.14)) !important; |
|
|
color: rgba(255, 255, 255, 0.95) !important; |
|
|
} |
|
|
|
|
|
/* Light theme adjustments for sidebar */ |
|
|
@media (prefers-color-scheme: light) { |
|
|
.sidebar-nav label { |
|
|
border-color: rgba(0, 0, 0, 0.08) !important; |
|
|
background: linear-gradient(180deg, rgba(255,255,255,0.95), rgba(255,255,255,0.90)) !important; |
|
|
color: rgba(0, 0, 0, 0.85) !important; |
|
|
} |
|
|
.sidebar-nav label:hover { |
|
|
border-color: rgba(99,102,241,0.25) !important; |
|
|
background: linear-gradient(180deg, rgba(99,102,241,0.08), rgba(59,130,246,0.06)) !important; |
|
|
} |
|
|
.sidebar-nav label.selected { |
|
|
border-color: rgba(99,102,241,0.35) !important; |
|
|
background: linear-gradient(180deg, rgba(99,102,241,0.16), rgba(59,130,246,0.12)) !important; |
|
|
color: rgba(0, 0, 0, 0.85) !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Hide scrollbars/arrows that can appear on the description block in some browsers */ |
|
|
article.prose, .prose, .gr-prose { |
|
|
overflow: visible !important; |
|
|
max-height: none !important; |
|
|
-ms-overflow-style: none !important; /* IE/Edge */ |
|
|
scrollbar-width: none !important; /* Firefox */ |
|
|
} |
|
|
article.prose::-webkit-scrollbar, |
|
|
.prose::-webkit-scrollbar, |
|
|
.gr-prose::-webkit-scrollbar { |
|
|
display: none !important; /* Chrome/Safari */ |
|
|
} |
|
|
|
|
|
/* Fix for white background on single-line inputs in dark mode */ |
|
|
.gradio-container input[type="text"], |
|
|
.gradio-container input[type="password"], |
|
|
.gradio-container input[type="number"], |
|
|
.gradio-container input[type="email"] { |
|
|
background-color: var(--input-background-fill) !important; |
|
|
color: var(--body-text-color) !important; |
|
|
} |
|
|
|
|
|
/* Custom glossy purple styling for primary action buttons */ |
|
|
.gradio-container button.primary { |
|
|
border: 1px solid rgba(99, 102, 241, 0.35) !important; |
|
|
background: linear-gradient(180deg, rgba(99, 102, 241, 0.25), rgba(59, 130, 246, 0.20)) !important; |
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), 0 2px 4px rgba(0, 0, 0, 0.15) !important; |
|
|
color: rgba(255, 255, 255, 0.95) !important; |
|
|
transition: background .2s ease, border-color .2s ease, box-shadow .2s ease, transform .06s ease !important; |
|
|
} |
|
|
.gradio-container button.primary:hover { |
|
|
border-color: rgba(99, 102, 241, 0.5) !important; |
|
|
background: linear-gradient(180deg, rgba(99, 102, 241, 0.35), rgba(59, 130, 246, 0.28)) !important; |
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 3px 6px rgba(0, 0, 0, 0.2) !important; |
|
|
} |
|
|
.gradio-container button.primary:active { |
|
|
transform: scale(0.98) !important; |
|
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2), 0 1px 2px rgba(0, 0, 0, 0.1) !important; |
|
|
} |
|
|
@media (prefers-color-scheme: light) { |
|
|
.gradio-container button.primary { |
|
|
border-color: rgba(99, 102, 241, 0.4) !important; |
|
|
background: linear-gradient(180deg, rgba(99, 102, 241, 0.85), rgba(79, 70, 229, 0.75)) !important; |
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 2px 4px rgba(0, 0, 0, 0.12) !important; |
|
|
color: rgba(255, 255, 255, 0.98) !important; |
|
|
} |
|
|
.gradio-container button.primary:hover { |
|
|
background: linear-gradient(180deg, rgba(99, 102, 241, 0.95), rgba(79, 70, 229, 0.85)) !important; |
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 3px 6px rgba(0, 0, 0, 0.15) !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Hide the actual tabs since we use the sidebar to control them */ |
|
|
.hidden-tabs .tab-nav, |
|
|
.hidden-tabs [role="tablist"] { |
|
|
display: none !important; |
|
|
} |
|
|
/* Hide the entire first row of the tabs container (contains tab buttons + overflow) */ |
|
|
.hidden-tabs > div:first-child { |
|
|
display: none !important; |
|
|
} |
|
|
/* Ensure audio component buttons remain visible - they're inside tab panels, not the first row */ |
|
|
.hidden-tabs [role="tabpanel"] button { |
|
|
display: inline-flex !important; |
|
|
} |
|
|
|
|
|
/* Custom scrollbar styling */ |
|
|
* { |
|
|
scrollbar-width: thin; |
|
|
scrollbar-color: rgba(61, 212, 159, 0.4) rgba(255, 255, 255, 0.05); |
|
|
} |
|
|
*::-webkit-scrollbar { |
|
|
width: 8px; |
|
|
height: 8px; |
|
|
} |
|
|
*::-webkit-scrollbar-track { |
|
|
background: rgba(255, 255, 255, 0.05); |
|
|
border-radius: 4px; |
|
|
} |
|
|
*::-webkit-scrollbar-thumb { |
|
|
background: linear-gradient(180deg, rgba(61, 212, 159, 0.5), rgba(17, 186, 136, 0.4)); |
|
|
border-radius: 4px; |
|
|
border: 1px solid rgba(119, 247, 209, 0.2); |
|
|
} |
|
|
*::-webkit-scrollbar-thumb:hover { |
|
|
background: linear-gradient(180deg, rgba(85, 250, 192, 0.7), rgba(65, 184, 131, 0.6)); |
|
|
} |
|
|
*::-webkit-scrollbar-corner { |
|
|
background: rgba(255, 255, 255, 0.05); |
|
|
} |
|
|
@media (prefers-color-scheme: light) { |
|
|
* { |
|
|
scrollbar-color: rgba(61, 212, 159, 0.4) rgba(0, 0, 0, 0.05); |
|
|
} |
|
|
*::-webkit-scrollbar-track { |
|
|
background: rgba(0, 0, 0, 0.05); |
|
|
} |
|
|
*::-webkit-scrollbar-thumb { |
|
|
background: linear-gradient(180deg, rgba(61, 212, 159, 0.5), rgba(17, 186, 136, 0.4)); |
|
|
border-color: rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
*::-webkit-scrollbar-thumb:hover { |
|
|
background: linear-gradient(180deg, rgba(85, 250, 192, 0.7), rgba(65, 184, 131, 0.6)); |
|
|
} |
|
|
*::-webkit-scrollbar-corner { |
|
|
background: rgba(0, 0, 0, 0.05); |
|
|
} |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
fetch_interface = build_fetch_interface() |
|
|
web_search_interface = build_search_interface() |
|
|
agent_terminal_interface = build_agent_terminal_interface() |
|
|
code_interface = build_code_interface() |
|
|
memory_interface = build_memory_interface() |
|
|
kokoro_interface = build_speech_interface() |
|
|
image_generation_interface = build_image_interface() |
|
|
video_generation_interface = build_video_interface() |
|
|
deep_research_interface = build_research_interface() |
|
|
fs_interface = build_fs_interface() |
|
|
shell_interface = build_shell_interface() |
|
|
obsidian_interface = build_obsidian_interface() |
|
|
|
|
|
_interfaces = [ |
|
|
agent_terminal_interface, |
|
|
fetch_interface, |
|
|
web_search_interface, |
|
|
code_interface, |
|
|
shell_interface, |
|
|
fs_interface, |
|
|
obsidian_interface, |
|
|
memory_interface, |
|
|
kokoro_interface, |
|
|
image_generation_interface, |
|
|
video_generation_interface, |
|
|
deep_research_interface, |
|
|
] |
|
|
_tab_names = [ |
|
|
"Agent Terminal", |
|
|
"Web Fetch", |
|
|
"Web Search", |
|
|
"Code Interpreter", |
|
|
"Shell Command", |
|
|
"File System", |
|
|
"Obsidian Vault", |
|
|
"Memory Manager", |
|
|
"Generate Speech", |
|
|
"Generate Image", |
|
|
"Generate Video", |
|
|
"Deep Research", |
|
|
] |
|
|
|
|
|
with gr.Blocks(title="Nymbo/Tools MCP") as demo: |
|
|
|
|
|
with gr.Sidebar(width=300, elem_classes="app-sidebar"): |
|
|
gr.Markdown("## Nymbo/Tools MCP\n<p style='font-size: 0.7rem; opacity: 0.85; margin-top: 2px;'>General purpose tools useful for any agent.</p>\n<code style='font-size: 0.7rem; word-break: break-all;'>https://nymbo.net/gradio_api/mcp/</code>") |
|
|
|
|
|
with gr.Accordion("Information", open=False): |
|
|
gr.HTML( |
|
|
""" |
|
|
<div class="info-accordion"> |
|
|
<div class="info-grid" style="grid-template-columns: 1fr;"> |
|
|
<section class="info-card"> |
|
|
<div class="info-card__body"> |
|
|
<h3>Connecting from an MCP Client</h3> |
|
|
<p> |
|
|
This Space also runs as a Model Context Protocol (MCP) server. Point your client to: |
|
|
<br/> |
|
|
<code>https://nymbo.net/gradio_api/mcp/</code> |
|
|
</p> |
|
|
<p>Example client configuration:</p> |
|
|
<pre><code class="language-json">{ |
|
|
"mcpServers": { |
|
|
"nymbo-tools": { |
|
|
"url": "https://nymbo.net/gradio_api/mcp/" |
|
|
} |
|
|
} |
|
|
}</code></pre> |
|
|
<p>Run the following commands in sequence to run the server locally:</p> |
|
|
<pre><code>git clone https://huggingface.co/spaces/Nymbo/Tools |
|
|
cd Tools |
|
|
python -m venv env |
|
|
source env/bin/activate |
|
|
pip install -r requirements.txt |
|
|
python app.py</code></pre> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
<section class="info-card"> |
|
|
<div class="info-card__body"> |
|
|
<h3>Enable Image Gen, Video Gen, and Deep Research</h3> |
|
|
<p> |
|
|
The <code>Generate_Image</code>, <code>Generate_Video</code>, and <code>Deep_Research</code> tools require a |
|
|
<code>HF_READ_TOKEN</code> set as a secret or environment variable. |
|
|
</p> |
|
|
<ul class="info-list"> |
|
|
<li>Duplicate this Space and add a HF token with model read access.</li> |
|
|
<li>Or run locally with <code>HF_READ_TOKEN</code> in your environment.</li> |
|
|
</ul> |
|
|
<div class="info-hint"> |
|
|
MCP clients can see these tools even without tokens, but calls will fail until a valid token is provided. |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
<section class="info-card"> |
|
|
<div class="info-card__body"> |
|
|
<h3>Persistent Memories and Files</h3> |
|
|
<p> |
|
|
In this public demo, memories and files created with the <code>Memory_Manager</code> and <code>File_System</code> are stored in the Space's running container and are cleared when the Space restarts. Content is visible to everyone—avoid personal data. |
|
|
</p> |
|
|
<p> |
|
|
When running locally, memories are saved to <code>memories.json</code> at the repo root for privacy, and files are saved to the <code>Tools/Filesystem</code> directory on disk. |
|
|
</p> |
|
|
</div> |
|
|
</section> |
|
|
|
|
|
<section class="info-card"> |
|
|
<div class="info-card__body"> |
|
|
<h3>Tool Notes & Kokoro Voice Legend</h3> |
|
|
<p><strong>No authentication required for:</strong></p> |
|
|
<ul class="info-list"> |
|
|
<li><code>Web_Fetch</code></li> |
|
|
<li><code>Web_Search</code></li> |
|
|
<li><code>Agent_Terminal</code></li> |
|
|
<li><code>Code_Interpreter</code></li> |
|
|
<li><code>Memory_Manager</code></li> |
|
|
<li><code>Generate_Speech</code></li> |
|
|
<li><code>File_System</code></li> |
|
|
<li><code>Shell_Command</code></li> |
|
|
</ul> |
|
|
<p><strong>Kokoro voice prefixes</strong></p> |
|
|
<table style="width:100%; border-collapse:collapse; font-size:0.9em; margin-top:8px;"> |
|
|
<thead> |
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.15);"> |
|
|
<th style="padding:6px 8px; text-align:left;">Accent</th> |
|
|
<th style="padding:6px 8px; text-align:center;">Female</th> |
|
|
<th style="padding:6px 8px; text-align:center;">Male</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody> |
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> |
|
|
<td style="padding:6px 8px; font-weight:600;">American</td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>af</code></td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>am</code></td> |
|
|
</tr> |
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> |
|
|
<td style="padding:6px 8px; font-weight:600;">British</td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>bf</code></td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>bm</code></td> |
|
|
</tr> |
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> |
|
|
<td style="padding:6px 8px; font-weight:600;">European</td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>ef</code></td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>em</code></td> |
|
|
</tr> |
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> |
|
|
<td style="padding:6px 8px; font-weight:600;">French</td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>ff</code></td> |
|
|
<td style="padding:6px 8px; text-align:center;">—</td> |
|
|
</tr> |
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> |
|
|
<td style="padding:6px 8px; font-weight:600;">Hindi</td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>hf</code></td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>hm</code></td> |
|
|
</tr> |
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> |
|
|
<td style="padding:6px 8px; font-weight:600;">Italian</td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>if</code></td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>im</code></td> |
|
|
</tr> |
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> |
|
|
<td style="padding:6px 8px; font-weight:600;">Japanese</td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>jf</code></td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>jm</code></td> |
|
|
</tr> |
|
|
<tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> |
|
|
<td style="padding:6px 8px; font-weight:600;">Portuguese</td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>pf</code></td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>pm</code></td> |
|
|
</tr> |
|
|
<tr> |
|
|
<td style="padding:6px 8px; font-weight:600;">Chinese</td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>zf</code></td> |
|
|
<td style="padding:6px 8px; text-align:center;"><code>zm</code></td> |
|
|
</tr> |
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</section> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
) |
|
|
|
|
|
gr.Markdown("### Tools") |
|
|
tool_selector = gr.Radio( |
|
|
choices=_tab_names, |
|
|
value=_tab_names[0], |
|
|
label="Select Tool", |
|
|
show_label=False, |
|
|
container=False, |
|
|
elem_classes="sidebar-nav" |
|
|
) |
|
|
|
|
|
with gr.Tabs(elem_classes="hidden-tabs", selected=_tab_names[0]) as tool_tabs: |
|
|
for name, interface in zip(_tab_names, _interfaces): |
|
|
with gr.TabItem(label=name, id=name, elem_id=f"tab-{name}"): |
|
|
interface.render() |
|
|
|
|
|
|
|
|
tool_selector.change( |
|
|
fn=None, |
|
|
inputs=tool_selector, |
|
|
outputs=None, |
|
|
js="(selected_tool) => { const buttons = document.querySelectorAll('.hidden-tabs button'); buttons.forEach(btn => { if (btn.innerText.trim() === selected_tool) { btn.click(); } }); }" |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(mcp_server=True, theme="Nymbo/Nymbo_Theme", css=CSS_STYLES, ssr_mode=False) |