HomePilot Deploy Bot commited on
Commit
632cf08
ยท
unverified ยท
1 Parent(s): bd09507

chore: sync installer from monorepo

Browse files
Files changed (1) hide show
  1. app.py +253 -451
app.py CHANGED
@@ -1,566 +1,368 @@
1
  """
2
- HomePilot Installer โ€” Enterprise Edition
3
-
4
- Premium installer wizard that deploys HomePilot into a user's own
5
- private HF Space. Design language matches ruslanmv.com/HomePilot:
6
- dark theme, cyanโ†’blueโ†’purple gradients, glass cards, smooth transitions.
 
 
 
7
  """
8
 
9
- import json
10
  import os
11
  import shutil
12
  import subprocess
13
  import tempfile
14
- from pathlib import Path
15
 
16
  import gradio as gr
17
 
18
  TEMPLATE_REPO = os.environ.get("TEMPLATE_REPO", "ruslanmv/HomePilot")
19
 
20
- # โ”€โ”€ Persona data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
21
-
22
- PERSONAS = {
23
- "starter": [
24
- ("๐ŸŒ™", "LunaLite", "Soft Greeter"),
25
- ("๐Ÿ˜Ž", "ChillBro", "Casual Regular"),
26
- ("๐Ÿ”", "Curiosa", "Question Driver"),
27
- ("โšก", "HypeKid", "Reaction Engine"),
28
- ],
29
- "retro": [
30
- ("๐Ÿ”‹", "VoltBuddy", "Electric Companion"),
31
- ("โš”๏ธ", "RoninZero", "Lone Warrior"),
32
- ("๐Ÿฆ–", "RivalKaiju", "Chaos Rival"),
33
- ("๐Ÿ’พ", "Glitchbyte", "Digital Glitch"),
34
- ("๐Ÿ—บ๏ธ", "QuestKid", "Young Adventurer"),
35
- ("๐Ÿง ", "SigmaSage", "Quiet Strategist"),
36
- ("๐Ÿƒ", "Wildcard", "Trickster"),
37
- ("๐ŸŒณ", "OldRoot", "Ancient Mentor"),
38
- ("๐Ÿ”ฎ", "MorphlingX", "Transformer"),
39
- ("๐ŸŒŒ", "NovaVoid", "Cosmic Entity"),
40
- ],
41
- }
42
-
43
- # โ”€โ”€ CSS: HomePilot enterprise theme โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
44
-
45
- CSS = """
46
- /* โ”€โ”€ Base โ”€โ”€ */
47
- :root {
48
- --hp-bg: #09090b;
49
- --hp-surface: #111113;
50
- --hp-card: #161618;
51
- --hp-border: rgba(255,255,255,0.06);
52
- --hp-border-hover: rgba(255,255,255,0.14);
53
- --hp-text: #e4e4e7;
54
- --hp-muted: #71717a;
55
- --hp-cyan: #06b6d4;
56
- --hp-blue: #3b82f6;
57
- --hp-purple: #8b5cf6;
58
- --hp-gradient: linear-gradient(135deg, #06b6d4, #3b82f6, #8b5cf6);
59
- --hp-glow: 0 0 24px rgba(59,130,246,0.15);
60
- --hp-radius: 12px;
61
- }
62
-
63
- .gradio-container {
64
- background: var(--hp-bg) !important;
65
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
66
- max-width: 900px !important;
67
- margin: 0 auto !important;
68
- }
69
-
70
- /* Remove default Gradio chrome */
71
- .gradio-container .main, footer { background: transparent !important; }
72
- footer { display: none !important; }
73
-
74
- /* โ”€โ”€ Hero โ”€โ”€ */
75
- .hp-hero {
76
- text-align: center;
77
- padding: 56px 24px 40px;
78
- position: relative;
79
- overflow: hidden;
80
- }
81
- .hp-hero::before {
82
- content: '';
83
- position: absolute;
84
- top: -40%;
85
- left: 50%;
86
- transform: translateX(-50%);
87
- width: 120%;
88
- height: 100%;
89
- background: radial-gradient(ellipse 60% 50% at 50% 0%, rgba(59,130,246,0.08), transparent);
90
- pointer-events: none;
91
- }
92
- .hp-hero h1 {
93
- font-size: clamp(2rem, 5vw, 2.8rem);
94
- font-weight: 800;
95
- letter-spacing: -0.03em;
96
- line-height: 1.1;
97
- margin: 0;
98
- position: relative;
99
- }
100
- .hp-hero h1 .gradient-text {
101
- background: var(--hp-gradient);
102
- -webkit-background-clip: text;
103
- -webkit-text-fill-color: transparent;
104
- background-clip: text;
105
- }
106
- .hp-hero .subtitle {
107
- color: var(--hp-muted);
108
- font-size: 15px;
109
- font-weight: 500;
110
- margin-top: 12px;
111
- position: relative;
112
- }
113
- .hp-hero .badges {
114
- display: flex;
115
- justify-content: center;
116
- gap: 8px;
117
- margin-top: 20px;
118
- flex-wrap: wrap;
119
- position: relative;
120
- }
121
- .hp-badge {
122
- display: inline-flex;
123
- align-items: center;
124
- gap: 6px;
125
- padding: 6px 14px;
126
- border-radius: 999px;
127
- font-size: 12px;
128
- font-weight: 600;
129
- letter-spacing: 0.02em;
130
- border: 1px solid var(--hp-border);
131
- background: var(--hp-surface);
132
- color: var(--hp-text);
133
- transition: all 0.2s;
134
- }
135
- .hp-badge:hover { border-color: var(--hp-border-hover); }
136
- .hp-badge .dot {
137
- width: 6px; height: 6px;
138
- border-radius: 50%;
139
- background: var(--hp-gradient);
140
- }
141
-
142
- /* โ”€โ”€ Step cards โ”€โ”€ */
143
- .hp-step {
144
- background: var(--hp-card) !important;
145
- border: 1px solid var(--hp-border) !important;
146
- border-radius: var(--hp-radius) !important;
147
- padding: 28px !important;
148
- margin-bottom: 16px !important;
149
- transition: border-color 0.2s !important;
150
- }
151
- .hp-step:hover { border-color: var(--hp-border-hover) !important; }
152
- .hp-step-header {
153
- display: flex;
154
- align-items: center;
155
- gap: 12px;
156
- margin-bottom: 16px;
157
- }
158
- .hp-step-num {
159
- width: 32px; height: 32px;
160
- border-radius: 10px;
161
- background: var(--hp-gradient);
162
- display: flex;
163
- align-items: center;
164
- justify-content: center;
165
- font-size: 14px;
166
- font-weight: 800;
167
- color: white;
168
- flex-shrink: 0;
169
- }
170
- .hp-step-title {
171
- font-size: 17px;
172
- font-weight: 700;
173
- color: var(--hp-text);
174
- letter-spacing: -0.01em;
175
- }
176
- .hp-step-desc {
177
- font-size: 13px;
178
- color: var(--hp-muted);
179
- margin-top: 2px;
180
- }
181
-
182
- /* โ”€โ”€ Inputs โ”€โ”€ */
183
- .hp-step input, .hp-step textarea {
184
- background: var(--hp-bg) !important;
185
- border: 1px solid var(--hp-border) !important;
186
- border-radius: 10px !important;
187
- color: var(--hp-text) !important;
188
- font-size: 15px !important;
189
- padding: 12px 16px !important;
190
- transition: border-color 0.2s !important;
191
- }
192
- .hp-step input:focus, .hp-step textarea:focus {
193
- border-color: var(--hp-blue) !important;
194
- box-shadow: 0 0 0 3px rgba(59,130,246,0.1) !important;
195
- }
196
- .hp-step label { color: var(--hp-muted) !important; font-weight: 600 !important; font-size: 13px !important; }
197
-
198
- /* โ”€โ”€ Persona grid โ”€โ”€ */
199
- .hp-personas {
200
- background: var(--hp-card);
201
- border: 1px solid var(--hp-border);
202
- border-radius: var(--hp-radius);
203
- padding: 24px;
204
- margin-top: 16px;
205
- }
206
- .hp-personas-title {
207
- font-size: 14px;
208
- font-weight: 700;
209
- color: var(--hp-text);
210
- margin-bottom: 4px;
211
- }
212
- .hp-personas-sub {
213
- font-size: 12px;
214
- color: var(--hp-muted);
215
- margin-bottom: 16px;
216
- }
217
- .hp-pack-label {
218
- font-size: 10px;
219
- font-weight: 700;
220
- text-transform: uppercase;
221
- letter-spacing: 0.08em;
222
- color: var(--hp-muted);
223
- margin-bottom: 8px;
224
- margin-top: 16px;
225
- }
226
- .hp-pack-label:first-child { margin-top: 0; }
227
- .hp-grid {
228
- display: grid;
229
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
230
- gap: 8px;
231
- }
232
- .hp-chip {
233
- display: flex;
234
- align-items: center;
235
- gap: 6px;
236
- padding: 8px 12px;
237
- border-radius: 10px;
238
- font-size: 12px;
239
- font-weight: 600;
240
- color: var(--hp-text);
241
- background: var(--hp-bg);
242
- border: 1px solid var(--hp-border);
243
- transition: all 0.2s;
244
- white-space: nowrap;
245
- overflow: hidden;
246
- }
247
- .hp-chip:hover {
248
- border-color: var(--hp-border-hover);
249
- transform: translateY(-1px);
250
- }
251
- .hp-chip .emoji { font-size: 14px; }
252
-
253
- /* โ”€โ”€ Install button โ”€โ”€ */
254
- .hp-install-btn {
255
- background: var(--hp-gradient) !important;
256
- border: none !important;
257
- border-radius: 12px !important;
258
- color: white !important;
259
- font-size: 16px !important;
260
- font-weight: 700 !important;
261
- letter-spacing: -0.01em !important;
262
- padding: 14px 32px !important;
263
- cursor: pointer !important;
264
- box-shadow: var(--hp-glow) !important;
265
- transition: all 0.2s !important;
266
- width: 100% !important;
267
- }
268
- .hp-install-btn:hover {
269
- transform: translateY(-1px) !important;
270
- box-shadow: 0 0 32px rgba(59,130,246,0.25) !important;
271
- }
272
-
273
- /* โ”€โ”€ Log output โ”€โ”€ */
274
- .hp-log {
275
- background: var(--hp-bg) !important;
276
- border: 1px solid var(--hp-border) !important;
277
- border-radius: var(--hp-radius) !important;
278
- padding: 20px !important;
279
- font-family: 'SF Mono', 'Fira Code', monospace !important;
280
- font-size: 13px !important;
281
- line-height: 1.7 !important;
282
- color: var(--hp-text) !important;
283
- min-height: 60px !important;
284
- }
285
-
286
- /* โ”€โ”€ Footer โ”€โ”€ */
287
- .hp-footer {
288
- text-align: center;
289
- padding: 32px 16px;
290
- border-top: 1px solid var(--hp-border);
291
- margin-top: 32px;
292
- }
293
- .hp-footer p { color: var(--hp-muted); font-size: 12px; margin: 4px 0; }
294
- .hp-footer a { color: var(--hp-blue); text-decoration: none; }
295
- .hp-footer a:hover { color: var(--hp-cyan); }
296
- """
297
-
298
  # โ”€โ”€ Core functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
299
 
300
-
301
- def validate_token(token: str) -> tuple[str, str, str]:
302
  if not token or len(token) < 10:
303
- return "error", "", "Token vacรญo o invรกlido"
304
  try:
305
  import requests
306
- r = requests.get(
307
- "https://huggingface.co/api/whoami-v2",
308
- headers={"Authorization": f"Bearer {token}"},
309
- timeout=10,
310
- )
311
  if r.ok:
312
  name = r.json().get("name", "")
313
- return "ok", name, f"Autenticado como **{name}**"
314
- return "error", "", f"Token rechazado (HTTP {r.status_code})"
315
  except Exception as e:
316
- return "error", "", f"Error de conexiรณn: {e}"
317
 
318
 
319
  def install_space(token, username, space_name, private, model):
320
  if not username:
321
- yield "โš ๏ธ Primero verifica tu token en el Paso 1"
322
  return
323
 
324
  repo_id = f"{username}/{space_name}"
325
  lines = []
326
-
327
  def log(msg):
328
  lines.append(msg)
329
  return "\n".join(lines)
330
 
331
- yield log(f"โ–ธ Creando Space **{repo_id}**...")
332
 
333
  try:
334
  import requests
335
-
336
  r = requests.post(
337
  "https://huggingface.co/api/repos/create",
338
  headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
339
  json={"type": "space", "name": space_name, "private": private, "sdk": "docker"},
340
- timeout=30,
341
- )
342
  if r.ok:
343
- yield log("โœ“ Space creado")
344
  elif r.status_code == 409:
345
- yield log("โœ“ Space ya existe โ€” actualizando")
346
  else:
347
- yield log(f"โœ— Error: {r.status_code} {r.text[:200]}")
348
  return
349
 
350
- yield log("โ–ธ Clonando template HomePilot...")
351
-
352
- with tempfile.TemporaryDirectory() as tmpdir:
353
  remote = f"https://user:{token}@huggingface.co/spaces/{repo_id}"
354
- template_remote = f"https://user:{token}@huggingface.co/spaces/{TEMPLATE_REPO}"
355
-
356
- subprocess.run(
357
- ["git", "-c", "credential.helper=", "clone", "--depth", "1",
358
- template_remote, f"{tmpdir}/template"],
359
- capture_output=True, timeout=60,
360
- )
361
 
362
- clone = subprocess.run(
363
- ["git", "-c", "credential.helper=", "clone", "--depth", "1",
364
- remote, f"{tmpdir}/space"],
365
- capture_output=True, timeout=30,
366
- )
367
  if clone.returncode != 0:
368
- os.makedirs(f"{tmpdir}/space", exist_ok=True)
369
- subprocess.run(["git", "init", "-b", "main", f"{tmpdir}/space"], capture_output=True)
370
- subprocess.run(["git", "-C", f"{tmpdir}/space", "remote", "add", "origin", remote], capture_output=True)
371
 
372
- space_dir = f"{tmpdir}/space"
373
- for item in Path(space_dir).iterdir():
 
374
  if item.name != ".git":
375
  shutil.rmtree(item) if item.is_dir() else item.unlink()
376
 
377
- yield log("โ–ธ Copiando archivos...")
378
- for item in Path(f"{tmpdir}/template").iterdir():
379
- if item.name == ".git":
380
- continue
381
- dest = Path(space_dir) / item.name
382
  shutil.copytree(item, dest) if item.is_dir() else shutil.copy2(item, dest)
383
 
384
- start_sh = Path(space_dir) / "start.sh"
385
- if start_sh.exists():
386
- content = start_sh.read_text()
387
- content = content.replace("OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:1.5b}",
388
- f"OLLAMA_MODEL=${{OLLAMA_MODEL:-{model}}}")
389
- start_sh.write_text(content)
390
 
391
  yield log(f"โ–ธ Modelo: **{model}**")
392
- yield log("โ–ธ Subiendo a Hugging Face...")
393
 
394
- subprocess.run(["git", "lfs", "install", "--local"], capture_output=True, cwd=space_dir)
395
  subprocess.run(["git", "lfs", "track", "*.hpersona", "*.png", "*.webp"],
396
- capture_output=True, cwd=space_dir)
397
- subprocess.run(["git", "-C", space_dir, "-c", "user.email=installer@homepilot.dev",
398
- "-c", "user.name=HomePilot Installer", "add", "-A"],
399
- capture_output=True, timeout=30)
400
- subprocess.run(["git", "-C", space_dir, "-c", "user.email=installer@homepilot.dev",
401
- "-c", "user.name=HomePilot Installer", "commit", "-m",
402
- f"HomePilot installed ({model})"],
403
  capture_output=True, timeout=30)
404
- push = subprocess.run(
405
- ["git", "-C", space_dir, "push", "--force", remote, "HEAD:main"],
406
- capture_output=True, text=True, timeout=120,
407
- )
408
-
409
- if push.returncode == 0:
410
- yield log("โœ“ Subido exitosamente")
411
- else:
412
- yield log(f"โœ— Push error: {push.stderr[:300]}")
413
  return
414
 
415
- space_url = f"https://huggingface.co/spaces/{repo_id}"
416
- yield log(f"""
417
- โœ“ **Instalaciรณn completa**
418
 
419
- Tu HomePilot estรก en: [{repo_id}]({space_url})
420
 
421
  **Prรณximos pasos:**
422
- 1. Ve a **Settings โ†’ Hardware** y selecciona GPU (opcional)
423
- 2. Espera ~3 min para el primer arranque
424
- 3. El modelo se descarga automรกticamente
425
-
426
- **14 Chata personas** listas para usar.""")
427
-
428
  except Exception as e:
429
- yield log(f"โœ— Error: {e}")
430
-
431
 
432
- # โ”€โ”€ Build personas HTML โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
433
 
434
- def _persona_html():
435
- parts = ['<div class="hp-personas">']
436
- parts.append('<div class="hp-personas-title">Personas pre-instaladas</div>')
437
- parts.append('<div class="hp-personas-sub">14 AI personas de Chata โ€” se importan automรกticamente al iniciar</div>')
438
 
439
- parts.append('<div class="hp-pack-label">Starter Pack</div>')
440
- parts.append('<div class="hp-grid">')
441
- for emoji, name, _ in PERSONAS["starter"]:
442
- parts.append(f'<div class="hp-chip"><span class="emoji">{emoji}</span>{name}</div>')
443
- parts.append('</div>')
444
 
445
- parts.append('<div class="hp-pack-label">Retro Pack</div>')
446
- parts.append('<div class="hp-grid">')
447
- for emoji, name, _ in PERSONAS["retro"]:
448
- parts.append(f'<div class="hp-chip"><span class="emoji">{emoji}</span>{name}</div>')
449
- parts.append('</div>')
 
450
 
451
- parts.append('</div>')
452
- return "\n".join(parts)
 
 
 
 
 
 
 
 
453
 
 
 
 
 
454
 
455
- # โ”€โ”€ Gradio UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
456
 
457
  def build_ui():
458
- with gr.Blocks(css=CSS, title="HomePilot Installer", theme=gr.themes.Soft(
459
- primary_hue="blue", secondary_hue="purple", neutral_hue="zinc",
 
 
 
 
 
 
 
 
 
 
460
  )) as app:
461
 
462
- # โ”€โ”€ Hero โ”€โ”€
463
  gr.HTML("""
464
- <div class="hp-hero">
465
- <h1>๐Ÿ  <span class="gradient-text">HomePilot Installer</span></h1>
466
- <p class="subtitle">Instala tu propio HomePilot con IA privada en Hugging Face Spaces</p>
467
- <div class="badges">
468
- <span class="hp-badge"><span class="dot"></span>14 Chata Personas</span>
469
- <span class="hp-badge"><span class="dot"></span>Ollama Built-in</span>
470
- <span class="hp-badge"><span class="dot"></span>GPU Ready</span>
471
- <span class="hp-badge"><span class="dot"></span>Private Space</span>
 
 
 
 
 
 
 
 
 
472
  </div>
473
  </div>
474
  """)
475
 
476
- # โ”€โ”€ Step 1: Auth โ”€โ”€
477
- with gr.Group(elem_classes="hp-step"):
478
- gr.HTML("""
479
- <div class="hp-step-header">
480
- <div class="hp-step-num">1</div>
481
- <div>
482
- <div class="hp-step-title">Autenticaciรณn</div>
483
- <div class="hp-step-desc">Necesitas un <a href="https://huggingface.co/settings/tokens" target="_blank" style="color: var(--hp-blue)">token de HF</a> con permisos de write.</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  </div>
485
  </div>
486
- """)
 
 
487
  with gr.Row():
488
- token_input = gr.Textbox(label="HF Token", placeholder="hf_...", type="password", scale=3)
489
- verify_btn = gr.Button("Verificar", variant="primary", scale=1)
490
  auth_status = gr.Markdown("")
491
  username_state = gr.State("")
492
 
493
- # โ”€โ”€ Step 2: Config โ”€โ”€
494
- with gr.Group(elem_classes="hp-step"):
495
- gr.HTML("""
496
- <div class="hp-step-header">
497
- <div class="hp-step-num">2</div>
498
- <div>
499
- <div class="hp-step-title">Configuraciรณn</div>
500
- <div class="hp-step-desc">Personaliza tu instalaciรณn</div>
501
- </div>
 
 
 
502
  </div>
503
- """)
 
 
504
  with gr.Row():
505
- space_name = gr.Textbox(label="Nombre del Space", value="HomePilot", scale=2)
506
  private_toggle = gr.Checkbox(label="Privado", value=True, scale=1)
507
  model_choice = gr.Dropdown(
508
- label="Modelo LLM",
509
  choices=[
510
- ("Qwen 2.5 1.5B (rรกpido, ligero)", "qwen2.5:1.5b"),
511
- ("Qwen 2.5 3B (mejor calidad)", "qwen2.5:3b"),
512
- ("Llama 3 8B (poderoso, necesita GPU)", "llama3:8b"),
513
- ("Gemma 2B (equilibrado)", "gemma:2b"),
514
- ("Phi 3 Mini (compacto)", "phi3:mini"),
515
  ],
516
  value="qwen2.5:1.5b",
517
  )
518
 
519
- # โ”€โ”€ Persona preview โ”€โ”€
520
- gr.HTML(_persona_html())
521
-
522
- # โ”€โ”€ Step 3: Install โ”€โ”€
523
- with gr.Group(elem_classes="hp-step"):
524
  gr.HTML("""
525
- <div class="hp-step-header">
526
- <div class="hp-step-num">3</div>
527
- <div>
528
- <div class="hp-step-title">Instalaciรณn</div>
529
- <div class="hp-step-desc">Un clic para desplegar tu HomePilot privado</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  </div>
531
  </div>
532
  """)
533
- install_btn = gr.Button("Instalar HomePilot โ†’", variant="primary",
534
- size="lg", elem_classes="hp-install-btn")
535
- install_log = gr.Markdown("", elem_classes="hp-log")
536
 
537
- # โ”€โ”€ Footer โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  gr.HTML("""
539
- <div class="hp-footer">
540
- <p style="font-size: 13px; color: var(--hp-text); font-weight: 600;">HomePilot โ€” Enterprise AI Assistant</p>
541
- <p>
542
- <a href="https://github.com/ruslanmv/HomePilot">GitHub</a> ยท
543
- <a href="https://huggingface.co/spaces/ruslanmv/HomePilot">Template</a> ยท
544
- <a href="https://huggingface.co/spaces/ruslanmv/Chata">Chata</a> ยท
545
- <a href="https://ruslanmv.com/HomePilot/">Web</a>
546
  </p>
547
  </div>
548
  """)
549
 
550
  # โ”€โ”€ Events โ”€โ”€
551
- def on_verify(token):
552
- status, username, message = validate_token(token)
553
- icon = "โœ“" if status == "ok" else "โœ—"
554
- return f"{icon} {message}", username
555
-
556
- verify_btn.click(fn=on_verify, inputs=[token_input], outputs=[auth_status, username_state])
557
  install_btn.click(fn=install_space,
558
  inputs=[token_input, username_state, space_name, private_toggle, model_choice],
559
  outputs=[install_log])
560
 
561
  return app
562
 
563
-
564
  if __name__ == "__main__":
565
  app = build_ui()
566
  app.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", "7860")), share=False)
 
1
  """
2
+ HomePilot Installer โ€” Enterprise Edition v2
3
+
4
+ Conversion-optimized installer following enterprise UX best practices:
5
+ 1. Lead with outcome, not setup
6
+ 2. Progressive disclosure (show value before asking credentials)
7
+ 3. One clear CTA
8
+ 4. Trust signals
9
+ 5. Branding consistency with ruslanmv.com/HomePilot
10
  """
11
 
 
12
  import os
13
  import shutil
14
  import subprocess
15
  import tempfile
 
16
 
17
  import gradio as gr
18
 
19
  TEMPLATE_REPO = os.environ.get("TEMPLATE_REPO", "ruslanmv/HomePilot")
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # โ”€โ”€ Core functions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
22
 
23
+ def validate_token(token: str):
 
24
  if not token or len(token) < 10:
25
+ return "โŒ Ingresa un token vรกlido", ""
26
  try:
27
  import requests
28
+ r = requests.get("https://huggingface.co/api/whoami-v2",
29
+ headers={"Authorization": f"Bearer {token}"}, timeout=10)
 
 
 
30
  if r.ok:
31
  name = r.json().get("name", "")
32
+ return f"โœ… Conectado como **{name}**", name
33
+ return f"โŒ Token rechazado (HTTP {r.status_code})", ""
34
  except Exception as e:
35
+ return f"โŒ Error: {e}", ""
36
 
37
 
38
  def install_space(token, username, space_name, private, model):
39
  if not username:
40
+ yield "โš ๏ธ Conecta tu cuenta primero (Paso 1)"
41
  return
42
 
43
  repo_id = f"{username}/{space_name}"
44
  lines = []
 
45
  def log(msg):
46
  lines.append(msg)
47
  return "\n".join(lines)
48
 
49
+ yield log(f"โ–ธ Creando **{repo_id}**...")
50
 
51
  try:
52
  import requests
 
53
  r = requests.post(
54
  "https://huggingface.co/api/repos/create",
55
  headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
56
  json={"type": "space", "name": space_name, "private": private, "sdk": "docker"},
57
+ timeout=30)
 
58
  if r.ok:
59
+ yield log("โœ… Space creado")
60
  elif r.status_code == 409:
61
+ yield log("โœ… Space existente โ€” actualizando")
62
  else:
63
+ yield log(f"โŒ Error: {r.text[:200]}")
64
  return
65
 
66
+ yield log("โ–ธ Descargando HomePilot...")
67
+ with tempfile.TemporaryDirectory() as tmp:
 
68
  remote = f"https://user:{token}@huggingface.co/spaces/{repo_id}"
69
+ tpl_remote = f"https://user:{token}@huggingface.co/spaces/{TEMPLATE_REPO}"
 
 
 
 
 
 
70
 
71
+ subprocess.run(["git", "-c", "credential.helper=", "clone", "--depth", "1",
72
+ tpl_remote, f"{tmp}/tpl"], capture_output=True, timeout=60)
73
+ clone = subprocess.run(["git", "-c", "credential.helper=", "clone", "--depth", "1",
74
+ remote, f"{tmp}/sp"], capture_output=True, timeout=30)
 
75
  if clone.returncode != 0:
76
+ os.makedirs(f"{tmp}/sp", exist_ok=True)
77
+ subprocess.run(["git", "init", "-b", "main", f"{tmp}/sp"], capture_output=True)
78
+ subprocess.run(["git", "-C", f"{tmp}/sp", "remote", "add", "origin", remote], capture_output=True)
79
 
80
+ from pathlib import Path
81
+ sp = Path(f"{tmp}/sp")
82
+ for item in sp.iterdir():
83
  if item.name != ".git":
84
  shutil.rmtree(item) if item.is_dir() else item.unlink()
85
 
86
+ yield log("โ–ธ Configurando...")
87
+ for item in Path(f"{tmp}/tpl").iterdir():
88
+ if item.name == ".git": continue
89
+ dest = sp / item.name
 
90
  shutil.copytree(item, dest) if item.is_dir() else shutil.copy2(item, dest)
91
 
92
+ start = sp / "start.sh"
93
+ if start.exists():
94
+ start.write_text(start.read_text().replace(
95
+ "OLLAMA_MODEL=${OLLAMA_MODEL:-qwen2.5:1.5b}",
96
+ f"OLLAMA_MODEL=${{OLLAMA_MODEL:-{model}}}"))
 
97
 
98
  yield log(f"โ–ธ Modelo: **{model}**")
99
+ yield log("โ–ธ Subiendo...")
100
 
101
+ subprocess.run(["git", "lfs", "install", "--local"], capture_output=True, cwd=str(sp))
102
  subprocess.run(["git", "lfs", "track", "*.hpersona", "*.png", "*.webp"],
103
+ capture_output=True, cwd=str(sp))
104
+ subprocess.run(["git", "-C", str(sp), "-c", "user.email=i@hp.dev",
105
+ "-c", "user.name=HP", "add", "-A"], capture_output=True, timeout=30)
106
+ subprocess.run(["git", "-C", str(sp), "-c", "user.email=i@hp.dev",
107
+ "-c", "user.name=HP", "commit", "-m", f"HomePilot ({model})"],
 
 
108
  capture_output=True, timeout=30)
109
+ push = subprocess.run(["git", "-C", str(sp), "push", "--force", remote, "HEAD:main"],
110
+ capture_output=True, text=True, timeout=120)
111
+ if push.returncode != 0:
112
+ yield log(f"โŒ {push.stderr[:200]}")
 
 
 
 
 
113
  return
114
 
115
+ url = f"https://huggingface.co/spaces/{repo_id}"
116
+ yield log(f"""โœ… **ยกListo!**
 
117
 
118
+ ๐Ÿ”— Tu HomePilot: [{repo_id}]({url})
119
 
120
  **Prรณximos pasos:**
121
+ 1. Espera ~3 min para el build
122
+ 2. El modelo se descarga automรกticamente
123
+ 3. 14 personas AI listas para usar""")
 
 
 
124
  except Exception as e:
125
+ yield log(f"โŒ {e}")
 
126
 
 
127
 
128
+ # โ”€โ”€ UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
129
 
130
+ CSS = """
131
+ /* Force dark theme on everything */
132
+ .gradio-container, .gradio-container .main { background: #09090b !important; }
133
+ footer { display: none !important; }
 
134
 
135
+ /* Fix Gradio Group cards to dark */
136
+ .gradio-container .gr-group, .gradio-container .gr-box,
137
+ .gradio-container .group, .gradio-container [class*="group"] {
138
+ background: #111113 !important;
139
+ border-color: rgba(255,255,255,0.08) !important;
140
+ }
141
 
142
+ /* Fix all inputs to dark */
143
+ .gradio-container input, .gradio-container textarea, .gradio-container select,
144
+ .gradio-container .wrap, .gradio-container .input-wrap {
145
+ background: #09090b !important;
146
+ border-color: rgba(255,255,255,0.1) !important;
147
+ color: #e4e4e7 !important;
148
+ }
149
+ .gradio-container label, .gradio-container .label-wrap {
150
+ color: #a1a1aa !important;
151
+ }
152
 
153
+ /* Fix checkbox */
154
+ .gradio-container input[type="checkbox"] {
155
+ accent-color: #3b82f6 !important;
156
+ }
157
 
158
+ /* Dropdown */
159
+ .gradio-container .dropdown-arrow { color: #71717a !important; }
160
+ .gradio-container ul[role="listbox"] {
161
+ background: #161618 !important;
162
+ border-color: rgba(255,255,255,0.1) !important;
163
+ }
164
+ .gradio-container ul[role="listbox"] li {
165
+ color: #e4e4e7 !important;
166
+ }
167
+ .gradio-container ul[role="listbox"] li:hover {
168
+ background: rgba(59,130,246,0.15) !important;
169
+ }
170
+ """
171
 
172
  def build_ui():
173
+ with gr.Blocks(css=CSS, title="HomePilot Installer", theme=gr.themes.Base(
174
+ primary_hue="blue", neutral_hue="zinc",
175
+ ).set(
176
+ body_background_fill="#09090b",
177
+ body_text_color="#e4e4e7",
178
+ block_background_fill="#111113",
179
+ block_border_color="rgba(255,255,255,0.08)",
180
+ input_background_fill="#09090b",
181
+ input_border_color="rgba(255,255,255,0.1)",
182
+ button_primary_background_fill="linear-gradient(135deg, #06b6d4, #3b82f6, #8b5cf6)",
183
+ button_primary_text_color="white",
184
+ button_primary_border_color="transparent",
185
  )) as app:
186
 
187
+ # โ”€โ”€ HERO: Lead with outcome โ”€โ”€
188
  gr.HTML("""
189
+ <div style="text-align:center; padding:48px 20px 12px; position:relative;">
190
+ <div style="position:absolute;top:-30%;left:50%;transform:translateX(-50%);width:120%;height:100%;
191
+ background:radial-gradient(ellipse 60% 50% at 50% 0%, rgba(59,130,246,0.1), transparent);
192
+ pointer-events:none;"></div>
193
+ <div style="position:relative;">
194
+ <p style="font-size:48px; margin:0; line-height:1;">๐Ÿ </p>
195
+ <h1 style="font-size:clamp(1.8rem,4vw,2.6rem); font-weight:800; letter-spacing:-0.03em;
196
+ margin:12px 0 0; line-height:1.15;">
197
+ <span style="background:linear-gradient(135deg,#06b6d4,#3b82f6,#8b5cf6);
198
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;">
199
+ Tu IA privada en 2 minutos
200
+ </span>
201
+ </h1>
202
+ <p style="color:#71717a; font-size:15px; margin:10px auto 0; max-width:480px; line-height:1.5;">
203
+ Despliega HomePilot con Ollama y 14 personas AI en tu propio
204
+ Hugging Face Space. Sin cรณdigo. Privado por defecto.
205
+ </p>
206
  </div>
207
  </div>
208
  """)
209
 
210
+ # โ”€โ”€ TRUST SIGNALS โ”€โ”€
211
+ gr.HTML("""
212
+ <div style="display:flex; justify-content:center; gap:24px; padding:20px 0 32px; flex-wrap:wrap;">
213
+ <div style="display:flex; align-items:center; gap:6px; color:#a1a1aa; font-size:13px; font-weight:500;">
214
+ <span style="color:#22c55e;">๐Ÿ”’</span> Privado por defecto
215
+ </div>
216
+ <div style="display:flex; align-items:center; gap:6px; color:#a1a1aa; font-size:13px; font-weight:500;">
217
+ <span style="color:#3b82f6;">๐Ÿง </span> Ollama integrado
218
+ </div>
219
+ <div style="display:flex; align-items:center; gap:6px; color:#a1a1aa; font-size:13px; font-weight:500;">
220
+ <span style="color:#8b5cf6;">โšก</span> GPU ready
221
+ </div>
222
+ <div style="display:flex; align-items:center; gap:6px; color:#a1a1aa; font-size:13px; font-weight:500;">
223
+ <span style="color:#f59e0b;">๐ŸŽญ</span> 14 personas
224
+ </div>
225
+ </div>
226
+ """)
227
+
228
+ # โ”€โ”€ STEP 1: Connect โ”€โ”€
229
+ gr.HTML("""
230
+ <div style="display:flex; align-items:center; gap:10px; padding:0 4px 8px;">
231
+ <div style="width:28px;height:28px;border-radius:8px;
232
+ background:linear-gradient(135deg,#06b6d4,#3b82f6);
233
+ display:flex;align-items:center;justify-content:center;
234
+ font-size:13px;font-weight:800;color:white;flex-shrink:0;">1</div>
235
+ <div>
236
+ <div style="font-size:15px;font-weight:700;color:#e4e4e7;">Conecta tu cuenta</div>
237
+ <div style="font-size:12px;color:#71717a;">
238
+ Solo necesitamos un <a href="https://huggingface.co/settings/tokens" target="_blank"
239
+ style="color:#3b82f6;text-decoration:none;">token de HF</a> con permiso write.
240
+ No almacenamos credenciales.
241
  </div>
242
  </div>
243
+ </div>
244
+ """)
245
+ with gr.Group():
246
  with gr.Row():
247
+ token_input = gr.Textbox(label="Token", placeholder="hf_...", type="password", scale=3)
248
+ verify_btn = gr.Button("Conectar", variant="primary", scale=1)
249
  auth_status = gr.Markdown("")
250
  username_state = gr.State("")
251
 
252
+ gr.HTML('<div style="height:16px"></div>')
253
+
254
+ # โ”€โ”€ STEP 2: Configure โ”€โ”€
255
+ gr.HTML("""
256
+ <div style="display:flex; align-items:center; gap:10px; padding:0 4px 8px;">
257
+ <div style="width:28px;height:28px;border-radius:8px;
258
+ background:linear-gradient(135deg,#3b82f6,#8b5cf6);
259
+ display:flex;align-items:center;justify-content:center;
260
+ font-size:13px;font-weight:800;color:white;flex-shrink:0;">2</div>
261
+ <div>
262
+ <div style="font-size:15px;font-weight:700;color:#e4e4e7;">Configura</div>
263
+ <div style="font-size:12px;color:#71717a;">Todo tiene valores por defecto โ€” solo cambia si quieres.</div>
264
  </div>
265
+ </div>
266
+ """)
267
+ with gr.Group():
268
  with gr.Row():
269
+ space_name = gr.Textbox(label="Nombre", value="HomePilot", scale=2)
270
  private_toggle = gr.Checkbox(label="Privado", value=True, scale=1)
271
  model_choice = gr.Dropdown(
272
+ label="Modelo",
273
  choices=[
274
+ ("Qwen 2.5 1.5B โ€” rรกpido, ideal para empezar", "qwen2.5:1.5b"),
275
+ ("Qwen 2.5 3B โ€” mejor calidad", "qwen2.5:3b"),
276
+ ("Llama 3 8B โ€” poderoso (necesita GPU)", "llama3:8b"),
277
+ ("Gemma 2B โ€” equilibrado", "gemma:2b"),
 
278
  ],
279
  value="qwen2.5:1.5b",
280
  )
281
 
282
+ # โ”€โ”€ PERSONAS PREVIEW (collapsed) โ”€โ”€
283
+ with gr.Accordion("๐ŸŽญ 14 personas AI incluidas", open=False):
 
 
 
284
  gr.HTML("""
285
+ <div style="padding:8px 0;">
286
+ <p style="color:#71717a;font-size:12px;margin:0 0 12px;">
287
+ Se importan automรกticamente al iniciar. Listas para chatear.
288
+ </p>
289
+ <div style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:8px;">
290
+ <span style="font-size:10px;font-weight:700;color:#71717a;text-transform:uppercase;
291
+ letter-spacing:0.05em;width:100%;margin-bottom:4px;">Starter Pack</span>
292
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
293
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐ŸŒ™ LunaLite</span>
294
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
295
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐Ÿ˜Ž ChillBro</span>
296
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
297
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐Ÿ” Curiosa</span>
298
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
299
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">โšก HypeKid</span>
300
+ </div>
301
+ <div style="display:flex;gap:4px;flex-wrap:wrap;">
302
+ <span style="font-size:10px;font-weight:700;color:#71717a;text-transform:uppercase;
303
+ letter-spacing:0.05em;width:100%;margin-bottom:4px;">Retro Pack</span>
304
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
305
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐Ÿ”‹ Volt</span>
306
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
307
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">โš”๏ธ Ronin</span>
308
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
309
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐Ÿฆ– Kaiju</span>
310
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
311
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐Ÿ’พ Glitch</span>
312
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
313
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐Ÿ—บ๏ธ Quest</span>
314
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
315
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐Ÿง  Sigma</span>
316
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
317
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐Ÿƒ Loki</span>
318
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
319
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐ŸŒณ OldRoot</span>
320
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
321
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐Ÿ”ฎ Morphling</span>
322
+ <span style="padding:5px 10px;border-radius:8px;font-size:12px;font-weight:500;
323
+ color:#e4e4e7;background:#161618;border:1px solid rgba(255,255,255,0.06);">๐ŸŒŒ Nova</span>
324
  </div>
325
  </div>
326
  """)
 
 
 
327
 
328
+ gr.HTML('<div style="height:16px"></div>')
329
+
330
+ # โ”€โ”€ STEP 3: Deploy โ”€โ”€
331
+ gr.HTML("""
332
+ <div style="display:flex; align-items:center; gap:10px; padding:0 4px 8px;">
333
+ <div style="width:28px;height:28px;border-radius:8px;
334
+ background:linear-gradient(135deg,#8b5cf6,#ec4899);
335
+ display:flex;align-items:center;justify-content:center;
336
+ font-size:13px;font-weight:800;color:white;flex-shrink:0;">3</div>
337
+ <div>
338
+ <div style="font-size:15px;font-weight:700;color:#e4e4e7;">Despliega</div>
339
+ <div style="font-size:12px;color:#71717a;">Un clic. Tu HomePilot estarรก listo en ~3 minutos.</div>
340
+ </div>
341
+ </div>
342
+ """)
343
+ install_btn = gr.Button("๐Ÿš€ Desplegar HomePilot", variant="primary", size="lg")
344
+ install_log = gr.Markdown("")
345
+
346
+ # โ”€โ”€ FOOTER โ”€โ”€
347
  gr.HTML("""
348
+ <div style="text-align:center; padding:40px 16px 20px; border-top:1px solid rgba(255,255,255,0.06); margin-top:32px;">
349
+ <p style="color:#52525b; font-size:12px; margin:0;">
350
+ <a href="https://ruslanmv.com/HomePilot/" style="color:#3b82f6;text-decoration:none;">HomePilot</a> ยท
351
+ <a href="https://huggingface.co/spaces/ruslanmv/HomePilot" style="color:#3b82f6;text-decoration:none;">Template</a> ยท
352
+ <a href="https://huggingface.co/spaces/ruslanmv/Chata" style="color:#3b82f6;text-decoration:none;">Chata</a> ยท
353
+ <a href="https://github.com/ruslanmv/HomePilot" style="color:#3b82f6;text-decoration:none;">GitHub</a>
 
354
  </p>
355
  </div>
356
  """)
357
 
358
  # โ”€โ”€ Events โ”€โ”€
359
+ verify_btn.click(fn=validate_token, inputs=[token_input], outputs=[auth_status, username_state])
 
 
 
 
 
360
  install_btn.click(fn=install_space,
361
  inputs=[token_input, username_state, space_name, private_toggle, model_choice],
362
  outputs=[install_log])
363
 
364
  return app
365
 
 
366
  if __name__ == "__main__":
367
  app = build_ui()
368
  app.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", "7860")), share=False)