Spaces:
Running
Running
Commit
·
8f809e2
1
Parent(s):
be473e6
GSK-2434 Add component to show logs (#17)
Browse files- update log area (f04482da801bfe6bf8c4f1fa92fab5677aa14158)
- fix pipe io bug (e631fcc66670d557b9e8f885528a392cd01188fd)
- clean code (748c85beba7853d456e65a347bb5eb03bafbe7cc)
- remove pipe| (b0a573f944ae019c75a4cde2876fa17b10b41add)
- fix run job logs (64f50dd45d73b4615e83790e7bf6c1e9c28a346f)
- add every for logs textbox (aaa034c2aae0d0d16ce112cced026c3d0fe04104)
- refresh log files not working (89d01cfcb69c644aa39afcbbcbb88d27c337ef9d)
- show log with textbox value (f227810f647cd8c0c849bd0338f289422971772f)
- fix log refresh (7f4008b268536b4f60fc0b9b57ad9fe5331b786a)
Co-authored-by: zcy <[email protected]>
- app.py +22 -11
- app_leaderboard.py +3 -4
- app_text_classification.py +46 -181
- fetch_utils.py +0 -1
- io_utils.py +59 -2
- run_jobs.py +29 -0
- text_classification_ui_helpers.py +181 -0
- tmp/pipe +0 -0
- wordings.py +1 -1
app.py
CHANGED
|
@@ -1,17 +1,28 @@
|
|
| 1 |
|
| 2 |
-
# Start apps
|
| 3 |
-
# from pathlib import Path
|
| 4 |
-
|
| 5 |
import gradio as gr
|
| 6 |
-
|
| 7 |
from app_text_classification import get_demo as get_demo_text_classification
|
| 8 |
from app_leaderboard import get_demo as get_demo_leaderboard
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
with gr.Blocks(theme=gr.themes.Soft(primary_hue="green")) as demo:
|
| 11 |
-
with gr.Tab("Text Classification"):
|
| 12 |
-
get_demo_text_classification()
|
| 13 |
-
with gr.Tab("Leaderboard"):
|
| 14 |
-
get_demo_leaderboard()
|
| 15 |
|
| 16 |
-
demo.queue(max_size=100)
|
| 17 |
-
demo.launch(share=False)
|
|
|
|
| 1 |
|
|
|
|
|
|
|
|
|
|
| 2 |
import gradio as gr
|
| 3 |
+
import atexit
|
| 4 |
from app_text_classification import get_demo as get_demo_text_classification
|
| 5 |
from app_leaderboard import get_demo as get_demo_leaderboard
|
| 6 |
+
from run_jobs import start_process_run_job, stop_thread
|
| 7 |
+
import threading
|
| 8 |
+
|
| 9 |
+
if threading.current_thread() is not threading.main_thread():
|
| 10 |
+
t = threading.current_thread()
|
| 11 |
+
try:
|
| 12 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue="green")) as demo:
|
| 13 |
+
with gr.Tab("Text Classification"):
|
| 14 |
+
get_demo_text_classification(demo)
|
| 15 |
+
with gr.Tab("Leaderboard"):
|
| 16 |
+
get_demo_leaderboard()
|
| 17 |
+
|
| 18 |
+
start_process_run_job()
|
| 19 |
+
|
| 20 |
+
demo.queue(max_size=100)
|
| 21 |
+
demo.launch(share=False)
|
| 22 |
+
atexit.register(stop_thread)
|
| 23 |
+
|
| 24 |
+
except Exception:
|
| 25 |
+
print("stop background thread")
|
| 26 |
+
stop_thread()
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
|
|
|
|
|
app_leaderboard.py
CHANGED
|
@@ -32,7 +32,7 @@ def get_dataset_ids(ds):
|
|
| 32 |
return dataset_ids
|
| 33 |
|
| 34 |
def get_types(ds):
|
| 35 |
-
# set
|
| 36 |
types = [str(t) for t in ds.dtypes.to_list()]
|
| 37 |
types = [t.replace('object', 'markdown') for t in types]
|
| 38 |
types = [t.replace('float64', 'number') for t in types]
|
|
@@ -61,10 +61,9 @@ def get_demo():
|
|
| 61 |
|
| 62 |
column_names = records.columns.tolist()
|
| 63 |
default_columns = ['model_id', 'dataset_id', 'total_issues', 'report_link']
|
| 64 |
-
|
| 65 |
-
default_df = records[default_columns]
|
| 66 |
types = get_types(default_df)
|
| 67 |
-
display_df = get_display_df(default_df)
|
| 68 |
|
| 69 |
with gr.Row():
|
| 70 |
task_select = gr.Dropdown(label='Task', choices=['text_classification', 'tabular'], value='text_classification', interactive=True)
|
|
|
|
| 32 |
return dataset_ids
|
| 33 |
|
| 34 |
def get_types(ds):
|
| 35 |
+
# set types for each column
|
| 36 |
types = [str(t) for t in ds.dtypes.to_list()]
|
| 37 |
types = [t.replace('object', 'markdown') for t in types]
|
| 38 |
types = [t.replace('float64', 'number') for t in types]
|
|
|
|
| 61 |
|
| 62 |
column_names = records.columns.tolist()
|
| 63 |
default_columns = ['model_id', 'dataset_id', 'total_issues', 'report_link']
|
| 64 |
+
default_df = records[default_columns] # extract columns selected
|
|
|
|
| 65 |
types = get_types(default_df)
|
| 66 |
+
display_df = get_display_df(default_df) # the styled dataframe to display
|
| 67 |
|
| 68 |
with gr.Row():
|
| 69 |
task_select = gr.Dropdown(label='Task', choices=['text_classification', 'tabular'], value='text_classification', interactive=True)
|
app_text_classification.py
CHANGED
|
@@ -1,22 +1,8 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
import
|
| 3 |
-
import
|
| 4 |
-
import
|
| 5 |
-
import
|
| 6 |
-
import logging
|
| 7 |
-
import collections
|
| 8 |
-
|
| 9 |
-
import json
|
| 10 |
-
|
| 11 |
-
from transformers.pipelines import TextClassificationPipeline
|
| 12 |
-
|
| 13 |
-
from text_classification import get_labels_and_features_from_dataset, check_model, get_example_prediction
|
| 14 |
-
from io_utils import read_scanners, write_scanners, read_inference_type, read_column_mapping, write_column_mapping, write_inference_type
|
| 15 |
-
from wordings import INTRODUCTION_MD, CONFIRM_MAPPING_DETAILS_MD, CONFIRM_MAPPING_DETAILS_FAIL_RAW
|
| 16 |
-
|
| 17 |
-
HF_REPO_ID = 'HF_REPO_ID'
|
| 18 |
-
HF_SPACE_ID = 'SPACE_ID'
|
| 19 |
-
HF_WRITE_TOKEN = 'HF_WRITE_TOKEN'
|
| 20 |
|
| 21 |
MAX_LABELS = 20
|
| 22 |
MAX_FEATURES = 20
|
|
@@ -25,76 +11,7 @@ EXAMPLE_MODEL_ID = 'cardiffnlp/twitter-roberta-base-sentiment-latest'
|
|
| 25 |
EXAMPLE_DATA_ID = 'tweet_eval'
|
| 26 |
CONFIG_PATH='./config.yaml'
|
| 27 |
|
| 28 |
-
def
|
| 29 |
-
all_mappings = read_column_mapping(CONFIG_PATH)
|
| 30 |
-
|
| 31 |
-
if "labels" not in all_mappings.keys():
|
| 32 |
-
gr.Warning(CONFIRM_MAPPING_DETAILS_FAIL_RAW)
|
| 33 |
-
return gr.update(interactive=True)
|
| 34 |
-
label_mapping = all_mappings["labels"]
|
| 35 |
-
|
| 36 |
-
if "features" not in all_mappings.keys():
|
| 37 |
-
gr.Warning(CONFIRM_MAPPING_DETAILS_FAIL_RAW)
|
| 38 |
-
return gr.update(interactive=True)
|
| 39 |
-
feature_mapping = all_mappings["features"]
|
| 40 |
-
|
| 41 |
-
# TODO: Set column mapping for some dataset such as `amazon_polarity`
|
| 42 |
-
if local:
|
| 43 |
-
command = [
|
| 44 |
-
"python",
|
| 45 |
-
"cli.py",
|
| 46 |
-
"--loader", "huggingface",
|
| 47 |
-
"--model", m_id,
|
| 48 |
-
"--dataset", d_id,
|
| 49 |
-
"--dataset_config", config,
|
| 50 |
-
"--dataset_split", split,
|
| 51 |
-
"--hf_token", os.environ.get(HF_WRITE_TOKEN),
|
| 52 |
-
"--discussion_repo", os.environ.get(HF_REPO_ID) or os.environ.get(HF_SPACE_ID),
|
| 53 |
-
"--output_format", "markdown",
|
| 54 |
-
"--output_portal", "huggingface",
|
| 55 |
-
"--feature_mapping", json.dumps(feature_mapping),
|
| 56 |
-
"--label_mapping", json.dumps(label_mapping),
|
| 57 |
-
"--scan_config", "../config.yaml",
|
| 58 |
-
]
|
| 59 |
-
|
| 60 |
-
eval_str = f"[{m_id}]<{d_id}({config}, {split} set)>"
|
| 61 |
-
start = time.time()
|
| 62 |
-
logging.info(f"Start local evaluation on {eval_str}")
|
| 63 |
-
|
| 64 |
-
evaluator = subprocess.Popen(
|
| 65 |
-
command,
|
| 66 |
-
cwd=os.path.join(os.path.dirname(os.path.realpath(__file__)), "cicd"),
|
| 67 |
-
stderr=subprocess.STDOUT,
|
| 68 |
-
)
|
| 69 |
-
result = evaluator.wait()
|
| 70 |
-
|
| 71 |
-
logging.info(f"Finished local evaluation exit code {result} on {eval_str}: {time.time() - start:.2f}s")
|
| 72 |
-
|
| 73 |
-
gr.Info(f"Finished local evaluation exit code {result} on {eval_str}: {time.time() - start:.2f}s")
|
| 74 |
-
else:
|
| 75 |
-
gr.Info("TODO: Submit task to an endpoint")
|
| 76 |
-
|
| 77 |
-
return gr.update(interactive=True) # Submit button
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def check_dataset_and_get_config(dataset_id):
|
| 81 |
-
try:
|
| 82 |
-
configs = datasets.get_dataset_config_names(dataset_id)
|
| 83 |
-
return gr.Dropdown(configs, value=configs[0], visible=True)
|
| 84 |
-
except Exception:
|
| 85 |
-
# Dataset may not exist
|
| 86 |
-
pass
|
| 87 |
-
|
| 88 |
-
def check_dataset_and_get_split(dataset_id, dataset_config):
|
| 89 |
-
try:
|
| 90 |
-
splits = list(datasets.load_dataset(dataset_id, dataset_config).keys())
|
| 91 |
-
return gr.Dropdown(splits, value=splits[0], visible=True)
|
| 92 |
-
except Exception:
|
| 93 |
-
# Dataset may not exist
|
| 94 |
-
# gr.Warning(f"Failed to load dataset {dataset_id} with config {dataset_config}: {e}")
|
| 95 |
-
pass
|
| 96 |
-
|
| 97 |
-
def get_demo():
|
| 98 |
with gr.Row():
|
| 99 |
gr.Markdown(INTRODUCTION_MD)
|
| 100 |
with gr.Row():
|
|
@@ -137,6 +54,9 @@ def get_demo():
|
|
| 137 |
|
| 138 |
with gr.Accordion(label='Scanner Advance Config (optional)', open=False):
|
| 139 |
selected = read_scanners('./config.yaml')
|
|
|
|
|
|
|
|
|
|
| 140 |
scan_config = selected + ['data_leakage']
|
| 141 |
scanners = gr.CheckboxGroup(choices=scan_config, value=selected, label='Scan Settings', visible=True)
|
| 142 |
|
|
@@ -147,102 +67,21 @@ def get_demo():
|
|
| 147 |
interactive=True,
|
| 148 |
size="lg",
|
| 149 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
ds_labels, ds_features = get_labels_and_features_from_dataset(dataset_id, dataset_config, dataset_split)
|
| 155 |
-
if labels is None:
|
| 156 |
-
return
|
| 157 |
-
labels = [*labels]
|
| 158 |
-
all_mappings = read_column_mapping(CONFIG_PATH)
|
| 159 |
-
|
| 160 |
-
if "labels" not in all_mappings.keys():
|
| 161 |
-
all_mappings["labels"] = dict()
|
| 162 |
-
for i, label in enumerate(labels[:MAX_LABELS]):
|
| 163 |
-
if label:
|
| 164 |
-
all_mappings["labels"][label] = ds_labels[i]
|
| 165 |
-
|
| 166 |
-
if "features" not in all_mappings.keys():
|
| 167 |
-
all_mappings["features"] = dict()
|
| 168 |
-
for i, feat in enumerate(labels[MAX_LABELS:(MAX_LABELS + MAX_FEATURES)]):
|
| 169 |
-
if feat:
|
| 170 |
-
all_mappings["features"][feat] = ds_features[i]
|
| 171 |
-
write_column_mapping(all_mappings)
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
lables = [gr.Dropdown(label=f"{label}", choices=model_labels, value=model_id2label[i], interactive=True, visible=True) for i, label in enumerate(ds_labels[:MAX_LABELS])]
|
| 176 |
-
lables += [gr.Dropdown(visible=False) for _ in range(MAX_LABELS - len(lables))]
|
| 177 |
-
# TODO: Substitute 'text' with more features for zero-shot
|
| 178 |
-
features = [gr.Dropdown(label=f"{feature}", choices=ds_features, value=ds_features[0], interactive=True, visible=True) for feature in ['text']]
|
| 179 |
-
features += [gr.Dropdown(visible=False) for _ in range(MAX_FEATURES - len(features))]
|
| 180 |
-
return lables + features
|
| 181 |
-
|
| 182 |
-
@gr.on(triggers=[model_id_input.change, dataset_config_input.change])
|
| 183 |
-
def clear_column_mapping_config():
|
| 184 |
-
write_column_mapping(None)
|
| 185 |
-
|
| 186 |
-
@gr.on(triggers=[model_id_input.change, dataset_config_input.change, dataset_split_input.change],
|
| 187 |
inputs=[model_id_input, dataset_id_input, dataset_config_input, dataset_split_input],
|
| 188 |
outputs=[example_input, example_prediction, column_mapping_accordion, *column_mappings])
|
| 189 |
-
def check_model_and_show_prediction(model_id, dataset_id, dataset_config, dataset_split):
|
| 190 |
-
ppl = check_model(model_id)
|
| 191 |
-
if ppl is None or not isinstance(ppl, TextClassificationPipeline):
|
| 192 |
-
gr.Warning("Please check your model.")
|
| 193 |
-
return (
|
| 194 |
-
gr.update(visible=False),
|
| 195 |
-
gr.update(visible=False),
|
| 196 |
-
*[gr.update(visible=False) for _ in range(MAX_LABELS + MAX_FEATURES)]
|
| 197 |
-
)
|
| 198 |
-
|
| 199 |
-
dropdown_placement = [gr.Dropdown(visible=False) for _ in range(MAX_LABELS + MAX_FEATURES)]
|
| 200 |
-
|
| 201 |
-
if ppl is None: # pipeline not found
|
| 202 |
-
gr.Warning("Model not found")
|
| 203 |
-
return (
|
| 204 |
-
gr.update(visible=False),
|
| 205 |
-
gr.update(visible=False),
|
| 206 |
-
gr.update(visible=False, open=False),
|
| 207 |
-
*dropdown_placement
|
| 208 |
-
)
|
| 209 |
-
model_id2label = ppl.model.config.id2label
|
| 210 |
-
ds_labels, ds_features = get_labels_and_features_from_dataset(dataset_id, dataset_config, dataset_split)
|
| 211 |
-
|
| 212 |
-
# when dataset does not have labels or features
|
| 213 |
-
if not isinstance(ds_labels, list) or not isinstance(ds_features, list):
|
| 214 |
-
gr.Warning(CONFIRM_MAPPING_DETAILS_FAIL_RAW)
|
| 215 |
-
return (
|
| 216 |
-
gr.update(visible=False),
|
| 217 |
-
gr.update(visible=False),
|
| 218 |
-
gr.update(visible=False, open=False),
|
| 219 |
-
*dropdown_placement
|
| 220 |
-
)
|
| 221 |
-
|
| 222 |
-
column_mappings = list_labels_and_features_from_dataset(
|
| 223 |
-
ds_labels,
|
| 224 |
-
ds_features,
|
| 225 |
-
model_id2label,
|
| 226 |
-
)
|
| 227 |
-
|
| 228 |
-
# when labels or features are not aligned
|
| 229 |
-
# show manually column mapping
|
| 230 |
-
if collections.Counter(model_id2label.items()) != collections.Counter(ds_labels) or ds_features[0] != 'text':
|
| 231 |
-
gr.Warning(CONFIRM_MAPPING_DETAILS_FAIL_RAW)
|
| 232 |
-
return (
|
| 233 |
-
gr.update(visible=False),
|
| 234 |
-
gr.update(visible=False),
|
| 235 |
-
gr.update(visible=True, open=True),
|
| 236 |
-
*column_mappings
|
| 237 |
-
)
|
| 238 |
-
|
| 239 |
-
prediction_input, prediction_output = get_example_prediction(ppl, dataset_id, dataset_config, dataset_split)
|
| 240 |
-
return (
|
| 241 |
-
gr.update(value=prediction_input, visible=True),
|
| 242 |
-
gr.update(value=prediction_output, visible=True),
|
| 243 |
-
gr.update(visible=True, open=False),
|
| 244 |
-
*column_mappings
|
| 245 |
-
)
|
| 246 |
|
| 247 |
dataset_id_input.blur(check_dataset_and_get_config, dataset_id_input, dataset_config_input)
|
| 248 |
|
|
@@ -266,5 +105,31 @@ def get_demo():
|
|
| 266 |
run_btn.click,
|
| 267 |
],
|
| 268 |
fn=try_submit,
|
| 269 |
-
inputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
outputs=[run_btn])
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import uuid
|
| 3 |
+
from io_utils import read_scanners, write_scanners, read_inference_type, write_inference_type, get_logs_file
|
| 4 |
+
from wordings import INTRODUCTION_MD, CONFIRM_MAPPING_DETAILS_MD
|
| 5 |
+
from text_classification_ui_helpers import try_submit, check_dataset_and_get_config, check_dataset_and_get_split, check_model_and_show_prediction, write_column_mapping_to_config, get_logs_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
MAX_LABELS = 20
|
| 8 |
MAX_FEATURES = 20
|
|
|
|
| 11 |
EXAMPLE_DATA_ID = 'tweet_eval'
|
| 12 |
CONFIG_PATH='./config.yaml'
|
| 13 |
|
| 14 |
+
def get_demo(demo):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
with gr.Row():
|
| 16 |
gr.Markdown(INTRODUCTION_MD)
|
| 17 |
with gr.Row():
|
|
|
|
| 54 |
|
| 55 |
with gr.Accordion(label='Scanner Advance Config (optional)', open=False):
|
| 56 |
selected = read_scanners('./config.yaml')
|
| 57 |
+
# currently we remove data_leakage from the default scanners
|
| 58 |
+
# Reason: data_leakage barely raises any issues and takes too many requests
|
| 59 |
+
# when using inference API, causing rate limit error
|
| 60 |
scan_config = selected + ['data_leakage']
|
| 61 |
scanners = gr.CheckboxGroup(choices=scan_config, value=selected, label='Scan Settings', visible=True)
|
| 62 |
|
|
|
|
| 67 |
interactive=True,
|
| 68 |
size="lg",
|
| 69 |
)
|
| 70 |
+
|
| 71 |
+
with gr.Row():
|
| 72 |
+
uid = uuid.uuid4()
|
| 73 |
+
uid_label = gr.Textbox(label="Evaluation ID:", value=uid, visible=False, interactive=False)
|
| 74 |
+
logs = gr.Textbox(label="Giskard Bot Evaluation Log:", visible=False)
|
| 75 |
+
demo.load(get_logs_file, uid_label, logs, every=0.5)
|
| 76 |
|
| 77 |
+
gr.on(triggers=[label.change for label in column_mappings],
|
| 78 |
+
fn=write_column_mapping_to_config,
|
| 79 |
+
inputs=[dataset_id_input, dataset_config_input, dataset_split_input, *column_mappings])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
gr.on(triggers=[model_id_input.change, dataset_config_input.change, dataset_split_input.change],
|
| 82 |
+
fn=check_model_and_show_prediction,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
inputs=[model_id_input, dataset_id_input, dataset_config_input, dataset_split_input],
|
| 84 |
outputs=[example_input, example_prediction, column_mapping_accordion, *column_mappings])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
dataset_id_input.blur(check_dataset_and_get_config, dataset_id_input, dataset_config_input)
|
| 87 |
|
|
|
|
| 105 |
run_btn.click,
|
| 106 |
],
|
| 107 |
fn=try_submit,
|
| 108 |
+
inputs=[
|
| 109 |
+
model_id_input,
|
| 110 |
+
dataset_id_input,
|
| 111 |
+
dataset_config_input,
|
| 112 |
+
dataset_split_input,
|
| 113 |
+
run_local,
|
| 114 |
+
uid_label],
|
| 115 |
+
outputs=[run_btn, logs])
|
| 116 |
+
|
| 117 |
+
def enable_run_btn():
|
| 118 |
+
return (gr.update(interactive=True))
|
| 119 |
+
gr.on(
|
| 120 |
+
triggers=[
|
| 121 |
+
model_id_input.change,
|
| 122 |
+
dataset_config_input.change,
|
| 123 |
+
dataset_split_input.change,
|
| 124 |
+
run_inference.change,
|
| 125 |
+
run_local.change,
|
| 126 |
+
scanners.change],
|
| 127 |
+
fn=enable_run_btn,
|
| 128 |
+
inputs=None,
|
| 129 |
+
outputs=[run_btn])
|
| 130 |
+
|
| 131 |
+
gr.on(
|
| 132 |
+
triggers=[label.change for label in column_mappings],
|
| 133 |
+
fn=enable_run_btn,
|
| 134 |
+
inputs=None,
|
| 135 |
outputs=[run_btn])
|
fetch_utils.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
import huggingface_hub
|
| 2 |
import datasets
|
| 3 |
import logging
|
| 4 |
|
|
|
|
|
|
|
| 1 |
import datasets
|
| 2 |
import logging
|
| 3 |
|
io_utils.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
import yaml
|
|
|
|
|
|
|
| 2 |
|
| 3 |
YAML_PATH = "./config.yaml"
|
|
|
|
| 4 |
|
| 5 |
class Dumper(yaml.Dumper):
|
| 6 |
def increase_indent(self, flow=False, *args, **kwargs):
|
|
@@ -49,14 +52,17 @@ def read_column_mapping(path):
|
|
| 49 |
column_mapping = {}
|
| 50 |
with open(path, "r") as f:
|
| 51 |
config = yaml.load(f, Loader=yaml.FullLoader)
|
| 52 |
-
|
|
|
|
| 53 |
return column_mapping
|
| 54 |
|
| 55 |
# write column mapping to yaml file
|
| 56 |
def write_column_mapping(mapping):
|
| 57 |
with open(YAML_PATH, "r") as f:
|
| 58 |
config = yaml.load(f, Loader=yaml.FullLoader)
|
| 59 |
-
if
|
|
|
|
|
|
|
| 60 |
del config["column_mapping"]
|
| 61 |
else:
|
| 62 |
config["column_mapping"] = mapping
|
|
@@ -71,3 +77,54 @@ def convert_column_mapping_to_json(df, label=""):
|
|
| 71 |
for _, row in df.iterrows():
|
| 72 |
column_mapping[label].append(row.tolist())
|
| 73 |
return column_mapping
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import yaml
|
| 2 |
+
import subprocess
|
| 3 |
+
import os
|
| 4 |
|
| 5 |
YAML_PATH = "./config.yaml"
|
| 6 |
+
PIPE_PATH = "./tmp/pipe"
|
| 7 |
|
| 8 |
class Dumper(yaml.Dumper):
|
| 9 |
def increase_indent(self, flow=False, *args, **kwargs):
|
|
|
|
| 52 |
column_mapping = {}
|
| 53 |
with open(path, "r") as f:
|
| 54 |
config = yaml.load(f, Loader=yaml.FullLoader)
|
| 55 |
+
if config:
|
| 56 |
+
column_mapping = config.get("column_mapping", dict())
|
| 57 |
return column_mapping
|
| 58 |
|
| 59 |
# write column mapping to yaml file
|
| 60 |
def write_column_mapping(mapping):
|
| 61 |
with open(YAML_PATH, "r") as f:
|
| 62 |
config = yaml.load(f, Loader=yaml.FullLoader)
|
| 63 |
+
if config is None:
|
| 64 |
+
return
|
| 65 |
+
if mapping is None and "column_mapping" in config.keys():
|
| 66 |
del config["column_mapping"]
|
| 67 |
else:
|
| 68 |
config["column_mapping"] = mapping
|
|
|
|
| 77 |
for _, row in df.iterrows():
|
| 78 |
column_mapping[label].append(row.tolist())
|
| 79 |
return column_mapping
|
| 80 |
+
|
| 81 |
+
def get_logs_file(uid):
|
| 82 |
+
try:
|
| 83 |
+
file = open(f"./tmp/{uid}_log", "r")
|
| 84 |
+
return file.read()
|
| 85 |
+
except Exception:
|
| 86 |
+
return "Log file does not exist"
|
| 87 |
+
|
| 88 |
+
def write_log_to_user_file(id, log):
|
| 89 |
+
with open(f"./tmp/{id}_log", "a") as f:
|
| 90 |
+
f.write(log)
|
| 91 |
+
|
| 92 |
+
def save_job_to_pipe(id, job, lock):
|
| 93 |
+
if not os.path.exists('./tmp'):
|
| 94 |
+
os.makedirs('./tmp')
|
| 95 |
+
job = [str(i) for i in job]
|
| 96 |
+
job = ",".join(job)
|
| 97 |
+
print(job)
|
| 98 |
+
with lock:
|
| 99 |
+
with open(PIPE_PATH, "a") as f:
|
| 100 |
+
# write each element in job
|
| 101 |
+
f.write(f'{id}@{job}\n')
|
| 102 |
+
|
| 103 |
+
def pop_job_from_pipe():
|
| 104 |
+
if not os.path.exists(PIPE_PATH):
|
| 105 |
+
return
|
| 106 |
+
with open(PIPE_PATH, "r") as f:
|
| 107 |
+
job = f.readline().strip()
|
| 108 |
+
remaining = f.readlines()
|
| 109 |
+
f.close()
|
| 110 |
+
print(job, remaining, ">>>>")
|
| 111 |
+
with open(PIPE_PATH, "w") as f:
|
| 112 |
+
f.write("\n".join(remaining))
|
| 113 |
+
f.close()
|
| 114 |
+
if len(job) == 0:
|
| 115 |
+
return
|
| 116 |
+
job_info = job.split('\n')[0].split("@")
|
| 117 |
+
if len(job_info) != 2:
|
| 118 |
+
raise ValueError("Invalid job info: ", job_info)
|
| 119 |
+
|
| 120 |
+
write_log_to_user_file(job_info[0], f"Running job {job_info}")
|
| 121 |
+
command = job_info[1].split(",")
|
| 122 |
+
|
| 123 |
+
write_log_to_user_file(job_info[0], f"Running command {command}")
|
| 124 |
+
log_file = open(f"./tmp/{job_info[0]}_log", "a")
|
| 125 |
+
subprocess.Popen(
|
| 126 |
+
command,
|
| 127 |
+
cwd=os.path.join(os.path.dirname(os.path.realpath(__file__)), "cicd"),
|
| 128 |
+
stdout=log_file,
|
| 129 |
+
stderr=log_file,
|
| 130 |
+
)
|
run_jobs.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from io_utils import pop_job_from_pipe
|
| 2 |
+
import time
|
| 3 |
+
import threading
|
| 4 |
+
|
| 5 |
+
def start_process_run_job():
|
| 6 |
+
try:
|
| 7 |
+
print("Running jobs in thread")
|
| 8 |
+
global thread
|
| 9 |
+
thread = threading.Thread(target=run_job)
|
| 10 |
+
thread.daemon = True
|
| 11 |
+
thread.do_run = True
|
| 12 |
+
thread.start()
|
| 13 |
+
|
| 14 |
+
except Exception as e:
|
| 15 |
+
print("Failed to start thread: ", e)
|
| 16 |
+
def stop_thread():
|
| 17 |
+
print("Stop thread")
|
| 18 |
+
thread.do_run = False
|
| 19 |
+
|
| 20 |
+
def run_job():
|
| 21 |
+
while True:
|
| 22 |
+
print(thread.do_run)
|
| 23 |
+
try:
|
| 24 |
+
pop_job_from_pipe()
|
| 25 |
+
time.sleep(10)
|
| 26 |
+
except KeyboardInterrupt:
|
| 27 |
+
print("KeyboardInterrupt stop background thread")
|
| 28 |
+
stop_thread()
|
| 29 |
+
break
|
text_classification_ui_helpers.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from wordings import CONFIRM_MAPPING_DETAILS_FAIL_RAW
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
import logging
|
| 6 |
+
import threading
|
| 7 |
+
from io_utils import read_column_mapping, write_column_mapping, save_job_to_pipe, write_log_to_user_file
|
| 8 |
+
import datasets
|
| 9 |
+
import collections
|
| 10 |
+
from text_classification import get_labels_and_features_from_dataset, check_model, get_example_prediction
|
| 11 |
+
from transformers.pipelines import TextClassificationPipeline
|
| 12 |
+
|
| 13 |
+
MAX_LABELS = 20
|
| 14 |
+
MAX_FEATURES = 20
|
| 15 |
+
|
| 16 |
+
HF_REPO_ID = 'HF_REPO_ID'
|
| 17 |
+
HF_SPACE_ID = 'SPACE_ID'
|
| 18 |
+
HF_WRITE_TOKEN = 'HF_WRITE_TOKEN'
|
| 19 |
+
CONFIG_PATH = "./config.yaml"
|
| 20 |
+
|
| 21 |
+
def check_dataset_and_get_config(dataset_id):
|
| 22 |
+
try:
|
| 23 |
+
write_column_mapping(None)
|
| 24 |
+
configs = datasets.get_dataset_config_names(dataset_id)
|
| 25 |
+
return gr.Dropdown(configs, value=configs[0], visible=True)
|
| 26 |
+
except Exception:
|
| 27 |
+
# Dataset may not exist
|
| 28 |
+
pass
|
| 29 |
+
|
| 30 |
+
def check_dataset_and_get_split(dataset_id, dataset_config):
|
| 31 |
+
try:
|
| 32 |
+
splits = list(datasets.load_dataset(dataset_id, dataset_config).keys())
|
| 33 |
+
return gr.Dropdown(splits, value=splits[0], visible=True)
|
| 34 |
+
except Exception:
|
| 35 |
+
# Dataset may not exist
|
| 36 |
+
# gr.Warning(f"Failed to load dataset {dataset_id} with config {dataset_config}: {e}")
|
| 37 |
+
pass
|
| 38 |
+
|
| 39 |
+
def write_column_mapping_to_config(dataset_id, dataset_config, dataset_split, *labels):
|
| 40 |
+
ds_labels, ds_features = get_labels_and_features_from_dataset(dataset_id, dataset_config, dataset_split)
|
| 41 |
+
if labels is None:
|
| 42 |
+
return
|
| 43 |
+
labels = [*labels]
|
| 44 |
+
all_mappings = read_column_mapping(CONFIG_PATH)
|
| 45 |
+
|
| 46 |
+
if all_mappings is None:
|
| 47 |
+
all_mappings = dict()
|
| 48 |
+
|
| 49 |
+
if "labels" not in all_mappings.keys():
|
| 50 |
+
all_mappings["labels"] = dict()
|
| 51 |
+
for i, label in enumerate(labels[:MAX_LABELS]):
|
| 52 |
+
if label:
|
| 53 |
+
all_mappings["labels"][label] = ds_labels[i]
|
| 54 |
+
|
| 55 |
+
if "features" not in all_mappings.keys():
|
| 56 |
+
all_mappings["features"] = dict()
|
| 57 |
+
for i, feat in enumerate(labels[MAX_LABELS:(MAX_LABELS + MAX_FEATURES)]):
|
| 58 |
+
if feat:
|
| 59 |
+
all_mappings["features"][feat] = ds_features[i]
|
| 60 |
+
write_column_mapping(all_mappings)
|
| 61 |
+
|
| 62 |
+
def list_labels_and_features_from_dataset(ds_labels, ds_features, model_id2label):
|
| 63 |
+
model_labels = list(model_id2label.values())
|
| 64 |
+
len_model_labels = len(model_labels)
|
| 65 |
+
print(model_labels, model_id2label, 3%len_model_labels)
|
| 66 |
+
lables = [gr.Dropdown(label=f"{label}", choices=model_labels, value=model_id2label[i%len_model_labels], interactive=True, visible=True) for i, label in enumerate(ds_labels[:MAX_LABELS])]
|
| 67 |
+
lables += [gr.Dropdown(visible=False) for _ in range(MAX_LABELS - len(lables))]
|
| 68 |
+
# TODO: Substitute 'text' with more features for zero-shot
|
| 69 |
+
features = [gr.Dropdown(label=f"{feature}", choices=ds_features, value=ds_features[0], interactive=True, visible=True) for feature in ['text']]
|
| 70 |
+
features += [gr.Dropdown(visible=False) for _ in range(MAX_FEATURES - len(features))]
|
| 71 |
+
return lables + features
|
| 72 |
+
|
| 73 |
+
def check_model_and_show_prediction(model_id, dataset_id, dataset_config, dataset_split):
|
| 74 |
+
ppl = check_model(model_id)
|
| 75 |
+
if ppl is None or not isinstance(ppl, TextClassificationPipeline):
|
| 76 |
+
gr.Warning("Please check your model.")
|
| 77 |
+
return (
|
| 78 |
+
gr.update(visible=False),
|
| 79 |
+
gr.update(visible=False),
|
| 80 |
+
*[gr.update(visible=False) for _ in range(MAX_LABELS + MAX_FEATURES)]
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
dropdown_placement = [gr.Dropdown(visible=False) for _ in range(MAX_LABELS + MAX_FEATURES)]
|
| 84 |
+
|
| 85 |
+
if ppl is None: # pipeline not found
|
| 86 |
+
gr.Warning("Model not found")
|
| 87 |
+
return (
|
| 88 |
+
gr.update(visible=False),
|
| 89 |
+
gr.update(visible=False),
|
| 90 |
+
gr.update(visible=False, open=False),
|
| 91 |
+
*dropdown_placement
|
| 92 |
+
)
|
| 93 |
+
model_id2label = ppl.model.config.id2label
|
| 94 |
+
ds_labels, ds_features = get_labels_and_features_from_dataset(dataset_id, dataset_config, dataset_split)
|
| 95 |
+
|
| 96 |
+
# when dataset does not have labels or features
|
| 97 |
+
if not isinstance(ds_labels, list) or not isinstance(ds_features, list):
|
| 98 |
+
# gr.Warning(CONFIRM_MAPPING_DETAILS_FAIL_RAW)
|
| 99 |
+
return (
|
| 100 |
+
gr.update(visible=False),
|
| 101 |
+
gr.update(visible=False),
|
| 102 |
+
gr.update(visible=False, open=False),
|
| 103 |
+
*dropdown_placement
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
column_mappings = list_labels_and_features_from_dataset(
|
| 107 |
+
ds_labels,
|
| 108 |
+
ds_features,
|
| 109 |
+
model_id2label,
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# when labels or features are not aligned
|
| 113 |
+
# show manually column mapping
|
| 114 |
+
if collections.Counter(model_id2label.values()) != collections.Counter(ds_labels) or ds_features[0] != 'text':
|
| 115 |
+
gr.Warning(CONFIRM_MAPPING_DETAILS_FAIL_RAW)
|
| 116 |
+
return (
|
| 117 |
+
gr.update(visible=False),
|
| 118 |
+
gr.update(visible=False),
|
| 119 |
+
gr.update(visible=True, open=True),
|
| 120 |
+
*column_mappings
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
prediction_input, prediction_output = get_example_prediction(ppl, dataset_id, dataset_config, dataset_split)
|
| 124 |
+
return (
|
| 125 |
+
gr.update(value=prediction_input, visible=True),
|
| 126 |
+
gr.update(value=prediction_output, visible=True),
|
| 127 |
+
gr.update(visible=True, open=False),
|
| 128 |
+
*column_mappings
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
def try_submit(m_id, d_id, config, split, local, uid):
|
| 132 |
+
all_mappings = read_column_mapping(CONFIG_PATH)
|
| 133 |
+
|
| 134 |
+
if all_mappings is None:
|
| 135 |
+
gr.Warning(CONFIRM_MAPPING_DETAILS_FAIL_RAW)
|
| 136 |
+
return (gr.update(interactive=True), gr.update(visible=False))
|
| 137 |
+
|
| 138 |
+
if "labels" not in all_mappings.keys():
|
| 139 |
+
gr.Warning(CONFIRM_MAPPING_DETAILS_FAIL_RAW)
|
| 140 |
+
return (gr.update(interactive=True), gr.update(visible=False))
|
| 141 |
+
label_mapping = all_mappings["labels"]
|
| 142 |
+
|
| 143 |
+
if "features" not in all_mappings.keys():
|
| 144 |
+
gr.Warning(CONFIRM_MAPPING_DETAILS_FAIL_RAW)
|
| 145 |
+
return (gr.update(interactive=True), gr.update(visible=False))
|
| 146 |
+
feature_mapping = all_mappings["features"]
|
| 147 |
+
|
| 148 |
+
# TODO: Set column mapping for some dataset such as `amazon_polarity`
|
| 149 |
+
if local:
|
| 150 |
+
command = [
|
| 151 |
+
"python",
|
| 152 |
+
"cli.py",
|
| 153 |
+
"--loader", "huggingface",
|
| 154 |
+
"--model", m_id,
|
| 155 |
+
"--dataset", d_id,
|
| 156 |
+
"--dataset_config", config,
|
| 157 |
+
"--dataset_split", split,
|
| 158 |
+
"--hf_token", os.environ.get(HF_WRITE_TOKEN),
|
| 159 |
+
"--discussion_repo", os.environ.get(HF_REPO_ID) or os.environ.get(HF_SPACE_ID),
|
| 160 |
+
"--output_format", "markdown",
|
| 161 |
+
"--output_portal", "huggingface",
|
| 162 |
+
"--feature_mapping", json.dumps(feature_mapping),
|
| 163 |
+
"--label_mapping", json.dumps(label_mapping),
|
| 164 |
+
"--scan_config", "../config.yaml",
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
eval_str = f"[{m_id}]<{d_id}({config}, {split} set)>"
|
| 168 |
+
logging.info(f"Start local evaluation on {eval_str}")
|
| 169 |
+
save_job_to_pipe(uid, command, threading.Lock())
|
| 170 |
+
write_log_to_user_file(uid, f"Start local evaluation on {eval_str}. Please wait for your job to start...\n")
|
| 171 |
+
gr.Info(f"Start local evaluation on {eval_str}")
|
| 172 |
+
|
| 173 |
+
return (
|
| 174 |
+
gr.update(interactive=False),
|
| 175 |
+
gr.update(lines=5, visible=True, interactive=False))
|
| 176 |
+
|
| 177 |
+
else:
|
| 178 |
+
gr.Info("TODO: Submit task to an endpoint")
|
| 179 |
+
|
| 180 |
+
return (gr.update(interactive=True), # Submit button
|
| 181 |
+
gr.update(visible=False))
|
tmp/pipe
ADDED
|
File without changes
|
wordings.py
CHANGED
|
@@ -8,7 +8,7 @@ CONFIRM_MAPPING_DETAILS_MD = '''
|
|
| 8 |
<h1 style="text-align: center;">
|
| 9 |
Confirm Pre-processing Details
|
| 10 |
</h1>
|
| 11 |
-
Please confirm the pre-processing details below. If you are not sure, please double check your model and dataset.
|
| 12 |
'''
|
| 13 |
CONFIRM_MAPPING_DETAILS_FAIL_MD = '''
|
| 14 |
<h1 style="text-align: center;">
|
|
|
|
| 8 |
<h1 style="text-align: center;">
|
| 9 |
Confirm Pre-processing Details
|
| 10 |
</h1>
|
| 11 |
+
Please confirm the pre-processing details below. Align the column names of your model in the <b>dropdown</b> menu to your dataset's. If you are not sure, please double check your model and dataset.
|
| 12 |
'''
|
| 13 |
CONFIRM_MAPPING_DETAILS_FAIL_MD = '''
|
| 14 |
<h1 style="text-align: center;">
|