BriranSus commited on
Commit
d3950b9
·
1 Parent(s): 5b86bdc

feat: add HTML and CSS

Browse files
Files changed (1) hide show
  1. app.py +365 -58
app.py CHANGED
@@ -6,6 +6,11 @@ from transformers import ViTModel
6
  from PIL import Image
7
  import pickle
8
  import re
 
 
 
 
 
9
 
10
  class Vocabulary:
11
  def __init__(self, freq_threshold=5):
@@ -36,11 +41,9 @@ class Encoder(nn.Module):
36
  def __init__(self, embed_dim, freeze=False):
37
  super().__init__()
38
  self.vit = ViTModel.from_pretrained("facebook/vit-mae-base")
39
-
40
  if freeze:
41
  for param in self.vit.parameters():
42
  param.requires_grad = False
43
-
44
  self.linear = nn.Sequential(
45
  nn.Linear(self.vit.config.hidden_size, embed_dim),
46
  nn.ReLU(),
@@ -61,9 +64,7 @@ class MultiHeadAttention(nn.Module):
61
  self.num_heads = num_heads
62
  self.hidden_dim = hidden_dim
63
  self.head_dim = hidden_dim // num_heads
64
-
65
  assert hidden_dim % num_heads == 0, "hidden_dim must be divisible by num_heads"
66
-
67
  self.query = nn.Linear(hidden_dim, hidden_dim)
68
  self.key = nn.Linear(encoder_dim, hidden_dim)
69
  self.value = nn.Linear(encoder_dim, hidden_dim)
@@ -74,7 +75,6 @@ class MultiHeadAttention(nn.Module):
74
  Q = self.query(hidden).view(B, self.num_heads, self.head_dim)
75
  K = self.key(encoder_outputs).view(B, N, self.num_heads, self.head_dim).transpose(1, 2)
76
  V = self.value(encoder_outputs).view(B, N, self.num_heads, self.head_dim).transpose(1, 2)
77
-
78
  scores = torch.matmul(Q.unsqueeze(2), K.transpose(-2, -1)) / (self.head_dim ** 0.5)
79
  attn = torch.softmax(scores, dim=-1)
80
  context = torch.matmul(attn, V)
@@ -93,7 +93,6 @@ class Decoder(nn.Module):
93
  def generate(self, features, max_len=50, start_index=1, end_index=2, beam_size=3, beam_search=True):
94
  B = features.size(0)
95
  device = features.device
96
-
97
  states = (torch.zeros(self.lstm.num_layers, B, self.lstm.hidden_size, device=device),
98
  torch.zeros(self.lstm.num_layers, B, self.lstm.hidden_size, device=device))
99
 
@@ -124,16 +123,13 @@ class Decoder(nn.Module):
124
  logits = self.fc(out.squeeze(1))
125
  log_probs = torch.log_softmax(logits, dim=1)
126
  top_log_probs, top_indices = log_probs.topk(beam_size, dim=1)
127
-
128
  for k in range(beam_size):
129
  next_seq = seq + [top_indices[0, k].item()]
130
  next_log_prob = log_prob + top_log_probs[0, k].item()
131
  new_beams.append((next_seq, next_log_prob, (h_new, c_new)))
132
-
133
  new_beams = sorted(new_beams, key=lambda x: x[1], reverse=True)[:beam_size]
134
  beams = new_beams
135
  if all(seq[-1] == end_index for seq, _, _ in beams): break
136
-
137
  best_seq = beams[0][0]
138
  if best_seq[0] == start_index: best_seq = best_seq[1:]
139
  return best_seq
@@ -149,48 +145,138 @@ class Model(nn.Module):
149
  captions = self.decoder.generate(features, max_len=max_len, beam_search=True)
150
  return captions
151
 
 
 
 
 
152
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
153
  EMBED_DIM = 256
154
  HIDDEN_DIM = 512
155
  VOCAB_PATH = "vocab-v4.pkl"
156
- MODEL_PATH = "vit_lstm_best-v4.pth"
157
-
158
- print("Loading Vocabulary...")
159
- try:
160
- with open(VOCAB_PATH, "rb") as f:
161
- vocab = pickle.load(f)
162
- print(f"Vocabulary Loaded. Size: {len(vocab)}")
163
- except FileNotFoundError:
164
- raise RuntimeError("vocab.pkl not found! Please upload it.")
165
-
166
- print("Initializing Model...")
167
- encoder = Encoder(EMBED_DIM, freeze=True)
168
- decoder = Decoder(EMBED_DIM, HIDDEN_DIM, len(vocab))
169
- model = Model(encoder, decoder).to(DEVICE)
170
-
171
- print("Loading Weights...")
172
- try:
173
- checkpoint = torch.load(MODEL_PATH, map_location=DEVICE)
174
- if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint:
175
- model.load_state_dict(checkpoint['model_state_dict'])
176
- else:
177
- model.load_state_dict(checkpoint)
178
- model.eval()
179
- print("Model Loaded Successfully!")
180
- except FileNotFoundError:
181
- raise RuntimeError("vit_lstm.pth not found! Please upload it.")
182
- except Exception as e:
183
- print(f"Warning loading weights: {e}")
184
-
185
- inference_transform = transforms.Compose([
186
- transforms.Resize((224, 224)),
187
- transforms.ToTensor(),
188
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
189
- ])
190
-
191
- def generate_caption(image):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  if image is None:
193
- return "Please upload an image."
 
 
 
 
194
 
195
  try:
196
  pil_image = image.convert("RGB")
@@ -202,24 +288,245 @@ def generate_caption(image):
202
  result_words = []
203
  for idx in output_indices:
204
  word = vocab.itos.get(idx, "<UNK>")
205
- if word == "<EOS>":
206
- break
207
  if word not in ("<SOS>", "<PAD>"):
208
  result_words.append(word)
209
 
210
  caption = " ".join(result_words)
211
- return caption
 
 
212
 
213
  except Exception as e:
214
- return f"Error occurred: {str(e)}"
215
-
216
- iface = gr.Interface(
217
- fn=generate_caption,
218
- inputs=gr.Image(type="pil", label="Upload Image"),
219
- outputs=gr.Textbox(label="Generated Caption"),
220
- title="ViT + LSTM Image Captioning",
221
- description="Upload an image to generate a caption using a Vision Transformer (Encoder) and LSTM (Decoder) architecture."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
  if __name__ == "__main__":
225
- iface.launch()
 
6
  from PIL import Image
7
  import pickle
8
  import re
9
+ import os
10
+
11
+ # ==========================================
12
+ # 1. CLASS DEFINITIONS (Must Match Training)
13
+ # ==========================================
14
 
15
  class Vocabulary:
16
  def __init__(self, freq_threshold=5):
 
41
  def __init__(self, embed_dim, freeze=False):
42
  super().__init__()
43
  self.vit = ViTModel.from_pretrained("facebook/vit-mae-base")
 
44
  if freeze:
45
  for param in self.vit.parameters():
46
  param.requires_grad = False
 
47
  self.linear = nn.Sequential(
48
  nn.Linear(self.vit.config.hidden_size, embed_dim),
49
  nn.ReLU(),
 
64
  self.num_heads = num_heads
65
  self.hidden_dim = hidden_dim
66
  self.head_dim = hidden_dim // num_heads
 
67
  assert hidden_dim % num_heads == 0, "hidden_dim must be divisible by num_heads"
 
68
  self.query = nn.Linear(hidden_dim, hidden_dim)
69
  self.key = nn.Linear(encoder_dim, hidden_dim)
70
  self.value = nn.Linear(encoder_dim, hidden_dim)
 
75
  Q = self.query(hidden).view(B, self.num_heads, self.head_dim)
76
  K = self.key(encoder_outputs).view(B, N, self.num_heads, self.head_dim).transpose(1, 2)
77
  V = self.value(encoder_outputs).view(B, N, self.num_heads, self.head_dim).transpose(1, 2)
 
78
  scores = torch.matmul(Q.unsqueeze(2), K.transpose(-2, -1)) / (self.head_dim ** 0.5)
79
  attn = torch.softmax(scores, dim=-1)
80
  context = torch.matmul(attn, V)
 
93
  def generate(self, features, max_len=50, start_index=1, end_index=2, beam_size=3, beam_search=True):
94
  B = features.size(0)
95
  device = features.device
 
96
  states = (torch.zeros(self.lstm.num_layers, B, self.lstm.hidden_size, device=device),
97
  torch.zeros(self.lstm.num_layers, B, self.lstm.hidden_size, device=device))
98
 
 
123
  logits = self.fc(out.squeeze(1))
124
  log_probs = torch.log_softmax(logits, dim=1)
125
  top_log_probs, top_indices = log_probs.topk(beam_size, dim=1)
 
126
  for k in range(beam_size):
127
  next_seq = seq + [top_indices[0, k].item()]
128
  next_log_prob = log_prob + top_log_probs[0, k].item()
129
  new_beams.append((next_seq, next_log_prob, (h_new, c_new)))
 
130
  new_beams = sorted(new_beams, key=lambda x: x[1], reverse=True)[:beam_size]
131
  beams = new_beams
132
  if all(seq[-1] == end_index for seq, _, _ in beams): break
 
133
  best_seq = beams[0][0]
134
  if best_seq[0] == start_index: best_seq = best_seq[1:]
135
  return best_seq
 
145
  captions = self.decoder.generate(features, max_len=max_len, beam_search=True)
146
  return captions
147
 
148
+ # ==========================================
149
+ # 2. SETUP AND LOADING
150
+ # ==========================================
151
+
152
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
153
  EMBED_DIM = 256
154
  HIDDEN_DIM = 512
155
  VOCAB_PATH = "vocab-v4.pkl"
156
+ MODEL_PATH = "vit_lstm_best-v4.pth"
157
+
158
+ # Global Variables
159
+ vocab = None
160
+ model = None
161
+ inference_transform = None
162
+
163
+ def load_system():
164
+ global vocab, model, inference_transform
165
+ print("Loading Vocabulary...")
166
+ try:
167
+ with open(VOCAB_PATH, "rb") as f:
168
+ vocab = pickle.load(f)
169
+ except Exception as e:
170
+ return f"Error loading vocab: {e}"
171
+
172
+ print("Initializing Model...")
173
+ encoder = Encoder(EMBED_DIM, freeze=True)
174
+ decoder = Decoder(EMBED_DIM, HIDDEN_DIM, len(vocab))
175
+ model = Model(encoder, decoder).to(DEVICE)
176
+
177
+ print("Loading Weights...")
178
+ try:
179
+ checkpoint = torch.load(MODEL_PATH, map_location=DEVICE)
180
+ if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint:
181
+ model.load_state_dict(checkpoint['model_state_dict'])
182
+ else:
183
+ model.load_state_dict(checkpoint)
184
+ model.eval()
185
+ except Exception as e:
186
+ return f"Error loading model weights: {e}"
187
+
188
+ inference_transform = transforms.Compose([
189
+ transforms.Resize((224, 224)),
190
+ transforms.ToTensor(),
191
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
192
+ ])
193
+ return "System Loaded"
194
+
195
+ # Load on startup
196
+ load_status = load_system()
197
+
198
+ # ==========================================
199
+ # 3. HTML FORMATTERS
200
+ # ==========================================
201
+
202
+ def format_loading_html():
203
+ return """
204
+ <div class="loading-box" style="
205
+ text-align: center;
206
+ padding: 40px;
207
+ border: 2px solid #6B7280;
208
+ border-radius: 15px;
209
+ background-color: #27272A;
210
+ ">
211
+ <h3 style="color: #6B7280; margin-bottom: 5px;">Status</h3>
212
+ <h2 style="color: #F3F4F6; font-size: 24px; margin: 10px 0;">Analyzing Image...</h2>
213
+
214
+ <div style="font-size: 48px; font-weight: bold; color: #6B7280;">
215
+ ...
216
+ </div>
217
+
218
+ <p style="color: #9CA3AF; font-weight: bold; margin-top: 5px;">Please wait...</p>
219
+ </div>
220
+ """
221
+
222
+ def format_result_html(caption):
223
+ return f"""
224
+ <div style="
225
+ text-align: center;
226
+ padding: 30px;
227
+ border: 2px solid #4F46E5;
228
+ border-radius: 15px;
229
+ background-color: #27272A;
230
+ box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.1);
231
+ ">
232
+ <h3 style="color: #818CF8; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 2px;">Generated Caption</h3>
233
+ <div style="
234
+ font-size: 28px;
235
+ font-weight: bold;
236
+ color: #F9FAFB;
237
+ margin: 20px 0;
238
+ line-height: 1.4;
239
+ ">
240
+ "{caption}"
241
+ </div>
242
+ <p style="color: #34D399; font-size: 14px;">✓ Analysis Complete</p>
243
+ </div>
244
+ """
245
+
246
+ def format_initial_html():
247
+ return """
248
+ <div style="
249
+ text-align: center;
250
+ padding: 40px;
251
+ border: 2px dashed #4B5563;
252
+ border-radius: 15px;
253
+ background-color: #1F2937;
254
+ color: #9CA3AF;
255
+ ">
256
+ <h3>Output Area</h3>
257
+ <p>Your generated caption will appear here.</p>
258
+ </div>
259
+ """
260
+
261
+ def format_error_html(error):
262
+ return f"""
263
+ <div style="text-align: center; padding: 20px; border: 2px solid #EF4444; border-radius: 15px; background-color: #450A0A;">
264
+ <h3 style="color: #F87171;">Error</h3>
265
+ <p style="color: #FECACA;">{error}</p>
266
+ </div>
267
+ """
268
+
269
+ # ==========================================
270
+ # 4. PREDICTION LOGIC
271
+ # ==========================================
272
+
273
+ def predict(image):
274
  if image is None:
275
+ yield format_error_html("No image uploaded"), ""
276
+ return
277
+
278
+ # Yield loading state
279
+ yield format_loading_html(), ""
280
 
281
  try:
282
  pil_image = image.convert("RGB")
 
288
  result_words = []
289
  for idx in output_indices:
290
  word = vocab.itos.get(idx, "<UNK>")
291
+ if word == "<EOS>": break
 
292
  if word not in ("<SOS>", "<PAD>"):
293
  result_words.append(word)
294
 
295
  caption = " ".join(result_words)
296
+
297
+ # Yield final result (HTML for display, Raw Text for clipboard)
298
+ yield format_result_html(caption), caption
299
 
300
  except Exception as e:
301
+ yield format_error_html(str(e)), ""
302
+
303
+ # ==========================================
304
+ # 5. JAVASCRIPT & CSS
305
+ # ==========================================
306
+
307
+ # JS for Copying text and Toggling Modal
308
+ custom_js = """
309
+ <script>
310
+ function copyToClipboard() {
311
+ // Select the hidden textarea
312
+ const textarea = document.querySelector('#hidden_caption_output textarea');
313
+ if (!textarea || !textarea.value) {
314
+ alert("No caption to copy!");
315
+ return;
316
+ }
317
+
318
+ // Copy logic
319
+ navigator.clipboard.writeText(textarea.value).then(function() {
320
+ // Show styled toast
321
+ const toast = document.createElement("div");
322
+ toast.innerText = "Caption Copied!";
323
+ toast.style.position = "fixed";
324
+ toast.style.bottom = "20px";
325
+ toast.style.right = "20px";
326
+ toast.style.backgroundColor = "#10B981";
327
+ toast.style.color = "white";
328
+ toast.style.padding = "10px 20px";
329
+ toast.style.borderRadius = "5px";
330
+ toast.style.zIndex = "9999";
331
+ document.body.appendChild(toast);
332
+ setTimeout(() => toast.remove(), 2000);
333
+ }, function(err) {
334
+ console.error('Async: Could not copy text: ', err);
335
+ });
336
+ }
337
+
338
+ function toggleApiModal() {
339
+ const modal = document.getElementById('api_modal');
340
+ if (modal.style.display === 'flex') {
341
+ modal.style.display = 'none';
342
+ } else {
343
+ modal.style.display = 'flex';
344
+ }
345
+ }
346
+
347
+ function closeApiModal(e) {
348
+ if (e.target.id === 'api_modal') {
349
+ document.getElementById('api_modal').style.display = 'none';
350
+ }
351
+ }
352
+ </script>
353
+ """
354
+
355
+ # CSS for Layout, Dark Mode, and Modal
356
+ custom_css = """
357
+ body { background-color: #111827; }
358
+ .container { max-width: 800px; margin: auto; padding-top: 20px; }
359
+ .header { text-align: center; margin-bottom: 30px; }
360
+ .header h1 { color: #818CF8; font-size: 2.5rem; }
361
+ .header p { color: #9CA3AF; }
362
+
363
+ /* Pulse Animation for Loading */
364
+ @keyframes pulse {
365
+ 0%, 100% { opacity: 1; }
366
+ 50% { opacity: 0.5; }
367
+ }
368
+ .loading-box { animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
369
+
370
+ /* Modal Styles */
371
+ #api_modal {
372
+ display: none; /* Hidden by default */
373
+ position: fixed;
374
+ z-index: 1000;
375
+ left: 0;
376
+ top: 0;
377
+ width: 100%;
378
+ height: 100%;
379
+ overflow: auto;
380
+ background-color: rgba(0,0,0,0.8);
381
+ justify-content: center;
382
+ align-items: center;
383
+ }
384
+ .modal-content {
385
+ background-color: #1F2937;
386
+ margin: auto;
387
+ padding: 30px;
388
+ border: 1px solid #374151;
389
+ border-radius: 10px;
390
+ width: 80%;
391
+ max-width: 600px;
392
+ color: #F3F4F6;
393
+ position: relative;
394
+ box-shadow: 0 10px 25px rgba(0,0,0,0.5);
395
+ }
396
+ .close-btn {
397
+ color: #9CA3AF;
398
+ float: right;
399
+ font-size: 28px;
400
+ font-weight: bold;
401
+ cursor: pointer;
402
+ }
403
+ .close-btn:hover { color: #F3F4F6; }
404
+ code {
405
+ background-color: #111827;
406
+ padding: 2px 5px;
407
+ border-radius: 4px;
408
+ color: #F472B6;
409
+ font-family: monospace;
410
+ }
411
+ pre {
412
+ background-color: #111827;
413
+ padding: 15px;
414
+ border-radius: 8px;
415
+ overflow-x: auto;
416
+ color: #D1D5DB;
417
+ }
418
+ """
419
+
420
+ # ==========================================
421
+ # 6. GRADIO INTERFACE CONSTRUCTION
422
+ # ==========================================
423
+
424
+ with gr.Blocks(css=custom_css, title="CogniCaption") as app:
425
+ # Inject JS helper functions
426
+ gr.HTML(custom_js)
427
+
428
+ # Hidden Modal HTML Structure
429
+ gr.HTML("""
430
+ <div id="api_modal" onclick="closeApiModal(event)">
431
+ <div class="modal-content">
432
+ <span class="close-btn" onclick="toggleApiModal()">&times;</span>
433
+ <h2 style="margin-top:0; color: #818CF8;">Use CogniCaption as API</h2>
434
+ <hr style="border-color: #374151; margin: 15px 0;">
435
+
436
+ <p>You can use this Hugging Face Space as an API via the <code>gradio_client</code>.</p>
437
+
438
+ <h4>1. API Endpoint</h4>
439
+ <div style="display:flex; gap:10px; margin-bottom:15px;">
440
+ <input type="text" value="https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME" readonly
441
+ style="width:100%; padding:10px; background:#111827; border:1px solid #374151; color:#9CA3AF; border-radius:5px;">
442
+ </div>
443
+
444
+ <h4>2. How to Request</h4>
445
+ <p style="font-size:0.9rem; color:#9CA3AF;">
446
+ Send an image (filepath or URL) to the <code>predict</code> endpoint. The API returns a generated text caption.
447
+ </p>
448
+
449
+ <h4>3. Python Example</h4>
450
+ <pre>
451
+ from gradio_client import Client
452
+
453
+ client = Client("YOUR_USERNAME/SPACE_NAME")
454
+ result = client.predict(
455
+ image="https://example.com/image.jpg",
456
+ api_name="/predict"
457
  )
458
+ print(result) # Outputs the caption tuple
459
+ </pre>
460
+
461
+ <div style="text-align:right; margin-top:20px;">
462
+ <button onclick="toggleApiModal()" style="
463
+ background-color: #4F46E5;
464
+ color: white;
465
+ border: none;
466
+ padding: 10px 20px;
467
+ border-radius: 5px;
468
+ cursor: pointer;">
469
+ OK, Got it
470
+ </button>
471
+ </div>
472
+ </div>
473
+ </div>
474
+ """)
475
+
476
+ with gr.Column(elem_class="container"):
477
+ # Header
478
+ gr.HTML("""
479
+ <div class="header">
480
+ <h1>CogniCaption</h1>
481
+ <p>ViT + LSTM Image Captioning System</p>
482
+ </div>
483
+ """)
484
+
485
+ # --- INPUT SECTION (TOP) ---
486
+ with gr.Column():
487
+ input_image = gr.Image(type="pil", label="Upload Image", elem_id="input_image")
488
+ submit_btn = gr.Button("Generate Caption", variant="primary", size="lg")
489
+
490
+ # Separator
491
+ gr.HTML("<hr style='border-color: #374151; margin: 30px 0;'>")
492
+
493
+ # --- OUTPUT SECTION (BOTTOM) ---
494
+ with gr.Column():
495
+ # The HTML Display for the user
496
+ output_display = gr.HTML(label="Result", value=format_initial_html())
497
+
498
+ # Hidden Textbox to hold the raw text for the Copy Button to access
499
+ hidden_caption_storage = gr.Textbox(visible=False, elem_id="hidden_caption_output")
500
+
501
+ # Action Buttons Row
502
+ with gr.Row():
503
+ copy_btn = gr.Button("Copy Caption to Clipboard", size="sm")
504
+ api_btn = gr.Button("Use CogniCaption As API", size="sm", variant="secondary")
505
+
506
+ # --- EVENT LISTENERS ---
507
+
508
+ # 1. Prediction Event
509
+ submit_btn.click(
510
+ fn=predict,
511
+ inputs=[input_image],
512
+ outputs=[output_display, hidden_caption_storage]
513
+ )
514
+
515
+ # 2. Copy Button Event (Trigger JS)
516
+ copy_btn.click(
517
+ fn=None,
518
+ inputs=None,
519
+ outputs=None,
520
+ js="copyToClipboard"
521
+ )
522
+
523
+ # 3. API Button Event (Trigger JS Modal)
524
+ api_btn.click(
525
+ fn=None,
526
+ inputs=None,
527
+ outputs=None,
528
+ js="toggleApiModal"
529
+ )
530
 
531
  if __name__ == "__main__":
532
+ app.launch()