From 2fed71096a4939eba0ff88f7425ed819253ce85b Mon Sep 17 00:00:00 2001 From: myhloli Date: Wed, 10 Jun 2026 00:28:10 +0800 Subject: [PATCH] feat: enhance visibility management for client options and image analysis based on backend selection --- mineru/cli/gradio_app.py | 86 ++++++++++++++++++--------------- mineru/resources/gradio_app.css | 10 ++++ mineru/resources/gradio_app.js | 50 +++++++++++++++++++ 3 files changed, 107 insertions(+), 39 deletions(-) diff --git a/mineru/cli/gradio_app.py b/mineru/cli/gradio_app.py index a68aeee5..64bd7d95 100644 --- a/mineru/cli/gradio_app.py +++ b/mineru/cli/gradio_app.py @@ -852,11 +852,23 @@ def is_image_analysis_option_visible(backend, effort=DEFAULT_HYBRID_EFFORT): return False -def is_ocr_options_visible(backend): - """判断 OCR 语言和强制 OCR 选项是否展示;Hybrid/VLM 已不需要用户指定语言。""" +def is_ocr_language_option_visible(backend: object) -> bool: + """判断 OCR 语言选项是否展示;lang 参数只对 pipeline 后端生效。""" return backend == "pipeline" +def is_force_ocr_option_visible(backend: object) -> bool: + """判断强制 OCR 开关是否展示;Hybrid 不需要 lang,但仍支持强制 OCR。""" + if not isinstance(backend, str): + return False + return backend == "pipeline" or backend.startswith("hybrid") + + +def frontend_managed_initial_visibility(is_visible: bool): + """转换前端托管显隐控件的初始状态;hidden 会保留 DOM 挂载,避免后续重新挂载。""" + return True if is_visible else "hidden" + + def should_use_client_side_output_generation(client_side_output_generation): """判断当前 Gradio 任务是否需要在客户端生成最终输出。""" return client_side_output_generation @@ -1729,22 +1741,25 @@ def main(ctx, def get_backend_info(backend_choice): return i18n(select_backend_info_key(backend_choice)) - # 更新界面函数 - def update_interface(backend_choice, effort_choice): + def build_interface_updates(backend_choice, effort_choice): + """构建 Gradio 后端联动更新,保证所有事件复用同一套显隐规则。""" formula_label_update = gr.update(label=get_formula_label(backend_choice), info=get_formula_info(backend_choice)) backend_info_update = gr.update(info=get_backend_info(backend_choice)) effort_update = gr.update(visible=is_effort_option_visible(backend_choice)) - ocr_options_update = gr.update(visible=is_ocr_options_visible(backend_choice)) + ocr_language_options_update = gr.update(visible=is_ocr_language_option_visible(backend_choice)) + force_ocr_update = gr.update(visible=is_force_ocr_option_visible(backend_choice)) - return ocr_options_update, formula_label_update, backend_info_update, effort_update + return ( + ocr_language_options_update, + force_ocr_update, + formula_label_update, + backend_info_update, + effort_update, + ) - def update_client_options_visibility(backend_choice): - """仅更新服务器地址控件显隐,避免隐藏 Row 首次切换到 http-client 时挂载失败。""" - return gr.update(visible=is_http_client_backend(backend_choice)) - - def update_image_analysis_visibility(backend_choice, effort_choice): - """仅更新图片分析控件显隐;实际开关由 Hybrid 后端兜底处理。""" - return gr.update(visible=is_image_analysis_option_visible(backend_choice, effort_choice)) + def update_interface(backend_choice, effort_choice): + """更新可由 Gradio 稳定管理的基础界面项,易重挂载的控件交给前端状态类处理。""" + return build_interface_updates(backend_choice, effort_choice) del kwargs _gradio_local_api_server.configure( @@ -1818,8 +1833,12 @@ def main(ctx, label=i18n("backend"), value=preferred_option, info=get_backend_info(preferred_option), + elem_classes=["mineru-backend-select"], ) - with gr.Row(visible=is_http_client_backend(preferred_option)) as client_options: + with gr.Row( + visible=frontend_managed_initial_visibility(is_http_client_backend(preferred_option)), + elem_classes=["mineru-client-options"], + ) as client_options: url = gr.Textbox( label=i18n("server_url"), value='http://localhost:30000', @@ -1925,8 +1944,11 @@ def main(ctx, image_analysis = gr.Checkbox( label=i18n("image_analysis_enable"), value=True, - visible=is_image_analysis_option_visible(preferred_option, DEFAULT_HYBRID_EFFORT), + visible=frontend_managed_initial_visibility( + is_image_analysis_option_visible(preferred_option, DEFAULT_HYBRID_EFFORT) + ), info=i18n("image_analysis_info"), + elem_classes=["mineru-image-analysis-option"], ) hybrid_effort = gr.Radio( list(HYBRID_EFFORT_CHOICES), @@ -1934,15 +1956,21 @@ def main(ctx, value=DEFAULT_HYBRID_EFFORT, visible=is_effort_option_visible(preferred_option), info=i18n("hybrid_effort_info"), + elem_classes=["mineru-hybrid-effort"], ) - with gr.Group(visible=is_ocr_options_visible(preferred_option)) as ocr_options: + with gr.Group(visible=is_ocr_language_option_visible(preferred_option)) as ocr_language_options: language = gr.Dropdown( all_lang, label=i18n("ocr_language"), value='ch (Chinese, English, Chinese Traditional)', info=i18n("ocr_language_info"), ) - is_ocr = gr.Checkbox(label=i18n("force_ocr"), value=False, info=i18n("force_ocr_info")) + is_ocr = gr.Checkbox( + label=i18n("force_ocr"), + value=False, + visible=is_force_ocr_option_visible(preferred_option), + info=i18n("force_ocr_info"), + ) # 添加事件处理 _private_api_kwargs = ( @@ -1953,34 +1981,14 @@ def main(ctx, backend.change( fn=update_interface, inputs=[backend, hybrid_effort], - outputs=[ocr_options, formula_enable, backend, hybrid_effort], - **_private_api_kwargs - ) - # 服务器地址区域单独更新,避免 Gradio load 多输出影响首次切换到 http-client 的隐藏 Row 挂载。 - backend.change( - fn=update_client_options_visibility, - inputs=backend, - outputs=client_options, - **_private_api_kwargs - ) - # 图片分析显隐单独更新,避免 Gradio load 多输出影响首次 medium -> high 的隐藏组件挂载。 - backend.change( - fn=update_image_analysis_visibility, - inputs=[backend, hybrid_effort], - outputs=image_analysis, + outputs=[ocr_language_options, is_ocr, formula_enable, backend, hybrid_effort], **_private_api_kwargs ) # 添加demo.load事件,在页面加载时触发一次界面更新 demo.load( fn=update_interface, inputs=[backend, hybrid_effort], - outputs=[ocr_options, formula_enable, backend, hybrid_effort], - **_private_api_kwargs - ) - hybrid_effort.change( - fn=update_image_analysis_visibility, - inputs=[backend, hybrid_effort], - outputs=image_analysis, + outputs=[ocr_language_options, is_ocr, formula_enable, backend, hybrid_effort], **_private_api_kwargs ) clear_bu.add([input_file, md, doc_show, md_text, content_list_json, output_file, is_ocr, office_html, status_panel]) diff --git a/mineru/resources/gradio_app.css b/mineru/resources/gradio_app.css index da582974..5f22da1f 100644 --- a/mineru/resources/gradio_app.css +++ b/mineru/resources/gradio_app.css @@ -169,6 +169,16 @@ body.mineru-advanced-popover-open .mineru-advanced-popover { visibility: hidden !important; pointer-events: none !important; } +.mineru-client-options, +.mineru-image-analysis-option { + display: none !important; +} +body.mineru-show-client-options .gradio-container .contain .mineru-client-options.mineru-client-options { + display: flex !important; +} +body.mineru-show-image-analysis .gradio-container .contain .mineru-image-analysis-option.mineru-image-analysis-option { + display: block !important; +} .mineru-advanced-popover ul.options { border-radius: 8px !important; box-shadow: var(--mineru-popover-dropdown-shadow) !important; diff --git a/mineru/resources/gradio_app.js b/mineru/resources/gradio_app.js index 59fc3682..235df338 100644 --- a/mineru/resources/gradio_app.js +++ b/mineru/resources/gradio_app.js @@ -6,6 +6,8 @@ window.__mineruAdvancedPopoverInstalled = POPOVER_SCRIPT_VERSION; const POPOVER_OPEN_CLASS = "mineru-advanced-popover-open"; + const CLIENT_OPTIONS_VISIBLE_CLASS = "mineru-show-client-options"; + const IMAGE_ANALYSIS_VISIBLE_CLASS = "mineru-show-image-analysis"; const OFFICE_PREVIEW_NOTICE_STORAGE_KEY = "mineru.officePreviewNoticeIgnored"; const OPEN_DELAY_MS = 120; const CLOSE_DELAY_MS = 280; @@ -91,6 +93,7 @@ const refreshMineruCustomHtml = () => { localizeMineruCustomText(); applyOfficePreviewNoticePreference(); + refreshMineruOptionVisibility(); }; // 兼容 Gradio 将 elem_classes 挂到按钮自身或按钮外层容器的两种 DOM 结构。 @@ -98,10 +101,51 @@ "button.mineru-advanced-open, .mineru-advanced-open button, .mineru-advanced-open" ); const findPopover = () => document.querySelector(".mineru-advanced-popover"); + const findBackendRoot = () => document.querySelector(".mineru-backend-select"); + const findEffortRoot = () => document.querySelector(".mineru-hybrid-effort"); let openTimer = null; let closeTimer = null; let visibilityTimer = null; let hoverHandlersInstalled = false; + + // 读取 Gradio Dropdown 当前值;value 属性比可见文本更稳定,避免中英文文案影响判断。 + const getBackendValue = () => { + const backendRoot = findBackendRoot(); + const backendControl = backendRoot?.querySelector('[role="listbox"]'); + return (backendControl?.value || backendControl?.textContent || "").trim(); + }; + + // 读取 Hybrid effort 当前值;控件在非 hybrid 后端会被 Gradio 隐藏,缺失时按空值处理。 + const getEffortValue = () => { + const effortRoot = findEffortRoot(); + const checkedRadio = effortRoot?.querySelector( + 'input[type="radio"]:checked, input[type="radio"][aria-checked="true"]' + ); + return (checkedRadio?.value || "").trim(); + }; + + // 根据当前 backend/effort 刷新前端状态类,避免依赖 Gradio 重新挂载隐藏组件。 + const refreshMineruOptionVisibility = () => { + const backend = getBackendValue(); + const effort = getEffortValue(); + const showClientOptions = backend.endsWith("http-client"); + const showImageAnalysis = backend.startsWith("vlm") + || (backend.startsWith("hybrid") && effort === "high"); + + document.body.classList.toggle(CLIENT_OPTIONS_VISIBLE_CLASS, showClientOptions); + document.body.classList.toggle(IMAGE_ANALYSIS_VISIBLE_CLASS, showImageAnalysis); + if (document.body.classList.contains(POPOVER_OPEN_CLASS)) { + positionPopover(); + } + }; + + // Gradio 控件会异步写回 value,延后一帧再读可以覆盖 Dropdown option 点击和 Radio 切换。 + const queueMineruOptionVisibilityRefresh = () => { + requestAnimationFrame(() => { + refreshMineruOptionVisibility(); + requestAnimationFrame(refreshMineruOptionVisibility); + }); + }; const findUploadFileInput = () => { const uploadRoot = document.querySelector(".mineru-upload-file"); if (!uploadRoot) { @@ -450,6 +494,7 @@ if (!(target instanceof Element)) { return; } + queueMineruOptionVisibilityRefresh(); if (target.closest(".office-preview-ignore-forever")) { const notice = target.closest(".office-preview-notice"); if (setOfficePreviewNoticeIgnored()) { @@ -488,11 +533,16 @@ document.addEventListener("input", (event) => { const target = event.target; + queueMineruOptionVisibilityRefresh(); if (target instanceof Element && target.closest(".mineru-advanced-popover")) { queueDropdownPosition(); } }); + document.addEventListener("change", () => { + queueMineruOptionVisibilityRefresh(); + }); + document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closePopover();