broadfield-dev commited on
Commit
54b40d5
Β·
verified Β·
1 Parent(s): babbd78

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +71 -90
app.py CHANGED
@@ -24,13 +24,9 @@ OUTPUT_DIR = "optimized_models"
24
  os.makedirs(OUTPUT_DIR, exist_ok=True)
25
 
26
 
27
- # --- 2. AMOP CORE PIPELINE FUNCTIONS ---
28
 
29
  def stage_1_analyze_model(model_id: str):
30
- """
31
- Performs Stage 1: Adaptive Model Analysis.
32
- Loads the model's configuration and recommends an optimization strategy.
33
- """
34
  log_stream = "[STAGE 1] Analyzing model...\n"
35
  try:
36
  config = AutoConfig.from_pretrained(model_id, trust_remote_code=True)
@@ -45,52 +41,40 @@ def stage_1_analyze_model(model_id: str):
45
  recommendation = ""
46
  if 'llama' in model_type or 'gpt' in model_type or 'mistral' in model_type:
47
  recommendation = "**Recommendation:** This is a large language model (LLM). For best CPU performance, a GGUF-based quantization strategy is typically state-of-the-art. This initial version of AMOP focuses on the ONNX pipeline. The recommended path is **Quantization -> ONNX Conversion**."
48
- elif 'bert' in model_type or 'roberta' in model_type:
49
- recommendation = "**Recommendation:** This is an encoder model. The full AMOP pipeline is recommended for a balance of size and performance: **Pruning -> Quantization -> ONNX Conversion**."
50
- elif 'vit' in model_type:
51
- recommendation = "**Recommendation:** This is a Vision Transformer. The recommended path is **Quantization -> ONNX Conversion**. Pruning may be less effective."
52
  else:
53
- recommendation = "**Recommendation:** Unrecognized architecture. The standard path of **Quantization -> ONNX Conversion** is a safe starting point."
54
 
55
  log_stream += f"Analysis complete. Architecture: {model_type}.\n"
56
- # GRADIO 5 UPDATE: Instead of gr.update(), return a new component object.
57
- return log_stream, analysis_report + "\n" + recommendation, gr.Group(visible=True)
58
  except Exception as e:
59
  error_msg = f"Failed to analyze model '{model_id}'. Error: {e}"
60
  logging.error(error_msg)
61
- return log_stream + error_msg, "Could not analyze model. Please check the model ID and try again.", gr.Group(visible=False)
62
-
63
 
64
  def stage_2_prune_model(model, prune_percentage: float):
65
  if prune_percentage == 0:
66
  return model, "Skipped pruning as percentage was 0."
67
-
68
  log_stream = "[STAGE 2] Pruning model...\n"
69
  for name, module in model.named_modules():
70
  if isinstance(module, torch.nn.Linear):
71
  prune.l1_unstructured(module, name='weight', amount=prune_percentage / 100.0)
72
  prune.remove(module, 'weight')
73
-
74
  log_stream += f"Pruning complete. Note: This version exports the original model to ONNX for maximum compatibility.\n"
75
  return model, log_stream
76
 
77
-
78
  def stage_3_and_4_quantize_and_onnx(model_id: str):
79
  log_stream = "[STAGE 3 & 4] Converting to ONNX and Quantizing...\n"
80
  try:
81
  run_id = datetime.now().strftime("%Y%m%d-%H%M%S")
82
  onnx_path = os.path.join(OUTPUT_DIR, f"{model_id.replace('/', '_')}-{run_id}-onnx")
83
- os.makedirs(onnx_path, exist_ok=True)
84
-
85
  main_export(model_id, output=onnx_path, task="auto", trust_remote_code=True)
86
  log_stream += f"Successfully exported base model to ONNX at: {onnx_path}\n"
87
 
88
  quantizer = ORTQuantizer.from_pretrained(onnx_path)
89
  dqconfig = AutoQuantizationConfig.avx512_vnni(is_static=False, per_channel=False)
90
-
91
  quantized_path = os.path.join(onnx_path, "quantized")
92
  quantizer.quantize(save_dir=quantized_path, quantization_config=dqconfig)
93
-
94
  log_stream += f"Successfully quantized model to: {quantized_path}\n"
95
  return quantized_path, log_stream
96
  except Exception as e:
@@ -98,31 +82,20 @@ def stage_3_and_4_quantize_and_onnx(model_id: str):
98
  logging.error(error_msg, exc_info=True)
99
  raise RuntimeError(error_msg)
100
 
101
-
102
- def stage_5_evaluate_and_package(
103
- model_id: str,
104
- optimized_model_path: str,
105
- pipeline_log: str,
106
- options: dict
107
- ):
108
  log_stream = "[STAGE 5] Evaluating and Packaging...\n"
109
  try:
110
  ort_model = ORTModelForCausalLM.from_pretrained(optimized_model_path)
111
  tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
112
-
113
  prompt = "My name is Philipp and I"
114
  inputs = tokenizer(prompt, return_tensors="pt")
115
-
116
  start_time = time.time()
117
  gen_tokens = ort_model.generate(**inputs, max_new_tokens=20)
118
  end_time = time.time()
119
-
120
  latency = (end_time - start_time) * 1000
121
  num_tokens = len(gen_tokens[0]) - inputs.input_ids.shape[1]
122
  ms_per_token = latency / num_tokens if num_tokens > 0 else float('inf')
123
-
124
- eval_report = f"- **Inference Latency:** {latency:.2f} ms\n"
125
- eval_report += f"- **Speed:** {ms_per_token:.2f} ms/token\n"
126
  log_stream += "Evaluation complete.\n"
127
  except Exception as e:
128
  eval_report = f"- **Evaluation Failed:** Could not run generation. This often happens if the base model is not a text-generation model. Error: {e}\n"
@@ -130,65 +103,56 @@ def stage_5_evaluate_and_package(
130
 
131
  if not HF_TOKEN:
132
  return "Skipping upload: HF_TOKEN not found.", log_stream + "Skipping upload: HF_TOKEN not found."
133
-
134
  try:
135
  repo_name = f"{model_id.split('/')[-1]}-amop-cpu"
136
  repo_url = api.create_repo(repo_id=repo_name, exist_ok=True, token=HF_TOKEN)
137
-
138
- with open("model_card_template.md", "r", encoding="utf-8") as f:
139
- template_content = f.read()
140
-
141
  model_card_content = template_content.format(
142
- repo_name=repo_name, model_id=model_id,
143
- optimization_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
144
  eval_report=eval_report, pruning_status="Enabled" if options['prune'] else "Disabled",
145
- pruning_percent=options['prune_percent'], repo_id=repo_url.repo_id,
146
- pipeline_log=pipeline_log
147
  )
148
-
149
  readme_path = os.path.join(optimized_model_path, "README.md")
150
  with open(readme_path, "w", encoding="utf-8") as f: f.write(model_card_content)
151
-
152
  tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
153
  tokenizer.save_pretrained(optimized_model_path)
154
-
155
- api.upload_folder(
156
- folder_path=optimized_model_path, repo_id=repo_url.repo_id,
157
- repo_type="model", token=HF_TOKEN
158
- )
159
-
160
- final_message = f"βœ… Success! Your optimized model is available at: [{repo_url.repo_id}](https://huggingface.co/{repo_url.repo_id})"
161
  log_stream += "Upload complete.\n"
162
  return final_message, log_stream
163
  except Exception as e:
164
  error_msg = f"Failed to upload to the Hub. Error: {e}"
165
  logging.error(error_msg, exc_info=True)
166
- return f"❌ Error: {error_msg}", log_stream + error_msg
167
 
168
 
169
- # --- 3. MAIN WORKFLOW FUNCTION (GENERATOR FOR GRADIO 5+) ---
170
 
171
  def run_amop_pipeline(model_id: str, do_prune: bool, prune_percent: float):
172
- """
173
- This is now a generator function. It 'yields' updates to the UI
174
- at each step, providing a real-time log.
175
- """
176
  if not model_id:
177
- yield "Please enter a Model ID.", ""
178
  return
179
 
180
- full_log = "[START] AMOP Pipeline Initiated.\n"
181
- yield gr.Markdown("πŸš€ Pipeline is running... Check logs for real-time updates."), full_log
182
-
 
 
 
 
 
 
 
 
183
  try:
184
  # Step 1: Load Model
185
  full_log += "Loading base model...\n"
186
- yield gr.Markdown("πŸš€ Pipeline is running... (1/5) Loading model"), full_log
187
  model = AutoModel.from_pretrained(model_id, trust_remote_code=True)
188
  full_log += f"Successfully loaded base model '{model_id}'.\n"
189
 
190
  # Step 2: Pruning
191
- yield gr.Markdown("πŸš€ Pipeline is running... (2/5) Pruning model"), full_log
192
  if do_prune:
193
  model, log = stage_2_prune_model(model, prune_percent)
194
  full_log += log
@@ -196,66 +160,83 @@ def run_amop_pipeline(model_id: str, do_prune: bool, prune_percent: float):
196
  full_log += "[STAGE 2] Pruning skipped by user.\n"
197
 
198
  # Step 3 & 4: ONNX Conversion
199
- yield gr.Markdown("πŸš€ Pipeline is running... (3/5) Converting to ONNX & Quantizing"), full_log
200
  optimized_path, log = stage_3_and_4_quantize_and_onnx(model_id)
201
  full_log += log
202
 
203
- # Step 5: Packaging
204
- yield gr.Markdown("πŸš€ Pipeline is running... (4/5) Evaluating and Packaging"), full_log
205
  options = {'prune': do_prune, 'prune_percent': prune_percent}
206
- final_status_msg, log = stage_5_evaluate_and_package(model_id, optimized_path, full_log, options)
207
  full_log += log
208
 
209
  # Final Step: Done
210
- yield gr.Markdown(final_status_msg), full_log
 
 
 
 
 
 
 
211
 
212
  except Exception as e:
213
  logging.error(f"AMOP Pipeline failed. Error: {e}", exc_info=True)
214
  full_log += f"\n[ERROR] Pipeline failed: {e}"
215
- yield f"❌ An error occurred during the pipeline. Check the logs for details.", full_log
 
 
 
 
 
 
216
 
217
 
218
- # --- 4. GRADIO USER INTERFACE (for Gradio 5+) ---
219
 
220
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
221
- gr.Markdown("# AMOP: Adaptive Model Optimization Pipeline")
222
- gr.Markdown(
223
- "**Turn any Hugging Face Hub model into a CPU-optimized version.** Enter a model ID, choose your optimizations, "
224
- "and get a new, smaller, and faster model repository ready for deployment."
225
- )
226
  if not HF_TOKEN:
227
  gr.Warning("You have not set your HF_TOKEN in the Space secrets! The final 'upload' step will be skipped. Please add a secret with the key `HF_TOKEN` and your Hugging Face write token as the value.")
228
 
229
  with gr.Row():
230
  with gr.Column(scale=1):
231
- model_id_input = gr.Textbox(label="Hugging Face Model ID", placeholder="e.g., gpt2, bert-base-uncased")
232
- analyze_button = gr.Button("1. Analyze Model")
 
 
 
 
 
233
 
234
- with gr.Group(visible=False) as optimization_options:
235
- gr.Markdown("### 2. Configure Optimization")
236
  analysis_report_output = gr.Markdown()
237
-
238
- prune_checkbox = gr.Checkbox(label="Enable Pruning (Stage 2)", value=False, info="Note: Pruning is applied conceptually; ONNX export uses the original model for wider compatibility in this version.")
239
  prune_slider = gr.Slider(minimum=0, maximum=90, value=20, step=5, label="Pruning Percentage (%)")
240
-
241
- gr.Checkbox(label="Enable Quantization & ONNX (Stages 3 & 4)", value=True, interactive=False)
242
- run_button = gr.Button("3. Run Optimization Pipeline", variant="primary")
243
 
244
  with gr.Column(scale=2):
245
  gr.Markdown("### Pipeline Status & Logs")
246
- final_output = gr.Markdown(value="*Pipeline has not been run yet.*", label="Final Result")
247
- log_output = gr.Textbox(label="Live Logs", lines=20, interactive=False)
 
 
 
248
 
 
249
  analyze_button.click(
250
  fn=stage_1_analyze_model,
251
  inputs=[model_id_input],
252
- outputs=[log_output, analysis_report_output, optimization_options]
253
  )
254
 
255
  run_button.click(
256
  fn=run_amop_pipeline,
257
  inputs=[model_id_input, prune_checkbox, prune_slider],
258
- outputs=[final_output, log_output]
259
  )
260
 
261
  if __name__ == "__main__":
 
24
  os.makedirs(OUTPUT_DIR, exist_ok=True)
25
 
26
 
27
+ # --- 2. AMOP CORE PIPELINE FUNCTIONS (Logic is the same) ---
28
 
29
  def stage_1_analyze_model(model_id: str):
 
 
 
 
30
  log_stream = "[STAGE 1] Analyzing model...\n"
31
  try:
32
  config = AutoConfig.from_pretrained(model_id, trust_remote_code=True)
 
41
  recommendation = ""
42
  if 'llama' in model_type or 'gpt' in model_type or 'mistral' in model_type:
43
  recommendation = "**Recommendation:** This is a large language model (LLM). For best CPU performance, a GGUF-based quantization strategy is typically state-of-the-art. This initial version of AMOP focuses on the ONNX pipeline. The recommended path is **Quantization -> ONNX Conversion**."
 
 
 
 
44
  else:
45
+ recommendation = "**Recommendation:** This is an encoder model or similar. The full AMOP pipeline is recommended for a balance of size and performance: **Pruning -> Quantization -> ONNX Conversion**."
46
 
47
  log_stream += f"Analysis complete. Architecture: {model_type}.\n"
48
+ ## UI/UX UPDATE ##: Return an open Accordion instead of a visible Group
49
+ return log_stream, analysis_report + "\n" + recommendation, gr.Accordion(open=True)
50
  except Exception as e:
51
  error_msg = f"Failed to analyze model '{model_id}'. Error: {e}"
52
  logging.error(error_msg)
53
+ return log_stream + error_msg, "Could not analyze model. Please check the model ID and try again.", gr.Accordion(open=False)
 
54
 
55
  def stage_2_prune_model(model, prune_percentage: float):
56
  if prune_percentage == 0:
57
  return model, "Skipped pruning as percentage was 0."
 
58
  log_stream = "[STAGE 2] Pruning model...\n"
59
  for name, module in model.named_modules():
60
  if isinstance(module, torch.nn.Linear):
61
  prune.l1_unstructured(module, name='weight', amount=prune_percentage / 100.0)
62
  prune.remove(module, 'weight')
 
63
  log_stream += f"Pruning complete. Note: This version exports the original model to ONNX for maximum compatibility.\n"
64
  return model, log_stream
65
 
 
66
  def stage_3_and_4_quantize_and_onnx(model_id: str):
67
  log_stream = "[STAGE 3 & 4] Converting to ONNX and Quantizing...\n"
68
  try:
69
  run_id = datetime.now().strftime("%Y%m%d-%H%M%S")
70
  onnx_path = os.path.join(OUTPUT_DIR, f"{model_id.replace('/', '_')}-{run_id}-onnx")
 
 
71
  main_export(model_id, output=onnx_path, task="auto", trust_remote_code=True)
72
  log_stream += f"Successfully exported base model to ONNX at: {onnx_path}\n"
73
 
74
  quantizer = ORTQuantizer.from_pretrained(onnx_path)
75
  dqconfig = AutoQuantizationConfig.avx512_vnni(is_static=False, per_channel=False)
 
76
  quantized_path = os.path.join(onnx_path, "quantized")
77
  quantizer.quantize(save_dir=quantized_path, quantization_config=dqconfig)
 
78
  log_stream += f"Successfully quantized model to: {quantized_path}\n"
79
  return quantized_path, log_stream
80
  except Exception as e:
 
82
  logging.error(error_msg, exc_info=True)
83
  raise RuntimeError(error_msg)
84
 
85
+ def stage_5_evaluate_and_package(model_id: str, optimized_model_path: str, pipeline_log: str, options: dict):
 
 
 
 
 
 
86
  log_stream = "[STAGE 5] Evaluating and Packaging...\n"
87
  try:
88
  ort_model = ORTModelForCausalLM.from_pretrained(optimized_model_path)
89
  tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
 
90
  prompt = "My name is Philipp and I"
91
  inputs = tokenizer(prompt, return_tensors="pt")
 
92
  start_time = time.time()
93
  gen_tokens = ort_model.generate(**inputs, max_new_tokens=20)
94
  end_time = time.time()
 
95
  latency = (end_time - start_time) * 1000
96
  num_tokens = len(gen_tokens[0]) - inputs.input_ids.shape[1]
97
  ms_per_token = latency / num_tokens if num_tokens > 0 else float('inf')
98
+ eval_report = f"- **Inference Latency:** {latency:.2f} ms\n- **Speed:** {ms_per_token:.2f} ms/token\n"
 
 
99
  log_stream += "Evaluation complete.\n"
100
  except Exception as e:
101
  eval_report = f"- **Evaluation Failed:** Could not run generation. This often happens if the base model is not a text-generation model. Error: {e}\n"
 
103
 
104
  if not HF_TOKEN:
105
  return "Skipping upload: HF_TOKEN not found.", log_stream + "Skipping upload: HF_TOKEN not found."
 
106
  try:
107
  repo_name = f"{model_id.split('/')[-1]}-amop-cpu"
108
  repo_url = api.create_repo(repo_id=repo_name, exist_ok=True, token=HF_TOKEN)
109
+ with open("model_card_template.md", "r", encoding="utf-8") as f: template_content = f.read()
 
 
 
110
  model_card_content = template_content.format(
111
+ repo_name=repo_name, model_id=model_id, optimization_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
 
112
  eval_report=eval_report, pruning_status="Enabled" if options['prune'] else "Disabled",
113
+ pruning_percent=options['prune_percent'], repo_id=repo_url.repo_id, pipeline_log=pipeline_log
 
114
  )
 
115
  readme_path = os.path.join(optimized_model_path, "README.md")
116
  with open(readme_path, "w", encoding="utf-8") as f: f.write(model_card_content)
 
117
  tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
118
  tokenizer.save_pretrained(optimized_model_path)
119
+ api.upload_folder(folder_path=optimized_model_path, repo_id=repo_url.repo_id, repo_type="model", token=HF_TOKEN)
120
+ final_message = f"Success! Your optimized model is available at: huggingface.co/{repo_url.repo_id}"
 
 
 
 
 
121
  log_stream += "Upload complete.\n"
122
  return final_message, log_stream
123
  except Exception as e:
124
  error_msg = f"Failed to upload to the Hub. Error: {e}"
125
  logging.error(error_msg, exc_info=True)
126
+ return f"Error: {error_msg}", log_stream + error_msg
127
 
128
 
129
+ # --- 3. MAIN WORKFLOW GENERATOR (HEAVILY UPDATED FOR UI/UX) ---
130
 
131
  def run_amop_pipeline(model_id: str, do_prune: bool, prune_percent: float):
 
 
 
 
132
  if not model_id:
133
+ yield {log_output: "Please enter a Model ID.", final_output: gr.Label(value="Idle", label="Status")}
134
  return
135
 
136
+ ## UI/UX UPDATE ##: Yield dictionaries to update multiple components at once.
137
+ # This provides immediate feedback that the process has started.
138
+ initial_log = "[START] AMOP Pipeline Initiated.\n"
139
+ yield {
140
+ run_button: gr.Button(interactive=False, value="πŸš€ Running..."),
141
+ analyze_button: gr.Button(interactive=False),
142
+ final_output: gr.Label(value={"label": "RUNNING", "confidences": None}, label="Status", show_label=True),
143
+ log_output: initial_log
144
+ }
145
+
146
+ full_log = initial_log
147
  try:
148
  # Step 1: Load Model
149
  full_log += "Loading base model...\n"
150
+ yield {final_output: gr.Label(value={"label": "Loading model (1/5)"}), log_output: full_log}
151
  model = AutoModel.from_pretrained(model_id, trust_remote_code=True)
152
  full_log += f"Successfully loaded base model '{model_id}'.\n"
153
 
154
  # Step 2: Pruning
155
+ yield {final_output: gr.Label(value={"label": "Pruning model (2/5)"}), log_output: full_log}
156
  if do_prune:
157
  model, log = stage_2_prune_model(model, prune_percent)
158
  full_log += log
 
160
  full_log += "[STAGE 2] Pruning skipped by user.\n"
161
 
162
  # Step 3 & 4: ONNX Conversion
163
+ yield {final_output: gr.Label(value={"label": "Converting to ONNX (3/5)"}), log_output: full_log}
164
  optimized_path, log = stage_3_and_4_quantize_and_onnx(model_id)
165
  full_log += log
166
 
167
+ # Step 5: Packaging and Evaluation
168
+ yield {final_output: gr.Label(value={"label": "Packaging & Uploading (4/5)"}), log_output: full_log}
169
  options = {'prune': do_prune, 'prune_percent': prune_percent}
170
+ final_message, log = stage_5_evaluate_and_package(model_id, optimized_path, full_log, options)
171
  full_log += log
172
 
173
  # Final Step: Done
174
+ yield {
175
+ final_output: gr.Label(value={"label": "SUCCESS", "confidences": None}, label="Status"),
176
+ log_output: full_log,
177
+ ## UI/UX UPDATE ##: Add a markdown component with a clickable link for the final result.
178
+ success_box: gr.Markdown(f"βœ… **Success!** Your optimized model is available here: [{model_id}-amop-cpu](https://huggingface.co/{api.whoami()['name']}/{model_id.split('/')[-1]}-amop-cpu)", visible=True),
179
+ run_button: gr.Button(interactive=True, value="3. Run Optimization Pipeline", variant="primary"),
180
+ analyze_button: gr.Button(interactive=True, value="1. Analyze Model")
181
+ }
182
 
183
  except Exception as e:
184
  logging.error(f"AMOP Pipeline failed. Error: {e}", exc_info=True)
185
  full_log += f"\n[ERROR] Pipeline failed: {e}"
186
+ yield {
187
+ final_output: gr.Label(value={"label": "ERROR", "confidences": None}, label="Status"),
188
+ log_output: full_log,
189
+ success_box: gr.Markdown(f"❌ **An error occurred.** Check the logs for details.", visible=True),
190
+ run_button: gr.Button(interactive=True, value="3. Run Optimization Pipeline", variant="primary"),
191
+ analyze_button: gr.Button(interactive=True, value="1. Analyze Model")
192
+ }
193
 
194
 
195
+ # --- 4. GRADIO USER INTERFACE (HEAVILY UPDATED FOR UI/UX) ---
196
 
197
+ with gr.Blocks(theme=gr.themes.Glass(), css=".gradio-container {background-color: #f5f5f5}") as demo:
198
+ gr.Markdown("# πŸš€ AMOP: Adaptive Model Optimization Pipeline")
199
+ gr.Markdown("Turn any Hugging Face Hub model into a CPU-optimized ONNX version. Follow the steps below.")
200
+
 
 
201
  if not HF_TOKEN:
202
  gr.Warning("You have not set your HF_TOKEN in the Space secrets! The final 'upload' step will be skipped. Please add a secret with the key `HF_TOKEN` and your Hugging Face write token as the value.")
203
 
204
  with gr.Row():
205
  with gr.Column(scale=1):
206
+ gr.Markdown("### 1. Select a Model")
207
+ model_id_input = gr.Textbox(
208
+ label="Hugging Face Model ID",
209
+ placeholder="e.g., gpt2, bert-base-uncased",
210
+ info="Enter the ID of a model from the Hub."
211
+ )
212
+ analyze_button = gr.Button("πŸ” Analyze Model", variant="secondary")
213
 
214
+ ## UI/UX UPDATE ##: Use an Accordion. It's closed by default, keeping the UI clean.
215
+ with gr.Accordion("βš™οΈ 2. Configure Optimization", open=False) as optimization_accordion:
216
  analysis_report_output = gr.Markdown()
217
+ prune_checkbox = gr.Checkbox(label="Enable Pruning (Stage 2)", value=False, info="Removes redundant weights from the model.")
 
218
  prune_slider = gr.Slider(minimum=0, maximum=90, value=20, step=5, label="Pruning Percentage (%)")
219
+ run_button = gr.Button("πŸš€ 3. Run Optimization Pipeline", variant="primary")
 
 
220
 
221
  with gr.Column(scale=2):
222
  gr.Markdown("### Pipeline Status & Logs")
223
+ ## UI/UX UPDATE ##: Use gr.Label for a clean, prominent status indicator.
224
+ final_output = gr.Label(value="Idle", label="Status", show_label=True)
225
+ ## UI/UX UPDATE ##: Add a dedicated box for the final success/error message.
226
+ success_box = gr.Markdown(visible=False)
227
+ log_output = gr.Textbox(label="Live Logs", lines=20, interactive=False, max_lines=20)
228
 
229
+ # Event Handlers
230
  analyze_button.click(
231
  fn=stage_1_analyze_model,
232
  inputs=[model_id_input],
233
+ outputs=[log_output, analysis_report_output, optimization_accordion]
234
  )
235
 
236
  run_button.click(
237
  fn=run_amop_pipeline,
238
  inputs=[model_id_input, prune_checkbox, prune_slider],
239
+ outputs=[run_button, analyze_button, final_output, log_output, success_box]
240
  )
241
 
242
  if __name__ == "__main__":