Dmgautomata commited on
Commit
9cd3d25
·
verified ·
1 Parent(s): 5ddda2d

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +617 -548
app.py CHANGED
@@ -1,548 +1,617 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Netlistify Training auf Hugging Face Spaces mit ZeroGPU.
5
-
6
- Diese Datei ist die Hauptdatei für den Hugging Face Space.
7
- """
8
-
9
- import gradio as gr
10
- import spaces
11
- import torch
12
- import os
13
- import sys
14
- from pathlib import Path
15
- from typing import Optional
16
- import shutil
17
-
18
- # Netlistify-Imports (müssen im Space verfügbar sein)
19
- sys.path.insert(0, "/tmp/Netlistify")
20
-
21
- # Hugging Face Token aus Environment Variable oder Space Secrets
22
- # Versuche verschiedene Quellen für den Token
23
- def get_hf_token():
24
- """Lädt HF Token aus verschiedenen Quellen."""
25
- # 1. Direkte Environment Variables
26
- token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
27
- if token:
28
- return token
29
-
30
- # 2. Versuche huggingface_hub's automatische Token-Erkennung
31
- try:
32
- from huggingface_hub import HfFolder
33
- token = HfFolder.get_token()
34
- if token:
35
- return token
36
- except:
37
- pass
38
-
39
- # 3. Prüfe ob Token-Datei existiert (für lokale Entwicklung)
40
- try:
41
- token_file = Path.home() / ".huggingface" / "token"
42
- if token_file.exists():
43
- with open(token_file, 'r') as f:
44
- token = f.read().strip()
45
- if token:
46
- return token
47
- except:
48
- pass
49
-
50
- return None
51
-
52
- HF_TOKEN = get_hf_token()
53
-
54
- @spaces.GPU(duration=3600) # 1 Stunde für Training (ZeroGPU Limit)
55
- def train_netlistify(
56
- dataset_repo_id: str,
57
- epochs: int = 10,
58
- batch_size: int = 64,
59
- learning_rate: float = 1e-4,
60
- dataset_size: int = -1,
61
- progress=gr.Progress()
62
- ):
63
- """
64
- Trainiert Netlistify DETR-Modell für Verbindungserkennung mit ZeroGPU.
65
-
66
- Args:
67
- dataset_repo_id: Hugging Face Dataset Repository-ID
68
- epochs: Anzahl Training-Epochs
69
- batch_size: Batch-Größe
70
- learning_rate: Learning Rate
71
- dataset_size: Anzahl Bilder (-1 = alle)
72
- progress: Gradio Progress-Tracker
73
- """
74
- try:
75
- # Prüfe GPU (innerhalb der dekorierten Funktion)
76
- if not torch.cuda.is_available():
77
- return "❌ Fehler: Keine GPU verfügbar. Prüfe ZeroGPU-Konfiguration."
78
-
79
- device = torch.device('cuda')
80
- gpu_name = torch.cuda.get_device_name(0)
81
- gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1e9
82
-
83
- progress(0.05, desc=f"✅ GPU erkannt: {gpu_name} ({gpu_memory:.1f} GB)")
84
-
85
- # Lade Dataset von Hugging Face
86
- try:
87
- from huggingface_hub import snapshot_download
88
-
89
- progress(0.1, desc="📥 Lade Dataset von Hugging Face...")
90
- # Verwende Token aus verschiedenen Quellen
91
- hf_token = get_hf_token()
92
-
93
- if not hf_token:
94
- # Debug-Info: Welche Environment Variables sind verfügbar?
95
- env_vars = {
96
- "HF_TOKEN": "❌ nicht gesetzt" if not os.getenv("HF_TOKEN") else "✅ gesetzt",
97
- "HUGGING_FACE_HUB_TOKEN": "❌ nicht gesetzt" if not os.getenv("HUGGING_FACE_HUB_TOKEN") else "✅ gesetzt",
98
- }
99
-
100
- debug_info = "\n".join([f"- {key}: {value}" for key, value in env_vars.items()])
101
-
102
- return f"""❌ Fehler: HF_TOKEN nicht gefunden.
103
-
104
- **Prüfe folgendes:**
105
-
106
- 1. **Space Settings → Secrets:**
107
- - Name muss exakt sein: `HF_TOKEN` (großgeschrieben, kein Leerzeichen)
108
- - Value: Dein Hugging Face Token (beginnt mit `hf_...`)
109
- - Klicke auf "Save" nach dem Hinzufügen
110
-
111
- 2. **Space neu starten:**
112
- - Nach dem Hinzufügen des Secrets: Settings → Restart Space
113
- - Warte bis Status "Running" ist
114
-
115
- 3. **Alternative Secret-Namen:**
116
- - Falls `HF_TOKEN` nicht funktioniert, versuche: `HUGGING_FACE_HUB_TOKEN`
117
-
118
- **Debug-Info (verfügbare Environment Variables):**
119
- {debug_info}
120
-
121
- **Hinweis:** Secrets sind erst nach einem Space-Neustart verfügbar!"""
122
-
123
- progress(0.12, desc="Authentifiziere mit Token...")
124
- dataset_path = snapshot_download(
125
- repo_id=dataset_repo_id,
126
- repo_type="dataset",
127
- local_dir="/tmp/netlistify_dataset",
128
- token=hf_token
129
- )
130
- progress(0.15, desc=f"✅ Dataset geladen: {dataset_path}")
131
- except Exception as e:
132
- return f"❌ Fehler beim Laden des Datasets: {e}\n\nStelle sicher, dass:\n- Das Dataset auf Hugging Face hochgeladen ist\n- Die Repository-ID korrekt ist\n- Du Zugriff auf das Dataset hast"
133
-
134
- # Bereite Dataset für Netlistify vor
135
- progress(0.2, desc="📦 Bereite Dataset vor...")
136
-
137
- # Netlistify erwartet: dataset_path/images/, dataset_path/labels/, dataset_path/pkl/
138
- train_dir = Path("/tmp/netlistify_train")
139
- train_dir.mkdir(exist_ok=True)
140
-
141
- # Kopiere/Verlinke Dataset-Struktur
142
- dataset_base = Path(dataset_path)
143
-
144
- # Prüfe ob ZIP-Dateien vorhanden sind und entpacke sie
145
- import zipfile
146
- zip_files = {
147
- "images.zip": "images",
148
- "components.zip": "components",
149
- "pkl.zip": "pkl"
150
- }
151
-
152
- extracted = False
153
- for zip_name, extract_dir in zip_files.items():
154
- zip_path = dataset_base / zip_name
155
- if zip_path.exists():
156
- progress(0.21, desc=f"📦 Entpacke {zip_name}...")
157
- extract_to = dataset_base / extract_dir
158
- extract_to.mkdir(exist_ok=True)
159
- try:
160
- with zipfile.ZipFile(zip_path, 'r') as zip_ref:
161
- # Prüfe ob ZIP verschachtelte Struktur hat (z.B. images/images/)
162
- file_list = zip_ref.namelist()
163
- has_nested = any('/' in f and f.split('/')[0] == extract_dir for f in file_list[:10])
164
-
165
- if has_nested:
166
- # Entpacke direkt ins extract_dir (ZIP enthält bereits extract_dir/)
167
- zip_ref.extractall(dataset_base)
168
- else:
169
- # Entpacke ins extract_dir
170
- zip_ref.extractall(extract_to)
171
- extracted = True
172
- progress(0.22, desc=f"✅ {zip_name} entpackt")
173
- except Exception as e:
174
- progress(0.22, desc=f"⚠️ Fehler beim Entpacken von {zip_name}: {e}")
175
-
176
- # Prüfe Dataset-Struktur
177
- images_dir = None
178
- labels_dir = None
179
- pkl_dir = None
180
-
181
- # Debug: Liste alle Verzeichnisse und Dateien
182
- debug_info = []
183
- debug_info.append(f"Dataset-Pfad: {dataset_base}")
184
- debug_info.append(f"Verfügbare Einträge:")
185
- try:
186
- for item in sorted(dataset_base.iterdir()):
187
- if item.is_dir():
188
- debug_info.append(f" 📁 {item.name}/")
189
- # Liste erste paar Dateien im Verzeichnis
190
- try:
191
- files = list(item.iterdir())[:3]
192
- for f in files:
193
- debug_info.append(f" - {f.name}")
194
- if len(list(item.iterdir())) > 3:
195
- debug_info.append(f" ... ({len(list(item.iterdir())) - 3} weitere)")
196
- except:
197
- pass
198
- elif item.is_file():
199
- debug_info.append(f" 📄 {item.name} ({item.stat().st_size / 1024 / 1024:.1f} MB)")
200
- except Exception as e:
201
- debug_info.append(f" Fehler beim Auflisten: {e}")
202
-
203
- # Verschiedene mögliche Strukturen prüfen
204
- # Struktur 1: images/images/, components/components/, pkl/
205
- if (dataset_base / "images" / "images").exists():
206
- images_dir = dataset_base / "images" / "images"
207
- # Prüfe components/components/ oder components/
208
- if (dataset_base / "components" / "components").exists():
209
- labels_dir = dataset_base / "components" / "components"
210
- elif (dataset_base / "components").exists():
211
- labels_dir = dataset_base / "components"
212
- pkl_dir = dataset_base / "pkl"
213
- # Struktur 2: images/, labels/ oder components/, pkl/
214
- elif (dataset_base / "images").exists():
215
- images_dir = dataset_base / "images"
216
- # Prüfe labels/ oder components/
217
- if (dataset_base / "labels").exists():
218
- labels_dir = dataset_base / "labels"
219
- elif (dataset_base / "components").exists():
220
- labels_dir = dataset_base / "components"
221
- pkl_dir = dataset_base / "pkl"
222
- # Struktur 3: Direkt im Root-Verzeichnis
223
- else:
224
- # Prüfe ob Dateien direkt im Root sind
225
- jpg_files = list(dataset_base.glob("*.jpg"))
226
- if jpg_files:
227
- # Erstelle temporäre Struktur
228
- images_dir = dataset_base
229
- labels_dir = dataset_base
230
- pkl_dir = dataset_base
231
-
232
- # Prüfe ob Verzeichnisse gefunden wurden
233
- if not images_dir or not images_dir.exists():
234
- debug_output = "\n".join(debug_info)
235
- return f"""❌ Dataset-Struktur nicht erkannt.
236
-
237
- **Erwartet:** images/, labels/ (oder components/), pkl/
238
-
239
- **Gefunden:**
240
- {debug_output}
241
-
242
- **Mögliche Lösungen:**
243
- 1. Dataset muss entpackt sein oder ZIP-Dateien (images.zip, components.zip, pkl.zip) enthalten
244
- 2. Struktur sollte sein:
245
- - images/ (oder images/images/)
246
- - labels/ oder components/ (oder components/components/)
247
- - pkl/
248
- 3. Prüfe ob ZIP-Dateien automatisch entpackt wurden"""
249
-
250
- # Prüfe ob Labels-Verzeichnis existiert (optional für einige Datasets)
251
- if not labels_dir or not labels_dir.exists():
252
- labels_dir = None # Labels sind optional für Netlistify
253
-
254
- # Erstelle Symlinks oder kopiere Dateien
255
- train_images = train_dir / "images"
256
- train_labels = train_dir / "labels"
257
- train_pkl = train_dir / "pkl"
258
-
259
- train_images.mkdir(exist_ok=True)
260
- train_labels.mkdir(exist_ok=True)
261
- train_pkl.mkdir(exist_ok=True)
262
-
263
- # Kopiere Dateien (erste N für Training)
264
- progress(0.25, desc="📋 Kopiere Dataset-Dateien...")
265
-
266
- img_files = list(images_dir.glob("*.jpg"))
267
- if dataset_size > 0:
268
- img_files = img_files[:dataset_size]
269
-
270
- for i, img_file in enumerate(img_files):
271
- if i % 100 == 0:
272
- progress(0.25 + (i / len(img_files)) * 0.1, desc=f"Kopiere Bilder: {i}/{len(img_files)}")
273
- shutil.copy2(img_file, train_images / img_file.name)
274
-
275
- # Kopiere zugehöriges Label (optional)
276
- if labels_dir:
277
- label_file = labels_dir / img_file.name.replace(".jpg", ".txt")
278
- if label_file.exists():
279
- shutil.copy2(label_file, train_labels / label_file.name)
280
-
281
- # Kopiere zugehörige PKL-Datei (optional)
282
- if pkl_dir and pkl_dir.exists():
283
- pkl_file = pkl_dir / img_file.name.replace(".jpg", ".pkl")
284
- if pkl_file.exists():
285
- shutil.copy2(pkl_file, train_pkl / pkl_file.name)
286
-
287
- progress(0.4, desc=f"✅ Dataset vorbereitet: {len(img_files)} Bilder")
288
-
289
- # Importiere Netlistify-Module
290
- progress(0.45, desc="🔧 Lade Netlistify-Module...")
291
-
292
- try:
293
- # Netlistify-Code muss im Space verfügbar sein
294
- # Option 1: Von GitHub klonen (falls nicht vorhanden)
295
- netlistify_dir = Path("/tmp/Netlistify")
296
- if not netlistify_dir.exists():
297
- import subprocess
298
- progress(0.46, desc="📥 Klone Netlistify von GitHub...")
299
- result = subprocess.run([
300
- "git", "clone",
301
- "https://github.com/NYCU-AI-EDA/Netlistify.git",
302
- str(netlistify_dir)
303
- ], capture_output=True, text=True, timeout=300)
304
- if result.returncode != 0:
305
- return f"❌ Fehler beim Klonen von Netlistify: {result.stderr}"
306
-
307
- sys.path.insert(0, str(netlistify_dir))
308
-
309
- # Importiere Netlistify-Module
310
- import main_config as config
311
- from main import main as train_main
312
- from Model import Model
313
- from slice import load_data
314
- import main_config
315
-
316
- # Setze Konfiguration
317
- main_config.REAL_DATA = True
318
- main_config.DATASET_PATH = str(train_dir)
319
- main_config.DATASET_SIZE = len(img_files) if dataset_size < 0 else dataset_size
320
- main_config.EPOCHS = epochs
321
- main_config.BATCH_SIZE = batch_size
322
- main_config.LEARNING_RATE = learning_rate
323
- main_config.DEVICE_IDS = [0]
324
- main_config.EVAL = False
325
- main_config.SMALL_IMAGE = True
326
-
327
- progress(0.5, desc="🚀 Starte Training...")
328
-
329
- # Starte Training
330
- # Da main() direkt ausgeführt wird, müssen wir es in einem separaten Prozess laufen lassen
331
- # oder die Logik direkt hier einbauen
332
-
333
- from main import create_model, xtransform, ytransform, criterion, eval_metrics
334
- from slice import FormalDatasetWindowedLinePair
335
-
336
- progress(0.55, desc="🏗️ Erstelle Modell...")
337
- network = create_model()
338
-
339
- progress(0.6, desc="📊 Lade Dataset...")
340
- dataset = FormalDatasetWindowedLinePair(
341
- main_config.DATASET_SIZE,
342
- main_config.DATASET_PATH,
343
- main_config.PICK,
344
- not main_config.SMALL_IMAGE,
345
- direction=main_config.DIRECTION,
346
- )
347
-
348
- progress(0.65, desc="🎯 Initialisiere Training...")
349
- model = Model(
350
- dataset,
351
- None, # eval_data
352
- xtransform=xtransform,
353
- ytransform=ytransform,
354
- amp=False,
355
- batch_size=main_config.BATCH_SIZE,
356
- eval=False,
357
- shuffle=True,
358
- )
359
-
360
- progress(0.7, desc="🔥 Training läuft...")
361
-
362
- # Training mit Progress-Updates
363
- import torch.optim as optim
364
-
365
- def training_callback(epoch, total_epochs, loss=None):
366
- progress_value = 0.7 + (epoch / total_epochs) * 0.25
367
- desc = f"Epoch {epoch+1}/{total_epochs}"
368
- if loss is not None:
369
- desc += f" - Loss: {loss:.4f}"
370
- progress(progress_value, desc=desc)
371
-
372
- # Starte Training
373
- model.fit(
374
- network,
375
- criterion,
376
- optim.Adam(network.parameters(), lr=main_config.LEARNING_RATE),
377
- main_config.EPOCHS,
378
- max_epochs=float("inf"),
379
- pretrained_path=main_config.PRETRAINED_PATH,
380
- keep=True,
381
- backprop_freq=main_config.BATCH_STEP,
382
- device_ids=main_config.DEVICE_IDS,
383
- eval_metrics=eval_metrics,
384
- keep_epoch=main_config.KEEP_EPOCH,
385
- keep_optimizer=main_config.KEEP_OPTIMIZER,
386
- config=None,
387
- upload=False,
388
- flush_cache_after_step=main_config.FLUSH_CACHE_AFTER_STEP,
389
- )
390
-
391
- progress(0.95, desc="💾 Speichere Modell...")
392
-
393
- # Modell-Pfad
394
- model_path = Path("/tmp/models")
395
- model_path.mkdir(exist_ok=True)
396
-
397
- # Finde bestes Modell
398
- runs_dir = netlistify_dir / "runs" / "FormalDatasetWindowedLinePair"
399
- if runs_dir.exists():
400
- latest_run = max(runs_dir.iterdir(), key=lambda x: x.stat().st_mtime)
401
- best_model = latest_run / "best_train.pth"
402
- if best_model.exists():
403
- shutil.copy2(best_model, model_path / "best_model.pth")
404
-
405
- progress(1.0, desc=" Training abgeschlossen!")
406
-
407
- return f"""
408
- Training erfolgreich abgeschlossen!
409
-
410
- 📊 **Training-Details:**
411
- - GPU: {gpu_name} ({gpu_memory:.1f} GB)
412
- - Epochs: {epochs}
413
- - Batch Size: {batch_size}
414
- - Learning Rate: {learning_rate}
415
- - Dataset-Größe: {len(img_files)} Bilder
416
-
417
- 💾 **Modell gespeichert:**
418
- - Pfad: {model_path}
419
- - Bestes Modell: best_model.pth
420
-
421
- 📁 **Nächste Schritte:**
422
- 1. Lade das trainierte Modell herunter
423
- 2. Verwende es für Inference in deiner Anwendung
424
- """
425
-
426
- except Exception as e:
427
- import traceback
428
- error_msg = f"❌ Fehler beim Training: {e}\n\n{traceback.format_exc()}"
429
- return error_msg
430
-
431
- except Exception as e:
432
- import traceback
433
- error_msg = f"❌ Fehler: {e}\n\n{traceback.format_exc()}"
434
- return error_msg
435
-
436
-
437
- def check_gpu_status():
438
- """Prüft GPU-Status."""
439
- try:
440
- if torch.cuda.is_available():
441
- gpu_name = torch.cuda.get_device_name(0)
442
- gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1e9
443
- return f"✅ GPU verfügbar: {gpu_name} ({gpu_memory:.1f} GB)"
444
- else:
445
- return "❌ Keine GPU verfügbar. Prüfe ZeroGPU-Konfiguration."
446
- except:
447
- return "⚠️ GPU-Status kann nicht geprüft werden (normal wenn keine GPU aktiv)"
448
-
449
-
450
- # Gradio Interface
451
- with gr.Blocks(title="Netlistify Training mit ZeroGPU") as app:
452
- gr.Markdown("""
453
- # 🔥 Netlistify Training mit ZeroGPU
454
-
455
- Trainiert Netlistify DETR-Modell für Verbindungserkennung auf Hugging Face Spaces mit ZeroGPU.
456
-
457
- **Voraussetzungen:**
458
- - Dataset auf Hugging Face hochgeladen (als Dataset Repository)
459
- - ZeroGPU Hardware aktiviert
460
- - Repository-ID des Datasets
461
- """)
462
-
463
- # GPU-Status
464
- with gr.Row():
465
- gpu_status = gr.Textbox(
466
- label="GPU-Status",
467
- value=check_gpu_status(),
468
- interactive=False
469
- )
470
- refresh_btn = gr.Button("🔄 Status aktualisieren")
471
- refresh_btn.click(fn=check_gpu_status, outputs=gpu_status)
472
-
473
- with gr.Row():
474
- with gr.Column():
475
- dataset_repo = gr.Textbox(
476
- label="Dataset Repository-ID",
477
- placeholder="username/netlistify-dataset",
478
- value="hanky2397/schematic_images",
479
- info="Hugging Face Dataset Repository (z.B. hanky2397/schematic_images)"
480
- )
481
-
482
- with gr.Row():
483
- epochs = gr.Number(
484
- label="Epochs",
485
- value=10,
486
- minimum=1,
487
- maximum=1000,
488
- info="Anzahl Training-Epochs"
489
- )
490
- batch_size = gr.Number(
491
- label="Batch Size",
492
- value=64,
493
- minimum=1,
494
- maximum=256,
495
- info="Batch-Größe"
496
- )
497
-
498
- with gr.Row():
499
- learning_rate = gr.Number(
500
- label="Learning Rate",
501
- value=1e-4,
502
- minimum=1e-6,
503
- maximum=1e-1,
504
- info="Learning Rate"
505
- )
506
- dataset_size = gr.Number(
507
- label="Dataset-Größe",
508
- value=-1,
509
- minimum=-1,
510
- maximum=100000,
511
- info="-1 = alle Bilder, sonst Anzahl"
512
- )
513
-
514
- with gr.Column():
515
- train_btn = gr.Button(
516
- "🚀 Training starten",
517
- variant="primary",
518
- size="lg"
519
- )
520
- output = gr.Textbox(
521
- label="Training-Status",
522
- lines=15,
523
- max_lines=30
524
- )
525
-
526
- train_btn.click(
527
- fn=train_netlistify,
528
- inputs=[dataset_repo, epochs, batch_size, learning_rate, dataset_size],
529
- outputs=output
530
- )
531
-
532
- gr.Markdown("""
533
- ## 📝 Hinweise
534
-
535
- - **ZeroGPU**: GPU wird automatisch zugewiesen wenn Training startet
536
- - **Dauer**: Standard-Limit ist 60 Sekunden, wurde auf 3 Stunden erhöht
537
- - **Checkpoints**: Modelle werden automatisch gespeichert
538
- - **Dataset**: Muss vorher auf Hugging Face hochgeladen werden
539
-
540
- ## 🔗 Links
541
-
542
- - [Netlistify GitHub](https://github.com/NYCU-AI-EDA/Netlistify)
543
- - [Dataset auf Hugging Face](https://huggingface.co/datasets/hanky2397/schematic_images)
544
- """)
545
-
546
- if __name__ == "__main__":
547
- app.launch()
548
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Netlistify Training auf Hugging Face Spaces mit ZeroGPU.
5
+
6
+ Diese Datei ist die Hauptdatei für den Hugging Face Space.
7
+ """
8
+
9
+ import gradio as gr
10
+ import spaces
11
+ import torch
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Optional
16
+ import shutil
17
+
18
+ # Netlistify-Imports werden später hinzugefügt, wenn Repository geklont wurde
19
+ # sys.path wird in train_netlistify() gesetzt
20
+
21
+ # Hugging Face Token aus Environment Variable oder Space Secrets
22
+ # Versuche verschiedene Quellen für den Token
23
+ def get_hf_token():
24
+ """Lädt HF Token aus verschiedenen Quellen."""
25
+ # 1. Direkte Environment Variables
26
+ token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
27
+ if token:
28
+ return token
29
+
30
+ # 2. Versuche huggingface_hub's automatische Token-Erkennung
31
+ try:
32
+ from huggingface_hub import HfFolder
33
+ token = HfFolder.get_token()
34
+ if token:
35
+ return token
36
+ except:
37
+ pass
38
+
39
+ # 3. Prüfe ob Token-Datei existiert (für lokale Entwicklung)
40
+ try:
41
+ token_file = Path.home() / ".huggingface" / "token"
42
+ if token_file.exists():
43
+ with open(token_file, 'r') as f:
44
+ token = f.read().strip()
45
+ if token:
46
+ return token
47
+ except:
48
+ pass
49
+
50
+ return None
51
+
52
+ HF_TOKEN = get_hf_token()
53
+
54
+ @spaces.GPU(duration=3600) # 1 Stunde (ZeroGPU Maximum-Limit, Standard: 60s)
55
+ def train_netlistify(
56
+ dataset_repo_id: str,
57
+ epochs: int = 10,
58
+ batch_size: int = 64,
59
+ learning_rate: float = 1e-4,
60
+ dataset_size: int = -1,
61
+ progress=gr.Progress()
62
+ ):
63
+ """
64
+ Trainiert Netlistify DETR-Modell für Verbindungserkennung mit ZeroGPU.
65
+
66
+ Args:
67
+ dataset_repo_id: Hugging Face Dataset Repository-ID
68
+ epochs: Anzahl Training-Epochs
69
+ batch_size: Batch-Größe
70
+ learning_rate: Learning Rate
71
+ dataset_size: Anzahl Bilder (-1 = alle)
72
+ progress: Gradio Progress-Tracker
73
+ """
74
+ try:
75
+ # Prüfe GPU (innerhalb der dekorierten Funktion)
76
+ if not torch.cuda.is_available():
77
+ return "❌ Fehler: Keine GPU verfügbar. Prüfe ZeroGPU-Konfiguration."
78
+
79
+ device = torch.device('cuda')
80
+ gpu_name = torch.cuda.get_device_name(0)
81
+ gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1e9
82
+
83
+ progress(0.05, desc=f"✅ GPU erkannt: {gpu_name} ({gpu_memory:.1f} GB)")
84
+
85
+ # Lade Dataset von Hugging Face
86
+ try:
87
+ from huggingface_hub import snapshot_download
88
+
89
+ progress(0.1, desc="📥 Lade Dataset von Hugging Face...")
90
+ # Verwende Token aus verschiedenen Quellen
91
+ hf_token = get_hf_token()
92
+
93
+ if not hf_token:
94
+ # Debug-Info: Welche Environment Variables sind verfügbar?
95
+ env_vars = {
96
+ "HF_TOKEN": "❌ nicht gesetzt" if not os.getenv("HF_TOKEN") else "✅ gesetzt",
97
+ "HUGGING_FACE_HUB_TOKEN": "❌ nicht gesetzt" if not os.getenv("HUGGING_FACE_HUB_TOKEN") else "✅ gesetzt",
98
+ }
99
+
100
+ debug_info = "\n".join([f"- {key}: {value}" for key, value in env_vars.items()])
101
+
102
+ return f"""❌ Fehler: HF_TOKEN nicht gefunden.
103
+
104
+ **Prüfe folgendes:**
105
+
106
+ 1. **Space Settings → Secrets:**
107
+ - Name muss exakt sein: `HF_TOKEN` (großgeschrieben, kein Leerzeichen)
108
+ - Value: Dein Hugging Face Token (beginnt mit `hf_...`)
109
+ - Klicke auf "Save" nach dem Hinzufügen
110
+
111
+ 2. **Space neu starten:**
112
+ - Nach dem Hinzufügen des Secrets: Settings → Restart Space
113
+ - Warte bis Status "Running" ist
114
+
115
+ 3. **Alternative Secret-Namen:**
116
+ - Falls `HF_TOKEN` nicht funktioniert, versuche: `HUGGING_FACE_HUB_TOKEN`
117
+
118
+ **Debug-Info (verfügbare Environment Variables):**
119
+ {debug_info}
120
+
121
+ **Hinweis:** Secrets sind erst nach einem Space-Neustart verfügbar!"""
122
+
123
+ progress(0.12, desc="Authentifiziere mit Token...")
124
+ dataset_path = snapshot_download(
125
+ repo_id=dataset_repo_id,
126
+ repo_type="dataset",
127
+ local_dir="/tmp/netlistify_dataset",
128
+ token=hf_token
129
+ )
130
+ progress(0.15, desc=f"✅ Dataset geladen: {dataset_path}")
131
+ except Exception as e:
132
+ error_msg = str(e)
133
+ if "401" in error_msg or "gated" in error_msg.lower() or "restricted" in error_msg.lower():
134
+ return f"""❌ Dataset-Zugriff verweigert (401 / Gated Repository)
135
+
136
+ Das Dataset ist zugriffsbeschränkt. Bitte folge diesen Schritten:
137
+
138
+ 1. Gehe zu: https://huggingface.co/datasets/hanky2397/schematic_images
139
+ 2. Klicke auf: "Agree and access repository" oder "Accept terms"
140
+ 3. Warte bis Zugriff gewährt wird (einige Sekunden)
141
+ 4. Prüfe Token in Space Settings → Secrets → HF_TOKEN
142
+ 5. Starte Space neu (Settings → Restart Space)
143
+ 6. Versuche Training erneut
144
+
145
+ Fehlerdetails: {error_msg}
146
+
147
+ Hinweis: Du musst eingeloggt sein und die Terms akzeptieren!"""
148
+ else:
149
+ return f"❌ Fehler beim Laden des Datasets: {error_msg}\n\nStelle sicher, dass:\n- Das Dataset auf Hugging Face hochgeladen ist\n- Die Repository-ID korrekt ist\n- Du Zugriff auf das Dataset hast"
150
+
151
+ # Bereite Dataset für Netlistify vor
152
+ progress(0.2, desc="📦 Bereite Dataset vor...")
153
+
154
+ # Netlistify erwartet: dataset_path/images/, dataset_path/labels/, dataset_path/pkl/
155
+ train_dir = Path("/tmp/netlistify_train")
156
+ train_dir.mkdir(exist_ok=True)
157
+
158
+ # Kopiere/Verlinke Dataset-Struktur
159
+ dataset_base = Path(dataset_path)
160
+
161
+ # Prüfe ob ZIP-Dateien vorhanden sind und entpacke sie
162
+ import zipfile
163
+ zip_files = {
164
+ "images.zip": "images",
165
+ "components.zip": "components",
166
+ "pkl.zip": "pkl"
167
+ }
168
+
169
+ extracted = False
170
+ for zip_name, extract_dir in zip_files.items():
171
+ zip_path = dataset_base / zip_name
172
+ if zip_path.exists():
173
+ progress(0.21, desc=f"📦 Entpacke {zip_name}...")
174
+ extract_to = dataset_base / extract_dir
175
+ extract_to.mkdir(exist_ok=True)
176
+ try:
177
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
178
+ # Prüfe ob ZIP verschachtelte Struktur hat (z.B. images/images/)
179
+ file_list = zip_ref.namelist()
180
+ has_nested = any('/' in f and f.split('/')[0] == extract_dir for f in file_list[:10])
181
+
182
+ if has_nested:
183
+ # Entpacke direkt ins extract_dir (ZIP enthält bereits extract_dir/)
184
+ zip_ref.extractall(dataset_base)
185
+ else:
186
+ # Entpacke ins extract_dir
187
+ zip_ref.extractall(extract_to)
188
+ extracted = True
189
+ progress(0.22, desc=f"✅ {zip_name} entpackt")
190
+ except Exception as e:
191
+ progress(0.22, desc=f"⚠️ Fehler beim Entpacken von {zip_name}: {e}")
192
+
193
+ # Prüfe Dataset-Struktur
194
+ images_dir = None
195
+ labels_dir = None
196
+ pkl_dir = None
197
+
198
+ # Debug: Liste alle Verzeichnisse und Dateien
199
+ debug_info = []
200
+ debug_info.append(f"Dataset-Pfad: {dataset_base}")
201
+ debug_info.append(f"Verfügbare Einträge:")
202
+ try:
203
+ for item in sorted(dataset_base.iterdir()):
204
+ if item.is_dir():
205
+ debug_info.append(f" 📁 {item.name}/")
206
+ # Liste erste paar Dateien im Verzeichnis
207
+ try:
208
+ files = list(item.iterdir())[:3]
209
+ for f in files:
210
+ debug_info.append(f" - {f.name}")
211
+ if len(list(item.iterdir())) > 3:
212
+ debug_info.append(f" ... ({len(list(item.iterdir())) - 3} weitere)")
213
+ except:
214
+ pass
215
+ elif item.is_file():
216
+ debug_info.append(f" 📄 {item.name} ({item.stat().st_size / 1024 / 1024:.1f} MB)")
217
+ except Exception as e:
218
+ debug_info.append(f" Fehler beim Auflisten: {e}")
219
+
220
+ # Verschiedene mögliche Strukturen prüfen
221
+ # Struktur 1: images/images/, components/components/, pkl/
222
+ if (dataset_base / "images" / "images").exists():
223
+ images_dir = dataset_base / "images" / "images"
224
+ # Prüfe components/components/ oder components/
225
+ if (dataset_base / "components" / "components").exists():
226
+ labels_dir = dataset_base / "components" / "components"
227
+ elif (dataset_base / "components").exists():
228
+ labels_dir = dataset_base / "components"
229
+ pkl_dir = dataset_base / "pkl"
230
+ # Struktur 2: images/, labels/ oder components/, pkl/
231
+ elif (dataset_base / "images").exists():
232
+ images_dir = dataset_base / "images"
233
+ # Prüfe labels/ oder components/
234
+ if (dataset_base / "labels").exists():
235
+ labels_dir = dataset_base / "labels"
236
+ elif (dataset_base / "components").exists():
237
+ labels_dir = dataset_base / "components"
238
+ pkl_dir = dataset_base / "pkl"
239
+ # Struktur 3: Direkt im Root-Verzeichnis
240
+ else:
241
+ # Prüfe ob Dateien direkt im Root sind
242
+ jpg_files = list(dataset_base.glob("*.jpg"))
243
+ if jpg_files:
244
+ # Erstelle temporäre Struktur
245
+ images_dir = dataset_base
246
+ labels_dir = dataset_base
247
+ pkl_dir = dataset_base
248
+
249
+ # Prüfe ob Verzeichnisse gefunden wurden
250
+ if not images_dir or not images_dir.exists():
251
+ debug_output = "\n".join(debug_info)
252
+ return f"""❌ Dataset-Struktur nicht erkannt.
253
+
254
+ **Erwartet:** images/, labels/ (oder components/), pkl/
255
+
256
+ **Gefunden:**
257
+ {debug_output}
258
+
259
+ **Mögliche Lösungen:**
260
+ 1. Dataset muss entpackt sein oder ZIP-Dateien (images.zip, components.zip, pkl.zip) enthalten
261
+ 2. Struktur sollte sein:
262
+ - images/ (oder images/images/)
263
+ - labels/ oder components/ (oder components/components/)
264
+ - pkl/
265
+ 3. Prüfe ob ZIP-Dateien automatisch entpackt wurden"""
266
+
267
+ # Prüfe ob Labels-Verzeichnis existiert (optional für einige Datasets)
268
+ if not labels_dir or not labels_dir.exists():
269
+ labels_dir = None # Labels sind optional für Netlistify
270
+
271
+ # Erstelle Symlinks oder kopiere Dateien
272
+ train_images = train_dir / "images"
273
+ train_labels = train_dir / "labels"
274
+ train_pkl = train_dir / "pkl"
275
+
276
+ train_images.mkdir(exist_ok=True)
277
+ train_labels.mkdir(exist_ok=True)
278
+ train_pkl.mkdir(exist_ok=True)
279
+
280
+ # Kopiere Dateien (erste N für Training)
281
+ progress(0.25, desc="📋 Kopiere Dataset-Dateien...")
282
+
283
+ img_files = list(images_dir.glob("*.jpg"))
284
+ if dataset_size > 0:
285
+ img_files = img_files[:dataset_size]
286
+
287
+ for i, img_file in enumerate(img_files):
288
+ if i % 100 == 0:
289
+ progress(0.25 + (i / len(img_files)) * 0.1, desc=f"Kopiere Bilder: {i}/{len(img_files)}")
290
+ shutil.copy2(img_file, train_images / img_file.name)
291
+
292
+ # Kopiere zugehöriges Label (optional)
293
+ if labels_dir:
294
+ label_file = labels_dir / img_file.name.replace(".jpg", ".txt")
295
+ if label_file.exists():
296
+ shutil.copy2(label_file, train_labels / label_file.name)
297
+
298
+ # Kopiere zugehörige PKL-Datei (optional)
299
+ if pkl_dir and pkl_dir.exists():
300
+ pkl_file = pkl_dir / img_file.name.replace(".jpg", ".pkl")
301
+ if pkl_file.exists():
302
+ shutil.copy2(pkl_file, train_pkl / pkl_file.name)
303
+
304
+ progress(0.4, desc=f"✅ Dataset vorbereitet: {len(img_files)} Bilder")
305
+
306
+ # Importiere Netlistify-Module
307
+ progress(0.45, desc="🔧 Lade Netlistify-Module...")
308
+
309
+ try:
310
+ # Netlistify-Code muss im Space verfügbar sein
311
+ # Option 1: Von GitHub klonen (falls nicht vorhanden)
312
+ netlistify_dir = Path("/tmp/Netlistify")
313
+
314
+ # Prüfe ob Verzeichnis existiert und main_config.py enthält
315
+ main_config_file = netlistify_dir / "main_config.py"
316
+ if not main_config_file.exists():
317
+ import subprocess
318
+ progress(0.46, desc="📥 Klone Netlistify von GitHub...")
319
+
320
+ # Lösche altes Verzeichnis falls vorhanden (aber leer)
321
+ if netlistify_dir.exists() and not any(netlistify_dir.iterdir()):
322
+ shutil.rmtree(netlistify_dir)
323
+
324
+ # Klone Repository
325
+ result = subprocess.run([
326
+ "git", "clone",
327
+ "https://github.com/NYCU-AI-EDA/Netlistify.git",
328
+ str(netlistify_dir)
329
+ ], capture_output=True, text=True, timeout=300)
330
+
331
+ if result.returncode != 0:
332
+ error_msg = result.stderr or result.stdout or "Unbekannter Fehler"
333
+ return f"""❌ Fehler beim Klonen von Netlistify:
334
+
335
+ **Git-Output:**
336
+ {error_msg}
337
+
338
+ **Mögliche Lösungen:**
339
+ 1. Prüfe Internet-Verbindung
340
+ 2. Prüfe ob GitHub erreichbar ist
341
+ 3. Versuche Training erneut (Repository wird beim nächsten Versuch geklont)"""
342
+
343
+ progress(0.47, desc="✅ Netlistify geklont")
344
+
345
+ # Prüfe ob main_config.py existiert
346
+ if not main_config_file.exists():
347
+ return f"""❌ Netlistify-Repository unvollständig.
348
+
349
+ **Erwartet:** {main_config_file}
350
+ **Gefunden:** Verzeichnis existiert, aber main_config.py fehlt
351
+
352
+ **Mögliche Lösungen:**
353
+ 1. Prüfe ob Repository korrekt geklont wurde
354
+ 2. Prüfe ob main_config.py im Repository existiert
355
+ 3. Versuche Training erneut"""
356
+
357
+ # Füge Netlistify zum Python-Pfad hinzu
358
+ netlistify_str = str(netlistify_dir)
359
+ if netlistify_str not in sys.path:
360
+ sys.path.insert(0, netlistify_str)
361
+
362
+ progress(0.48, desc="📦 Importiere Netlistify-Module...")
363
+
364
+ # Importiere Netlistify-Module
365
+ try:
366
+ import main_config
367
+ except ImportError as e:
368
+ return f"""❌ Fehler beim Import von main_config:
369
+
370
+ **Fehler:** {e}
371
+ **Python-Pfad:** {sys.path[:3]}
372
+ **Netlistify-Verzeichnis:** {netlistify_dir}
373
+ **main_config.py existiert:** {main_config_file.exists()}
374
+
375
+ **Mögliche Lösungen:**
376
+ 1. Prüfe ob Netlistify korrekt geklont wurde
377
+ 2. Prüfe ob alle Abhängigkeiten installiert sind
378
+ 3. Versuche Training erneut"""
379
+
380
+ # Weitere Imports
381
+ from main import main as train_main
382
+ from Model import Model
383
+ from slice import load_data
384
+
385
+ # Setze Konfiguration
386
+ main_config.REAL_DATA = True
387
+ main_config.DATASET_PATH = str(train_dir)
388
+ main_config.DATASET_SIZE = len(img_files) if dataset_size < 0 else dataset_size
389
+ main_config.EPOCHS = epochs
390
+ main_config.BATCH_SIZE = batch_size
391
+ main_config.LEARNING_RATE = learning_rate
392
+ main_config.DEVICE_IDS = [0]
393
+ main_config.EVAL = False
394
+ main_config.SMALL_IMAGE = True
395
+
396
+ progress(0.5, desc="🚀 Starte Training...")
397
+
398
+ # Starte Training
399
+ # Da main() direkt ausgeführt wird, müssen wir es in einem separaten Prozess laufen lassen
400
+ # oder die Logik direkt hier einbauen
401
+
402
+ from main import create_model, xtransform, ytransform, criterion, eval_metrics
403
+ from slice import FormalDatasetWindowedLinePair
404
+
405
+ progress(0.55, desc="🏗️ Erstelle Modell...")
406
+ network = create_model()
407
+
408
+ progress(0.6, desc="📊 Lade Dataset...")
409
+ dataset = FormalDatasetWindowedLinePair(
410
+ main_config.DATASET_SIZE,
411
+ main_config.DATASET_PATH,
412
+ main_config.PICK,
413
+ not main_config.SMALL_IMAGE,
414
+ direction=main_config.DIRECTION,
415
+ )
416
+
417
+ progress(0.65, desc="🎯 Initialisiere Training...")
418
+ model = Model(
419
+ dataset,
420
+ None, # eval_data
421
+ xtransform=xtransform,
422
+ ytransform=ytransform,
423
+ amp=False,
424
+ batch_size=main_config.BATCH_SIZE,
425
+ eval=False,
426
+ shuffle=True,
427
+ )
428
+
429
+ progress(0.7, desc="🔥 Training läuft...")
430
+
431
+ # Training mit Progress-Updates
432
+ import torch.optim as optim
433
+
434
+ def training_callback(epoch, total_epochs, loss=None):
435
+ progress_value = 0.7 + (epoch / total_epochs) * 0.25
436
+ desc = f"Epoch {epoch+1}/{total_epochs}"
437
+ if loss is not None:
438
+ desc += f" - Loss: {loss:.4f}"
439
+ progress(progress_value, desc=desc)
440
+
441
+ # Starte Training
442
+ model.fit(
443
+ network,
444
+ criterion,
445
+ optim.Adam(network.parameters(), lr=main_config.LEARNING_RATE),
446
+ main_config.EPOCHS,
447
+ max_epochs=float("inf"),
448
+ pretrained_path=main_config.PRETRAINED_PATH,
449
+ keep=True,
450
+ backprop_freq=main_config.BATCH_STEP,
451
+ device_ids=main_config.DEVICE_IDS,
452
+ eval_metrics=eval_metrics,
453
+ keep_epoch=main_config.KEEP_EPOCH,
454
+ keep_optimizer=main_config.KEEP_OPTIMIZER,
455
+ config=None,
456
+ upload=False,
457
+ flush_cache_after_step=main_config.FLUSH_CACHE_AFTER_STEP,
458
+ )
459
+
460
+ progress(0.95, desc="💾 Speichere Modell...")
461
+
462
+ # Modell-Pfad
463
+ model_path = Path("/tmp/models")
464
+ model_path.mkdir(exist_ok=True)
465
+
466
+ # Finde bestes Modell
467
+ runs_dir = netlistify_dir / "runs" / "FormalDatasetWindowedLinePair"
468
+ if runs_dir.exists():
469
+ latest_run = max(runs_dir.iterdir(), key=lambda x: x.stat().st_mtime)
470
+ best_model = latest_run / "best_train.pth"
471
+ if best_model.exists():
472
+ shutil.copy2(best_model, model_path / "best_model.pth")
473
+
474
+ progress(1.0, desc="✅ Training abgeschlossen!")
475
+
476
+ return f"""
477
+ ✅ Training erfolgreich abgeschlossen!
478
+
479
+ 📊 **Training-Details:**
480
+ - GPU: {gpu_name} ({gpu_memory:.1f} GB)
481
+ - Epochs: {epochs}
482
+ - Batch Size: {batch_size}
483
+ - Learning Rate: {learning_rate}
484
+ - Dataset-Größe: {len(img_files)} Bilder
485
+
486
+ 💾 **Modell gespeichert:**
487
+ - Pfad: {model_path}
488
+ - Bestes Modell: best_model.pth
489
+
490
+ 📁 **Nächste Schritte:**
491
+ 1. Lade das trainierte Modell herunter
492
+ 2. Verwende es für Inference in deiner Anwendung
493
+ """
494
+
495
+ except Exception as e:
496
+ import traceback
497
+ error_msg = f"❌ Fehler beim Training: {e}\n\n{traceback.format_exc()}"
498
+ return error_msg
499
+
500
+ except Exception as e:
501
+ import traceback
502
+ error_msg = f"❌ Fehler: {e}\n\n{traceback.format_exc()}"
503
+ return error_msg
504
+
505
+
506
+ def check_gpu_status():
507
+ """Prüft GPU-Status."""
508
+ try:
509
+ if torch.cuda.is_available():
510
+ gpu_name = torch.cuda.get_device_name(0)
511
+ gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1e9
512
+ return f"✅ GPU verfügbar: {gpu_name} ({gpu_memory:.1f} GB)"
513
+ else:
514
+ return "❌ Keine GPU verfügbar. Prüfe ZeroGPU-Konfiguration."
515
+ except:
516
+ return "⚠️ GPU-Status kann nicht geprüft werden (normal wenn keine GPU aktiv)"
517
+
518
+
519
+ # Gradio Interface
520
+ with gr.Blocks(title="Netlistify Training mit ZeroGPU") as app:
521
+ gr.Markdown("""
522
+ # 🔥 Netlistify Training mit ZeroGPU
523
+
524
+ Trainiert Netlistify DETR-Modell für Verbindungserkennung auf Hugging Face Spaces mit ZeroGPU.
525
+
526
+ **Voraussetzungen:**
527
+ - Dataset auf Hugging Face hochgeladen (als Dataset Repository)
528
+ - ZeroGPU Hardware aktiviert
529
+ - Repository-ID des Datasets
530
+ """)
531
+
532
+ # GPU-Status
533
+ with gr.Row():
534
+ gpu_status = gr.Textbox(
535
+ label="GPU-Status",
536
+ value=check_gpu_status(),
537
+ interactive=False
538
+ )
539
+ refresh_btn = gr.Button("🔄 Status aktualisieren")
540
+ refresh_btn.click(fn=check_gpu_status, outputs=gpu_status)
541
+
542
+ with gr.Row():
543
+ with gr.Column():
544
+ dataset_repo = gr.Textbox(
545
+ label="Dataset Repository-ID",
546
+ placeholder="username/netlistify-dataset",
547
+ value="hanky2397/schematic_images",
548
+ info="Hugging Face Dataset Repository (z.B. hanky2397/schematic_images)"
549
+ )
550
+
551
+ with gr.Row():
552
+ epochs = gr.Number(
553
+ label="Epochs",
554
+ value=10,
555
+ minimum=1,
556
+ maximum=1000,
557
+ info="Anzahl Training-Epochs"
558
+ )
559
+ batch_size = gr.Number(
560
+ label="Batch Size",
561
+ value=64,
562
+ minimum=1,
563
+ maximum=256,
564
+ info="Batch-Größe"
565
+ )
566
+
567
+ with gr.Row():
568
+ learning_rate = gr.Number(
569
+ label="Learning Rate",
570
+ value=1e-4,
571
+ minimum=1e-6,
572
+ maximum=1e-1,
573
+ info="Learning Rate"
574
+ )
575
+ dataset_size = gr.Number(
576
+ label="Dataset-Größe",
577
+ value=-1,
578
+ minimum=-1,
579
+ maximum=100000,
580
+ info="-1 = alle Bilder, sonst Anzahl"
581
+ )
582
+
583
+ with gr.Column():
584
+ train_btn = gr.Button(
585
+ "🚀 Training starten",
586
+ variant="primary",
587
+ size="lg"
588
+ )
589
+ output = gr.Textbox(
590
+ label="Training-Status",
591
+ lines=15,
592
+ max_lines=30
593
+ )
594
+
595
+ train_btn.click(
596
+ fn=train_netlistify,
597
+ inputs=[dataset_repo, epochs, batch_size, learning_rate, dataset_size],
598
+ outputs=output
599
+ )
600
+
601
+ gr.Markdown("""
602
+ ## 📝 Hinweise
603
+
604
+ - **ZeroGPU**: GPU wird automatisch zugewiesen wenn Training startet
605
+ - **Dauer**: Standard-Limit ist 60 Sekunden, wurde auf 1 Stunde (3600 Sekunden) erhöht
606
+ - **Checkpoints**: Modelle werden automatisch gespeichert
607
+ - **Dataset**: Muss vorher auf Hugging Face hochgeladen werden
608
+
609
+ ## 🔗 Links
610
+
611
+ - [Netlistify GitHub](https://github.com/NYCU-AI-EDA/Netlistify)
612
+ - [Dataset auf Hugging Face](https://huggingface.co/datasets/hanky2397/schematic_images)
613
+ """)
614
+
615
+ if __name__ == "__main__":
616
+ app.launch()
617
+