Update app.py
Browse files
app.py
CHANGED
@@ -1,12 +1,11 @@
|
|
1 |
# Purpose: One Space that offers up to seven tools/tabs (all exposed as MCP tools):
|
2 |
# 1) Fetch — convert webpages to clean Markdown format
|
3 |
-
# 2)
|
4 |
-
# 3)
|
5 |
-
# 4)
|
6 |
# 5) Memory Manager — lightweight JSON-based local memory store
|
7 |
-
# 6) Image
|
8 |
-
# 7) Video
|
9 |
-
# 8) Deep Research
|
10 |
|
11 |
from __future__ import annotations
|
12 |
|
@@ -349,7 +348,7 @@ def _truncate_markdown(markdown: str, max_chars: int) -> Tuple[str, Dict[str, an
|
|
349 |
return truncated + truncation_notice, metadata
|
350 |
|
351 |
|
352 |
-
def
|
353 |
url: Annotated[str, "The absolute URL to fetch (must return HTML)."],
|
354 |
max_chars: Annotated[int, "Maximum characters to return (0 = no limit, full page content)."] = 3000,
|
355 |
strip_selectors: Annotated[str, "CSS selectors to remove (comma-separated, e.g., '.header, .footer, nav')."] = "",
|
@@ -375,10 +374,10 @@ def Fetch_Webpage( # <-- MCP tool #1 (Fetch)
|
|
375 |
depending on the url_scraper setting. Content is length-limited by max_chars
|
376 |
and includes detailed truncation metadata when content is truncated.
|
377 |
"""
|
378 |
-
_log_call_start("
|
379 |
if not url or not url.strip():
|
380 |
result = "Please enter a valid URL."
|
381 |
-
_log_call_end("
|
382 |
return result
|
383 |
|
384 |
try:
|
@@ -386,14 +385,14 @@ def Fetch_Webpage( # <-- MCP tool #1 (Fetch)
|
|
386 |
resp.raise_for_status()
|
387 |
except requests.exceptions.RequestException as e:
|
388 |
result = f"An error occurred: {e}"
|
389 |
-
_log_call_end("
|
390 |
return result
|
391 |
|
392 |
final_url = str(resp.url)
|
393 |
ctype = resp.headers.get("Content-Type", "")
|
394 |
if "html" not in ctype.lower():
|
395 |
result = f"Unsupported content type for extraction: {ctype or 'unknown'}"
|
396 |
-
_log_call_end("
|
397 |
return result
|
398 |
|
399 |
# Decode to text
|
@@ -419,7 +418,7 @@ def Fetch_Webpage( # <-- MCP tool #1 (Fetch)
|
|
419 |
if offset > 0:
|
420 |
if offset >= len(full_result):
|
421 |
result = f"Offset {offset} exceeds content length ({len(full_result)} characters). Content ends at position {len(full_result)}."
|
422 |
-
_log_call_end("
|
423 |
return result
|
424 |
result = full_result[offset:]
|
425 |
else:
|
@@ -433,12 +432,12 @@ def Fetch_Webpage( # <-- MCP tool #1 (Fetch)
|
|
433 |
metadata["total_chars_estimate"] = len(full_result)
|
434 |
metadata["next_cursor"] = offset + metadata["next_cursor"] if metadata["next_cursor"] else None
|
435 |
|
436 |
-
_log_call_end("
|
437 |
return result
|
438 |
|
439 |
|
440 |
# ============================================
|
441 |
-
# DuckDuckGo
|
442 |
# ============================================
|
443 |
|
444 |
import asyncio
|
@@ -527,7 +526,7 @@ class SlowHost(Exception):
|
|
527 |
def _fetch_page_markdown_fast(url: str, max_chars: int = 3000, timeout: float = 10.0) -> str:
|
528 |
"""Fetch a single URL quickly; raise SlowHost on timeout.
|
529 |
|
530 |
-
Uses a shorter HTTP timeout to detect slow hosts, then reuses
|
531 |
logic for conversion to Markdown. Returns empty string on non-timeout errors.
|
532 |
"""
|
533 |
try:
|
@@ -545,7 +544,7 @@ def _fetch_page_markdown_fast(url: str, max_chars: int = 3000, timeout: float =
|
|
545 |
if "html" not in ctype.lower():
|
546 |
return ""
|
547 |
|
548 |
-
# Decode to text and convert similar to
|
549 |
resp.encoding = resp.encoding or resp.apparent_encoding
|
550 |
html = resp.text
|
551 |
soup = BeautifulSoup(html, "lxml")
|
@@ -672,7 +671,7 @@ def _format_search_result(result: dict, search_type: str, index: int) -> list[st
|
|
672 |
return lines
|
673 |
|
674 |
|
675 |
-
def
|
676 |
query: Annotated[str, "The search query (supports operators like site:, quotes, OR)."],
|
677 |
max_results: Annotated[int, "Number of results to return (1–20)."] = 5,
|
678 |
page: Annotated[int, "Page number for pagination (1-based, each page contains max_results items)."] = 1,
|
@@ -680,7 +679,7 @@ def Search_DuckDuckGo( # <-- MCP tool #2 (DDG Search)
|
|
680 |
offset: Annotated[int, "Result offset to start from (overrides page if > 0, for precise continuation)."] = 0,
|
681 |
) -> str:
|
682 |
"""
|
683 |
-
Run a
|
684 |
|
685 |
Features smart fallback: if 'news' search returns no results, automatically retries with 'text'
|
686 |
search to catch sources like Hacker News that might not appear in news-specific results.
|
@@ -709,10 +708,10 @@ def Search_DuckDuckGo( # <-- MCP tool #2 (DDG Search)
|
|
709 |
If 'news' search fails, results include a note about automatic fallback to 'text' search.
|
710 |
Includes next_offset information for easy continuation.
|
711 |
"""
|
712 |
-
_log_call_start("
|
713 |
if not query or not query.strip():
|
714 |
result = "No search query provided. Please enter a search term."
|
715 |
-
_log_call_end("
|
716 |
return result
|
717 |
|
718 |
# Validate parameters
|
@@ -784,7 +783,7 @@ def Search_DuckDuckGo( # <-- MCP tool #2 (DDG Search)
|
|
784 |
raw = _perform_search(search_type)
|
785 |
except Exception as e:
|
786 |
result = f"Error: {str(e)}"
|
787 |
-
_log_call_end("
|
788 |
return result
|
789 |
|
790 |
# Smart fallback: if news search returns empty and we haven't tried text yet, try text search
|
@@ -801,7 +800,7 @@ def Search_DuckDuckGo( # <-- MCP tool #2 (DDG Search)
|
|
801 |
if not raw:
|
802 |
fallback_note = " (also tried 'text' search as fallback)" if original_search_type == "news" and used_fallback else ""
|
803 |
result = f"No {original_search_type} results found for query: {query}{fallback_note}"
|
804 |
-
_log_call_end("
|
805 |
return result
|
806 |
|
807 |
# Apply pagination by slicing the results
|
@@ -812,7 +811,7 @@ def Search_DuckDuckGo( # <-- MCP tool #2 (DDG Search)
|
|
812 |
result = f"Offset {actual_offset} exceeds available results ({len(raw)} total). Try offset=0 to start from beginning."
|
813 |
else:
|
814 |
result = f"No {original_search_type} results found on page {calculated_page} for query: {query}. Try page 1 or reduce page number."
|
815 |
-
_log_call_end("
|
816 |
return result
|
817 |
|
818 |
# Format results based on search type
|
@@ -854,7 +853,7 @@ def Search_DuckDuckGo( # <-- MCP tool #2 (DDG Search)
|
|
854 |
search_info = f"type={original_search_type}"
|
855 |
if used_fallback:
|
856 |
search_info += "→text"
|
857 |
-
_log_call_end("
|
858 |
return result
|
859 |
|
860 |
|
@@ -862,7 +861,7 @@ def Search_DuckDuckGo( # <-- MCP tool #2 (DDG Search)
|
|
862 |
# Code Execution: Python (MCP tool #3)
|
863 |
# ======================================
|
864 |
|
865 |
-
def
|
866 |
"""
|
867 |
Execute arbitrary Python code and return captured stdout or an error message.
|
868 |
|
@@ -873,10 +872,10 @@ def Execute_Python(code: Annotated[str, "Python source code to run; stdout is ca
|
|
873 |
str: Combined stdout produced by the code, or the exception text if
|
874 |
execution failed.
|
875 |
"""
|
876 |
-
_log_call_start("
|
877 |
if code is None:
|
878 |
result = "No code provided."
|
879 |
-
_log_call_end("
|
880 |
return result
|
881 |
|
882 |
old_stdout = sys.stdout
|
@@ -888,12 +887,12 @@ def Execute_Python(code: Annotated[str, "Python source code to run; stdout is ca
|
|
888 |
result = str(e)
|
889 |
finally:
|
890 |
sys.stdout = old_stdout
|
891 |
-
_log_call_end("
|
892 |
return result
|
893 |
|
894 |
|
895 |
# ==========================
|
896 |
-
#
|
897 |
# ==========================
|
898 |
|
899 |
_KOKORO_STATE = {
|
@@ -1451,7 +1450,7 @@ def _mem_delete(
|
|
1451 |
|
1452 |
# --- Fetch tab (compact controllable extraction) ---
|
1453 |
fetch_interface = gr.Interface(
|
1454 |
-
fn=
|
1455 |
inputs=[
|
1456 |
gr.Textbox(label="URL", placeholder="https://example.com/article"),
|
1457 |
gr.Slider(
|
@@ -1483,7 +1482,7 @@ fetch_interface = gr.Interface(
|
|
1483 |
),
|
1484 |
],
|
1485 |
outputs=gr.Markdown(label="Extracted Content"),
|
1486 |
-
title="Fetch
|
1487 |
description=(
|
1488 |
"<div style=\"text-align:center\">Convert any webpage to clean Markdown format with precision controls, or extract all links. Supports custom element removal, length limits, and pagination with offset.</div>"
|
1489 |
),
|
@@ -1500,9 +1499,9 @@ fetch_interface = gr.Interface(
|
|
1500 |
flagging_mode="never",
|
1501 |
)
|
1502 |
|
1503 |
-
# ---
|
1504 |
concise_interface = gr.Interface(
|
1505 |
-
fn=
|
1506 |
inputs=[
|
1507 |
gr.Textbox(label="Query", placeholder="topic OR site:example.com"),
|
1508 |
gr.Slider(minimum=1, maximum=20, value=5, step=1, label="Max results"),
|
@@ -1523,12 +1522,12 @@ concise_interface = gr.Interface(
|
|
1523 |
),
|
1524 |
],
|
1525 |
outputs=gr.Textbox(label="Search Results", interactive=False),
|
1526 |
-
title="
|
1527 |
description=(
|
1528 |
"<div style=\"text-align:center\">Multi-type web search with readable output format, date detection, and flexible pagination. Supports text, news, images, videos, and books. Features smart fallback for news searches and precise offset control.</div>"
|
1529 |
),
|
1530 |
api_description=(
|
1531 |
-
"Run a
|
1532 |
"Features smart fallback: if 'news' search returns no results, automatically retries with 'text' search "
|
1533 |
"to catch sources like Hacker News that might not appear in news-specific results. "
|
1534 |
"Supports advanced search operators: site: for specific domains, quotes for exact phrases, "
|
@@ -1545,12 +1544,12 @@ concise_interface = gr.Interface(
|
|
1545 |
|
1546 |
##
|
1547 |
|
1548 |
-
# ---
|
1549 |
code_interface = gr.Interface(
|
1550 |
-
fn=
|
1551 |
inputs=gr.Code(label="Python Code", language="python"),
|
1552 |
outputs=gr.Textbox(label="Output"),
|
1553 |
-
title="
|
1554 |
description=(
|
1555 |
"<div style=\"text-align:center\">Execute Python code and see the output.</div>"
|
1556 |
),
|
@@ -1575,7 +1574,7 @@ CSS_STYLES = """
|
|
1575 |
/* Place bold tools list on line 2, normal auth note on line 3 (below title) */
|
1576 |
.app-title::before {
|
1577 |
grid-row: 2;
|
1578 |
-
|
1579 |
display: block;
|
1580 |
font-size: 1rem;
|
1581 |
font-weight: 700;
|
@@ -1753,7 +1752,7 @@ CSS_STYLES = """
|
|
1753 |
}
|
1754 |
"""
|
1755 |
|
1756 |
-
# ---
|
1757 |
available_voices = get_kokoro_voices()
|
1758 |
kokoro_interface = gr.Interface(
|
1759 |
fn=Generate_Speech,
|
@@ -1768,7 +1767,7 @@ kokoro_interface = gr.Interface(
|
|
1768 |
),
|
1769 |
],
|
1770 |
outputs=gr.Audio(label="Audio", type="numpy", format="wav", show_download_button=True),
|
1771 |
-
title="
|
1772 |
description=(
|
1773 |
"<div style=\"text-align:center\">Generate speech with Kokoro-82M. Supports multiple languages and accents. Runs on CPU or CUDA if available.</div>"
|
1774 |
),
|
@@ -1893,7 +1892,7 @@ memory_interface = gr.Interface(
|
|
1893 |
)
|
1894 |
|
1895 |
# ==========================
|
1896 |
-
# Image
|
1897 |
# ==========================
|
1898 |
|
1899 |
HF_API_TOKEN = os.getenv("HF_READ_TOKEN")
|
@@ -2005,7 +2004,7 @@ image_generation_interface = gr.Interface(
|
|
2005 |
gr.Slider(minimum=64, maximum=1216, value=1024, step=32, label="Height"),
|
2006 |
],
|
2007 |
outputs=gr.Image(label="Generated Image"),
|
2008 |
-
title="Image
|
2009 |
description=(
|
2010 |
"<div style=\"text-align:center\">Generate images via Hugging Face serverless inference. "
|
2011 |
"Default model is FLUX.1-Krea-dev.</div>"
|
@@ -2024,7 +2023,7 @@ image_generation_interface = gr.Interface(
|
|
2024 |
)
|
2025 |
|
2026 |
# ==========================
|
2027 |
-
# Video
|
2028 |
# ==========================
|
2029 |
|
2030 |
def _write_video_tmp(data_iter_or_bytes: object, suffix: str = ".mp4") -> str:
|
@@ -2201,7 +2200,7 @@ video_generation_interface = gr.Interface(
|
|
2201 |
gr.Slider(minimum=1.0, maximum=10.0, value=4.0, step=0.5, label="Duration (s)"),
|
2202 |
],
|
2203 |
outputs=gr.Video(label="Generated Video", show_download_button=True, format="mp4"),
|
2204 |
-
title="Video
|
2205 |
description=(
|
2206 |
"<div style=\"text-align:center\">Generate short videos via Hugging Face serverless inference. "
|
2207 |
"Default model is Wan2.2-T2V-A14B.</div>"
|
@@ -2279,13 +2278,13 @@ def _search_urls_only(query: str, max_results: int) -> list[str]:
|
|
2279 |
|
2280 |
|
2281 |
def _fetch_page_markdown(url: str, max_chars: int = 3000) -> str:
|
2282 |
-
"""Fetch a single URL and return cleaned Markdown using existing
|
2283 |
|
2284 |
Returns empty string on error.
|
2285 |
"""
|
2286 |
try:
|
2287 |
# Intentionally skip global fetch rate limiting for Deep Research speed.
|
2288 |
-
return
|
2289 |
except Exception:
|
2290 |
return ""
|
2291 |
|
@@ -2753,13 +2752,13 @@ _interfaces = [
|
|
2753 |
deep_research_interface,
|
2754 |
]
|
2755 |
_tab_names = [
|
2756 |
-
"Fetch
|
2757 |
-
"
|
2758 |
-
"
|
2759 |
"Memory Manager",
|
2760 |
-
"
|
2761 |
-
"Image
|
2762 |
-
"Video
|
2763 |
"Deep Research",
|
2764 |
]
|
2765 |
|
@@ -2831,10 +2830,10 @@ with gr.Blocks(title="Nymbo/Tools MCP", theme="Nymbo/Nymbo_Theme", css=CSS_STYLE
|
|
2831 |
<div class="info-card__body">
|
2832 |
<h3>Tool Notes & Kokoro Voice Legend</h3>
|
2833 |
<p>
|
2834 |
-
No authentication required for: <code>
|
2835 |
-
<code>
|
2836 |
</p>
|
2837 |
-
<p><strong>Kokoro
|
2838 |
<ul class="info-list" style="display:grid;grid-template-columns:repeat(2,minmax(160px,1fr));gap:6px 16px;">
|
2839 |
<li><code>af</code> — American female</li>
|
2840 |
<li><code>am</code> — American male</li>
|
|
|
1 |
# Purpose: One Space that offers up to seven tools/tabs (all exposed as MCP tools):
|
2 |
# 1) Fetch — convert webpages to clean Markdown format
|
3 |
+
# 2) Web Search — compact JSONL search output (DuckDuckGo backend)
|
4 |
+
# 3) Code Interpreter — execute Python code and capture stdout/errors
|
5 |
+
# 4) Generate Speech — synthesize speech from text using Kokoro-82M with 54 voice options
|
6 |
# 5) Memory Manager — lightweight JSON-based local memory store
|
7 |
+
# 6) Generate Image - HF serverless inference providers (requires HF_READ_TOKEN)
|
8 |
+
# 7) Generate Video - HF serverless inference providers (requires HF_READ_TOKEN)
|
|
|
9 |
|
10 |
from __future__ import annotations
|
11 |
|
|
|
348 |
return truncated + truncation_notice, metadata
|
349 |
|
350 |
|
351 |
+
def Web_Fetch( # <-- MCP tool #1 (Fetch)
|
352 |
url: Annotated[str, "The absolute URL to fetch (must return HTML)."],
|
353 |
max_chars: Annotated[int, "Maximum characters to return (0 = no limit, full page content)."] = 3000,
|
354 |
strip_selectors: Annotated[str, "CSS selectors to remove (comma-separated, e.g., '.header, .footer, nav')."] = "",
|
|
|
374 |
depending on the url_scraper setting. Content is length-limited by max_chars
|
375 |
and includes detailed truncation metadata when content is truncated.
|
376 |
"""
|
377 |
+
_log_call_start("Web_Fetch", url=url, max_chars=max_chars, strip_selectors=strip_selectors, url_scraper=url_scraper, offset=offset)
|
378 |
if not url or not url.strip():
|
379 |
result = "Please enter a valid URL."
|
380 |
+
_log_call_end("Web_Fetch", _truncate_for_log(result))
|
381 |
return result
|
382 |
|
383 |
try:
|
|
|
385 |
resp.raise_for_status()
|
386 |
except requests.exceptions.RequestException as e:
|
387 |
result = f"An error occurred: {e}"
|
388 |
+
_log_call_end("Web_Fetch", _truncate_for_log(result))
|
389 |
return result
|
390 |
|
391 |
final_url = str(resp.url)
|
392 |
ctype = resp.headers.get("Content-Type", "")
|
393 |
if "html" not in ctype.lower():
|
394 |
result = f"Unsupported content type for extraction: {ctype or 'unknown'}"
|
395 |
+
_log_call_end("Web_Fetch", _truncate_for_log(result))
|
396 |
return result
|
397 |
|
398 |
# Decode to text
|
|
|
418 |
if offset > 0:
|
419 |
if offset >= len(full_result):
|
420 |
result = f"Offset {offset} exceeds content length ({len(full_result)} characters). Content ends at position {len(full_result)}."
|
421 |
+
_log_call_end("Web_Fetch", _truncate_for_log(result))
|
422 |
return result
|
423 |
result = full_result[offset:]
|
424 |
else:
|
|
|
432 |
metadata["total_chars_estimate"] = len(full_result)
|
433 |
metadata["next_cursor"] = offset + metadata["next_cursor"] if metadata["next_cursor"] else None
|
434 |
|
435 |
+
_log_call_end("Web_Fetch", f"chars={len(result)}, url_scraper={url_scraper}, offset={offset}")
|
436 |
return result
|
437 |
|
438 |
|
439 |
# ============================================
|
440 |
+
# Web Search (DuckDuckGo backend): Enhanced with error handling & rate limiting
|
441 |
# ============================================
|
442 |
|
443 |
import asyncio
|
|
|
526 |
def _fetch_page_markdown_fast(url: str, max_chars: int = 3000, timeout: float = 10.0) -> str:
|
527 |
"""Fetch a single URL quickly; raise SlowHost on timeout.
|
528 |
|
529 |
+
Uses a shorter HTTP timeout to detect slow hosts, then reuses Web_Fetch
|
530 |
logic for conversion to Markdown. Returns empty string on non-timeout errors.
|
531 |
"""
|
532 |
try:
|
|
|
544 |
if "html" not in ctype.lower():
|
545 |
return ""
|
546 |
|
547 |
+
# Decode to text and convert similar to Web_Fetch (lean path)
|
548 |
resp.encoding = resp.encoding or resp.apparent_encoding
|
549 |
html = resp.text
|
550 |
soup = BeautifulSoup(html, "lxml")
|
|
|
671 |
return lines
|
672 |
|
673 |
|
674 |
+
def Web_Search( # <-- MCP tool #2 (Web Search)
|
675 |
query: Annotated[str, "The search query (supports operators like site:, quotes, OR)."],
|
676 |
max_results: Annotated[int, "Number of results to return (1–20)."] = 5,
|
677 |
page: Annotated[int, "Page number for pagination (1-based, each page contains max_results items)."] = 1,
|
|
|
679 |
offset: Annotated[int, "Result offset to start from (overrides page if > 0, for precise continuation)."] = 0,
|
680 |
) -> str:
|
681 |
"""
|
682 |
+
Run a web search (DuckDuckGo backend) and return formatted results with support for multiple content types.
|
683 |
|
684 |
Features smart fallback: if 'news' search returns no results, automatically retries with 'text'
|
685 |
search to catch sources like Hacker News that might not appear in news-specific results.
|
|
|
708 |
If 'news' search fails, results include a note about automatic fallback to 'text' search.
|
709 |
Includes next_offset information for easy continuation.
|
710 |
"""
|
711 |
+
_log_call_start("Web_Search", query=query, max_results=max_results, page=page, search_type=search_type, offset=offset)
|
712 |
if not query or not query.strip():
|
713 |
result = "No search query provided. Please enter a search term."
|
714 |
+
_log_call_end("Web_Search", _truncate_for_log(result))
|
715 |
return result
|
716 |
|
717 |
# Validate parameters
|
|
|
783 |
raw = _perform_search(search_type)
|
784 |
except Exception as e:
|
785 |
result = f"Error: {str(e)}"
|
786 |
+
_log_call_end("Web_Search", _truncate_for_log(result))
|
787 |
return result
|
788 |
|
789 |
# Smart fallback: if news search returns empty and we haven't tried text yet, try text search
|
|
|
800 |
if not raw:
|
801 |
fallback_note = " (also tried 'text' search as fallback)" if original_search_type == "news" and used_fallback else ""
|
802 |
result = f"No {original_search_type} results found for query: {query}{fallback_note}"
|
803 |
+
_log_call_end("Web_Search", _truncate_for_log(result))
|
804 |
return result
|
805 |
|
806 |
# Apply pagination by slicing the results
|
|
|
811 |
result = f"Offset {actual_offset} exceeds available results ({len(raw)} total). Try offset=0 to start from beginning."
|
812 |
else:
|
813 |
result = f"No {original_search_type} results found on page {calculated_page} for query: {query}. Try page 1 or reduce page number."
|
814 |
+
_log_call_end("Web_Search", _truncate_for_log(result))
|
815 |
return result
|
816 |
|
817 |
# Format results based on search type
|
|
|
853 |
search_info = f"type={original_search_type}"
|
854 |
if used_fallback:
|
855 |
search_info += "→text"
|
856 |
+
_log_call_end("Web_Search", f"{search_info} page={calculated_page} offset={actual_offset} results={len(paginated_results)} chars={len(result)}")
|
857 |
return result
|
858 |
|
859 |
|
|
|
861 |
# Code Execution: Python (MCP tool #3)
|
862 |
# ======================================
|
863 |
|
864 |
+
def Code_Interpreter(code: Annotated[str, "Python source code to run; stdout is captured and returned."]) -> str:
|
865 |
"""
|
866 |
Execute arbitrary Python code and return captured stdout or an error message.
|
867 |
|
|
|
872 |
str: Combined stdout produced by the code, or the exception text if
|
873 |
execution failed.
|
874 |
"""
|
875 |
+
_log_call_start("Code_Interpreter", code=_truncate_for_log(code or "", 300))
|
876 |
if code is None:
|
877 |
result = "No code provided."
|
878 |
+
_log_call_end("Code_Interpreter", result)
|
879 |
return result
|
880 |
|
881 |
old_stdout = sys.stdout
|
|
|
887 |
result = str(e)
|
888 |
finally:
|
889 |
sys.stdout = old_stdout
|
890 |
+
_log_call_end("Code_Interpreter", _truncate_for_log(result))
|
891 |
return result
|
892 |
|
893 |
|
894 |
# ==========================
|
895 |
+
# Generate Speech (MCP tool #4)
|
896 |
# ==========================
|
897 |
|
898 |
_KOKORO_STATE = {
|
|
|
1450 |
|
1451 |
# --- Fetch tab (compact controllable extraction) ---
|
1452 |
fetch_interface = gr.Interface(
|
1453 |
+
fn=Web_Fetch,
|
1454 |
inputs=[
|
1455 |
gr.Textbox(label="URL", placeholder="https://example.com/article"),
|
1456 |
gr.Slider(
|
|
|
1482 |
),
|
1483 |
],
|
1484 |
outputs=gr.Markdown(label="Extracted Content"),
|
1485 |
+
title="Web Fetch",
|
1486 |
description=(
|
1487 |
"<div style=\"text-align:center\">Convert any webpage to clean Markdown format with precision controls, or extract all links. Supports custom element removal, length limits, and pagination with offset.</div>"
|
1488 |
),
|
|
|
1499 |
flagging_mode="never",
|
1500 |
)
|
1501 |
|
1502 |
+
# --- Web Search tab (readable output only) ---
|
1503 |
concise_interface = gr.Interface(
|
1504 |
+
fn=Web_Search,
|
1505 |
inputs=[
|
1506 |
gr.Textbox(label="Query", placeholder="topic OR site:example.com"),
|
1507 |
gr.Slider(minimum=1, maximum=20, value=5, step=1, label="Max results"),
|
|
|
1522 |
),
|
1523 |
],
|
1524 |
outputs=gr.Textbox(label="Search Results", interactive=False),
|
1525 |
+
title="Web Search",
|
1526 |
description=(
|
1527 |
"<div style=\"text-align:center\">Multi-type web search with readable output format, date detection, and flexible pagination. Supports text, news, images, videos, and books. Features smart fallback for news searches and precise offset control.</div>"
|
1528 |
),
|
1529 |
api_description=(
|
1530 |
+
"Run a web search (DuckDuckGo backend) with support for multiple content types and return formatted results. "
|
1531 |
"Features smart fallback: if 'news' search returns no results, automatically retries with 'text' search "
|
1532 |
"to catch sources like Hacker News that might not appear in news-specific results. "
|
1533 |
"Supports advanced search operators: site: for specific domains, quotes for exact phrases, "
|
|
|
1544 |
|
1545 |
##
|
1546 |
|
1547 |
+
# --- Code Interpreter tab (Python) ---
|
1548 |
code_interface = gr.Interface(
|
1549 |
+
fn=Code_Interpreter,
|
1550 |
inputs=gr.Code(label="Python Code", language="python"),
|
1551 |
outputs=gr.Textbox(label="Output"),
|
1552 |
+
title="Code Interpreter",
|
1553 |
description=(
|
1554 |
"<div style=\"text-align:center\">Execute Python code and see the output.</div>"
|
1555 |
),
|
|
|
1574 |
/* Place bold tools list on line 2, normal auth note on line 3 (below title) */
|
1575 |
.app-title::before {
|
1576 |
grid-row: 2;
|
1577 |
+
content: "Web Fetch | Web Search | Code Interpreter | Memory Manager | Generate Speech | Generate Image | Generate Video | Deep Research";
|
1578 |
display: block;
|
1579 |
font-size: 1rem;
|
1580 |
font-weight: 700;
|
|
|
1752 |
}
|
1753 |
"""
|
1754 |
|
1755 |
+
# --- Generate Speech tab (text to speech) ---
|
1756 |
available_voices = get_kokoro_voices()
|
1757 |
kokoro_interface = gr.Interface(
|
1758 |
fn=Generate_Speech,
|
|
|
1767 |
),
|
1768 |
],
|
1769 |
outputs=gr.Audio(label="Audio", type="numpy", format="wav", show_download_button=True),
|
1770 |
+
title="Generate Speech",
|
1771 |
description=(
|
1772 |
"<div style=\"text-align:center\">Generate speech with Kokoro-82M. Supports multiple languages and accents. Runs on CPU or CUDA if available.</div>"
|
1773 |
),
|
|
|
1892 |
)
|
1893 |
|
1894 |
# ==========================
|
1895 |
+
# Generate Image (Serverless)
|
1896 |
# ==========================
|
1897 |
|
1898 |
HF_API_TOKEN = os.getenv("HF_READ_TOKEN")
|
|
|
2004 |
gr.Slider(minimum=64, maximum=1216, value=1024, step=32, label="Height"),
|
2005 |
],
|
2006 |
outputs=gr.Image(label="Generated Image"),
|
2007 |
+
title="Generate Image",
|
2008 |
description=(
|
2009 |
"<div style=\"text-align:center\">Generate images via Hugging Face serverless inference. "
|
2010 |
"Default model is FLUX.1-Krea-dev.</div>"
|
|
|
2023 |
)
|
2024 |
|
2025 |
# ==========================
|
2026 |
+
# Generate Video (Serverless)
|
2027 |
# ==========================
|
2028 |
|
2029 |
def _write_video_tmp(data_iter_or_bytes: object, suffix: str = ".mp4") -> str:
|
|
|
2200 |
gr.Slider(minimum=1.0, maximum=10.0, value=4.0, step=0.5, label="Duration (s)"),
|
2201 |
],
|
2202 |
outputs=gr.Video(label="Generated Video", show_download_button=True, format="mp4"),
|
2203 |
+
title="Generate Video",
|
2204 |
description=(
|
2205 |
"<div style=\"text-align:center\">Generate short videos via Hugging Face serverless inference. "
|
2206 |
"Default model is Wan2.2-T2V-A14B.</div>"
|
|
|
2278 |
|
2279 |
|
2280 |
def _fetch_page_markdown(url: str, max_chars: int = 3000) -> str:
|
2281 |
+
"""Fetch a single URL and return cleaned Markdown using existing Web_Fetch.
|
2282 |
|
2283 |
Returns empty string on error.
|
2284 |
"""
|
2285 |
try:
|
2286 |
# Intentionally skip global fetch rate limiting for Deep Research speed.
|
2287 |
+
return Web_Fetch(url=url, max_chars=max_chars, strip_selectors="", url_scraper=False, offset=0) # type: ignore[misc]
|
2288 |
except Exception:
|
2289 |
return ""
|
2290 |
|
|
|
2752 |
deep_research_interface,
|
2753 |
]
|
2754 |
_tab_names = [
|
2755 |
+
"Web Fetch",
|
2756 |
+
"Web Search",
|
2757 |
+
"Code Interpreter",
|
2758 |
"Memory Manager",
|
2759 |
+
"Generate Speech",
|
2760 |
+
"Generate Image",
|
2761 |
+
"Generate Video",
|
2762 |
"Deep Research",
|
2763 |
]
|
2764 |
|
|
|
2830 |
<div class="info-card__body">
|
2831 |
<h3>Tool Notes & Kokoro Voice Legend</h3>
|
2832 |
<p>
|
2833 |
+
No authentication required for: <code>Web_Fetch</code>, <code>Web_Search</code>,
|
2834 |
+
<code>Code_Interpreter</code>, and <code>Generate_Speech</code>.
|
2835 |
</p>
|
2836 |
+
<p><strong>Kokoro voice prefixes</strong></p>
|
2837 |
<ul class="info-list" style="display:grid;grid-template-columns:repeat(2,minmax(160px,1fr));gap:6px 16px;">
|
2838 |
<li><code>af</code> — American female</li>
|
2839 |
<li><code>am</code> — American male</li>
|