# -*- coding: utf-8 -*-
"""
Gradio Web Interface for City Map Poster Generator
"""
import os
import json
import tempfile
# Monkey-patch gradio_client bug before importing gradio
# Fix for: TypeError: argument of type 'bool' is not iterable
# and APIInfoParseError: Cannot parse schema True
try:
import gradio_client.utils as client_utils
# Patch _json_schema_to_python_type to handle boolean schemas
_original_json_schema_to_python_type = client_utils._json_schema_to_python_type
def _patched_json_schema_to_python_type(schema, defs):
# Handle boolean schema (e.g., additionalProperties: true/false)
if schema is True:
return "Any"
if schema is False:
return "None"
return _original_json_schema_to_python_type(schema, defs)
client_utils._json_schema_to_python_type = _patched_json_schema_to_python_type
except Exception as e:
print(f"Warning: Could not patch gradio_client: {e}")
import gradio as gr
# Import from the main module
from cities_data import (
get_countries,
get_provinces,
get_cities,
get_districts,
translate,
get_country_key,
)
# --- Constants ---
THEMES_DIR = "themes"
FONTS_DIR = "fonts"
POSTERS_DIR = "posters"
# Layer Constants
LAYERS_EN = ["Motorway", "Primary Roads", "Secondary Roads", "Water", "Parks"]
LAYERS_CN = ["高速公路", "主干道", "次干道", "水域", "公园"]
LAYER_KEYS = ["motorway", "primary", "secondary", "water", "parks"]
def get_available_themes():
"""Scans the themes directory and returns a list of available theme names."""
if not os.path.exists(THEMES_DIR):
return []
themes = []
for file in sorted(os.listdir(THEMES_DIR)):
if file.endswith(".json"):
theme_name = file[:-5]
themes.append(theme_name)
return themes
def get_theme_choices(lang="en"):
"""Return a list of (Display Name, Internal Name) tuples for themes."""
internal_names = get_available_themes()
choices = []
for internal_name in internal_names:
theme_info = load_theme_info(internal_name)
if theme_info:
proper_name = theme_info.get("name", internal_name)
display_name = translate(proper_name, lang)
choices.append((display_name, internal_name))
else:
choices.append((internal_name, internal_name))
return choices
def load_theme_info(theme_name):
"""Load theme details for preview."""
theme_file = os.path.join(THEMES_DIR, f"{theme_name}.json")
if os.path.exists(theme_file):
with open(theme_file, "r") as f:
return json.load(f)
return None
def get_theme_preview_html(theme_name, lang="en"):
"""Generate HTML preview for a theme."""
theme = load_theme_info(theme_name)
lang_code = "en" if lang == "English" else "cn"
if not theme:
return f"
{'Theme load failed' if lang_code == 'en' else '主题加载失败'}
"
# Translated labels
labels = {
"bg": "Background" if lang_code == "en" else "背景",
"text": "Text" if lang_code == "en" else "文字",
"motorway": "Motorway" if lang_code == "en" else "高速公路",
"primary": "Primary Road" if lang_code == "en" else "主干道",
"secondary": "Secondary Road" if lang_code == "en" else "次干道",
"water": "Water" if lang_code == "en" else "水域",
"parks": "Parks" if lang_code == "en" else "公园",
}
# Create color swatches
colors = [
(labels["bg"], theme.get("bg", "#FFFFFF")),
(labels["text"], theme.get("text", "#000000")),
(labels["motorway"], theme.get("road_motorway", "#000000")),
(labels["primary"], theme.get("road_primary", "#333333")),
(labels["secondary"], theme.get("road_secondary", "#666666")),
(labels["water"], theme.get("water", "#C0C0C0")),
(labels["parks"], theme.get("parks", "#F0F0F0")),
]
display_name = translate(theme.get("name", theme_name), lang_code)
description = theme.get("description", "")
# Optional: translate description if we really want to, but might be too much work
html = f"""
{display_name}
{description}
"""
for label, color in colors:
html += f"""
"""
html += "
"
return html
def generate_poster(
location_mode,
custom_lat,
custom_lon,
custom_city_name,
custom_country_name,
country_display,
province,
city_dropdown,
district_dropdown,
theme_name,
distance,
width,
height,
output_format,
no_crop,
show_text,
poster_lang,
layers_selection,
progress=gr.Progress(),
):
"""
Generate the map poster with given parameters.
"""
# Import here to avoid circular imports and ensure THEME is set correctly
import create_map_poster as cmp
# Decode layer selection
show_motorway = any(x in ["Motorway", "高速公路"] for x in layers_selection)
show_primary = any(x in ["Primary Roads", "主干道"] for x in layers_selection)
show_secondary = any(x in ["Secondary Roads", "次干道"] for x in layers_selection)
show_water = any(x in ["Water", "水域"] for x in layers_selection)
show_parks = any(x in ["Parks", "公园"] for x in layers_selection)
# Determine display names based on poster_lang
lang_code = "en" if poster_lang == "English" else "cn"
actual_distance = distance
display_city = ""
display_country = ""
coords = None
progress(0.1, desc="正在加载主题...")
# Load theme
cmp.THEME = cmp.load_theme(theme_name)
if location_mode == "自定义坐标" or location_mode == "Custom Coordinates":
lat_val = float(custom_lat)
lon_val = float(custom_lon)
# Basic validation: Latitude must be between -90 and 90
if lat_val < -90 or lat_val > 90:
return None, f"❌ 纬度超出范围 (-90 到 90)。你是否填反了经纬度?(当前填入: {lat_val})"
coords = (lat_val, lon_val)
display_city = custom_city_name if custom_city_name else ""
display_country = custom_country_name if custom_country_name else ""
# Determine output filename component
selected_location = display_city
else:
# Standard Dropdown Logic
# Parse country
# Since we now use (Display, Value) tuples where Value is the key,
# country_display is likely already the key. get_country_key remains for safety.
selected_country = get_country_key(country_display)
# Use dropdown selection
# If district is selected and isn't "整个城市", use its coordinates
selected_location = (
district_dropdown
if (district_dropdown and district_dropdown != city_dropdown)
else city_dropdown
)
# Detect if we are selecting an entire province
is_whole_province = False
if selected_country == "中国" and selected_location:
if selected_location.endswith("_WHOLE"):
is_whole_province = True
selected_location = selected_location.replace("_WHOLE", "")
if not selected_location:
return None, "❌ 请选择城市或区县名称"
if not selected_country:
return None, "❌ 请选择国家"
# For whole province, we might want to override the distance if it's too small
if is_whole_province:
# A province is much larger than a city. Default 10km is way too small.
# We'll use a larger default or just trust the user if they've slid it up,
# but let's ensure it's at least 150km for a province.
if distance < 100000:
actual_distance = 200000 # 200km default for province
print(
f"Whole province detected ({selected_location}). Increasing distance to {actual_distance}m"
)
if selected_country == "中国" and lang_code == "cn":
if is_whole_province:
display_city = selected_location
display_country = "中国"
# Hierarchical logic for China (Chinese language)
elif district_dropdown and district_dropdown != city_dropdown:
# Case 3 & 4: District selected
display_city = district_dropdown
if province == city_dropdown:
# Case 3: District of Municipality
display_country = f"中国 {province}"
else:
# Case 4: District of Regular City
display_country = f"中国 {province} {city_dropdown}"
else:
# Case 1 & 2: City selected (or "Whole City")
display_city = city_dropdown
if province == city_dropdown:
# Case 1: Municipality
display_country = "中国"
else:
# Case 2: Regular City
display_country = f"中国 {province}"
else:
# Standard logic for International or English posters
display_city = translate(selected_location, lang_code)
display_country = translate(selected_country, lang_code)
progress(0.2, desc="正在获取坐标...")
try:
# We search coordinates using the selection
# CMP might need the "original" names if OSM behaves better with them
# For China, "广州" is better than "Guangzhou" for geopy sometimes, but geopy is usually good.
# Let's ensure we use the 'original' internal key if possible for best OSM matching
# However, for cities, we don't have a strict key map like for countries.
# We can try to translate to CN if it's a Chinese city.
search_city = selected_location
search_country = selected_country
# Use city as parent if searching for a district, otherwise use province
search_parent = (
city_dropdown
if (district_dropdown and district_dropdown != city_dropdown)
else province
)
coords = cmp.get_coordinates(
search_city, search_country, parent=search_parent
)
except Exception as e:
return None, f"❌ 无法找到城市坐标: {str(e)}"
progress(0.3, desc="正在生成海报...")
# Generate output filename (using English/Slugified names)
en_city = translate(selected_location, "en")
temp_dir = tempfile.gettempdir()
output_file = cmp.generate_output_filename(
en_city, theme_name, output_format, directory=temp_dir
)
try:
# Wrap the generator to yield status updates and final result
for status in cmp.create_poster(
display_city, # Pass translated names for display
display_country,
coords,
actual_distance,
output_file,
output_format,
width=width,
height=height,
no_crop=no_crop,
show_text=show_text,
show_motorway=show_motorway,
show_primary=show_primary,
show_secondary=show_secondary,
show_water=show_water,
show_parks=show_parks,
):
# Translate common status messages if possible
status_display = status
if lang_code == "cn":
if "Downloading street network" in status:
status_display = "正在下载街道数据..."
elif "Downloading features" in status:
status_display = "正在下载水域和公园数据..."
elif "Rendering map" in status:
status_display = "正在渲染地图..."
elif "Applying road styles" in status:
status_display = "正在应用样式..."
elif "Saving to" in status:
status_display = "正在保存海报..."
elif "Done" in status:
status_display = "完成!"
yield None, f"⏳ {status_display}", None
progress(1.0, desc="完成!")
yield output_file, f"✅ 海报生成成功!保存至: {output_file}", output_file
except Exception as e:
import traceback
traceback.print_exc()
yield None, f"❌ 生成失败: {str(e)}", None
def update_provinces(country, lang="en"):
"""Update province dropdown based on country selection and language."""
# country is the internal key
lang_code = "en" if lang == "English" else "cn"
provinces = get_provinces(country, lang_code)
if provinces:
return gr.update(choices=provinces, value=provinces[0][1], visible=True)
return gr.update(choices=[], value=None, visible=False)
def update_cities(country, province, lang="en"):
"""Update city dropdown based on province selection and language."""
# country and province are internal keys
lang_code = "en" if lang == "English" else "cn"
cities = get_cities(country, province, lang_code)
if cities:
# For China, if it's a municipality, get_cities returns [province_name]
# We should automatically trigger update_districts then.
return gr.update(choices=cities, value=cities[0][1])
return gr.update(choices=[], value=None)
def update_districts(country, province, city, lang="en"):
"""Update district dropdown based on city selection."""
if country != "中国" or not city or city.endswith("_WHOLE"):
return gr.update(choices=[], value=None, visible=False)
lang_code = "en" if lang == "English" else "cn"
districts = get_districts(country, province, city, lang_code)
if districts:
return gr.update(choices=districts, value=districts[0][1], visible=True)
return gr.update(choices=[], value=None, visible=False)
def on_theme_change(theme_name, lang="en"):
"""Update theme preview when theme changes."""
return get_theme_preview_html(theme_name, lang)
# --- Build Gradio Interface ---
def create_interface():
"""Create and return the Gradio interface."""
# Get initial data (Default English)
# Get initial data (Default English)
default_lang_code = "cn"
default_lang_radio = "中文"
countries = get_countries(default_lang_code)
theme_choices = get_theme_choices(default_lang_code)
default_country_key = "中国"
default_provinces = get_provinces(default_country_key, default_lang_code)
default_province_key = default_provinces[0][1] if default_provinces else None
default_cities = (
get_cities(default_country_key, default_province_key, default_lang_code)
if default_province_key
else []
)
default_city_key = default_cities[0][1] if default_cities else None
# Handle district initialization carefully to avoid "Value not in choices" error
default_districts = (
get_districts(
default_country_key,
default_province_key,
default_city_key,
default_lang_code,
)
if default_city_key
else []
)
default_district_key = default_districts[0][1] if default_districts else None
default_theme = theme_choices[0][1] if theme_choices else "feature_based"
default_theme = theme_choices[0][1] if theme_choices else "feature_based"
with gr.Blocks(
title="城市地图海报生成器",
theme=gr.themes.Default(),
css="""
.header-title {
text-align: center;
font-size: 2em;
font-weight: bold;
margin-bottom: 0.5em;
background: linear-gradient(135deg, #FF8C00 0%, #FFA500 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-subtitle {
text-align: center;
color: #666;
margin-bottom: 1.5em;
}
.section-title {
font-weight: 600;
font-size: 1.1em;
margin-bottom: 0.5em;
color: #333;
}
.output-image {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
""",
) as demo:
# Header
gr.HTML("""
⚠️ 注意!
• 特大城市(如北京): 当城市面积过大时,中心定位可能不准。
• 省级行政区生成超级慢!: 正在优化中。
• 地点中英文不完善 : 由于地点中英文翻译数据量过大,未能显示完善。
• 小城市 : 由于 OpenStreetMap 数据缺失,部分图层(如公园/水域)可能无法显示。
• 生成速度 : 国外地点使用国外服务器数据且渲染逻辑较基础,下载和生成速度可能较慢。
• 数据来源 : © OpenStreetMap contributors
""")
with gr.Row():
# Left Column - Controls
with gr.Column(scale=1):
# City Selection Section
gr.HTML('📍 城市选择
')
lang_radio = gr.Radio(
choices=["中文", "English"],
value="中文",
label="海报语言",
info="选择海报及界面的显示语言",
)
location_mode = gr.Radio(
choices=["城市选择", "自定义坐标"],
value="城市选择",
label="定位方式",
info="选择通过列表选择城市或输入自定义经纬度",
)
with gr.Group(visible=True) as city_selection_group:
country_dropdown = gr.Dropdown(
choices=countries,
value=default_country_key,
label="选择国家",
interactive=True,
)
province_dropdown = gr.Dropdown(
choices=default_provinces,
value=default_province_key,
label="选择省份/州",
interactive=True,
)
city_dropdown = gr.Dropdown(
choices=default_cities,
value=default_city_key,
label="选择城市",
interactive=True,
)
district_dropdown = gr.Dropdown(
choices=default_districts,
value=default_district_key,
label="选择区县",
interactive=True,
visible=(default_country_key == "中国" and default_district_key is not None),
)
with gr.Group(visible=False) as custom_coords_group:
custom_coords_info = gr.Markdown(
"💡 **填写指南**:\n"
"- **纬度 (Latitude)**: 南北向坐标,范围 -90 至 90 (中国约 18~53)。\n"
"- **经度 (Longitude)**: 东西向坐标,范围 -180 至 180 (中国约 73~135)。\n\n"
"您可以访问 [坐标拾取系统](https://map.jiqrxx.com/jingweidu/) 获取精确数值。"
)
with gr.Row():
custom_lat = gr.Number(
label="纬度 (Latitude)", value=None, precision=6
)
custom_lon = gr.Number(
label="经度 (Longitude)", value=None, precision=6
)
custom_city_name = gr.Textbox(
label="主标题 (Main Title)", placeholder="如: Shanghai 或 '我们的家'"
)
custom_country_name = gr.Textbox(
label="副标题 (Subtitle)",
placeholder="如: China 或 '2024.10.20'",
value="",
)
gr.HTML("
")
# Theme Section
gr.HTML('🎨 主题风格
')
theme_dropdown = gr.Dropdown(
choices=theme_choices,
value=default_theme,
label="选择主题",
interactive=True,
)
theme_preview = gr.HTML(
value=get_theme_preview_html(default_theme, default_lang_radio),
label="主题预览",
)
gr.HTML("
")
# Parameters Section
gr.HTML('⚙️ 参数设置
')
distance_slider = gr.Slider(
minimum=4000,
maximum=30000,
value=10000,
step=1000,
label="地图范围 (米)",
info="4000-6000: 小城区 | 8000-12000: 中等城市 | 15000+: 大都市 (范围越大生成越慢)",
)
with gr.Row():
width_input = gr.Number(
value=12.0, label="宽度 (英寸)", minimum=6, maximum=24
)
height_input = gr.Number(
value=16.0, label="高度 (英寸)", minimum=8, maximum=32
)
format_radio = gr.Radio(
choices=["png", "svg", "pdf"],
value="png",
label="输出格式",
info="PNG: 适合打印 | SVG: 矢量图 | PDF: 文档",
)
no_crop_checkbox = gr.Checkbox(
value=False,
label="保留边距 (不裁剪)",
info="勾选后保留海报边缘背景",
)
show_text_checkbox = gr.Checkbox(
value=True,
label="显示文字",
info="在海报上显示城市名和经纬度",
)
layers_checkbox = gr.CheckboxGroup(
choices=LAYERS_EN,
value=LAYERS_EN,
label="图层显示",
info="选择需要显示的地图元素",
)
# Generate Button
generate_btn = gr.Button("🚀 生成海报", variant="primary", size="lg")
# Right Column - Output
with gr.Column(scale=1):
gr.HTML('🖼️ 生成结果
')
output_image = gr.Image(
label="海报预览",
type="filepath",
elem_classes=["output-image"],
height=600,
interactive=False,
)
output_status = gr.Textbox(label="状态", interactive=False)
download_btn = gr.DownloadButton(label="📥 下载海报", visible=False)
poster_state = gr.State()
# --- Event Handlers ---
# Language change -> update all dropdowns
def on_lang_change(
lang,
current_country,
current_province,
current_city,
current_district,
current_theme,
current_layers,
):
lang_code = "en" if lang == "English" else "cn"
new_countries = get_countries(lang_code)
# Use keys to maintain selection
new_provinces = get_provinces(current_country, lang_code)
new_cities = (
get_cities(current_country, current_province, lang_code)
if current_province
else []
)
new_districts = (
get_districts(
current_country, current_province, current_city, lang_code
)
if current_city
else []
)
# Ensure district value is valid for new choices
valid_district = None
if new_districts:
if any(v == current_district for d, v in new_districts):
valid_district = current_district
else:
valid_district = new_districts[0][1]
# Themes
new_theme_choices = get_theme_choices(lang_code)
new_preview = get_theme_preview_html(current_theme, lang)
# Layers
map_en_to_key = dict(zip(LAYERS_EN, LAYER_KEYS))
map_cn_to_key = dict(zip(LAYERS_CN, LAYER_KEYS))
map_key_to_en = dict(zip(LAYER_KEYS, LAYERS_EN))
map_key_to_cn = dict(zip(LAYER_KEYS, LAYERS_CN))
current_keys = []
for x in current_layers:
if x in map_en_to_key:
current_keys.append(map_en_to_key[x])
elif x in map_cn_to_key:
current_keys.append(map_cn_to_key[x])
target_map = map_key_to_en if lang == "English" else map_key_to_cn
new_layer_choices = LAYERS_EN if lang == "English" else LAYERS_CN
new_layer_values = [target_map[k] for k in current_keys if k in target_map]
return (
gr.update(
choices=new_countries,
value=current_country,
label="Select Country" if lang == "English" else "选择国家",
),
gr.update(
choices=new_provinces,
value=current_province,
label="Select Province/State"
if lang == "English"
else "选择省份/州",
),
gr.update(
choices=new_cities,
value=current_city,
label="Select City" if lang == "English" else "选择城市",
),
gr.update(
choices=new_districts,
value=valid_district,
visible=(current_country == "中国" and valid_district is not None),
label="Select District" if lang == "English" else "选择区县",
),
gr.update(
choices=new_theme_choices,
label="Select Theme" if lang == "English" else "选择主题",
),
new_preview,
gr.update(
choices=new_layer_choices,
value=new_layer_values,
label="Layers" if lang == "English" else "图层显示",
),
gr.update(
label="Map Range (m)" if lang == "English" else "地图范围 (米)",
info="4000-6000: Small | 8000-12000: Medium | 15000+: Large"
if lang == "English"
else "4000-6000: 小城区 | 8000-12000: 中等城市 | 15000+: 大都市 (范围越大生成越慢)",
),
gr.update(label="Width (inch)" if lang == "English" else "宽度 (英寸)"),
gr.update(
label="Height (inch)" if lang == "English" else "高度 (英寸)"
),
gr.update(
label="Format" if lang == "English" else "输出格式",
info="PNG: Print | SVG: Vector | PDF: Doc"
if lang == "English"
else "PNG: 适合打印 | SVG: 矢量图 | PDF: 文档",
),
gr.update(
label="No Crop (Keep Margins)"
if lang == "English"
else "保留边距 (不裁剪)",
info="Keep background margins"
if lang == "English"
else "勾选后保留海报边缘背景",
),
gr.update(
label="Show Text" if lang == "English" else "显示文字",
info="Show city name and coordinates on poster"
if lang == "English"
else "在海报上显示城市名和经纬度",
),
gr.update(
value="🚀 Generate Poster" if lang == "English" else "🚀 生成海报"
),
gr.update(
label="📥 Download Poster" if lang == "English" else "📥 下载海报"
),
gr.update(
label="Main Title" if lang == "English" else "主标题 (Main Title)",
placeholder="e.g. Shanghai or 'Our Home'" if lang == "English" else "如: Shanghai 或 '我们的家'"
),
gr.update(
label="Subtitle" if lang == "English" else "副标题 (Subtitle)",
placeholder="e.g. China or '2024.10.20'" if lang == "English" else "如: China 或 '2024.10.20'"
),
gr.update(
value="💡 **Guide**:\n"
"- **Latitude**: North-South, range -90 to 90 (China: ~18~53).\n"
"- **Longitude**: East-West, range -180 to 180 (China: ~73~135).\n\n"
"Use [Coordinate Picker](https://map.jiqrxx.com/jingweidu/) to find exact values."
if lang == "English" else
"💡 **填写指南**:\n"
"- **纬度 (Latitude)**: 南北向坐标,范围 -90 至 90 (中国约 18~53)。\n"
"- **经度 (Longitude)**: 东西向坐标,范围 -180 至 180 (中国约 73~135)。\n\n"
"您可以访问 [坐标拾取系统](https://map.jiqrxx.com/jingweidu/) 获取精确数值。"
)
)
lang_radio.change(
fn=on_lang_change,
inputs=[
lang_radio,
country_dropdown,
province_dropdown,
city_dropdown,
district_dropdown,
theme_dropdown,
layers_checkbox,
],
outputs=[
country_dropdown,
province_dropdown,
city_dropdown,
district_dropdown,
theme_dropdown,
theme_preview,
layers_checkbox,
distance_slider,
width_input,
height_input,
format_radio,
no_crop_checkbox,
show_text_checkbox,
generate_btn,
download_btn,
custom_city_name,
custom_country_name,
custom_coords_info,
],
)
def toggle_location_mode(mode):
is_custom = mode in ["自定义坐标", "Custom Coordinates"]
return {
city_selection_group: gr.update(visible=not is_custom),
custom_coords_group: gr.update(visible=is_custom),
}
location_mode.change(
fn=toggle_location_mode,
inputs=[location_mode],
outputs=[city_selection_group, custom_coords_group],
)
# Country change -> update provinces
country_dropdown.change(
fn=update_provinces,
inputs=[country_dropdown, lang_radio],
outputs=[province_dropdown],
)
# Province change -> update cities
province_dropdown.change(
fn=update_cities,
inputs=[country_dropdown, province_dropdown, lang_radio],
outputs=[city_dropdown],
).then( # Update districts after city changes
fn=update_districts,
inputs=[country_dropdown, province_dropdown, city_dropdown, lang_radio],
outputs=[district_dropdown],
)
# City change -> update districts
city_dropdown.change(
fn=update_districts,
inputs=[country_dropdown, province_dropdown, city_dropdown, lang_radio],
outputs=[district_dropdown],
)
# Theme change -> update preview
theme_dropdown.change(
fn=on_theme_change,
inputs=[theme_dropdown, lang_radio],
outputs=[theme_preview],
)
# Generate button click
def on_generate_complete(filepath, status):
"""Handle generate completion - show download button if successful."""
if filepath and os.path.exists(filepath):
return filepath, status, gr.update(visible=True, value=filepath)
return filepath, status, gr.update(visible=False)
generate_btn.click(
fn=generate_poster,
inputs=[
location_mode,
custom_lat,
custom_lon,
custom_city_name,
custom_country_name,
country_dropdown,
province_dropdown,
city_dropdown,
district_dropdown,
theme_dropdown,
distance_slider,
width_input,
height_input,
format_radio,
no_crop_checkbox,
show_text_checkbox,
lang_radio,
layers_checkbox,
],
outputs=[output_image, output_status, poster_state],
).then(
fn=on_generate_complete,
inputs=[poster_state, output_status],
outputs=[output_image, output_status, download_btn],
)
# Footer
gr.HTML("""
""")
return demo
# --- Main Entry ---
if __name__ == "__main__":
demo = create_interface()
demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)