Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| import os | |
| import re | |
| import markdown | |
| from typing import Dict, Any | |
| def create_manual_tab(constant: Dict[str, Any]) -> Dict[str, Any]: | |
| # 添加自定义CSS,增大文本大小并添加左侧导航栏样式 | |
| custom_css = """ | |
| <style> | |
| /* 增大整体文本大小 */ | |
| .manual-content { | |
| font-size: 16px !important; | |
| line-height: 1.6 !important; | |
| } | |
| /* 标题样式 */ | |
| .manual-content h1 { | |
| font-size: 28px !important; | |
| margin-top: 30px !important; | |
| margin-bottom: 20px !important; | |
| border-bottom: 2px solid #3498db; | |
| padding-bottom: 10px; | |
| } | |
| .manual-content h2 { | |
| font-size: 24px !important; | |
| margin-top: 25px !important; | |
| margin-bottom: 15px !important; | |
| border-bottom: 1px solid #ddd; | |
| padding-bottom: 8px; | |
| } | |
| .manual-content h3 { | |
| font-size: 20px !important; | |
| margin-top: 20px !important; | |
| margin-bottom: 10px !important; | |
| } | |
| .manual-content h4 { | |
| font-size: 18px !important; | |
| margin-top: 15px !important; | |
| margin-bottom: 10px !important; | |
| } | |
| /* 段落和列表样式 */ | |
| .manual-content p, .manual-content li { | |
| font-size: 16px !important; | |
| margin-bottom: 10px !important; | |
| } | |
| /* 嵌套列表样式 */ | |
| .manual-content ul, .manual-content ol { | |
| padding-left: 25px !important; | |
| margin-bottom: 15px !important; | |
| list-style-position: outside !important; | |
| } | |
| .manual-content ul ul, | |
| .manual-content ol ol, | |
| .manual-content ul ol, | |
| .manual-content ol ul { | |
| margin-top: 5px !important; | |
| margin-bottom: 5px !important; | |
| padding-left: 25px !important; | |
| } | |
| .manual-content ul { | |
| list-style-type: disc !important; | |
| } | |
| .manual-content ul ul { | |
| list-style-type: circle !important; | |
| } | |
| .manual-content ul ul ul { | |
| list-style-type: square !important; | |
| } | |
| .manual-content ol { | |
| list-style-type: decimal !important; | |
| } | |
| .manual-content ol ol { | |
| list-style-type: lower-alpha !important; | |
| } | |
| .manual-content ol ol ol { | |
| list-style-type: lower-roman !important; | |
| } | |
| /* 确保列表项正确显示 */ | |
| .manual-content li { | |
| display: list-item !important; | |
| margin-bottom: 5px !important; | |
| } | |
| .manual-content li p { | |
| margin-bottom: 5px !important; | |
| display: inline-block !important; | |
| } | |
| /* 代码块样式 */ | |
| .manual-content pre { | |
| background-color: #f5f5f5 !important; | |
| padding: 15px !important; | |
| border-radius: 5px !important; | |
| overflow-x: auto !important; | |
| margin: 15px 0 !important; | |
| } | |
| .manual-content code { | |
| font-family: 'Courier New', Courier, monospace !important; | |
| font-size: 15px !important; | |
| } | |
| /* 表格样式 */ | |
| .manual-content table { | |
| width: 100% !important; | |
| border-collapse: collapse !important; | |
| margin: 20px 0 !important; | |
| } | |
| .manual-content th, .manual-content td { | |
| border: 1px solid #ddd !important; | |
| padding: 12px !important; | |
| text-align: left !important; | |
| } | |
| .manual-content th { | |
| background-color: #f2f2f2 !important; | |
| font-weight: bold !important; | |
| } | |
| /* 左侧导航栏样式 */ | |
| .manual-container { | |
| display: flex !important; | |
| width: 100% !important; | |
| position: relative !important; | |
| } | |
| .manual-nav { | |
| width: 250px !important; | |
| padding: 18px !important; | |
| background-color: #f8f9fa !important; | |
| border-right: 1px solid #ddd !important; | |
| border-radius: 5px !important; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1) !important; | |
| font-size: 14px !important; | |
| line-height: 1.4 !important; | |
| align-self: flex-start !important; | |
| position: sticky !important; | |
| top: 20px !important; | |
| max-height: 100% !important; | |
| overflow-y: auto !important; | |
| } | |
| .manual-nav ul { | |
| list-style-type: none !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| } | |
| .manual-nav li { | |
| margin-bottom: 5px !important; | |
| } | |
| .manual-nav a { | |
| display: block !important; | |
| padding: 6px 8px !important; | |
| color: #333 !important; | |
| text-decoration: none !important; | |
| border-radius: 4px !important; | |
| line-height: 1.4 !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| .manual-nav a:hover { | |
| background-color: #e9ecef !important; | |
| transform: translateX(2px) !important; | |
| } | |
| .manual-nav .nav-h2 { | |
| padding-left: 15px !important; | |
| font-size: 13px !important; | |
| } | |
| .manual-nav .nav-h3 { | |
| padding-left: 30px !important; | |
| font-size: 12px !important; | |
| } | |
| .manual-content { | |
| flex: 1 !important; | |
| padding: 20px !important; | |
| overflow-y: auto !important; | |
| margin-left: 20px !important; | |
| } | |
| /* 响应式设计 */ | |
| @media (max-width: 768px) { | |
| .manual-container { | |
| flex-direction: column !important; | |
| } | |
| .manual-nav { | |
| position: static !important; | |
| width: 100% !important; | |
| height: auto !important; | |
| max-height: 300px !important; | |
| margin-bottom: 20px !important; | |
| } | |
| .manual-content { | |
| margin-left: 0 !important; | |
| width: 100% !important; | |
| } | |
| } | |
| /* 滚动条样式 */ | |
| .manual-nav::-webkit-scrollbar { | |
| width: 6px !important; | |
| } | |
| .manual-nav::-webkit-scrollbar-track { | |
| background: #f1f1f1 !important; | |
| border-radius: 10px !important; | |
| } | |
| .manual-nav::-webkit-scrollbar-thumb { | |
| background: #c1c1c1 !important; | |
| border-radius: 10px !important; | |
| } | |
| .manual-nav::-webkit-scrollbar-thumb:hover { | |
| background: #a8a8a8 !important; | |
| } | |
| /* 强化列表样式 */ | |
| .manual-content ul li, .manual-content ol li { | |
| margin-bottom: 8px !important; | |
| line-height: 1.5 !important; | |
| } | |
| .manual-content ul li:last-child, .manual-content ol li:last-child { | |
| margin-bottom: 0 !important; | |
| } | |
| .manual-content ul ul, .manual-content ol ol, .manual-content ul ol, .manual-content ol ul { | |
| margin-top: 8px !important; | |
| } | |
| /* 强调列表项内容 */ | |
| .manual-content li strong, .manual-content li b { | |
| color: #2c3e50 !important; | |
| } | |
| /* 确保列表项内的代码块正确显示 */ | |
| .manual-content li code { | |
| background-color: #f5f5f5 !important; | |
| padding: 2px 4px !important; | |
| border-radius: 3px !important; | |
| font-size: 14px !important; | |
| color: #e83e8c !important; | |
| } | |
| /* 添加到您现有的custom_css字符串中 */ | |
| .manual-content img { | |
| max-width: 100% !important; | |
| height: auto !important; | |
| display: block !important; | |
| margin: 20px auto !important; | |
| border-radius: 5px !important; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important; | |
| } | |
| /* 为图片添加描述样式 */ | |
| .manual-content p img + em { | |
| display: block !important; | |
| text-align: center !important; | |
| color: #666 !important; | |
| font-size: 14px !important; | |
| margin-top: 8px !important; | |
| } | |
| /* 图片点击放大效果相关样式 */ | |
| .manual-content img:hover { | |
| cursor: pointer !important; | |
| transform: scale(1.01) !important; | |
| transition: transform 0.2s ease !important; | |
| } | |
| </style> | |
| """ | |
| # 添加JavaScript代码,用于处理导航点击 | |
| custom_js = """ | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // 为所有导航链接添加点击事件 | |
| document.querySelectorAll('.manual-nav a').forEach(function(link) { | |
| link.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| const targetId = this.getAttribute('href').substring(1); | |
| const targetElement = document.getElementById(targetId); | |
| if (targetElement) { | |
| targetElement.scrollIntoView({ | |
| behavior: 'smooth' | |
| }); | |
| } | |
| }); | |
| }); | |
| // 为所有手册内容中的图片添加点击事件 | |
| document.querySelectorAll('.manual-content img').forEach(function(img) { | |
| img.addEventListener('click', function() { | |
| // 创建一个模态框来显示大图 | |
| const modal = document.createElement('div'); | |
| modal.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background-color:rgba(0,0,0,0.8); display:flex; justify-content:center; align-items:center; z-index:9999;'; | |
| // 创建大图元素 | |
| const largeImg = document.createElement('img'); | |
| largeImg.src = this.src; | |
| largeImg.style.cssText = 'max-width:90%; max-height:90%; object-fit:contain;'; | |
| // 将大图添加到模态框 | |
| modal.appendChild(largeImg); | |
| // 点击模态框关闭它 | |
| modal.addEventListener('click', function() { | |
| document.body.removeChild(modal); | |
| }); | |
| // 将模态框添加到body | |
| document.body.appendChild(modal); | |
| }); | |
| }); | |
| }); | |
| </script> | |
| """ | |
| # 使用Python的markdown库将Markdown转换为HTML | |
| def markdown_to_html(markdown_content, base_path="src/web/manual"): | |
| """将Markdown内容转换为HTML,并将图片嵌入为base64编码""" | |
| # 处理图片路径,使用base64编码直接嵌入图片 | |
| def embed_image(match): | |
| alt_text = match.group(1) | |
| img_path = match.group(2) | |
| # 检查路径是否为外部URL | |
| if img_path.startswith(('http://', 'https://')): | |
| return f'<img src="{img_path}" alt="{alt_text}" />' | |
| # 处理本地图片路径 | |
| try: | |
| # 去掉开头的/以获取正确的路径 | |
| if img_path.startswith('/'): | |
| img_path = img_path[1:] | |
| # 获取绝对路径 | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| project_root = os.path.dirname(os.path.dirname(current_dir)) | |
| abs_img_path = os.path.join(project_root, img_path) | |
| # 读取图片并转换为base64 | |
| import base64 | |
| from pathlib import Path | |
| image_path = Path(abs_img_path) | |
| if image_path.exists(): | |
| image_type = image_path.suffix.lstrip('.').lower() | |
| if image_type == 'jpg': | |
| image_type = 'jpeg' | |
| with open(image_path, "rb") as img_file: | |
| encoded_string = base64.b64encode(img_file.read()).decode('utf-8') | |
| return f'<img src="data:image/{image_type};base64,{encoded_string}" alt="{alt_text}" style="max-width:100%; height:auto;" />' | |
| else: | |
| print(f"图片文件不存在: {abs_img_path}") | |
| return f'<span style="color:red;">[图片不存在: {img_path}]</span>' | |
| except Exception as e: | |
| print(f"处理图片时出错: {e}, 路径: {img_path}") | |
| return f'<span style="color:red;">[图片加载错误: {img_path}]</span>' | |
| # 使用正则表达式处理所有图片标记 | |
| pattern = r'!\[(.*?)\]\((.*?)\)' | |
| processed_content = re.sub(pattern, embed_image, markdown_content) | |
| # 使用Python的markdown库进行转换 | |
| html = markdown.markdown( | |
| processed_content, | |
| extensions=[ | |
| 'tables', | |
| 'fenced_code', | |
| 'codehilite', | |
| 'nl2br', | |
| 'extra', | |
| 'mdx_truly_sane_lists' | |
| ], | |
| extension_configs={ | |
| 'mdx_truly_sane_lists': { | |
| 'nested_indent': 2, | |
| 'truly_sane': True | |
| } | |
| } | |
| ) | |
| return html | |
| # 从Markdown内容生成HTML导航栏和处理内容 | |
| def generate_toc_and_content(markdown_content): | |
| """从Markdown内容生成HTML导航栏和处理内容""" | |
| # 提取所有标题 | |
| headers = re.findall(r'^(#{1,3})\s+(.+)$', markdown_content, re.MULTILINE) | |
| if not headers: | |
| return "<div class='manual-nav'><p>目录加载中...</p></div>", markdown_content | |
| toc_html = "<div class='manual-nav'><ul>" | |
| # 为每个标题创建导航项 | |
| for i, (level, title) in enumerate(headers): | |
| level_num = len(level) | |
| header_id = f"header-{i}" | |
| # 根据标题级别添加类 | |
| css_class = "" | |
| if level_num == 2: | |
| css_class = "nav-h2" | |
| elif level_num == 3: | |
| css_class = "nav-h3" | |
| toc_html += f"<li><a href='#{header_id}' class='{css_class}'>{title}</a></li>" | |
| toc_html += "</ul></div>" | |
| # 为Markdown内容中的标题添加ID | |
| processed_content = markdown_content | |
| for i, (level, title) in enumerate(headers): | |
| header_id = f"header-{i}" | |
| header_pattern = f"{level} {title}" | |
| header_replacement = f"{level} <span id='{header_id}'></span>{title}" | |
| processed_content = processed_content.replace(header_pattern, header_replacement, 1) | |
| # 将处理后的Markdown转换为HTML | |
| html_content = markdown_to_html(processed_content) | |
| return toc_html, html_content | |
| with gr.Tab("Manual"): | |
| # 添加自定义CSS和JavaScript | |
| gr.HTML(custom_css + custom_js) | |
| with gr.Row(): | |
| language = gr.Dropdown(choices=['English', 'Chinese'], value='English', label='Language', interactive=True) | |
| with gr.Tab("Training"): | |
| training_content = load_manual_training(language.value) | |
| toc_html, html_content = generate_toc_and_content(training_content) | |
| training_md = gr.HTML(f""" | |
| <div class="manual-container"> | |
| {toc_html} | |
| <div class="manual-content">{html_content}</div> | |
| </div> | |
| """) | |
| with gr.Tab("Prediction"): | |
| prediction_content = load_manual_prediction(language.value) | |
| toc_html, html_content = generate_toc_and_content(prediction_content) | |
| prediction_md = gr.HTML(f""" | |
| <div class="manual-container"> | |
| {toc_html} | |
| <div class="manual-content">{html_content}</div> | |
| </div> | |
| """) | |
| with gr.Tab("Evaluation"): | |
| evaluation_content = load_manual_evaluation(language.value) | |
| toc_html, html_content = generate_toc_and_content(evaluation_content) | |
| evaluation_md = gr.HTML(f""" | |
| <div class="manual-container"> | |
| {toc_html} | |
| <div class="manual-content">{html_content}</div> | |
| </div> | |
| """) | |
| with gr.Tab("Download"): | |
| download_content = load_manual_download(language.value) | |
| toc_html, html_content = generate_toc_and_content(download_content) | |
| download_md = gr.HTML(f""" | |
| <div class="manual-container"> | |
| {toc_html} | |
| <div class="manual-content">{html_content}</div> | |
| </div> | |
| """) | |
| with gr.Tab("FAQ"): | |
| faq_content = load_manual_faq(language.value) | |
| toc_html, html_content = generate_toc_and_content(faq_content) | |
| faq_md = gr.HTML(f""" | |
| <div class="manual-container"> | |
| {toc_html} | |
| <div class="manual-content">{html_content}</div> | |
| </div> | |
| """) | |
| # 正确绑定语言切换事件 | |
| language.change( | |
| fn=update_manual, | |
| inputs=[language], | |
| outputs=[training_md, prediction_md, evaluation_md, download_md, faq_md] | |
| ) | |
| return {"training_md": training_md, "prediction_md": prediction_md, "evaluation_md": evaluation_md, "download_md": download_md, "faq_md": faq_md} | |
| def update_manual(language): | |
| """更新手册内容""" | |
| training_content = load_manual_training(language) | |
| prediction_content = load_manual_prediction(language) | |
| evaluation_content = load_manual_evaluation(language) | |
| download_content = load_manual_download(language) | |
| faq_content = load_manual_faq(language) | |
| # 使用Python的markdown库将Markdown转换为HTML | |
| def markdown_to_html(markdown_content, base_path="src/web/manual"): | |
| """将Markdown内容转换为HTML,并将图片嵌入为base64编码""" | |
| # 处理图片路径,使用base64编码直接嵌入图片 | |
| def embed_image(match): | |
| alt_text = match.group(1) | |
| img_path = match.group(2) | |
| # 检查路径是否为外部URL | |
| if img_path.startswith(('http://', 'https://')): | |
| return f'<img src="{img_path}" alt="{alt_text}" />' | |
| # 处理本地图片路径 | |
| try: | |
| # 去掉开头的/以获取正确的路径 | |
| if img_path.startswith('/'): | |
| img_path = img_path[1:] | |
| # 获取绝对路径 | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| project_root = os.path.dirname(os.path.dirname(current_dir)) | |
| abs_img_path = os.path.join(project_root, img_path) | |
| # 读取图片并转换为base64 | |
| import base64 | |
| from pathlib import Path | |
| image_path = Path(abs_img_path) | |
| if image_path.exists(): | |
| image_type = image_path.suffix.lstrip('.').lower() | |
| if image_type == 'jpg': | |
| image_type = 'jpeg' | |
| with open(image_path, "rb") as img_file: | |
| encoded_string = base64.b64encode(img_file.read()).decode('utf-8') | |
| return f'<img src="data:image/{image_type};base64,{encoded_string}" alt="{alt_text}" style="max-width:100%; height:auto;" />' | |
| else: | |
| print(f"图片文件不存在: {abs_img_path}") | |
| return f'<span style="color:red;">[图片不存在: {img_path}]</span>' | |
| except Exception as e: | |
| print(f"处理图片时出错: {e}, 路径: {img_path}") | |
| return f'<span style="color:red;">[图片加载错误: {img_path}]</span>' | |
| # 使用正则表达式处理所有图片标记 | |
| pattern = r'!\[(.*?)\]\((.*?)\)' | |
| processed_content = re.sub(pattern, embed_image, markdown_content) | |
| # 使用Python的markdown库进行转换 | |
| html = markdown.markdown( | |
| processed_content, | |
| extensions=[ | |
| 'tables', | |
| 'fenced_code', | |
| 'codehilite', | |
| 'nl2br', | |
| 'extra', | |
| 'mdx_truly_sane_lists' | |
| ], | |
| extension_configs={ | |
| 'mdx_truly_sane_lists': { | |
| 'nested_indent': 2, | |
| 'truly_sane': True | |
| } | |
| } | |
| ) | |
| return html | |
| # 为每个内容生成导航栏和HTML内容 | |
| def generate_toc_and_content(markdown_content): | |
| """从Markdown内容生成HTML导航栏和处理内容""" | |
| # 提取所有标题 | |
| headers = re.findall(r'^(#{1,3})\s+(.+)$', markdown_content, re.MULTILINE) | |
| if not headers: | |
| return "<div class='manual-nav'><p>目录加载中...</p></div>", markdown_content | |
| toc_html = "<div class='manual-nav'><ul>" | |
| # 为每个标题创建导航项 | |
| for i, (level, title) in enumerate(headers): | |
| level_num = len(level) | |
| header_id = f"header-{i}" | |
| # 根据标题级别添加类 | |
| css_class = "" | |
| if level_num == 2: | |
| css_class = "nav-h2" | |
| elif level_num == 3: | |
| css_class = "nav-h3" | |
| toc_html += f"<li><a href='#{header_id}' class='{css_class}'>{title}</a></li>" | |
| toc_html += "</ul></div>" | |
| # 为Markdown内容中的标题添加ID | |
| processed_content = markdown_content | |
| for i, (level, title) in enumerate(headers): | |
| header_id = f"header-{i}" | |
| header_pattern = f"{level} {title}" | |
| header_replacement = f"{level} <span id='{header_id}'></span>{title}" | |
| processed_content = processed_content.replace(header_pattern, header_replacement, 1) | |
| # 将处理后的Markdown转换为HTML | |
| html_content = markdown_to_html(processed_content) | |
| return toc_html, html_content | |
| # 生成带导航栏的HTML | |
| training_toc, training_html = generate_toc_and_content(training_content) | |
| prediction_toc, prediction_html = generate_toc_and_content(prediction_content) | |
| evaluation_toc, evaluation_html = generate_toc_and_content(evaluation_content) | |
| download_toc, download_html = generate_toc_and_content(download_content) | |
| faq_toc, faq_html = generate_toc_and_content(faq_content) | |
| training_output = f""" | |
| <div class="manual-container"> | |
| {training_toc} | |
| <div class="manual-content">{training_html}</div> | |
| </div> | |
| """ | |
| prediction_output = f""" | |
| <div class="manual-container"> | |
| {prediction_toc} | |
| <div class="manual-content">{prediction_html}</div> | |
| </div> | |
| """ | |
| evaluation_output = f""" | |
| <div class="manual-container"> | |
| {evaluation_toc} | |
| <div class="manual-content">{evaluation_html}</div> | |
| </div> | |
| """ | |
| download_output = f""" | |
| <div class="manual-container"> | |
| {download_toc} | |
| <div class="manual-content">{download_html}</div> | |
| </div> | |
| """ | |
| faq_output = f""" | |
| <div class="manual-container"> | |
| {faq_toc} | |
| <div class="manual-content">{faq_html}</div> | |
| </div> | |
| """ | |
| return training_output, prediction_output, evaluation_output, download_output, faq_output | |
| def load_manual_training(language): | |
| if language == 'Chinese': | |
| manual_path = os.path.join("src/web/manual", "TrainingManual_ZH.md") | |
| else: | |
| manual_path = os.path.join("src/web/manual", "TrainingManual_EN.md") | |
| try: | |
| with open(manual_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| except Exception as e: | |
| return f"# Error loading manual\n\n{str(e)}" | |
| def load_manual_prediction(language): | |
| if language == 'Chinese': | |
| manual_path = os.path.join("src/web/manual", "PredictionManual_ZH.md") | |
| else: | |
| manual_path = os.path.join("src/web/manual", "PredictionManual_EN.md") | |
| try: | |
| with open(manual_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| except Exception as e: | |
| return f"# Error loading manual\n\n{str(e)}" | |
| def load_manual_evaluation(language): | |
| if language == 'Chinese': | |
| manual_path = os.path.join("src/web/manual", "EvaluationManual_ZH.md") | |
| else: | |
| manual_path = os.path.join("src/web/manual", "EvaluationManual_EN.md") | |
| try: | |
| with open(manual_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| except Exception as e: | |
| return f"# Error loading manual\n\n{str(e)}" | |
| def load_manual_download(language): | |
| if language == 'Chinese': | |
| manual_path = os.path.join("src/web/manual", "DownloadManual_ZH.md") | |
| else: | |
| manual_path = os.path.join("src/web/manual", "DownloadManual_EN.md") | |
| try: | |
| with open(manual_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| except Exception as e: | |
| return f"# Error loading manual\n\n{str(e)}" | |
| def load_manual_faq(language): | |
| if language == 'Chinese': | |
| manual_path = os.path.join("src/web/manual", "QAManual_ZH.md") | |
| else: | |
| manual_path = os.path.join("src/web/manual", "QAManual_EN.md") | |
| try: | |
| with open(manual_path, "r", encoding="utf-8") as f: | |
| return f.read() | |
| except Exception as e: | |
| return f"# FAQ\n\n{str(e)}" |