gtani commited on
Commit
f48260f
·
verified ·
1 Parent(s): c8109a0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +243 -173
app.py CHANGED
@@ -1,72 +1,118 @@
1
- import os, base64
2
  import gradio as gr
3
  import pandas as pd
4
  from pathlib import Path
5
  from typing import Dict, List, Tuple
 
 
6
 
7
- # --- load & encode logo so it can't be clicked/downloaded ---
8
- LOGO_PATH = Path("rowsquared-logo-large.png")
9
- with open(LOGO_PATH, "rb") as f:
10
- logo_b64 = base64.b64encode(f.read()).decode()
11
-
12
- # --- load your ISCO hierarchy and build nested dict ---
13
  PROCESSED_DATA_DIR = Path(".")
 
 
 
 
 
 
 
 
 
 
14
  df_isco = (
15
  pd.read_excel(
16
  PROCESSED_DATA_DIR / "isco_imperfect.xlsx",
17
  converters={"major": str, "sub_major": str, "minor": str, "unit": str},
18
- )[["major_label","sub_major_label","minor_label","unit_label"]]
19
- .dropna().drop_duplicates().reset_index(drop=True)
 
 
20
  )
21
- hierarchy: Dict[str,Dict[str,Dict[str,List[str]]]] = {}
 
 
22
  for _, r in df_isco.iterrows():
23
- hierarchy.setdefault(r.major_label, {})\
24
- .setdefault(r.sub_major_label, {})\
25
- .setdefault(r.minor_label, [])\
26
  .append(r.unit_label)
 
 
27
  for maj in hierarchy:
28
  for sub in hierarchy[maj]:
29
  for mn in hierarchy[maj][sub]:
30
- hierarchy[maj][sub][mn] = sorted(dict.fromkeys(hierarchy[maj][sub][mn]))
31
-
32
- def majors(): return sorted(hierarchy.keys())
33
- def submajors(m): return sorted(hierarchy.get(m,{}).keys())
34
- def minors(m,s): return sorted(hierarchy.get(m,{}).get(s,{}).keys())
35
- def units(m,s,n): return hierarchy.get(m,{}).get(s,{}).get(n,[])
36
-
37
- # --- load your records ---
38
- records = pd.read_excel(PROCESSED_DATA_DIR/"isco_predictions.xlsx").copy()
39
- for c in ["major_label","sub_major_label","minor_label","unit_label"]:
40
- if c not in records: records[c] = ""
41
- if "annotated" not in records: records["annotated"] = False
42
- records.reset_index(drop=True,inplace=True)
43
-
44
- # --- clamp + save + load + handlers ---
45
- def clamp_path(maj,sub,mn,un):
46
- maj_c = majors()
47
- maj = maj if maj in maj_c else (maj_c[0] if maj_c else "")
48
- sub_c = submajors(maj)
49
- sub = sub if sub in sub_c else (sub_c[0] if sub_c else "")
50
- mn_c = minors(maj,sub)
51
- mn = mn if mn in mn_c else (mn_c[0] if mn_c else "")
52
- un_c = units(maj,sub,mn)
53
- un = un if un in un_c else (un_c[0] if un_c else "")
54
- return maj,sub,mn,un, maj_c,sub_c,mn_c,un_c
55
-
56
- def save_record(i,maj,sub,mn,un):
57
- records.loc[i,["major_label","sub_major_label","minor_label","unit_label"]] = [maj,sub,mn,un]
58
- records.loc[i,"annotated"] = True
59
-
60
- def status_text(i):
61
- return f"**Status**: {'✅ Annotated' if records.loc[i,'annotated'] else '❌ Not Annotated'}"
62
-
63
- def load_record(i):
64
- r = records.loc[i]
65
- maj,sub,mn,un,maj_c,sub_c,mn_c,un_c = clamp_path(r.major_label, r.sub_major_label, r.minor_label, r.unit_label)
66
- save_record(i,maj,sub,mn,un)
67
- md = f"## Occupation: {r.occupation_title_main}\n## Industry: {r.industry_title_main}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  return (
69
- md,
70
  status_text(i),
71
  gr.update(choices=maj_c, value=maj),
72
  gr.update(choices=sub_c, value=sub),
@@ -74,153 +120,176 @@ def load_record(i):
74
  gr.update(choices=un_c, value=un),
75
  )
76
 
77
- def on_major_change(new_maj,i):
78
- sub_c = submajors(new_maj); sub = sub_c[0] if sub_c else ""
79
- mn_c = minors(new_maj,sub); mn = mn_c[0] if mn_c else ""
80
- un_c = units(new_maj,sub,mn); un = un_c[0] if un_c else ""
81
- save_record(i,new_maj,sub,mn,un)
 
 
 
 
 
 
82
  return (
83
- gr.update(choices=majors(), value=new_maj),
84
- gr.update(choices=sub_c, value=sub),
85
- gr.update(choices=mn_c, value=mn),
86
- gr.update(choices=un_c, value=un),
87
  status_text(i),
88
  )
89
 
90
- def on_sub_change(new_sub,i,maj):
91
- mn_c = minors(maj,new_sub); mn = mn_c[0] if mn_c else ""
92
- un_c = units(maj,new_sub,mn); un = un_c[0] if un_c else ""
93
- records.loc[i,["sub_major_label","minor_label","unit_label"]] = [new_sub,mn,un]
94
- records.loc[i,"annotated"]=True
 
 
95
  return (
96
- gr.update(choices=submajors(maj),value=new_sub),
97
- gr.update(choices=mn_c, value=mn),
98
- gr.update(choices=un_c, value=un),
99
  status_text(i),
100
  )
101
 
102
- def on_minor_change(new_mn,i,maj,sub):
103
- un_c = units(maj,sub,new_mn); un = un_c[0] if un_c else ""
104
- records.loc[i,["minor_label","unit_label"]]=[new_mn,un]
105
- records.loc[i,"annotated"]=True
 
106
  return (
107
- gr.update(choices=minors(maj,sub),value=new_mn),
108
- gr.update(choices=un_c, value=un),
109
  status_text(i),
110
  )
111
 
112
- def on_unit_change(new_un,i,maj,sub,mn):
113
- un_c=units(maj,sub,mn)
114
- new_un = new_un if new_un in un_c else (un_c[0] if un_c else "")
115
- records.loc[i,"unit_label"]=new_un
116
- records.loc[i,"annotated"]=True
117
- return gr.update(choices=un_c,value=new_un), status_text(i)
118
-
119
- def go_next(i): return (i+1)%len(records)
120
- def go_prev(i): return (i-1)%len(records)
121
-
122
- def save_and_jump(i,direction):
123
- r=records.loc[i]
124
- maj,sub,mn,un,*_ = clamp_path(r.major_label,r.sub_major_label,r.minor_label,r.unit_label)
125
- save_record(i,maj,sub,mn,un)
126
- j = go_next(i) if direction=="next" else go_prev(i)
127
- return (j,)+load_record(j)
128
-
129
- def download_csv():
130
- out = PROCESSED_DATA_DIR/"annotated_output.csv"
131
- records.to_csv(out,index=False)
132
- return str(out)
133
-
134
- # --- build with login gate ---
135
- def build_gradio_app():
136
- USER = os.getenv("APP_USER","")
137
- PWD = os.getenv("APP_PASS","")
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
140
  gr.HTML("""
141
  <style>
142
- footer, .share-link, .api-link {display:none!important}
143
- #title{text-align:center;margin:0.5em}
144
- </style>""")
145
-
146
- # === capture the two panels ===
147
- with gr.Column(elem_id="login_panel") as login_panel:
148
- gr.HTML(f'<img src="data:image/png;base64,{logo_b64}" width="160" '
149
- 'style="pointer-events:none;user-select:none;display:block;margin:auto;"/>')
150
- user_in = gr.Textbox(label="Username")
151
- pass_in = gr.Textbox(label="Password", type="password")
152
- login_btn = gr.Button("🔒 Log in")
153
- login_msg = gr.Markdown("", visible=False)
154
-
155
- with gr.Column(elem_id="app_panel", visible=False) as app_panel:
156
- gr.HTML(f'<img src="data:image/png;base64,{logo_b64}" width="160" '
157
- 'style="pointer-events:none;user-select:none;display:block;margin:auto;"/>')
158
- gr.Markdown("# ISCO Annotation", elem_id="title")
159
- idx_state = gr.State(0)
160
  record_md = gr.Markdown()
161
  status_md = gr.Markdown()
162
- prev_btn = gr.Button("⬅ Previous")
163
- next_btn = gr.Button("✅ Next")
164
- major_radio = gr.Radio("Level 1 Major", choices=[], interactive=True)
165
- sub_radio = gr.Radio("Level 2 Sub-major",choices=[], interactive=True)
166
- minor_radio = gr.Radio("Level 3 Minor", choices=[], interactive=True)
167
- unit_radio = gr.Radio("Level 4 Unit", choices=[], interactive=True)
168
- download_btn = gr.Button("📥 Download CSV")
169
- download_file = gr.File(visible=False)
170
-
171
- # --- login handler ---
172
- def check_creds(u,p):
173
- if u==USER and p==PWD:
174
- return (
175
- gr.update(visible=False), # hide login
176
- gr.update(visible=True), # show app
177
- gr.update(visible=False), # hide error
178
- "" # clear user_in
179
- )
180
- else:
181
- return (None, None,
182
- gr.update(visible=True,value="❌ Bad credentials"),
183
- "")
184
-
185
- login_btn.click(
186
- check_creds,
187
- inputs=[user_in,pass_in],
188
- outputs=[login_panel,app_panel,login_msg,user_in]
189
- )
190
 
191
- # --- main app wiring ---
192
- demo.load(lambda: (0,)+load_record(0),
193
- outputs=[idx_state,record_md,status_md,
194
- major_radio,sub_radio,minor_radio,unit_radio])
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
- next_btn.click(lambda i: save_and_jump(i,"next"),
 
 
 
 
 
 
197
  inputs=[idx_state],
198
- outputs=[idx_state,record_md,status_md,
199
- major_radio,sub_radio,minor_radio,unit_radio])
200
- prev_btn.click(lambda i: save_and_jump(i,"prev"),
201
  inputs=[idx_state],
202
- outputs=[idx_state,record_md,status_md,
203
- major_radio,sub_radio,minor_radio,unit_radio])
204
-
205
- major_radio.change(on_major_change,
206
- inputs=[major_radio,idx_state],
207
- outputs=[major_radio,sub_radio,minor_radio,unit_radio,status_md])
208
- sub_radio.change(on_sub_change,
209
- inputs=[sub_radio,idx_state,major_radio],
210
- outputs=[sub_radio,minor_radio,unit_radio,status_md])
211
- minor_radio.change(on_minor_change,
212
- inputs=[minor_radio,idx_state,major_radio,sub_radio],
213
- outputs=[minor_radio,unit_radio,status_md])
214
- unit_radio.change(on_unit_change,
215
- inputs=[unit_radio,idx_state,major_radio,sub_radio,minor_radio],
216
- outputs=[unit_radio,status_md])
217
-
218
- download_btn.click(download_csv, outputs=[download_file]).then(
 
 
 
 
 
 
 
 
 
 
 
 
219
  lambda: gr.update(visible=True), None, [download_file]
220
  )
221
 
222
  return demo
223
 
 
224
  if __name__=="__main__":
225
  demo = build_gradio_app()
226
  demo.queue().launch(
@@ -229,4 +298,5 @@ if __name__=="__main__":
229
  auth=(os.getenv("APP_USER",""), os.getenv("APP_PASS","")),
230
  server_name="0.0.0.0", # optional, but explicit
231
  server_port=int(os.getenv("PORT", 7860)),
232
- )
 
 
 
1
  import gradio as gr
2
  import pandas as pd
3
  from pathlib import Path
4
  from typing import Dict, List, Tuple
5
+ import os
6
+ import base64
7
 
 
 
 
 
 
 
8
  PROCESSED_DATA_DIR = Path(".")
9
+ # Embed logo as a base64 data URI to avoid Gradio toolbar interactions
10
+ logo_path = "rowsquared-logo-large.png"
11
+ with open(logo_path, "rb") as f:
12
+ logo_b64 = base64.b64encode(f.read()).decode("utf-8")
13
+
14
+
15
+
16
+ # ----------------------------
17
+ # Data loading & preprocessing
18
+ # ----------------------------
19
  df_isco = (
20
  pd.read_excel(
21
  PROCESSED_DATA_DIR / "isco_imperfect.xlsx",
22
  converters={"major": str, "sub_major": str, "minor": str, "unit": str},
23
+ )[["major_label", "sub_major_label", "minor_label", "unit_label"]]
24
+ .dropna()
25
+ .drop_duplicates()
26
+ .reset_index(drop=True)
27
  )
28
+
29
+ # Build nested hierarchy dict: {major: {sub: {minor: [units]}}}
30
+ hierarchy: Dict[str, Dict[str, Dict[str, List[str]]]] = {}
31
  for _, r in df_isco.iterrows():
32
+ hierarchy.setdefault(r.major_label, {}) \
33
+ .setdefault(r.sub_major_label, {}) \
34
+ .setdefault(r.minor_label, []) \
35
  .append(r.unit_label)
36
+
37
+ # Ensure uniqueness & sorting at leaf lists
38
  for maj in hierarchy:
39
  for sub in hierarchy[maj]:
40
  for mn in hierarchy[maj][sub]:
41
+ hierarchy[maj][sub][mn] = sorted(list(dict.fromkeys(hierarchy[maj][sub][mn])))
42
+
43
+ # Fast helpers for children
44
+ def majors() -> List[str]:
45
+ return sorted(hierarchy.keys())
46
+
47
+ def submajors(maj: str) -> List[str]:
48
+ return sorted(hierarchy.get(maj, {}).keys())
49
+
50
+ def minors(maj: str, sub: str) -> List[str]:
51
+ return sorted(hierarchy.get(maj, {}).get(sub, {}).keys())
52
+
53
+ def units(maj: str, sub: str, mn: str) -> List[str]:
54
+ return hierarchy.get(maj, {}).get(sub, {}).get(mn, [])
55
+
56
+ # ----------------------------
57
+ # Records to annotate
58
+ # ----------------------------
59
+ records = pd.read_excel(PROCESSED_DATA_DIR / "isco_predictions.xlsx").copy()
60
+ for col in ["major_label", "sub_major_label", "minor_label", "unit_label"]:
61
+ if col not in records:
62
+ records[col] = ""
63
+
64
+ if "annotated" not in records:
65
+ records["annotated"] = False
66
+
67
+ # ensure not views
68
+ for col in ["major_label", "sub_major_label", "minor_label", "unit_label", "annotated"]:
69
+ records[col] = records[col].copy()
70
+
71
+ records.reset_index(drop=True, inplace=True)
72
+
73
+ # -----------------------------------
74
+ # Core logic: clamp & state management
75
+ # -----------------------------------
76
+ def clamp_path(maj: str, sub: str, mn: str, un: str
77
+ ) -> Tuple[str, str, str, str, List[str], List[str], List[str], List[str]]:
78
+ """Return a valid (maj, sub, mn, un) tuple + their choices lists.
79
+ Only replace a level if it's invalid for the hierarchy."""
80
+ maj_choices = majors()
81
+ if maj not in maj_choices:
82
+ maj = maj_choices[0] if maj_choices else ""
83
+
84
+ sub_choices = submajors(maj) if maj else []
85
+ if sub not in sub_choices:
86
+ sub = sub_choices[0] if sub_choices else ""
87
+
88
+ mn_choices = minors(maj, sub) if sub else []
89
+ if mn not in mn_choices:
90
+ mn = mn_choices[0] if mn_choices else ""
91
+
92
+ un_choices = units(maj, sub, mn) if mn else []
93
+ if un not in un_choices:
94
+ un = un_choices[0] if un_choices else ""
95
+
96
+ return maj, sub, mn, un, maj_choices, sub_choices, mn_choices, un_choices
97
+
98
+ def save_record(i: int, maj: str, sub: str, mn: str, un: str) -> None:
99
+ records.loc[i, ["major_label", "sub_major_label", "minor_label", "unit_label"]] = [maj, sub, mn, un]
100
+ records.loc[i, "annotated"] = True
101
+
102
+ def status_text(i: int) -> str:
103
+ return f"**Status**: {'✅ Annotated' if records.loc[i, 'annotated'] else '❌ Not Annotated'}"
104
+
105
+ def load_record(i: int):
106
+ rec = records.loc[i]
107
+ maj, sub, mn, un, maj_c, sub_c, mn_c, un_c = clamp_path(
108
+ rec["major_label"], rec["sub_major_label"], rec["minor_label"], rec["unit_label"]
109
+ )
110
+ # Persist clamped values back (only if changed)
111
+ save_record(i, maj, sub, mn, un)
112
+
113
+ record_md = f"## Occupation: {rec['occupation_title_main']}\n## Industry: {rec['industry_title_main']}"
114
  return (
115
+ record_md,
116
  status_text(i),
117
  gr.update(choices=maj_c, value=maj),
118
  gr.update(choices=sub_c, value=sub),
 
120
  gr.update(choices=un_c, value=un),
121
  )
122
 
123
+ # ---------------------
124
+ # Event handler helpers
125
+ # ---------------------
126
+ def on_major_change(new_major: str, i: int):
127
+ sub_c = submajors(new_major)
128
+ sub = sub_c[0] if sub_c else ""
129
+ mn_c = minors(new_major, sub) if sub else []
130
+ mn = mn_c[0] if mn_c else ""
131
+ un_c = units(new_major, sub, mn) if mn else []
132
+ un = un_c[0] if un_c else ""
133
+ save_record(i, new_major, sub, mn, un)
134
  return (
135
+ gr.update(choices=majors(), value=new_major),
136
+ gr.update(choices=sub_c, value=sub),
137
+ gr.update(choices=mn_c, value=mn),
138
+ gr.update(choices=un_c, value=un),
139
  status_text(i),
140
  )
141
 
142
+ def on_sub_change(new_sub: str, i: int, major: str):
143
+ mn_c = minors(major, new_sub)
144
+ mn = mn_c[0] if mn_c else ""
145
+ un_c = units(major, new_sub, mn) if mn else []
146
+ un = un_c[0] if un_c else ""
147
+ records.loc[i, ["sub_major_label", "minor_label", "unit_label"]] = [new_sub, mn, un]
148
+ records.loc[i, "annotated"] = True
149
  return (
150
+ gr.update(choices=submajors(major), value=new_sub),
151
+ gr.update(choices=mn_c, value=mn),
152
+ gr.update(choices=un_c, value=un),
153
  status_text(i),
154
  )
155
 
156
+ def on_minor_change(new_minor: str, i: int, major: str, sub: str):
157
+ un_c = units(major, sub, new_minor)
158
+ un = un_c[0] if un_c else ""
159
+ records.loc[i, ["minor_label", "unit_label"]] = [new_minor, un]
160
+ records.loc[i, "annotated"] = True
161
  return (
162
+ gr.update(choices=minors(major, sub), value=new_minor),
163
+ gr.update(choices=un_c, value=un),
164
  status_text(i),
165
  )
166
 
167
+ def on_unit_change(new_unit: str, i: int, major: str, sub: str, mn: str):
168
+ un_c = units(major, sub, mn)
169
+ if new_unit not in un_c:
170
+ new_unit = un_c[0] if un_c else ""
171
+ records.loc[i, "unit_label"] = new_unit
172
+ records.loc[i, "annotated"] = True
173
+ return gr.update(choices=un_c, value=new_unit), status_text(i)
174
+
175
+ def go_next(i: int) -> int:
176
+ return (i + 1) % len(records)
177
+
178
+ def go_prev(i: int) -> int:
179
+ return (i - 1) % len(records)
180
+
181
+ # ---- NAVIGATION: save + move + reload in ONE callback ----
 
 
 
 
 
 
 
 
 
 
 
182
 
183
+ def save_and_jump(i: int, direction: str):
184
+ # Final safety net: clamp and persist whatever is currently stored
185
+ rec = records.loc[i]
186
+ maj, sub, mn, un, *_ = clamp_path(
187
+ rec["major_label"], rec["sub_major_label"], rec["minor_label"], rec["unit_label"]
188
+ )
189
+ save_record(i, maj, sub, mn, un)
190
+ new_i = go_next(i) if direction == "next" else go_prev(i)
191
+ return (new_i,) + load_record(new_i)
192
+
193
+ def download_annotations() -> str:
194
+ path = PROCESSED_DATA_DIR / "annotated_output.csv"
195
+ records.to_csv(path, index=False)
196
+ return str(path)
197
+
198
+ # --------------
199
+ # Build the UI
200
+ # --------------
201
+ def build_gradio_app():
202
  with gr.Blocks() as demo:
203
+ with gr.Row():
204
+ with gr.Column(scale=1):
205
+ # Static logo, non-interactive
206
+ gr.HTML(
207
+ f'<img src="data:image/png;base64,{logo_b64}" width="200" style="pointer-events:none; user-select:none; display:block;" />'
208
+ )
209
+ with gr.Row():
210
+ gr.Markdown("# ISCO Annotation", elem_id="isco-title")
211
  gr.HTML("""
212
  <style>
213
+ #isco-title {
214
+ text-align: center;
215
+ width: 100%;
216
+ margin: 0.5em 0;
217
+ }
218
+ footer { display: none !important; }
219
+ .gradio-container .api-link, .gradio-container .share-link { display: none !important; }
220
+ </style>
221
+ """)
222
+
223
+ idx_state = gr.State(0)
224
+
225
+ with gr.Group():
 
 
 
 
 
226
  record_md = gr.Markdown()
227
  status_md = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
+ with gr.Row():
230
+ prev_btn = gr.Button("⬅ Previous")
231
+ next_btn = gr.Button("✅ Next")
232
+
233
+ with gr.Row():
234
+ with gr.Column():
235
+ major_radio = gr.Radio(label="Level 1: Major", choices=[], interactive=True)
236
+ with gr.Column():
237
+ sub_radio = gr.Radio(label="Level 2: Sub-major", choices=[], interactive=True)
238
+ with gr.Column():
239
+ minor_radio = gr.Radio(label="Level 3: Minor", choices=[], interactive=True)
240
+ with gr.Column():
241
+ unit_radio = gr.Radio(label="Level 4: Unit", choices=[], interactive=True)
242
+
243
+ download_btn = gr.Button("📥 Download Annotations")
244
+ download_file = gr.File(label="Annotated CSV", visible=False)
245
 
246
+ # Initial load
247
+ demo.load(
248
+ lambda: (0,) + load_record(0),
249
+ outputs=[idx_state, record_md, status_md, major_radio, sub_radio, minor_radio, unit_radio],
250
+ )
251
+
252
+ next_btn.click(lambda i: save_and_jump(i, "next"),
253
  inputs=[idx_state],
254
+ outputs=[idx_state, record_md, status_md, major_radio, sub_radio, minor_radio, unit_radio])
255
+
256
+ prev_btn.click(lambda i: save_and_jump(i, "prev"),
257
  inputs=[idx_state],
258
+ outputs=[idx_state, record_md, status_md, major_radio, sub_radio, minor_radio, unit_radio])
259
+
260
+ # Change handlers (also update status)
261
+ major_radio.change(
262
+ on_major_change,
263
+ inputs=[major_radio, idx_state],
264
+ outputs=[major_radio, sub_radio, minor_radio, unit_radio, status_md],
265
+ )
266
+
267
+ sub_radio.change(
268
+ on_sub_change,
269
+ inputs=[sub_radio, idx_state, major_radio],
270
+ outputs=[sub_radio, minor_radio, unit_radio, status_md],
271
+ )
272
+
273
+ minor_radio.change(
274
+ on_minor_change,
275
+ inputs=[minor_radio, idx_state, major_radio, sub_radio],
276
+ outputs=[minor_radio, unit_radio, status_md],
277
+ )
278
+
279
+ unit_radio.change(
280
+ on_unit_change,
281
+ inputs=[unit_radio, idx_state, major_radio, sub_radio, minor_radio],
282
+ outputs=[unit_radio, status_md],
283
+ )
284
+
285
+ # Download
286
+ download_btn.click(download_annotations, outputs=[download_file]).then(
287
  lambda: gr.update(visible=True), None, [download_file]
288
  )
289
 
290
  return demo
291
 
292
+
293
  if __name__=="__main__":
294
  demo = build_gradio_app()
295
  demo.queue().launch(
 
298
  auth=(os.getenv("APP_USER",""), os.getenv("APP_PASS","")),
299
  server_name="0.0.0.0", # optional, but explicit
300
  server_port=int(os.getenv("PORT", 7860)),
301
+ )
302
+