Jiawei Dong
api fixed
d77d8e5
Raw
History Blame Contribute Delete
30.4 kB
"""
Gradio demo — Document Processing (Smart Doc + Translation).
Tabs:
- Smart Doc → https://connect.zhihuiya.com/rd-llm/v1/documents/doc_parsing
- Translation → https://connect.zhihuiya.com/hiro_translation
Auth: env HIRO_API_KEY, fallback RD_LLM_API_KEY (shared for both tabs).
"""
from __future__ import annotations
import html
import json
import os
import time
from pathlib import Path
# Shared hosts often leave /tmp/gradio owned by another user → PermissionError on upload.
_GRADIO_TMP = Path(__file__).resolve().parent / ".gradio_tmp"
_GRADIO_TMP.mkdir(parents=True, exist_ok=True)
os.environ.setdefault("GRADIO_TEMP_DIR", str(_GRADIO_TMP))
import gradio as gr
from api_client import (
DEFAULT_BASE,
SMARTDOC_URL,
parse_document,
resolve_api_key,
iter_translate_fast,
stream_translate,
)
CUSTOM_CSS = """
.gradio-container {
max-width: 100% !important;
}
.main-title {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 0.25rem 0;
color: var(--block-title-text-color, var(--color-accent, var(--body-text-color)));
}
.status-pill {
display: inline-block;
padding: 0.4rem 0.75rem;
border-radius: var(--block-radius);
font-size: 0.8rem;
line-height: 1.4;
border: 1px solid var(--block-border-color);
background: var(--block-background-fill);
color: var(--body-text-color);
}
.status-pill.ok {
border-color: var(--border-color-accent-subdued, var(--block-border-color));
color: var(--color-accent, var(--body-text-color));
}
.status-pill.err {
border-color: var(--error-border-color, var(--block-border-color));
color: var(--error-text-color, var(--body-text-color));
}
.col-header {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--body-text-color-subdued);
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--block-border-color);
background: var(--block-background-fill);
}
.pairs-wrap {
max-height: 62vh;
overflow-y: auto;
overflow-anchor: auto;
border: 1px solid var(--block-border-color);
border-radius: var(--block-radius);
background: var(--block-background-fill);
}
.pair-block {
border-bottom: 1px solid var(--block-border-color);
}
.pair-block:last-child { border-bottom: none; }
.pair-idx {
padding: 0.35rem 1rem;
font-size: 0.7rem;
color: var(--body-text-color-subdued);
background: var(--body-background-fill);
border-bottom: 1px solid var(--block-border-color);
}
.pair-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
}
.pair-cell {
padding: 0.85rem 1rem;
font-family: var(--font-mono);
font-size: 0.82rem;
line-height: 1.55;
color: var(--body-text-color);
white-space: pre-wrap;
word-break: break-word;
}
.pair-cell.src {
border-right: 1px solid var(--block-border-color);
background: var(--input-background-fill);
}
.pair-cell.tgt {
background: var(--background-fill-secondary, var(--block-background-fill));
}
.header-row {
display: grid;
grid-template-columns: 1fr 1fr;
}
.empty-hint {
padding: 3rem 1rem;
text-align: center;
color: var(--body-text-color-subdued);
font-size: 0.9rem;
}
.results-stack {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.results-panel-col {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.progress-section {
flex: 0 0 auto;
padding: 0.6rem 0.85rem;
border: 1px solid var(--block-border-color);
border-radius: var(--block-radius);
background: var(--block-background-fill);
}
.progress-meta {
font-size: 0.8rem;
color: var(--body-text-color-subdued);
margin-bottom: 0.45rem;
line-height: 1.4;
}
.progress-track {
height: 6px;
border-radius: 999px;
background: var(--background-fill-secondary, var(--input-background-fill));
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 999px;
background: var(--button-primary-background-fill);
}
.progress-pct {
margin-top: 0.35rem;
font-size: 0.72rem;
color: var(--body-text-color-subdued);
text-align: right;
}
#input-box textarea {
font-size: 0.95rem !important;
line-height: 1.6 !important;
border-radius: var(--block-radius) !important;
}
.toolbar-row > :last-child {
margin-left: auto;
}
.hiro-full-translation {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: pre-wrap;
border: 0;
resize: none;
}
footer { display: none !important; }
.smartdoc-status {
font-size: 0.9rem;
color: var(--body-text-color-subdued);
margin: 0.25rem 0 0.75rem 0;
}
.smartdoc-status.ok { color: var(--color-accent, #047857); }
.smartdoc-status.err { color: var(--error-text-color, #b42318); }
"""
RESULTS_SHELL_HTML = """
<div class="results-stack">
<textarea class="hiro-full-translation" readonly aria-hidden="true" tabindex="-1"></textarea>
<div class="progress-section">
<div class="progress-meta" id="hiro-progress-meta">Ready</div>
<div class="progress-track">
<div class="progress-fill" id="hiro-progress-fill" style="width:0%"></div>
</div>
<div class="progress-pct" id="hiro-progress-pct">0%</div>
</div>
<div class="pairs-wrap" id="hiro-pairs-scroll">
<div id="hiro-pairs-header"></div>
<div id="hiro-pairs-body">
<div class="empty-hint">Translation results will appear here</div>
</div>
</div>
</div>
"""
LANG_OPTIONS = [
("Chinese", "zh"),
("English", "en"),
("Japanese", "ja"),
("Korean", "ko"),
("German", "de"),
("French", "fr"),
("Russian", "ru"),
("Spanish", "es"),
("Portuguese", "pt"),
("Italian", "it"),
("Dutch", "nl"),
("Arabic", "ar"),
("Hindi", "hi"),
("Thai", "th"),
("Vietnamese", "vi"),
("Indonesian", "id"),
("Turkish", "tr"),
("Polish", "pl"),
]
EXAMPLES = [
["本发明公开了一种档案管理文件储存用分类标识装置。", "zh", "en"],
[
"本发明公开了一种档案管理文件储存用分类标识装置。\n我是智慧芽的研发",
"zh",
"en",
],
[
"The present invention relates to a heat exchanger comprising a plurality of fluid passages.",
"en",
"zh",
],
[
"1.一种蓝莓早促快发春梢的绿色生产技术,其特征在于,包括:(1)新主枝的培养、(2)调节树体结构。",
"zh",
"en",
],
]
_STREAM_YIELD_INTERVAL_S = 0.12
_STREAM_YIELD_MIN_NEW_SEGMENTS = 8
def _resolve_base(api_base: str) -> str:
return (api_base or DEFAULT_BASE).strip().rstrip("/")
def _parse_progress(progress: str | None) -> tuple[int, int]:
if not progress or "/" not in progress:
return 0, 0
try:
cur, total = progress.split("/", 1)
return int(cur.strip()), int(total.strip())
except ValueError:
return 0, 0
def _full_translation_text(pairs: list[tuple[str, str]]) -> str:
return "".join(trans for _, trans in pairs if trans)
def _pairs_for_display(pairs: list[tuple[str, str]]) -> list[tuple[str, str]]:
return [(orig, trans) for orig, trans in pairs if orig.strip() or trans.strip()]
def _progress_percent(progress: str | None, pair_count: int) -> float:
cur, total = _parse_progress(progress)
if total > 0:
return min(100.0, round(100.0 * cur / total, 1))
if pair_count > 0:
return 100.0
return 0.0
def _stream_state_payload(
*,
visible_pairs: list[tuple[str, str]],
full: str,
lang: str,
percent: float,
status: str,
reset: bool = False,
) -> str:
return json.dumps(
{
"reset": reset,
"visible_pairs": visible_pairs,
"total_visible_count": len(visible_pairs),
"full": full,
"percent": max(0.0, min(100.0, float(percent))),
"status": status,
"lang": lang,
},
ensure_ascii=False,
)
def _pack_outputs(*, stream_payload: str, panel_visible: bool, copy_enabled: bool) -> tuple:
return (
stream_payload,
gr.update(visible=panel_visible),
gr.update(interactive=copy_enabled),
)
def _empty_outputs():
payload = _stream_state_payload(
visible_pairs=[],
full="",
lang="zh2en",
percent=0,
status="Waiting for input…",
reset=True,
)
return _pack_outputs(stream_payload=payload, panel_visible=False, copy_enabled=False)
def _translation_outputs(
pairs: list[tuple[str, str]],
*,
lang: str,
percent: float,
status: str,
panel_visible: bool = True,
reset: bool = False,
):
visible = _pairs_for_display(pairs)
full = _full_translation_text(pairs)
payload = _stream_state_payload(
visible_pairs=visible,
full=full,
lang=lang,
percent=percent,
status=status,
reset=reset,
)
return _pack_outputs(
stream_payload=payload,
panel_visible=panel_visible,
copy_enabled=bool(full.strip()),
)
COPY_TRANSLATION_JS = """
() => {
const ta = document.querySelector(".hiro-full-translation");
const text = ta ? ta.value : "";
if (!String(text).trim()) {
alert("Nothing to copy yet");
return [];
}
const copyFallback = (value) => {
const el = document.createElement("textarea");
el.value = value;
el.style.position = "fixed";
el.style.left = "-9999px";
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
};
const done = () => {
const btn = document.getElementById("copy-translation-btn");
if (!btn) return;
const label = btn.querySelector("button") || btn;
const orig = label.textContent;
label.textContent = "Copied";
setTimeout(() => { label.textContent = orig; }, 1500);
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(String(text)).then(done).catch(() => {
copyFallback(String(text));
done();
});
} else {
copyFallback(String(text));
done();
}
return [];
}
"""
UPDATE_RESULTS_JS = """
(stateJson) => {
if (window.__hiroApplyStreamState == null) {
window.__hiroApplyStreamState = (raw) => {
if (window.__hiroResults == null) {
window.__hiroResults = { count: 0, layout: null, lastJson: "" };
}
let state;
try {
state = JSON.parse(raw || "{}");
} catch (_err) {
return;
}
const store = window.__hiroResults;
if (state.reset) {
store.count = 0;
store.layout = null;
store.lastJson = "";
}
if (raw === store.lastJson) return;
store.lastJson = raw;
const esc = (value) => {
const el = document.createElement("div");
el.textContent = value == null ? "" : String(value);
return el.innerHTML;
};
const labelsFor = (lang) => {
const codeMap = {
zh: "ZH", en: "EN", ja: "JA", ko: "KO", de: "DE",
fr: "FR", ru: "RU", es: "ES", pt: "PT", it: "IT",
nl: "NL", ar: "AR", hi: "HI", th: "TH", vi: "VI",
id: "ID", tr: "TR", pl: "PL",
};
const parts = String(lang || "zh2en").split("2");
const srcCode = (parts[0] || "zh").toLowerCase();
const tgtCode = (parts[1] || "en").toLowerCase();
const src = codeMap[srcCode] || srcCode.toUpperCase();
const tgt = codeMap[tgtCode] || tgtCode.toUpperCase();
return { src: `Source (${src})`, tgt: `Target (${tgt})` };
};
const meta = document.getElementById("hiro-progress-meta");
const fill = document.getElementById("hiro-progress-fill");
const pct = document.getElementById("hiro-progress-pct");
const header = document.getElementById("hiro-pairs-header");
const body = document.getElementById("hiro-pairs-body");
const scroll = document.getElementById("hiro-pairs-scroll");
const ta = document.querySelector(".hiro-full-translation");
const pairs = Array.isArray(state.visible_pairs) ? state.visible_pairs : [];
const layoutKey = state.lang || "zh2en";
const totalVisible = state.total_visible_count != null
? state.total_visible_count
: pairs.length;
if (meta) meta.textContent = state.status || "";
if (fill) fill.style.width = `${state.percent || 0}%`;
if (pct) pct.textContent = `${Math.round(state.percent || 0)}%`;
if (ta) ta.value = state.full || "";
document.querySelectorAll(".hiro-full-translation").forEach((el) => {
el.value = state.full || "";
});
if (!header || !body || !scroll) return;
if (state.reset || store.layout !== layoutKey) {
store.count = 0;
store.layout = layoutKey;
body.innerHTML = "";
const labels = labelsFor(state.lang);
header.innerHTML = `
<div class="header-row">
<div class="col-header">${esc(labels.src)}</div>
<div class="col-header">${esc(labels.tgt)}</div>
</div>`;
}
if (totalVisible < store.count) {
store.count = 0;
body.innerHTML = "";
}
if (totalVisible === 0 && store.count === 0) {
if (!body.querySelector(".empty-hint")) {
body.innerHTML = '<div class="empty-hint">Translation results will appear here</div>';
}
} else {
const hint = body.querySelector(".empty-hint");
if (hint) hint.remove();
}
const savedScroll = scroll.scrollTop;
const startIdx = store.count;
const newPairs = pairs.slice(startIdx);
for (let i = 0; i < newPairs.length; i++) {
const [orig, trans] = newPairs[i];
const displayIdx = startIdx + i + 1;
body.insertAdjacentHTML(
"beforeend",
`<div class="pair-block" data-idx="${displayIdx}">
<div class="pair-idx">Segment ${displayIdx}</div>
<div class="pair-row">
<div class="pair-cell src">${esc(orig)}</div>
<div class="pair-cell tgt">${esc(trans)}</div>
</div>
</div>`
);
}
store.count = totalVisible;
scroll.scrollTop = savedScroll;
};
}
window.__hiroApplyStreamState(stateJson);
return [];
}
"""
INIT_STREAM_WATCH_JS = """
() => {
if (window.__hiroStreamWatch) return [];
window.__hiroStreamWatch = true;
const bind = () => {
const input = document.querySelector("#hiro-stream-state textarea");
if (!input) return false;
let last = input.value;
setInterval(() => {
if (input.value !== last) {
last = input.value;
if (typeof window.__hiroApplyStreamState === "function") {
window.__hiroApplyStreamState(input.value);
}
}
}, 120);
return true;
};
if (!bind()) {
const boot = new MutationObserver(() => {
if (bind()) boot.disconnect();
});
boot.observe(document.body, { childList: true, subtree: true });
}
return [];
}
"""
def run_translation(text: str, lang: str, mode: str):
text = (text or "").strip()
if not text:
yield _empty_outputs()
return
base = _resolve_base(DEFAULT_BASE)
auth = resolve_api_key() or None
pairs: list[tuple[str, str]] = []
t0 = time.perf_counter()
last_progress = "0/0"
last_emitted_visible = 0
last_yield_t = 0.0
def emit(*, percent: float, status: str, panel_visible: bool = True, reset: bool = False, force: bool = False):
nonlocal last_emitted_visible, last_yield_t
visible = _pairs_for_display(pairs)
if not force and not reset and len(visible) == last_emitted_visible and percent < 100:
return None
last_emitted_visible = len(visible)
last_yield_t = time.perf_counter()
return _translation_outputs(
pairs,
lang=lang,
percent=percent,
status=status,
panel_visible=panel_visible,
reset=reset,
)
yield emit(percent=0, status="Connecting to gateway…", reset=True)
try:
if mode == "fast":
# Async fast: POST /translate/async then poll GET /translate/async/{taskId}
task_id = ""
for event in iter_translate_fast(text, lang, base_url=base, api_key=auth):
if not event.get("done"):
task_id = str(event.get("task_id") or task_id)
short = f"{task_id[:8]}…" if task_id else "…"
state = event.get("state") or "pending"
phase = event.get("phase") or "polling"
elapsed = time.perf_counter() - t0
if phase == "submitted":
status = f"Submitted async task {short} · {elapsed:.1f}s"
pct = 5.0
else:
status = f"Polling {short} · {state} · {elapsed:.1f}s"
pct = min(90.0, 10.0 + elapsed)
out = emit(percent=pct, status=status, force=True)
if out is not None:
yield out
continue
orig = event.get("text_original", text)
trans = event.get("text_translated", "")
elapsed = time.perf_counter() - t0
pairs = [(orig, trans)]
status = f"Done · fast (async) · {elapsed:.1f}s"
out_chars = event.get("translated_character_count")
if out_chars is not None:
status += f" · {out_chars} chars"
if task_id or event.get("task_id"):
tid = str(event.get("task_id") or task_id)
status += f" · task {tid[:8]}…"
yield emit(percent=100, status=status, force=True)
return
for chunk in stream_translate(text, lang, base_url=base, api_key=auth):
orig = chunk.get("text_original") or ""
trans = chunk.get("text_translated") or ""
if orig or trans:
pairs.append((orig, trans))
last_progress = chunk.get("progress") or last_progress
pct = _progress_percent(last_progress, len(pairs))
total = _parse_progress(last_progress)[1]
desc = (
f"Translating {last_progress}"
if total
else f"Received {len(pairs)} segment(s)"
)
elapsed = time.perf_counter() - t0
visible = _pairs_for_display(pairs)
new_visible = len(visible) - last_emitted_visible
now = time.perf_counter()
if (
now - last_yield_t >= _STREAM_YIELD_INTERVAL_S
or new_visible >= _STREAM_YIELD_MIN_NEW_SEGMENTS
):
out = emit(
percent=pct,
status=f"Streaming · {desc} · {elapsed:.1f}s",
)
if out is not None:
yield out
elapsed = time.perf_counter() - t0
yield emit(
percent=100,
status=(
f"Done · {len(pairs)} segment(s) · {elapsed:.1f}s · {last_progress}"
),
force=True,
)
except Exception as exc:
elapsed = time.perf_counter() - t0
err = html.escape(str(exc))
payload = _stream_state_payload(
visible_pairs=[],
full="",
lang=lang,
percent=0,
status=f"Failed · {elapsed:.1f}s · {err}",
reset=True,
)
yield _pack_outputs(stream_payload=payload, panel_visible=True, copy_enabled=False)
def _file_path(file_obj) -> str | None:
if file_obj is None:
return None
if isinstance(file_obj, (list, tuple)):
return _file_path(file_obj[0]) if file_obj else None
if isinstance(file_obj, str):
return file_obj
return getattr(file_obj, "name", None) or getattr(file_obj, "path", None)
def run_smartdoc(file_obj):
path = _file_path(file_obj)
if not path:
return (
'<p class="smartdoc-status err">Please select a file first</p>',
"*Rendered Markdown will appear here after parsing.*",
"[]",
)
try:
data = parse_document(path)
pages = data.get("total_pages") or 0
results = data.get("results") or []
markdown = data.get("markdown") or ""
status = (
f'<p class="smartdoc-status ok">Complete · {pages} pages · '
f"{len(results)} results</p>"
)
if not markdown.strip():
markdown = "*No Markdown content was returned.*"
return status, markdown, json.dumps(results, ensure_ascii=False, indent=2)
except Exception as exc:
return (
f'<p class="smartdoc-status err">Parsing failed: {html.escape(str(exc))}</p>',
"*Parsing failed. Check the status message.*",
"[]",
)
def build_theme() -> gr.Theme:
return gr.themes.Soft(
primary_hue="blue",
secondary_hue="indigo",
neutral_hue="slate",
radius_size="lg",
font=[gr.themes.GoogleFont("DM Sans"), "system-ui", "sans-serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"],
)
def build_ui() -> gr.Blocks:
with gr.Blocks(
title="Document Processing Demo",
css=CUSTOM_CSS,
theme=build_theme(),
) as demo:
gr.HTML('<p class="main-title">Document Processing Demo</p>')
with gr.Tabs():
with gr.Tab("Smart Doc"):
gr.Markdown(
f"Upload a document for layout + OCR parsing via "
f"`{SMARTDOC_URL}` · PDF / image / Office · **10 MB** max"
)
doc_file = gr.File(
label="Document",
file_count="single",
file_types=[
".pdf",
".png",
".jpg",
".jpeg",
".doc",
".docx",
".ppt",
".pptx",
".xls",
".xlsx",
".odt",
".odp",
".ods",
".rtf",
],
)
with gr.Row(elem_classes=["toolbar-row"]):
parse_btn = gr.Button("Parse Document", variant="primary", scale=0)
clear_doc_btn = gr.Button("Clear", scale=0)
smartdoc_status = gr.HTML(
value='<p class="smartdoc-status">Select a file to begin</p>'
)
with gr.Tabs():
with gr.Tab("Markdown"):
smartdoc_md = gr.Markdown(
value="*Rendered Markdown will appear here after parsing.*",
latex_delimiters=[
{"left": "$$", "right": "$$", "display": True},
{"left": "$", "right": "$", "display": False},
{"left": "\\(", "right": "\\)", "display": False},
{"left": "\\[", "right": "\\]", "display": True},
],
)
with gr.Tab("Results"):
smartdoc_json = gr.Code(
value="[]",
language="json",
label="Structured results",
lines=22,
)
with gr.Tab("Translation"):
with gr.Row():
source_lang = gr.Dropdown(
choices=LANG_OPTIONS,
value="zh",
label="Source language",
scale=1,
)
target_lang = gr.Dropdown(
choices=LANG_OPTIONS,
value="en",
label="Target language",
scale=1,
)
mode = gr.Radio(
choices=["stream", "fast"],
value="stream",
label="Mode",
info=(
"stream: SSE segments via POST /translate; "
"fast: async submit + poll via /translate/async"
),
scale=1,
)
lang = gr.Textbox(value="zh2en", visible=False)
gr.Markdown("**Examples**")
example_btns: list[tuple[gr.Button, str, str, str]] = []
with gr.Row():
for i, (sample, src, tgt) in enumerate(EXAMPLES, start=1):
preview = sample.replace("\n", " ")[:28]
if len(sample) > 28:
preview += "…"
btn = gr.Button(f"{i}. {preview}", size="sm", variant="secondary")
example_btns.append((btn, sample, src, tgt))
input_text = gr.Textbox(
label="Source text",
placeholder=(
"Enter patent-related text. Supports Chinese, English, "
"Japanese, Korean, and more."
),
lines=8,
elem_id="input-box",
)
with gr.Row(elem_classes=["toolbar-row"]):
submit_btn = gr.Button("Translate", variant="primary", scale=0)
clear_btn = gr.Button("Clear", scale=0)
copy_btn = gr.Button(
"Copy full translation",
variant="secondary",
interactive=False,
scale=0,
elem_id="copy-translation-btn",
)
with gr.Column(
visible=False, elem_classes=["results-panel-col"]
) as results_panel:
gr.HTML(RESULTS_SHELL_HTML, elem_id="translation-results")
stream_state = gr.Textbox(
value=_stream_state_payload(
visible_pairs=[],
full="",
lang="zh2en",
percent=0,
status="Ready",
),
visible=False,
elem_id="hiro-stream-state",
)
def _combine_lang(src: str, tgt: str) -> str:
return f"{src}2{tgt}"
source_lang.change(
_combine_lang, inputs=[source_lang, target_lang], outputs=[lang]
)
target_lang.change(
_combine_lang, inputs=[source_lang, target_lang], outputs=[lang]
)
for btn, sample, src, tgt in example_btns:
btn.click(
lambda _s=sample, _src=src, _tgt=tgt: (_s, _src, _tgt, f"{_src}2{_tgt}"),
outputs=[input_text, source_lang, target_lang, lang],
)
submit_btn.click(
fn=run_translation,
inputs=[input_text, lang, mode],
outputs=[stream_state, results_panel, copy_btn],
)
stream_state.change(
fn=None,
inputs=[stream_state],
outputs=None,
js=UPDATE_RESULTS_JS,
queue=False,
)
copy_btn.click(
fn=None,
inputs=None,
outputs=None,
js=COPY_TRANSLATION_JS,
queue=False,
)
def on_clear():
payload = _stream_state_payload(
visible_pairs=[],
full="",
lang="zh2en",
percent=0,
status="Waiting for input…",
reset=True,
)
return "", *_pack_outputs(
stream_payload=payload, panel_visible=False, copy_enabled=False
)
clear_btn.click(
on_clear,
outputs=[input_text, stream_state, results_panel, copy_btn],
)
parse_btn.click(
fn=run_smartdoc,
inputs=[doc_file],
outputs=[smartdoc_status, smartdoc_md, smartdoc_json],
)
def on_clear_doc():
return (
None,
'<p class="smartdoc-status">Select a file to begin</p>',
"*Rendered Markdown will appear here after parsing.*",
"[]",
)
clear_doc_btn.click(
on_clear_doc,
outputs=[doc_file, smartdoc_status, smartdoc_md, smartdoc_json],
)
demo.load(fn=None, js=INIT_STREAM_WATCH_JS)
return demo
demo = build_ui()
demo.queue(default_concurrency_limit=2)
if __name__ == "__main__":
# Spaces health-check needs 0.0.0.0 (not 127.0.0.1).
demo.launch(server_name="0.0.0.0", server_port=7860)