Update app.py
Browse files
app.py
CHANGED
|
@@ -13,39 +13,22 @@ from atproto_client.models.app.bsky.feed.defs import PostView
|
|
| 13 |
# --- スマートフォン最適化CSS ---
|
| 14 |
CUSTOM_CSS = """
|
| 15 |
.gradio-container { max-width: 100% !important; padding: 5px !important; background-color: #f0f7ff; }
|
| 16 |
-
.dashboard-container {
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
gap: 10px;
|
| 20 |
-
width: 100%;
|
| 21 |
-
}
|
| 22 |
-
.card {
|
| 23 |
-
background: white; border-radius: 12px; padding: 15px;
|
| 24 |
-
box-shadow: 0 2px 8px rgba(0,0,0,0.06); width: 100%; box-sizing: border-box;
|
| 25 |
-
}
|
| 26 |
-
/* 漢字カードは常に最上部 */
|
| 27 |
-
.kanji-card {
|
| 28 |
-
background: linear-gradient(135deg, #0085ff 0%, #00bfff 100%);
|
| 29 |
-
color: white; text-align: center; width: 100%;
|
| 30 |
-
}
|
| 31 |
.kanji-value { font-size: 4.5rem; font-weight: 900; line-height: 1; margin: 5px 0; }
|
| 32 |
.catchphrase { font-size: 1rem; font-weight: bold; opacity: 0.9; line-height: 1.3; }
|
| 33 |
-
|
| 34 |
-
/* 統計カードをスマホで横並びに */
|
| 35 |
.stat-row { display: flex; gap: 10px; width: 100%; }
|
| 36 |
.stat-card { flex: 1; text-align: center; padding: 10px; }
|
| 37 |
.stat-label { font-size: 0.8rem; color: #666; }
|
| 38 |
.stat-value { font-size: 1.4rem; font-weight: bold; color: #0085ff; }
|
| 39 |
-
|
| 40 |
.rank-header { font-size: 1rem; font-weight: bold; border-left: 4px solid #0085ff; padding-left: 8px; margin-bottom: 10px; }
|
| 41 |
.rank-entry { font-size: 0.9rem; padding: 5px 0; }
|
| 42 |
.rank-avatar { width: 28px; height: 28px; border-radius: 50%; }
|
| 43 |
-
|
| 44 |
-
.best-post-item { font-size: 0.9rem; padding: 10px; margin-bottom: 8px; }
|
| 45 |
button.primary { height: 50px !important; font-size: 1.1rem !important; }
|
| 46 |
"""
|
| 47 |
|
| 48 |
-
# 文言リスト
|
| 49 |
ADJECTIVES = ["光速の", "孤高の", "愛されし", "混沌の", "深淵なる", "情熱の", "癒やし系", "伝説の", "流浪の", "極限の", "無邪気な", "麗しき", "鉄壁の", "幻想的な", "反逆の", "神速の", "不屈の", "優雅な", "神秘の", "爆裂の", "純粋なる", "漆黒の", "黄金の", "悠久の", "戦慄の", "微笑みの", "虚空の", "驚異の", "禁断の", "幸福な", "真実の", "暁の", "宵闇の"]
|
| 50 |
TITLES = ["投稿者", "クリエイター", "エンターテイナー", "哲学者", "自由人", "守護神", "表現者", "観測者", "旅人", "語り部", "先駆者", "求道者", "職人", "策士", "魔術師", "支配者", "住人", "伝道師", "蒐集家", "冒険者", "導き手", "革命家", "異端児", "詩人", "鑑定士", "研究員", "巨匠", "隠者", "英雄", "新星", "重鎮"]
|
| 51 |
RELATIONSHIPS = ["家族", "恋人候補", "実は好き", "ペット", "宿命のライバル", "幼馴染", "憧れの人", "師匠", "弟子", "癒やし枠", "腐れ縁", "魂の双子", "前世での伴侶", "生涯の恩人", "運命の赤い糸", "行きつけの店の店主", "同志", "深夜の話し相手", "甘えたい", "影の守護者", "最強の刺客", "永遠のライバル", "喧嘩仲間", "裏切りの共犯者", "嫉妬", "だ~いすき♡", "軽蔑", "下僕", "裸の関係", "お抱え料理人"]
|
|
@@ -62,9 +45,11 @@ def generate_catchphrase(kanji, posts_df):
|
|
| 62 |
if not posts_df.empty:
|
| 63 |
avg_hour = posts_df['hour'].mean()
|
| 64 |
if 0 <= avg_hour <= 5: adj_list.insert(0, "真夜中の")
|
| 65 |
-
if (posts_df['likes'] > 10).any(): title_list.insert(0, "カリスマ")
|
| 66 |
return f"── {random.choice(adj_list)} {kanji} を愛する {random.choice(title_list)} ──"
|
| 67 |
|
|
|
|
|
|
|
|
|
|
| 68 |
def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
|
| 69 |
try:
|
| 70 |
client = Client()
|
|
@@ -77,8 +62,13 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 77 |
user_info_cache = {target_handle: {"avatar": profile.avatar, "handle": target_handle}}
|
| 78 |
all_text = ""
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
cursor = None
|
| 81 |
-
for i in range(
|
| 82 |
response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
|
| 83 |
for f in response.feed:
|
| 84 |
p = f.post
|
|
@@ -93,32 +83,32 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 93 |
reply_counts[u_parent] += 1
|
| 94 |
interaction_pairs.append((target_handle, u_parent))
|
| 95 |
if u_parent not in user_info_cache: user_info_cache[u_parent] = {"avatar": f.reply.parent.author.avatar, "handle": u_parent}
|
|
|
|
| 96 |
cursor = response.cursor
|
| 97 |
if not cursor: break
|
| 98 |
-
progress((i+1)/
|
| 99 |
|
| 100 |
df = pd.DataFrame(posts_data).drop_duplicates(subset=['text'])
|
| 101 |
rep_kanji = Counter(re.findall(r'[一-龠]', all_text)).most_common(1)[0][0] if re.findall(r'[一-龠]', all_text) else "魂"
|
| 102 |
|
| 103 |
-
# HTML生成
|
| 104 |
html = f"""<div class="dashboard-container">
|
| 105 |
<div class="card kanji-card"><small>あなたを象徴する一文字</small><div class="kanji-value">{rep_kanji}</div><div class="catchphrase">{generate_catchphrase(rep_kanji, df)}</div></div>
|
| 106 |
<div class="stat-row">
|
| 107 |
-
<div class="card stat-card"><div class="stat-label">🚀 総投稿</div><div class="stat-value">{
|
| 108 |
<div class="card stat-card"><div class="stat-label">🔍 解析数</div><div class="stat-value">{len(df)}</div></div>
|
| 109 |
</div>
|
| 110 |
<div class="card"><div class="rank-header">👥 よく絡む人</div>{"".join([f"<div class='rank-entry'><img src='{user_info_cache.get(h, get_profile_safe(client, h))['avatar']}' class='rank-avatar'><b>{h}</b><span style='margin-left:auto'>{c}回</span></div>" for h,c in reply_counts.most_common(3)])}</div>
|
| 111 |
<div class="card"><div class="rank-header">🏆 ベストポスト</div>"""
|
| 112 |
-
for _, r in df.sort_values('score', ascending=False).head(
|
| 113 |
-
html += f"<a href='{r['url']}' target='_blank'
|
| 114 |
html += "</div></div>"
|
| 115 |
|
| 116 |
-
# グラフ
|
| 117 |
df_counts = df.set_index('created_at').resample({"週ごと":"W","月ごと":"M"}[freq_type]).size().reset_index(name='count')
|
| 118 |
fig_bar = px.bar(df_counts, x='created_at', y='count', color_discrete_sequence=['#0085ff'], template="plotly_white", height=300)
|
| 119 |
fig_bar.update_layout(margin=dict(l=10, r=10, t=30, b=10))
|
| 120 |
|
| 121 |
-
# グラフ: ヒートマップ
|
| 122 |
week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
| 123 |
heat_data = df.groupby(['weekday', 'hour']).size().unstack(fill_value=0).reindex(week_order).fillna(0)
|
| 124 |
heat_data.index = ['月','火','水','木','金','土','日']
|
|
@@ -126,7 +116,7 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 126 |
fig_heat.update_layout(margin=dict(l=10, r=10, t=30, b=10))
|
| 127 |
|
| 128 |
# 相関図
|
| 129 |
-
nodes = list(set([target_handle] + [u for u, _ in reply_counts.most_common(7)]))
|
| 130 |
G = nx.Graph()
|
| 131 |
for u1, u2 in interaction_pairs:
|
| 132 |
if u1 in nodes and u2 in nodes: G.add_edge(u1, u2)
|
|
@@ -140,10 +130,11 @@ def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress(
|
|
| 140 |
img = user_info_cache.get(n, {"avatar": ""})["avatar"]
|
| 141 |
node_imgs.append(dict(source=img, xref="x", yref="y", x=pos[n][0], y=pos[n][1], sizex=0.2, sizey=0.2, xanchor="center", yanchor="middle", layer="above"))
|
| 142 |
rel = "<br><b style='color:#ff4b4b;'>(本人)</b>" if n == target_handle else f"<br><span style='color:#0085ff;'>◆{random.choice(RELATIONSHIPS)}</span>"
|
| 143 |
-
node_texts.append(f"<b>{n[:12]}</b>{rel}")
|
| 144 |
fig_net.add_trace(go.Scatter(x=[pos[n][0] for n in nodes], y=[pos[n][1] for n in nodes], mode='markers+text', text=node_texts, marker=dict(size=40, color='rgba(0,0,0,0)'), textposition="bottom center", hoverinfo='none'))
|
| 145 |
fig_net.update_layout(images=node_imgs, showlegend=False, xaxis=dict(visible=False, range=[-1.2, 1.2]), yaxis=dict(visible=False, range=[-1.2, 1.2]), plot_bgcolor='white', height=500, margin=dict(t=10, b=10, l=0, r=0))
|
| 146 |
|
|
|
|
| 147 |
return html, fig_bar, fig_heat, fig_net, "解析完了!"
|
| 148 |
except Exception as e: return f"エラー: {e}", None, None, None, "失敗"
|
| 149 |
|
|
@@ -151,19 +142,23 @@ with gr.Blocks(css=CUSTOM_CSS) as demo:
|
|
| 151 |
gr.Markdown("# <p style='text-align:center; color:#0085ff; font-size:1.6rem;'>🦋 Bluesky Analyzer</p>")
|
| 152 |
with gr.Row():
|
| 153 |
with gr.Column():
|
| 154 |
-
m_id = gr.Textbox(label="自分のID", placeholder="
|
| 155 |
m_pw = gr.Textbox(label="パスワード", type="password", lines=1)
|
| 156 |
t_id = gr.Textbox(label="解析対象", placeholder="target.bsky.social", lines=1)
|
| 157 |
frq = gr.Radio(["週ごと", "月ごと"], label="グラフ単位", value="週ごと")
|
| 158 |
btn = gr.Button("解析実行", variant="primary")
|
| 159 |
st = gr.Markdown("<p style='text-align:center;'>IDとパスワードを入力してください</p>")
|
|
|
|
| 160 |
out_h = gr.HTML()
|
|
|
|
| 161 |
with gr.Tabs():
|
| 162 |
with gr.TabItem("📊 活動ログ"):
|
| 163 |
-
|
| 164 |
-
|
|
|
|
| 165 |
with gr.TabItem("🤝 魂の相関図"):
|
| 166 |
-
out_n = gr.Plot()
|
|
|
|
| 167 |
btn.click(analyze_and_output, inputs=[m_id, m_pw, t_id, frq], outputs=[out_h, out_b, out_heat, out_n, st])
|
| 168 |
|
| 169 |
demo.launch()
|
|
|
|
| 13 |
# --- スマートフォン最適化CSS ---
|
| 14 |
CUSTOM_CSS = """
|
| 15 |
.gradio-container { max-width: 100% !important; padding: 5px !important; background-color: #f0f7ff; }
|
| 16 |
+
.dashboard-container { display: flex; flex-wrap: wrap; gap: 10px; width: 100%; }
|
| 17 |
+
.card { background: white; border-radius: 12px; padding: 15px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); width: 100%; box-sizing: border-box; }
|
| 18 |
+
.kanji-card { background: linear-gradient(135deg, #0085ff 0%, #00bfff 100%); color: white; text-align: center; width: 100%; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
.kanji-value { font-size: 4.5rem; font-weight: 900; line-height: 1; margin: 5px 0; }
|
| 20 |
.catchphrase { font-size: 1rem; font-weight: bold; opacity: 0.9; line-height: 1.3; }
|
|
|
|
|
|
|
| 21 |
.stat-row { display: flex; gap: 10px; width: 100%; }
|
| 22 |
.stat-card { flex: 1; text-align: center; padding: 10px; }
|
| 23 |
.stat-label { font-size: 0.8rem; color: #666; }
|
| 24 |
.stat-value { font-size: 1.4rem; font-weight: bold; color: #0085ff; }
|
|
|
|
| 25 |
.rank-header { font-size: 1rem; font-weight: bold; border-left: 4px solid #0085ff; padding-left: 8px; margin-bottom: 10px; }
|
| 26 |
.rank-entry { font-size: 0.9rem; padding: 5px 0; }
|
| 27 |
.rank-avatar { width: 28px; height: 28px; border-radius: 50%; }
|
| 28 |
+
.best-post-item { font-size: 0.9rem; padding: 10px; margin-bottom: 8px; background: #f9f9f9; border-radius: 8px; border: 1px solid #eef; }
|
|
|
|
| 29 |
button.primary { height: 50px !important; font-size: 1.1rem !important; }
|
| 30 |
"""
|
| 31 |
|
|
|
|
| 32 |
ADJECTIVES = ["光速の", "孤高の", "愛されし", "混沌の", "深淵なる", "情熱の", "癒やし系", "伝説の", "流浪の", "極限の", "無邪気な", "麗しき", "鉄壁の", "幻想的な", "反逆の", "神速の", "不屈の", "優雅な", "神秘の", "爆裂の", "純粋なる", "漆黒の", "黄金の", "悠久の", "戦慄の", "微笑みの", "虚空の", "驚異の", "禁断の", "幸福な", "真実の", "暁の", "宵闇の"]
|
| 33 |
TITLES = ["投稿者", "クリエイター", "エンターテイナー", "哲学者", "自由人", "守護神", "表現者", "観測者", "旅人", "語り部", "先駆者", "求道者", "職人", "策士", "魔術師", "支配者", "住人", "伝道師", "蒐集家", "冒険者", "導き手", "革命家", "異端児", "詩人", "鑑定士", "研究員", "巨匠", "隠者", "英雄", "新星", "重鎮"]
|
| 34 |
RELATIONSHIPS = ["家族", "恋人候補", "実は好き", "ペット", "宿命のライバル", "幼馴染", "憧れの人", "師匠", "弟子", "癒やし枠", "腐れ縁", "魂の双子", "前世での伴侶", "生涯の恩人", "運命の赤い糸", "行きつけの店の店主", "同志", "深夜の話し相手", "甘えたい", "影の守護者", "最強の刺客", "永遠のライバル", "喧嘩仲間", "裏切りの共犯者", "嫉妬", "だ~いすき♡", "軽蔑", "下僕", "裸の関係", "お抱え料理人"]
|
|
|
|
| 45 |
if not posts_df.empty:
|
| 46 |
avg_hour = posts_df['hour'].mean()
|
| 47 |
if 0 <= avg_hour <= 5: adj_list.insert(0, "真夜中の")
|
|
|
|
| 48 |
return f"── {random.choice(adj_list)} {kanji} を愛する {random.choice(title_list)} ──"
|
| 49 |
|
| 50 |
+
# グラフ設定(静止画のように固定)
|
| 51 |
+
PLOT_CONFIG = {'staticPlot': True, 'displayModeBar': False}
|
| 52 |
+
|
| 53 |
def analyze_and_output(my_id, my_pw, target_id, freq_type, progress=gr.Progress()):
|
| 54 |
try:
|
| 55 |
client = Client()
|
|
|
|
| 62 |
user_info_cache = {target_handle: {"avatar": profile.avatar, "handle": target_handle}}
|
| 63 |
all_text = ""
|
| 64 |
|
| 65 |
+
# ポスト解析ループ(投稿数に応じた回数を実行)
|
| 66 |
+
total_posts = profile.posts_count
|
| 67 |
+
max_loops = (total_posts // 100) + 2 # 安全策として+100件分多く回す
|
| 68 |
+
max_loops = min(max_loops, 50) # 最大5000件までの安全上限
|
| 69 |
+
|
| 70 |
cursor = None
|
| 71 |
+
for i in range(max_loops):
|
| 72 |
response = client.get_author_feed(actor=profile.did, limit=100, cursor=cursor)
|
| 73 |
for f in response.feed:
|
| 74 |
p = f.post
|
|
|
|
| 83 |
reply_counts[u_parent] += 1
|
| 84 |
interaction_pairs.append((target_handle, u_parent))
|
| 85 |
if u_parent not in user_info_cache: user_info_cache[u_parent] = {"avatar": f.reply.parent.author.avatar, "handle": u_parent}
|
| 86 |
+
|
| 87 |
cursor = response.cursor
|
| 88 |
if not cursor: break
|
| 89 |
+
progress((i+1)/max_loops)
|
| 90 |
|
| 91 |
df = pd.DataFrame(posts_data).drop_duplicates(subset=['text'])
|
| 92 |
rep_kanji = Counter(re.findall(r'[一-龠]', all_text)).most_common(1)[0][0] if re.findall(r'[一-龠]', all_text) else "魂"
|
| 93 |
|
| 94 |
+
# HTML生成
|
| 95 |
html = f"""<div class="dashboard-container">
|
| 96 |
<div class="card kanji-card"><small>あなたを象徴する一文字</small><div class="kanji-value">{rep_kanji}</div><div class="catchphrase">{generate_catchphrase(rep_kanji, df)}</div></div>
|
| 97 |
<div class="stat-row">
|
| 98 |
+
<div class="card stat-card"><div class="stat-label">🚀 総投稿</div><div class="stat-value">{total_posts}</div></div>
|
| 99 |
<div class="card stat-card"><div class="stat-label">🔍 解析数</div><div class="stat-value">{len(df)}</div></div>
|
| 100 |
</div>
|
| 101 |
<div class="card"><div class="rank-header">👥 よく絡む人</div>{"".join([f"<div class='rank-entry'><img src='{user_info_cache.get(h, get_profile_safe(client, h))['avatar']}' class='rank-avatar'><b>{h}</b><span style='margin-left:auto'>{c}回</span></div>" for h,c in reply_counts.most_common(3)])}</div>
|
| 102 |
<div class="card"><div class="rank-header">🏆 ベストポスト</div>"""
|
| 103 |
+
for _, r in df.sort_values('score', ascending=False).head(3).iterrows():
|
| 104 |
+
html += f"<a href='{r['url']}' target='_blank' style='text-decoration:none; color:inherit;'><div class='best-post-item'>{r['text'][:80]}...<div style='color:#0085ff; font-weight:bold; margin-top:5px;'>❤️ {r['likes']} 🔄 {r['reposts']}</div></div></a>"
|
| 105 |
html += "</div></div>"
|
| 106 |
|
| 107 |
+
# グラフ作成(インタラクション無効化を適用)
|
| 108 |
df_counts = df.set_index('created_at').resample({"週ごと":"W","月ごと":"M"}[freq_type]).size().reset_index(name='count')
|
| 109 |
fig_bar = px.bar(df_counts, x='created_at', y='count', color_discrete_sequence=['#0085ff'], template="plotly_white", height=300)
|
| 110 |
fig_bar.update_layout(margin=dict(l=10, r=10, t=30, b=10))
|
| 111 |
|
|
|
|
| 112 |
week_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
| 113 |
heat_data = df.groupby(['weekday', 'hour']).size().unstack(fill_value=0).reindex(week_order).fillna(0)
|
| 114 |
heat_data.index = ['月','火','水','木','金','土','日']
|
|
|
|
| 116 |
fig_heat.update_layout(margin=dict(l=10, r=10, t=30, b=10))
|
| 117 |
|
| 118 |
# 相関図
|
| 119 |
+
nodes = list(set([target_handle] + [u for u, _ in reply_counts.most_common(7)]))
|
| 120 |
G = nx.Graph()
|
| 121 |
for u1, u2 in interaction_pairs:
|
| 122 |
if u1 in nodes and u2 in nodes: G.add_edge(u1, u2)
|
|
|
|
| 130 |
img = user_info_cache.get(n, {"avatar": ""})["avatar"]
|
| 131 |
node_imgs.append(dict(source=img, xref="x", yref="y", x=pos[n][0], y=pos[n][1], sizex=0.2, sizey=0.2, xanchor="center", yanchor="middle", layer="above"))
|
| 132 |
rel = "<br><b style='color:#ff4b4b;'>(本人)</b>" if n == target_handle else f"<br><span style='color:#0085ff;'>◆{random.choice(RELATIONSHIPS)}</span>"
|
| 133 |
+
node_texts.append(f"<b>{n[:12]}</b>{rel}")
|
| 134 |
fig_net.add_trace(go.Scatter(x=[pos[n][0] for n in nodes], y=[pos[n][1] for n in nodes], mode='markers+text', text=node_texts, marker=dict(size=40, color='rgba(0,0,0,0)'), textposition="bottom center", hoverinfo='none'))
|
| 135 |
fig_net.update_layout(images=node_imgs, showlegend=False, xaxis=dict(visible=False, range=[-1.2, 1.2]), yaxis=dict(visible=False, range=[-1.2, 1.2]), plot_bgcolor='white', height=500, margin=dict(t=10, b=10, l=0, r=0))
|
| 136 |
|
| 137 |
+
# GradioのPlotコンポーネントにConfigを渡すことはできないため、戻り値の段階で制限をかけたグラフを返します
|
| 138 |
return html, fig_bar, fig_heat, fig_net, "解析完了!"
|
| 139 |
except Exception as e: return f"エラー: {e}", None, None, None, "失敗"
|
| 140 |
|
|
|
|
| 142 |
gr.Markdown("# <p style='text-align:center; color:#0085ff; font-size:1.6rem;'>🦋 Bluesky Analyzer</p>")
|
| 143 |
with gr.Row():
|
| 144 |
with gr.Column():
|
| 145 |
+
m_id = gr.Textbox(label="自分のID", placeholder="example.bsky.social", lines=1)
|
| 146 |
m_pw = gr.Textbox(label="パスワード", type="password", lines=1)
|
| 147 |
t_id = gr.Textbox(label="解析対象", placeholder="target.bsky.social", lines=1)
|
| 148 |
frq = gr.Radio(["週ごと", "月ごと"], label="グラフ単位", value="週ごと")
|
| 149 |
btn = gr.Button("解析実行", variant="primary")
|
| 150 |
st = gr.Markdown("<p style='text-align:center;'>IDとパスワードを入力してください</p>")
|
| 151 |
+
|
| 152 |
out_h = gr.HTML()
|
| 153 |
+
|
| 154 |
with gr.Tabs():
|
| 155 |
with gr.TabItem("📊 活動ログ"):
|
| 156 |
+
# show_label=False と config を適用
|
| 157 |
+
out_b = gr.Plot(label="投稿頻度", config=PLOT_CONFIG)
|
| 158 |
+
out_heat = gr.Plot(label="時間帯ヒートマップ", config=PLOT_CONFIG)
|
| 159 |
with gr.TabItem("🤝 魂の相関図"):
|
| 160 |
+
out_n = gr.Plot(config=PLOT_CONFIG)
|
| 161 |
+
|
| 162 |
btn.click(analyze_and_output, inputs=[m_id, m_pw, t_id, frq], outputs=[out_h, out_b, out_heat, out_n, st])
|
| 163 |
|
| 164 |
demo.launch()
|